Initial implementation

This commit is contained in:
oguzhankoral
2025-07-11 05:29:12 +03:00
parent 73f5fd4cf5
commit 22bb18cc10
26 changed files with 1940 additions and 1 deletions
@@ -0,0 +1,76 @@
<template>
<div
class="border py-1 px-2 rounded"
:class="selected ? 'bg-blue-100' : 'bg-foundation'"
>
<div class="flex flex-col justify-between">
<button
class="flex flex-row justify-between items-center"
@click="$emit('select', folderContent)"
>
<div class="text-body-xs text-foreground">
{{ folderContent.attributes.displayName || folderContent.attributes.name }}
</div>
<div class="flex flex-row gap-2 items-center" :class="expanded ? 'mb-1' : ''">
<FormButton
v-if="folderContent.storageUrn"
:icon-left="ArrowDownTrayIcon"
hide-text
color="outline"
size="sm"
@click.stop="$emit('download', folderContent)"
>
Details
</FormButton>
<FormButton
size="sm"
hide-text
:icon-left="!expanded ? ChevronDownIcon : ChevronUpIcon"
color="outline"
@click.stop="expanded = !expanded"
></FormButton>
</div>
</button>
<div v-if="expanded" class="space-y-1">
<hr />
<div class="text-xs italic">Type: {{ folderContent.type }}</div>
<div class="text-xs text-gray-500 break-all">
Lineage ID: {{ folderContent.id }}
</div>
<div
v-if="folderContent.latestVersionId"
class="text-xs text-blue-500 break-all"
>
Version ID: {{ folderContent.latestVersionId }}
</div>
<div v-if="folderContent.storageUrn" class="text-xs text-green-600 break-all">
Storage URN: {{ folderContent.storageUrn }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {
ArrowDownTrayIcon,
ChevronDownIcon,
ChevronUpIcon
} from '@heroicons/vue/20/solid'
import type { AccItem } from '~/lib/acc/types'
defineProps<{
folderContent: AccItem
loading: boolean
selected: boolean
}>()
defineEmits<{
(e: 'download', item: AccItem): void
(e: 'select', item: AccItem): void
}>()
const expanded = ref(false)
</script>
@@ -0,0 +1,39 @@
<template>
<div>
<div class="flex text-body-xs text-foreground font-medium">Files</div>
<div v-if="loading" class="text-xs italic">Loading files...</div>
<div v-else-if="folderContents.length" class="flex flex-col space-y-0.5">
<div v-for="item in revitContents" :key="item.id">
<ProjectPageAccFileItem
:folder-content="item"
:loading="loading"
:selected="item.id === selectedFolderContent?.id"
@download="(i) => $emit('download', i)"
@select="(i) => $emit('select', i)"
></ProjectPageAccFileItem>
</div>
</div>
<div v-else class="text-xs italic">No files found in this folder.</div>
</div>
</template>
<script setup lang="ts">
import type { AccItem } from '~/lib/acc/types'
const props = defineProps<{
selectedFolderContent: AccItem | undefined
folderContents: AccItem[]
loading: boolean
}>()
defineEmits<{
(e: 'download', item: AccItem): void
(e: 'select', item: AccItem): void
}>()
const revitContents = computed(() =>
props.folderContents.filter((fc) =>
(fc.attributes.name || fc.attributes.displayName).includes('.rvt')
)
)
</script>
@@ -0,0 +1,59 @@
<template>
<div>
<FormSelectBase
v-model="selectedHub"
name="accHubs"
label="Hubs"
show-label
:items="hubs"
size="base"
color="foundation"
placeholder="Select hub"
@update:model-value="handleHubChange"
>
<template #something-selected="{ value }">
{{ isArray(value) ? value[0].attributes.name : value.attributes.name }}
</template>
<template #option="{ item }">
{{ item.attributes.name }}
</template>
</FormSelectBase>
<div v-if="!loading && hubs.length == 0" class="text-xs italic">No hubs found.</div>
</div>
</template>
<script setup lang="ts">
import { isArray } from 'lodash-es'
import type { AccHub } from '~/lib/acc/types'
const props = defineProps<{
hubs: AccHub[]
loading: boolean
}>()
const emits = defineEmits<{
(e: 'hub-selected', hub: AccHub): void
}>()
const handleHubChange = (newHub: AccHub | AccHub[] | undefined) => {
// is array not likely but make TS happy
if (!newHub || isArray(newHub)) {
return
}
emits('hub-selected', newHub)
}
const selectedHub = ref<AccHub>()
watch(
() => props.hubs,
(newHubs) => {
if (newHubs.length > 0) {
selectedHub.value = newHubs[0]
emits('hub-selected', newHubs[0])
}
},
{ immediate: true }
)
</script>
@@ -0,0 +1,62 @@
<template>
<div>
<FormSelectBase
v-model="selectedProject"
name="accProjects"
label="Projects"
show-label
:items="projects"
size="base"
color="foundation"
placeholder="Select hub"
@update:model-value="handleProjectChange"
>
<template #something-selected="{ value }">
{{ isArray(value) ? value[0].attributes.name : value.attributes.name }}
</template>
<template #option="{ item }">
{{ item.attributes.name }}
</template>
</FormSelectBase>
<div v-if="!loading && projects.length == 0" class="text-xs italic">
No projects found.
</div>
</div>
</template>
<script setup lang="ts">
import { isArray } from 'lodash-es'
import type { AccProject } from '~/lib/acc/types'
const props = defineProps<{
hubId: string
projects: AccProject[]
loading: boolean
}>()
const emits = defineEmits<{
(e: 'project-selected', hubId: string, projectId: string): void
}>()
const selectedProject = ref<AccProject>()
const handleProjectChange = (newProject: AccProject | AccProject[] | undefined) => {
// is array not likely but make TS happy
if (!newProject || isArray(newProject)) {
return
}
emits('project-selected', props.hubId, newProject.id)
}
watch(
() => props.projects,
(newProjects) => {
if (newProjects.length > 0) {
selectedProject.value = newProjects[0]
emits('project-selected', props.hubId, newProjects[0].id)
}
},
{ immediate: true }
)
</script>
@@ -0,0 +1,38 @@
<template>
<CommonBadge
:color-classes="
[runStatusClasses(status), 'shrink-0 grow-0 text-foreground'].join(' ')
"
>
{{ status.toUpperCase() }}
</CommonBadge>
</template>
<script setup lang="ts">
import type { AccSyncItemStatus } from '~/lib/acc/types'
defineProps<{
status: AccSyncItemStatus
}>()
const runStatusClasses = (run: AccSyncItemStatus) => {
const classParts = ['w-24 justify-center']
switch (run) {
case 'syncing':
classParts.push('bg-info-lighter')
break
case 'paused':
classParts.push('bg-warning-lighter')
break
case 'failed':
classParts.push('bg-danger-lighter')
break
case 'sync':
classParts.push('bg-success-lighter')
break
}
return classParts.join(' ')
}
</script>
@@ -0,0 +1,400 @@
<template>
<div class="flex flex-col space-y-2">
<div class="flex text-body-xs text-foreground font-medium">Sync items</div>
<LayoutTable
class="bg-foundation"
:columns="[
{ id: 'status', header: 'Status', classes: 'col-span-2' },
{ id: 'accFileName', header: 'File name', classes: 'col-span-3' },
{ id: 'modelName', header: 'Model name', classes: 'col-span-3' },
{ id: 'createdBy', header: 'Created by', classes: 'col-span-3' }
]"
:items="syncs"
>
<template #status="{ item }">
<ProjectPageAccSyncStatus :status="item.status" />
</template>
<template #accFileName="{ item }">
{{ item.accItem.attributes.name || item.accItem.attributes.displayName }}
</template>
<template #modelName="{ item }">
{{ item.modelName }}
</template>
<template #createdBy="{ item }">
{{ item.createdBy }}
</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">
<div class="flex flex-col space-y-2">
<div v-if="step === 0">
<ProjectPageAccHubs
:hubs="hubs"
:loading="loadingHubs"
@hub-selected="onHubClick"
/>
<ProjectPageAccProjects
v-if="selectedHubId"
:hub-id="selectedHubId"
:projects="projects"
:loading="loadingProjects"
@project-selected="onProjectClick"
/>
<ProjectPageAccFiles
v-if="selectedProjectId"
:folder-contents="folderContents"
:selected-folder-content="selectedFolderContent"
:loading="loadingFiles"
@download="onDownloadClick"
@select="onFileSelected"
/>
<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="!selectedFolderContent" @click="step++">
Next
</FormButton>
</div>
</div>
<div v-if="step === 1" class="flex flex-col justify-between space-y-2">
<div>
Selected ACC file:
{{
selectedFolderContent?.attributes.name ||
selectedFolderContent?.attributes.displayName
}}
</div>
<FormSelectBase
v-model="selectedModel"
:label="'Models'"
:name="'accModelSelector'"
show-label
:items="models"
mount-menu-on-body
>
<template #something-selected="{ value }">
{{ isArray(value) ? value[0].name : value.name }}
</template>
<template #option="{ item }">
{{ item.name }}
</template>
</FormSelectBase>
<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="!selectedModel" @click="addSync">
Add
</FormButton>
</div>
</div>
</div>
</LayoutDialog>
</div>
</template>
<script setup lang="ts">
import type {
AccTokens,
AccHub,
AccProject,
AccItem,
AccSyncItem
} from '~/lib/acc/types'
import { ref, computed } from 'vue'
import type {
ProjectLatestModelsPaginationQueryVariables,
ProjectPageLatestItemsModelItemFragment
} from '~/lib/common/generated/gql/graphql'
import { useQuery } from '@vue/apollo-composable'
import { latestModelsQuery } from '~/lib/projects/graphql/queries'
import { isArray } from 'lodash-es'
const props = defineProps<{
projectId: string
tokens: AccTokens | undefined
syncs: AccSyncItem[]
isLoggedIn: boolean
}>()
const internalSyncs = computed(() => props.syncs)
const step = ref(0)
const showNewSyncDialog = ref(false)
const { triggerNotification } = useGlobalToast()
const tokens = computed(() => props.tokens)
const hubs = ref<AccHub[]>([])
const loadingHubs = ref(false)
const selectedHub = ref<AccHub | null>(null)
const selectedHubId = ref<string | null>(null)
const folderUrn = ref<string | null>(null)
const projects = ref<AccProject[]>([])
const loadingProjects = ref(false)
const selectedProjectId = ref<string | null>(null)
const folderContents = ref<AccItem[]>([])
const selectedFolderContent = ref<AccItem>()
const loadingFiles = ref(false)
const searchText = ref<string>()
const selectedModel = ref<ProjectPageLatestItemsModelItemFragment>()
const latestModelsQueryVariables = computed(
(): ProjectLatestModelsPaginationQueryVariables => {
const shouldHaveFilter = searchText.value && searchText.value.length > 0
return {
projectId: props.projectId,
filter: shouldHaveFilter
? {
search: searchText.value || null
}
: null
}
}
)
const { result: baseResult } = useQuery(
latestModelsQuery,
() => latestModelsQueryVariables.value
)
const models = computed(() => baseResult.value?.project?.models?.items || [])
const fetchHubs = async () => {
loadingHubs.value = true
try {
const res = await fetch('https://developer.api.autodesk.com/project/v1/hubs', {
headers: { Authorization: `Bearer ${tokens.value!.access_token}` }
})
if (!res.ok) throw new Error('Failed to fetch hubs')
hubs.value = (await res.json()).data
// console.log('Hubs', hubs.value)
} catch (error) {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Failed to fetch hubs',
description: error instanceof Error ? error.message : 'Unexpected error'
})
} finally {
loadingHubs.value = false
}
}
const onHubClick = async (hub: AccHub) => {
selectedHub.value = hub
selectedHubId.value = hub.id
await fetchProjects(hub.id)
}
const fetchProjects = async (hubId: string) => {
loadingProjects.value = true
try {
const res = await fetch(
`https://developer.api.autodesk.com/project/v1/hubs/${hubId}/projects`,
{ headers: { Authorization: `Bearer ${tokens.value!.access_token}` } }
)
if (!res.ok) throw new Error('Failed to fetch projects')
projects.value = (await res.json()).data
// console.log('Projects', projects.value)
} catch (error) {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Error fetching projects',
description: error instanceof Error ? error.message : 'Unexpected error'
})
} finally {
loadingProjects.value = false
}
}
const onProjectClick = async (hubId: string, projectId: string) => {
selectedProjectId.value = projectId
loadingFiles.value = true
folderContents.value = []
const rootFolderId = await getProjectRootFolderId(hubId, projectId)
if (rootFolderId) {
const collectedFiles = await fetchFolderContents(projectId, rootFolderId, [])
folderContents.value = collectedFiles
// console.log('collectedFiles under root folder',collectedFiles)
}
loadingFiles.value = false
}
const getProjectRootFolderId = async (hubId: string, projectId: string) => {
try {
const res = await fetch(
`https://developer.api.autodesk.com/project/v1/hubs/${hubId}/projects/${projectId}`,
{ headers: { Authorization: `Bearer ${tokens.value!.access_token}` } }
)
if (!res.ok) throw (new Error('Failed to get project details'), null)
const r = await res.json()
// console.log('root folder id', r)
folderUrn.value = r.data.relationships?.rootFolder?.data?.id || null
return folderUrn.value
} catch (error) {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Error getting project root folder ID',
description: error instanceof Error ? error.message : 'Unexpected error'
})
}
}
const fetchFolderContents = async (
projectId: string,
folderId: string,
collectedItems: AccItem[] = []
) => {
try {
const res = await fetch(
`https://developer.api.autodesk.com/data/v1/projects/${projectId}/folders/${folderId}/contents`,
{ headers: { Authorization: `Bearer ${tokens.value!.access_token}` } }
)
if (!res.ok) {
throw new Error(`Failed to fetch contents of folder ${folderId}`)
}
const data = (await res.json()).data
const folderPromises: Promise<AccItem[]>[] = []
const itemPromises: Promise<void>[] = []
for (const item of data) {
if (item.type === 'folders') {
folderPromises.push(fetchFolderContents(projectId, item.id, collectedItems))
} else if (item.type === 'items') {
itemPromises.push(
(async () => {
const version = await fetchItemLatestVersion(projectId, item.id)
if (version) {
const storageUrn = version.relationships?.storage?.data?.id || null
collectedItems.push({
...item,
latestVersionId: version.id,
storageUrn
})
} else {
collectedItems.push(item)
}
})()
)
}
}
await Promise.all([...folderPromises, ...itemPromises])
} catch (error) {
triggerNotification({
type: ToastNotificationType.Danger,
title: `Error fetching folder contents for ${folderId}:`,
description: error instanceof Error ? error.message : 'Unexpected error'
})
}
return collectedItems
}
const fetchItemLatestVersion = async (projectId: string, itemId: string) => {
try {
const res = await fetch(
`https://developer.api.autodesk.com/data/v1/projects/${projectId}/items/${encodeURIComponent(
itemId
)}/versions`,
{ headers: { Authorization: `Bearer ${tokens.value!.access_token}` } }
)
if (!res.ok) {
throw new Error(`Failed to fetch versions for item ${itemId}`)
}
const versions = (await res.json()).data
// console.log('versions', versions)
if (versions.length > 0) return versions[0]
} catch (error) {
triggerNotification({
type: ToastNotificationType.Danger,
title: `Error fetching versions for item ${itemId}:`,
description: error instanceof Error ? error.message : 'Unexpected error'
})
}
return null
}
const getSignedDownloadUrl = async (projectId: string, versionId: string) => {
const res = await fetch(
`https://developer.api.autodesk.com/data/v1/projects/${projectId}/versions/${encodeURIComponent(
versionId
)}/download`,
{ headers: { Authorization: `Bearer ${tokens.value!.access_token}` } }
)
if (!res.ok) throw new Error(`Failed to generate ACC download URL`)
const { links } = await res.json()
return links.self.href
}
const onDownloadClick = async (item: AccItem) => {
try {
const signedUrl = await getSignedDownloadUrl(
selectedProjectId.value as string,
item.latestVersionId as string
)
window.open(signedUrl, '_blank')
} catch (e) {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Download failed',
description: e instanceof Error ? e.message : 'Unexpected error'
})
}
}
const onFileSelected = (item: AccItem) => {
selectedFolderContent.value = item
}
// const handleModelSelect = (model: ProjectPageLatestItemsModelItemFragment) => {
// selectedModel.value = model
// }
const addSync = async () => {
const item = {
id: 'whatever',
accHub: selectedHub.value,
accHubId: selectedHubId.value,
accHubUrn: folderUrn.value,
modelId: selectedModel.value?.id,
projectId: props.projectId,
projectName: '',
modelName: selectedModel.value?.displayName,
createdBy: 'cat',
accItem: selectedFolderContent.value,
status: 'syncing'
} as AccSyncItem
internalSyncs.value.push(item)
await fetch('/acc/sync-item-created', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(item)
})
showNewSyncDialog.value = false
step.value = 0
}
watch(tokens, (newTokens) => {
if (newTokens?.access_token) {
fetchHubs()
}
})
</script>
@@ -0,0 +1,168 @@
<template>
<div class="flex flex-col text-xs space-y-2">
<!-- TODO: Get sync items from graphql -->
<ProjectPageAccSyncs
:project-id="projectId"
:is-logged-in="hasTokens"
:tokens="tokens"
:syncs="[]"
></ProjectPageAccSyncs>
<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>
<!-- 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>
<FormButton
v-if="hasTokens"
class="mt-4"
color="outline"
size="sm"
@click="tokens = undefined"
>
Log out
</FormButton>
<div>
{{ tokens?.access_token }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { AccTokens, AccUserInfo } from '~/lib/acc/types'
const props = defineProps<{ projectId: string }>()
const { triggerNotification } = useGlobalToast()
const tokens = ref<AccTokens>()
const hasTokens = computed(() => !!tokens.value?.access_token)
const loadingTokens = ref(true)
const userInfo = ref<AccUserInfo>()
const loadingUser = ref(false)
// const syncs = ref<AccSyncItem[]>([
// {
// id: '1',
// projectId: '',
// modelId: '',
// projectName: 'test',
// modelName: 'test',
// status: 'paused',
// createdBy: 'Oguzhan Koral',
// accItem: {
// id: 'yo',
// attributes: {
// name: 'whatever.rvt',
// displayName: 'whatever.rvt'
// }
// }
// }
// ])
// AUTH + TOKEN FLOW
const fetchTokens = async () => {
try {
const res = await fetch('/auth/acc/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('/auth/acc/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('/auth/acc/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)
})
watch(tokens, (newTokens) => {
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>
+54
View File
@@ -0,0 +1,54 @@
export type AccTokens = {
access_token: string
refresh_token: string
token_type: string
id_token: string
expires_in: number
}
export type AccUserInfo = {
userId: string
userName: string
emailId: string
firstName: string
lastName: string
}
export type AccHub = {
id: string
attributes: { name: string; extension: Record<string, unknown> }
}
export type AccProject = {
id: string
attributes: { name: string; lastModifiedTime: string }
relationships: Record<string, unknown>
}
export type AccItem = {
id: string
type?: string
latestVersionId?: string // we mutate on the way
storageUrn?: string // we mutate on the way
attributes: {
name: string
displayName: string
createTime?: string
extension?: Record<string, unknown>
}
}
export type AccSyncItem = {
id: string
accHub: AccHub
accHubId: string
createdBy: string
projectId: string
modelId: string
projectName: string
modelName: string
accItem: AccItem
status: AccSyncItemStatus
}
export type AccSyncItemStatus = 'sync' | 'syncing' | 'paused' | 'failed'
@@ -934,6 +934,15 @@ export type CreateServerRegionInput = {
name: Scalars['String']['input'];
};
export type CreateSyncItemInput = {
accFileLineageId: Scalars['String']['input'];
accHubId: Scalars['String']['input'];
accProjectId: Scalars['String']['input'];
accRootFolderUrn: Scalars['String']['input'];
modelId: Scalars['String']['input'];
projectId: Scalars['String']['input'];
};
export type CreateUserEmailInput = {
email: Scalars['String']['input'];
};
@@ -965,6 +974,11 @@ export type DeleteModelInput = {
projectId: Scalars['ID']['input'];
};
export type DeleteSyncItemInput = {
accFileLineageId: Scalars['ID']['input'];
projectId: Scalars['ID']['input'];
};
export type DeleteUserEmailInput = {
id: Scalars['ID']['input'];
};
@@ -1607,6 +1621,7 @@ export type Mutation = {
streamUpdatePermission?: Maybe<Scalars['Boolean']['output']>;
/** @deprecated Part of the old API surface and will be removed in the future. Use ProjectMutations.batchDelete instead. */
streamsDelete: Scalars['Boolean']['output'];
syncItemMutations: SyncItemMutations;
/**
* Used for broadcasting real time typing status in comment threads. Does not persist any info.
* @deprecated Use broadcastViewerUserActivity
@@ -2123,6 +2138,8 @@ export type Project = {
role?: Maybe<Scalars['String']['output']>;
/** Source apps used in any models of this project */
sourceApps: Array<Scalars['String']['output']>;
syncItem: SyncItem;
syncItems: SyncItemCollection;
team: Array<ProjectCollaborator>;
updatedAt: Scalars['DateTime']['output'];
/** Retrieve a specific project version by its ID */
@@ -2230,6 +2247,11 @@ export type ProjectPendingImportedModelsArgs = {
};
export type ProjectSyncItemArgs = {
id: Scalars['String']['input'];
};
export type ProjectVersionArgs = {
id: Scalars['String']['input'];
};
@@ -3645,6 +3667,7 @@ export type Subscription = {
projectPendingModelsUpdated: ProjectPendingModelsUpdatedMessage;
/** Subscribe to changes to a project's pending versions */
projectPendingVersionsUpdated: ProjectPendingVersionsUpdatedMessage;
projectSyncItemsUpdated: Scalars['String']['output'];
/** Subscribe to updates to any triggered automations statuses in the project */
projectTriggeredAutomationsStatusUpdated: ProjectTriggeredAutomationsStatusUpdatedMessage;
/** Track updates to a specific project */
@@ -3774,6 +3797,12 @@ export type SubscriptionProjectPendingVersionsUpdatedArgs = {
};
export type SubscriptionProjectSyncItemsUpdatedArgs = {
id: Scalars['String']['input'];
itemIds?: InputMaybe<Array<Scalars['String']['input']>>;
};
export type SubscriptionProjectTriggeredAutomationsStatusUpdatedArgs = {
projectId: Scalars['String']['input'];
};
@@ -3839,6 +3868,59 @@ export type SubscriptionWorkspaceUpdatedArgs = {
workspaceSlug?: InputMaybe<Scalars['String']['input']>;
};
export type SyncItem = {
__typename?: 'SyncItem';
accFileLineageId: Scalars['String']['output'];
accHubId: Scalars['String']['output'];
accProjectId: Scalars['String']['output'];
accRootFolderUrn: Scalars['String']['output'];
accWebhookId: Scalars['String']['output'];
author?: Maybe<LimitedUser>;
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
modelId: Scalars['String']['output'];
projectId: Scalars['String']['output'];
status: SyncItemStatus;
updatedAt: Scalars['DateTime']['output'];
};
export type SyncItemCollection = {
__typename?: 'SyncItemCollection';
cursor?: Maybe<Scalars['String']['output']>;
items: Array<SyncItem>;
totalCount: Scalars['Int']['output'];
};
export type SyncItemMutations = {
__typename?: 'SyncItemMutations';
create: SyncItem;
delete: Scalars['Boolean']['output'];
update: SyncItem;
};
export type SyncItemMutationsCreateArgs = {
input: CreateSyncItemInput;
};
export type SyncItemMutationsDeleteArgs = {
input: DeleteSyncItemInput;
};
export type SyncItemMutationsUpdateArgs = {
input: UpdateSyncItemInput;
};
export const SyncItemStatus = {
Failed: 'FAILED',
Paused: 'PAUSED',
Sync: 'SYNC',
Syncing: 'SYNCING'
} as const;
export type SyncItemStatus = typeof SyncItemStatus[keyof typeof SyncItemStatus];
export type TestAutomationRun = {
__typename?: 'TestAutomationRun';
automationRunId: Scalars['String']['output'];
@@ -3908,6 +3990,12 @@ export type UpdateServerRegionInput = {
name?: InputMaybe<Scalars['String']['input']>;
};
export type UpdateSyncItemInput = {
accFileLineageId: Scalars['ID']['input'];
projectId: Scalars['ID']['input'];
status: SyncItemStatus;
};
/** Only non-null values will be updated */
export type UpdateVersionInput = {
message?: InputMaybe<Scalars['String']['input']>;
@@ -7969,6 +8057,9 @@ export type AllObjectTypes = {
StreamCollaborator: StreamCollaborator,
StreamCollection: StreamCollection,
Subscription: Subscription,
SyncItem: SyncItem,
SyncItemCollection: SyncItemCollection,
SyncItemMutations: SyncItemMutations,
TestAutomationRun: TestAutomationRun,
TestAutomationRunTrigger: TestAutomationRunTrigger,
TestAutomationRunTriggerPayload: TestAutomationRunTriggerPayload,
@@ -8561,6 +8652,7 @@ export type MutationFieldArgs = {
streamUpdate: MutationStreamUpdateArgs,
streamUpdatePermission: MutationStreamUpdatePermissionArgs,
streamsDelete: MutationStreamsDeleteArgs,
syncItemMutations: {},
userCommentThreadActivityBroadcast: MutationUserCommentThreadActivityBroadcastArgs,
userDelete: MutationUserDeleteArgs,
userNotificationPreferencesUpdate: MutationUserNotificationPreferencesUpdateArgs,
@@ -8662,6 +8754,8 @@ export type ProjectFieldArgs = {
permissions: {},
role: {},
sourceApps: {},
syncItem: ProjectSyncItemArgs,
syncItems: {},
team: {},
updatedAt: {},
version: ProjectVersionArgs,
@@ -9045,6 +9139,7 @@ export type SubscriptionFieldArgs = {
projectModelsUpdated: SubscriptionProjectModelsUpdatedArgs,
projectPendingModelsUpdated: SubscriptionProjectPendingModelsUpdatedArgs,
projectPendingVersionsUpdated: SubscriptionProjectPendingVersionsUpdatedArgs,
projectSyncItemsUpdated: SubscriptionProjectSyncItemsUpdatedArgs,
projectTriggeredAutomationsStatusUpdated: SubscriptionProjectTriggeredAutomationsStatusUpdatedArgs,
projectUpdated: SubscriptionProjectUpdatedArgs,
projectVersionGendoAIRenderCreated: SubscriptionProjectVersionGendoAiRenderCreatedArgs,
@@ -9061,6 +9156,30 @@ export type SubscriptionFieldArgs = {
workspaceProjectsUpdated: SubscriptionWorkspaceProjectsUpdatedArgs,
workspaceUpdated: SubscriptionWorkspaceUpdatedArgs,
}
export type SyncItemFieldArgs = {
accFileLineageId: {},
accHubId: {},
accProjectId: {},
accRootFolderUrn: {},
accWebhookId: {},
author: {},
createdAt: {},
id: {},
modelId: {},
projectId: {},
status: {},
updatedAt: {},
}
export type SyncItemCollectionFieldArgs = {
cursor: {},
items: {},
totalCount: {},
}
export type SyncItemMutationsFieldArgs = {
create: SyncItemMutationsCreateArgs,
delete: SyncItemMutationsDeleteArgs,
update: SyncItemMutationsUpdateArgs,
}
export type TestAutomationRunFieldArgs = {
automationRunId: {},
functionRunId: {},
@@ -9595,6 +9714,9 @@ export type AllObjectFieldArgTypes = {
StreamCollaborator: StreamCollaboratorFieldArgs,
StreamCollection: StreamCollectionFieldArgs,
Subscription: SubscriptionFieldArgs,
SyncItem: SyncItemFieldArgs,
SyncItemCollection: SyncItemCollectionFieldArgs,
SyncItemMutations: SyncItemMutationsFieldArgs,
TestAutomationRun: TestAutomationRunFieldArgs,
TestAutomationRunTrigger: TestAutomationRunTriggerFieldArgs,
TestAutomationRunTriggerPayload: TestAutomationRunTriggerPayloadFieldArgs,
@@ -14,6 +14,8 @@ export const forgottenPasswordRoute = '/authn/forgotten-password'
export const verifyEmailRoute = '/verify-email'
export const verifyEmailCountdownRoute = '/verify-email?source=registration'
export const serverManagementRoute = '/server-management'
export const accLoginRoute = '/authn/acc'
export const accRoute = '/acc'
export const connectorsRoute = '/connectors'
export const tutorialsRoute = '/tutorials'
export const docsPageUrl = 'https://docs.speckle.systems/'
@@ -79,7 +81,7 @@ export const settingsWorkspaceRoutes = {
export const projectRoute = (
id: string,
tab?: 'models' | 'discussions' | 'automations' | 'collaborators' | 'settings'
tab?: 'models' | 'discussions' | 'automations' | 'collaborators' | 'settings' | 'acc'
) => {
let res = `/projects/${id}`
if (tab && tab !== 'models') {
@@ -255,6 +255,14 @@ const pageTabItems = computed((): LayoutPageTabItem[] => {
})
}
if (isAccEnabled.value) {
//and the rest of checks
items.push({
title: 'ACC',
id: 'acc'
})
}
if (canReadSettings.value?.authorized) {
items.push({
title: 'Collaborators',
@@ -270,6 +278,8 @@ const pageTabItems = computed((): LayoutPageTabItem[] => {
return items
})
const isAccEnabled = ref(true) // TODO
const findTabById = (id: string) =>
pageTabItems.value.find((tab) => tab.id === id) || pageTabItems.value[0]
@@ -286,6 +296,7 @@ const activePageTab = computed({
const path = router.currentRoute.value.path
if (/\/discussions\/?$/i.test(path)) return findTabById('discussions')
if (/\/automations\/?.*$/i.test(path)) return findTabById('automations')
if (/\/acc\/?.*$/i.test(path)) return findTabById('acc')
if (/\/collaborators\/?/i.test(path) && canReadSettings.value?.authorized)
return findTabById('collaborators')
if (/\/settings\/?/i.test(path) && canReadSettings.value?.authorized)
@@ -301,6 +312,9 @@ const activePageTab = computed({
case 'discussions':
router.push({ path: projectRoute(projectId.value, 'discussions') })
break
case 'acc':
router.push({ path: projectRoute(projectId.value, 'acc') })
break
case 'automations':
router.push({ path: projectRoute(projectId.value, 'automations') })
break
@@ -0,0 +1,21 @@
<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>
@@ -0,0 +1,68 @@
extend type Project {
accSyncItems: AccSyncItemCollection!
accSyncItem(id: String!): AccSyncItem!
}
type AccSyncItemCollection {
totalCount: Int!
cursor: String
items: [AccSyncItem!]!
}
type AccSyncItem {
id: ID!
projectId: String!
modelId: String!
accHubId: String!
accProjectId: String!
accRootFolderUrn: String!
accFileLineageId: String!
accWebhookId: String
status: AccSyncItemStatus!
author: LimitedUser
createdAt: DateTime!
updatedAt: DateTime!
}
enum AccSyncItemStatus {
SYNC
SYNCING
FAILED
PAUSED
}
input DeleteAccSyncItemInput {
projectId: ID!
accFileLineageId: ID!
}
input UpdateAccSyncItemInput {
projectId: ID!
accFileLineageId: ID!
status: AccSyncItemStatus!
}
input CreateAccSyncItemInput {
projectId: String!
modelId: String!
accHubId: String!
accProjectId: String!
accRootFolderUrn: String!
accFileLineageId: String!
}
type AccSyncItemMutations {
create(input: CreateAccSyncItemInput!): AccSyncItem
# delete(input: DeleteAccSyncItemInput!): Boolean!
# update(input: UpdateAccSyncItemInput!): AccSyncItem!
}
extend type Mutation {
accSyncItemMutations: AccSyncItemMutations!
@hasServerRole(role: SERVER_GUEST)
@hasScope(scope: "streams:write")
}
extend type Subscription {
projectAccSyncItemsUpdated(id: String!, itemIds: [String!]): String!
}
@@ -0,0 +1,35 @@
import { AccSyncItem } from '@/modules/acc/helpers/types'
import {
DeleteAccSyncItemInput,
UpdateAccSyncItemInput
} from '@/modules/core/graph/generated/graphql'
export const accSyncItemEventsNamespace = 'accSyncItems' as const
export const AccSyncItemEvents = {
Created: `${accSyncItemEventsNamespace}:created`,
Updated: `${accSyncItemEventsNamespace}:updated`,
Deleted: `${accSyncItemEventsNamespace}:deleted`
} as const
export type AccSyncItemEventsPayloads = {
[AccSyncItemEvents.Created]: {
syncItem: AccSyncItem
projectId: string
}
[AccSyncItemEvents.Updated]: {
oldSyncItem: AccSyncItem
newSyncItem: AccSyncItem
projectId: string
userId?: string
input: UpdateAccSyncItemInput
}
[AccSyncItemEvents.Deleted]: {
syncItem: AccSyncItem
projectId: string
userId?: string
input: DeleteAccSyncItemInput
}
}
@@ -0,0 +1,114 @@
import { AccSyncItem } from '@/modules/acc/helpers/types'
import { createAccSyncItemAndNotifyFactory } from '@/modules/acc/repositories/accSyncItems'
import { TokenResourceIdentifierType } from '@/modules/core/domain/tokens/types'
import { Resolvers } from '@/modules/core/graph/generated/graphql'
import { throwIfResourceAccessNotAllowed } from '@/modules/core/helpers/token'
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
import { getEventBus } from '@/modules/shared/services/eventBus'
import cryptoRandomString from 'crypto-random-string'
import { GraphQLError } from 'graphql/error'
import { Knex } from 'knex'
const ACC_SYNC_ITEMS = 'acc_sync_items'
const tables = {
accSyncItems: (db: Knex) => db<AccSyncItem>(ACC_SYNC_ITEMS)
}
const resolvers: Resolvers = {
Project: {
async accSyncItems(parent, args, ctx) {
throwIfResourceAccessNotAllowed({
resourceId: parent.id,
resourceAccessRules: ctx.resourceAccessRules,
resourceType: TokenResourceIdentifierType.Project
})
const projectDB = await getProjectDbClient({ projectId: parent.id })
const items = await tables
.accSyncItems(projectDB)
.where({ projectId: parent.id })
.orderBy('createdAt', 'desc')
return {
totalCount: items.length,
cursor: null, // TODO
items: items.map((item) => ({
...item,
author: null // TODO
}))
}
},
async accSyncItem(parent, args, ctx) {
const { id } = args
throwIfResourceAccessNotAllowed({
resourceId: parent.id,
resourceAccessRules: ctx.resourceAccessRules,
resourceType: TokenResourceIdentifierType.Project
})
// Get project-scoped DB
const projectDB = await getProjectDbClient({ projectId: parent.id })
const item = await tables.accSyncItems(projectDB).where({ id }).first()
if (!item) throw new Error(`SyncItem with id "${id}" not found`) // TODO: create acc kind error types later
return {
...item,
author: null // TODO
}
}
},
Mutation: {
accSyncItemMutations: () => ({})
},
AccSyncItemMutations: {
async create(parent, args, ctx) {
const { input } = args
console.log('create', input)
throwIfResourceAccessNotAllowed({
resourceId: input.projectId,
resourceAccessRules: ctx.resourceAccessRules,
resourceType: TokenResourceIdentifierType.Project
})
const projectDB = await getProjectDbClient({ projectId: input.projectId })
const existing = await tables
.accSyncItems(projectDB)
.where({ accFileLineageId: input.accFileLineageId })
.first()
if (existing) {
throw new GraphQLError(
`A SyncItem with accFileLineageId "${input.accFileLineageId}" already exists.`,
{
extensions: { code: 'DUPLICATE_ACC_FILE_LINEAGE_ID' }
}
)
}
const createSyncItem = createAccSyncItemAndNotifyFactory({
db: await getProjectDbClient({ projectId: input.projectId }),
eventEmit: getEventBus().emit
})
const newItem = await createSyncItem({
id: cryptoRandomString({ length: 10 }),
status: 'SYNCING',
...input
})
return newItem
}
// async update(parent, args, ctx) {
// console.log('update', args)
// },
// async delete(parent, args, ctx) {
// console.log('delete', args)
// }
}
}
export default resolvers
@@ -0,0 +1,46 @@
import { UserRecord } from '@/modules/core/helpers/types'
import type { Session, SessionData } from 'express-session'
declare module 'express-session' {
interface SessionData extends AccSessionData {}
}
declare module 'http' {
interface IncomingMessage extends AccSessionData {
/**
* Not sure why I have to do this, the session type is picked up correctly in some places, but not others
*/
session: Session & Partial<SessionData>
}
}
type AccTokens = {
access_token: string
refresh_token: string
token_type: string
id_token: string
expires_in: number
}
export type AccSessionData = {
accTokens?: AccTokens
codeVerifier?: string
projectId?: string
}
export type AccSyncItem = {
id: string
projectId: string
modelId: string
accHubId: string
accProjectId: string
accRootFolderUrn: string
accFileLineageId: string
accWebhookId?: string
status: AccSyncItemStatus
author: UserRecord
createdAt: Date
updatedAt: Date
}
export type AccSyncItemStatus = 'SYNC' | 'SYNCING' | 'PAUSED' | 'FAILED'
+138
View File
@@ -0,0 +1,138 @@
/* eslint-disable camelcase */
import { createAccOidcFlow } from '@/modules/acc/oidcHelper'
import { registerAccWebhook } from '@/modules/acc/webhook'
import { sessionMiddlewareFactory } from '@/modules/auth/middleware'
import { SpeckleModule } from '@/modules/shared/helpers/typeHelper'
import { moduleLogger } from '@/observability/logging'
import { Express } from 'express'
export default function accRestApi(app: Express) {
const sessionMiddleware = sessionMiddlewareFactory()
app.post('/auth/acc/login', sessionMiddleware, async (req, res) => {
const { projectId } = req.body
req.session.projectId = projectId
const accFlow = createAccOidcFlow()
const { codeVerifier, codeChallenge } = accFlow.generateCodeVerifier()
req.session.codeVerifier = codeVerifier
const authorizeUrl = accFlow.buildAuthorizeUrl({
clientId: process.env.ACC_CLIENT_ID ?? '',
redirectUri: process.env.ACC_REDIRECT_URL ?? '',
codeChallenge,
scopes: [
'user-profile:read',
'data:read',
'data:create',
'viewables:read',
'openid'
]
})
return res.json({ authorizeUrl })
})
app.get('/auth/acc/callback', sessionMiddleware, async (req, res) => {
const { code } = req.query
const codeVerifier = req.session.codeVerifier
if (!code || !codeVerifier) {
return res.status(400).send({ error: 'Missing code or verifier' })
}
const accFlow = createAccOidcFlow()
try {
const tokens = await accFlow.exchangeCodeForTokens({
code: String(code),
codeVerifier,
clientId: process.env.ACC_CLIENT_ID ?? '',
clientSecret: process.env.ACC_CLIENT_SECRET ?? '',
redirectUri: process.env.ACC_REDIRECT_URL ?? ''
})
req.session.accTokens = tokens
return res.redirect(`/projects/${req.session.projectId}/acc`)
} catch (error) {
console.error('Token exchange failed:', error)
return res.status(500).send({ error: 'Token exchange failed' })
}
})
app.get('/auth/acc/status', sessionMiddleware, (req, res) => {
if (!req.session.accTokens) {
return res.status(404).send({ error: 'No ACC tokens found' })
}
res.send(req.session.accTokens)
})
app.post('/auth/acc/refresh', sessionMiddleware, async (req, res) => {
const { refresh_token } = req.session.accTokens || {}
if (!refresh_token) {
return res.status(401).json({ error: 'No refresh token found' })
}
try {
const params = new URLSearchParams({
grant_type: 'refresh_token',
client_id: process.env.ACC_CLIENT_ID ?? '',
client_secret: process.env.ACC_CLIENT_SECRET ?? '',
refresh_token
})
const response = await fetch(
'https://developer.api.autodesk.com/authentication/v2/token',
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
}
)
if (!response.ok) {
console.error(await response.text())
return res.status(500).json({ error: 'Failed to refresh token' })
}
const newTokens = await response.json()
req.session.accTokens = newTokens
res.json(newTokens)
} catch (error) {
console.error('Error refreshing token:', error)
res.status(500).json({ error: 'Error refreshing token' })
}
})
app.post('/acc/sync-item-created', sessionMiddleware, async (req, res) => {
const { accHubUrn } = req.body
console.log(req.body)
console.log(accHubUrn)
if (!req.session.accTokens) {
throw new Error('whatever')
}
const { access_token } = req.session.accTokens
await registerAccWebhook({
accessToken: access_token,
hubUrn: accHubUrn,
region: 'EMEA',
event: ''
})
res.status(200)
})
app.post('/acc/webhook/callback', sessionMiddleware, async (req, res) => {
console.log(req.body)
res.status(200)
})
}
export const init: SpeckleModule['init'] = async ({ app }) => {
moduleLogger.info('🔑 Init acc module')
// Hoist rest
accRestApi(app)
}
export const finalize: SpeckleModule['finalize'] = async () => {}
@@ -0,0 +1,36 @@
import { Knex } from 'knex'
const TABLE_NAME = 'acc_sync_items'
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable(TABLE_NAME, (table) => {
table.string('id', 10).primary()
table.string('projectId').notNullable().references('id').inTable('streams')
table.string('modelId').notNullable()
table.string('accHubId').notNullable()
table.string('accProjectId').notNullable()
table.string('accRootFolderUrn').notNullable()
table.string('accFileLineageId').notNullable().unique()
table.string('accWebhookId').nullable()
table
.enu('status', ['SYNC', 'SYNCING', 'FAILED', 'PAUSED'])
.notNullable()
.defaultTo('SYNC')
// Foreign key to users table if needed
table.string('authorId').nullable()
table
.timestamp('createdAt', { precision: 3, useTz: true })
.defaultTo(knex.fn.now())
.notNullable()
table
.timestamp('updatedAt', { precision: 3, useTz: true })
.defaultTo(knex.fn.now())
.notNullable()
})
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTable(TABLE_NAME)
}
+78
View File
@@ -0,0 +1,78 @@
/* eslint-disable camelcase */
// modules/accIntegration/oidcHelper.ts
import axios from 'axios'
import crypto from 'crypto'
interface BuildAuthorizeUrlOptions {
clientId: string
redirectUri: string
codeChallenge: string
scopes: string[]
}
interface ExchangeCodeOptions {
code: string
codeVerifier: string
clientId: string
clientSecret: string
redirectUri: string
}
export function createAccOidcFlow() {
return {
generateCodeVerifier() {
const codeVerifier = crypto.randomBytes(32).toString('base64url')
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url')
return { codeVerifier, codeChallenge }
},
buildAuthorizeUrl({
clientId,
redirectUri,
codeChallenge,
scopes
}: BuildAuthorizeUrlOptions) {
const params = new URLSearchParams({
response_type: 'code',
client_id: clientId,
redirect_uri: redirectUri,
scope: scopes.join(' '),
code_challenge: codeChallenge,
code_challenge_method: 'S256'
})
return `https://developer.api.autodesk.com/authentication/v2/authorize?${params.toString()}`
},
async exchangeCodeForTokens({
code,
codeVerifier,
clientId,
clientSecret,
redirectUri
}: ExchangeCodeOptions) {
const params = new URLSearchParams({
grant_type: 'authorization_code',
client_id: clientId,
client_secret: clientSecret,
redirect_uri: redirectUri,
code,
code_verifier: codeVerifier
})
const response = await axios.post(
'https://developer.api.autodesk.com/authentication/v2/token',
params.toString(),
{
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
}
)
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return response.data // includes access_token, refresh_token, expires_in, token_type, etc.
}
}
}
@@ -0,0 +1,42 @@
import { AccSyncItemEvents } from '@/modules/acc/domain/events'
import { AccSyncItem } from '@/modules/acc/helpers/types'
import { EventBusEmit } from '@/modules/shared/services/eventBus'
import { Knex } from 'knex'
const ACC_SYNC_ITEMS = 'acc_sync_items'
const tables = {
accSyncItems: (db: Knex) => db<AccSyncItem>(ACC_SYNC_ITEMS)
}
export type CreateAccSyncItemAndNotify = (
input: Omit<AccSyncItem, 'author' | 'createdAt' | 'updatedAt'>
) => Promise<AccSyncItem>
export const createAccSyncItemAndNotifyFactory = (deps: {
db: Knex
eventEmit: EventBusEmit
}): CreateAccSyncItemAndNotify => {
return async (input) => {
const now = new Date()
const [item] = await tables
.accSyncItems(deps.db)
.insert({
...input,
createdAt: now,
updatedAt: now
})
.returning('*')
await deps.eventEmit({
eventName: AccSyncItemEvents.Created,
payload: {
syncItem: item,
projectId: item.projectId
}
})
return item
}
}
+44
View File
@@ -0,0 +1,44 @@
const accWebhookCallbackUrl =
'https://oguzhans-macbook-pro.mermaid-emperor.ts.net//acc/webhook/callback'
export async function registerAccWebhook({
accessToken,
hubUrn,
region,
event = 'dm.lineage.updated'
}: {
accessToken: string
hubUrn: string
region: string
event: string
}) {
const response = await fetch(
`https://developer.api.autodesk.com/webhooks/v1/systems/data/events/${event}/hooks`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'x-ads-region': `${region}`
},
body: JSON.stringify({
callbackUrl: accWebhookCallbackUrl,
scope: {
folder: {
hubUrn
}
}
})
}
)
if (!response.ok) {
throw new Error(`Webhook registration failed: ${await response.text()}`)
}
const res = await response.json()
console.log(res)
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return res
}
@@ -40,6 +40,47 @@ export type Scalars = {
JSONObject: { input: Record<string, unknown>; output: Record<string, unknown>; }
};
export type AccSyncItem = {
__typename?: 'AccSyncItem';
accFileLineageId: Scalars['String']['output'];
accHubId: Scalars['String']['output'];
accProjectId: Scalars['String']['output'];
accRootFolderUrn: Scalars['String']['output'];
accWebhookId?: Maybe<Scalars['String']['output']>;
author?: Maybe<LimitedUser>;
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
modelId: Scalars['String']['output'];
projectId: Scalars['String']['output'];
status: AccSyncItemStatus;
updatedAt: Scalars['DateTime']['output'];
};
export type AccSyncItemCollection = {
__typename?: 'AccSyncItemCollection';
cursor?: Maybe<Scalars['String']['output']>;
items: Array<AccSyncItem>;
totalCount: Scalars['Int']['output'];
};
export type AccSyncItemMutations = {
__typename?: 'AccSyncItemMutations';
create?: Maybe<AccSyncItem>;
};
export type AccSyncItemMutationsCreateArgs = {
input: CreateAccSyncItemInput;
};
export const AccSyncItemStatus = {
Failed: 'FAILED',
Paused: 'PAUSED',
Sync: 'SYNC',
Syncing: 'SYNCING'
} as const;
export type AccSyncItemStatus = typeof AccSyncItemStatus[keyof typeof AccSyncItemStatus];
export type ActiveUserMutations = {
__typename?: 'ActiveUserMutations';
emailMutations: UserEmailMutations;
@@ -908,6 +949,15 @@ export type CountOnlyCollection = {
totalCount: Scalars['Int']['output'];
};
export type CreateAccSyncItemInput = {
accFileLineageId: Scalars['String']['input'];
accHubId: Scalars['String']['input'];
accProjectId: Scalars['String']['input'];
accRootFolderUrn: Scalars['String']['input'];
modelId: Scalars['String']['input'];
projectId: Scalars['String']['input'];
};
export type CreateAutomateFunctionInput = {
description: Scalars['String']['input'];
/** Base64 encoded image data string */
@@ -983,6 +1033,11 @@ export type CurrencyBasedPrices = {
usd: WorkspacePaidPlanPrices;
};
export type DeleteAccSyncItemInput = {
accFileLineageId: Scalars['ID']['input'];
projectId: Scalars['ID']['input'];
};
export type DeleteModelInput = {
id: Scalars['ID']['input'];
projectId: Scalars['ID']['input'];
@@ -1468,6 +1523,7 @@ export type Mutation = {
__typename?: 'Mutation';
/** The void stares back. */
_?: Maybe<Scalars['String']['output']>;
accSyncItemMutations: AccSyncItemMutations;
/** Various Active User oriented mutations */
activeUserMutations: ActiveUserMutations;
admin: AdminMutations;
@@ -2100,6 +2156,8 @@ export type Price = {
export type Project = {
__typename?: 'Project';
accSyncItem: AccSyncItem;
accSyncItems: AccSyncItemCollection;
allowPublicComments: Scalars['Boolean']['output'];
/** Get a single automation by id. Error will be thrown if automation is not found or inaccessible. */
automation: Automation;
@@ -2161,6 +2219,11 @@ export type Project = {
};
export type ProjectAccSyncItemArgs = {
id: Scalars['String']['input'];
};
export type ProjectAutomationArgs = {
id: Scalars['String']['input'];
};
@@ -3650,6 +3713,7 @@ export type Subscription = {
* Note: Only works in test environment
*/
ping: Scalars['String']['output'];
projectAccSyncItemsUpdated: Scalars['String']['output'];
/** Subscribe to updates to automations in the project */
projectAutomationsUpdated: ProjectAutomationsUpdatedMessage;
/**
@@ -3766,6 +3830,12 @@ export type SubscriptionCommitUpdatedArgs = {
};
export type SubscriptionProjectAccSyncItemsUpdatedArgs = {
id: Scalars['String']['input'];
itemIds?: InputMaybe<Array<Scalars['String']['input']>>;
};
export type SubscriptionProjectAutomationsUpdatedArgs = {
projectId: Scalars['String']['input'];
};
@@ -3906,6 +3976,12 @@ export type TriggeredAutomationsStatus = {
statusMessage?: Maybe<Scalars['String']['output']>;
};
export type UpdateAccSyncItemInput = {
accFileLineageId: Scalars['ID']['input'];
projectId: Scalars['ID']['input'];
status: AccSyncItemStatus;
};
/** Any null values will be ignored */
export type UpdateAutomateFunctionInput = {
description?: InputMaybe<Scalars['String']['input']>;
@@ -5338,6 +5414,10 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
/** Mapping between all available schema types and the resolvers types */
export type ResolversTypes = {
AccSyncItem: ResolverTypeWrapper<Omit<AccSyncItem, 'author'> & { author?: Maybe<ResolversTypes['LimitedUser']> }>;
AccSyncItemCollection: ResolverTypeWrapper<Omit<AccSyncItemCollection, 'items'> & { items: Array<ResolversTypes['AccSyncItem']> }>;
AccSyncItemMutations: ResolverTypeWrapper<Omit<AccSyncItemMutations, 'create'> & { create?: Maybe<ResolversTypes['AccSyncItem']> }>;
AccSyncItemStatus: AccSyncItemStatus;
ActiveUserMutations: ResolverTypeWrapper<MutationsObjectGraphQLReturn>;
Activity: ResolverTypeWrapper<Activity>;
ActivityCollection: ResolverTypeWrapper<ActivityCollectionGraphQLReturn>;
@@ -5422,6 +5502,7 @@ export type ResolversTypes = {
CommitsDeleteInput: CommitsDeleteInput;
CommitsMoveInput: CommitsMoveInput;
CountOnlyCollection: ResolverTypeWrapper<CountOnlyCollection>;
CreateAccSyncItemInput: CreateAccSyncItemInput;
CreateAutomateFunctionInput: CreateAutomateFunctionInput;
CreateAutomateFunctionWithoutVersionInput: CreateAutomateFunctionWithoutVersionInput;
CreateCommentInput: CreateCommentInput;
@@ -5433,6 +5514,7 @@ export type ResolversTypes = {
Currency: Currency;
CurrencyBasedPrices: ResolverTypeWrapper<Omit<CurrencyBasedPrices, 'gbp' | 'usd'> & { gbp: ResolversTypes['WorkspacePaidPlanPrices'], usd: ResolversTypes['WorkspacePaidPlanPrices'] }>;
DateTime: ResolverTypeWrapper<Scalars['DateTime']['output']>;
DeleteAccSyncItemInput: DeleteAccSyncItemInput;
DeleteModelInput: DeleteModelInput;
DeleteUserEmailInput: DeleteUserEmailInput;
DeleteVersionsInput: DeleteVersionsInput;
@@ -5581,6 +5663,7 @@ export type ResolversTypes = {
TokenResourceIdentifierInput: TokenResourceIdentifierInput;
TokenResourceIdentifierType: TokenResourceIdentifierType;
TriggeredAutomationsStatus: ResolverTypeWrapper<TriggeredAutomationsStatusGraphQLReturn>;
UpdateAccSyncItemInput: UpdateAccSyncItemInput;
UpdateAutomateFunctionInput: UpdateAutomateFunctionInput;
UpdateModelInput: UpdateModelInput;
UpdateServerRegionInput: UpdateServerRegionInput;
@@ -5686,6 +5769,9 @@ export type ResolversTypes = {
/** Mapping between all available schema types and the resolvers parents */
export type ResolversParentTypes = {
AccSyncItem: Omit<AccSyncItem, 'author'> & { author?: Maybe<ResolversParentTypes['LimitedUser']> };
AccSyncItemCollection: Omit<AccSyncItemCollection, 'items'> & { items: Array<ResolversParentTypes['AccSyncItem']> };
AccSyncItemMutations: Omit<AccSyncItemMutations, 'create'> & { create?: Maybe<ResolversParentTypes['AccSyncItem']> };
ActiveUserMutations: MutationsObjectGraphQLReturn;
Activity: Activity;
ActivityCollection: ActivityCollectionGraphQLReturn;
@@ -5766,6 +5852,7 @@ export type ResolversParentTypes = {
CommitsDeleteInput: CommitsDeleteInput;
CommitsMoveInput: CommitsMoveInput;
CountOnlyCollection: CountOnlyCollection;
CreateAccSyncItemInput: CreateAccSyncItemInput;
CreateAutomateFunctionInput: CreateAutomateFunctionInput;
CreateAutomateFunctionWithoutVersionInput: CreateAutomateFunctionWithoutVersionInput;
CreateCommentInput: CreateCommentInput;
@@ -5776,6 +5863,7 @@ export type ResolversParentTypes = {
CreateVersionInput: CreateVersionInput;
CurrencyBasedPrices: Omit<CurrencyBasedPrices, 'gbp' | 'usd'> & { gbp: ResolversParentTypes['WorkspacePaidPlanPrices'], usd: ResolversParentTypes['WorkspacePaidPlanPrices'] };
DateTime: Scalars['DateTime']['output'];
DeleteAccSyncItemInput: DeleteAccSyncItemInput;
DeleteModelInput: DeleteModelInput;
DeleteUserEmailInput: DeleteUserEmailInput;
DeleteVersionsInput: DeleteVersionsInput;
@@ -5906,6 +5994,7 @@ export type ResolversParentTypes = {
TokenResourceIdentifier: TokenResourceIdentifier;
TokenResourceIdentifierInput: TokenResourceIdentifierInput;
TriggeredAutomationsStatus: TriggeredAutomationsStatusGraphQLReturn;
UpdateAccSyncItemInput: UpdateAccSyncItemInput;
UpdateAutomateFunctionInput: UpdateAutomateFunctionInput;
UpdateModelInput: UpdateModelInput;
UpdateServerRegionInput: UpdateServerRegionInput;
@@ -6033,6 +6122,34 @@ export type IsOwnerDirectiveArgs = { };
export type IsOwnerDirectiveResolver<Result, Parent, ContextType = GraphQLContext, Args = IsOwnerDirectiveArgs> = DirectiveResolverFn<Result, Parent, ContextType, Args>;
export type AccSyncItemResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccSyncItem'] = ResolversParentTypes['AccSyncItem']> = {
accFileLineageId?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
accHubId?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
accProjectId?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
accRootFolderUrn?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
accWebhookId?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
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>;
status?: Resolver<ResolversTypes['AccSyncItemStatus'], ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type AccSyncItemCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccSyncItemCollection'] = ResolversParentTypes['AccSyncItemCollection']> = {
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
items?: Resolver<Array<ResolversTypes['AccSyncItem']>, ParentType, ContextType>;
totalCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type AccSyncItemMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccSyncItemMutations'] = ResolversParentTypes['AccSyncItemMutations']> = {
create?: Resolver<Maybe<ResolversTypes['AccSyncItem']>, ParentType, ContextType, RequireFields<AccSyncItemMutationsCreateArgs, 'input'>>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type ActiveUserMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['ActiveUserMutations'] = ResolversParentTypes['ActiveUserMutations']> = {
emailMutations?: Resolver<ResolversTypes['UserEmailMutations'], ParentType, ContextType>;
finishOnboarding?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, Partial<ActiveUserMutationsFinishOnboardingArgs>>;
@@ -6660,6 +6777,7 @@ export type ModelsTreeItemCollectionResolvers<ContextType = GraphQLContext, Pare
export type MutationResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['Mutation'] = ResolversParentTypes['Mutation']> = {
_?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
accSyncItemMutations?: Resolver<ResolversTypes['AccSyncItemMutations'], ParentType, ContextType>;
activeUserMutations?: Resolver<ResolversTypes['ActiveUserMutations'], ParentType, ContextType>;
admin?: Resolver<ResolversTypes['AdminMutations'], ParentType, ContextType>;
adminDeleteUser?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationAdminDeleteUserArgs, 'userConfirmation'>>;
@@ -6805,6 +6923,8 @@ export type PriceResolvers<ContextType = GraphQLContext, ParentType extends Reso
};
export type ProjectResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['Project'] = ResolversParentTypes['Project']> = {
accSyncItem?: Resolver<ResolversTypes['AccSyncItem'], ParentType, ContextType, RequireFields<ProjectAccSyncItemArgs, 'id'>>;
accSyncItems?: Resolver<ResolversTypes['AccSyncItemCollection'], ParentType, ContextType>;
allowPublicComments?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
automation?: Resolver<ResolversTypes['Automation'], ParentType, ContextType, RequireFields<ProjectAutomationArgs, 'id'>>;
automations?: Resolver<ResolversTypes['AutomationCollection'], ParentType, ContextType, Partial<ProjectAutomationsArgs>>;
@@ -7304,6 +7424,7 @@ export type SubscriptionResolvers<ContextType = GraphQLContext, ParentType exten
commitDeleted?: SubscriptionResolver<Maybe<ResolversTypes['JSONObject']>, "commitDeleted", ParentType, ContextType, RequireFields<SubscriptionCommitDeletedArgs, 'streamId'>>;
commitUpdated?: SubscriptionResolver<Maybe<ResolversTypes['JSONObject']>, "commitUpdated", ParentType, ContextType, RequireFields<SubscriptionCommitUpdatedArgs, 'streamId'>>;
ping?: SubscriptionResolver<ResolversTypes['String'], "ping", ParentType, ContextType>;
projectAccSyncItemsUpdated?: SubscriptionResolver<ResolversTypes['String'], "projectAccSyncItemsUpdated", ParentType, ContextType, RequireFields<SubscriptionProjectAccSyncItemsUpdatedArgs, 'id'>>;
projectAutomationsUpdated?: SubscriptionResolver<ResolversTypes['ProjectAutomationsUpdatedMessage'], "projectAutomationsUpdated", ParentType, ContextType, RequireFields<SubscriptionProjectAutomationsUpdatedArgs, 'projectId'>>;
projectCommentsUpdated?: SubscriptionResolver<ResolversTypes['ProjectCommentsUpdatedMessage'], "projectCommentsUpdated", ParentType, ContextType, RequireFields<SubscriptionProjectCommentsUpdatedArgs, 'target'>>;
projectFileImportUpdated?: SubscriptionResolver<ResolversTypes['ProjectFileImportUpdatedMessage'], "projectFileImportUpdated", ParentType, ContextType, RequireFields<SubscriptionProjectFileImportUpdatedArgs, 'id'>>;
@@ -7858,6 +7979,9 @@ export type WorkspaceUpdatedMessageResolvers<ContextType = GraphQLContext, Paren
};
export type Resolvers<ContextType = GraphQLContext> = {
AccSyncItem?: AccSyncItemResolvers<ContextType>;
AccSyncItemCollection?: AccSyncItemCollectionResolvers<ContextType>;
AccSyncItemMutations?: AccSyncItemMutationsResolvers<ContextType>;
ActiveUserMutations?: ActiveUserMutationsResolvers<ContextType>;
Activity?: ActivityResolvers<ContextType>;
ActivityCollection?: ActivityCollectionResolvers<ContextType>;
@@ -20,6 +20,47 @@ export type Scalars = {
JSONObject: { input: Record<string, unknown>; output: Record<string, unknown>; }
};
export type AccSyncItem = {
__typename?: 'AccSyncItem';
accFileLineageId: Scalars['String']['output'];
accHubId: Scalars['String']['output'];
accProjectId: Scalars['String']['output'];
accRootFolderUrn: Scalars['String']['output'];
accWebhookId?: Maybe<Scalars['String']['output']>;
author?: Maybe<LimitedUser>;
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
modelId: Scalars['String']['output'];
projectId: Scalars['String']['output'];
status: AccSyncItemStatus;
updatedAt: Scalars['DateTime']['output'];
};
export type AccSyncItemCollection = {
__typename?: 'AccSyncItemCollection';
cursor?: Maybe<Scalars['String']['output']>;
items: Array<AccSyncItem>;
totalCount: Scalars['Int']['output'];
};
export type AccSyncItemMutations = {
__typename?: 'AccSyncItemMutations';
create?: Maybe<AccSyncItem>;
};
export type AccSyncItemMutationsCreateArgs = {
input: CreateAccSyncItemInput;
};
export const AccSyncItemStatus = {
Failed: 'FAILED',
Paused: 'PAUSED',
Sync: 'SYNC',
Syncing: 'SYNCING'
} as const;
export type AccSyncItemStatus = typeof AccSyncItemStatus[keyof typeof AccSyncItemStatus];
export type ActiveUserMutations = {
__typename?: 'ActiveUserMutations';
emailMutations: UserEmailMutations;
@@ -888,6 +929,15 @@ export type CountOnlyCollection = {
totalCount: Scalars['Int']['output'];
};
export type CreateAccSyncItemInput = {
accFileLineageId: Scalars['String']['input'];
accHubId: Scalars['String']['input'];
accProjectId: Scalars['String']['input'];
accRootFolderUrn: Scalars['String']['input'];
modelId: Scalars['String']['input'];
projectId: Scalars['String']['input'];
};
export type CreateAutomateFunctionInput = {
description: Scalars['String']['input'];
/** Base64 encoded image data string */
@@ -963,6 +1013,11 @@ export type CurrencyBasedPrices = {
usd: WorkspacePaidPlanPrices;
};
export type DeleteAccSyncItemInput = {
accFileLineageId: Scalars['ID']['input'];
projectId: Scalars['ID']['input'];
};
export type DeleteModelInput = {
id: Scalars['ID']['input'];
projectId: Scalars['ID']['input'];
@@ -1448,6 +1503,7 @@ export type Mutation = {
__typename?: 'Mutation';
/** The void stares back. */
_?: Maybe<Scalars['String']['output']>;
accSyncItemMutations: AccSyncItemMutations;
/** Various Active User oriented mutations */
activeUserMutations: ActiveUserMutations;
admin: AdminMutations;
@@ -2080,6 +2136,8 @@ export type Price = {
export type Project = {
__typename?: 'Project';
accSyncItem: AccSyncItem;
accSyncItems: AccSyncItemCollection;
allowPublicComments: Scalars['Boolean']['output'];
/** Get a single automation by id. Error will be thrown if automation is not found or inaccessible. */
automation: Automation;
@@ -2141,6 +2199,11 @@ export type Project = {
};
export type ProjectAccSyncItemArgs = {
id: Scalars['String']['input'];
};
export type ProjectAutomationArgs = {
id: Scalars['String']['input'];
};
@@ -3630,6 +3693,7 @@ export type Subscription = {
* Note: Only works in test environment
*/
ping: Scalars['String']['output'];
projectAccSyncItemsUpdated: Scalars['String']['output'];
/** Subscribe to updates to automations in the project */
projectAutomationsUpdated: ProjectAutomationsUpdatedMessage;
/**
@@ -3746,6 +3810,12 @@ export type SubscriptionCommitUpdatedArgs = {
};
export type SubscriptionProjectAccSyncItemsUpdatedArgs = {
id: Scalars['String']['input'];
itemIds?: InputMaybe<Array<Scalars['String']['input']>>;
};
export type SubscriptionProjectAutomationsUpdatedArgs = {
projectId: Scalars['String']['input'];
};
@@ -3886,6 +3956,12 @@ export type TriggeredAutomationsStatus = {
statusMessage?: Maybe<Scalars['String']['output']>;
};
export type UpdateAccSyncItemInput = {
accFileLineageId: Scalars['ID']['input'];
projectId: Scalars['ID']['input'];
status: AccSyncItemStatus;
};
/** Any null values will be ignored */
export type UpdateAutomateFunctionInput = {
description?: InputMaybe<Scalars['String']['input']>;
+2
View File
@@ -81,6 +81,7 @@ const getEnabledModuleNames = () => {
FF_GATEKEEPER_MODULE_ENABLED
} = getFeatureFlags()
const moduleNames = [
'acc',
'accessrequests',
'activitystream',
'apiexplorer',
@@ -102,6 +103,7 @@ const getEnabledModuleNames = () => {
'multiregion'
]
// TODO: add acc with feature flag?
if (FF_AUTOMATE_MODULE_ENABLED) moduleNames.push('automate')
if (FF_GENDOAI_MODULE_ENABLED) moduleNames.push('gendo')
// the order of the event listeners matters
@@ -53,6 +53,10 @@ import {
fileuploadEventNamespace,
FileuploadEventsPayloads
} from '@/modules/fileuploads/domain/events'
import {
accSyncItemEventsNamespace,
AccSyncItemEventsPayloads
} from '@/modules/acc/domain/events'
type AllEventsWildcard = '**'
type EventWildcard = '*'
@@ -70,6 +74,7 @@ type TestEventsPayloads = {
// we should only ever extend this type, other helper types will be derived from this
type EventsByNamespace = {
test: TestEventsPayloads
[accSyncItemEventsNamespace]: AccSyncItemEventsPayloads
[workspaceEventNamespace]: WorkspaceEventsPayloads
[gatekeeperEventNamespace]: GatekeeperEventPayloads
[serverinvitesEventNamespace]: ServerInvitesEventsPayloads
@@ -21,6 +21,47 @@ export type Scalars = {
JSONObject: { input: Record<string, unknown>; output: Record<string, unknown>; }
};
export type AccSyncItem = {
__typename?: 'AccSyncItem';
accFileLineageId: Scalars['String']['output'];
accHubId: Scalars['String']['output'];
accProjectId: Scalars['String']['output'];
accRootFolderUrn: Scalars['String']['output'];
accWebhookId?: Maybe<Scalars['String']['output']>;
author?: Maybe<LimitedUser>;
createdAt: Scalars['DateTime']['output'];
id: Scalars['ID']['output'];
modelId: Scalars['String']['output'];
projectId: Scalars['String']['output'];
status: AccSyncItemStatus;
updatedAt: Scalars['DateTime']['output'];
};
export type AccSyncItemCollection = {
__typename?: 'AccSyncItemCollection';
cursor?: Maybe<Scalars['String']['output']>;
items: Array<AccSyncItem>;
totalCount: Scalars['Int']['output'];
};
export type AccSyncItemMutations = {
__typename?: 'AccSyncItemMutations';
create?: Maybe<AccSyncItem>;
};
export type AccSyncItemMutationsCreateArgs = {
input: CreateAccSyncItemInput;
};
export const AccSyncItemStatus = {
Failed: 'FAILED',
Paused: 'PAUSED',
Sync: 'SYNC',
Syncing: 'SYNCING'
} as const;
export type AccSyncItemStatus = typeof AccSyncItemStatus[keyof typeof AccSyncItemStatus];
export type ActiveUserMutations = {
__typename?: 'ActiveUserMutations';
emailMutations: UserEmailMutations;
@@ -889,6 +930,15 @@ export type CountOnlyCollection = {
totalCount: Scalars['Int']['output'];
};
export type CreateAccSyncItemInput = {
accFileLineageId: Scalars['String']['input'];
accHubId: Scalars['String']['input'];
accProjectId: Scalars['String']['input'];
accRootFolderUrn: Scalars['String']['input'];
modelId: Scalars['String']['input'];
projectId: Scalars['String']['input'];
};
export type CreateAutomateFunctionInput = {
description: Scalars['String']['input'];
/** Base64 encoded image data string */
@@ -964,6 +1014,11 @@ export type CurrencyBasedPrices = {
usd: WorkspacePaidPlanPrices;
};
export type DeleteAccSyncItemInput = {
accFileLineageId: Scalars['ID']['input'];
projectId: Scalars['ID']['input'];
};
export type DeleteModelInput = {
id: Scalars['ID']['input'];
projectId: Scalars['ID']['input'];
@@ -1449,6 +1504,7 @@ export type Mutation = {
__typename?: 'Mutation';
/** The void stares back. */
_?: Maybe<Scalars['String']['output']>;
accSyncItemMutations: AccSyncItemMutations;
/** Various Active User oriented mutations */
activeUserMutations: ActiveUserMutations;
admin: AdminMutations;
@@ -2081,6 +2137,8 @@ export type Price = {
export type Project = {
__typename?: 'Project';
accSyncItem: AccSyncItem;
accSyncItems: AccSyncItemCollection;
allowPublicComments: Scalars['Boolean']['output'];
/** Get a single automation by id. Error will be thrown if automation is not found or inaccessible. */
automation: Automation;
@@ -2142,6 +2200,11 @@ export type Project = {
};
export type ProjectAccSyncItemArgs = {
id: Scalars['String']['input'];
};
export type ProjectAutomationArgs = {
id: Scalars['String']['input'];
};
@@ -3631,6 +3694,7 @@ export type Subscription = {
* Note: Only works in test environment
*/
ping: Scalars['String']['output'];
projectAccSyncItemsUpdated: Scalars['String']['output'];
/** Subscribe to updates to automations in the project */
projectAutomationsUpdated: ProjectAutomationsUpdatedMessage;
/**
@@ -3747,6 +3811,12 @@ export type SubscriptionCommitUpdatedArgs = {
};
export type SubscriptionProjectAccSyncItemsUpdatedArgs = {
id: Scalars['String']['input'];
itemIds?: InputMaybe<Array<Scalars['String']['input']>>;
};
export type SubscriptionProjectAutomationsUpdatedArgs = {
projectId: Scalars['String']['input'];
};
@@ -3887,6 +3957,12 @@ export type TriggeredAutomationsStatus = {
statusMessage?: Maybe<Scalars['String']['output']>;
};
export type UpdateAccSyncItemInput = {
accFileLineageId: Scalars['ID']['input'];
projectId: Scalars['ID']['input'];
status: AccSyncItemStatus;
};
/** Any null values will be ignored */
export type UpdateAutomateFunctionInput = {
description?: InputMaybe<Scalars['String']['input']>;