From 22bb18cc102688ecfa0a0eb318924bd588e6db61 Mon Sep 17 00:00:00 2001 From: oguzhankoral Date: Fri, 11 Jul 2025 05:29:12 +0300 Subject: [PATCH] Initial implementation --- .../components/project/page/acc/FileItem.vue | 76 ++++ .../components/project/page/acc/Files.vue | 39 ++ .../components/project/page/acc/Hubs.vue | 59 +++ .../components/project/page/acc/Projects.vue | 62 +++ .../project/page/acc/SyncStatus.vue | 38 ++ .../components/project/page/acc/Syncs.vue | 400 ++++++++++++++++++ .../components/project/page/acc/Tab.vue | 168 ++++++++ packages/frontend-2/lib/acc/types.ts | 54 +++ .../lib/common/generated/gql/graphql.ts | 122 ++++++ .../frontend-2/lib/common/helpers/route.ts | 4 +- .../frontend-2/pages/projects/[id]/index.vue | 14 + .../pages/projects/[id]/index/acc.vue | 21 + .../assets/acc/typedefs/accSyncItems.graphql | 68 +++ packages/server/modules/acc/domain/events.ts | 35 ++ .../acc/graph/resolvers/accSyncItems.ts | 114 +++++ packages/server/modules/acc/helpers/types.ts | 46 ++ packages/server/modules/acc/index.ts | 138 ++++++ .../20250710232704_create_sync_items_table.ts | 36 ++ packages/server/modules/acc/oidcHelper.ts | 78 ++++ .../modules/acc/repositories/accSyncItems.ts | 42 ++ packages/server/modules/acc/webhook.ts | 44 ++ .../modules/core/graph/generated/graphql.ts | 124 ++++++ .../graph/generated/graphql.ts | 76 ++++ packages/server/modules/index.ts | 2 + .../modules/shared/services/eventBus.ts | 5 + .../server/test/graphql/generated/graphql.ts | 76 ++++ 26 files changed, 1940 insertions(+), 1 deletion(-) create mode 100644 packages/frontend-2/components/project/page/acc/FileItem.vue create mode 100644 packages/frontend-2/components/project/page/acc/Files.vue create mode 100644 packages/frontend-2/components/project/page/acc/Hubs.vue create mode 100644 packages/frontend-2/components/project/page/acc/Projects.vue create mode 100644 packages/frontend-2/components/project/page/acc/SyncStatus.vue create mode 100644 packages/frontend-2/components/project/page/acc/Syncs.vue create mode 100644 packages/frontend-2/components/project/page/acc/Tab.vue create mode 100644 packages/frontend-2/lib/acc/types.ts create mode 100644 packages/frontend-2/pages/projects/[id]/index/acc.vue create mode 100644 packages/server/assets/acc/typedefs/accSyncItems.graphql create mode 100644 packages/server/modules/acc/domain/events.ts create mode 100644 packages/server/modules/acc/graph/resolvers/accSyncItems.ts create mode 100644 packages/server/modules/acc/helpers/types.ts create mode 100644 packages/server/modules/acc/index.ts create mode 100644 packages/server/modules/acc/migrations/20250710232704_create_sync_items_table.ts create mode 100644 packages/server/modules/acc/oidcHelper.ts create mode 100644 packages/server/modules/acc/repositories/accSyncItems.ts create mode 100644 packages/server/modules/acc/webhook.ts diff --git a/packages/frontend-2/components/project/page/acc/FileItem.vue b/packages/frontend-2/components/project/page/acc/FileItem.vue new file mode 100644 index 000000000..d267d1d6a --- /dev/null +++ b/packages/frontend-2/components/project/page/acc/FileItem.vue @@ -0,0 +1,76 @@ + + + diff --git a/packages/frontend-2/components/project/page/acc/Files.vue b/packages/frontend-2/components/project/page/acc/Files.vue new file mode 100644 index 000000000..d8a724572 --- /dev/null +++ b/packages/frontend-2/components/project/page/acc/Files.vue @@ -0,0 +1,39 @@ + + + diff --git a/packages/frontend-2/components/project/page/acc/Hubs.vue b/packages/frontend-2/components/project/page/acc/Hubs.vue new file mode 100644 index 000000000..bc6f6df69 --- /dev/null +++ b/packages/frontend-2/components/project/page/acc/Hubs.vue @@ -0,0 +1,59 @@ + + + diff --git a/packages/frontend-2/components/project/page/acc/Projects.vue b/packages/frontend-2/components/project/page/acc/Projects.vue new file mode 100644 index 000000000..df1b7cd18 --- /dev/null +++ b/packages/frontend-2/components/project/page/acc/Projects.vue @@ -0,0 +1,62 @@ + + + diff --git a/packages/frontend-2/components/project/page/acc/SyncStatus.vue b/packages/frontend-2/components/project/page/acc/SyncStatus.vue new file mode 100644 index 000000000..6d0097c90 --- /dev/null +++ b/packages/frontend-2/components/project/page/acc/SyncStatus.vue @@ -0,0 +1,38 @@ + + + diff --git a/packages/frontend-2/components/project/page/acc/Syncs.vue b/packages/frontend-2/components/project/page/acc/Syncs.vue new file mode 100644 index 000000000..8709de141 --- /dev/null +++ b/packages/frontend-2/components/project/page/acc/Syncs.vue @@ -0,0 +1,400 @@ + + + diff --git a/packages/frontend-2/components/project/page/acc/Tab.vue b/packages/frontend-2/components/project/page/acc/Tab.vue new file mode 100644 index 000000000..9c6c8d87f --- /dev/null +++ b/packages/frontend-2/components/project/page/acc/Tab.vue @@ -0,0 +1,168 @@ + + + diff --git a/packages/frontend-2/lib/acc/types.ts b/packages/frontend-2/lib/acc/types.ts new file mode 100644 index 000000000..832f0e576 --- /dev/null +++ b/packages/frontend-2/lib/acc/types.ts @@ -0,0 +1,54 @@ +export type AccTokens = { + access_token: string + refresh_token: string + token_type: string + id_token: string + expires_in: number +} + +export type AccUserInfo = { + userId: string + userName: string + emailId: string + firstName: string + lastName: string +} + +export type AccHub = { + id: string + attributes: { name: string; extension: Record } +} + +export type AccProject = { + id: string + attributes: { name: string; lastModifiedTime: string } + relationships: Record +} + +export type AccItem = { + id: string + type?: string + latestVersionId?: string // we mutate on the way + storageUrn?: string // we mutate on the way + attributes: { + name: string + displayName: string + createTime?: string + extension?: Record + } +} + +export type AccSyncItem = { + id: string + accHub: AccHub + accHubId: string + createdBy: string + projectId: string + modelId: string + projectName: string + modelName: string + accItem: AccItem + status: AccSyncItemStatus +} + +export type AccSyncItemStatus = 'sync' | 'syncing' | 'paused' | 'failed' diff --git a/packages/frontend-2/lib/common/generated/gql/graphql.ts b/packages/frontend-2/lib/common/generated/gql/graphql.ts index 5e4b8ed21..a35f82758 100644 --- a/packages/frontend-2/lib/common/generated/gql/graphql.ts +++ b/packages/frontend-2/lib/common/generated/gql/graphql.ts @@ -934,6 +934,15 @@ export type CreateServerRegionInput = { name: Scalars['String']['input']; }; +export type CreateSyncItemInput = { + accFileLineageId: Scalars['String']['input']; + accHubId: Scalars['String']['input']; + accProjectId: Scalars['String']['input']; + accRootFolderUrn: Scalars['String']['input']; + modelId: Scalars['String']['input']; + projectId: Scalars['String']['input']; +}; + export type CreateUserEmailInput = { email: Scalars['String']['input']; }; @@ -965,6 +974,11 @@ export type DeleteModelInput = { projectId: Scalars['ID']['input']; }; +export type DeleteSyncItemInput = { + accFileLineageId: Scalars['ID']['input']; + projectId: Scalars['ID']['input']; +}; + export type DeleteUserEmailInput = { id: Scalars['ID']['input']; }; @@ -1607,6 +1621,7 @@ export type Mutation = { streamUpdatePermission?: Maybe; /** @deprecated Part of the old API surface and will be removed in the future. Use ProjectMutations.batchDelete instead. */ streamsDelete: Scalars['Boolean']['output']; + syncItemMutations: SyncItemMutations; /** * Used for broadcasting real time typing status in comment threads. Does not persist any info. * @deprecated Use broadcastViewerUserActivity @@ -2123,6 +2138,8 @@ export type Project = { role?: Maybe; /** Source apps used in any models of this project */ sourceApps: Array; + syncItem: SyncItem; + syncItems: SyncItemCollection; team: Array; updatedAt: Scalars['DateTime']['output']; /** Retrieve a specific project version by its ID */ @@ -2230,6 +2247,11 @@ export type ProjectPendingImportedModelsArgs = { }; +export type ProjectSyncItemArgs = { + id: Scalars['String']['input']; +}; + + export type ProjectVersionArgs = { id: Scalars['String']['input']; }; @@ -3645,6 +3667,7 @@ export type Subscription = { projectPendingModelsUpdated: ProjectPendingModelsUpdatedMessage; /** Subscribe to changes to a project's pending versions */ projectPendingVersionsUpdated: ProjectPendingVersionsUpdatedMessage; + projectSyncItemsUpdated: Scalars['String']['output']; /** Subscribe to updates to any triggered automations statuses in the project */ projectTriggeredAutomationsStatusUpdated: ProjectTriggeredAutomationsStatusUpdatedMessage; /** Track updates to a specific project */ @@ -3774,6 +3797,12 @@ export type SubscriptionProjectPendingVersionsUpdatedArgs = { }; +export type SubscriptionProjectSyncItemsUpdatedArgs = { + id: Scalars['String']['input']; + itemIds?: InputMaybe>; +}; + + export type SubscriptionProjectTriggeredAutomationsStatusUpdatedArgs = { projectId: Scalars['String']['input']; }; @@ -3839,6 +3868,59 @@ export type SubscriptionWorkspaceUpdatedArgs = { workspaceSlug?: InputMaybe; }; +export type SyncItem = { + __typename?: 'SyncItem'; + accFileLineageId: Scalars['String']['output']; + accHubId: Scalars['String']['output']; + accProjectId: Scalars['String']['output']; + accRootFolderUrn: Scalars['String']['output']; + accWebhookId: Scalars['String']['output']; + author?: Maybe; + createdAt: Scalars['DateTime']['output']; + id: Scalars['ID']['output']; + modelId: Scalars['String']['output']; + projectId: Scalars['String']['output']; + status: SyncItemStatus; + updatedAt: Scalars['DateTime']['output']; +}; + +export type SyncItemCollection = { + __typename?: 'SyncItemCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']['output']; +}; + +export type SyncItemMutations = { + __typename?: 'SyncItemMutations'; + create: SyncItem; + delete: Scalars['Boolean']['output']; + update: SyncItem; +}; + + +export type SyncItemMutationsCreateArgs = { + input: CreateSyncItemInput; +}; + + +export type SyncItemMutationsDeleteArgs = { + input: DeleteSyncItemInput; +}; + + +export type SyncItemMutationsUpdateArgs = { + input: UpdateSyncItemInput; +}; + +export const SyncItemStatus = { + Failed: 'FAILED', + Paused: 'PAUSED', + Sync: 'SYNC', + Syncing: 'SYNCING' +} as const; + +export type SyncItemStatus = typeof SyncItemStatus[keyof typeof SyncItemStatus]; export type TestAutomationRun = { __typename?: 'TestAutomationRun'; automationRunId: Scalars['String']['output']; @@ -3908,6 +3990,12 @@ export type UpdateServerRegionInput = { name?: InputMaybe; }; +export type UpdateSyncItemInput = { + accFileLineageId: Scalars['ID']['input']; + projectId: Scalars['ID']['input']; + status: SyncItemStatus; +}; + /** Only non-null values will be updated */ export type UpdateVersionInput = { message?: InputMaybe; @@ -7969,6 +8057,9 @@ export type AllObjectTypes = { StreamCollaborator: StreamCollaborator, StreamCollection: StreamCollection, Subscription: Subscription, + SyncItem: SyncItem, + SyncItemCollection: SyncItemCollection, + SyncItemMutations: SyncItemMutations, TestAutomationRun: TestAutomationRun, TestAutomationRunTrigger: TestAutomationRunTrigger, TestAutomationRunTriggerPayload: TestAutomationRunTriggerPayload, @@ -8561,6 +8652,7 @@ export type MutationFieldArgs = { streamUpdate: MutationStreamUpdateArgs, streamUpdatePermission: MutationStreamUpdatePermissionArgs, streamsDelete: MutationStreamsDeleteArgs, + syncItemMutations: {}, userCommentThreadActivityBroadcast: MutationUserCommentThreadActivityBroadcastArgs, userDelete: MutationUserDeleteArgs, userNotificationPreferencesUpdate: MutationUserNotificationPreferencesUpdateArgs, @@ -8662,6 +8754,8 @@ export type ProjectFieldArgs = { permissions: {}, role: {}, sourceApps: {}, + syncItem: ProjectSyncItemArgs, + syncItems: {}, team: {}, updatedAt: {}, version: ProjectVersionArgs, @@ -9045,6 +9139,7 @@ export type SubscriptionFieldArgs = { projectModelsUpdated: SubscriptionProjectModelsUpdatedArgs, projectPendingModelsUpdated: SubscriptionProjectPendingModelsUpdatedArgs, projectPendingVersionsUpdated: SubscriptionProjectPendingVersionsUpdatedArgs, + projectSyncItemsUpdated: SubscriptionProjectSyncItemsUpdatedArgs, projectTriggeredAutomationsStatusUpdated: SubscriptionProjectTriggeredAutomationsStatusUpdatedArgs, projectUpdated: SubscriptionProjectUpdatedArgs, projectVersionGendoAIRenderCreated: SubscriptionProjectVersionGendoAiRenderCreatedArgs, @@ -9061,6 +9156,30 @@ export type SubscriptionFieldArgs = { workspaceProjectsUpdated: SubscriptionWorkspaceProjectsUpdatedArgs, workspaceUpdated: SubscriptionWorkspaceUpdatedArgs, } +export type SyncItemFieldArgs = { + accFileLineageId: {}, + accHubId: {}, + accProjectId: {}, + accRootFolderUrn: {}, + accWebhookId: {}, + author: {}, + createdAt: {}, + id: {}, + modelId: {}, + projectId: {}, + status: {}, + updatedAt: {}, +} +export type SyncItemCollectionFieldArgs = { + cursor: {}, + items: {}, + totalCount: {}, +} +export type SyncItemMutationsFieldArgs = { + create: SyncItemMutationsCreateArgs, + delete: SyncItemMutationsDeleteArgs, + update: SyncItemMutationsUpdateArgs, +} export type TestAutomationRunFieldArgs = { automationRunId: {}, functionRunId: {}, @@ -9595,6 +9714,9 @@ export type AllObjectFieldArgTypes = { StreamCollaborator: StreamCollaboratorFieldArgs, StreamCollection: StreamCollectionFieldArgs, Subscription: SubscriptionFieldArgs, + SyncItem: SyncItemFieldArgs, + SyncItemCollection: SyncItemCollectionFieldArgs, + SyncItemMutations: SyncItemMutationsFieldArgs, TestAutomationRun: TestAutomationRunFieldArgs, TestAutomationRunTrigger: TestAutomationRunTriggerFieldArgs, TestAutomationRunTriggerPayload: TestAutomationRunTriggerPayloadFieldArgs, diff --git a/packages/frontend-2/lib/common/helpers/route.ts b/packages/frontend-2/lib/common/helpers/route.ts index 5d61bbe50..4dbac8298 100644 --- a/packages/frontend-2/lib/common/helpers/route.ts +++ b/packages/frontend-2/lib/common/helpers/route.ts @@ -14,6 +14,8 @@ export const forgottenPasswordRoute = '/authn/forgotten-password' export const verifyEmailRoute = '/verify-email' export const verifyEmailCountdownRoute = '/verify-email?source=registration' export const serverManagementRoute = '/server-management' +export const accLoginRoute = '/authn/acc' +export const accRoute = '/acc' export const connectorsRoute = '/connectors' export const tutorialsRoute = '/tutorials' export const docsPageUrl = 'https://docs.speckle.systems/' @@ -79,7 +81,7 @@ export const settingsWorkspaceRoutes = { export const projectRoute = ( id: string, - tab?: 'models' | 'discussions' | 'automations' | 'collaborators' | 'settings' + tab?: 'models' | 'discussions' | 'automations' | 'collaborators' | 'settings' | 'acc' ) => { let res = `/projects/${id}` if (tab && tab !== 'models') { diff --git a/packages/frontend-2/pages/projects/[id]/index.vue b/packages/frontend-2/pages/projects/[id]/index.vue index c0e919c8e..38a2a1642 100644 --- a/packages/frontend-2/pages/projects/[id]/index.vue +++ b/packages/frontend-2/pages/projects/[id]/index.vue @@ -255,6 +255,14 @@ const pageTabItems = computed((): LayoutPageTabItem[] => { }) } + if (isAccEnabled.value) { + //and the rest of checks + items.push({ + title: 'ACC', + id: 'acc' + }) + } + if (canReadSettings.value?.authorized) { items.push({ title: 'Collaborators', @@ -270,6 +278,8 @@ const pageTabItems = computed((): LayoutPageTabItem[] => { return items }) +const isAccEnabled = ref(true) // TODO + const findTabById = (id: string) => pageTabItems.value.find((tab) => tab.id === id) || pageTabItems.value[0] @@ -286,6 +296,7 @@ const activePageTab = computed({ const path = router.currentRoute.value.path if (/\/discussions\/?$/i.test(path)) return findTabById('discussions') if (/\/automations\/?.*$/i.test(path)) return findTabById('automations') + if (/\/acc\/?.*$/i.test(path)) return findTabById('acc') if (/\/collaborators\/?/i.test(path) && canReadSettings.value?.authorized) return findTabById('collaborators') if (/\/settings\/?/i.test(path) && canReadSettings.value?.authorized) @@ -301,6 +312,9 @@ const activePageTab = computed({ case 'discussions': router.push({ path: projectRoute(projectId.value, 'discussions') }) break + case 'acc': + router.push({ path: projectRoute(projectId.value, 'acc') }) + break case 'automations': router.push({ path: projectRoute(projectId.value, 'automations') }) break diff --git a/packages/frontend-2/pages/projects/[id]/index/acc.vue b/packages/frontend-2/pages/projects/[id]/index/acc.vue new file mode 100644 index 000000000..c3672e065 --- /dev/null +++ b/packages/frontend-2/pages/projects/[id]/index/acc.vue @@ -0,0 +1,21 @@ + + diff --git a/packages/server/assets/acc/typedefs/accSyncItems.graphql b/packages/server/assets/acc/typedefs/accSyncItems.graphql new file mode 100644 index 000000000..76ae4aa23 --- /dev/null +++ b/packages/server/assets/acc/typedefs/accSyncItems.graphql @@ -0,0 +1,68 @@ +extend type Project { + accSyncItems: AccSyncItemCollection! + accSyncItem(id: String!): AccSyncItem! +} + +type AccSyncItemCollection { + totalCount: Int! + cursor: String + items: [AccSyncItem!]! +} + +type AccSyncItem { + id: ID! + projectId: String! + modelId: String! + accHubId: String! + accProjectId: String! + accRootFolderUrn: String! + accFileLineageId: String! + accWebhookId: String + status: AccSyncItemStatus! + author: LimitedUser + createdAt: DateTime! + updatedAt: DateTime! +} + +enum AccSyncItemStatus { + SYNC + SYNCING + FAILED + PAUSED +} + +input DeleteAccSyncItemInput { + projectId: ID! + accFileLineageId: ID! +} + +input UpdateAccSyncItemInput { + projectId: ID! + accFileLineageId: ID! + status: AccSyncItemStatus! +} + +input CreateAccSyncItemInput { + projectId: String! + modelId: String! + accHubId: String! + accProjectId: String! + accRootFolderUrn: String! + accFileLineageId: String! +} + +type AccSyncItemMutations { + create(input: CreateAccSyncItemInput!): AccSyncItem + # delete(input: DeleteAccSyncItemInput!): Boolean! + # update(input: UpdateAccSyncItemInput!): AccSyncItem! +} + +extend type Mutation { + accSyncItemMutations: AccSyncItemMutations! + @hasServerRole(role: SERVER_GUEST) + @hasScope(scope: "streams:write") +} + +extend type Subscription { + projectAccSyncItemsUpdated(id: String!, itemIds: [String!]): String! +} diff --git a/packages/server/modules/acc/domain/events.ts b/packages/server/modules/acc/domain/events.ts new file mode 100644 index 000000000..77ee01ffd --- /dev/null +++ b/packages/server/modules/acc/domain/events.ts @@ -0,0 +1,35 @@ +import { AccSyncItem } from '@/modules/acc/helpers/types' +import { + DeleteAccSyncItemInput, + UpdateAccSyncItemInput +} from '@/modules/core/graph/generated/graphql' + +export const accSyncItemEventsNamespace = 'accSyncItems' as const + +export const AccSyncItemEvents = { + Created: `${accSyncItemEventsNamespace}:created`, + Updated: `${accSyncItemEventsNamespace}:updated`, + Deleted: `${accSyncItemEventsNamespace}:deleted` +} as const + +export type AccSyncItemEventsPayloads = { + [AccSyncItemEvents.Created]: { + syncItem: AccSyncItem + projectId: string + } + + [AccSyncItemEvents.Updated]: { + oldSyncItem: AccSyncItem + newSyncItem: AccSyncItem + projectId: string + userId?: string + input: UpdateAccSyncItemInput + } + + [AccSyncItemEvents.Deleted]: { + syncItem: AccSyncItem + projectId: string + userId?: string + input: DeleteAccSyncItemInput + } +} diff --git a/packages/server/modules/acc/graph/resolvers/accSyncItems.ts b/packages/server/modules/acc/graph/resolvers/accSyncItems.ts new file mode 100644 index 000000000..16ff7514b --- /dev/null +++ b/packages/server/modules/acc/graph/resolvers/accSyncItems.ts @@ -0,0 +1,114 @@ +import { AccSyncItem } from '@/modules/acc/helpers/types' +import { createAccSyncItemAndNotifyFactory } from '@/modules/acc/repositories/accSyncItems' +import { TokenResourceIdentifierType } from '@/modules/core/domain/tokens/types' +import { Resolvers } from '@/modules/core/graph/generated/graphql' +import { throwIfResourceAccessNotAllowed } from '@/modules/core/helpers/token' +import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector' +import { getEventBus } from '@/modules/shared/services/eventBus' +import cryptoRandomString from 'crypto-random-string' +import { GraphQLError } from 'graphql/error' +import { Knex } from 'knex' + +const ACC_SYNC_ITEMS = 'acc_sync_items' + +const tables = { + accSyncItems: (db: Knex) => db(ACC_SYNC_ITEMS) +} + +const resolvers: Resolvers = { + Project: { + async accSyncItems(parent, args, ctx) { + throwIfResourceAccessNotAllowed({ + resourceId: parent.id, + resourceAccessRules: ctx.resourceAccessRules, + resourceType: TokenResourceIdentifierType.Project + }) + + const projectDB = await getProjectDbClient({ projectId: parent.id }) + + const items = await tables + .accSyncItems(projectDB) + .where({ projectId: parent.id }) + .orderBy('createdAt', 'desc') + + return { + totalCount: items.length, + cursor: null, // TODO + items: items.map((item) => ({ + ...item, + author: null // TODO + })) + } + }, + async accSyncItem(parent, args, ctx) { + const { id } = args + throwIfResourceAccessNotAllowed({ + resourceId: parent.id, + resourceAccessRules: ctx.resourceAccessRules, + resourceType: TokenResourceIdentifierType.Project + }) + + // Get project-scoped DB + const projectDB = await getProjectDbClient({ projectId: parent.id }) + const item = await tables.accSyncItems(projectDB).where({ id }).first() + + if (!item) throw new Error(`SyncItem with id "${id}" not found`) // TODO: create acc kind error types later + + return { + ...item, + author: null // TODO + } + } + }, + Mutation: { + accSyncItemMutations: () => ({}) + }, + AccSyncItemMutations: { + async create(parent, args, ctx) { + const { input } = args + console.log('create', input) + throwIfResourceAccessNotAllowed({ + resourceId: input.projectId, + resourceAccessRules: ctx.resourceAccessRules, + resourceType: TokenResourceIdentifierType.Project + }) + + const projectDB = await getProjectDbClient({ projectId: input.projectId }) + + const existing = await tables + .accSyncItems(projectDB) + .where({ accFileLineageId: input.accFileLineageId }) + .first() + + if (existing) { + throw new GraphQLError( + `A SyncItem with accFileLineageId "${input.accFileLineageId}" already exists.`, + { + extensions: { code: 'DUPLICATE_ACC_FILE_LINEAGE_ID' } + } + ) + } + + const createSyncItem = createAccSyncItemAndNotifyFactory({ + db: await getProjectDbClient({ projectId: input.projectId }), + eventEmit: getEventBus().emit + }) + + const newItem = await createSyncItem({ + id: cryptoRandomString({ length: 10 }), + status: 'SYNCING', + ...input + }) + + return newItem + } + // async update(parent, args, ctx) { + // console.log('update', args) + // }, + // async delete(parent, args, ctx) { + // console.log('delete', args) + // } + } +} + +export default resolvers diff --git a/packages/server/modules/acc/helpers/types.ts b/packages/server/modules/acc/helpers/types.ts new file mode 100644 index 000000000..53a2be5df --- /dev/null +++ b/packages/server/modules/acc/helpers/types.ts @@ -0,0 +1,46 @@ +import { UserRecord } from '@/modules/core/helpers/types' +import type { Session, SessionData } from 'express-session' + +declare module 'express-session' { + interface SessionData extends AccSessionData {} +} + +declare module 'http' { + interface IncomingMessage extends AccSessionData { + /** + * Not sure why I have to do this, the session type is picked up correctly in some places, but not others + */ + session: Session & Partial + } +} + +type AccTokens = { + access_token: string + refresh_token: string + token_type: string + id_token: string + expires_in: number +} + +export type AccSessionData = { + accTokens?: AccTokens + codeVerifier?: string + projectId?: string +} + +export type AccSyncItem = { + id: string + projectId: string + modelId: string + accHubId: string + accProjectId: string + accRootFolderUrn: string + accFileLineageId: string + accWebhookId?: string + status: AccSyncItemStatus + author: UserRecord + createdAt: Date + updatedAt: Date +} + +export type AccSyncItemStatus = 'SYNC' | 'SYNCING' | 'PAUSED' | 'FAILED' diff --git a/packages/server/modules/acc/index.ts b/packages/server/modules/acc/index.ts new file mode 100644 index 000000000..7b913f8ff --- /dev/null +++ b/packages/server/modules/acc/index.ts @@ -0,0 +1,138 @@ +/* eslint-disable camelcase */ +import { createAccOidcFlow } from '@/modules/acc/oidcHelper' +import { registerAccWebhook } from '@/modules/acc/webhook' +import { sessionMiddlewareFactory } from '@/modules/auth/middleware' +import { SpeckleModule } from '@/modules/shared/helpers/typeHelper' +import { moduleLogger } from '@/observability/logging' +import { Express } from 'express' + +export default function accRestApi(app: Express) { + const sessionMiddleware = sessionMiddlewareFactory() + app.post('/auth/acc/login', sessionMiddleware, async (req, res) => { + const { projectId } = req.body + req.session.projectId = projectId + + const accFlow = createAccOidcFlow() + const { codeVerifier, codeChallenge } = accFlow.generateCodeVerifier() + req.session.codeVerifier = codeVerifier + + const authorizeUrl = accFlow.buildAuthorizeUrl({ + clientId: process.env.ACC_CLIENT_ID ?? '', + redirectUri: process.env.ACC_REDIRECT_URL ?? '', + codeChallenge, + scopes: [ + 'user-profile:read', + 'data:read', + 'data:create', + 'viewables:read', + 'openid' + ] + }) + + return res.json({ authorizeUrl }) + }) + + app.get('/auth/acc/callback', sessionMiddleware, async (req, res) => { + const { code } = req.query + const codeVerifier = req.session.codeVerifier + + if (!code || !codeVerifier) { + return res.status(400).send({ error: 'Missing code or verifier' }) + } + + const accFlow = createAccOidcFlow() + try { + const tokens = await accFlow.exchangeCodeForTokens({ + code: String(code), + codeVerifier, + clientId: process.env.ACC_CLIENT_ID ?? '', + clientSecret: process.env.ACC_CLIENT_SECRET ?? '', + redirectUri: process.env.ACC_REDIRECT_URL ?? '' + }) + + req.session.accTokens = tokens + + return res.redirect(`/projects/${req.session.projectId}/acc`) + } catch (error) { + console.error('Token exchange failed:', error) + return res.status(500).send({ error: 'Token exchange failed' }) + } + }) + + app.get('/auth/acc/status', sessionMiddleware, (req, res) => { + if (!req.session.accTokens) { + return res.status(404).send({ error: 'No ACC tokens found' }) + } + res.send(req.session.accTokens) + }) + + app.post('/auth/acc/refresh', sessionMiddleware, async (req, res) => { + const { refresh_token } = req.session.accTokens || {} + if (!refresh_token) { + return res.status(401).json({ error: 'No refresh token found' }) + } + + try { + const params = new URLSearchParams({ + grant_type: 'refresh_token', + client_id: process.env.ACC_CLIENT_ID ?? '', + client_secret: process.env.ACC_CLIENT_SECRET ?? '', + refresh_token + }) + + const response = await fetch( + 'https://developer.api.autodesk.com/authentication/v2/token', + { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: params + } + ) + + if (!response.ok) { + console.error(await response.text()) + return res.status(500).json({ error: 'Failed to refresh token' }) + } + + const newTokens = await response.json() + req.session.accTokens = newTokens + + res.json(newTokens) + } catch (error) { + console.error('Error refreshing token:', error) + res.status(500).json({ error: 'Error refreshing token' }) + } + }) + + app.post('/acc/sync-item-created', sessionMiddleware, async (req, res) => { + const { accHubUrn } = req.body + console.log(req.body) + console.log(accHubUrn) + + if (!req.session.accTokens) { + throw new Error('whatever') + } + const { access_token } = req.session.accTokens + await registerAccWebhook({ + accessToken: access_token, + hubUrn: accHubUrn, + region: 'EMEA', + event: '' + }) + res.status(200) + }) + + app.post('/acc/webhook/callback', sessionMiddleware, async (req, res) => { + console.log(req.body) + res.status(200) + }) +} + +export const init: SpeckleModule['init'] = async ({ app }) => { + moduleLogger.info('🔑 Init acc module') + + // Hoist rest + accRestApi(app) +} + +export const finalize: SpeckleModule['finalize'] = async () => {} diff --git a/packages/server/modules/acc/migrations/20250710232704_create_sync_items_table.ts b/packages/server/modules/acc/migrations/20250710232704_create_sync_items_table.ts new file mode 100644 index 000000000..c88a88286 --- /dev/null +++ b/packages/server/modules/acc/migrations/20250710232704_create_sync_items_table.ts @@ -0,0 +1,36 @@ +import { Knex } from 'knex' + +const TABLE_NAME = 'acc_sync_items' + +export async function up(knex: Knex): Promise { + await knex.schema.createTable(TABLE_NAME, (table) => { + table.string('id', 10).primary() + table.string('projectId').notNullable().references('id').inTable('streams') + table.string('modelId').notNullable() + table.string('accHubId').notNullable() + table.string('accProjectId').notNullable() + table.string('accRootFolderUrn').notNullable() + table.string('accFileLineageId').notNullable().unique() + table.string('accWebhookId').nullable() + table + .enu('status', ['SYNC', 'SYNCING', 'FAILED', 'PAUSED']) + .notNullable() + .defaultTo('SYNC') + + // Foreign key to users table if needed + table.string('authorId').nullable() + + table + .timestamp('createdAt', { precision: 3, useTz: true }) + .defaultTo(knex.fn.now()) + .notNullable() + table + .timestamp('updatedAt', { precision: 3, useTz: true }) + .defaultTo(knex.fn.now()) + .notNullable() + }) +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTable(TABLE_NAME) +} diff --git a/packages/server/modules/acc/oidcHelper.ts b/packages/server/modules/acc/oidcHelper.ts new file mode 100644 index 000000000..736ab37bf --- /dev/null +++ b/packages/server/modules/acc/oidcHelper.ts @@ -0,0 +1,78 @@ +/* eslint-disable camelcase */ +// modules/accIntegration/oidcHelper.ts +import axios from 'axios' +import crypto from 'crypto' + +interface BuildAuthorizeUrlOptions { + clientId: string + redirectUri: string + codeChallenge: string + scopes: string[] +} + +interface ExchangeCodeOptions { + code: string + codeVerifier: string + clientId: string + clientSecret: string + redirectUri: string +} + +export function createAccOidcFlow() { + return { + generateCodeVerifier() { + const codeVerifier = crypto.randomBytes(32).toString('base64url') + const codeChallenge = crypto + .createHash('sha256') + .update(codeVerifier) + .digest('base64url') + return { codeVerifier, codeChallenge } + }, + + buildAuthorizeUrl({ + clientId, + redirectUri, + codeChallenge, + scopes + }: BuildAuthorizeUrlOptions) { + const params = new URLSearchParams({ + response_type: 'code', + client_id: clientId, + redirect_uri: redirectUri, + scope: scopes.join(' '), + code_challenge: codeChallenge, + code_challenge_method: 'S256' + }) + + return `https://developer.api.autodesk.com/authentication/v2/authorize?${params.toString()}` + }, + + async exchangeCodeForTokens({ + code, + codeVerifier, + clientId, + clientSecret, + redirectUri + }: ExchangeCodeOptions) { + const params = new URLSearchParams({ + grant_type: 'authorization_code', + client_id: clientId, + client_secret: clientSecret, + redirect_uri: redirectUri, + code, + code_verifier: codeVerifier + }) + + const response = await axios.post( + 'https://developer.api.autodesk.com/authentication/v2/token', + params.toString(), + { + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } + } + ) + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return response.data // includes access_token, refresh_token, expires_in, token_type, etc. + } + } +} diff --git a/packages/server/modules/acc/repositories/accSyncItems.ts b/packages/server/modules/acc/repositories/accSyncItems.ts new file mode 100644 index 000000000..cee276928 --- /dev/null +++ b/packages/server/modules/acc/repositories/accSyncItems.ts @@ -0,0 +1,42 @@ +import { AccSyncItemEvents } from '@/modules/acc/domain/events' +import { AccSyncItem } from '@/modules/acc/helpers/types' +import { EventBusEmit } from '@/modules/shared/services/eventBus' +import { Knex } from 'knex' + +const ACC_SYNC_ITEMS = 'acc_sync_items' + +const tables = { + accSyncItems: (db: Knex) => db(ACC_SYNC_ITEMS) +} + +export type CreateAccSyncItemAndNotify = ( + input: Omit +) => Promise + +export const createAccSyncItemAndNotifyFactory = (deps: { + db: Knex + eventEmit: EventBusEmit +}): CreateAccSyncItemAndNotify => { + return async (input) => { + const now = new Date() + + const [item] = await tables + .accSyncItems(deps.db) + .insert({ + ...input, + createdAt: now, + updatedAt: now + }) + .returning('*') + + await deps.eventEmit({ + eventName: AccSyncItemEvents.Created, + payload: { + syncItem: item, + projectId: item.projectId + } + }) + + return item + } +} diff --git a/packages/server/modules/acc/webhook.ts b/packages/server/modules/acc/webhook.ts new file mode 100644 index 000000000..1880362bb --- /dev/null +++ b/packages/server/modules/acc/webhook.ts @@ -0,0 +1,44 @@ +const accWebhookCallbackUrl = + 'https://oguzhans-macbook-pro.mermaid-emperor.ts.net//acc/webhook/callback' + +export async function registerAccWebhook({ + accessToken, + hubUrn, + region, + event = 'dm.lineage.updated' +}: { + accessToken: string + hubUrn: string + region: string + event: string +}) { + const response = await fetch( + `https://developer.api.autodesk.com/webhooks/v1/systems/data/events/${event}/hooks`, + { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'x-ads-region': `${region}` + }, + body: JSON.stringify({ + callbackUrl: accWebhookCallbackUrl, + scope: { + folder: { + hubUrn + } + } + }) + } + ) + + if (!response.ok) { + throw new Error(`Webhook registration failed: ${await response.text()}`) + } + + const res = await response.json() + console.log(res) + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return res +} diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index 6183d7766..5f1f5c402 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -40,6 +40,47 @@ export type Scalars = { JSONObject: { input: Record; output: Record; } }; +export type AccSyncItem = { + __typename?: 'AccSyncItem'; + accFileLineageId: Scalars['String']['output']; + accHubId: Scalars['String']['output']; + accProjectId: Scalars['String']['output']; + accRootFolderUrn: Scalars['String']['output']; + accWebhookId?: Maybe; + author?: Maybe; + createdAt: Scalars['DateTime']['output']; + id: Scalars['ID']['output']; + modelId: Scalars['String']['output']; + projectId: Scalars['String']['output']; + status: AccSyncItemStatus; + updatedAt: Scalars['DateTime']['output']; +}; + +export type AccSyncItemCollection = { + __typename?: 'AccSyncItemCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']['output']; +}; + +export type AccSyncItemMutations = { + __typename?: 'AccSyncItemMutations'; + create?: Maybe; +}; + + +export type AccSyncItemMutationsCreateArgs = { + input: CreateAccSyncItemInput; +}; + +export const AccSyncItemStatus = { + Failed: 'FAILED', + Paused: 'PAUSED', + Sync: 'SYNC', + Syncing: 'SYNCING' +} as const; + +export type AccSyncItemStatus = typeof AccSyncItemStatus[keyof typeof AccSyncItemStatus]; export type ActiveUserMutations = { __typename?: 'ActiveUserMutations'; emailMutations: UserEmailMutations; @@ -908,6 +949,15 @@ export type CountOnlyCollection = { totalCount: Scalars['Int']['output']; }; +export type CreateAccSyncItemInput = { + accFileLineageId: Scalars['String']['input']; + accHubId: Scalars['String']['input']; + accProjectId: Scalars['String']['input']; + accRootFolderUrn: Scalars['String']['input']; + modelId: Scalars['String']['input']; + projectId: Scalars['String']['input']; +}; + export type CreateAutomateFunctionInput = { description: Scalars['String']['input']; /** Base64 encoded image data string */ @@ -983,6 +1033,11 @@ export type CurrencyBasedPrices = { usd: WorkspacePaidPlanPrices; }; +export type DeleteAccSyncItemInput = { + accFileLineageId: Scalars['ID']['input']; + projectId: Scalars['ID']['input']; +}; + export type DeleteModelInput = { id: Scalars['ID']['input']; projectId: Scalars['ID']['input']; @@ -1468,6 +1523,7 @@ export type Mutation = { __typename?: 'Mutation'; /** The void stares back. */ _?: Maybe; + accSyncItemMutations: AccSyncItemMutations; /** Various Active User oriented mutations */ activeUserMutations: ActiveUserMutations; admin: AdminMutations; @@ -2100,6 +2156,8 @@ export type Price = { export type Project = { __typename?: 'Project'; + accSyncItem: AccSyncItem; + accSyncItems: AccSyncItemCollection; allowPublicComments: Scalars['Boolean']['output']; /** Get a single automation by id. Error will be thrown if automation is not found or inaccessible. */ automation: Automation; @@ -2161,6 +2219,11 @@ export type Project = { }; +export type ProjectAccSyncItemArgs = { + id: Scalars['String']['input']; +}; + + export type ProjectAutomationArgs = { id: Scalars['String']['input']; }; @@ -3650,6 +3713,7 @@ export type Subscription = { * Note: Only works in test environment */ ping: Scalars['String']['output']; + projectAccSyncItemsUpdated: Scalars['String']['output']; /** Subscribe to updates to automations in the project */ projectAutomationsUpdated: ProjectAutomationsUpdatedMessage; /** @@ -3766,6 +3830,12 @@ export type SubscriptionCommitUpdatedArgs = { }; +export type SubscriptionProjectAccSyncItemsUpdatedArgs = { + id: Scalars['String']['input']; + itemIds?: InputMaybe>; +}; + + export type SubscriptionProjectAutomationsUpdatedArgs = { projectId: Scalars['String']['input']; }; @@ -3906,6 +3976,12 @@ export type TriggeredAutomationsStatus = { statusMessage?: Maybe; }; +export type UpdateAccSyncItemInput = { + accFileLineageId: Scalars['ID']['input']; + projectId: Scalars['ID']['input']; + status: AccSyncItemStatus; +}; + /** Any null values will be ignored */ export type UpdateAutomateFunctionInput = { description?: InputMaybe; @@ -5338,6 +5414,10 @@ export type DirectiveResolverFn & { author?: Maybe }>; + AccSyncItemCollection: ResolverTypeWrapper & { items: Array }>; + AccSyncItemMutations: ResolverTypeWrapper & { create?: Maybe }>; + AccSyncItemStatus: AccSyncItemStatus; ActiveUserMutations: ResolverTypeWrapper; Activity: ResolverTypeWrapper; ActivityCollection: ResolverTypeWrapper; @@ -5422,6 +5502,7 @@ export type ResolversTypes = { CommitsDeleteInput: CommitsDeleteInput; CommitsMoveInput: CommitsMoveInput; CountOnlyCollection: ResolverTypeWrapper; + CreateAccSyncItemInput: CreateAccSyncItemInput; CreateAutomateFunctionInput: CreateAutomateFunctionInput; CreateAutomateFunctionWithoutVersionInput: CreateAutomateFunctionWithoutVersionInput; CreateCommentInput: CreateCommentInput; @@ -5433,6 +5514,7 @@ export type ResolversTypes = { Currency: Currency; CurrencyBasedPrices: ResolverTypeWrapper & { gbp: ResolversTypes['WorkspacePaidPlanPrices'], usd: ResolversTypes['WorkspacePaidPlanPrices'] }>; DateTime: ResolverTypeWrapper; + DeleteAccSyncItemInput: DeleteAccSyncItemInput; DeleteModelInput: DeleteModelInput; DeleteUserEmailInput: DeleteUserEmailInput; DeleteVersionsInput: DeleteVersionsInput; @@ -5581,6 +5663,7 @@ export type ResolversTypes = { TokenResourceIdentifierInput: TokenResourceIdentifierInput; TokenResourceIdentifierType: TokenResourceIdentifierType; TriggeredAutomationsStatus: ResolverTypeWrapper; + UpdateAccSyncItemInput: UpdateAccSyncItemInput; UpdateAutomateFunctionInput: UpdateAutomateFunctionInput; UpdateModelInput: UpdateModelInput; UpdateServerRegionInput: UpdateServerRegionInput; @@ -5686,6 +5769,9 @@ export type ResolversTypes = { /** Mapping between all available schema types and the resolvers parents */ export type ResolversParentTypes = { + AccSyncItem: Omit & { author?: Maybe }; + AccSyncItemCollection: Omit & { items: Array }; + AccSyncItemMutations: Omit & { create?: Maybe }; ActiveUserMutations: MutationsObjectGraphQLReturn; Activity: Activity; ActivityCollection: ActivityCollectionGraphQLReturn; @@ -5766,6 +5852,7 @@ export type ResolversParentTypes = { CommitsDeleteInput: CommitsDeleteInput; CommitsMoveInput: CommitsMoveInput; CountOnlyCollection: CountOnlyCollection; + CreateAccSyncItemInput: CreateAccSyncItemInput; CreateAutomateFunctionInput: CreateAutomateFunctionInput; CreateAutomateFunctionWithoutVersionInput: CreateAutomateFunctionWithoutVersionInput; CreateCommentInput: CreateCommentInput; @@ -5776,6 +5863,7 @@ export type ResolversParentTypes = { CreateVersionInput: CreateVersionInput; CurrencyBasedPrices: Omit & { gbp: ResolversParentTypes['WorkspacePaidPlanPrices'], usd: ResolversParentTypes['WorkspacePaidPlanPrices'] }; DateTime: Scalars['DateTime']['output']; + DeleteAccSyncItemInput: DeleteAccSyncItemInput; DeleteModelInput: DeleteModelInput; DeleteUserEmailInput: DeleteUserEmailInput; DeleteVersionsInput: DeleteVersionsInput; @@ -5906,6 +5994,7 @@ export type ResolversParentTypes = { TokenResourceIdentifier: TokenResourceIdentifier; TokenResourceIdentifierInput: TokenResourceIdentifierInput; TriggeredAutomationsStatus: TriggeredAutomationsStatusGraphQLReturn; + UpdateAccSyncItemInput: UpdateAccSyncItemInput; UpdateAutomateFunctionInput: UpdateAutomateFunctionInput; UpdateModelInput: UpdateModelInput; UpdateServerRegionInput: UpdateServerRegionInput; @@ -6033,6 +6122,34 @@ export type IsOwnerDirectiveArgs = { }; export type IsOwnerDirectiveResolver = DirectiveResolverFn; +export type AccSyncItemResolvers = { + accFileLineageId?: Resolver; + accHubId?: Resolver; + accProjectId?: Resolver; + accRootFolderUrn?: Resolver; + accWebhookId?: Resolver, ParentType, ContextType>; + author?: Resolver, ParentType, ContextType>; + createdAt?: Resolver; + id?: Resolver; + modelId?: Resolver; + projectId?: Resolver; + status?: Resolver; + updatedAt?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type AccSyncItemCollectionResolvers = { + cursor?: Resolver, ParentType, ContextType>; + items?: Resolver, ParentType, ContextType>; + totalCount?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type AccSyncItemMutationsResolvers = { + create?: Resolver, ParentType, ContextType, RequireFields>; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type ActiveUserMutationsResolvers = { emailMutations?: Resolver; finishOnboarding?: Resolver>; @@ -6660,6 +6777,7 @@ export type ModelsTreeItemCollectionResolvers = { _?: Resolver, ParentType, ContextType>; + accSyncItemMutations?: Resolver; activeUserMutations?: Resolver; admin?: Resolver; adminDeleteUser?: Resolver>; @@ -6805,6 +6923,8 @@ export type PriceResolvers = { + accSyncItem?: Resolver>; + accSyncItems?: Resolver; allowPublicComments?: Resolver; automation?: Resolver>; automations?: Resolver>; @@ -7304,6 +7424,7 @@ export type SubscriptionResolvers, "commitDeleted", ParentType, ContextType, RequireFields>; commitUpdated?: SubscriptionResolver, "commitUpdated", ParentType, ContextType, RequireFields>; ping?: SubscriptionResolver; + projectAccSyncItemsUpdated?: SubscriptionResolver>; projectAutomationsUpdated?: SubscriptionResolver>; projectCommentsUpdated?: SubscriptionResolver>; projectFileImportUpdated?: SubscriptionResolver>; @@ -7858,6 +7979,9 @@ export type WorkspaceUpdatedMessageResolvers = { + AccSyncItem?: AccSyncItemResolvers; + AccSyncItemCollection?: AccSyncItemCollectionResolvers; + AccSyncItemMutations?: AccSyncItemMutationsResolvers; ActiveUserMutations?: ActiveUserMutationsResolvers; Activity?: ActivityResolvers; ActivityCollection?: ActivityCollectionResolvers; diff --git a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts index 629af2260..cd8bd5467 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -20,6 +20,47 @@ export type Scalars = { JSONObject: { input: Record; output: Record; } }; +export type AccSyncItem = { + __typename?: 'AccSyncItem'; + accFileLineageId: Scalars['String']['output']; + accHubId: Scalars['String']['output']; + accProjectId: Scalars['String']['output']; + accRootFolderUrn: Scalars['String']['output']; + accWebhookId?: Maybe; + author?: Maybe; + createdAt: Scalars['DateTime']['output']; + id: Scalars['ID']['output']; + modelId: Scalars['String']['output']; + projectId: Scalars['String']['output']; + status: AccSyncItemStatus; + updatedAt: Scalars['DateTime']['output']; +}; + +export type AccSyncItemCollection = { + __typename?: 'AccSyncItemCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']['output']; +}; + +export type AccSyncItemMutations = { + __typename?: 'AccSyncItemMutations'; + create?: Maybe; +}; + + +export type AccSyncItemMutationsCreateArgs = { + input: CreateAccSyncItemInput; +}; + +export const AccSyncItemStatus = { + Failed: 'FAILED', + Paused: 'PAUSED', + Sync: 'SYNC', + Syncing: 'SYNCING' +} as const; + +export type AccSyncItemStatus = typeof AccSyncItemStatus[keyof typeof AccSyncItemStatus]; export type ActiveUserMutations = { __typename?: 'ActiveUserMutations'; emailMutations: UserEmailMutations; @@ -888,6 +929,15 @@ export type CountOnlyCollection = { totalCount: Scalars['Int']['output']; }; +export type CreateAccSyncItemInput = { + accFileLineageId: Scalars['String']['input']; + accHubId: Scalars['String']['input']; + accProjectId: Scalars['String']['input']; + accRootFolderUrn: Scalars['String']['input']; + modelId: Scalars['String']['input']; + projectId: Scalars['String']['input']; +}; + export type CreateAutomateFunctionInput = { description: Scalars['String']['input']; /** Base64 encoded image data string */ @@ -963,6 +1013,11 @@ export type CurrencyBasedPrices = { usd: WorkspacePaidPlanPrices; }; +export type DeleteAccSyncItemInput = { + accFileLineageId: Scalars['ID']['input']; + projectId: Scalars['ID']['input']; +}; + export type DeleteModelInput = { id: Scalars['ID']['input']; projectId: Scalars['ID']['input']; @@ -1448,6 +1503,7 @@ export type Mutation = { __typename?: 'Mutation'; /** The void stares back. */ _?: Maybe; + accSyncItemMutations: AccSyncItemMutations; /** Various Active User oriented mutations */ activeUserMutations: ActiveUserMutations; admin: AdminMutations; @@ -2080,6 +2136,8 @@ export type Price = { export type Project = { __typename?: 'Project'; + accSyncItem: AccSyncItem; + accSyncItems: AccSyncItemCollection; allowPublicComments: Scalars['Boolean']['output']; /** Get a single automation by id. Error will be thrown if automation is not found or inaccessible. */ automation: Automation; @@ -2141,6 +2199,11 @@ export type Project = { }; +export type ProjectAccSyncItemArgs = { + id: Scalars['String']['input']; +}; + + export type ProjectAutomationArgs = { id: Scalars['String']['input']; }; @@ -3630,6 +3693,7 @@ export type Subscription = { * Note: Only works in test environment */ ping: Scalars['String']['output']; + projectAccSyncItemsUpdated: Scalars['String']['output']; /** Subscribe to updates to automations in the project */ projectAutomationsUpdated: ProjectAutomationsUpdatedMessage; /** @@ -3746,6 +3810,12 @@ export type SubscriptionCommitUpdatedArgs = { }; +export type SubscriptionProjectAccSyncItemsUpdatedArgs = { + id: Scalars['String']['input']; + itemIds?: InputMaybe>; +}; + + export type SubscriptionProjectAutomationsUpdatedArgs = { projectId: Scalars['String']['input']; }; @@ -3886,6 +3956,12 @@ export type TriggeredAutomationsStatus = { statusMessage?: Maybe; }; +export type UpdateAccSyncItemInput = { + accFileLineageId: Scalars['ID']['input']; + projectId: Scalars['ID']['input']; + status: AccSyncItemStatus; +}; + /** Any null values will be ignored */ export type UpdateAutomateFunctionInput = { description?: InputMaybe; diff --git a/packages/server/modules/index.ts b/packages/server/modules/index.ts index 68c94c908..54928845d 100644 --- a/packages/server/modules/index.ts +++ b/packages/server/modules/index.ts @@ -81,6 +81,7 @@ const getEnabledModuleNames = () => { FF_GATEKEEPER_MODULE_ENABLED } = getFeatureFlags() const moduleNames = [ + 'acc', 'accessrequests', 'activitystream', 'apiexplorer', @@ -102,6 +103,7 @@ const getEnabledModuleNames = () => { 'multiregion' ] + // TODO: add acc with feature flag? if (FF_AUTOMATE_MODULE_ENABLED) moduleNames.push('automate') if (FF_GENDOAI_MODULE_ENABLED) moduleNames.push('gendo') // the order of the event listeners matters diff --git a/packages/server/modules/shared/services/eventBus.ts b/packages/server/modules/shared/services/eventBus.ts index aff9f2587..a9100555c 100644 --- a/packages/server/modules/shared/services/eventBus.ts +++ b/packages/server/modules/shared/services/eventBus.ts @@ -53,6 +53,10 @@ import { fileuploadEventNamespace, FileuploadEventsPayloads } from '@/modules/fileuploads/domain/events' +import { + accSyncItemEventsNamespace, + AccSyncItemEventsPayloads +} from '@/modules/acc/domain/events' type AllEventsWildcard = '**' type EventWildcard = '*' @@ -70,6 +74,7 @@ type TestEventsPayloads = { // we should only ever extend this type, other helper types will be derived from this type EventsByNamespace = { test: TestEventsPayloads + [accSyncItemEventsNamespace]: AccSyncItemEventsPayloads [workspaceEventNamespace]: WorkspaceEventsPayloads [gatekeeperEventNamespace]: GatekeeperEventPayloads [serverinvitesEventNamespace]: ServerInvitesEventsPayloads diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index 06332ab2c..d293bf7c9 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -21,6 +21,47 @@ export type Scalars = { JSONObject: { input: Record; output: Record; } }; +export type AccSyncItem = { + __typename?: 'AccSyncItem'; + accFileLineageId: Scalars['String']['output']; + accHubId: Scalars['String']['output']; + accProjectId: Scalars['String']['output']; + accRootFolderUrn: Scalars['String']['output']; + accWebhookId?: Maybe; + author?: Maybe; + createdAt: Scalars['DateTime']['output']; + id: Scalars['ID']['output']; + modelId: Scalars['String']['output']; + projectId: Scalars['String']['output']; + status: AccSyncItemStatus; + updatedAt: Scalars['DateTime']['output']; +}; + +export type AccSyncItemCollection = { + __typename?: 'AccSyncItemCollection'; + cursor?: Maybe; + items: Array; + totalCount: Scalars['Int']['output']; +}; + +export type AccSyncItemMutations = { + __typename?: 'AccSyncItemMutations'; + create?: Maybe; +}; + + +export type AccSyncItemMutationsCreateArgs = { + input: CreateAccSyncItemInput; +}; + +export const AccSyncItemStatus = { + Failed: 'FAILED', + Paused: 'PAUSED', + Sync: 'SYNC', + Syncing: 'SYNCING' +} as const; + +export type AccSyncItemStatus = typeof AccSyncItemStatus[keyof typeof AccSyncItemStatus]; export type ActiveUserMutations = { __typename?: 'ActiveUserMutations'; emailMutations: UserEmailMutations; @@ -889,6 +930,15 @@ export type CountOnlyCollection = { totalCount: Scalars['Int']['output']; }; +export type CreateAccSyncItemInput = { + accFileLineageId: Scalars['String']['input']; + accHubId: Scalars['String']['input']; + accProjectId: Scalars['String']['input']; + accRootFolderUrn: Scalars['String']['input']; + modelId: Scalars['String']['input']; + projectId: Scalars['String']['input']; +}; + export type CreateAutomateFunctionInput = { description: Scalars['String']['input']; /** Base64 encoded image data string */ @@ -964,6 +1014,11 @@ export type CurrencyBasedPrices = { usd: WorkspacePaidPlanPrices; }; +export type DeleteAccSyncItemInput = { + accFileLineageId: Scalars['ID']['input']; + projectId: Scalars['ID']['input']; +}; + export type DeleteModelInput = { id: Scalars['ID']['input']; projectId: Scalars['ID']['input']; @@ -1449,6 +1504,7 @@ export type Mutation = { __typename?: 'Mutation'; /** The void stares back. */ _?: Maybe; + accSyncItemMutations: AccSyncItemMutations; /** Various Active User oriented mutations */ activeUserMutations: ActiveUserMutations; admin: AdminMutations; @@ -2081,6 +2137,8 @@ export type Price = { export type Project = { __typename?: 'Project'; + accSyncItem: AccSyncItem; + accSyncItems: AccSyncItemCollection; allowPublicComments: Scalars['Boolean']['output']; /** Get a single automation by id. Error will be thrown if automation is not found or inaccessible. */ automation: Automation; @@ -2142,6 +2200,11 @@ export type Project = { }; +export type ProjectAccSyncItemArgs = { + id: Scalars['String']['input']; +}; + + export type ProjectAutomationArgs = { id: Scalars['String']['input']; }; @@ -3631,6 +3694,7 @@ export type Subscription = { * Note: Only works in test environment */ ping: Scalars['String']['output']; + projectAccSyncItemsUpdated: Scalars['String']['output']; /** Subscribe to updates to automations in the project */ projectAutomationsUpdated: ProjectAutomationsUpdatedMessage; /** @@ -3747,6 +3811,12 @@ export type SubscriptionCommitUpdatedArgs = { }; +export type SubscriptionProjectAccSyncItemsUpdatedArgs = { + id: Scalars['String']['input']; + itemIds?: InputMaybe>; +}; + + export type SubscriptionProjectAutomationsUpdatedArgs = { projectId: Scalars['String']['input']; }; @@ -3887,6 +3957,12 @@ export type TriggeredAutomationsStatus = { statusMessage?: Maybe; }; +export type UpdateAccSyncItemInput = { + accFileLineageId: Scalars['ID']['input']; + projectId: Scalars['ID']['input']; + status: AccSyncItemStatus; +}; + /** Any null values will be ignored */ export type UpdateAutomateFunctionInput = { description?: InputMaybe;