feat(acc): revamp (#5501)

* chore(acc): put permission gql in correct place

* feat(acc): swap to new rvt import

* fix(acc): add oda secrets

* feat(acc): auth cookies

* feat(acc): introduce integrations as workspace setting

* feat(acc): create sync item from models

* fix(acc): bump

* fix(acc): naming lost in merge

* feat(acc): no acc tab - table under settings

* chore(acc): new sync but will disapper

* feat(acc): see statuses over model list

* chore(acc): fix return type

* chore(acc): type saga

* chore(acc): status badge

* chore(acc): refactor acc gql (#5556)

* checkpoint

* fix(acc): refactor gql items

* feat(acc): double button

* chore(acc): gqlgen

* fix(acc): model ids are not project ids

* chore(acc): bump function version

* chore(acc): split up clients

* feat(acc): more-optimised gql folder fetching schema

* feat(acc): acc folder contents gql impl

* feat(acc): apollo cache optimisations

* chore(acc): gqlgen

* fix(acc): return something for

* fix(acc): handle null values correctly

* chore(acc): specify prod functions

---------

Co-authored-by: Chuck Driesler <chuck@speckle.systems>
This commit is contained in:
Oğuzhan Koral
2025-10-03 15:54:17 +03:00
committed by GitHub
parent 168137bff9
commit 05e00d2c5c
80 changed files with 3210 additions and 1476 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

@@ -0,0 +1,107 @@
<template>
<div
class="flex items-center justify-between border border-foreground-1 bg-foundation rounded-lg p-2"
>
<div class="flex items-center space-x-3">
<img
:src="integration.logo"
alt=""
class="w-10 h-10 p-1 object-cover border border-foreground-1 rounded-lg"
/>
<div class="flex flex-col">
<span class="font-medium">{{ integration.name }}</span>
<span class="text-sm text-foreground-2">
{{ integration.description }}
</span>
</div>
</div>
<div class="flex items-center space-x-4">
<CommonLoadingIcon v-if="loading" :loading="true" class="opacity-50 mr-2" />
<div v-else-if="integration.enabled">
<div class="flex items-center text-sm text-foreground-2 space-x-2">
<span
v-if="
integration.status === 'connected' || integration.status === 'expired'
"
class="w-2 h-2 rounded-full"
:class="{
'bg-success': integration.status === 'connected',
'bg-warning': integration.status === 'expired'
}"
></span>
<div>{{ statusText() }}</div>
<!-- CTA -->
<FormButton size="sm" color="outline" @click="handleCTA()">
<span v-if="integration.status === 'notConnected'">Log in</span>
<span v-else-if="integration.status === 'expired'">Reconnect</span>
<span v-else>Log out</span>
</FormButton>
</div>
</div>
<div v-else>
<FormButton size="sm" @click="$emit('upgrade')">Upgrade</FormButton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useAccAuthManager } from '~/lib/acc/composables/useAccAuthManager'
import { useAccIntegration } from '~/lib/integrations/composables/useAccIntegration'
const props = defineProps<{ workspaceId: string; workspaceSlug: string }>()
defineEmits<{
(e: 'handleCTA'): void
(e: 'upgrade'): void
}>()
const loading = ref(true)
const { integration, checkConnection } = useAccIntegration()
const { tokens, authAcc, logOut, tryGetTokensFromCookies, fetchTokens } =
useAccAuthManager() // later can be generalized
// await checkConnection(props.workspaceSlug, props.workspaceId || '')
const statusText = () => {
switch (integration.value.status) {
case 'connected':
return 'Connected'
case 'expired':
return 'Expired'
case 'notConnected':
return ''
default:
break
}
}
const handleCTA = async () => {
if (
integration.value.status === 'notConnected' ||
integration.value.status === 'expired'
) {
authAcc(`/settings/workspaces/${props.workspaceSlug}/integrations`)
} else {
logOut()
await checkConnection(props.workspaceSlug, props.workspaceId || '')
}
}
onMounted(async () => {
loading.value = true
try {
await tryGetTokensFromCookies()
if (!tokens.value) {
await fetchTokens()
}
await checkConnection(props.workspaceSlug, props.workspaceId || '')
} finally {
loading.value = false
}
})
</script>
@@ -0,0 +1,95 @@
<template>
<div class="flex flex-row h-full overflow-hidden border rounded-lg bg-foundation">
<!-- Left Pane for tree -->
<div class="w-1/4 p-2 overflow-y-auto border-r">
<h3 class="font-semibold text-lg text-center">Folders</h3>
<hr class="mb-1" />
<div v-if="!rootFolder"></div>
<ul
v-else-if="rootFolder && rootFolder.children?.items.length"
class="space-y-1 pt-1"
>
<IntegrationsAccFolderNode
v-for="folder in rootFolderChildren"
:key="folder.id"
:project-id="projectId"
:folder-id="folder.id"
:tokens="tokens"
:selected-folder-id="selectedFolderId"
@select="onFolderClick"
/>
</ul>
</div>
<!-- Right Pane for content -->
<div class="w-3/4 p-2 overflow-y-auto">
<h3 class="font-semibold text-lg text-center">Files</h3>
<hr class="mb-1" />
<IntegrationsAccFolderContents
v-if="!!selectedFolderId"
:key="`contents-${selectedFolderId}`"
:project-id="projectId"
:folder-id="selectedFolderId"
:tokens="tokens"
:selected-file-id="selectedFileId"
@select="onFileSelected"
/>
</div>
</div>
</template>
<script setup lang="ts">
import type { AccTokens } from '@speckle/shared/acc'
import { ref, watch } from 'vue'
import { useAcc, type AccItemVersion } from '~/lib/acc/composables/useAccFiles'
import { useAccFolder } from '~/lib/acc/composables/useAccFolderData'
const props = defineProps<{
hubId: string
projectId: string
tokens: AccTokens | undefined
}>()
const emit = defineEmits<{
'file-selected': [fileId: string, fileVersion: AccItemVersion]
}>()
const { init, rootProjectFolderId } = useAcc()
const rootFolder = useAccFolder(props.projectId, rootProjectFolderId, props.tokens)
const rootFolderChildren = computed(
() =>
rootFolder.value?.children?.items?.filter(
(child) => child.name === 'Project Files'
) ?? []
)
const selectedFolderId = ref<string | undefined>()
const selectedFileId = ref<string | undefined>()
const onFolderClick = async (folderId: string) => {
selectedFolderId.value = folderId
selectedFileId.value = undefined
}
const onFileSelected = (fileId: string, fileVersion: AccItemVersion) => {
selectedFileId.value = fileId
emit('file-selected', fileId, fileVersion)
}
// Watch for changes in projectId to re-initialize the folder tree
watch(
() => props.projectId,
async (newProjectId) => {
selectedFolderId.value = undefined
selectedFileId.value = undefined
if (newProjectId && props.tokens) {
await init(props.hubId, newProjectId, props.tokens.access_token)
}
},
{ immediate: true }
)
watch(rootFolderChildren, (newValue) => {
selectedFolderId.value = newValue.at(0)?.id
})
</script>
@@ -0,0 +1,56 @@
<template>
<div>
<ul v-if="items?.length" class="space-y-1">
<template v-for="item in items" :key="item.id">
<li
class="flex items-center space-x-1 px-2 rounded-md transition-colors w-full"
:class="{
'bg-foundation-focus font-semibold': selectedFileId === item.id,
'hover:bg-primary-muted cursor-pointer': selectedFileId !== item.id
}"
>
<button
class="flex items-center space-x-1 p-1 rounded-md transition-colors w-full"
@click="
emit('select', item.id, removeNullOrUndefinedKeys(item.latestVersion))
"
>
<span>
{{ item.name }}
</span>
</button>
</li>
<hr />
</template>
</ul>
<div v-else class="text-center text-foreground-2 py-2">
<span>No files found.</span>
</div>
</div>
</template>
<script setup lang="ts">
import { removeNullOrUndefinedKeys } from '@speckle/shared'
import type { AccTokens } from '@speckle/shared/acc'
import type { AccItemVersion } from '~/lib/acc/composables/useAccFiles'
import { useAccFolder } from '~/lib/acc/composables/useAccFolderData'
const props = defineProps<{
projectId: string
folderId: string
tokens: AccTokens | undefined
selectedFileId: string | undefined
}>()
const emit = defineEmits<{
select: [fileId: string, fileVersion: AccItemVersion]
}>()
const folder = useAccFolder(props.projectId, props.folderId, props.tokens)
const items = computed(() => {
return folder.value.contents?.items.filter(
(item) => item.latestVersion.fileType?.toLowerCase() === 'rvt'
)
})
</script>
@@ -0,0 +1,104 @@
<template>
<li>
<button
class="flex items-center space-x-1 p-1 rounded-md transition-colors w-full"
:class="{
'bg-foundation-focus font-semibold': selectedFolderId === folder.id,
'hover:bg-primary-muted cursor-pointer': selectedFolderId !== folder.id
}"
@click="emit('select', folderId)"
>
<ChevronDownIcon
:class="`h-4 w-5 transition ${!isExpanded ? '-rotate-90' : 'rotate-0'}`"
@click.stop="isExpanded = !isExpanded"
/>
<span>{{ folder.name }}</span>
</button>
<ul
v-if="isExpanded && folder.children && folder.children.items.length > 0"
class="ml-4 mt-1 space-y-1"
>
<IntegrationsAccFolderNode
v-for="child in folder.children.items"
:key="child.id"
:folder-id="child.id"
:project-id="projectId"
:tokens="tokens"
:selected-folder-id="selectedFolderId"
@select="(id) => emit('select', id)"
/>
</ul>
</li>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ChevronDownIcon } from '@heroicons/vue/24/outline'
import { graphql } from '~/lib/common/generated/gql'
import { useAccFolder } from '~/lib/acc/composables/useAccFolderData'
import type { AccTokens } from '@speckle/shared/acc'
graphql(`
fragment AccIntegrationFolderNode_AccFolder on AccFolder {
id
name
contents {
items {
id
name
latestVersion {
id
name
versionNumber
fileType
}
}
}
children {
items {
id
name
children {
items {
id
name
}
}
contents {
items {
id
name
}
}
}
}
}
`)
const props = defineProps<{
// TODO ACC Maybe inject from shared local state within file navigation
projectId: string
folderId: string
tokens?: AccTokens
// TODO ACC Maybe inject from shared local state within file navigation
selectedFolderId: string | undefined
}>()
const emit = defineEmits<{
select: [folderId: string]
}>()
const folder = useAccFolder(props.projectId, props.folderId, props.tokens)
// watch(
// folder,
// (f) => {
// console.log({ resultFolder: f })
// },
// {
// immediate: true
// }
// )
const isExpanded = ref(false)
</script>
@@ -11,12 +11,12 @@
/>
<CommonLoadingBar v-if="loading" loading />
<ProjectPageAccModelItem
<IntegrationsAccModelItem
v-for="model in models"
:key="model.id"
:model="model"
:selected="model.id === selectedModel?.id"
:disabled="!!props.accSyncItems?.find((i) => i.modelId === model.id)"
:disabled="!!props.accSyncItems?.find((i) => i.model?.id === model.id)"
@click="onModelItemClicked(model)"
/>
<button
@@ -0,0 +1,59 @@
<template>
<div v-tippy="statusLabel">
<CommonBadge
:color-classes="
[runStatusClasses(status), 'shrink-0 grow-0 text-foreground'].join(' ')
"
>
ACC
</CommonBadge>
</div>
</template>
<script setup lang="ts">
import { graphql } from '~/lib/common/generated/gql'
import type {
AccSyncItemStatus,
SyncStatusModelItem_AccSyncItemFragment
} from '~/lib/common/generated/gql/graphql'
graphql(`
fragment SyncStatusModelItem_AccSyncItem on AccSyncItem {
id
status
}
`)
const props = defineProps<{
item: SyncStatusModelItem_AccSyncItemFragment
}>()
const status = computed(() => props.item.status)
const statusLabel = computed(
() => status.value.charAt(0).toUpperCase() + status.value.slice(1)
)
const runStatusClasses = (run: AccSyncItemStatus) => {
const classParts = ['w-24 justify-center']
switch (run) {
case 'syncing':
classParts.push('bg-info-lighter')
break
case 'pending':
classParts.push('bg-warning-lighter')
break
case 'paused':
classParts.push('bg-warning-lighter')
break
case 'failed':
classParts.push('bg-danger-lighter')
break
case 'succeeded':
classParts.push('bg-success-lighter')
break
}
return classParts.join(' ')
}
</script>
@@ -0,0 +1,142 @@
<template>
<div class="flex flex-col space-y-2">
<div class="flex text-body-xs text-foreground font-medium">Sync models</div>
<LayoutTable
class="bg-foundation"
:columns="[
{ id: 'status', header: 'Status', classes: 'col-span-2' },
{ id: 'accFileName', header: 'File name', classes: 'col-span-2' },
{ id: 'accFileViewName', header: 'View name', classes: 'col-span-2' },
{ id: 'modelId', header: 'Model id', classes: 'col-span-2' },
{ id: 'createdBy', header: 'Created by', classes: 'col-span-2' },
{ id: 'actions', header: 'Actions', classes: 'col-span-2' }
]"
:items="accSyncItems"
>
<template #status="{ item }">
<IntegrationsAccSyncStatus :status="item.status" />
</template>
<template #accFileName="{ item }">
{{ item.accFileName }}
</template>
<template #accFileViewName="{ item }">
{{ item.accFileViewName || '-' }}
</template>
<template #modelId="{ item }">
<NuxtLink
class="text-foreground-1 hover:text-blue-500 underline"
:to="`/projects/${projectId}/models/${item.model?.id}`"
>
{{ item.model?.id }}
</NuxtLink>
</template>
<template #createdBy="{ item }">
{{ item.author?.name }}
</template>
<template #actions="{ item }">
<div class="space-x-2">
<FormButton
hide-text
color="outline"
:icon-left="item.status === 'paused' ? PlayIcon : PauseIcon"
@click="handleStatusSyncItem(item.id, item.status === 'paused')"
/>
<FormButton
hide-text
color="outline"
:icon-left="TrashIcon"
@click="handleDeleteSyncItem(item.id)"
/>
</div>
</template>
</LayoutTable>
</div>
</template>
<script setup lang="ts">
import type { AccTokens } from '@speckle/shared/acc'
import { useMutation, useQuery, useSubscription } from '@vue/apollo-composable'
import {
accSyncItemDeleteMutation,
accSyncItemUpdateMutation
} from '~/lib/acc/graphql/mutations'
import { projectAccSyncItemsQuery } from '~/lib/acc/graphql/queries'
import { onProjectAccSyncItemUpdatedSubscription } from '~/lib/acc/graphql/subscriptions'
import { PauseIcon } from '@heroicons/vue/24/solid'
import { TrashIcon, PlayIcon } from '@heroicons/vue/24/outline'
const props = defineProps<{
projectId: string
tokens: AccTokens | undefined
isLoggedIn: boolean
}>()
const { triggerNotification } = useGlobalToast()
const { result: accSyncItemsResult, refetch: refetchAccSyncItems } = useQuery(
projectAccSyncItemsQuery,
() => ({
id: props.projectId
})
)
const accSyncItems = computed(
() => accSyncItemsResult.value?.project.accSyncItems.items || []
)
const { onResult: onProjectAccSyncItemsUpdated } = useSubscription(
onProjectAccSyncItemUpdatedSubscription,
() => ({
id: props.projectId
})
)
onProjectAccSyncItemsUpdated((res) => {
// TODO ACC: Mutate local cache instead of refetch
refetchAccSyncItems()
triggerNotification({
type: ToastNotificationType.Info,
title: `ACC sync model ${res.data?.projectAccSyncItemsUpdated.type.toLowerCase()}`,
description: res.data?.projectAccSyncItemsUpdated.accSyncItem?.accFileName
})
})
const { mutate: deleteAccSyncItem } = useMutation(accSyncItemDeleteMutation)
const handleDeleteSyncItem = async (id: string) => {
try {
await deleteAccSyncItem({
input: {
projectId: props.projectId,
id
}
})
} catch (error) {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Delete sync item failed',
description: error instanceof Error ? error.message : 'Unexpected error'
})
}
}
const { mutate: updateAccSyncItem } = useMutation(accSyncItemUpdateMutation)
const handleStatusSyncItem = async (id: string, isPaused: boolean) => {
try {
await updateAccSyncItem({
input: {
projectId: props.projectId,
id,
status: isPaused ? 'pending' : 'paused'
}
})
} catch (error) {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Update sync item failed',
description: error instanceof Error ? error.message : 'Unexpected error'
})
}
}
</script>
@@ -0,0 +1,198 @@
<template>
<LayoutDialog
v-model:open="isOpen"
:title="dialogTitle"
:buttons="dialogButtons"
max-width="lg"
fullscreen="none"
>
<div class="flex flex-col space-y-2">
<IntegrationsAccHubs
:hubs="hubs"
:loading="loadingHubs"
@hub-selected="onHubClick"
/>
<IntegrationsAccProjects
v-if="selectedHubId"
:hub-id="selectedHubId"
:projects="projects"
:loading="loadingProjects"
@project-selected="onProjectClick"
/>
<IntegrationsAccFileSelector
v-if="selectedProjectId && selectedHubId && tokens"
:key="selectedProjectId"
:hub-id="selectedHubId"
:project-id="selectedProjectId"
:tokens="tokens"
@file-selected="onFileSelected"
/>
<FormTextInput
v-model="revitViewName"
name="revitFileViewName"
color="foundation"
label="Revit view name (Optional)"
show-label
:disabled="!selectedFileVersion"
></FormTextInput>
</div>
</LayoutDialog>
</template>
<script setup lang="ts">
import type { AccHub } from '@speckle/shared/acc'
import type { LayoutDialogButton } from '@speckle/ui-components'
import { useMutation } from '@vue/apollo-composable'
import { useForm } from 'vee-validate'
import { useAccAuthManager } from '~/lib/acc/composables/useAccAuthManager'
import {
useAcc,
type AccFolder,
type AccItemVersion
} from '~/lib/acc/composables/useAccFiles'
import { accSyncItemCreateMutation } from '~/lib/acc/graphql/mutations'
import { useCreateNewModel } from '~/lib/projects/composables/modelManagement'
type FormValues = { feedback: string }
const props = defineProps<{
title?: string
intro?: string
hideSuppport?: boolean
projectId?: string
metadata?: Record<string, unknown>
}>()
const isOpen = defineModel<boolean>('open', { required: true })
const { handleSubmit } = useForm<FormValues>()
const dialogButtons = computed((): LayoutDialogButton[] => [
{
text: 'Create',
props: { color: 'primary' },
onClick: () => {
onSubmit()
},
id: 'createAccSync'
}
])
const { triggerNotification } = useGlobalToast()
const createModel = useCreateNewModel()
const dialogTitle = computed(() => props.title || 'Create sync from ACC')
const onSubmit = handleSubmit(async () => {
await addSync()
isOpen.value = false
})
const selectedHub = ref<AccHub | null>(null)
const selectedHubId = ref<string | null>(null)
const selectedProjectId = ref<string | null>(null)
const revitViewName = ref<string>()
const selectedFolder = ref<AccFolder | undefined>()
const selectedFileId = ref<string | undefined>()
const selectedFileVersion = ref<AccItemVersion | undefined>()
const {
hubs,
fetchHubs,
loadingHubs,
projects,
fetchProjects,
loadingProjects,
rootProjectFolderId,
init
} = useAcc()
const { tokens, tryGetTokensFromCookies } = useAccAuthManager()
const onHubClick = async (hub: AccHub) => {
selectedHub.value = hub
selectedHubId.value = hub.id
await fetchProjects(hub.id, tokens.value!.access_token)
}
const onProjectClick = async (hubId: string, projectId: string) => {
selectedFolder.value = undefined
selectedFileVersion.value = undefined
selectedProjectId.value = projectId
await init(hubId, projectId, tokens.value!.access_token)
}
const onFileSelected = (fileId: string, fileVersion: AccItemVersion) => {
selectedFileId.value = fileId
selectedFileVersion.value = fileVersion
}
const { mutate: createAccSyncItem } = useMutation(accSyncItemCreateMutation)
const addSync = async () => {
try {
if (!selectedFileVersion.value || !selectedFileVersion.value.fileType) {
return
}
const fileVersion = selectedFileVersion.value.versionNumber
const accFileViewName = revitViewName.value === '' ? undefined : revitViewName.value
const res = await createModel({
name: selectedFileVersion.value.name,
description: '',
projectId: props.projectId as string
})
await createAccSyncItem({
input: {
projectId: props.projectId as string,
modelId: res?.id as string,
accRegion: selectedHub.value?.attributes?.region as string,
accFileExtension: selectedFileVersion.value.fileType,
accHubId: selectedHubId.value!,
accProjectId: selectedProjectId.value as string,
accRootProjectFolderUrn: rootProjectFolderId.value!,
accFileLineageUrn: selectedFileId.value as string,
accFileName: selectedFileVersion.value.name,
accFileVersionIndex: fileVersion,
accFileVersionUrn: selectedFileVersion.value.id,
accFileViewName
}
})
} catch (error) {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Add sync item failed',
description: error instanceof Error ? error.message : 'Unexpected error'
})
} finally {
revitViewName.value = undefined
}
}
watch(tokens, (newTokens) => {
if (newTokens?.access_token) {
fetchHubs(newTokens?.access_token)
}
})
onMounted(async () => {
await tryGetTokensFromCookies()
if (tokens.value) {
fetchHubs(tokens.value.access_token)
}
})
watch(isOpen, (newVal) => {
if (newVal) {
selectedFileVersion.value = undefined
}
})
</script>
@@ -1,128 +0,0 @@
<template>
<div class="flex flex-row h-full overflow-hidden border rounded-lg bg-foundation">
<!-- Left Pane for tree -->
<div class="w-1/4 p-2 overflow-y-auto border-r">
<h3 class="font-semibold text-lg text-center">Folders</h3>
<hr class="mb-1" />
<div v-if="loadingTree" class="text-center text-foreground-2 py-2">
Loading folders...
<InfiniteLoading />
</div>
<ul v-else-if="folderTree && folderTree.children" class="space-y-1 pt-1">
<ProjectPageAccFolderNode
v-for="folder in folderTree.children"
:key="folder.id"
:folder="folder"
:on-select-folder="onFolderClick"
:selected-folder-id="selectedFolder?.id"
/>
</ul>
<div v-else class="text-center text-foreground-2 py-4">No folders found.</div>
</div>
<!-- Right Pane for content -->
<div class="w-3/4 p-2 overflow-y-auto">
<h3 class="font-semibold text-lg text-center">Files</h3>
<hr class="mb-1" />
<div
v-if="loadingItems || loadingTree"
class="text-center text-foreground-2 py-2"
>
<div v-if="selectedFolder">
<span>Loading files in</span>
<span class="font-bold">{{ ` ${selectedFolder.attributes.name} ` }}</span>
<span>folder.</span>
<InfiniteLoading />
</div>
<div v-else>Waiting for folder selection...</div>
</div>
<ul v-else-if="folderItems.length > 0" class="space-y-1">
<template v-for="item in folderItems" :key="item.id">
<li
class="flex items-center space-x-1 px-2 rounded-md transition-colors w-full"
:class="{
'bg-foundation-focus font-semibold': selectedFile?.id === item.id,
'hover:bg-primary-muted cursor-pointer': selectedFile?.id !== item.id
}"
>
<button
class="flex items-center space-x-1 p-1 rounded-md transition-colors w-full"
@click="onFileSelected(item)"
>
<span>
{{ item.attributes.name || item.attributes.displayName }}
</span>
</button>
</li>
<hr />
</template>
</ul>
<div v-else class="text-center text-foreground-2 py-2">
<div v-if="selectedFolder">
<span>No files found in</span>
<span class="font-bold">{{ ` ${selectedFolder.attributes.name} ` }}</span>
<span>folder.</span>
</div>
<span v-else>Select a folder to view files..</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { AccTokens } from '@speckle/shared/acc'
import { ref, watch } from 'vue'
import { useAcc } from '~/lib/acc/composables/useAcc'
import type { AccFolder, AccItem } from '~/lib/acc/composables/useAcc'
const props = defineProps<{
hubId: string
projectId: string
tokens: AccTokens | undefined
}>()
const emit = defineEmits(['file-selected'])
const {
loadingTree,
loadingItems,
folderTree,
folderItems,
fetchItemsForFolder,
init
} = useAcc()
const selectedFolder = ref<AccFolder | undefined>()
const selectedFile = ref<AccItem | undefined>()
const onFolderClick = async (folder: AccFolder) => {
selectedFolder.value = folder
selectedFile.value = undefined
await fetchItemsForFolder(folder.id, props.projectId, props.tokens!.access_token)
}
const onFileSelected = (item: AccItem) => {
selectedFile.value = item
emit('file-selected', item)
}
// Watch for changes in projectId to re-initialize the folder tree
watch(
() => props.projectId,
async (newProjectId) => {
selectedFolder.value = undefined
selectedFile.value = undefined
if (newProjectId && props.tokens) {
await init(props.hubId, newProjectId, props.tokens.access_token)
// Automatically select the first folder and fetch its files
if (
folderTree.value &&
folderTree.value.children &&
folderTree.value.children.length > 0
) {
await onFolderClick(folderTree.value.children[0])
}
}
},
{ immediate: true }
)
</script>
@@ -1,49 +0,0 @@
<template>
<li>
<button
class="flex items-center space-x-1 p-1 rounded-md transition-colors w-full"
:class="{
'bg-foundation-focus font-semibold': selectedFolderId === folder.id,
'hover:bg-primary-muted cursor-pointer': selectedFolderId !== folder.id
}"
@click="select(folder)"
>
<ChevronDownIcon
:class="`h-4 w-5 transition ${!isExpanded ? '-rotate-90' : 'rotate-0'}`"
@click.stop="isExpanded = !isExpanded"
/>
<span>{{ folder.attributes.name }}</span>
</button>
<ul
v-if="isExpanded && folder.children && folder.children.length > 0"
class="ml-4 mt-1 space-y-1"
>
<ProjectPageAccFolderNode
v-for="child in folder.children"
:key="child.id"
:folder="child"
:on-select-folder="onSelectFolder"
:selected-folder-id="selectedFolderId"
/>
</ul>
</li>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ChevronDownIcon } from '@heroicons/vue/24/outline'
import type { AccFolder } from '~/lib/acc/composables/useAcc'
const props = defineProps<{
folder: AccFolder
onSelectFolder: (folder: AccFolder) => void
selectedFolderId: string | undefined
}>()
const isExpanded = ref(false)
const select = (folder: AccFolder) => {
props.onSelectFolder(folder)
}
</script>
@@ -1,339 +0,0 @@
<template>
<div class="flex flex-col space-y-2">
<div class="flex text-body-xs text-foreground font-medium">Sync models</div>
<LayoutTable
class="bg-foundation"
:columns="[
{ id: 'status', header: 'Status', classes: 'col-span-2' },
{ id: 'accFileName', header: 'File name', classes: 'col-span-2' },
{ id: 'accFileViewName', header: 'View name', classes: 'col-span-2' },
{ id: 'modelId', header: 'Model id', classes: 'col-span-2' },
{ id: 'createdBy', header: 'Created by', classes: 'col-span-2' },
{ id: 'actions', header: 'Actions', classes: 'col-span-2' }
]"
:items="accSyncItems"
>
<template #status="{ item }">
<ProjectPageAccSyncStatus :status="item.status" />
</template>
<template #accFileName="{ item }">
{{ item.accFileName }}
</template>
<template #accFileViewName="{ item }">
{{ item.accFileViewName || '-' }}
</template>
<template #modelId="{ item }">
<NuxtLink
class="text-foreground-1 hover:text-blue-500 underline"
:to="`/projects/${projectId}/models/${item.modelId}`"
>
{{ item.modelId }}
</NuxtLink>
</template>
<template #createdBy="{ item }">
{{ item.author?.name }}
</template>
<template #actions="{ item }">
<div class="space-x-2">
<FormButton
hide-text
color="outline"
:icon-left="item.status === 'paused' ? PlayIcon : PauseIcon"
@click="handleStatusSyncItem(item.id, item.status === 'paused')"
/>
<FormButton
hide-text
color="outline"
:icon-left="TrashIcon"
@click="handleDeleteSyncItem(item.id)"
/>
</div>
</template>
</LayoutTable>
<FormButton
color="outline"
size="sm"
:disabled="!isLoggedIn"
:disabled-tooltip="'Log in required'"
@click="showNewSyncDialog = true"
>
<template #default>
<div v-tippy="isLoggedIn ? undefined : 'Log in required'">New sync</div>
</template>
</FormButton>
<LayoutDialog
v-model:open="showNewSyncDialog"
title="Create new sync"
@fully-closed="step = 0"
>
<div class="flex flex-col">
<div v-if="step === 0" class="space-y-2">
<ProjectPageAccHubs
:hubs="hubs"
:loading="loadingHubs"
@hub-selected="onHubClick"
/>
<ProjectPageAccProjects
v-if="selectedHubId"
:hub-id="selectedHubId"
:projects="projects"
:loading="loadingProjects"
@project-selected="onProjectClick"
/>
<ProjectPageAccFileSelector
v-if="selectedProjectId && selectedHubId && tokens"
:hub-id="selectedHubId"
:project-id="selectedProjectId"
:tokens="tokens"
@file-selected="onFileSelected"
/>
<FormTextInput
v-model="revitViewName"
name="revitFileViewName"
color="foundation"
label="Revit view name (Optional)"
show-label
:disabled="!selectedFile"
></FormTextInput>
<div class="flex flex-row justify-center mt-4 space-x-2">
<FormButton size="sm" color="outline" @click="showNewSyncDialog = false">
Cancel
</FormButton>
<FormButton size="sm" :disabled="!selectedFile" @click="step++">
Next
</FormButton>
</div>
</div>
<div v-if="step === 1" class="flex flex-col space-y-2">
<CommonAlert color="info" hide-icon>
<template #title>
Selected ACC file:
{{
selectedFile?.attributes.name || selectedFile?.attributes.displayName
}}
</template>
</CommonAlert>
<hr />
<ProjectPageAccModelSelector
:project-id="projectId"
:acc-sync-items="accSyncItems"
@model-selected="(model) => (selectedModel = model)"
/>
<hr />
<div class="flex flex-row justify-center space-x-2">
<FormButton size="sm" color="outline" @click="showNewSyncDialog = false">
Cancel
</FormButton>
<FormButton size="sm" :disabled="!selectedModel" @click="addSync">
Add
</FormButton>
</div>
</div>
</div>
</LayoutDialog>
</div>
</template>
<script setup lang="ts">
import type { AccTokens, AccHub, AccItem } from '@speckle/shared/acc'
import { ref, computed, watch } from 'vue'
import type { ProjectPageLatestItemsModelItemFragment } from '~/lib/common/generated/gql/graphql'
import { useMutation, useQuery, useSubscription } from '@vue/apollo-composable'
import {
accSyncItemCreateMutation,
accSyncItemDeleteMutation,
accSyncItemUpdateMutation
} from '~/lib/acc/graphql/mutations'
import { projectAccSyncItemsQuery } from '~/lib/acc/graphql/queries'
import { onProjectAccSyncItemUpdatedSubscription } from '~/lib/acc/graphql/subscriptions'
import { PauseIcon } from '@heroicons/vue/24/solid'
import { TrashIcon, PlayIcon } from '@heroicons/vue/24/outline'
import type { AccFolder } from '~/lib/acc/composables/useAcc'
import { useAcc } from '~/lib/acc/composables/useAcc'
const props = defineProps<{
projectId: string
tokens: AccTokens | undefined
isLoggedIn: boolean
}>()
const step = ref(0)
const showNewSyncDialog = ref(false)
const { triggerNotification } = useGlobalToast()
const tokens = computed(() => props.tokens)
const selectedHub = ref<AccHub | null>(null)
const selectedHubId = ref<string | null>(null)
const selectedProjectId = ref<string | null>(null)
const revitViewName = ref<string>()
const selectedModel = ref<ProjectPageLatestItemsModelItemFragment>()
const selectedFolder = ref<AccFolder | undefined>()
const selectedFile = ref<AccItem | undefined>()
// Use the composable to get the state and functions
const {
hubs,
fetchHubs,
loadingHubs,
projects,
fetchProjects,
loadingProjects,
folderTree,
fetchItemsForFolder,
rootProjectFolderId,
init
} = useAcc()
const onFileSelected = (item: AccItem) => {
selectedFile.value = item
}
const { result: accSyncItemsResult, refetch: refetchAccSyncItems } = useQuery(
projectAccSyncItemsQuery,
() => ({
id: props.projectId
})
)
const accSyncItems = computed(
() => accSyncItemsResult.value?.project.accSyncItems.items || []
)
const { onResult: onProjectAccSyncItemsUpdated } = useSubscription(
onProjectAccSyncItemUpdatedSubscription,
() => ({
id: props.projectId
})
)
onProjectAccSyncItemsUpdated((res) => {
// TODO ACC: Mutate local cache instead of refetch
refetchAccSyncItems()
triggerNotification({
type: ToastNotificationType.Info,
title: `ACC sync model ${res.data?.projectAccSyncItemsUpdated.type.toLowerCase()}`,
description: res.data?.projectAccSyncItemsUpdated.accSyncItem?.accFileName
})
})
const onHubClick = async (hub: AccHub) => {
selectedHub.value = hub
selectedHubId.value = hub.id
await fetchProjects(hub.id, tokens.value!.access_token)
}
// Refactored onProjectClick to use the composable's init function
const onProjectClick = async (hubId: string, projectId: string) => {
selectedFolder.value = undefined
selectedFile.value = undefined
selectedProjectId.value = projectId
await init(hubId, projectId, tokens.value!.access_token)
// defaulting to first
if (folderTree.value && folderTree.value.children) {
selectedFolder.value = folderTree.value?.children[0]
await fetchItemsForFolder(
selectedFolder.value.id,
selectedProjectId.value,
tokens.value!.access_token
)
}
}
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(selectedFile.value?.latestVersionId?.split('?')[1]).get(
'version'
)
)
const accFileViewName = revitViewName.value === '' ? undefined : revitViewName.value
await createAccSyncItem({
input: {
projectId: props.projectId,
modelId: selectedModel.value?.id as string,
accRegion: selectedHub.value?.attributes?.region as string,
accFileExtension: selectedFile.value?.fileExtension as string,
accHubId: selectedHubId.value!,
accProjectId: selectedProjectId.value as string,
accRootProjectFolderUrn: rootProjectFolderId.value!,
accFileLineageUrn: selectedFile.value?.id as string,
accFileName: (selectedFile.value?.attributes.displayName ||
selectedFile.value?.attributes.name) as string,
accFileVersionIndex: fileVersion,
accFileVersionUrn: selectedFile.value?.latestVersionId as string,
accFileViewName
}
})
} catch (error) {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Add sync item failed',
description: error instanceof Error ? error.message : 'Unexpected error'
})
} finally {
revitViewName.value = undefined
showNewSyncDialog.value = false
step.value = 0
}
}
const { mutate: deleteAccSyncItem } = useMutation(accSyncItemDeleteMutation)
const handleDeleteSyncItem = async (id: string) => {
try {
await deleteAccSyncItem({
input: {
projectId: props.projectId,
id
}
})
} catch (error) {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Delete sync item failed',
description: error instanceof Error ? error.message : 'Unexpected error'
})
}
}
const { mutate: updateAccSyncItem } = useMutation(accSyncItemUpdateMutation)
const handleStatusSyncItem = async (id: string, isPaused: boolean) => {
try {
await updateAccSyncItem({
input: {
projectId: props.projectId,
id,
status: isPaused ? 'pending' : 'paused'
}
})
} catch (error) {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Update sync item failed',
description: error instanceof Error ? error.message : 'Unexpected error'
})
}
}
watch(tokens, (newTokens) => {
if (newTokens?.access_token) {
fetchHubs(newTokens?.access_token)
}
})
</script>
@@ -1,152 +0,0 @@
<template>
<div class="flex flex-col text-xs space-y-2">
<ProjectPageAccSyncs
:project-id="projectId"
:is-logged-in="hasTokens"
:tokens="tokens"
/>
<ClientOnly>
<div v-if="!hasTokens">
<CommonLoadingBar v-if="loadingTokens" :loading="true" class="my-2" />
<div v-else>
<hr class="mb-2" />
<FormButton size="sm" @click="authAcc()">Connect to ACC</FormButton>
</div>
</div>
</ClientOnly>
<!-- USER INFO -->
<div v-if="userInfo" class="flex flex-col space-y-2">
<hr class="my-2" />
<div class="flex flex-col text ml-1 space-y-2 mb-2">
<span>
<strong>Name:</strong>
{{ userInfo.firstName }} {{ userInfo.lastName }}
</span>
<span>
<strong>Email:</strong>
{{ userInfo.emailId }}
</span>
<span>
<strong>User ID:</strong>
{{ userInfo.userId }}
</span>
</div>
<!-- <div v-if="tokens?.access_token" class="flex flex-row items-center space-x-2">
<FormButton
class="mr-2"
hide-text
:icon-left="DocumentDuplicateIcon"
color="outline"
@click="copy(tokens?.access_token)"
>
Copy to clipboard
</FormButton>
{{ tokens?.access_token.slice(0, 32) }}...
</div> -->
</div>
</div>
</template>
<script setup lang="ts">
import type { AccTokens, AccUserInfo } from '@speckle/shared/acc'
// import { DocumentDuplicateIcon } from '@heroicons/vue/24/outline'
const props = defineProps<{ projectId: string }>()
const { triggerNotification } = useGlobalToast()
// const { copy } = useClipboard()
const apiOrigin = useApiOrigin()
const tokens = ref<AccTokens>()
const hasTokens = computed(() => !!tokens.value?.access_token)
const loadingTokens = ref(true)
const userInfo = ref<AccUserInfo>()
const loadingUser = ref(false)
// AUTH + TOKEN FLOW
const fetchTokens = async () => {
try {
const res = await fetch(`${apiOrigin}/api/v1/acc/auth/status`, {
credentials: 'include'
})
if (!res.ok) return
tokens.value = await res.json()
} finally {
loadingTokens.value = false
}
}
fetchTokens()
const authAcc = async () => {
try {
const response = await fetch(`${apiOrigin}/api/v1/acc/auth/login`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectId: props.projectId })
})
if (!response.ok) throw new Error('Failed to initiate ACC login.')
const { authorizeUrl } = await response.json()
if (!authorizeUrl) throw new Error('No authorize URL returned by server.')
window.location.href = authorizeUrl
} catch (error) {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Error starting ACC login',
description: error instanceof Error ? error.message : 'Unexpected error'
})
}
}
const scheduleRefresh = (expiresInSeconds: number) => {
const refreshTime = (expiresInSeconds - 60) * 1000
setTimeout(async () => {
loadingTokens.value = true
const res = await fetch(`${apiOrigin}/api/v1/acc/auth/refresh`, {
method: 'POST',
credentials: 'include'
})
if (res.ok) {
const refreshed = await res.json()
await fetchTokens()
triggerNotification({
type: ToastNotificationType.Success,
title: 'ACC tokens refreshed',
description: refreshed
})
scheduleRefresh(refreshed.expires_in)
}
loadingTokens.value = false
}, refreshTime)
}
watch(tokens, (newTokens) => {
if (newTokens?.expires_in) scheduleRefresh(newTokens.expires_in)
if (newTokens?.access_token) {
fetchUserInfo()
}
})
// USER INFO
const fetchUserInfo = async () => {
loadingUser.value = true
try {
const res = await fetch(
'https://developer.api.autodesk.com/userprofile/v1/users/@me',
{ headers: { Authorization: `Bearer ${tokens.value!.access_token}` } }
)
if (!res.ok) throw new Error('Failed to get user info directly from ACC')
userInfo.value = await res.json()
} catch (error) {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Error fetching user info directly',
description: error instanceof Error ? error.message : 'Unexpected error'
})
} finally {
loadingUser.value = false
}
}
</script>
@@ -26,6 +26,18 @@
New model
</FormButton>
</div>
<!-- I believe for now sync limits corralate with model limit since new sync creates new model, once we have limits for syncs, this should change -->
<div
v-tippy="canCreateModel.cantClickCreateReason.value"
class="grow inline-flex sm:grow-0 lg:hidden"
>
<FormButton
:disabled="!canCreateModel.canClickCreate.value"
@click="showNewAccSync = true"
>
New sync
</FormButton>
</div>
</div>
</div>
<div
@@ -78,7 +90,23 @@
>
View all in 3D
</FormButton>
<div v-tippy="canCreateModel.cantClickCreateReason.value" class="test123">
<LayoutMenu
v-if="showAccIntegration"
v-model:open="showMenu"
:items="menuItems"
:menu-position="HorizontalDirection.Left"
:menu-id="menuId"
@click.stop.prevent
@chosen="onActionChosen"
>
<FormButton color="primary" @click="showMenu = !showMenu">
<div class="flex items-center gap-1">
Add model
<ChevronDownIcon class="h-3 w-3" />
</div>
</FormButton>
</LayoutMenu>
<div v-else v-tippy="canCreateModel.cantClickCreateReason.value">
<FormButton
:disabled="!canCreateModel.canClickCreate.value"
class="hidden lg:inline-flex shrink-0"
@@ -91,9 +119,11 @@
</div>
</div>
<ProjectModelsAdd v-model:open="showNewDialog" :project="project" />
<IntegrationsAccDialogCreateSync :open="showNewAccSync" :project-id="project?.id" />
</div>
</template>
<script setup lang="ts">
import { ChevronDownIcon } from '@heroicons/vue/24/outline'
import { SourceApps, SpeckleViewer } from '@speckle/shared'
import type { SourceAppDefinition } from '@speckle/shared'
import { debounce } from 'lodash-es'
@@ -103,9 +133,13 @@ import type {
ProjectModelsPageHeader_ProjectFragment
} from '~~/lib/common/generated/gql/graphql'
import { modelRoute } from '~~/lib/common/helpers/route'
import type { GridListToggleValue } from '~~/lib/layout/helpers/components'
import type {
GridListToggleValue,
LayoutMenuItem
} from '~~/lib/layout/helpers/components'
import { useMixpanel } from '~~/lib/core/composables/mp'
import { useCanCreateModel } from '~/lib/projects/composables/permissions'
import { HorizontalDirection } from '@speckle/ui-components'
const emit = defineEmits<{
(e: 'update:selected-members', val: FormUsersSelectItemFragment[]): void
@@ -147,6 +181,9 @@ graphql(`
canCreateModel {
...FullPermissionCheckResult
}
canReadAccIntegrationSettings {
...FullPermissionCheckResult
}
}
...ProjectModelsAdd_Project
}
@@ -168,6 +205,8 @@ const sourceAppsBtnId = useId()
const router = useRouter()
const mp = useMixpanel()
const menuId = useId()
const onViewAllClick = () => {
router.push(allModelsRoute.value)
@@ -180,6 +219,11 @@ const onViewAllClick = () => {
}
const showNewDialog = ref(false)
const showNewAccSync = ref(false)
const showAccIntegration = computed(
() => props.project?.permissions.canReadAccIntegrationSettings.authorized
)
const canCreateModel = useCanCreateModel({
project: computed(() => props.project)
@@ -217,6 +261,46 @@ const allModelsRoute = computed(() => {
return modelRoute(props.projectId, resourceIdString)
})
const showMenu = ref(false)
enum AddNewModelActionTypes {
NewModel = 'new-model',
NewAccSyncItem = 'new-acc-sync-item'
}
const menuItems = computed<LayoutMenuItem[][]>(() => [
[
{
title: 'Create new model...',
id: AddNewModelActionTypes.NewModel,
disabled: !canCreateModel.canClickCreate.value,
disabledTooltip: canCreateModel.cantClickCreateReason.value
},
// TODO ACC: Upload a file
{
// TODO: Do we show this disabled in all non-enterprise cases?
title: 'Sync from ACC...',
id: AddNewModelActionTypes.NewAccSyncItem,
// I believe for now sync limits corralate with model limit since new sync creates new model, once we have limits for syncs, this should change
disabled: !canCreateModel.canClickCreate.value,
disabledTooltip: canCreateModel.cantClickCreateReason.value
}
]
])
const onActionChosen = (params: { item: LayoutMenuItem; event: MouseEvent }) => {
const { item } = params
switch (item.id) {
case AddNewModelActionTypes.NewModel:
handleCreateModelClick()
break
case AddNewModelActionTypes.NewAccSyncItem:
showNewAccSync.value = true
break
}
}
const team = computed(() => props.project?.team.map((t) => t.user) || [])
const updateDebouncedSearch = debounce(() => {
@@ -55,8 +55,12 @@
submodel
</FormButton>
</div>
<div v-if="accSyncItem" class="flex items-center ml-2">
<IntegrationsAccSyncStatusModelItem :item="accSyncItem" />
</div>
<!-- Spacer -->
<div class="flex-grow"></div>
<template v-if="!isPendingFileUpload(item)">
<div
v-show="
@@ -308,6 +312,9 @@ graphql(`
...ProjectPageLatestItemsModelItem
...ProjectCardImportFileArea_Model
...ProjectPageModelsCard_Model
accSyncItem {
...SyncStatusModelItem_AccSyncItem
}
}
hasChildren
updatedAt
@@ -332,6 +339,10 @@ const props = defineProps<{
const router = useRouter()
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
const accSyncItem = computed(() =>
props.item.__typename === 'ModelsTreeItem' ? props.item.model?.accSyncItem : undefined
)
const importArea = ref(
null as Nullable<{
triggerPicker: () => void
@@ -10,6 +10,7 @@
:disabled="loading"
class="z-[1] relative"
/>
<ProjectPageModelsResults
v-model:grid-or-list="gridOrList"
v-model:search="search"
@@ -0,0 +1,64 @@
<template>
<div class="flex flex-col text-xs space-y-2">
<IntegrationsAccSyncs
:project-id="projectId"
:is-logged-in="hasTokens"
:tokens="tokens"
/>
<ClientOnly>
<div v-if="!hasTokens">
<CommonLoadingBar v-if="loadingTokens" :loading="true" class="my-2" />
<div v-else>
<hr class="mb-2" />
<FormButton size="sm" @click="authAcc(`/projects/${projectId}/acc`)">
Connect to ACC
</FormButton>
</div>
</div>
<!-- USER INFO -->
<div v-if="userInfo" class="flex flex-col space-y-2">
<hr class="my-2" />
<div class="flex flex-col text ml-1 space-y-2 mb-2">
<span>
<strong>Name:</strong>
{{ userInfo.firstName }} {{ userInfo.lastName }}
</span>
<span>
<strong>Email:</strong>
{{ userInfo.emailId }}
</span>
<span>
<strong>User ID:</strong>
{{ userInfo.userId }}
</span>
</div>
</div>
</ClientOnly>
</div>
</template>
<script setup lang="ts">
import { useAccAuthManager } from '~/lib/acc/composables/useAccAuthManager'
import { useAccUser } from '~/lib/acc/composables/useAccUser'
defineProps<{ projectId: string }>()
const hasTokens = computed(() => !!tokens.value?.access_token)
const { tokens, loadingTokens, authAcc, tryGetTokensFromCookies } = useAccAuthManager()
const { userInfo, fetchUserInfo } = useAccUser()
watch(tokens, async (newTokens) => {
if (newTokens?.access_token) {
await fetchUserInfo(newTokens?.access_token)
}
})
onMounted(async () => {
await tryGetTokensFromCookies()
if (tokens.value) {
await fetchUserInfo(tokens.value?.access_token)
}
})
</script>
@@ -1,272 +0,0 @@
import type { AccHub, AccProject } from '@speckle/shared/acc'
import { ref } from 'vue'
// Placeholder types for demonstration. You should use your actual types.
export interface AccItem {
id: string
type: string
attributes: {
name: string
displayName: string
fileType?: string
objectCount?: number
}
latestVersionId?: string
fileExtension?: string
storageUrn?: string | null
}
export interface AccFolder extends AccItem {
children?: AccFolder[]
}
/**
* A composable function to handle ACC data fetching and state management.
* The project details are passed to the `init` function and exptects to refresh all state when user selected new project.
*/
export function useAcc() {
const loadingTree = ref<boolean>(false)
const loadingItems = ref<boolean>(false)
const loadingHubs = ref<boolean>(false)
const loadingProjects = ref<boolean>(false)
const folderTree = ref<AccFolder | undefined>()
const folderItems = ref<AccItem[]>([])
const hubs = ref<AccHub[]>([])
const projects = ref<AccProject[]>([])
const rootProjectFolderId = ref<string | undefined>()
const supportedFileExtensions = ['rvt']
const logger = useLogger()
// ACC API Functions
/**
* Fetches all hubs for the authenticated user.
*/
const fetchHubs = async (token: string) => {
loadingHubs.value = true
try {
const res = await fetch('https://developer.api.autodesk.com/project/v1/hubs', {
headers: { Authorization: `Bearer ${token}` }
})
if (!res.ok) throw new Error('Failed to fetch hubs')
hubs.value = (await res.json()).data
} catch (error) {
logger.error(error, 'Error fetching ACC hubs')
hubs.value = []
} finally {
loadingHubs.value = false
}
}
/**
* Fetches all projects for a given hub.
*/
const fetchProjects = async (hubId: string, token: string) => {
loadingProjects.value = true
try {
const res = await fetch(
`https://developer.api.autodesk.com/project/v1/hubs/${hubId}/projects`,
{ headers: { Authorization: `Bearer ${token}` } }
)
if (!res.ok) throw new Error('Failed to fetch projects')
projects.value = (await res.json()).data
} catch (error) {
logger.error(error, 'Error fetching ACC projects')
projects.value = []
} finally {
loadingProjects.value = false
}
}
/**
* Fetches the root folder ID for the project.
*/
const getProjectRootFolderId = async (
hubId: string,
projectId: string,
token: string
): Promise<string | undefined> => {
try {
const res = await fetch(
`https://developer.api.autodesk.com/project/v1/hubs/${hubId}/projects/${projectId}`,
{ headers: { Authorization: `Bearer ${token}` } }
)
if (!res.ok) throw new Error('Failed to get project details')
const r = await res.json()
rootProjectFolderId.value = r.data.relationships?.rootFolder?.data?.id || null
return rootProjectFolderId.value
} catch (error) {
logger.error(error, `Error getting root folder ID for project: ${projectId}`)
return undefined
}
}
/**
* Fetches the immediate contents (folders and items) of a single folder.
* This is a non-recursive, single-level fetch.
*/
const fetchFolderContents = async (
projectId: string,
folderId: string,
token: string
): Promise<AccItem[]> => {
try {
const res = await fetch(
`https://developer.api.autodesk.com/data/v1/projects/${projectId}/folders/${folderId}/contents`,
{ headers: { Authorization: `Bearer ${token}` } }
)
if (!res.ok) {
throw new Error(`Failed to fetch contents of folder ${folderId}`)
}
const data = (await res.json()).data
return data
} catch (error) {
logger.error(error, `Error fetching folder contents for ${folderId}:`)
return []
}
}
/**
* Fetches the latest version details for a specific item (file).
* This function is separated for on-demand use.
*/
const fetchItemLatestVersion = async (
projectId: string,
itemId: string,
token: string
) => {
try {
const res = await fetch(
`https://developer.api.autodesk.com/data/v1/projects/${projectId}/items/${itemId}/tip`,
{ headers: { Authorization: `Bearer ${token}` } }
)
if (!res.ok) {
throw new Error(`Failed to fetch latest version for item ${itemId}`)
}
const data = (await res.json()).data
return data
} catch (error) {
logger.error(error, `Error fetching latest version for item ${itemId}`)
return null
}
}
// Application Logic
/**
* Builds the nested folder tree structure on initial project load.
* This is a recursive function that only fetches folders, not files.
* It now uses `attributes.objectCount` to avoid unnecessary API calls for empty folders.
*/
const buildFolderTree = async (
projectId: string,
folderId: string,
token: string
): Promise<AccFolder> => {
const contents = await fetchFolderContents(projectId, folderId, token)
const folders = contents.filter((item) => item.type === 'folders') as AccFolder[]
const populatedFolders: AccFolder[] = []
for (const folder of folders) {
// We only want to add a folder to the tree if it contains something.
// The `objectCount` attribute tells us if it's empty.
if (folder.attributes.objectCount && folder.attributes.objectCount > 0) {
// Recursively build the full subtree for this folder
const subTree = await buildFolderTree(projectId, folder.id, token)
populatedFolders.push({
id: folder.id,
type: folder.type,
attributes: {
name: folder.attributes.name || folder.attributes.displayName,
displayName: folder.attributes.displayName
},
children: subTree.children
})
}
}
const folderTree = {
id: folderId,
type: 'folders',
attributes: { name: 'Root Folder', displayName: 'Root Folder' },
relationships: {},
children: populatedFolders
} as AccFolder
return folderTree
}
/**
* Fetches all items (files) for a specific folder when a user clicks on it.
*/
const fetchItemsForFolder = async (
folderId: string,
projectId: string,
token: string
) => {
loadingItems.value = true
folderItems.value = [] // Clear previous items
const contents = await fetchFolderContents(projectId, folderId, token)
const items = contents.filter((item) => item.type === 'items') as AccItem[] // items === files
const itemPromises = items.map(async (item) => {
const version = await fetchItemLatestVersion(projectId, item.id, token)
if (version) {
const storageUrn = version.relationships?.storage?.data?.id || null
return {
...item,
latestVersionId: version.id,
fileExtension: version.attributes.fileType,
storageUrn
}
}
return item
})
folderItems.value = (await Promise.all(itemPromises)).filter((item) =>
supportedFileExtensions.includes(item.fileExtension)
)
loadingItems.value = false
}
/**
* Main entry point to initialize the folder tree for the selected project.
*/
const init = async (hubId: string, projectId: string, token: string) => {
loadingTree.value = true
folderItems.value = []
folderTree.value = undefined
rootProjectFolderId.value = undefined
try {
const rootFolderId = await getProjectRootFolderId(hubId, projectId, token)
if (rootFolderId) {
folderTree.value = await buildFolderTree(projectId, rootFolderId, token)
}
} catch (error) {
logger.error(error, 'Failed to initialize Autodesk ACC composable')
} finally {
loadingTree.value = false
}
}
return {
loadingTree,
loadingItems,
loadingHubs,
loadingProjects,
folderTree,
folderItems,
hubs,
projects,
rootProjectFolderId,
fetchHubs,
fetchProjects,
fetchItemsForFolder,
init
}
}
@@ -0,0 +1,177 @@
import type { AccTokens } from '@speckle/shared/acc'
import Cookies from 'js-cookie'
/**
* Manages authentication logic of ACC.
* We store tokens and its timestamp in under `acc_tokens` cookie.
* Detection of "Refresh needed" happens with timestamp check.
* ACC auth logic returns only expires in seconds and we need to correlate it with timestamp to substract later to understand refresh needed or not.
* Token lifespans:
* - Bearer token: 60 minutes
* - Refresh token: 15 days
*/
export function useAccAuthManager() {
const ACC_COOKIE_KEY = 'acc_tokens'
const logger = useLogger()
const apiOrigin = useApiOrigin()
const { triggerNotification } = useGlobalToast()
const loadingTokens = ref(false)
const tokens = ref<AccTokens>()
const isExpired = ref(false)
const REFRESH_TOKEN_LIFESPAN = 15 * 24 * 60 * 60 // in seconds
/**
* Main logic to understand existing token in cookies is expired or not.
* If refresh needed, we refresh and schedule
* Otherwise, we calculate the time diff and schedule refresh accordingly
*/
const tryGetTokensFromCookies = async () => {
const accTokens = Cookies.get(ACC_COOKIE_KEY)
if (accTokens) {
logger.info('Acc tokens are found in cookies')
const tokensInCookies = JSON.parse(accTokens) as AccTokens
const timeDiff = (Date.now() - tokensInCookies.timestamp) / 1000 // in seconds
if (timeDiff > REFRESH_TOKEN_LIFESPAN) {
logger.info('Acc refresh token in cookies is expired')
isExpired.value = true
logOut()
} else if (timeDiff + 300 > tokensInCookies.expires_in) {
logger.info('Acc access token in cookies need refreshing')
// 300s (6min) is arbitrary guard
const refreshedTokens = await refreshTokens(tokensInCookies)
tokens.value = refreshedTokens
await saveTokensToCookies()
if (tokens.value) scheduleRefresh(tokens.value)
} else {
logger.info('Acc tokens in cookies still valid')
tokens.value = tokensInCookies
const remainingTime = tokensInCookies.expires_in - timeDiff
scheduleRefresh(tokens.value, remainingTime)
}
}
loadingTokens.value = false
}
const logOut = () => {
tokens.value = undefined
Cookies.remove(ACC_COOKIE_KEY)
}
const refreshTokens = async (tokensToRefresh: AccTokens) => {
try {
loadingTokens.value = true
const res = await fetch(`${apiOrigin}/api/v1/acc/auth/refresh`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(tokensToRefresh)
})
if (res.ok) {
const refreshedTokens = await res.json()
tokens.value = { ...refreshedTokens, timestamp: Date.now() }
return refreshedTokens
}
} catch (error) {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Error on refreshing ACC credientials',
description: error instanceof Error ? error.message : 'Unexpected error'
})
} finally {
loadingTokens.value = false
}
}
const saveTokensToCookies = async () => {
const tokensWithTimestamp = { ...tokens.value, timestamp: Date.now() }
Cookies.set('acc_tokens', JSON.stringify(tokensWithTimestamp), {
expires: 30, // since acc refresh token lifespan 15 days, it is a safe expiration
secure: true,
sameSite: 'Strict'
})
isExpired.value = false
}
const fetchTokens = async () => {
try {
loadingTokens.value = true
const res = await fetch(`${apiOrigin}/api/v1/acc/auth/status`, {
credentials: 'include'
})
if (!res.ok) return
tokens.value = await res.json()
if (tokens.value?.expires_in) {
scheduleRefresh(tokens.value)
}
await saveTokensToCookies()
} finally {
loadingTokens.value = false
}
}
const authAcc = async (callbackEndpoint: string) => {
try {
const response = await fetch(`${apiOrigin}/api/v1/acc/auth/login`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ callbackEndpoint })
})
if (!response.ok) throw new Error('Failed to initiate ACC login.')
const { authorizeUrl } = await response.json()
if (!authorizeUrl) throw new Error('No authorize URL returned by server.')
window.location.href = authorizeUrl
} catch (error) {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Error starting ACC login',
description: error instanceof Error ? error.message : 'Unexpected error'
})
}
}
const scheduleRefresh = (
tokensToScheduleRefresh: AccTokens,
resfreshInSeconds?: number
) => {
const refreshTimeInMs =
(resfreshInSeconds ?? tokensToScheduleRefresh.expires_in) * 1000
setTimeout(async () => {
loadingTokens.value = true
const res = await fetch(`${apiOrigin}/api/v1/acc/auth/refresh`, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(tokensToScheduleRefresh)
})
if (res.ok) {
const refreshed = await res.json()
tokens.value = refreshed
// triggerNotification({
// type: ToastNotificationType.Success,
// title: 'ACC tokens refreshed'
// })
await saveTokensToCookies()
scheduleRefresh(refreshed, refreshed.expires_in)
}
loadingTokens.value = false
}, refreshTimeInMs)
}
return {
isExpired,
tokens,
loadingTokens,
authAcc,
logOut,
fetchTokens,
refreshTokens,
tryGetTokensFromCookies,
saveTokensToCookies
}
}
@@ -0,0 +1,179 @@
import type { AccHub, AccProject } from '@speckle/shared/acc'
import { ref } from 'vue'
// Placeholder types for demonstration. You should use your actual types.
export interface AccItem {
id: string
type: string
attributes: {
name: string
displayName: string
fileType?: string
objectCount?: number
}
latestVersionId?: string
fileExtension?: string
storageUrn?: string | null
}
export interface AccFolder extends AccItem {
children?: AccFolder[]
}
export type AccItemVersion = {
id: string
name: string
fileType?: string
versionNumber: number
}
/**
* A composable function to handle ACC data fetching and state management.
* The project details are passed to the `init` function and exptects to refresh all state when user selected new project.
*/
export function useAcc() {
const loadingTree = ref<boolean>(false)
const loadingItems = ref<boolean>(false)
const loadingHubs = ref<boolean>(false)
const loadingProjects = ref<boolean>(false)
const folderTree = ref<AccFolder | undefined>()
const folderItems = ref<AccItem[]>([])
const hubs = ref<AccHub[]>([])
const projects = ref<AccProject[]>([])
const rootProjectFolderId = ref<string | undefined>()
const logger = useLogger()
// ACC API Functions
/**
* Fetches all hubs for the authenticated user.
*/
const fetchHubs = async (token: string) => {
loadingHubs.value = true
try {
const res = await fetch('https://developer.api.autodesk.com/project/v1/hubs', {
headers: { Authorization: `Bearer ${token}` }
})
if (!res.ok) throw new Error('Failed to fetch hubs')
hubs.value = (await res.json()).data
} catch (error) {
logger.error(error, 'Error fetching ACC hubs')
hubs.value = []
} finally {
loadingHubs.value = false
}
}
// type HubsResponse = {
// data: {
// id: string
// type: 'hubs'
// attributes: {
// name: string
// region: string
// }
// }[]
// }
/**
* Fetches all projects for a given hub.
*/
const fetchProjects = async (hubId: string, token: string) => {
loadingProjects.value = true
try {
const res = await fetch(
`https://developer.api.autodesk.com/project/v1/hubs/${hubId}/projects`,
{ headers: { Authorization: `Bearer ${token}` } }
)
if (!res.ok) throw new Error('Failed to fetch projects')
projects.value = (await res.json()).data
} catch (error) {
logger.error(error, 'Error fetching ACC projects')
projects.value = []
} finally {
loadingProjects.value = false
}
}
// type ProjectsResponse = {
// data: {
// id: string
// type: 'projects'
// attributes: {
// name: string
// }
// relationships: {
// hub: {
// data: {
// id: string
// type: string
// }
// }
// rootFolder: {
// data: {
// id: string
// }
// }
// }
// }[]
// }
/**
* Fetches the root folder ID for the project.
*/
const getProjectRootFolderId = async (
hubId: string,
projectId: string,
token: string
): Promise<string | undefined> => {
try {
const res = await fetch(
`https://developer.api.autodesk.com/project/v1/hubs/${hubId}/projects/${projectId}`,
{ headers: { Authorization: `Bearer ${token}` } }
)
if (!res.ok) throw new Error('Failed to get project details')
const r = await res.json()
rootProjectFolderId.value = r.data.relationships?.rootFolder?.data?.id || null
return rootProjectFolderId.value
} catch (error) {
logger.error(error, `Error getting root folder ID for project: ${projectId}`)
return undefined
}
}
/**
* Main entry point to initialize the folder tree for the selected project.
*/
const init = async (hubId: string, projectId: string, token: string) => {
loadingTree.value = true
folderItems.value = []
folderTree.value = undefined
rootProjectFolderId.value = undefined
try {
await getProjectRootFolderId(hubId, projectId, token)
} catch (error) {
logger.error(error, 'Failed to initialize Autodesk ACC composable')
} finally {
loadingTree.value = false
}
}
return {
loadingTree,
loadingItems,
loadingHubs,
loadingProjects,
folderTree,
folderItems,
hubs,
projects,
rootProjectFolderId,
fetchHubs,
fetchProjects,
init
}
}
@@ -0,0 +1,81 @@
import { gql } from '@apollo/client/core'
import type { AccTokens } from '@speckle/shared/acc'
import { useApolloClient, useQuery } from '@vue/apollo-composable'
import { accFolderDataQuery } from '~/lib/acc/graphql/queries'
import type { AccIntegrationFolderNode_AccFolderFragment } from '~/lib/common/generated/gql/graphql'
import { useActiveWorkspaceSlug } from '~/lib/user/composables/activeWorkspace'
export const useAccFolder = (
accProjectId: string,
accFolderId: MaybeRef<string | undefined>,
accTokens?: AccTokens
) => {
const workspaceSlug = useActiveWorkspaceSlug()
const apollo = useApolloClient()
const cachedFolder = computed(() => {
return apollo.client.cache.readFragment<AccIntegrationFolderNode_AccFolderFragment>(
{
id: `AccFolder:${unref(accFolderId)}`,
fragment: gql`
fragment AccFolderContents on AccFolder {
id
name
contents {
items {
id
name
latestVersion {
id
name
versionNumber
fileType
}
}
}
children {
items {
id
name
}
}
}
`
}
)
})
// watch(cachedFolder, (v) => {
// console.log({ cachedFolder: v })
// }, {
// immediate: true
// })
const { result: folder } = useQuery(
accFolderDataQuery,
() => ({
workspaceSlug: workspaceSlug.value!,
accToken: accTokens!.access_token,
accProjectId,
accFolderId: unref(accFolderId)!
}),
() => ({
enabled: !!unref(accFolderId) && !!accTokens && !!workspaceSlug.value
})
)
// watch(folder, (v) => {
// console.log({ queryFolder: v })
// }, {
// immediate: true
// })
const folderData = computed(() => ({
id: accFolderId,
...cachedFolder.value,
...folder.value?.workspaceBySlug.integrations?.acc?.folder
}))
return folderData
}
@@ -0,0 +1,34 @@
import type { AccUserInfo } from '@speckle/shared/acc'
export function useAccUser() {
const { triggerNotification } = useGlobalToast()
const loadingUser = ref(false)
const userInfo = ref<AccUserInfo>()
const fetchUserInfo = async (token: string) => {
loadingUser.value = true
try {
const res = await fetch(
'https://developer.api.autodesk.com/userprofile/v1/users/@me',
{ headers: { Authorization: `Bearer ${token}` } }
)
if (!res.ok) throw new Error('Failed to get user info directly from ACC')
userInfo.value = await res.json()
} catch (error) {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Error fetching user info directly',
description: error instanceof Error ? error.message : 'Unexpected error'
})
} finally {
loadingUser.value = false
}
}
return {
loadingUser,
userInfo,
fetchUserInfo
}
}
@@ -3,8 +3,12 @@ import { graphql } from '~~/lib/common/generated/gql'
export const projectAccSyncItemFragment = graphql(`
fragment ProjectAccSyncItem on AccSyncItem {
id
projectId
modelId
project {
id
}
model {
id
}
accRegion
accHubId
accProjectId
@@ -11,3 +11,24 @@ export const projectAccSyncItemsQuery = graphql(`
}
}
`)
export const accFolderDataQuery = graphql(`
query AccFolderData(
$workspaceSlug: String!
$accToken: String!
$accProjectId: String!
$accFolderId: String!
) {
workspaceBySlug(slug: $workspaceSlug) {
id
integrations {
acc(token: $accToken) {
folder(projectId: $accProjectId, folderId: $accFolderId) {
id
...AccIntegrationFolderNode_AccFolder
}
}
}
}
}
`)
+12
View File
@@ -1,4 +1,16 @@
import type { AccHub, AccItem } from '@speckle/shared/acc'
import type { Integration } from '~/lib/integrations/types'
import accLogo from '~/assets/images/integrations/acc.png'
export const AccIntegration: Integration = {
cookieKey: 'acc_tokens',
name: 'Autodesk Construction Cloud',
description: 'Sync your files in ACC into Speckle.',
logo: accLogo,
connected: false,
enabled: false,
status: 'notConnected'
}
// TODO ACC: Replace with type information inferred from gql queries, if possible
export type AccSyncItem = {
@@ -63,6 +63,8 @@ type Documents = {
"\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n": typeof types.HeaderNavShare_ProjectFragmentDoc,
"\n fragment HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n workspaceSlug\n user {\n id\n }\n }\n": typeof types.HeaderNavNotificationsProjectInvite_PendingStreamCollaboratorFragmentDoc,
"\n fragment HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspace {\n id\n name\n }\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": typeof types.HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaboratorFragmentDoc,
"\n fragment AccIntegrationFolderNode_AccFolder on AccFolder {\n id\n name\n contents {\n items {\n id\n name\n latestVersion {\n id\n name\n versionNumber\n fileType\n }\n }\n }\n children {\n items {\n id\n name\n children {\n items {\n id\n name\n }\n }\n contents {\n items {\n id\n name\n }\n }\n }\n }\n }\n": typeof types.AccIntegrationFolderNode_AccFolderFragmentDoc,
"\n fragment SyncStatusModelItem_AccSyncItem on AccSyncItem {\n id\n status\n }\n": typeof types.SyncStatusModelItem_AccSyncItemFragmentDoc,
"\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n slug\n domainBasedMembershipProtectionEnabled\n defaultSeatType\n domains {\n domain\n id\n }\n seats {\n editors {\n available\n }\n }\n ...InviteDialogSharedSelectUsers_Workspace\n ...WorkspacesPlan_Workspace\n }\n": typeof types.InviteDialogWorkspace_WorkspaceFragmentDoc,
"\n fragment InviteDialogProject_Project on Project {\n id\n name\n workspaceId\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n ...WorkspacesPlan_Workspace\n }\n ...InviteDialogProjectRow_Project\n }\n": typeof types.InviteDialogProject_ProjectFragmentDoc,
"\n fragment InviteDialogProjectRow_Project on Project {\n id\n workspaceId\n workspace {\n id\n role\n }\n }\n": typeof types.InviteDialogProjectRow_ProjectFragmentDoc,
@@ -115,10 +117,10 @@ type Documents = {
"\n fragment ProjectPageModelsActions_Project on Project {\n id\n workspace {\n id\n slug\n }\n ...ProjectsModelPageEmbed_Project\n }\n": typeof types.ProjectPageModelsActions_ProjectFragmentDoc,
"\n fragment ProjectPageModelsCardProject on Project {\n id\n role\n visibility\n ...ProjectPageModelsActions_Project\n ...ProjectCardImportFileArea_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageModelsCardProjectFragmentDoc,
"\n fragment ProjectPageModelsCard_Model on Model {\n id\n homeView {\n id\n resourceIds\n }\n lastUpload: uploads(input: { limit: 1, cursor: null }) {\n items {\n id\n updatedAt\n convertedStatus\n }\n }\n lastVersion: versions(limit: 1, cursor: null) {\n items {\n id\n createdAt\n }\n }\n }\n": typeof types.ProjectPageModelsCard_ModelFragmentDoc,
"\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n models {\n totalCount\n }\n team {\n id\n user {\n ...FormUsersSelectItem\n }\n }\n workspace {\n id\n role\n slug\n name\n readOnly\n plan {\n name\n }\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectModelsAdd_Project\n }\n": typeof types.ProjectModelsPageHeader_ProjectFragmentDoc,
"\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n models {\n totalCount\n }\n team {\n id\n user {\n ...FormUsersSelectItem\n }\n }\n workspace {\n id\n role\n slug\n name\n readOnly\n plan {\n name\n }\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectModelsAdd_Project\n }\n": typeof types.ProjectModelsPageHeader_ProjectFragmentDoc,
"\n fragment ProjectModelsPageResults_Project on Project {\n ...ProjectPageLatestItemsModels\n }\n": typeof types.ProjectModelsPageResults_ProjectFragmentDoc,
"\n fragment ProjectPageModelsStructureItem_Project on Project {\n id\n ...ProjectPageModelsActions_Project\n ...ProjectCardImportFileArea_Project\n ...UseCanCreateModel_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageModelsStructureItem_ProjectFragmentDoc,
"\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n ...ProjectPageModelsCard_Model\n }\n hasChildren\n updatedAt\n }\n": typeof types.SingleLevelModelTreeItemFragmentDoc,
"\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n ...ProjectPageModelsCard_Model\n accSyncItem {\n ...SyncStatusModelItem_AccSyncItem\n }\n }\n hasChildren\n updatedAt\n }\n": typeof types.SingleLevelModelTreeItemFragmentDoc,
"\n fragment ProjectPageModelsUploadsDialog_FileUpload on FileUpload {\n id\n convertedStatus\n convertedMessage\n fileName\n fileSize\n convertedLastUpdate\n convertedVersionId\n uploadDate\n uploadComplete\n branchName\n ...UseFailedFileImportJobUtils_FileUpload\n }\n": typeof types.ProjectPageModelsUploadsDialog_FileUploadFragmentDoc,
"\n query GetModelUploads(\n $projectId: String!\n $modelId: String!\n $input: GetModelUploadsInput!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n uploads(input: $input) {\n totalCount\n cursor\n items {\n id\n ...ProjectPageModelsUploadsDialog_FileUpload\n }\n }\n }\n }\n }\n": typeof types.GetModelUploadsDocument,
"\n fragment ProjectPageModelsCardDeleteDialog on Model {\n id\n name\n }\n": typeof types.ProjectPageModelsCardDeleteDialogFragmentDoc,
@@ -224,11 +226,13 @@ type Documents = {
"\n fragment WorkspaceSidebar_Workspace on Workspace {\n ...WorkspaceSidebarMembers_Workspace\n ...WorkspaceSidebarAbout_Workspace\n ...WorkspaceSidebarSecurity_Workspace\n id\n role\n slug\n domains {\n id\n }\n plan {\n name\n }\n }\n": typeof types.WorkspaceSidebar_WorkspaceFragmentDoc,
"\n fragment WorkspaceWizard_Workspace on Workspace {\n creationState {\n completed\n state\n }\n name\n slug\n }\n": typeof types.WorkspaceWizard_WorkspaceFragmentDoc,
"\n fragment WorkspaceWizardStepRegion_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": typeof types.WorkspaceWizardStepRegion_ServerInfoFragmentDoc,
"\n fragment ProjectAccSyncItem on AccSyncItem {\n id\n projectId\n modelId\n accRegion\n accHubId\n accProjectId\n accRootProjectFolderUrn\n accFileLineageUrn\n accFileName\n accFileExtension\n accFileVersionIndex\n accFileViewName\n updatedAt\n status\n author {\n name\n avatar\n }\n }\n": typeof types.ProjectAccSyncItemFragmentDoc,
"\n fragment AccFolderContents on AccFolder {\n id\n name\n contents {\n items {\n id\n name\n latestVersion {\n id\n name\n versionNumber\n fileType\n }\n }\n }\n children {\n items {\n id\n name\n }\n }\n }\n ": typeof types.AccFolderContentsFragmentDoc,
"\n fragment ProjectAccSyncItem on AccSyncItem {\n id\n project {\n id\n }\n model {\n id\n }\n accRegion\n accHubId\n accProjectId\n accRootProjectFolderUrn\n accFileLineageUrn\n accFileName\n accFileExtension\n accFileVersionIndex\n accFileViewName\n updatedAt\n status\n author {\n name\n avatar\n }\n }\n": typeof types.ProjectAccSyncItemFragmentDoc,
"\n mutation CreateAccSyncItem($input: CreateAccSyncItemInput!) {\n accSyncItemMutations {\n create(input: $input) {\n id\n accFileLineageUrn\n status\n }\n }\n }\n": typeof types.CreateAccSyncItemDocument,
"\n mutation DeleteAccSyncItem($input: DeleteAccSyncItemInput!) {\n accSyncItemMutations {\n delete(input: $input)\n }\n }\n": typeof types.DeleteAccSyncItemDocument,
"\n mutation UpdateAccSyncItem($input: UpdateAccSyncItemInput!) {\n accSyncItemMutations {\n update(input: $input) {\n id\n status\n }\n }\n }\n": typeof types.UpdateAccSyncItemDocument,
"\n query ProjectAccSyncItems($id: String!) {\n project(id: $id) {\n accSyncItems {\n items {\n ...ProjectAccSyncItem\n }\n }\n }\n }\n": typeof types.ProjectAccSyncItemsDocument,
"\n query AccFolderData(\n $workspaceSlug: String!\n $accToken: String!\n $accProjectId: String!\n $accFolderId: String!\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n integrations {\n acc(token: $accToken) {\n folder(projectId: $accProjectId, folderId: $accFolderId) {\n id\n ...AccIntegrationFolderNode_AccFolder\n }\n }\n }\n }\n }\n": typeof types.AccFolderDataDocument,
"\n subscription OnProjectAccSyncItemUpdated($id: String!, $itemIds: [String!]) {\n projectAccSyncItemsUpdated(id: $id, itemIds: $itemIds) {\n type\n accSyncItem {\n ...ProjectAccSyncItem\n }\n }\n }\n": typeof types.OnProjectAccSyncItemUpdatedDocument,
"\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n email\n verified\n primary\n }\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n ...ProjectsAdd_User\n }\n }\n": typeof types.ActiveUserMainMetadataDocument,
"\n query ActiveUserProjectsToMove($filter: UserProjectsFilter) {\n activeUser {\n id\n projects(filter: $filter) {\n totalCount\n }\n }\n }\n": typeof types.ActiveUserProjectsToMoveDocument,
@@ -508,6 +512,7 @@ type Documents = {
"\n mutation WorkspaceUpdateAutoJoinMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n discoverabilityAutoJoinEnabled\n }\n }\n }\n": typeof types.WorkspaceUpdateAutoJoinMutationDocument,
"\n mutation WorkspaceUpdateDefaultSeatTypeMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n defaultSeatType\n }\n }\n }\n": typeof types.WorkspaceUpdateDefaultSeatTypeMutationDocument,
"\n mutation WorkspaceUpdateExclusiveMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n isExclusive\n }\n }\n }\n": typeof types.WorkspaceUpdateExclusiveMutationDocument,
"\n query Workspace($featureName: WorkspaceFeatureName!, $workspaceId: String!) {\n workspace(id: $workspaceId) {\n hasAccessToFeature(featureName: $featureName)\n }\n }\n": typeof types.WorkspaceDocument,
"\n query WorkspaceAccessCheck($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n slug\n }\n activeUser {\n id\n activeWorkspace {\n id\n slug\n }\n }\n }\n": typeof types.WorkspaceAccessCheckDocument,
"\n query WorkspacePageQuery(\n $workspaceSlug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n ...WorkspacePage_Workspace\n }\n }\n": typeof types.WorkspacePageQueryDocument,
"\n query WorkspaceProjectsQuery(\n $workspaceSlug: String!\n $filter: WorkspaceProjectsFilter\n $cursor: String\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n projects(filter: $filter, cursor: $cursor, limit: 10) {\n ...WorkspaceDashboardProjectList_ProjectCollection\n }\n }\n }\n": typeof types.WorkspaceProjectsQueryDocument,
@@ -540,10 +545,10 @@ type Documents = {
"\n fragment AutomateFunctionPage_AutomateFunction on AutomateFunction {\n id\n name\n description\n logo\n supportedSourceApps\n tags\n ...AutomateFunctionPageHeader_Function\n ...AutomateFunctionPageInfo_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n creator {\n id\n }\n }\n": typeof types.AutomateFunctionPage_AutomateFunctionFragmentDoc,
"\n query AutomateFunctionPage($functionId: ID!) {\n automateFunction(id: $functionId) {\n ...AutomateFunctionPage_AutomateFunction\n }\n activeUser {\n workspaces {\n items {\n ...AutomateFunctionCreateDialog_Workspace\n ...AutomateFunctionEditDialog_Workspace\n }\n }\n }\n }\n": typeof types.AutomateFunctionPageDocument,
"\n query AutomateFunctionPageWorkspace($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n ...AutomateFunctionPageHeader_Workspace\n }\n }\n": typeof types.AutomateFunctionPageWorkspaceDocument,
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n ...WorkspaceMoveProject_Project\n }\n": typeof types.ProjectPageProjectFragmentDoc,
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n ...WorkspaceMoveProject_Project\n }\n": typeof types.ProjectPageProjectFragmentDoc,
"\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageAutomationHeader_Automation\n ...ProjectPageAutomationFunctions_Automation\n ...ProjectPageAutomationRuns_Automation\n }\n": typeof types.ProjectPageAutomationPage_AutomationFragmentDoc,
"\n fragment ProjectPageAutomationPage_Project on Project {\n id\n workspaceId\n ...ProjectPageAutomationHeader_Project\n }\n": typeof types.ProjectPageAutomationPage_ProjectFragmentDoc,
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageSettingsTab_ProjectFragmentDoc,
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageSettingsTab_ProjectFragmentDoc,
"\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": typeof types.SettingsServerProjects_ProjectCollectionFragmentDoc,
"\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n": typeof types.SettingsServerRegionsDocument,
"\n fragment SettingsWorkspacesGeneral_Workspace on Workspace {\n ...SettingsWorkspacesGeneralEditAvatar_Workspace\n ...SettingsWorkspaceGeneralDeleteDialog_Workspace\n ...SettingsWorkspacesGeneralEditSlugDialog_Workspace\n id\n name\n slug\n description\n logo\n role\n plan {\n status\n name\n }\n embedOptions {\n hideSpeckleBranding\n }\n permissions {\n canEditEmbedOptions {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.SettingsWorkspacesGeneral_WorkspaceFragmentDoc,
@@ -606,6 +611,8 @@ const documents: Documents = {
"\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n": types.HeaderNavShare_ProjectFragmentDoc,
"\n fragment HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n workspaceSlug\n user {\n id\n }\n }\n": types.HeaderNavNotificationsProjectInvite_PendingStreamCollaboratorFragmentDoc,
"\n fragment HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspace {\n id\n name\n }\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": types.HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaboratorFragmentDoc,
"\n fragment AccIntegrationFolderNode_AccFolder on AccFolder {\n id\n name\n contents {\n items {\n id\n name\n latestVersion {\n id\n name\n versionNumber\n fileType\n }\n }\n }\n children {\n items {\n id\n name\n children {\n items {\n id\n name\n }\n }\n contents {\n items {\n id\n name\n }\n }\n }\n }\n }\n": types.AccIntegrationFolderNode_AccFolderFragmentDoc,
"\n fragment SyncStatusModelItem_AccSyncItem on AccSyncItem {\n id\n status\n }\n": types.SyncStatusModelItem_AccSyncItemFragmentDoc,
"\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n slug\n domainBasedMembershipProtectionEnabled\n defaultSeatType\n domains {\n domain\n id\n }\n seats {\n editors {\n available\n }\n }\n ...InviteDialogSharedSelectUsers_Workspace\n ...WorkspacesPlan_Workspace\n }\n": types.InviteDialogWorkspace_WorkspaceFragmentDoc,
"\n fragment InviteDialogProject_Project on Project {\n id\n name\n workspaceId\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n ...WorkspacesPlan_Workspace\n }\n ...InviteDialogProjectRow_Project\n }\n": types.InviteDialogProject_ProjectFragmentDoc,
"\n fragment InviteDialogProjectRow_Project on Project {\n id\n workspaceId\n workspace {\n id\n role\n }\n }\n": types.InviteDialogProjectRow_ProjectFragmentDoc,
@@ -658,10 +665,10 @@ const documents: Documents = {
"\n fragment ProjectPageModelsActions_Project on Project {\n id\n workspace {\n id\n slug\n }\n ...ProjectsModelPageEmbed_Project\n }\n": types.ProjectPageModelsActions_ProjectFragmentDoc,
"\n fragment ProjectPageModelsCardProject on Project {\n id\n role\n visibility\n ...ProjectPageModelsActions_Project\n ...ProjectCardImportFileArea_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageModelsCardProjectFragmentDoc,
"\n fragment ProjectPageModelsCard_Model on Model {\n id\n homeView {\n id\n resourceIds\n }\n lastUpload: uploads(input: { limit: 1, cursor: null }) {\n items {\n id\n updatedAt\n convertedStatus\n }\n }\n lastVersion: versions(limit: 1, cursor: null) {\n items {\n id\n createdAt\n }\n }\n }\n": types.ProjectPageModelsCard_ModelFragmentDoc,
"\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n models {\n totalCount\n }\n team {\n id\n user {\n ...FormUsersSelectItem\n }\n }\n workspace {\n id\n role\n slug\n name\n readOnly\n plan {\n name\n }\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectModelsAdd_Project\n }\n": types.ProjectModelsPageHeader_ProjectFragmentDoc,
"\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n models {\n totalCount\n }\n team {\n id\n user {\n ...FormUsersSelectItem\n }\n }\n workspace {\n id\n role\n slug\n name\n readOnly\n plan {\n name\n }\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectModelsAdd_Project\n }\n": types.ProjectModelsPageHeader_ProjectFragmentDoc,
"\n fragment ProjectModelsPageResults_Project on Project {\n ...ProjectPageLatestItemsModels\n }\n": types.ProjectModelsPageResults_ProjectFragmentDoc,
"\n fragment ProjectPageModelsStructureItem_Project on Project {\n id\n ...ProjectPageModelsActions_Project\n ...ProjectCardImportFileArea_Project\n ...UseCanCreateModel_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageModelsStructureItem_ProjectFragmentDoc,
"\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n ...ProjectPageModelsCard_Model\n }\n hasChildren\n updatedAt\n }\n": types.SingleLevelModelTreeItemFragmentDoc,
"\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n ...ProjectPageModelsCard_Model\n accSyncItem {\n ...SyncStatusModelItem_AccSyncItem\n }\n }\n hasChildren\n updatedAt\n }\n": types.SingleLevelModelTreeItemFragmentDoc,
"\n fragment ProjectPageModelsUploadsDialog_FileUpload on FileUpload {\n id\n convertedStatus\n convertedMessage\n fileName\n fileSize\n convertedLastUpdate\n convertedVersionId\n uploadDate\n uploadComplete\n branchName\n ...UseFailedFileImportJobUtils_FileUpload\n }\n": types.ProjectPageModelsUploadsDialog_FileUploadFragmentDoc,
"\n query GetModelUploads(\n $projectId: String!\n $modelId: String!\n $input: GetModelUploadsInput!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n uploads(input: $input) {\n totalCount\n cursor\n items {\n id\n ...ProjectPageModelsUploadsDialog_FileUpload\n }\n }\n }\n }\n }\n": types.GetModelUploadsDocument,
"\n fragment ProjectPageModelsCardDeleteDialog on Model {\n id\n name\n }\n": types.ProjectPageModelsCardDeleteDialogFragmentDoc,
@@ -767,11 +774,13 @@ const documents: Documents = {
"\n fragment WorkspaceSidebar_Workspace on Workspace {\n ...WorkspaceSidebarMembers_Workspace\n ...WorkspaceSidebarAbout_Workspace\n ...WorkspaceSidebarSecurity_Workspace\n id\n role\n slug\n domains {\n id\n }\n plan {\n name\n }\n }\n": types.WorkspaceSidebar_WorkspaceFragmentDoc,
"\n fragment WorkspaceWizard_Workspace on Workspace {\n creationState {\n completed\n state\n }\n name\n slug\n }\n": types.WorkspaceWizard_WorkspaceFragmentDoc,
"\n fragment WorkspaceWizardStepRegion_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": types.WorkspaceWizardStepRegion_ServerInfoFragmentDoc,
"\n fragment ProjectAccSyncItem on AccSyncItem {\n id\n projectId\n modelId\n accRegion\n accHubId\n accProjectId\n accRootProjectFolderUrn\n accFileLineageUrn\n accFileName\n accFileExtension\n accFileVersionIndex\n accFileViewName\n updatedAt\n status\n author {\n name\n avatar\n }\n }\n": types.ProjectAccSyncItemFragmentDoc,
"\n fragment AccFolderContents on AccFolder {\n id\n name\n contents {\n items {\n id\n name\n latestVersion {\n id\n name\n versionNumber\n fileType\n }\n }\n }\n children {\n items {\n id\n name\n }\n }\n }\n ": types.AccFolderContentsFragmentDoc,
"\n fragment ProjectAccSyncItem on AccSyncItem {\n id\n project {\n id\n }\n model {\n id\n }\n accRegion\n accHubId\n accProjectId\n accRootProjectFolderUrn\n accFileLineageUrn\n accFileName\n accFileExtension\n accFileVersionIndex\n accFileViewName\n updatedAt\n status\n author {\n name\n avatar\n }\n }\n": types.ProjectAccSyncItemFragmentDoc,
"\n mutation CreateAccSyncItem($input: CreateAccSyncItemInput!) {\n accSyncItemMutations {\n create(input: $input) {\n id\n accFileLineageUrn\n status\n }\n }\n }\n": types.CreateAccSyncItemDocument,
"\n mutation DeleteAccSyncItem($input: DeleteAccSyncItemInput!) {\n accSyncItemMutations {\n delete(input: $input)\n }\n }\n": types.DeleteAccSyncItemDocument,
"\n mutation UpdateAccSyncItem($input: UpdateAccSyncItemInput!) {\n accSyncItemMutations {\n update(input: $input) {\n id\n status\n }\n }\n }\n": types.UpdateAccSyncItemDocument,
"\n query ProjectAccSyncItems($id: String!) {\n project(id: $id) {\n accSyncItems {\n items {\n ...ProjectAccSyncItem\n }\n }\n }\n }\n": types.ProjectAccSyncItemsDocument,
"\n query AccFolderData(\n $workspaceSlug: String!\n $accToken: String!\n $accProjectId: String!\n $accFolderId: String!\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n integrations {\n acc(token: $accToken) {\n folder(projectId: $accProjectId, folderId: $accFolderId) {\n id\n ...AccIntegrationFolderNode_AccFolder\n }\n }\n }\n }\n }\n": types.AccFolderDataDocument,
"\n subscription OnProjectAccSyncItemUpdated($id: String!, $itemIds: [String!]) {\n projectAccSyncItemsUpdated(id: $id, itemIds: $itemIds) {\n type\n accSyncItem {\n ...ProjectAccSyncItem\n }\n }\n }\n": types.OnProjectAccSyncItemUpdatedDocument,
"\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n email\n verified\n primary\n }\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n ...ProjectsAdd_User\n }\n }\n": types.ActiveUserMainMetadataDocument,
"\n query ActiveUserProjectsToMove($filter: UserProjectsFilter) {\n activeUser {\n id\n projects(filter: $filter) {\n totalCount\n }\n }\n }\n": types.ActiveUserProjectsToMoveDocument,
@@ -1051,6 +1060,7 @@ const documents: Documents = {
"\n mutation WorkspaceUpdateAutoJoinMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n discoverabilityAutoJoinEnabled\n }\n }\n }\n": types.WorkspaceUpdateAutoJoinMutationDocument,
"\n mutation WorkspaceUpdateDefaultSeatTypeMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n defaultSeatType\n }\n }\n }\n": types.WorkspaceUpdateDefaultSeatTypeMutationDocument,
"\n mutation WorkspaceUpdateExclusiveMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n isExclusive\n }\n }\n }\n": types.WorkspaceUpdateExclusiveMutationDocument,
"\n query Workspace($featureName: WorkspaceFeatureName!, $workspaceId: String!) {\n workspace(id: $workspaceId) {\n hasAccessToFeature(featureName: $featureName)\n }\n }\n": types.WorkspaceDocument,
"\n query WorkspaceAccessCheck($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n slug\n }\n activeUser {\n id\n activeWorkspace {\n id\n slug\n }\n }\n }\n": types.WorkspaceAccessCheckDocument,
"\n query WorkspacePageQuery(\n $workspaceSlug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n ...WorkspacePage_Workspace\n }\n }\n": types.WorkspacePageQueryDocument,
"\n query WorkspaceProjectsQuery(\n $workspaceSlug: String!\n $filter: WorkspaceProjectsFilter\n $cursor: String\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n projects(filter: $filter, cursor: $cursor, limit: 10) {\n ...WorkspaceDashboardProjectList_ProjectCollection\n }\n }\n }\n": types.WorkspaceProjectsQueryDocument,
@@ -1083,10 +1093,10 @@ const documents: Documents = {
"\n fragment AutomateFunctionPage_AutomateFunction on AutomateFunction {\n id\n name\n description\n logo\n supportedSourceApps\n tags\n ...AutomateFunctionPageHeader_Function\n ...AutomateFunctionPageInfo_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n creator {\n id\n }\n }\n": types.AutomateFunctionPage_AutomateFunctionFragmentDoc,
"\n query AutomateFunctionPage($functionId: ID!) {\n automateFunction(id: $functionId) {\n ...AutomateFunctionPage_AutomateFunction\n }\n activeUser {\n workspaces {\n items {\n ...AutomateFunctionCreateDialog_Workspace\n ...AutomateFunctionEditDialog_Workspace\n }\n }\n }\n }\n": types.AutomateFunctionPageDocument,
"\n query AutomateFunctionPageWorkspace($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n ...AutomateFunctionPageHeader_Workspace\n }\n }\n": types.AutomateFunctionPageWorkspaceDocument,
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n ...WorkspaceMoveProject_Project\n }\n": types.ProjectPageProjectFragmentDoc,
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n ...WorkspaceMoveProject_Project\n }\n": types.ProjectPageProjectFragmentDoc,
"\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageAutomationHeader_Automation\n ...ProjectPageAutomationFunctions_Automation\n ...ProjectPageAutomationRuns_Automation\n }\n": types.ProjectPageAutomationPage_AutomationFragmentDoc,
"\n fragment ProjectPageAutomationPage_Project on Project {\n id\n workspaceId\n ...ProjectPageAutomationHeader_Project\n }\n": types.ProjectPageAutomationPage_ProjectFragmentDoc,
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageSettingsTab_ProjectFragmentDoc,
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageSettingsTab_ProjectFragmentDoc,
"\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": types.SettingsServerProjects_ProjectCollectionFragmentDoc,
"\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n": types.SettingsServerRegionsDocument,
"\n fragment SettingsWorkspacesGeneral_Workspace on Workspace {\n ...SettingsWorkspacesGeneralEditAvatar_Workspace\n ...SettingsWorkspaceGeneralDeleteDialog_Workspace\n ...SettingsWorkspacesGeneralEditSlugDialog_Workspace\n id\n name\n slug\n description\n logo\n role\n plan {\n status\n name\n }\n embedOptions {\n hideSpeckleBranding\n }\n permissions {\n canEditEmbedOptions {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.SettingsWorkspacesGeneral_WorkspaceFragmentDoc,
@@ -1310,6 +1320,14 @@ export function graphql(source: "\n fragment HeaderNavNotificationsProjectInvit
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspace {\n id\n name\n }\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n"): (typeof documents)["\n fragment HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspace {\n id\n name\n }\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment AccIntegrationFolderNode_AccFolder on AccFolder {\n id\n name\n contents {\n items {\n id\n name\n latestVersion {\n id\n name\n versionNumber\n fileType\n }\n }\n }\n children {\n items {\n id\n name\n children {\n items {\n id\n name\n }\n }\n contents {\n items {\n id\n name\n }\n }\n }\n }\n }\n"): (typeof documents)["\n fragment AccIntegrationFolderNode_AccFolder on AccFolder {\n id\n name\n contents {\n items {\n id\n name\n latestVersion {\n id\n name\n versionNumber\n fileType\n }\n }\n }\n children {\n items {\n id\n name\n children {\n items {\n id\n name\n }\n }\n contents {\n items {\n id\n name\n }\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment SyncStatusModelItem_AccSyncItem on AccSyncItem {\n id\n status\n }\n"): (typeof documents)["\n fragment SyncStatusModelItem_AccSyncItem on AccSyncItem {\n id\n status\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1521,7 +1539,7 @@ export function graphql(source: "\n fragment ProjectPageModelsCard_Model on Mod
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n models {\n totalCount\n }\n team {\n id\n user {\n ...FormUsersSelectItem\n }\n }\n workspace {\n id\n role\n slug\n name\n readOnly\n plan {\n name\n }\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectModelsAdd_Project\n }\n"): (typeof documents)["\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n models {\n totalCount\n }\n team {\n id\n user {\n ...FormUsersSelectItem\n }\n }\n workspace {\n id\n role\n slug\n name\n readOnly\n plan {\n name\n }\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectModelsAdd_Project\n }\n"];
export function graphql(source: "\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n models {\n totalCount\n }\n team {\n id\n user {\n ...FormUsersSelectItem\n }\n }\n workspace {\n id\n role\n slug\n name\n readOnly\n plan {\n name\n }\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectModelsAdd_Project\n }\n"): (typeof documents)["\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n models {\n totalCount\n }\n team {\n id\n user {\n ...FormUsersSelectItem\n }\n }\n workspace {\n id\n role\n slug\n name\n readOnly\n plan {\n name\n }\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectModelsAdd_Project\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1533,7 +1551,7 @@ export function graphql(source: "\n fragment ProjectPageModelsStructureItem_Pro
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n ...ProjectPageModelsCard_Model\n }\n hasChildren\n updatedAt\n }\n"): (typeof documents)["\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n ...ProjectPageModelsCard_Model\n }\n hasChildren\n updatedAt\n }\n"];
export function graphql(source: "\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n ...ProjectPageModelsCard_Model\n accSyncItem {\n ...SyncStatusModelItem_AccSyncItem\n }\n }\n hasChildren\n updatedAt\n }\n"): (typeof documents)["\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n ...ProjectPageModelsCard_Model\n accSyncItem {\n ...SyncStatusModelItem_AccSyncItem\n }\n }\n hasChildren\n updatedAt\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1957,7 +1975,11 @@ export function graphql(source: "\n fragment WorkspaceWizardStepRegion_ServerIn
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ProjectAccSyncItem on AccSyncItem {\n id\n projectId\n modelId\n accRegion\n accHubId\n accProjectId\n accRootProjectFolderUrn\n accFileLineageUrn\n accFileName\n accFileExtension\n accFileVersionIndex\n accFileViewName\n updatedAt\n status\n author {\n name\n avatar\n }\n }\n"): (typeof documents)["\n fragment ProjectAccSyncItem on AccSyncItem {\n id\n projectId\n modelId\n accRegion\n accHubId\n accProjectId\n accRootProjectFolderUrn\n accFileLineageUrn\n accFileName\n accFileExtension\n accFileVersionIndex\n accFileViewName\n updatedAt\n status\n author {\n name\n avatar\n }\n }\n"];
export function graphql(source: "\n fragment AccFolderContents on AccFolder {\n id\n name\n contents {\n items {\n id\n name\n latestVersion {\n id\n name\n versionNumber\n fileType\n }\n }\n }\n children {\n items {\n id\n name\n }\n }\n }\n "): (typeof documents)["\n fragment AccFolderContents on AccFolder {\n id\n name\n contents {\n items {\n id\n name\n latestVersion {\n id\n name\n versionNumber\n fileType\n }\n }\n }\n children {\n items {\n id\n name\n }\n }\n }\n "];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ProjectAccSyncItem on AccSyncItem {\n id\n project {\n id\n }\n model {\n id\n }\n accRegion\n accHubId\n accProjectId\n accRootProjectFolderUrn\n accFileLineageUrn\n accFileName\n accFileExtension\n accFileVersionIndex\n accFileViewName\n updatedAt\n status\n author {\n name\n avatar\n }\n }\n"): (typeof documents)["\n fragment ProjectAccSyncItem on AccSyncItem {\n id\n project {\n id\n }\n model {\n id\n }\n accRegion\n accHubId\n accProjectId\n accRootProjectFolderUrn\n accFileLineageUrn\n accFileName\n accFileExtension\n accFileVersionIndex\n accFileViewName\n updatedAt\n status\n author {\n name\n avatar\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1974,6 +1996,10 @@ export function graphql(source: "\n mutation UpdateAccSyncItem($input: UpdateAc
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query ProjectAccSyncItems($id: String!) {\n project(id: $id) {\n accSyncItems {\n items {\n ...ProjectAccSyncItem\n }\n }\n }\n }\n"): (typeof documents)["\n query ProjectAccSyncItems($id: String!) {\n project(id: $id) {\n accSyncItems {\n items {\n ...ProjectAccSyncItem\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query AccFolderData(\n $workspaceSlug: String!\n $accToken: String!\n $accProjectId: String!\n $accFolderId: String!\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n integrations {\n acc(token: $accToken) {\n folder(projectId: $accProjectId, folderId: $accFolderId) {\n id\n ...AccIntegrationFolderNode_AccFolder\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query AccFolderData(\n $workspaceSlug: String!\n $accToken: String!\n $accProjectId: String!\n $accFolderId: String!\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n integrations {\n acc(token: $accToken) {\n folder(projectId: $accProjectId, folderId: $accFolderId) {\n id\n ...AccIntegrationFolderNode_AccFolder\n }\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -3090,6 +3116,10 @@ export function graphql(source: "\n mutation WorkspaceUpdateDefaultSeatTypeMuta
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation WorkspaceUpdateExclusiveMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n isExclusive\n }\n }\n }\n"): (typeof documents)["\n mutation WorkspaceUpdateExclusiveMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n isExclusive\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query Workspace($featureName: WorkspaceFeatureName!, $workspaceId: String!) {\n workspace(id: $workspaceId) {\n hasAccessToFeature(featureName: $featureName)\n }\n }\n"): (typeof documents)["\n query Workspace($featureName: WorkspaceFeatureName!, $workspaceId: String!) {\n workspace(id: $workspaceId) {\n hasAccessToFeature(featureName: $featureName)\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -3221,7 +3251,7 @@ export function graphql(source: "\n query AutomateFunctionPageWorkspace($worksp
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n ...WorkspaceMoveProject_Project\n }\n"): (typeof documents)["\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n ...WorkspaceMoveProject_Project\n }\n"];
export function graphql(source: "\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n ...WorkspaceMoveProject_Project\n }\n"): (typeof documents)["\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n ...WorkspaceMoveProject_Project\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -3233,7 +3263,7 @@ export function graphql(source: "\n fragment ProjectPageAutomationPage_Project
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n }\n }\n"];
export function graphql(source: "\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
File diff suppressed because one or more lines are too long
@@ -70,6 +70,11 @@ export const settingsWorkspaceRoutes = {
route: (slug: MaybeNullOrUndefined<string>) =>
slug ? `/settings/workspaces/${slug}/projects` : '/'
},
integrations: {
name: 'settings-workspaces-slug-integrations',
route: (slug: MaybeNullOrUndefined<string>) =>
slug ? `/settings/workspaces/${slug}/integrations` : '/'
},
automation: {
name: 'settings-workspaces-slug-automation',
route: (slug?: string) => `/settings/workspaces/${slug}/automation`
@@ -134,6 +139,9 @@ export const projectWebhooksRoute = (projectId: string) =>
export const projectTokensRoute = (projectId: string) =>
`/projects/${projectId}/settings/tokens`
export const projectIntegrationsRoute = (projectId: string) =>
`/projects/${projectId}/settings/integrations`
export const threadRedirectRoute = (projectId: string, threadId: string) =>
`/projects/${projectId}/threads/${threadId}`
@@ -0,0 +1,69 @@
import { useApolloClient } from '@vue/apollo-composable'
import { useAccAuthManager } from '~/lib/acc/composables/useAccAuthManager'
import { AccIntegration } from '~/lib/acc/types'
import { WorkspaceFeatureName } from '~/lib/common/generated/gql/graphql'
import type { Integration } from '~/lib/integrations/types'
import { workspaceFeatureEnabledCheckQuery } from '~/lib/workspaces/graphql/queries'
export function useAccIntegration() {
const integration = ref<Integration>(AccIntegration)
const apollo = useApolloClient().client
const loading = ref(false)
const checkConnection = async (workspaceSlug: string, workspaceId: string) => {
loading.value = true
try {
const isAccModuleEnabled = useIsAccModuleEnabled()
if (isAccModuleEnabled) {
const accIntegationEnabled = await isAccEnabledInWorkspace(workspaceId)
const callbackEndpoint = `settings/workspaces/${workspaceSlug}/integrations`
if (accIntegationEnabled) {
const { isExpired, tokens, tryGetTokensFromCookies } = useAccAuthManager()
await tryGetTokensFromCookies() // also refreshes the tokens - so we can rely on existance of tokens to say 'connected'
integration.value = {
...AccIntegration,
connected: tokens.value !== undefined,
status: isExpired.value
? 'expired'
: tokens.value !== undefined
? 'connected'
: 'notConnected',
enabled: true,
callbackEndpoint
}
} else {
integration.value = { ...AccIntegration, callbackEndpoint }
}
}
} finally {
loading.value = false
}
}
const isAccEnabledInWorkspace = async (workspaceId: string) => {
const { data } = await apollo.query({
query: workspaceFeatureEnabledCheckQuery,
variables: {
workspaceId,
featureName: WorkspaceFeatureName.AccIntegration
},
fetchPolicy: 'network-only'
})
return data?.workspace?.hasAccessToFeature ?? false
}
const checkCredientials = async () => {
const { tryGetTokensFromCookies } = useAccAuthManager()
await tryGetTokensFromCookies()
}
return {
loading,
integration,
checkConnection,
checkCredientials
}
}
@@ -0,0 +1,10 @@
export type Integration = {
name: string
description: string
cookieKey: string
logo: string
connected: boolean
enabled: boolean
status: 'connected' | 'expired' | 'notConnected'
callbackEndpoint?: string
}
@@ -66,6 +66,12 @@ export const useSettingsMenu = () => {
}
]
: []),
{
title: 'Integrations',
name: settingsWorkspaceRoutes.integrations.name,
route: (slug?: string) => settingsWorkspaceRoutes.integrations.route(slug),
permission: [Roles.Workspace.Admin, Roles.Workspace.Member]
},
{
title: 'Security',
name: settingsWorkspaceRoutes.security.name,
@@ -1,5 +1,13 @@
import { graphql } from '~~/lib/common/generated/gql'
export const workspaceFeatureEnabledCheckQuery = graphql(`
query Workspace($featureName: WorkspaceFeatureName!, $workspaceId: String!) {
workspace(id: $workspaceId) {
hasAccessToFeature(featureName: $featureName)
}
}
`)
export const workspaceAccessCheckQuery = graphql(`
query WorkspaceAccessCheck($slug: String!) {
workspaceBySlug(slug: $slug) {
@@ -104,9 +104,6 @@ graphql(`
canReadSettings {
...FullPermissionCheckResult
}
canReadAccIntegrationSettings {
...FullPermissionCheckResult
}
canUpdate {
...FullPermissionCheckResult
}
@@ -182,9 +179,7 @@ const modelCount = computed(() => project.value?.modelCount.totalCount)
const commentCount = computed(() => project.value?.commentThreadCount.totalCount)
const canReadSettings = computed(() => project.value?.permissions.canReadSettings)
const canReadAccIntegrationSettings = computed(
() => project.value?.permissions.canReadAccIntegrationSettings
)
const canUpdate = computed(() => project.value?.permissions.canUpdate)
const hasRole = computed(() => project.value?.role)
const teamUsers = computed(() => project.value?.team.map((t) => t.user) || [])
@@ -233,7 +228,6 @@ const onInviteAccepted = async (params: { accepted: boolean }) => {
const isOwner = computed(() => project.value?.role === Roles.Stream.Owner)
const isAutomateEnabled = useIsAutomateModuleEnabled()
const isWorkspacesEnabled = useIsWorkspacesEnabled()
const isAccEnabled = useIsAccModuleEnabled()
const pageTabItems = computed((): LayoutPageTabItem[] => {
const items: LayoutPageTabItem[] = [
@@ -260,13 +254,6 @@ const pageTabItems = computed((): LayoutPageTabItem[] => {
})
}
if (isAccEnabled.value && canReadAccIntegrationSettings.value?.authorized) {
items.push({
title: 'ACC',
id: 'acc'
})
}
if (canReadSettings.value?.authorized) {
items.push({
title: 'Collaborators',
@@ -1,21 +0,0 @@
<template>
<ProjectPageAccTab :project-id="projectId" />
</template>
<script setup lang="ts">
import type { ProjectPageProjectFragment } from '~~/lib/common/generated/gql/graphql'
const route = useRoute()
const attrs = useAttrs() as {
project: ProjectPageProjectFragment
}
const projectName = computed(() =>
attrs.project.name.length ? attrs.project.name : ''
)
const projectId = computed(() => route.params.id as string)
useHead({
title: `Acc | ${projectName.value}`
})
</script>
@@ -14,7 +14,8 @@ import { LayoutTabsVertical, type LayoutPageTabItem } from '@speckle/ui-componen
import {
projectSettingsRoute,
projectWebhooksRoute,
projectTokensRoute
projectTokensRoute,
projectIntegrationsRoute
} from '~~/lib/common/helpers/route'
import { graphql } from '~~/lib/common/generated/gql'
import type { ProjectPageSettingsTab_ProjectFragment } from '~~/lib/common/generated/gql/graphql'
@@ -34,6 +35,9 @@ graphql(`
canReadEmbedTokens {
...FullPermissionCheckResult
}
canReadAccIntegrationSettings {
...FullPermissionCheckResult
}
}
}
`)
@@ -49,6 +53,10 @@ const canReadWebhooks = computed(() => attrs.project.permissions.canReadWebhooks
const projectName = computed(() =>
attrs.project.name.length ? attrs.project.name : ''
)
const isAccEnabled = useIsAccModuleEnabled() // check permission over project
const canReadAccIntegrationSettings = computed(
() => attrs.project.permissions.canReadAccIntegrationSettings
)
useHead({
title: `Settings | ${projectName.value}`
@@ -70,6 +78,12 @@ const settingsTabItems = computed((): LayoutPageTabItem[] => [
id: 'tokens',
disabled: !canReadEmbedTokens.value.authorized,
disabledMessage: canReadEmbedTokens.value.message
},
{
title: 'Integrations',
id: 'integrations',
disabled: isAccEnabled && !canReadAccIntegrationSettings.value.authorized,
disabledMessage: canReadAccIntegrationSettings.value.message
}
])
@@ -80,6 +94,7 @@ const activeSettingsPageTab = computed({
const path = route.path
if (path.includes('/settings/webhooks')) return settingsTabItems.value[1]
if (path.includes('/settings/tokens')) return settingsTabItems.value[2]
if (path.includes('/settings/integrations')) return settingsTabItems.value[3]
return settingsTabItems.value[0]
},
set: (val: LayoutPageTabItem) => {
@@ -90,6 +105,9 @@ const activeSettingsPageTab = computed({
case 'tokens':
router.push(projectTokensRoute(projectId.value))
break
case 'integrations':
router.push(projectIntegrationsRoute(projectId.value))
break
case 'general':
default:
router.push(projectSettingsRoute(projectId.value))
@@ -0,0 +1,7 @@
<template>
<ProjectPageSettingsAccTab :project-id="projectId" />
</template>
<script setup lang="ts">
const route = useRoute()
const projectId = computed(() => route.params.id as string)
</script>
@@ -0,0 +1,33 @@
<template>
<section>
<SettingsSectionHeader
title="Integrations"
text="Connect your workspace to authorized applications."
/>
<IntegrationsAccCard
:workspace-id="workspaceResult?.workspaceBySlug.id || ''"
:workspace-slug="routeSlug"
></IntegrationsAccCard>
<!-- <div v-for="integration in integrations" :key="integration.cookieKey">
<IntegrationsCard
:integration="integration"
@handle-c-t-a="handleCTA(integration)"
></IntegrationsCard>
</div> -->
</section>
</template>
<script setup lang="ts">
import { useQuery } from '@vue/apollo-composable'
import { settingsWorkspaceGeneralQuery } from '~/lib/settings/graphql/queries'
definePageMeta({
layout: 'settings'
})
const route = useRoute()
const routeSlug = computed(() => (route.params.slug as string) || '')
const { result: workspaceResult } = useQuery(settingsWorkspaceGeneralQuery, () => ({
slug: routeSlug.value
}))
</script>
@@ -0,0 +1,74 @@
extend type WorkspaceIntegrations {
acc(token: String): AccIntegration
}
type AccIntegration {
hub(id: String!): AccHub!
hubs: AccHubCollection!
project(hubId: String!, projectId: String!): AccProject!
folder(projectId: String!, folderId: String!): AccFolder!
item(projectId: String!, itemId: String!): AccItem!
}
type AccHub {
id: ID!
name: String!
project(id: ID!): AccProject!
projects: AccProjectCollection!
}
type AccHubCollection {
items: [AccHub!]!
cursor: String
}
type AccProject {
id: ID!
name: String!
# hub: AccHub!
folder(id: String!): AccFolder!
rootFolder: AccFolder!
}
type AccProjectCollection {
items: [AccProject!]!
cursor: String
}
type AccFolder {
id: ID!
name: String!
# project: AccProject!
contents: AccItemCollection!
children: AccFolderCollection!
}
type AccFolderCollection {
items: [AccFolder!]!
cursor: String
}
type AccItem {
"""
lineage urn
"""
id: ID!
name: String!
# version(version: Int): AccItemVersion!
latestVersion: AccItemVersion!
}
type AccItemCollection {
items: [AccItem!]!
cursor: String
}
type AccItemVersion {
"""
version urn
"""
id: ID!
name: String!
versionNumber: Int!
fileType: String
}
@@ -0,0 +1,5 @@
extend type Workspace {
integrations: WorkspaceIntegrations
}
type WorkspaceIntegrations
@@ -0,0 +1,3 @@
extend type ProjectPermissionChecks {
canReadAccIntegrationSettings: PermissionCheckResult!
}
@@ -3,6 +3,10 @@ extend type Project {
accSyncItem(id: String!): AccSyncItem!
}
extend type Model {
accSyncItem: AccSyncItem
}
type AccSyncItemCollection {
items: [AccSyncItem!]!
totalCount: Int!
@@ -11,8 +15,9 @@ type AccSyncItemCollection {
type AccSyncItem {
id: ID!
projectId: String!
modelId: String!
project: Project!
model: Model
author: LimitedUser
accRegion: String!
accHubId: String!
accProjectId: String!
@@ -25,7 +30,6 @@ type AccSyncItem {
accFileViewName: String
accWebhookId: String
status: AccSyncItemStatus!
author: LimitedUser
createdAt: DateTime!
updatedAt: DateTime!
}
@@ -10,7 +10,6 @@ type ProjectPermissionChecks {
canDelete: PermissionCheckResult!
canUpdateAllowPublicComments: PermissionCheckResult!
canReadSettings: PermissionCheckResult!
canReadAccIntegrationSettings: PermissionCheckResult!
canReadWebhooks: PermissionCheckResult!
canLeave: PermissionCheckResult!
canRequestRender: PermissionCheckResult!
+6
View File
@@ -186,6 +186,12 @@ const config: CodegenConfig = {
'@/modules/core/helpers/graphTypes#RootPermissionChecksGraphQLReturn',
WorkspacePermissionChecks:
'@/modules/workspacesCore/helpers/graphTypes#WorkspacePermissionChecksGraphQLReturn',
WorkspaceIntegrations:
'@/modules/acc/helpers/graphTypes#WorkspaceIntegrationsGraphQLReturn',
AccIntegration:
'@/modules/acc/helpers/graphTypes#AccIntegrationGraphQLReturn',
AccFolder: '@/modules/acc/helpers/graphTypes#AccFolderGraphQLReturn',
AccItem: '@/modules/acc/helpers/graphTypes#AccItemGraphQLReturn',
AccSyncItem: '@/modules/acc/helpers/graphTypes#AccSyncItemGraphQLReturn',
AccSyncItemMutations:
'@/modules/acc/helpers/graphTypes#AccSyncItemMutationsGraphQLReturn',
@@ -1,310 +0,0 @@
/* 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'
import {
getAutodeskIntegrationClientId,
getAutodeskIntegrationClientSecret
} from '@/modules/shared/helpers/envHelper'
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
token: string
method?: RequestInit['method']
body?: URLSearchParams | Record<string, unknown>
headers?: Record<string, string>
}) => {
const { url, method = 'get', body, headers = {}, token } = params
const response = await fetch(url, {
method,
headers: {
'Content-Type':
body instanceof URLSearchParams
? 'application/x-www-form-urlencoded'
: 'application/json',
Authorization: `Bearer ${token}`,
...headers
},
body:
body && body instanceof URLSearchParams
? body
: isObjectLike(body)
? JSON.stringify(body)
: undefined
})
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()
const token = Buffer.from(`${clientId}:${clientSecret}`, 'utf8').toString('base64')
const response = await fetch(
'https://developer.api.autodesk.com/authentication/v2/token',
{
method: 'POST',
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: 'data:read account:read viewables:read'
}),
headers: {
Authorization: `Basic ${token}`,
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
}
}
)
const data = await response.json()
return z
.object({
access_token: z.string(),
token_type: z.string(),
expires_in: z.number()
})
.parse(data)
}
/**
* Get base Autodesk API endpoint for a given region.
* NOTE: This has been true for all endpoints used so far but should be validated for any new ones!
*/
const getRegionUrl = (region: AccRegion): string => {
switch (region) {
case AccRegions.EMEA:
return 'https://developer.api.autodesk.com/modelderivative/v2/regions/eu'
default:
return 'https://developer.api.autodesk.com/modelderivative/v2'
}
}
const getApiUrl = (path: string, region: AccRegion): string => {
return `${getRegionUrl(region)}${path}`
}
/**
* Encode a urn string in the modified url-safe base64 format expected by the Autodesk API
*/
const encodeUrn = (urn: string): string => {
return btoa(urn).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '')
}
type AccWebhookConfig = {
// The (unencoded) ACC folder urn to subscribe to. Event will fire for all files in this folder.
rootProjectFolderUrn: string
// The Speckle endpoint to hit when the given event fires.
callbackUrl: string
// The ACC webhook event to subscribe to. You can register an event to a given callback url only once.
event: 'dm.version.added'
// The region where the request is executed
region: AccRegion
}
/**
* Register relevant webhook callbacks for integration with a given ACC project.
* @see https://aps.autodesk.com/en/docs/webhooks/v1/reference/http/webhooks/systems-system-events-event-hooks-POST/
* @returns null if webhook already exists
*/
export const tryRegisterAccWebhook = async (
webhook: AccWebhookConfig
): Promise<string | null> => {
const { rootProjectFolderUrn, callbackUrl, event, region } = webhook
const tokenData = await getToken()
const response = await fetch(
`https://developer.api.autodesk.com/webhooks/v1/systems/data/events/${event}/hooks`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${tokenData.access_token}`,
'Content-Type': 'application/json',
'x-ads-region': region
},
body: JSON.stringify({
callbackUrl,
scope: {
folder: rootProjectFolderUrn
}
})
}
)
if (response.ok && response.status === 201) {
const webhookId = response.headers.get('Location')?.split('/').at(-1)
if (!webhookId) {
logger.info({ location: response.headers.get('Location') })
throw new Error('Webhook created but failed to parse id')
}
return webhookId
}
const e = await response.json().catch(() => null)
const isConflict =
response.status === 409 &&
e?.code === 'CONFLICT_ERROR' &&
e?.detail?.includes('Failed to save duplicate webhooks scope')
if (isConflict) {
logger.warn('Webhook already exists. Skipping registration.')
return null
}
throw new Error(`Webhook registration failed: ${JSON.stringify(e, null, 2)}`)
}
/**
* Get a manifest of the available derivative assets for a given design urn
* @see https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/manifest/urn-manifest-GET/
*/
export const getManifestByUrn = async (
params: {
urn: string
region: AccRegion
},
context: {
token: string
}
): Promise<ModelDerivativeServiceDesignManifest> => {
const { urn, region = AccRegions.EMEA } = params
const { token } = context
const encodedUrn = encodeUrn(urn)
const url = getApiUrl(`/designdata/${encodedUrn}/manifest`, region)
return await invokeJsonRequest({
url,
token,
method: 'GET'
})
}
@@ -0,0 +1,211 @@
import {
encodeUrn,
getApiUrl,
invokeJsonRequest
} from '@/modules/acc/clients/autodesk/helpers'
import { getToken } from '@/modules/acc/clients/autodesk/tokens'
import type { AccRegion } from '@/modules/acc/domain/acc/constants'
import { AccRegions } from '@/modules/acc/domain/acc/constants'
import type {
DataManagementFolderContentsFolder,
DataManagementFolderContentsItem,
DataManagementFolderContentsItemVersion,
ModelDerivativeServiceDesignManifest
} from '@/modules/acc/domain/acc/types'
import { logger } from '@/observability/logging'
type AccWebhookConfig = {
// The (unencoded) ACC folder urn to subscribe to. Event will fire for all files in this folder.
rootProjectFolderUrn: string
// The Speckle endpoint to hit when the given event fires.
callbackUrl: string
// The ACC webhook event to subscribe to. You can register an event to a given callback url only once.
event: 'dm.version.added'
// The region where the request is executed
region: AccRegion
}
/**
* Register relevant webhook callbacks for integration with a given ACC project.
* @see https://aps.autodesk.com/en/docs/webhooks/v1/reference/http/webhooks/systems-system-events-event-hooks-POST/
* @returns null if webhook already exists
*/
export const tryRegisterAccWebhook = async (
webhook: AccWebhookConfig
): Promise<string | null> => {
const { rootProjectFolderUrn, callbackUrl, event, region } = webhook
const tokenData = await getToken()
const response = await fetch(
`https://developer.api.autodesk.com/webhooks/v1/systems/data/events/${event}/hooks`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${tokenData.access_token}`,
'Content-Type': 'application/json',
'x-ads-region': region
},
body: JSON.stringify({
callbackUrl,
scope: {
folder: rootProjectFolderUrn
}
})
}
)
if (response.ok && response.status === 201) {
const webhookId = response.headers.get('Location')?.split('/').at(-1)
if (!webhookId) {
logger.info({ location: response.headers.get('Location') })
throw new Error('Webhook created but failed to parse id')
}
return webhookId
}
const e = await response.json().catch(() => null)
const isConflict =
response.status === 409 &&
e?.code === 'CONFLICT_ERROR' &&
e?.detail?.includes('Failed to save duplicate webhooks scope')
if (isConflict) {
logger.warn('Webhook already exists. Skipping registration.')
return null
}
throw new Error(`Webhook registration failed: ${JSON.stringify(e, null, 2)}`)
}
type GetFolderMetadataResponse = { data: DataManagementFolderContentsFolder }
/**
* Get information about a given folder, like its name and object count
* @see https://aps.autodesk.com/en/docs/data/v2/reference/http/projects-project_id-folders-folder_id-GET/
*/
export const getFolderMetadata = async (
params: {
projectId: string
folderId: string
},
context: {
token: string
userId?: string
}
): Promise<DataManagementFolderContentsFolder> => {
const { projectId, folderId } = params
const { token } = context
const { data } = (await invokeJsonRequest({
url: `https://developer.api.autodesk.com/data/v1/projects/${projectId}/folders/${folderId}`,
method: 'GET',
token
})) as GetFolderMetadataResponse
return data
}
type GetFolderContentsResponse = {
data: (DataManagementFolderContentsFolder | DataManagementFolderContentsItem)[]
}
/**
* Get the contents (subfolders and items) of a given ACC folder
* @see https://aps.autodesk.com/en/docs/data/v2/reference/http/projects-project_id-folders-folder_id-contents-GET/
*/
export const getFolderContents = async (
args: {
projectId: string
folderId: string
type?: 'items' | 'folders'
},
context: {
token: string
userId?: string
}
): Promise<
(DataManagementFolderContentsFolder | DataManagementFolderContentsItem)[]
> => {
const { projectId, folderId } = args
const { token } = context
const url = new URL(
`https://developer.api.autodesk.com/data/v1/projects/${projectId}/folders/${folderId}/contents`
)
const params = new URLSearchParams()
if (args.type) {
params.append('filter[type]', args.type)
}
url.search = params.toString()
const { data } = (await invokeJsonRequest({
url: url.toString(),
method: 'GET',
token
})) as GetFolderContentsResponse
return data ?? []
}
type GetItemLatestVersionResponse = {
data: DataManagementFolderContentsItemVersion
}
/**
* Get item version information, like version number and file type, for the latest version of a given file
* @see https://aps.autodesk.com/en/docs/data/v2/reference/http/projects-project_id-items-item_id-tip-GET/
*/
export const getItemLatestVersion = async (
args: {
projectId: string
itemId: string
},
context: {
token: string
userId?: string
}
): Promise<DataManagementFolderContentsItemVersion> => {
const { projectId, itemId } = args
const { token } = context
const { data } = (await invokeJsonRequest({
url: `https://developer.api.autodesk.com/data/v1/projects/${projectId}/items/${itemId}/tip`,
method: 'GET',
token
})) as GetItemLatestVersionResponse
return data
}
/**
* Get a manifest of the available derivative assets for a given design urn
* @see https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/manifest/urn-manifest-GET/
*/
export const getManifestByUrn = async (
params: {
urn: string
region: AccRegion
},
context: {
token: string
}
): Promise<ModelDerivativeServiceDesignManifest> => {
const { urn, region = AccRegions.EMEA } = params
const { token } = context
const encodedUrn = encodeUrn(urn)
const url = getApiUrl(`/designdata/${encodedUrn}/manifest`, region)
return await invokeJsonRequest({
url,
token,
method: 'GET'
})
}
@@ -0,0 +1,61 @@
import type { AccRegion } from '@/modules/acc/domain/acc/constants'
import { AccRegions } from '@/modules/acc/domain/acc/constants'
import { AutodeskApiRequestError } from '@/modules/acc/errors/acc'
import { logger } from '@/observability/logging'
export const invokeJsonRequest = async <T>(params: {
url: string
token: string
method?: RequestInit['method']
body?: string
headers?: Record<string, string>
}) => {
const { url, method = 'get', body, headers = {}, token } = params
try {
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
...headers
},
body
})
return (await response.json()) as T
} catch (e) {
logger.error(
{
...params,
error: e
},
'Autodesk request failed'
)
throw new AutodeskApiRequestError(method, url)
}
}
/**
* Get base Autodesk API endpoint for a given region.
* NOTE: This has been true for all endpoints used so far but should be validated for any new ones!
*/
export const getRegionUrl = (region: AccRegion): string => {
switch (region) {
case AccRegions.EMEA:
return 'https://developer.api.autodesk.com/modelderivative/v2/regions/eu'
default:
return 'https://developer.api.autodesk.com/modelderivative/v2'
}
}
export const getApiUrl = (path: string, region: AccRegion): string => {
return `${getRegionUrl(region)}${path}`
}
/**
* Encode a urn string in the modified url-safe base64 format expected by the Autodesk API
*/
export const encodeUrn = (urn: string): string => {
return btoa(urn).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '')
}
@@ -0,0 +1,167 @@
/* eslint-disable camelcase */
import crypto from 'crypto'
import { z } from 'zod'
import type { AccTokens } from '@speckle/shared/acc'
import {
getAutodeskIntegrationClientId,
getAutodeskIntegrationClientSecret
} from '@/modules/shared/helpers/envHelper'
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().optional(),
expires_in: z.number()
})
.transform((data) => ({
...data,
timestamp: Date.now()
}))
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 clientId = getAutodeskIntegrationClientId()
const clientSecret = getAutodeskIntegrationClientSecret()
const token = Buffer.from(`${clientId}:${clientSecret}`, 'utf8').toString('base64')
const params = new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: args.refresh_token
})
const response = await fetch(
'https://developer.api.autodesk.com/authentication/v2/token',
{
method: 'POST',
headers: {
Authorization: `Basic ${token}`,
Accept: 'application/json',
'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()
const token = Buffer.from(`${clientId}:${clientSecret}`, 'utf8').toString('base64')
const response = await fetch(
'https://developer.api.autodesk.com/authentication/v2/token',
{
method: 'POST',
body: new URLSearchParams({
grant_type: 'client_credentials',
scope: 'data:read account:read viewables:read'
}),
headers: {
Authorization: `Basic ${token}`,
Accept: 'application/json',
'Content-Type': 'application/x-www-form-urlencoded'
}
}
)
const data = await response.json()
return z
.object({
access_token: z.string(),
token_type: z.string(),
expires_in: z.number()
})
.parse(data)
}
@@ -0,0 +1,25 @@
import type { StringEnumValues } from '@speckle/shared'
import { StringEnum } from '@speckle/shared'
export const AccSyncItemStatuses = StringEnum([
// A new file version had been detected, and we are awaiting a processable file.
'pending',
// We are actively processing the new file version. (The Automate function has been triggered.)
'syncing',
'failed',
'paused',
'succeeded'
])
export type AccSyncItemStatus = StringEnumValues<typeof AccSyncItemStatuses>
export const AccRegions = StringEnum([
'US',
'EMEA',
'AUS',
'CAN',
'DEU',
'IND',
'JPN',
'GBR'
])
export type AccRegion = StringEnumValues<typeof AccRegions>
@@ -1,4 +1,4 @@
import type { AccSyncItem } from '@/modules/acc/domain/types'
import type { AccSyncItem } from '@/modules/acc/domain/acc/types'
export const accSyncItemEventsNamespace = 'accSyncItems' as const
@@ -1,5 +1,5 @@
import type { AccSyncItemStatus } from '@/modules/acc/domain/constants'
import type { AccSyncItem } from '@/modules/acc/domain/types'
import type { AccSyncItemStatus } from '@/modules/acc/domain/acc/constants'
import type { AccSyncItem } from '@/modules/acc/domain/acc/types'
import type { Exact } from 'type-fest'
export type UpsertAccSyncItem = <Item extends Exact<AccSyncItem, Item>>(
@@ -13,7 +13,14 @@ export type UpdateAccSyncItemStatus = (args: {
export type GetAccSyncItemById = (args: { id: string }) => Promise<AccSyncItem | null>
export type GetAccSyncItemByModelId = (args: {
modelId: string
}) => Promise<AccSyncItem | null>
export type GetAccSyncItemsById = (args: { ids: string[] }) => Promise<AccSyncItem[]>
export type GetAccSyncItemsByModelId = (args: {
ids: string[]
}) => Promise<AccSyncItem[]>
export type ListAccSyncItems = (args: {
projectId: string
@@ -1,4 +1,4 @@
import type { AccRegion, AccSyncItemStatus } from '@/modules/acc/domain/constants'
import type { AccRegion, AccSyncItemStatus } from '@/modules/acc/domain/acc/constants'
export type AccSyncItem = {
id: string
@@ -22,6 +22,36 @@ export type AccSyncItem = {
updatedAt: Date
}
export type DataManagementFolderContentsFolder = {
id: string
type: 'folders'
attributes: {
name?: string
displayName: string
objectCount: number
}
}
export type DataManagementFolderContentsItem = {
id: string
type: 'items'
attributes: {
name?: string
displayName: string
}
}
export type DataManagementFolderContentsItemVersion = {
id: string
type: 'versions'
attributes: {
name?: string
displayName: string
versionNumber: number
fileType?: string
}
}
export type ModelDerivativeServiceDesignManifest = {
type: 'manifest'
region: string
@@ -1,32 +1,10 @@
import type { StringEnumValues } from '@speckle/shared'
import { StringEnum } from '@speckle/shared'
export const ImporterAutomateFunctions = {
svf2: {
functionId: '4665e0b3ba',
functionReleaseId: '470ec84b63'
},
rvt: {
functionId: '0725cb0ac6',
functionReleaseId: 'b5c16a1606'
}
}
export const AccSyncItemStatuses = StringEnum([
// A new file version had been detected, and we are awaiting a processable file.
'pending',
// We are actively processing the new file version. (The Automate function has been triggered.)
'syncing',
'failed',
'paused',
'succeeded'
])
export type AccSyncItemStatus = StringEnumValues<typeof AccSyncItemStatuses>
export const AccRegions = StringEnum([
'US',
'EMEA',
'AUS',
'CAN',
'DEU',
'IND',
'JPN',
'GBR'
])
export type AccRegion = StringEnumValues<typeof AccRegions>
+35
View File
@@ -6,6 +6,30 @@ export class AccModuleDisabledError extends BaseError {
static statusCode = 423
}
export class AccNotAuthorizedError extends BaseError {
static defaultMessage = 'ACC token missing or not authorized'
static code = 'ACC_MODULE_NOT_AUTHORIZED'
static statusCode = 401
}
export class AccNotYetImplementedError extends BaseError {
static defaultMessage =
'This functionality for the ACC integration is not yet implemented'
static code = 'ACC_MODULE_NOT_YET_IMPLEMENTED'
static statusCode = 501
}
export class AutodeskApiRequestError extends BaseError {
static defaultMessage = 'Error during external request to Autodesk'
static code = 'ACC_AUTODESK_REQUEST_ERROR'
static statusCode = 500
constructor(method: string, endpoint: string) {
super()
this.message = `Failed to issue ${method} request to ${endpoint}`
}
}
export class DuplicateSyncItemError extends BaseError {
static defaultMessage = 'A sync item with this lineage urn already exists.'
static code = 'ACC_DUPLICATE_SYNC_ITEM_LINEAGE_URN'
@@ -28,3 +52,14 @@ export class SyncItemAutomationTriggerError extends BaseError {
static code = 'ACC_SYNC_ITEM_AUTOMATION_TRIGGER_ERROR'
static statusCode = 422
}
export class SyncItemUnsupportedFileExtensionError extends BaseError {
static defaultMessage = 'Cannot sync this file type from ACC'
static code = 'ACC_SYNC_ITEM_UNSUPPORTED_FILE_EXTENSION'
static statusCode = 422
constructor(fileExtension: string) {
super()
this.message = `Received sync item update with unsupported file extension ${fileExtension}`
}
}
@@ -1,4 +1,4 @@
import { AccSyncItemEvents } from '@/modules/acc/domain/events'
import { AccSyncItemEvents } from '@/modules/acc/domain/acc/events'
import type { EventBusListen, EventPayload } from '@/modules/shared/services/eventBus'
import type { PublishSubscription } from '@/modules/shared/utils/subscriptions'
import { ProjectSubscriptions } from '@/modules/shared/utils/subscriptions'
@@ -0,0 +1,94 @@
import { getFolderContents } from '@/modules/acc/clients/autodesk/acc'
import type {
AccSyncItem,
DataManagementFolderContentsFolder,
DataManagementFolderContentsItem
} from '@/modules/acc/domain/acc/types'
import {
getAccSyncItemsByIdFactory,
getAccSyncItemsByModelIdFactory
} from '@/modules/acc/repositories/accSyncItems'
import { defineRequestDataloaders } from '@/modules/shared/helpers/graphqlHelper'
import type { Nullable } from '@speckle/shared'
import { keyBy } from 'lodash-es'
declare module '@/modules/core/loaders' {
interface ModularizedDataLoaders
extends Partial<ReturnType<typeof dataLoadersDefinition>> {}
}
const dataLoadersDefinition = defineRequestDataloaders(
({ createLoader, deps: { db } }) => {
const getAccSyncItemsById = getAccSyncItemsByIdFactory({ db })
const getAccSyncItemsByModelId = getAccSyncItemsByModelIdFactory({ db })
return {
acc: {
getFolderChildren: createLoader<
{ projectId: string; folderId: string; token: string },
DataManagementFolderContentsFolder[],
string
>(
async (folderIds) => {
return await Promise.all(
folderIds.map(async ({ projectId, folderId, token }) => {
const items = await getFolderContents(
{ projectId, folderId, type: 'folders' },
{ token }
)
return items.filter(
(item): item is DataManagementFolderContentsFolder =>
item.type === 'folders'
)
})
)
},
{
cacheKeyFn: (args) => `${args.projectId}-${args.projectId}`
}
),
getFolderContents: createLoader<
{ projectId: string; folderId: string; token: string },
DataManagementFolderContentsItem[],
string
>(
async (folderIds) => {
return await Promise.all(
folderIds.map(async ({ projectId, folderId, token }) => {
const items = await getFolderContents(
{ projectId, folderId, type: 'items' },
{ token }
)
return items.filter(
(item): item is DataManagementFolderContentsItem =>
item.type === 'items'
)
})
)
},
{
cacheKeyFn: (args) => `${args.projectId}-${args.projectId}`
}
),
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)
}),
getAccSyncItemByModelId: createLoader<string, Nullable<AccSyncItem>>(
async (ids) => {
const results = keyBy(
await getAccSyncItemsByModelId({ ids: ids.slice() }),
(i) => i.modelId
)
return ids.map((i) => results[i] || null)
}
)
}
}
}
)
export default dataLoadersDefinition
@@ -59,15 +59,117 @@ import {
ProjectSubscriptions
} from '@/modules/shared/utils/subscriptions'
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
import { AccModuleDisabledError, SyncItemNotFoundError } from '@/modules/acc/errors/acc'
import {
AccModuleDisabledError,
AccNotAuthorizedError,
SyncItemNotFoundError
} from '@/modules/acc/errors/acc'
import { getFeatureFlags } from '@speckle/shared/environment'
import type { AccRegion } from '@/modules/acc/domain/constants'
import type { AccRegion } from '@/modules/acc/domain/acc/constants'
import { ProjectNotFoundError } from '@/modules/core/errors/projects'
import {
mapFolderToGql,
mapItemToGql,
mapVersionToGql
} from '@/modules/acc/helpers/acc'
import {
getFolderMetadata,
getItemLatestVersion
} from '@/modules/acc/clients/autodesk/acc'
const { FF_ACC_INTEGRATION_ENABLED, FF_AUTOMATE_MODULE_ENABLED } = getFeatureFlags()
const enableAcc = FF_ACC_INTEGRATION_ENABLED && FF_AUTOMATE_MODULE_ENABLED
const resolvers: Resolvers = {
WorkspaceIntegrations: {
acc: async (_parent, args, ctx) => {
if (!args.token) {
throw new AccNotAuthorizedError()
}
// TODO ACC: Replace with Speckle user - ACC user association
ctx.accToken = args.token
return {}
}
},
AccIntegration: {
folder: async (_parent, args) => {
const { projectId, folderId } = args
return {
id: folderId,
projectId
}
}
},
AccFolder: {
name: async (parent, _args, ctx) => {
if (parent.name) return parent.name
const folderMetadata = await getFolderMetadata(
{
projectId: parent.projectId,
folderId: parent.id
},
{
token: ctx.accToken!
}
)
return (
folderMetadata.attributes.name ?? folderMetadata.attributes.displayName ?? ''
)
},
contents: async (parent, _args, ctx) => {
const { id: folderId, projectId } = parent
const { accToken } = ctx
const files = await ctx.loaders.acc!.getFolderContents.load({
projectId,
folderId,
token: accToken!
})
const items = files.map((file) => ({
...mapItemToGql(file),
projectId
}))
return { items }
},
children: async (parent, _args, ctx) => {
const { id: folderId, projectId } = parent
const { accToken } = ctx
const folders = await ctx.loaders.acc!.getFolderChildren.load({
projectId,
folderId,
token: accToken!
})
const items = folders.map((folder) => ({
...mapFolderToGql(folder),
projectId
}))
return { items }
}
},
AccItem: {
latestVersion: async (parent, _args, ctx) => {
const { id: itemId, projectId } = parent
const { accToken } = ctx
const version = await getItemLatestVersion(
{
projectId,
itemId
},
{
token: accToken!
}
)
return mapVersionToGql(version)
}
},
Mutation: {
accSyncItemMutations: () => ({})
},
@@ -193,6 +295,14 @@ const resolvers: Resolvers = {
}
},
AccSyncItem: {
project: async (parent, _args, context) => {
const project = await context.loaders.streams.getStream.load(parent.projectId)
if (!project) throw new ProjectNotFoundError()
return project
},
model: async (parent, _args, context) => {
return await context.loaders.branches.getById.load(parent.modelId)
},
author: async (parent, _args, context) => {
return await context.loaders.users.getUser.load(parent.authorId)
}
@@ -237,7 +347,32 @@ const resolvers: Resolvers = {
resourceType: TokenResourceIdentifierType.Project
})
const syncItem = await ctx.loaders.acc.getAccSyncItem.load(id)
const syncItem = await ctx.loaders.acc!.getAccSyncItem.load(id)
if (!syncItem) {
throw new SyncItemNotFoundError()
}
return syncItem
}
},
Model: {
accSyncItem: async (parent, _args, context) => {
const authResult =
await context.authPolicies.project.canReadAccIntegrationSettings({
userId: context.userId,
projectId: parent.streamId
})
throwIfAuthNotOk(authResult)
throwIfResourceAccessNotAllowed({
resourceId: parent.streamId,
resourceAccessRules: context.resourceAccessRules,
resourceType: TokenResourceIdentifierType.Project
})
const syncItem = await context.loaders.acc!.getAccSyncItemByModelId.load(
parent.id
)
if (!syncItem) {
throw new SyncItemNotFoundError()
@@ -275,6 +410,11 @@ const resolvers: Resolvers = {
}
const disabledResolvers: Resolvers = {
WorkspaceIntegrations: {
async acc() {
throw new AccModuleDisabledError()
}
},
Mutation: {
accSyncItemMutations: () => ({})
},
@@ -297,6 +437,11 @@ const disabledResolvers: Resolvers = {
throw new AccModuleDisabledError()
}
},
Model: {
async accSyncItem() {
return null
}
},
Subscription: {
projectAccSyncItemsUpdated: {
subscribe: filteredSubscribe(
@@ -0,0 +1,9 @@
import type { Resolvers } from '@/modules/core/graph/generated/graphql'
const resolvers: Resolvers = {
Workspace: {
integrations: () => ({})
}
}
export default resolvers
@@ -0,0 +1,50 @@
import type {
DataManagementFolderContentsFolder,
DataManagementFolderContentsItem,
DataManagementFolderContentsItemVersion
} from '@/modules/acc/domain/acc/types'
import type {
AccFolderGraphQLReturn,
AccItemGraphQLReturn,
AccItemVersionGraphQLReturn
} from '@/modules/acc/helpers/graphTypes'
export const filterContentsToFolders = (
contents: (DataManagementFolderContentsFolder | DataManagementFolderContentsItem)[]
): DataManagementFolderContentsFolder[] => {
return contents.filter(
(entry): entry is DataManagementFolderContentsFolder => entry.type === 'folders'
)
}
export const filterContentsToItems = (
contents: (DataManagementFolderContentsFolder | DataManagementFolderContentsItem)[]
): DataManagementFolderContentsItem[] => {
return contents.filter(
(entry): entry is DataManagementFolderContentsItem => entry.type === 'items'
)
}
export const mapFolderToGql = (
folder: DataManagementFolderContentsFolder
): Omit<AccFolderGraphQLReturn, 'projectId'> => ({
id: folder.id,
name: folder.attributes.name ?? folder.attributes.displayName,
objectCount: folder.attributes.objectCount
})
export const mapItemToGql = (
item: DataManagementFolderContentsItem
): Omit<AccItemGraphQLReturn, 'projectId'> => ({
id: item.id,
name: item.attributes.name ?? item.attributes.displayName
})
export const mapVersionToGql = (
version: DataManagementFolderContentsItemVersion
): AccItemVersionGraphQLReturn => ({
id: version.id,
name: version.attributes.name ?? version.attributes.displayName,
versionNumber: version.attributes.versionNumber,
fileType: version.attributes.fileType
})
@@ -1,4 +1,25 @@
import type { AccSyncItem } from '@/modules/acc/domain/types'
import type { AccSyncItem } from '@/modules/acc/domain/acc/types'
export type WorkspaceIntegrationsGraphQLReturn = {}
export type AccIntegrationGraphQLReturn = {}
export type AccFolderGraphQLReturn = {
id: string
projectId: string
// Resolver will use name provided instead of re-fetching, if possible
name?: string
objectCount?: number
}
export type AccItemGraphQLReturn = {
id: string
name: string
projectId: string
}
export type AccItemVersionGraphQLReturn = {
id: string
name: string
versionNumber: number
fileType?: string
}
export type AccSyncItemGraphQLReturn = AccSyncItem
export type AccSyncItemMutationsGraphQLReturn = {}
@@ -1,4 +1,4 @@
import type { ModelDerivativeServiceDesignManifest } from '@/modules/acc/domain/types'
import type { ModelDerivativeServiceDesignManifest } from '@/modules/acc/domain/acc/types'
export const isReadyForImport = (
manifest: ModelDerivativeServiceDesignManifest
@@ -3,14 +3,15 @@ import type {
CountAccSyncItems,
DeleteAccSyncItemById,
GetAccSyncItemById,
GetAccSyncItemByModelId,
GetAccSyncItemsById,
ListAccSyncItems,
QueryAllAccSyncItems,
UpdateAccSyncItemStatus,
UpsertAccSyncItem
} from '@/modules/acc/domain/operations'
} from '@/modules/acc/domain/acc/operations'
import { executeBatchedSelect } from '@/modules/shared/helpers/dbHelper'
import type { AccSyncItem } from '@/modules/acc/domain/types'
import type { AccSyncItem } from '@/modules/acc/domain/acc/types'
import type { Knex } from 'knex'
import { without } from 'lodash-es'
@@ -30,6 +31,18 @@ export const getAccSyncItemByIdFactory =
)
}
export const getAccSyncItemByModelIdFactory =
(deps: { db: Knex }): GetAccSyncItemByModelId =>
async ({ modelId }) => {
return (
(await tables
.accSyncItems(deps.db)
.select()
.where(AccSyncItems.col.modelId, modelId)
.first()) ?? null
)
}
export const getAccSyncItemsByIdFactory =
(deps: { db: Knex }): GetAccSyncItemsById =>
async ({ ids }) => {
@@ -38,6 +51,17 @@ export const getAccSyncItemsByIdFactory =
return await tables.accSyncItems(deps.db).select().whereIn(AccSyncItems.col.id, ids)
}
export const getAccSyncItemsByModelIdFactory =
(deps: { db: Knex }): GetAccSyncItemsById =>
async ({ ids }) => {
if (!ids.length) return []
return await tables
.accSyncItems(deps.db)
.select()
.whereIn(AccSyncItems.col.modelId, ids)
}
export const upsertAccSyncItemFactory =
(deps: { db: Knex }): UpsertAccSyncItem =>
async (item) => {
+20 -9
View File
@@ -5,7 +5,7 @@ import {
exchangeCodeForTokens,
exchangeRefreshTokenForTokens,
generateCodeVerifier
} from '@/modules/acc/clients/autodesk'
} from '@/modules/acc/clients/autodesk/tokens'
import { sessionMiddlewareFactory } from '@/modules/auth/middleware'
import { corsMiddlewareFactory } from '@/modules/core/configs/cors'
import {
@@ -13,6 +13,7 @@ import {
getAutodeskIntegrationClientSecret,
getServerOrigin
} from '@/modules/shared/helpers/envHelper'
import { logger } from '@/observability/logging'
import type { Express } from 'express'
export const setupAccOidcEndpoints = (app: Express) => {
@@ -24,8 +25,8 @@ export const setupAccOidcEndpoints = (app: Express) => {
corsMiddleware,
sessionMiddleware,
async (req, res) => {
const { projectId } = req.body
req.session.projectId = projectId
const { callbackEndpoint } = req.body
req.session.callbackEndpoint = callbackEndpoint
const { codeVerifier, codeChallenge } = generateCodeVerifier()
req.session.codeVerifier = codeVerifier
@@ -66,7 +67,13 @@ export const setupAccOidcEndpoints = (app: Express) => {
req.session.accTokens = tokens
return res.redirect(`/projects/${req.session.projectId}/acc`)
logger.warn(req.session)
if (!req.session.callbackEndpoint) {
return res.status(500)
}
return res.redirect(req.session.callbackEndpoint)
} catch (error) {
console.error('Token exchange failed:', error)
return res.status(500).send({ error: 'Token exchange failed' })
@@ -75,10 +82,14 @@ export const setupAccOidcEndpoints = (app: Express) => {
)
app.get('/api/v1/acc/auth/status', corsMiddleware, sessionMiddleware, (req, res) => {
if (!req.session.accTokens) {
return res.status(404).send({ error: 'No ACC tokens found' })
try {
if (!req.session.accTokens) {
return res.status(404).send({ error: 'No ACC tokens found' })
}
res.send(req.session.accTokens)
} finally {
req.session.accTokens = undefined // we wanna return it just once
}
res.send(req.session.accTokens)
})
app.post(
@@ -86,7 +97,7 @@ export const setupAccOidcEndpoints = (app: Express) => {
corsMiddleware,
sessionMiddleware,
async (req, res) => {
const { refresh_token } = req.session.accTokens || {}
const { refresh_token } = req.body || {}
if (!refresh_token) {
return res.status(401).json({ error: 'No refresh token found' })
}
@@ -97,7 +108,7 @@ export const setupAccOidcEndpoints = (app: Express) => {
res.json(newTokens)
} catch (error) {
console.error('Error refreshing token:', error)
res.status(500).json({ error: 'Error refreshing token' })
res.status(500).json({ error })
}
}
)
@@ -1,9 +1,7 @@
import {
AccSyncItemStatuses,
ImporterAutomateFunctions
} from '@/modules/acc/domain/constants'
import type { UpdateAccSyncItemStatus } from '@/modules/acc/domain/operations'
import type { AccSyncItem } from '@/modules/acc/domain/types'
import { ImporterAutomateFunctions } from '@/modules/acc/domain/constants'
import { AccSyncItemStatuses } from '@/modules/acc/domain/acc/constants'
import type { UpdateAccSyncItemStatus } from '@/modules/acc/domain/acc/operations'
import type { AccSyncItem } from '@/modules/acc/domain/acc/types'
import {
SyncItemAutomationTriggerError,
SyncItemNotFoundError
@@ -22,7 +20,9 @@ import type { CreateAndStoreAppToken } from '@/modules/core/domain/tokens/operat
import { TokenResourceIdentifierType } from '@/modules/core/graph/generated/graphql'
import {
getAutodeskIntegrationClientId,
getAutodeskIntegrationClientSecret
getAutodeskIntegrationClientSecret,
getOdaUserId,
getOdaUserSecret
} from '@/modules/shared/helpers/envHelper'
import { logger } from '@/observability/logging'
import { Scopes } from '@speckle/shared'
@@ -92,8 +92,8 @@ export const triggerSyncItemAutomationFactory =
functionRuns: [
{
id: cryptoRandomString({ length: 15 }),
functionId: ImporterAutomateFunctions.svf2.functionId,
functionReleaseId: ImporterAutomateFunctions.svf2.functionReleaseId,
functionId: ImporterAutomateFunctions.rvt.functionId,
functionReleaseId: ImporterAutomateFunctions.rvt.functionReleaseId,
status: 'pending' as const,
elapsed: 0,
results: null,
@@ -148,9 +148,12 @@ export const triggerSyncItemAutomationFactory =
modelId: syncItem.modelId,
versionUrn: syncItem.accFileVersionUrn,
viewName: syncItem.accFileViewName ?? null,
autodeskProjectId: syncItem.accProjectId.replace('b.', ''),
autodeskRegion: syncItem.accRegion === 'EMEA' ? 1 : 0,
autodeskClientId: getAutodeskIntegrationClientId(),
autodeskClientSecret: getAutodeskIntegrationClientSecret()
autodeskClientSecret: getAutodeskIntegrationClientSecret(),
odaUserId: getOdaUserId(),
odaUserSignature: getOdaUserSecret()
}
})),
manifests: [
+2 -22
View File
@@ -4,9 +4,6 @@ import {
} from '@/modules/acc/repositories/accSyncItems'
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/helpers/svfUtils'
import type { Logger } from '@/observability/logging'
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
import {
getAutomationFactory,
@@ -22,7 +19,7 @@ import {
} from '@/modules/core/repositories/tokens'
import { createAppTokenFactory } from '@/modules/core/services/tokens'
import { TIME_MS } from '@speckle/shared'
import { AccSyncItemStatuses, type AccRegion } from '@/modules/acc/domain/constants'
import { AccSyncItemStatuses } from '@/modules/acc/domain/acc/constants'
import { triggerSyncItemAutomationFactory } from '@/modules/acc/services/automate'
const queryAllAccSyncItems = queryAllAccSyncItemsFactory({ db })
@@ -30,28 +27,11 @@ const queryAllAccSyncItems = queryAllAccSyncItemsFactory({ db })
export const schedulePendingSyncItemsCheck = (deps: {
scheduleExecution: ScheduleExecution
}) => {
const callback = async (_now: Date, { logger }: { logger: Logger }) => {
const tokenData = await getToken()
const callback = async () => {
for await (const items of queryAllAccSyncItems({
filter: { status: AccSyncItemStatuses.pending }
})) {
for (const syncItem of items) {
const manifest = await getManifestByUrn(
{
urn: syncItem.accFileVersionUrn,
region: syncItem.accRegion as AccRegion
},
{ token: tokenData.access_token }
)
const isReady = isReadyForImport(manifest)
logger.info(
{ isReady, syncItem, manifest },
'Checking pending sync item {syncItem.id} for import readiness.'
)
if (!isReady) continue
const projectDb = await getProjectDbClient({ projectId: syncItem.projectId })
await triggerSyncItemAutomationFactory({
@@ -1,23 +1,19 @@
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 { tryRegisterAccWebhook } from '@/modules/acc/clients/autodesk/acc'
import { ImporterAutomateFunctions } from '@/modules/acc/domain/constants'
import { AccSyncItemStatuses } from '@/modules/acc/domain/acc/constants'
import { AccSyncItemEvents } from '@/modules/acc/domain/acc/events'
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'
} from '@/modules/acc/domain/acc/operations'
import type { AccSyncItem } from '@/modules/acc/domain/acc/types'
import {
SyncItemNotFoundError,
SyncItemUnsupportedFileExtensionError
} from '@/modules/acc/errors/acc'
import type { TriggerSyncItemAutomation } from '@/modules/acc/services/automate'
import type {
CreateAutomation,
@@ -82,8 +78,8 @@ export const createAccSyncItemFactory =
automationId: automation.id,
functions: [
{
functionId: ImporterAutomateFunctions.svf2.functionId,
functionReleaseId: ImporterAutomateFunctions.svf2.functionReleaseId
functionId: ImporterAutomateFunctions.rvt.functionId,
functionReleaseId: ImporterAutomateFunctions.rvt.functionReleaseId
}
],
triggerDefinitions: {
@@ -114,7 +110,6 @@ export const createAccSyncItemFactory =
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,
@@ -125,19 +120,14 @@ export const createAccSyncItemFactory =
})
// 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 })
switch (newSyncItem.accFileExtension.toLowerCase()) {
case 'rvt': {
return await deps.triggerSyncItemAutomation({ id: newSyncItem.id })
}
default: {
throw new SyncItemUnsupportedFileExtensionError(newSyncItem.accFileExtension)
}
}
}
export type GetPaginatedAccSyncItems = (params: {
@@ -1,8 +1,8 @@
import { AccSyncItemStatuses } from '@/modules/acc/domain/constants'
import { AccSyncItemStatuses } from '@/modules/acc/domain/acc/constants'
import type {
QueryAllAccSyncItems,
UpsertAccSyncItem
} from '@/modules/acc/domain/operations'
} from '@/modules/acc/domain/acc/operations'
import { logger } from '@/observability/logging'
type OnVersionAdded = (params: {
@@ -92,8 +92,6 @@ 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> {}
@@ -141,7 +139,6 @@ const dataLoadersDefinition = defineRequestDataloaders(
const getStreamsCollaboratorCounts = getStreamsCollaboratorCountsFactory({
db
})
const getAccSyncItemsById = getAccSyncItemsByIdFactory({ db })
return {
streams: {
@@ -551,15 +548,6 @@ 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(
@@ -15,7 +15,7 @@ import type { ActivityCollectionGraphQLReturn } from '@/modules/activitystream/h
import type { ServerAppGraphQLReturn, ServerAppListItemGraphQLReturn } from '@/modules/auth/helpers/graphTypes';
import type { GendoAIRenderGraphQLReturn } from '@/modules/gendo/helpers/types/graphTypes';
import type { ServerRegionItemGraphQLReturn } from '@/modules/multiregion/helpers/graphTypes';
import type { AccSyncItemGraphQLReturn, AccSyncItemMutationsGraphQLReturn } from '@/modules/acc/helpers/graphTypes';
import type { WorkspaceIntegrationsGraphQLReturn, AccIntegrationGraphQLReturn, AccFolderGraphQLReturn, AccItemGraphQLReturn, AccSyncItemGraphQLReturn, AccSyncItemMutationsGraphQLReturn } from '@/modules/acc/helpers/graphTypes';
import type { SavedViewGraphQLReturn, SavedViewGroupGraphQLReturn, SavedViewPermissionChecksGraphQLReturn, SavedViewGroupPermissionChecksGraphQLReturn, ProjectSavedViewsUpdatedMessageGraphQLReturn, ProjectSavedViewGroupsUpdatedMessageGraphQLReturn, BeforeChangeSavedViewGraphQLReturn, ExtendedViewerResourcesGraphQLReturn } from '@/modules/viewer/helpers/graphTypes';
import type { DashboardGraphQLReturn, DashboardMutationsGraphQLReturn, DashboardPermissionChecksGraphQLReturn, DashboardTokenGraphQLReturn } from '@/modules/dashboards/helpers/graphTypes';
import type { GraphQLContext } from '@/modules/shared/helpers/typeHelper';
@@ -44,6 +44,113 @@ export type Scalars = {
JSONObject: { input: Record<string, unknown>; output: Record<string, unknown>; }
};
export type AccFolder = {
__typename?: 'AccFolder';
children: AccFolderCollection;
contents: AccItemCollection;
id: Scalars['ID']['output'];
name: Scalars['String']['output'];
};
export type AccFolderCollection = {
__typename?: 'AccFolderCollection';
cursor?: Maybe<Scalars['String']['output']>;
items: Array<AccFolder>;
};
export type AccHub = {
__typename?: 'AccHub';
id: Scalars['ID']['output'];
name: Scalars['String']['output'];
project: AccProject;
projects: AccProjectCollection;
};
export type AccHubProjectArgs = {
id: Scalars['ID']['input'];
};
export type AccHubCollection = {
__typename?: 'AccHubCollection';
cursor?: Maybe<Scalars['String']['output']>;
items: Array<AccHub>;
};
export type AccIntegration = {
__typename?: 'AccIntegration';
folder: AccFolder;
hub: AccHub;
hubs: AccHubCollection;
item: AccItem;
project: AccProject;
};
export type AccIntegrationFolderArgs = {
folderId: Scalars['String']['input'];
projectId: Scalars['String']['input'];
};
export type AccIntegrationHubArgs = {
id: Scalars['String']['input'];
};
export type AccIntegrationItemArgs = {
itemId: Scalars['String']['input'];
projectId: Scalars['String']['input'];
};
export type AccIntegrationProjectArgs = {
hubId: Scalars['String']['input'];
projectId: Scalars['String']['input'];
};
export type AccItem = {
__typename?: 'AccItem';
/** lineage urn */
id: Scalars['ID']['output'];
latestVersion: AccItemVersion;
name: Scalars['String']['output'];
};
export type AccItemCollection = {
__typename?: 'AccItemCollection';
cursor?: Maybe<Scalars['String']['output']>;
items: Array<AccItem>;
};
export type AccItemVersion = {
__typename?: 'AccItemVersion';
fileType?: Maybe<Scalars['String']['output']>;
/** version urn */
id: Scalars['ID']['output'];
name: Scalars['String']['output'];
versionNumber: Scalars['Int']['output'];
};
export type AccProject = {
__typename?: 'AccProject';
folder: AccFolder;
id: Scalars['ID']['output'];
name: Scalars['String']['output'];
rootFolder: AccFolder;
};
export type AccProjectFolderArgs = {
id: Scalars['String']['input'];
};
export type AccProjectCollection = {
__typename?: 'AccProjectCollection';
cursor?: Maybe<Scalars['String']['output']>;
items: Array<AccProject>;
};
export type AccSyncItem = {
__typename?: 'AccSyncItem';
accFileExtension: Scalars['String']['output'];
@@ -60,8 +167,8 @@ export type AccSyncItem = {
author?: Maybe<LimitedUser>;
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
modelId: Scalars['String']['output'];
projectId: Scalars['String']['output'];
model?: Maybe<Model>;
project: Project;
status: AccSyncItemStatus;
updatedAt: Scalars['DateTime']['output'];
};
@@ -1762,6 +1869,7 @@ export type MarkReceivedVersionInput = {
export type Model = {
__typename?: 'Model';
accSyncItem?: Maybe<AccSyncItem>;
author?: Maybe<LimitedUser>;
automationsStatus?: Maybe<TriggeredAutomationsStatus>;
/** Return a model tree of children */
@@ -5497,6 +5605,7 @@ export type Workspace = {
embedOptions: WorkspaceEmbedOptions;
hasAccessToFeature: Scalars['Boolean']['output'];
id: Scalars['ID']['output'];
integrations?: Maybe<WorkspaceIntegrations>;
/** Only available to workspace owners/members */
invitedTeam?: Maybe<Array<PendingWorkspaceCollaborator>>;
/** Exclusive workspaces do not allow their workspace members to create or join other workspaces as members. */
@@ -5684,6 +5793,16 @@ export type WorkspaceIdentifier =
{ id: Scalars['String']['input']; slug?: never; }
| { id?: never; slug: Scalars['String']['input']; };
export type WorkspaceIntegrations = {
__typename?: 'WorkspaceIntegrations';
acc?: Maybe<AccIntegration>;
};
export type WorkspaceIntegrationsAccArgs = {
token?: InputMaybe<Scalars['String']['input']>;
};
export type WorkspaceInviteCreateInput = {
/** Either this or userId must be filled */
email?: InputMaybe<Scalars['String']['input']>;
@@ -6264,6 +6383,16 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
/** Mapping between all available schema types and the resolvers types */
export type ResolversTypes = {
AccFolder: ResolverTypeWrapper<AccFolderGraphQLReturn>;
AccFolderCollection: ResolverTypeWrapper<Omit<AccFolderCollection, 'items'> & { items: Array<ResolversTypes['AccFolder']> }>;
AccHub: ResolverTypeWrapper<Omit<AccHub, 'project' | 'projects'> & { project: ResolversTypes['AccProject'], projects: ResolversTypes['AccProjectCollection'] }>;
AccHubCollection: ResolverTypeWrapper<Omit<AccHubCollection, 'items'> & { items: Array<ResolversTypes['AccHub']> }>;
AccIntegration: ResolverTypeWrapper<AccIntegrationGraphQLReturn>;
AccItem: ResolverTypeWrapper<AccItemGraphQLReturn>;
AccItemCollection: ResolverTypeWrapper<Omit<AccItemCollection, 'items'> & { items: Array<ResolversTypes['AccItem']> }>;
AccItemVersion: ResolverTypeWrapper<AccItemVersion>;
AccProject: ResolverTypeWrapper<Omit<AccProject, 'folder' | 'rootFolder'> & { folder: ResolversTypes['AccFolder'], rootFolder: ResolversTypes['AccFolder'] }>;
AccProjectCollection: ResolverTypeWrapper<Omit<AccProjectCollection, 'items'> & { items: Array<ResolversTypes['AccProject']> }>;
AccSyncItem: ResolverTypeWrapper<AccSyncItemGraphQLReturn>;
AccSyncItemCollection: ResolverTypeWrapper<Omit<AccSyncItemCollection, 'items'> & { items: Array<ResolversTypes['AccSyncItem']> }>;
AccSyncItemMutations: ResolverTypeWrapper<AccSyncItemMutationsGraphQLReturn>;
@@ -6625,6 +6754,7 @@ export type ResolversTypes = {
WorkspaceFeatureFlagName: WorkspaceFeatureFlagName;
WorkspaceFeatureName: WorkspaceFeatureName;
WorkspaceIdentifier: WorkspaceIdentifier;
WorkspaceIntegrations: ResolverTypeWrapper<WorkspaceIntegrationsGraphQLReturn>;
WorkspaceInviteCreateInput: WorkspaceInviteCreateInput;
WorkspaceInviteLookupOptions: WorkspaceInviteLookupOptions;
WorkspaceInviteMutations: ResolverTypeWrapper<WorkspaceInviteMutationsGraphQLReturn>;
@@ -6674,6 +6804,16 @@ export type ResolversTypes = {
/** Mapping between all available schema types and the resolvers parents */
export type ResolversParentTypes = {
AccFolder: AccFolderGraphQLReturn;
AccFolderCollection: Omit<AccFolderCollection, 'items'> & { items: Array<ResolversParentTypes['AccFolder']> };
AccHub: Omit<AccHub, 'project' | 'projects'> & { project: ResolversParentTypes['AccProject'], projects: ResolversParentTypes['AccProjectCollection'] };
AccHubCollection: Omit<AccHubCollection, 'items'> & { items: Array<ResolversParentTypes['AccHub']> };
AccIntegration: AccIntegrationGraphQLReturn;
AccItem: AccItemGraphQLReturn;
AccItemCollection: Omit<AccItemCollection, 'items'> & { items: Array<ResolversParentTypes['AccItem']> };
AccItemVersion: AccItemVersion;
AccProject: Omit<AccProject, 'folder' | 'rootFolder'> & { folder: ResolversParentTypes['AccFolder'], rootFolder: ResolversParentTypes['AccFolder'] };
AccProjectCollection: Omit<AccProjectCollection, 'items'> & { items: Array<ResolversParentTypes['AccProject']> };
AccSyncItem: AccSyncItemGraphQLReturn;
AccSyncItemCollection: Omit<AccSyncItemCollection, 'items'> & { items: Array<ResolversParentTypes['AccSyncItem']> };
AccSyncItemMutations: AccSyncItemMutationsGraphQLReturn;
@@ -7002,6 +7142,7 @@ export type ResolversParentTypes = {
WorkspaceDomainDeleteInput: WorkspaceDomainDeleteInput;
WorkspaceEmbedOptions: WorkspaceEmbedOptions;
WorkspaceIdentifier: WorkspaceIdentifier;
WorkspaceIntegrations: WorkspaceIntegrationsGraphQLReturn;
WorkspaceInviteCreateInput: WorkspaceInviteCreateInput;
WorkspaceInviteLookupOptions: WorkspaceInviteLookupOptions;
WorkspaceInviteMutations: WorkspaceInviteMutationsGraphQLReturn;
@@ -7076,6 +7217,78 @@ export type IsOwnerDirectiveArgs = { };
export type IsOwnerDirectiveResolver<Result, Parent, ContextType = GraphQLContext, Args = IsOwnerDirectiveArgs> = DirectiveResolverFn<Result, Parent, ContextType, Args>;
export type AccFolderResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccFolder'] = ResolversParentTypes['AccFolder']> = {
children?: Resolver<ResolversTypes['AccFolderCollection'], ParentType, ContextType>;
contents?: Resolver<ResolversTypes['AccItemCollection'], ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type AccFolderCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccFolderCollection'] = ResolversParentTypes['AccFolderCollection']> = {
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
items?: Resolver<Array<ResolversTypes['AccFolder']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type AccHubResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccHub'] = ResolversParentTypes['AccHub']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
project?: Resolver<ResolversTypes['AccProject'], ParentType, ContextType, RequireFields<AccHubProjectArgs, 'id'>>;
projects?: Resolver<ResolversTypes['AccProjectCollection'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type AccHubCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccHubCollection'] = ResolversParentTypes['AccHubCollection']> = {
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
items?: Resolver<Array<ResolversTypes['AccHub']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type AccIntegrationResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccIntegration'] = ResolversParentTypes['AccIntegration']> = {
folder?: Resolver<ResolversTypes['AccFolder'], ParentType, ContextType, RequireFields<AccIntegrationFolderArgs, 'folderId' | 'projectId'>>;
hub?: Resolver<ResolversTypes['AccHub'], ParentType, ContextType, RequireFields<AccIntegrationHubArgs, 'id'>>;
hubs?: Resolver<ResolversTypes['AccHubCollection'], ParentType, ContextType>;
item?: Resolver<ResolversTypes['AccItem'], ParentType, ContextType, RequireFields<AccIntegrationItemArgs, 'itemId' | 'projectId'>>;
project?: Resolver<ResolversTypes['AccProject'], ParentType, ContextType, RequireFields<AccIntegrationProjectArgs, 'hubId' | 'projectId'>>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type AccItemResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccItem'] = ResolversParentTypes['AccItem']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
latestVersion?: Resolver<ResolversTypes['AccItemVersion'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type AccItemCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccItemCollection'] = ResolversParentTypes['AccItemCollection']> = {
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
items?: Resolver<Array<ResolversTypes['AccItem']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type AccItemVersionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccItemVersion'] = ResolversParentTypes['AccItemVersion']> = {
fileType?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
versionNumber?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type AccProjectResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccProject'] = ResolversParentTypes['AccProject']> = {
folder?: Resolver<ResolversTypes['AccFolder'], ParentType, ContextType, RequireFields<AccProjectFolderArgs, 'id'>>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
rootFolder?: Resolver<ResolversTypes['AccFolder'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type AccProjectCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccProjectCollection'] = ResolversParentTypes['AccProjectCollection']> = {
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
items?: Resolver<Array<ResolversTypes['AccProject']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type AccSyncItemResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccSyncItem'] = ResolversParentTypes['AccSyncItem']> = {
accFileExtension?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
accFileLineageUrn?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -7091,8 +7304,8 @@ export type AccSyncItemResolvers<ContextType = GraphQLContext, ParentType extend
author?: Resolver<Maybe<ResolversTypes['LimitedUser']>, ParentType, ContextType>;
createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
modelId?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
projectId?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
model?: Resolver<Maybe<ResolversTypes['Model']>, ParentType, ContextType>;
project?: Resolver<ResolversTypes['Project'], ParentType, ContextType>;
status?: Resolver<ResolversTypes['AccSyncItemStatus'], ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@@ -7811,6 +8024,7 @@ export type LimitedWorkspaceJoinRequestCollectionResolvers<ContextType = GraphQL
};
export type ModelResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['Model'] = ResolversParentTypes['Model']> = {
accSyncItem?: Resolver<Maybe<ResolversTypes['AccSyncItem']>, ParentType, ContextType>;
author?: Resolver<Maybe<ResolversTypes['LimitedUser']>, ParentType, ContextType>;
automationsStatus?: Resolver<Maybe<ResolversTypes['TriggeredAutomationsStatus']>, ParentType, ContextType>;
childrenTree?: Resolver<Array<ResolversTypes['ModelsTreeItem']>, ParentType, ContextType>;
@@ -8968,6 +9182,7 @@ export type WorkspaceResolvers<ContextType = GraphQLContext, ParentType extends
embedOptions?: Resolver<ResolversTypes['WorkspaceEmbedOptions'], ParentType, ContextType>;
hasAccessToFeature?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<WorkspaceHasAccessToFeatureArgs, 'featureName'>>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
integrations?: Resolver<Maybe<ResolversTypes['WorkspaceIntegrations']>, ParentType, ContextType>;
invitedTeam?: Resolver<Maybe<Array<ResolversTypes['PendingWorkspaceCollaborator']>>, ParentType, ContextType, Partial<WorkspaceInvitedTeamArgs>>;
isExclusive?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
logo?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -9038,6 +9253,11 @@ export type WorkspaceEmbedOptionsResolvers<ContextType = GraphQLContext, ParentT
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type WorkspaceIntegrationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceIntegrations'] = ResolversParentTypes['WorkspaceIntegrations']> = {
acc?: Resolver<Maybe<ResolversTypes['AccIntegration']>, ParentType, ContextType, Partial<WorkspaceIntegrationsAccArgs>>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type WorkspaceInviteMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceInviteMutations'] = ResolversParentTypes['WorkspaceInviteMutations']> = {
batchCreate?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceInviteMutationsBatchCreateArgs, 'input' | 'workspaceId'>>;
cancel?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceInviteMutationsCancelArgs, 'inviteId' | 'workspaceId'>>;
@@ -9220,6 +9440,16 @@ export type WorkspaceUpdatedMessageResolvers<ContextType = GraphQLContext, Paren
};
export type Resolvers<ContextType = GraphQLContext> = {
AccFolder?: AccFolderResolvers<ContextType>;
AccFolderCollection?: AccFolderCollectionResolvers<ContextType>;
AccHub?: AccHubResolvers<ContextType>;
AccHubCollection?: AccHubCollectionResolvers<ContextType>;
AccIntegration?: AccIntegrationResolvers<ContextType>;
AccItem?: AccItemResolvers<ContextType>;
AccItemCollection?: AccItemCollectionResolvers<ContextType>;
AccItemVersion?: AccItemVersionResolvers<ContextType>;
AccProject?: AccProjectResolvers<ContextType>;
AccProjectCollection?: AccProjectCollectionResolvers<ContextType>;
AccSyncItem?: AccSyncItemResolvers<ContextType>;
AccSyncItemCollection?: AccSyncItemCollectionResolvers<ContextType>;
AccSyncItemMutations?: AccSyncItemMutationsResolvers<ContextType>;
@@ -9414,6 +9644,7 @@ export type Resolvers<ContextType = GraphQLContext> = {
WorkspaceCreationState?: WorkspaceCreationStateResolvers<ContextType>;
WorkspaceDomain?: WorkspaceDomainResolvers<ContextType>;
WorkspaceEmbedOptions?: WorkspaceEmbedOptionsResolvers<ContextType>;
WorkspaceIntegrations?: WorkspaceIntegrationsResolvers<ContextType>;
WorkspaceInviteMutations?: WorkspaceInviteMutationsResolvers<ContextType>;
WorkspaceJoinRequest?: WorkspaceJoinRequestResolvers<ContextType>;
WorkspaceJoinRequestCollection?: WorkspaceJoinRequestCollectionResolvers<ContextType>;
@@ -548,5 +548,13 @@ export function getAutodeskIntegrationClientSecret() {
return getStringFromEnv('AUTODESK_INTEGRATION_CLIENT_SECRET')
}
export function getOdaUserId() {
return getStringFromEnv('ODA_USER_ID')
}
export function getOdaUserSecret() {
return getStringFromEnv('ODA_USER_SECRET')
}
export const areSavedViewsEnabled = (): boolean =>
getFeatureFlags().FF_SAVED_VIEWS_ENABLED
@@ -56,6 +56,8 @@ export type GraphQLContext = BaseContext &
authPolicies: AuthPolicies & {
clearCache: () => void
}
// TODO: Remove in favor of `x-user-id` header
accToken?: string
/**
* Request-scoped GraphQL dataloaders
* @see https://github.com/graphql/dataloader
@@ -56,7 +56,7 @@ import type {
import type {
accSyncItemEventsNamespace,
AccSyncItemEventsPayloads
} from '@/modules/acc/domain/events'
} from '@/modules/acc/domain/acc/events'
import type {
emailsEventNamespace,
EmailsEventsPayloads
+1 -1
View File
@@ -14,5 +14,5 @@ declare module 'http' {
export type AccSessionData = {
accTokens?: AccTokens
codeVerifier?: string
projectId?: string
callbackEndpoint?: string
}
+2 -1
View File
@@ -2,8 +2,9 @@ export type AccTokens = {
access_token: string
refresh_token: string
token_type: string
id_token: string
id_token?: string | undefined
expires_in: number
timestamp: number
}
export type AccUserInfo = {
@@ -622,6 +622,15 @@ Generate the environment variables for Speckle server and Speckle objects deploy
secretKeyRef:
name: {{ default .Values.secretName .Values.server.accIntegration.clientSecret.secretName }}
key: {{ default "acc_integration_client_secret" .Values.server.accIntegration.clientSecret.secretKey }}
- name: ODA_USER_ID
value: {{ default "user_id" .Values.server.oda.userId }}
- name: ODA_USER_SECRET
valueFrom:
secretKeyRef:
name: {{ default .Values.secretName .Values.server.oda.userSecret.secretName }}
key: {{ default "user_secret" .Values.server.oda.userSecret.secretKey }}
{{- end }}
- name: FF_DASHBOARDS_MODULE_ENABLED
@@ -54,6 +54,7 @@ secrets:
{{- end }}
{{- if .Values.featureFlags.accIntegrationEnabled }}
- name: {{ default .Values.secretName .Values.server.accIntegration.clientSecret.secretName }}
- name: {{ default .Values.secretName .Values.server.oda.userSecret.secretName }}
{{- end }}
{{- if .Values.featureFlags.nextGenFileImporterEnabled }}
- name: {{ default .Values.secretName .Values.ifc_import_service.db.connectionString.secretName }}
@@ -54,6 +54,7 @@ secrets:
{{- end }}
{{- if .Values.featureFlags.accIntegrationEnabled }}
- name: {{ default .Values.secretName .Values.server.accIntegration.clientSecret.secretName }}
- name: {{ default .Values.secretName .Values.server.oda.userSecret.secretName }}
{{- end }}
{{- if .Values.featureFlags.nextGenFileImporterEnabled }}
- name: {{ default .Values.secretName .Values.ifc_import_service.db.connectionString.secretName }}