fix(acc): getter gql and more
This commit is contained in:
@@ -44,7 +44,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/vue/20/solid'
|
||||
import type { AccItem } from '~/lib/acc/types'
|
||||
import type { AccItem } from '@speckle/shared/acc'
|
||||
|
||||
defineProps<{
|
||||
folderContent: AccItem
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AccItem } from '~/lib/acc/types'
|
||||
import type { AccItem } from '@speckle/shared/acc'
|
||||
import type { ProjectAccSyncItemFragment } from '~/lib/common/generated/gql/graphql'
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { isArray } from 'lodash-es'
|
||||
import type { AccHub } from '~/lib/acc/types'
|
||||
import type { AccHub } from '@speckle/shared/acc'
|
||||
|
||||
const props = defineProps<{
|
||||
hubs: AccHub[]
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { isArray } from 'lodash-es'
|
||||
import type { AccProject } from '~/lib/acc/types'
|
||||
import type { AccProject } from '@speckle/shared/acc'
|
||||
|
||||
const props = defineProps<{
|
||||
hubId: string
|
||||
|
||||
@@ -19,19 +19,19 @@ const runStatusClasses = (run: AccSyncItemStatus) => {
|
||||
const classParts = ['w-24 justify-center']
|
||||
|
||||
switch (run) {
|
||||
case 'SYNCING':
|
||||
case 'syncing':
|
||||
classParts.push('bg-info-lighter')
|
||||
break
|
||||
case 'PENDING':
|
||||
case 'pending':
|
||||
classParts.push('bg-warning-lighter')
|
||||
break
|
||||
case 'PAUSED':
|
||||
case 'paused':
|
||||
classParts.push('bg-warning-lighter')
|
||||
break
|
||||
case 'FAILED':
|
||||
case 'failed':
|
||||
classParts.push('bg-danger-lighter')
|
||||
break
|
||||
case 'SUCCEEDED':
|
||||
case 'succeeded':
|
||||
classParts.push('bg-success-lighter')
|
||||
break
|
||||
}
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
<FormButton
|
||||
hide-text
|
||||
color="outline"
|
||||
:icon-left="item.status === 'PAUSED' ? PlayIcon : PauseIcon"
|
||||
@click="handleStatusSyncItem(item.id, item.status === 'PAUSED')"
|
||||
:icon-left="item.status === 'paused' ? PlayIcon : PauseIcon"
|
||||
@click="handleStatusSyncItem(item.id, item.status === 'paused')"
|
||||
/>
|
||||
<FormButton
|
||||
hide-text
|
||||
@@ -144,7 +144,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AccTokens, AccHub, AccProject, AccItem } from '~/lib/acc/types'
|
||||
import type { AccTokens, AccHub, AccProject, AccItem } from '@speckle/shared/acc'
|
||||
import { ref, computed } from 'vue'
|
||||
import type {
|
||||
ProjectLatestModelsPaginationQueryVariables,
|
||||
@@ -169,8 +169,6 @@ const props = defineProps<{
|
||||
isLoggedIn: boolean
|
||||
}>()
|
||||
|
||||
// TODO ACC: Need to think about data residency from "ACC > Speckle" and warn users accordingly
|
||||
|
||||
const step = ref(0)
|
||||
|
||||
const showNewSyncDialog = ref(false)
|
||||
@@ -481,7 +479,7 @@ const handleStatusSyncItem = async (id: string, isPaused: boolean) => {
|
||||
input: {
|
||||
projectId: props.projectId,
|
||||
id,
|
||||
status: isPaused ? 'PENDING' : 'PAUSED'
|
||||
status: isPaused ? 'pending' : 'paused'
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AccTokens, AccUserInfo } from '~/lib/acc/types'
|
||||
import type { AccTokens, AccUserInfo } from '@speckle/shared/acc'
|
||||
// import { DocumentDuplicateIcon } from '@heroicons/vue/24/outline'
|
||||
|
||||
const props = defineProps<{ projectId: string }>()
|
||||
|
||||
@@ -1,47 +1,6 @@
|
||||
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; region: string; extension: Record<string, unknown> }
|
||||
}
|
||||
|
||||
export type AccProject = {
|
||||
id: string
|
||||
attributes: { name: string; lastModifiedTime: string }
|
||||
relationships: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type AccItem = {
|
||||
id: string
|
||||
type?: string
|
||||
latestVersionId?: string // we mutate on the way
|
||||
fileExtension: string
|
||||
storageUrn?: string // we mutate on the way
|
||||
attributes: {
|
||||
name: string
|
||||
displayName: string
|
||||
createTime?: string
|
||||
extension?: Record<string, unknown>
|
||||
versionNumber: number
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: looks stale, we can consider to move this types into @shared
|
||||
import type { AccHub, AccItem } from '@speckle/shared/acc'
|
||||
|
||||
// TODO ACC: Replace with type information inferred from gql queries, if possible
|
||||
export type AccSyncItem = {
|
||||
id: string
|
||||
accHub: AccHub
|
||||
@@ -55,4 +14,9 @@ export type AccSyncItem = {
|
||||
status: AccSyncItemStatus
|
||||
}
|
||||
|
||||
export type AccSyncItemStatus = 'sync' | 'syncing' | 'paused' | 'failed'
|
||||
export type AccSyncItemStatus =
|
||||
| 'pending'
|
||||
| 'syncing'
|
||||
| 'paused'
|
||||
| 'failed'
|
||||
| 'succeeded'
|
||||
|
||||
@@ -74,11 +74,11 @@ export type AccSyncItemMutationsUpdateArgs = {
|
||||
};
|
||||
|
||||
export const AccSyncItemStatus = {
|
||||
Failed: 'FAILED',
|
||||
Paused: 'PAUSED',
|
||||
Pending: 'PENDING',
|
||||
Succeeded: 'SUCCEEDED',
|
||||
Syncing: 'SYNCING'
|
||||
Failed: 'failed',
|
||||
Paused: 'paused',
|
||||
Pending: 'pending',
|
||||
Succeeded: 'succeeded',
|
||||
Syncing: 'syncing'
|
||||
} as const;
|
||||
|
||||
export type AccSyncItemStatus = typeof AccSyncItemStatus[keyof typeof AccSyncItemStatus];
|
||||
|
||||
@@ -31,11 +31,11 @@ type AccSyncItem {
|
||||
}
|
||||
|
||||
enum AccSyncItemStatus {
|
||||
PENDING
|
||||
SYNCING
|
||||
FAILED
|
||||
SUCCEEDED
|
||||
PAUSED
|
||||
pending
|
||||
syncing
|
||||
failed
|
||||
succeeded
|
||||
paused
|
||||
}
|
||||
|
||||
input DeleteAccSyncItemInput {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import type { AccTokens } from '@speckle/shared/acc'
|
||||
import type { AccRegion } from '@/modules/acc/domain/constants'
|
||||
import { AccRegions } from '@/modules/acc/domain/constants'
|
||||
import type { ModelDerivativeServiceDesignManifest } from '@/modules/acc/domain/types'
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
import { logger } from '@/observability/logging'
|
||||
import { isObjectLike } from 'lodash-es'
|
||||
import { z } from 'zod'
|
||||
import crypto from 'crypto'
|
||||
|
||||
const invokeJsonRequest = async <T>(params: {
|
||||
url: string
|
||||
@@ -41,12 +43,121 @@ const invokeJsonRequest = async <T>(params: {
|
||||
return (await response.json()) as T
|
||||
}
|
||||
|
||||
interface BuildAuthorizeUrlOptions {
|
||||
clientId: string
|
||||
redirectUri: string
|
||||
codeChallenge: string
|
||||
scopes: string[]
|
||||
}
|
||||
|
||||
interface ExchangeCodeOptions {
|
||||
code: string
|
||||
codeVerifier: string
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
redirectUri: string
|
||||
}
|
||||
|
||||
const AccTokens = z.object({
|
||||
access_token: z.string(),
|
||||
refresh_token: z.string(),
|
||||
token_type: z.string(),
|
||||
id_token: z.string(),
|
||||
expires_in: z.number()
|
||||
})
|
||||
|
||||
export const generateCodeVerifier = () => {
|
||||
const codeVerifier = crypto.randomBytes(32).toString('base64url')
|
||||
const codeChallenge = crypto
|
||||
.createHash('sha256')
|
||||
.update(codeVerifier)
|
||||
.digest('base64url')
|
||||
return { codeVerifier, codeChallenge }
|
||||
}
|
||||
|
||||
export const 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()}`
|
||||
}
|
||||
|
||||
export const exchangeCodeForTokens = async ({
|
||||
code,
|
||||
codeVerifier,
|
||||
clientId,
|
||||
clientSecret,
|
||||
redirectUri
|
||||
}: ExchangeCodeOptions): Promise<AccTokens> => {
|
||||
const params = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uri: redirectUri,
|
||||
code,
|
||||
code_verifier: codeVerifier
|
||||
})
|
||||
|
||||
const response = await fetch(
|
||||
'https://developer.api.autodesk.com/authentication/v2/token',
|
||||
{
|
||||
method: 'POST',
|
||||
body: params,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return AccTokens.parse(data)
|
||||
}
|
||||
|
||||
export const exchangeRefreshTokenForTokens = async (args: {
|
||||
refresh_token: string
|
||||
}): Promise<AccTokens> => {
|
||||
const params = new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
client_id: getAutodeskIntegrationClientId(),
|
||||
client_secret: getAutodeskIntegrationClientSecret(),
|
||||
refresh_token: args.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
|
||||
}
|
||||
)
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return AccTokens.parse(data)
|
||||
}
|
||||
|
||||
type AutodeskIntegrationTokenData = {
|
||||
access_token: string
|
||||
token_type: string
|
||||
expires_in: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a valid token for server-side operations as our custom integration
|
||||
*/
|
||||
export const getToken = async (): Promise<AutodeskIntegrationTokenData> => {
|
||||
const clientId = getAutodeskIntegrationClientId()
|
||||
const clientSecret = getAutodeskIntegrationClientSecret()
|
||||
|
||||
@@ -7,12 +7,12 @@ export const ImporterAutomateFunctions = {
|
||||
|
||||
export const AccSyncItemStatuses = {
|
||||
// A new file version had been detected, and we are awaiting a processable file.
|
||||
pending: 'PENDING',
|
||||
pending: 'pending',
|
||||
// We are actively processing the new file version. (The Automate function has been triggered.)
|
||||
syncing: 'SYNCING',
|
||||
failed: 'FAILED',
|
||||
paused: 'PAUSED',
|
||||
succeeded: 'SUCCEEDED'
|
||||
syncing: 'syncing',
|
||||
failed: 'failed',
|
||||
paused: 'paused',
|
||||
succeeded: 'succeeded'
|
||||
} as const
|
||||
export type AccSyncItemStatus =
|
||||
(typeof AccSyncItemStatuses)[keyof typeof AccSyncItemStatuses]
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { AccSyncItemStatus } from '@/modules/acc/domain/constants'
|
||||
import type { AccSyncItem } from '@/modules/acc/domain/types'
|
||||
import type { Exact } from 'type-fest'
|
||||
|
||||
export type UpsertAccSyncItem = (item: AccSyncItem) => Promise<void>
|
||||
export type UpsertAccSyncItem = <Item extends Exact<AccSyncItem, Item>>(
|
||||
item: Item
|
||||
) => Promise<void>
|
||||
|
||||
export type UpdateAccSyncItemStatus = (args: {
|
||||
id: string
|
||||
@@ -10,6 +13,8 @@ export type UpdateAccSyncItemStatus = (args: {
|
||||
|
||||
export type GetAccSyncItemById = (args: { id: string }) => Promise<AccSyncItem | null>
|
||||
|
||||
export type GetAccSyncItemsById = (args: { ids: string[] }) => Promise<AccSyncItem[]>
|
||||
|
||||
export type ListAccSyncItems = (args: {
|
||||
projectId: string
|
||||
filter?: {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { BaseError } from '@/modules/shared/errors/base'
|
||||
|
||||
export class AccModuleDisabledError extends BaseError {
|
||||
static defaultMessage = 'ACC integration module is disabled'
|
||||
static code = 'ACC_MODULE_DISABLED'
|
||||
static statusCode = 423
|
||||
}
|
||||
|
||||
export class DuplicateSyncItemError extends BaseError {
|
||||
static defaultMessage = 'A sync item with this lineage urn already exists.'
|
||||
static code = 'ACC_DUPLICATE_SYNC_ITEM_LINEAGE_URN'
|
||||
@@ -14,9 +20,11 @@ export class DuplicateSyncItemError extends BaseError {
|
||||
export class SyncItemNotFoundError extends BaseError {
|
||||
static defaultMessage = 'Sync item not found'
|
||||
static code = 'ACC_SYNC_ITEM_NOT_FOUND'
|
||||
static statusCode = 404
|
||||
}
|
||||
|
||||
export class SyncItemAutomationTriggerError extends BaseError {
|
||||
static defaultMessage = 'Failed to trigger automation associated with sync item'
|
||||
static code = 'ACC_SYNC_ITEM_AUTOMATION_TRIGGER_ERROR'
|
||||
static statusCode = 422
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
import {
|
||||
createAccSyncItemFactory,
|
||||
deleteAccSyncItemFactory,
|
||||
getAccSyncItemFactory,
|
||||
getPaginatedAccSyncItemsFactory,
|
||||
updateAccSyncItemFactory
|
||||
} from '@/modules/acc/services/management'
|
||||
@@ -40,7 +39,6 @@ import { TokenResourceIdentifierType } from '@/modules/core/domain/tokens/types'
|
||||
import type { Resolvers } from '@/modules/core/graph/generated/graphql'
|
||||
import { throwIfResourceAccessNotAllowed } from '@/modules/core/helpers/token'
|
||||
import { getBranchesByIdsFactory } from '@/modules/core/repositories/branches'
|
||||
import { getUserFactory } from '@/modules/core/repositories/users'
|
||||
import { validateStreamAccessFactory } from '@/modules/core/services/streams/access'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import { authorizeResolver } from '@/modules/shared'
|
||||
@@ -61,6 +59,12 @@ import {
|
||||
ProjectSubscriptions
|
||||
} from '@/modules/shared/utils/subscriptions'
|
||||
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
|
||||
import { AccModuleDisabledError, SyncItemNotFoundError } from '@/modules/acc/errors/acc'
|
||||
import { getFeatureFlags } from '@speckle/shared/environment'
|
||||
|
||||
const { FF_ACC_INTEGRATION_ENABLED, FF_AUTOMATE_MODULE_ENABLED } = getFeatureFlags()
|
||||
|
||||
const enableAcc = FF_ACC_INTEGRATION_ENABLED && FF_AUTOMATE_MODULE_ENABLED
|
||||
|
||||
const resolvers: Resolvers = {
|
||||
Mutation: {
|
||||
@@ -164,8 +168,8 @@ const resolvers: Resolvers = {
|
||||
}
|
||||
},
|
||||
AccSyncItem: {
|
||||
author: async (parent) => {
|
||||
return await getUserFactory({ db })(parent.authorId)
|
||||
author: async (parent, _args, context) => {
|
||||
return await context.loaders.users.getUser.load(parent.authorId)
|
||||
}
|
||||
},
|
||||
Project: {
|
||||
@@ -198,9 +202,13 @@ const resolvers: Resolvers = {
|
||||
resourceType: TokenResourceIdentifierType.Project
|
||||
})
|
||||
|
||||
return await getAccSyncItemFactory({
|
||||
getAccSyncItemById: getAccSyncItemByIdFactory({ db })
|
||||
})({ id })
|
||||
const syncItem = await ctx.loaders.acc.getAccSyncItem.load(id)
|
||||
|
||||
if (!syncItem) {
|
||||
throw new SyncItemNotFoundError()
|
||||
}
|
||||
|
||||
return syncItem
|
||||
}
|
||||
},
|
||||
Subscription: {
|
||||
@@ -231,4 +239,37 @@ const resolvers: Resolvers = {
|
||||
}
|
||||
}
|
||||
|
||||
export default resolvers
|
||||
const disabledResolvers: Resolvers = {
|
||||
Mutation: {
|
||||
accSyncItemMutations: () => ({})
|
||||
},
|
||||
AccSyncItemMutations: {
|
||||
async create() {
|
||||
throw new AccModuleDisabledError()
|
||||
},
|
||||
async update() {
|
||||
throw new AccModuleDisabledError()
|
||||
},
|
||||
async delete() {
|
||||
throw new AccModuleDisabledError()
|
||||
}
|
||||
},
|
||||
Project: {
|
||||
async accSyncItem() {
|
||||
throw new AccModuleDisabledError()
|
||||
},
|
||||
async accSyncItems() {
|
||||
throw new AccModuleDisabledError()
|
||||
}
|
||||
},
|
||||
Subscription: {
|
||||
projectAccSyncItemsUpdated: {
|
||||
subscribe: filteredSubscribe(
|
||||
ProjectSubscriptions.ProjectAccSyncItemUpdated,
|
||||
async () => false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default enableAcc ? resolvers : disabledResolvers
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import crypto from 'crypto'
|
||||
import { z } from 'zod'
|
||||
|
||||
interface BuildAuthorizeUrlOptions {
|
||||
clientId: string
|
||||
redirectUri: string
|
||||
codeChallenge: string
|
||||
scopes: string[]
|
||||
}
|
||||
|
||||
interface ExchangeCodeOptions {
|
||||
code: string
|
||||
codeVerifier: string
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
redirectUri: string
|
||||
}
|
||||
|
||||
const AccTokens = z.object({
|
||||
access_token: z.string(),
|
||||
refresh_token: z.string(),
|
||||
token_type: z.string(),
|
||||
id_token: z.string(),
|
||||
expires_in: z.number()
|
||||
})
|
||||
|
||||
export type AccTokens = z.infer<typeof AccTokens>
|
||||
|
||||
export const generateCodeVerifier = () => {
|
||||
const codeVerifier = crypto.randomBytes(32).toString('base64url')
|
||||
const codeChallenge = crypto
|
||||
.createHash('sha256')
|
||||
.update(codeVerifier)
|
||||
.digest('base64url')
|
||||
return { codeVerifier, codeChallenge }
|
||||
}
|
||||
|
||||
export const 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()}`
|
||||
}
|
||||
|
||||
export const exchangeCodeForTokens = async ({
|
||||
code,
|
||||
codeVerifier,
|
||||
clientId,
|
||||
clientSecret,
|
||||
redirectUri
|
||||
}: ExchangeCodeOptions): Promise<AccTokens> => {
|
||||
const params = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uri: redirectUri,
|
||||
code,
|
||||
code_verifier: codeVerifier
|
||||
})
|
||||
|
||||
const response = await fetch(
|
||||
'https://developer.api.autodesk.com/authentication/v2/token',
|
||||
{
|
||||
method: 'POST',
|
||||
body: params,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return AccTokens.parse(data)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AccTokens } from '@/modules/acc/helpers/oidcHelper'
|
||||
import type { AccTokens } from '@speckle/shared/acc'
|
||||
import type { Session, SessionData } from 'express-session'
|
||||
|
||||
declare module 'express-session' {
|
||||
|
||||
@@ -8,14 +8,14 @@ import {
|
||||
} from '@/modules/core/repositories/scheduledTasks'
|
||||
import type { ScheduleExecution } from '@/modules/core/domain/scheduledTasks/operations'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import { accOidc } from '@/modules/acc/rest/oidc'
|
||||
import { accWebhooks } from '@/modules/acc/rest/webhooks'
|
||||
import { setupAccOidcEndpoints } from '@/modules/acc/rest/oidc'
|
||||
import { setupAccWebhookEndpoints } from '@/modules/acc/rest/webhooks'
|
||||
import { schedulePendingSyncItemsCheck } from '@/modules/acc/services/cron'
|
||||
import { reportSubscriptionEventsFactory } from '@/modules/acc/events/eventListeners'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { publish } from '@/modules/shared/utils/subscriptions'
|
||||
|
||||
const { FF_ACC_INTEGRATION_ENABLED } = getFeatureFlags()
|
||||
const { FF_ACC_INTEGRATION_ENABLED, FF_AUTOMATE_MODULE_ENABLED } = getFeatureFlags()
|
||||
|
||||
const scheduleExecution = scheduleExecutionFactory({
|
||||
acquireTaskLock: acquireTaskLockFactory({ db }),
|
||||
@@ -27,24 +27,23 @@ let scheduledTask: ReturnType<ScheduleExecution> | null = null
|
||||
|
||||
const accModule: SpeckleModule = {
|
||||
init: async ({ app, isInitial }) => {
|
||||
if (!FF_ACC_INTEGRATION_ENABLED) return
|
||||
if (!FF_ACC_INTEGRATION_ENABLED || !FF_AUTOMATE_MODULE_ENABLED) return
|
||||
|
||||
moduleLogger.info('🖕 Init acc module')
|
||||
moduleLogger.info('🖕 Init ACC module')
|
||||
|
||||
if (isInitial) {
|
||||
accOidc(app)
|
||||
accWebhooks(app)
|
||||
setupAccOidcEndpoints(app)
|
||||
setupAccWebhookEndpoints(app)
|
||||
quitListeners = reportSubscriptionEventsFactory({
|
||||
eventListen: getEventBus().listen,
|
||||
publish
|
||||
})
|
||||
})()
|
||||
scheduledTask = schedulePendingSyncItemsCheck({ scheduleExecution })
|
||||
}
|
||||
},
|
||||
shutdown: () => {
|
||||
if (!FF_ACC_INTEGRATION_ENABLED) return
|
||||
quitListeners?.()
|
||||
scheduledTask?.stop()
|
||||
scheduledTask?.stop?.()
|
||||
},
|
||||
finalize: () => {}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
CountAccSyncItems,
|
||||
DeleteAccSyncItemById,
|
||||
GetAccSyncItemById,
|
||||
GetAccSyncItemsById,
|
||||
ListAccSyncItems,
|
||||
QueryAllAccSyncItems,
|
||||
UpdateAccSyncItemStatus,
|
||||
@@ -23,12 +24,20 @@ export const getAccSyncItemByIdFactory =
|
||||
return (
|
||||
(await tables
|
||||
.accSyncItems(deps.db)
|
||||
.select('*')
|
||||
.select()
|
||||
.where(AccSyncItems.col.id, id)
|
||||
.first()) ?? null
|
||||
)
|
||||
}
|
||||
|
||||
export const getAccSyncItemsByIdFactory =
|
||||
(deps: { db: Knex }): GetAccSyncItemsById =>
|
||||
async ({ ids }) => {
|
||||
if (!ids.length) return []
|
||||
|
||||
return await tables.accSyncItems(deps.db).select().whereIn(AccSyncItems.col.id, ids)
|
||||
}
|
||||
|
||||
export const upsertAccSyncItemFactory =
|
||||
(deps: { db: Knex }): UpsertAccSyncItem =>
|
||||
async (item) => {
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
import {
|
||||
buildAuthorizeUrl,
|
||||
exchangeCodeForTokens,
|
||||
exchangeRefreshTokenForTokens,
|
||||
generateCodeVerifier
|
||||
} from '@/modules/acc/helpers/oidcHelper'
|
||||
} from '@/modules/acc/clients/autodesk'
|
||||
import { sessionMiddlewareFactory } from '@/modules/auth/middleware'
|
||||
import { corsMiddlewareFactory } from '@/modules/core/configs/cors'
|
||||
import {
|
||||
@@ -15,7 +16,7 @@ import {
|
||||
} from '@/modules/shared/helpers/envHelper'
|
||||
import type { Express } from 'express'
|
||||
|
||||
export const accOidc = (app: Express) => {
|
||||
export const setupAccOidcEndpoints = (app: Express) => {
|
||||
const corsMiddleware = corsMiddlewareFactory({
|
||||
corsConfig: {
|
||||
origin: [getServerOrigin(), getFrontendOrigin()],
|
||||
@@ -101,30 +102,8 @@ export const accOidc = (app: Express) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
client_id: getAutodeskIntegrationClientId(),
|
||||
client_secret: getAutodeskIntegrationClientSecret(),
|
||||
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()
|
||||
const newTokens = await exchangeRefreshTokenForTokens({ refresh_token })
|
||||
req.session.accTokens = newTokens
|
||||
|
||||
res.json(newTokens)
|
||||
} catch (error) {
|
||||
console.error('Error refreshing token:', error)
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { Express } from 'express'
|
||||
import { z } from 'zod'
|
||||
import { db } from '@/db/knex'
|
||||
|
||||
export const accWebhooks = (app: Express) => {
|
||||
export const setupAccWebhookEndpoints = (app: Express) => {
|
||||
const sessionMiddleware = sessionMiddlewareFactory()
|
||||
app.post('/api/v1/acc/webhook/callback', sessionMiddleware, async (req, res) => {
|
||||
logger.info({ hook: req.body?.hook, payload: req.body?.payload })
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import type { ScheduleExecution } from '@/modules/core/domain/scheduledTasks/operations'
|
||||
import { db } from '@/db/knex'
|
||||
import { getManifestByUrn, getToken } from '@/modules/acc/clients/autodesk'
|
||||
import { isReadyForImport } from '@/modules/acc/domain/logic'
|
||||
import { isReadyForImport } from '@/modules/acc/helpers/svfUtils'
|
||||
import type { Logger } from '@/observability/logging'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import {
|
||||
|
||||
@@ -3,9 +3,12 @@ import {
|
||||
getToken,
|
||||
tryRegisterAccWebhook
|
||||
} from '@/modules/acc/clients/autodesk'
|
||||
import { ImporterAutomateFunctions } from '@/modules/acc/domain/constants'
|
||||
import {
|
||||
AccSyncItemStatuses,
|
||||
ImporterAutomateFunctions
|
||||
} from '@/modules/acc/domain/constants'
|
||||
import { AccSyncItemEvents } from '@/modules/acc/domain/events'
|
||||
import { isReadyForImport } from '@/modules/acc/domain/logic'
|
||||
import { isReadyForImport } from '@/modules/acc/helpers/svfUtils'
|
||||
import type {
|
||||
CountAccSyncItems,
|
||||
DeleteAccSyncItemById,
|
||||
@@ -27,6 +30,7 @@ import {
|
||||
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<
|
||||
@@ -101,7 +105,7 @@ export const createAccSyncItemFactory =
|
||||
...syncItem,
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
automationId: automation.id,
|
||||
status: 'PENDING',
|
||||
status: AccSyncItemStatuses.pending,
|
||||
authorId: creatorUserId,
|
||||
accWebhookId: webhookId ?? undefined,
|
||||
createdAt: new Date(),
|
||||
@@ -136,20 +140,6 @@ export const createAccSyncItemFactory =
|
||||
return await deps.triggerSyncItemAutomation({ id: newSyncItem.id })
|
||||
}
|
||||
|
||||
export type GetAccSyncItem = (params: { id: string }) => Promise<AccSyncItem>
|
||||
|
||||
export const getAccSyncItemFactory =
|
||||
(deps: { getAccSyncItemById: GetAccSyncItemById }): GetAccSyncItem =>
|
||||
async ({ id }) => {
|
||||
const syncItem = await deps.getAccSyncItemById({ id })
|
||||
|
||||
if (!syncItem) {
|
||||
throw new SyncItemNotFoundError()
|
||||
}
|
||||
|
||||
return syncItem
|
||||
}
|
||||
|
||||
export type GetPaginatedAccSyncItems = (params: {
|
||||
projectId: string
|
||||
filter?: {
|
||||
@@ -190,8 +180,10 @@ export const getPaginatedAccSyncItemsFactory =
|
||||
}
|
||||
}
|
||||
|
||||
export type UpdateAccSyncItem = (params: {
|
||||
syncItem: Partial<AccSyncItem> & Pick<AccSyncItem, 'id'>
|
||||
export type UpdateAccSyncItem = <
|
||||
Item extends Exact<Partial<AccSyncItem> & Pick<AccSyncItem, 'id'>, Item>
|
||||
>(params: {
|
||||
syncItem: Item
|
||||
}) => Promise<AccSyncItem>
|
||||
|
||||
export const updateAccSyncItemFactory =
|
||||
|
||||
@@ -92,6 +92,8 @@ import type {
|
||||
import { logger } from '@/observability/logging'
|
||||
import { getLastVersionsByProjectIdFactory } from '@/modules/core/repositories/versions'
|
||||
import type { StreamRoles } from '@speckle/shared'
|
||||
import { getAccSyncItemsByIdFactory } from '@/modules/acc/repositories/accSyncItems'
|
||||
import type { AccSyncItem } from '@/modules/acc/domain/types'
|
||||
|
||||
declare module '@/modules/core/loaders' {
|
||||
interface ModularizedDataLoaders extends ReturnType<typeof dataLoadersDefinition> {}
|
||||
@@ -139,6 +141,7 @@ const dataLoadersDefinition = defineRequestDataloaders(
|
||||
const getStreamsCollaboratorCounts = getStreamsCollaboratorCountsFactory({
|
||||
db
|
||||
})
|
||||
const getAccSyncItemsById = getAccSyncItemsByIdFactory({ db })
|
||||
|
||||
return {
|
||||
streams: {
|
||||
@@ -548,6 +551,15 @@ const dataLoadersDefinition = defineRequestDataloaders(
|
||||
return appIds.map((i) => results[i] || [])
|
||||
})
|
||||
},
|
||||
acc: {
|
||||
getAccSyncItem: createLoader<string, Nullable<AccSyncItem>>(async (ids) => {
|
||||
const results = keyBy(
|
||||
await getAccSyncItemsById({ ids: ids.slice() }),
|
||||
(i) => i.id
|
||||
)
|
||||
return ids.map((i) => results[i] || null)
|
||||
})
|
||||
},
|
||||
automations: {
|
||||
getAutomation: createLoader<string, Nullable<AutomationRecord>>(async (ids) => {
|
||||
const results = keyBy(
|
||||
|
||||
@@ -94,11 +94,11 @@ export type AccSyncItemMutationsUpdateArgs = {
|
||||
};
|
||||
|
||||
export const AccSyncItemStatus = {
|
||||
Failed: 'FAILED',
|
||||
Paused: 'PAUSED',
|
||||
Pending: 'PENDING',
|
||||
Succeeded: 'SUCCEEDED',
|
||||
Syncing: 'SYNCING'
|
||||
Failed: 'failed',
|
||||
Paused: 'paused',
|
||||
Pending: 'pending',
|
||||
Succeeded: 'succeeded',
|
||||
Syncing: 'syncing'
|
||||
} as const;
|
||||
|
||||
export type AccSyncItemStatus = typeof AccSyncItemStatus[keyof typeof AccSyncItemStatus];
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
"./viewer/route": "./src/viewer/helpers/route.ts",
|
||||
"./viewer/state": "./src/viewer/helpers/state.ts",
|
||||
"./automate": "./src/automate/index.ts",
|
||||
"./acc": "./src/acc/index.ts",
|
||||
"./dist/*": "./dist/*"
|
||||
},
|
||||
"exclude": [
|
||||
@@ -310,6 +311,16 @@
|
||||
"default": "./dist/commonjs/automate/index.js"
|
||||
}
|
||||
},
|
||||
"./acc": {
|
||||
"import": {
|
||||
"types": "./dist/esm/acc/index.d.ts",
|
||||
"default": "./dist/esm/acc/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./dist/commonjs/acc/index.d.ts",
|
||||
"default": "./dist/commonjs/acc/index.js"
|
||||
}
|
||||
},
|
||||
"./dist/*": "./dist/*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
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; region: string; extension: Record<string, unknown> }
|
||||
}
|
||||
|
||||
export type AccProject = {
|
||||
id: string
|
||||
attributes: { name: string; lastModifiedTime: string }
|
||||
relationships: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type AccItem = {
|
||||
id: string
|
||||
type?: string
|
||||
latestVersionId?: string // we mutate on the way
|
||||
fileExtension: string
|
||||
storageUrn?: string // we mutate on the way
|
||||
attributes: {
|
||||
name: string
|
||||
displayName: string
|
||||
createTime?: string
|
||||
extension?: Record<string, unknown>
|
||||
versionNumber: number
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './helpers/types.js'
|
||||
Reference in New Issue
Block a user