Files
speckle-server/packages/server/modules/acc/services/management.ts
T
2025-08-04 23:05:39 +01:00

249 lines
6.6 KiB
TypeScript

import {
getManifestByUrn,
getToken,
tryRegisterAccWebhook
} from '@/modules/acc/clients/autodesk'
import {
AccSyncItemStatuses,
ImporterAutomateFunctions
} from '@/modules/acc/domain/constants'
import { AccSyncItemEvents } from '@/modules/acc/domain/events'
import { isReadyForImport } from '@/modules/acc/helpers/svfUtils'
import type {
CountAccSyncItems,
DeleteAccSyncItemById,
GetAccSyncItemById,
ListAccSyncItems,
UpsertAccSyncItem
} from '@/modules/acc/domain/operations'
import type { AccSyncItem } from '@/modules/acc/domain/types'
import { SyncItemNotFoundError } from '@/modules/acc/errors/acc'
import type { TriggerSyncItemAutomation } from '@/modules/acc/services/automate'
import type {
CreateAutomation,
CreateAutomationRevision
} from '@/modules/automate/domain/operations'
import {
decodeIsoDateCursor,
encodeIsoDateCursor
} from '@/modules/shared/helpers/dbHelper'
import { getServerOrigin } from '@/modules/shared/helpers/envHelper'
import type { EventBusEmit } from '@/modules/shared/services/eventBus'
import cryptoRandomString from 'crypto-random-string'
import type { Exact } from 'type-fest'
export type CreateAccSyncItem = (params: {
syncItem: Pick<
AccSyncItem,
| 'projectId'
| 'modelId'
| 'accRegion'
| 'accHubId'
| 'accProjectId'
| 'accRootProjectFolderUrn'
| 'accFileLineageUrn'
| 'accFileName'
| 'accFileExtension'
| 'accFileVersionIndex'
| 'accFileVersionUrn'
| 'accFileViewName'
>
creatorUserId: string
}) => Promise<AccSyncItem>
export const createAccSyncItemFactory =
(deps: {
upsertAccSyncItem: UpsertAccSyncItem
createAutomation: CreateAutomation
createAutomationRevision: CreateAutomationRevision
triggerSyncItemAutomation: TriggerSyncItemAutomation
eventEmit: EventBusEmit
}): CreateAccSyncItem =>
async ({ syncItem, creatorUserId }) => {
const webhookId = await tryRegisterAccWebhook({
// For local development, you may set your public tailscale url as your local server's canonical origin
callbackUrl: `${getServerOrigin()}/api/v1/acc/webhook/callback`,
event: 'dm.version.added',
rootProjectFolderUrn: syncItem.accRootProjectFolderUrn,
region: syncItem.accRegion
})
const { automation } = await deps.createAutomation({
input: {
name: 'SVF2 Importer',
enabled: false
},
projectId: syncItem.projectId,
userId: creatorUserId
})
await deps.createAutomationRevision({
input: {
automationId: automation.id,
functions: [
{
functionId: ImporterAutomateFunctions.svf2.functionId,
functionReleaseId: ImporterAutomateFunctions.svf2.functionReleaseId
}
],
triggerDefinitions: {
version: 1,
definitions: [
{
// TODO ACC: FILE_UPLOADED
type: 'VERSION_CREATED',
modelId: syncItem.modelId
}
]
}
},
userId: creatorUserId,
skipInputValidation: true
})
const newSyncItem: AccSyncItem = {
...syncItem,
id: cryptoRandomString({ length: 10 }),
automationId: automation.id,
status: AccSyncItemStatuses.pending,
authorId: creatorUserId,
accWebhookId: webhookId ?? undefined,
createdAt: new Date(),
updatedAt: new Date()
}
await deps.upsertAccSyncItem(newSyncItem)
// TODO ACC: somehow i could not managed to get subsriptions work, doing stupid timeout refetch in FE after create/delete/update
// Once we have it properly TODO ogu: fix it on FE
await deps.eventEmit({
eventName: AccSyncItemEvents.Created,
payload: {
syncItem: newSyncItem,
projectId: newSyncItem.projectId
}
})
// Import new sync item immediately, if possible
const tokenData = await getToken()
const manifest = await getManifestByUrn(
{
urn: newSyncItem.accFileVersionUrn,
region: newSyncItem.accRegion
},
{ token: tokenData.access_token }
)
const isReady = isReadyForImport(manifest)
if (!isReady) return newSyncItem
return await deps.triggerSyncItemAutomation({ id: newSyncItem.id })
}
export type GetPaginatedAccSyncItems = (params: {
projectId: string
filter?: {
limit: number | null
cursor: string | null
}
}) => Promise<{
items: AccSyncItem[]
totalCount: number
cursor: string | null
}>
export const getPaginatedAccSyncItemsFactory =
(deps: {
listAccSyncItems: ListAccSyncItems
countAccSyncItems: CountAccSyncItems
}): GetPaginatedAccSyncItems =>
async ({ projectId, filter = {} }) => {
const cursor = filter.cursor ? decodeIsoDateCursor(filter.cursor) : null
const [items, totalCount] = await Promise.all([
deps.listAccSyncItems({
projectId,
filter: {
updatedBefore: cursor,
limit: filter?.limit ?? null
}
}),
deps.countAccSyncItems({ projectId })
])
const lastItem = items.at(-1)
return {
items,
totalCount,
cursor: lastItem ? encodeIsoDateCursor(lastItem.updatedAt) : null
}
}
export type UpdateAccSyncItem = <
Item extends Exact<Partial<AccSyncItem> & Pick<AccSyncItem, 'id'>, Item>
>(params: {
syncItem: Item
}) => Promise<AccSyncItem>
export const updateAccSyncItemFactory =
(deps: {
getAccSyncItemById: GetAccSyncItemById
upsertAccSyncItem: UpsertAccSyncItem
eventEmit: EventBusEmit
}): UpdateAccSyncItem =>
async ({ syncItem }) => {
const existingSyncItem = await deps.getAccSyncItemById({
id: syncItem.id
})
if (!existingSyncItem) {
throw new SyncItemNotFoundError()
}
const newSyncItem: AccSyncItem = {
...existingSyncItem,
...syncItem,
updatedAt: new Date()
}
await deps.upsertAccSyncItem(newSyncItem)
await deps.eventEmit({
eventName: AccSyncItemEvents.Updated,
payload: {
oldSyncItem: existingSyncItem,
newSyncItem,
projectId: newSyncItem.projectId
}
})
return newSyncItem
}
export type DeleteAccSyncItem = (params: {
id: string
projectId: string
}) => Promise<void>
export const deleteAccSyncItemFactory =
(deps: {
deleteAccSyncItemById: DeleteAccSyncItemById
eventEmit: EventBusEmit
}): DeleteAccSyncItem =>
async ({ id, projectId }) => {
const itemCount = await deps.deleteAccSyncItemById({ id })
if (itemCount === 0) {
throw new SyncItemNotFoundError()
}
await deps.eventEmit({
eventName: AccSyncItemEvents.Deleted,
payload: {
id,
projectId
}
})
}