Webhooks on!
This commit is contained in:
@@ -22,7 +22,7 @@ const runStatusClasses = (run: AccSyncItemStatus) => {
|
||||
case 'SYNCING':
|
||||
classParts.push('bg-info-lighter')
|
||||
break
|
||||
case 'INITIALIZING':
|
||||
case 'PENDING':
|
||||
classParts.push('bg-warning-lighter')
|
||||
break
|
||||
case 'PAUSED':
|
||||
@@ -31,7 +31,7 @@ const runStatusClasses = (run: AccSyncItemStatus) => {
|
||||
case 'FAILED':
|
||||
classParts.push('bg-danger-lighter')
|
||||
break
|
||||
case 'SYNC':
|
||||
case 'SUCCEEDED':
|
||||
classParts.push('bg-success-lighter')
|
||||
break
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ const props = defineProps<{
|
||||
isLoggedIn: boolean
|
||||
}>()
|
||||
|
||||
// const internalSyncs = computed(() => props.syncs)
|
||||
// TODO ACC: Need to think about data residency from "ACC > Speckle" and warn users accordingly
|
||||
|
||||
const step = ref(0)
|
||||
|
||||
@@ -424,20 +424,27 @@ const { mutate: createAccSyncItem } = useMutation(accSyncItemCreateMutation)
|
||||
|
||||
const addSync = async () => {
|
||||
try {
|
||||
// annoying but looks like ACC does not give the exact version number directly
|
||||
const fileVersion = Number(
|
||||
new URLSearchParams(
|
||||
selectedFolderContent.value?.latestVersionId?.split('?')[1]
|
||||
).get('version')
|
||||
)
|
||||
|
||||
await createAccSyncItem({
|
||||
input: {
|
||||
projectId: props.projectId,
|
||||
modelId: selectedModel.value?.id as string,
|
||||
accRegion: selectedHub.value?.attributes?.region as string,
|
||||
accFileExtension: selectedFolderContent.value?.fileExtension as string, // TODO
|
||||
accFileExtension: selectedFolderContent.value?.fileExtension as string,
|
||||
accHubId: selectedHubId.value!,
|
||||
accProjectId: selectedProjectId.value as string,
|
||||
accRootProjectFolderId: rootProjectFolderId.value!,
|
||||
accFileLineageId: selectedFolderContent.value?.id as string,
|
||||
accFileName: (selectedFolderContent.value?.attributes.displayName ||
|
||||
selectedFolderContent.value?.attributes.name) as string,
|
||||
accFileVersionIndex: 1, // TODO ACC
|
||||
accFileVersionUrn: selectedFolderContent.value?.id as string // TODO ACC
|
||||
accFileVersionIndex: fileVersion,
|
||||
accFileVersionUrn: selectedFolderContent.value?.latestVersionId as string
|
||||
}
|
||||
})
|
||||
// TODO: NEED TO GO AWAY WHEN WE HAVE PROPER SUBSCRIPTIONS
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
filteredSubscribe,
|
||||
ProjectSubscriptions
|
||||
} from '@/modules/shared/utils/subscriptions'
|
||||
import { createTestAutomation } from '@/test/speckle-helpers/automationHelper'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { GraphQLError } from 'graphql/error'
|
||||
import { Knex } from 'knex'
|
||||
@@ -115,7 +114,7 @@ const resolvers: Resolvers = {
|
||||
|
||||
await storeAutomationFactory({ db: projectDb })({
|
||||
id: automationId,
|
||||
name: "converter",
|
||||
name: 'converter',
|
||||
userId: ctx.userId!,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { createAccOidcFlow } from '@/modules/acc/oidcHelper'
|
||||
import { registerAccWebhook } from '@/modules/acc/webhook'
|
||||
import { tryRegisterAccWebhook } from '@/modules/acc/webhook'
|
||||
import { sessionMiddlewareFactory } from '@/modules/auth/middleware'
|
||||
import { SpeckleModule } from '@/modules/shared/helpers/typeHelper'
|
||||
import { moduleLogger } from '@/observability/logging'
|
||||
@@ -122,7 +122,7 @@ export default function accRestApi(app: Express) {
|
||||
throw new Error('whatever')
|
||||
}
|
||||
const { access_token } = req.session.accTokens
|
||||
await registerAccWebhook({
|
||||
await tryRegisterAccWebhook({
|
||||
accessToken: access_token,
|
||||
rootProjectId: accHubUrn,
|
||||
region: 'EMEA',
|
||||
@@ -141,8 +141,8 @@ export default function accRestApi(app: Express) {
|
||||
return res.status(400).send({ error: 'Missing lineageUrn' })
|
||||
}
|
||||
|
||||
const sourceFileVersionIndex = Number.parseInt(req.body?.payload?.version ?? '0')
|
||||
const sourceFileVersionUrn = req.body?.payload?.source
|
||||
const accFileVersionIndex = Number.parseInt(req.body?.payload?.version ?? '0')
|
||||
const accFileVersionUrn = req.body?.payload?.source
|
||||
|
||||
// TODO ACC: need to know when svf2 is generated, whether with timeout or a webhook that unknown for now
|
||||
|
||||
@@ -151,11 +151,11 @@ export default function accRestApi(app: Express) {
|
||||
|
||||
const affectedRows = await db('acc_sync_items')
|
||||
.where({ accFileLineageId: lineageUrn })
|
||||
.andWhere(AccSyncItems.col.accFileVersionIndex, '<', sourceFileVersionIndex)
|
||||
.andWhere(AccSyncItems.col.accFileVersionIndex, '<', accFileVersionIndex)
|
||||
.update({
|
||||
status: 'PENDING',
|
||||
sourceFileVersionIndex,
|
||||
sourceFileVersionUrn
|
||||
accFileVersionIndex,
|
||||
accFileVersionUrn
|
||||
})
|
||||
.returning('*')
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { AccSyncItems } from '@/modules/acc/dbSchema'
|
||||
import { AccSyncItemEvents } from '@/modules/acc/domain/events'
|
||||
import {
|
||||
DeleteAccSyncItem,
|
||||
QueryAllAccSyncItems,
|
||||
UpsertAccSyncItem
|
||||
} from '@/modules/acc/domain/operations'
|
||||
@@ -9,7 +8,7 @@ import { executeBatchedSelect } from '@/modules/shared/helpers/dbHelper'
|
||||
import { AccSyncItem } from '@/modules/acc/domain/types'
|
||||
import { EventBusEmit } from '@/modules/shared/services/eventBus'
|
||||
import { Knex } from 'knex'
|
||||
import { omit } from 'lodash'
|
||||
import { tryRegisterAccWebhook } from '@/modules/acc/webhook'
|
||||
|
||||
const tables = {
|
||||
accSyncItems: (db: Knex) => db<AccSyncItem>(AccSyncItems.name)
|
||||
@@ -19,18 +18,59 @@ export type CreateAccSyncItemAndNotify = (
|
||||
input: Omit<AccSyncItem, 'createdAt' | 'updatedAt'>
|
||||
) => Promise<AccSyncItem>
|
||||
|
||||
export const getAutodeskAccessToken = async (): Promise<string> => {
|
||||
try {
|
||||
const clientId = process.env.ACC_CLIENT_ID
|
||||
const clientSecret = process.env.ACC_CLIENT_SECRET
|
||||
|
||||
const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
|
||||
|
||||
const response = await fetch(
|
||||
'https://developer.api.autodesk.com/authentication/v2/token',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${basicAuth}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'client_credentials',
|
||||
scope: 'data:read account:read viewables:read'
|
||||
}).toString()
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errText = await response.text()
|
||||
throw new Error(`Failed to get access token: ${response.status} ${errText}`)
|
||||
}
|
||||
|
||||
const json = await response.json()
|
||||
if (!json.access_token) {
|
||||
throw new Error('access token is not found')
|
||||
}
|
||||
return json.access_token as string
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const createAccSyncItemAndNotifyFactory = (deps: {
|
||||
db: Knex
|
||||
eventEmit: EventBusEmit
|
||||
}): CreateAccSyncItemAndNotify => {
|
||||
return async (input) => {
|
||||
// TODO ACC: register webhook if it is not yet
|
||||
// const accWebhook = await registerAccWebhook({
|
||||
// accessToken: '', // TODO ACC: get the token from 2legged server-to-server auth
|
||||
// rootProjectId: input.accRootProjectFolderId,
|
||||
// region: input.accRegion,
|
||||
// event: 'dm.version.added' // NOTE ACC: you can register an event only once
|
||||
// })
|
||||
// TODO ACC: we might need to cache the retrieved token to prevent rate limiting etc.
|
||||
|
||||
const token = await getAutodeskAccessToken()
|
||||
await tryRegisterAccWebhook({
|
||||
accessToken: token, // TODO ACC: get the token from 2legged server-to-server auth
|
||||
rootProjectId: input.accRootProjectFolderId,
|
||||
region: input.accRegion,
|
||||
event: 'dm.version.added' // NOTE ACC: you can register an event only once
|
||||
})
|
||||
// TODO ACC: get webhook id and store it in item -> not sure it is make sense to have many webhooks per file as `/acc/webhook/callback/:filelineageUrn`
|
||||
|
||||
// TODO ACC: trigger automation and update status of sync item
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isDevEnv } from '@/modules/shared/helpers/envHelper'
|
||||
import { logger } from '@/observability/logging'
|
||||
|
||||
const tailscaleUrl = 'https://oguzhans-macbook-pro.mermaid-emperor.ts.net' // TODO ACC for dev: Get your local url from tailscale and then we will got rid of
|
||||
|
||||
@@ -6,9 +7,9 @@ const accWebhookCallbackUrl = `${
|
||||
isDevEnv() ? tailscaleUrl : process.env.FRONTEND_HOST
|
||||
}/acc/webhook/callback`
|
||||
|
||||
export async function registerAccWebhook({
|
||||
export async function tryRegisterAccWebhook({
|
||||
accessToken,
|
||||
rootProjectId: hubUrn,
|
||||
rootProjectId,
|
||||
region,
|
||||
event
|
||||
}: {
|
||||
@@ -17,6 +18,14 @@ export async function registerAccWebhook({
|
||||
region: string
|
||||
event: string
|
||||
}) {
|
||||
const body = {
|
||||
callbackUrl: accWebhookCallbackUrl,
|
||||
scope: {
|
||||
folder: rootProjectId
|
||||
}
|
||||
}
|
||||
logger.info(body)
|
||||
logger.info(accessToken)
|
||||
const response = await fetch(
|
||||
`https://developer.api.autodesk.com/webhooks/v1/systems/data/events/${event}/hooks`,
|
||||
{
|
||||
@@ -24,21 +33,26 @@ export async function registerAccWebhook({
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'x-ads-region': `${region}`
|
||||
'x-ads-region': region
|
||||
},
|
||||
body: JSON.stringify({
|
||||
callbackUrl: `${accWebhookCallbackUrl}/${event}`,
|
||||
scope: {
|
||||
folder: {
|
||||
hubUrn
|
||||
}
|
||||
}
|
||||
})
|
||||
body: JSON.stringify(body)
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Webhook registration failed: ${await response.text()}`)
|
||||
const errJson = await response.json().catch(() => null)
|
||||
|
||||
const isConflict =
|
||||
response.status === 409 &&
|
||||
errJson?.code === 'CONFLICT_ERROR' &&
|
||||
errJson?.detail?.includes('Failed to save duplicate webhooks scope')
|
||||
|
||||
if (isConflict) {
|
||||
logger.warn('Webhook already exists. Skipping registration.')
|
||||
return null // Swallow and return
|
||||
}
|
||||
|
||||
throw new Error(`Webhook registration failed: ${JSON.stringify(errJson, null, 2)}`)
|
||||
}
|
||||
|
||||
const res = await response.json()
|
||||
|
||||
Reference in New Issue
Block a user