Webhooks on!

This commit is contained in:
oguzhankoral
2025-07-22 17:45:22 +01:00
parent b27960851d
commit b2f6401ca7
6 changed files with 96 additions and 36 deletions
@@ -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(),
+7 -7
View File
@@ -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
+26 -12
View File
@@ -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()