Files
speckle-server/packages/server/modules/cross-server-sync/services/project.ts
T
Kristaps Fabians Geikins 94b0b5f5e0 fix(server): cross server sync not respecting token (#2181)
* fix(server): cross server sync not respecting token

* minor adjustment

* getting rid of token positional arg
2024-03-29 15:21:06 +02:00

242 lines
6.2 KiB
TypeScript

import { crossServerSyncLogger, Logger } from '@/logging/logging'
import { getUser } from '@/modules/core/repositories/users'
import { CrossServerProjectSyncError } from '@/modules/cross-server-sync/errors'
import {
createApolloClient,
GraphQLClient,
gql,
assertValidGraphQLResult
} from '@/modules/cross-server-sync/utils/graphqlClient'
import { CrossSyncProjectMetadataQuery } from '@/modules/cross-server-sync/graph/generated/graphql'
import { omit } from 'lodash'
import { downloadCommit } from '@/modules/cross-server-sync/services/commit'
import { getFrontendOrigin } from '@/modules/shared/helpers/envHelper'
import { createStreamReturnRecord } from '@/modules/core/services/streams/management'
import { createBranchAndNotify } from '@/modules/core/services/branch/management'
import { getStreamBranchByName } from '@/modules/core/repositories/branches'
type ProjectMetadata = Awaited<ReturnType<typeof getProjectMetadata>>
const PROJECT_URL_RGX = /((https?:\/\/)?[\w.\-_]+)\/projects\/([\w]+)\/?/i
const projectMetadataQuery = gql`
query CrossSyncProjectMetadata($id: String!, $versionsCursor: String) {
project(id: $id) {
id
name
description
visibility
versions(limit: 100, cursor: $versionsCursor) {
totalCount
cursor
items {
id
createdAt
model {
id
name
}
}
}
}
}
`
const getLocalResources = async (params: { authorId: string }) => {
const { authorId } = params
const user = await getUser(authorId)
if (!user) {
throw new CrossServerProjectSyncError('Target author not found')
}
return { user }
}
const parseIncomingUrl = (projectUrl: string) => {
const [, origin, , projectId] = PROJECT_URL_RGX.exec(projectUrl) || []
if (!origin || !projectId) {
throw new CrossServerProjectSyncError('Invalid project URL')
}
return { origin, projectId }
}
const getProjectMetadata = async (params: {
client: GraphQLClient
projectId: string
}) => {
const { client, projectId } = params
// Load 1st page
const res = await client.query<CrossSyncProjectMetadataQuery>({
query: projectMetadataQuery,
variables: {
id: projectId
}
})
assertValidGraphQLResult(res, 'Project metadata query')
const projectInfo = omit(res.data.project, ['versions'])
const versions = res.data.project.versions.items
// Load all pages of versions
let cursor = res.data.project.versions.cursor
let failsafe = 10
while (cursor && failsafe-- > 0) {
const res = await client.query<CrossSyncProjectMetadataQuery>({
query: projectMetadataQuery,
variables: {
id: projectId,
versionsCursor: cursor
}
})
assertValidGraphQLResult(res, 'Project metadata query')
versions.push(...res.data.project.versions.items)
cursor = res.data.project.versions.cursor
}
// Sort versions by descending creation data
versions.sort(
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
)
return { projectInfo, versions }
}
const ensureBranch = async (params: {
streamId: string
branchName: string
authorId: string
}) => {
const { streamId, branchName, authorId } = params
const existingBranch = await getStreamBranchByName(streamId, branchName)
if (!existingBranch) {
const newBranch = await createBranchAndNotify(
{
streamId,
name: branchName
},
authorId
)
return newBranch
}
return existingBranch
}
const importVersions = async (params: {
logger: Logger
projectInfo: ProjectMetadata
localProjectId: string
localAuthorId: string
origin: string
syncComments?: boolean
token?: string
}) => {
const {
logger,
projectInfo,
origin,
localProjectId,
syncComments,
localAuthorId,
token
} = params
const projectId = projectInfo.projectInfo.id
logger.debug(`Serially downloading ${projectInfo.versions.length} versions...`)
for (const version of projectInfo.versions) {
// Ensure branch exists
const branchName = version.model.name
await ensureBranch({
streamId: localProjectId,
branchName,
authorId: localAuthorId
})
// Actually download
const url = new URL(
`/projects/${projectId}/models/${version.model.id}@${version.id}`,
origin
)
await downloadCommit(
{
commitUrl: url.toString(),
targetStreamId: localProjectId,
commentAuthorId: syncComments ? localAuthorId : undefined,
branchName,
token
},
{ logger }
)
}
}
/**
* Downloads a project from an external FE2 Speckle server instance
*/
export const downloadProject = async (
params: {
/**
* An FE2 project URL (must be publicly accessible)
*/
projectUrl: string
/**
* ID of user that should own the project locally
*/
authorId: string
syncComments?: boolean
/**
* Specify if target project is private
*/
token?: string
},
options?: Partial<{
logger: Logger
}>
) => {
const { projectUrl, authorId, syncComments, token } = params
const { logger = crossServerSyncLogger } = options || {}
logger.info(`Project download started at: ${new Date().toISOString()}`)
const localResources = await getLocalResources({ authorId })
const parsedUrl = parseIncomingUrl(projectUrl)
const client = await createApolloClient(parsedUrl.origin, { token })
logger.debug(`Resolving project metadata and associated versions...`)
const projectInfo = await getProjectMetadata({
client,
projectId: parsedUrl.projectId
})
logger.debug(`Creating project locally...`)
const project = await createStreamReturnRecord({
...projectInfo.projectInfo,
ownerId: localResources.user.id
})
await importVersions({
logger,
projectInfo,
localProjectId: project.id,
localAuthorId: localResources.user.id,
origin: parsedUrl.origin,
syncComments,
token
})
logger.info(`Project download completed at: ${new Date().toISOString()}`)
const newProjectUrl = new URL(
`/projects/${project.id}`,
getFrontendOrigin(true)
).toString()
logger.info(`New Project URL: ${newProjectUrl}`)
return {
newProjectUrl,
projectId: project.id,
project
}
}