bde148f286
* wip * some extra fixes * stuff kinda works? * need to figure out mocks * need to figure out mocks * fix db listener * gqlgen fix * minor gqlgen watch adjustment * lint fixes * delete old codegen file * converting migrations to ESM * getModuleDIrectory * vitest sort of works * added back ts-vitest * resolve gql double load * fixing test timeout configs * TSC lint fix * fix automate tests * moar debugging * debugging * more debugging * codegen update * server works * yargs migrated * chore(server): getting rid of global mocks for Server ESM (#5046) * got rid of email mock * got rid of comment mocks * got rid of multi region mocks * got rid of stripe mock * admin override mock updated * removed final mock * fixing import.meta.resolve calls * another import.meta.resolve fix * added requested test * nyc ESM fix * removed unneeded deps + linting * yarn lock forgot to commit * tryna fix flakyness * email capture util fix * sendEmail fix * fix TSX check * sender transporter fix + CR comments * merge main fix * test fixx * circleci fix * gqlgen bigint fix * error formatter fix * more error formatting improvements * esmloader added to Dockerfile * more dockerfile fixes * bg jobs fix
387 lines
13 KiB
TypeScript
387 lines
13 KiB
TypeScript
import { TIME } from '@speckle/shared'
|
|
import { Resolvers } from '@/modules/core/graph/generated/graphql'
|
|
import { db } from '@/db/knex'
|
|
import {
|
|
getBranchPendingVersionsFactory,
|
|
getFileInfoFactory,
|
|
getFileInfoFactoryV2,
|
|
getModelUploadsItemsFactory,
|
|
getModelUploadsTotalCountFactory,
|
|
getStreamFileUploadsFactory,
|
|
getStreamPendingModelsFactory,
|
|
saveUploadFileFactory,
|
|
saveUploadFileFactoryV2
|
|
} from '@/modules/fileuploads/repositories/fileUploads'
|
|
import {
|
|
FileImportSubscriptions,
|
|
filteredSubscribe
|
|
} from '@/modules/shared/utils/subscriptions'
|
|
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
|
import {
|
|
BadRequestError,
|
|
ForbiddenError,
|
|
MisconfiguredEnvironmentError
|
|
} from '@/modules/shared/errors'
|
|
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
|
|
import {
|
|
fileImportServiceShouldUsePrivateObjectsServerUrl,
|
|
getFileUploadUrlExpiryMinutes,
|
|
getPrivateObjectsServerOrigin,
|
|
getServerOrigin,
|
|
isFileUploadsEnabled
|
|
} from '@/modules/shared/helpers/envHelper'
|
|
import { getProjectObjectStorage } from '@/modules/multiregion/utils/blobStorageSelector'
|
|
import {
|
|
getBlobFactory,
|
|
updateBlobFactory,
|
|
upsertBlobFactory
|
|
} from '@/modules/blobstorage/repositories'
|
|
import {
|
|
getBlobMetadataFromStorage,
|
|
getSignedUrlFactory
|
|
} from '@/modules/blobstorage/clients/objectStorage'
|
|
import { registerUploadCompleteAndStartFileImportFactory } from '@/modules/fileuploads/services/presigned'
|
|
import {
|
|
generatePresignedUrlFactory,
|
|
registerCompletedUploadFactory
|
|
} from '@/modules/blobstorage/services/presigned'
|
|
import { getEventBus } from '@/modules/shared/services/eventBus'
|
|
import {
|
|
insertNewUploadAndNotifyFactory,
|
|
insertNewUploadAndNotifyFactoryV2
|
|
} from '@/modules/fileuploads/services/management'
|
|
import {
|
|
storeApiTokenFactory,
|
|
storeTokenResourceAccessDefinitionsFactory,
|
|
storeTokenScopesFactory,
|
|
storeUserServerAppTokenFactory
|
|
} from '@/modules/core/repositories/tokens'
|
|
import { createAppTokenFactory } from '@/modules/core/services/tokens'
|
|
import { fileImportQueues } from '@/modules/fileuploads/queues/fileimports'
|
|
import { pushJobToFileImporterFactory } from '@/modules/fileuploads/services/createFileImport'
|
|
import { getBranchesByIdsFactory } from '@/modules/core/repositories/branches'
|
|
import { getFileSizeLimit } from '@/modules/blobstorage/services/management'
|
|
import cryptoRandomString from 'crypto-random-string'
|
|
import { getFeatureFlags } from '@speckle/shared/environment'
|
|
import { throwIfResourceAccessNotAllowed } from '@/modules/core/helpers/token'
|
|
import { TokenResourceIdentifierType } from '@/modules/core/domain/tokens/types'
|
|
import { getModelUploadsFactory } from '@/modules/fileuploads/services/management'
|
|
import {
|
|
FileUploadRecord,
|
|
FileUploadRecordV2
|
|
} from '@/modules/fileuploads/helpers/types'
|
|
import { GraphQLContext } from '@/modules/shared/helpers/typeHelper'
|
|
|
|
const { FF_LARGE_FILE_IMPORTS_ENABLED, FF_NEXT_GEN_FILE_IMPORTER_ENABLED } =
|
|
getFeatureFlags()
|
|
|
|
const getFileUploadModel = async (params: {
|
|
upload: FileUploadRecord | FileUploadRecordV2
|
|
ctx: GraphQLContext
|
|
}) => {
|
|
const { upload, ctx } = params
|
|
const projectId = 'streamId' in upload ? upload.streamId : upload.projectId
|
|
|
|
const projectDb = await getProjectDbClient({ projectId })
|
|
if ('modelId' in upload && upload.modelId) {
|
|
return await ctx.loaders
|
|
.forRegion({ db: projectDb })
|
|
.branches.getById.load(upload.modelId)
|
|
}
|
|
|
|
if ('branchName' in upload && upload.branchName) {
|
|
return await ctx.loaders
|
|
.forRegion({ db: projectDb })
|
|
.streams.getStreamBranchByName.forStream(projectId)
|
|
.load(upload.branchName.toLowerCase())
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
const fileUploadMutations: Resolvers['FileUploadMutations'] = {
|
|
async generateUploadUrl(_parent, args, ctx) {
|
|
if (!FF_LARGE_FILE_IMPORTS_ENABLED) {
|
|
throw new MisconfiguredEnvironmentError(
|
|
'The large file import feature is not enabled on this server. Please contact your Speckle administrator.'
|
|
)
|
|
}
|
|
const { projectId } = args.input
|
|
if (!ctx.userId) {
|
|
throw new ForbiddenError('No userId provided')
|
|
}
|
|
|
|
throwIfResourceAccessNotAllowed({
|
|
resourceId: projectId,
|
|
resourceType: TokenResourceIdentifierType.Project,
|
|
resourceAccessRules: ctx.resourceAccessRules
|
|
})
|
|
|
|
const canImport = await ctx.authPolicies.project.canPublish({
|
|
userId: ctx.userId,
|
|
projectId
|
|
})
|
|
throwIfAuthNotOk(canImport)
|
|
|
|
if (!isFileUploadsEnabled())
|
|
throw new BadRequestError('File uploads are not enabled for this server')
|
|
|
|
const [projectDb, projectStorage] = await Promise.all([
|
|
getProjectDbClient({ projectId }),
|
|
getProjectObjectStorage({ projectId })
|
|
])
|
|
|
|
const generatePresignedUrl = generatePresignedUrlFactory({
|
|
getSignedUrl: getSignedUrlFactory({
|
|
objectStorage: projectStorage.public
|
|
}),
|
|
upsertBlob: upsertBlobFactory({
|
|
db: projectDb
|
|
})
|
|
})
|
|
const blobId = cryptoRandomString({ length: 10 })
|
|
|
|
const url = await generatePresignedUrl({
|
|
projectId: args.input.projectId,
|
|
blobId,
|
|
userId: ctx.userId,
|
|
fileName: args.input.fileName,
|
|
urlExpiryDurationSeconds: getFileUploadUrlExpiryMinutes() * TIME.minute
|
|
})
|
|
|
|
return { url, fileId: blobId }
|
|
},
|
|
async startFileImport(_parent, args, ctx) {
|
|
const { projectId } = args.input
|
|
if (!ctx.userId) {
|
|
throw new ForbiddenError('No userId provided')
|
|
}
|
|
|
|
throwIfResourceAccessNotAllowed({
|
|
resourceId: projectId,
|
|
resourceType: TokenResourceIdentifierType.Project,
|
|
resourceAccessRules: ctx.resourceAccessRules
|
|
})
|
|
|
|
const canImport = await ctx.authPolicies.project.canPublish({
|
|
userId: ctx.userId,
|
|
projectId
|
|
})
|
|
throwIfAuthNotOk(canImport)
|
|
|
|
if (!isFileUploadsEnabled())
|
|
throw new BadRequestError('File uploads are not enabled for this server')
|
|
|
|
if (!FF_LARGE_FILE_IMPORTS_ENABLED) {
|
|
throw new MisconfiguredEnvironmentError(
|
|
'The large file import feature is not enabled on this server. Please contact your Speckle administrator.'
|
|
)
|
|
}
|
|
|
|
const [projectDb, projectStorage] = await Promise.all([
|
|
getProjectDbClient({ projectId }),
|
|
getProjectObjectStorage({ projectId })
|
|
])
|
|
|
|
const pushJobToFileImporter = pushJobToFileImporterFactory({
|
|
getServerOrigin: fileImportServiceShouldUsePrivateObjectsServerUrl()
|
|
? getPrivateObjectsServerOrigin
|
|
: getServerOrigin,
|
|
createAppToken: createAppTokenFactory({
|
|
storeApiToken: storeApiTokenFactory({ db }),
|
|
storeTokenScopes: storeTokenScopesFactory({ db }),
|
|
storeTokenResourceAccessDefinitions: storeTokenResourceAccessDefinitionsFactory(
|
|
{ db }
|
|
),
|
|
storeUserServerAppToken: storeUserServerAppTokenFactory({ db })
|
|
})
|
|
})
|
|
|
|
const insertNewUploadAndNotifyV2 = insertNewUploadAndNotifyFactoryV2({
|
|
queues: fileImportQueues,
|
|
pushJobToFileImporter,
|
|
saveUploadFile: saveUploadFileFactoryV2({ db: projectDb }),
|
|
emit: getEventBus().emit
|
|
})
|
|
|
|
const insertNewUploadAndNotify = insertNewUploadAndNotifyFactory({
|
|
saveUploadFile: saveUploadFileFactory({ db: projectDb }),
|
|
emit: getEventBus().emit
|
|
})
|
|
|
|
const registerUploadCompleteAndStartFileImport =
|
|
registerUploadCompleteAndStartFileImportFactory({
|
|
registerCompletedUpload: registerCompletedUploadFactory({
|
|
logger: ctx.log,
|
|
getBlob: getBlobFactory({ db: projectDb }),
|
|
updateBlob: updateBlobFactory({
|
|
db: projectDb
|
|
}),
|
|
getBlobMetadata: getBlobMetadataFromStorage({
|
|
objectStorage: projectStorage.private
|
|
})
|
|
}),
|
|
insertNewUploadAndNotify: FF_NEXT_GEN_FILE_IMPORTER_ENABLED
|
|
? insertNewUploadAndNotifyV2
|
|
: insertNewUploadAndNotify,
|
|
getFileInfo: getFileInfoFactoryV2({ db: projectDb }),
|
|
getModelsByIds: getBranchesByIdsFactory({ db: projectDb })
|
|
})
|
|
|
|
const maximumFileSize = getFileSizeLimit()
|
|
|
|
const uploadedFileData = await registerUploadCompleteAndStartFileImport({
|
|
projectId: args.input.projectId,
|
|
fileId: args.input.fileId,
|
|
modelId: args.input.modelId,
|
|
userId: ctx.userId,
|
|
expectedETag: args.input.etag,
|
|
maximumFileSize
|
|
})
|
|
|
|
return {
|
|
...uploadedFileData,
|
|
streamId: uploadedFileData.projectId,
|
|
branchName: uploadedFileData.modelName
|
|
}
|
|
}
|
|
}
|
|
|
|
export default {
|
|
Stream: {
|
|
async fileUploads(parent) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.id })
|
|
return await getStreamFileUploadsFactory({ db: projectDb })({
|
|
streamId: parent.id
|
|
})
|
|
},
|
|
async fileUpload(parent, args) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.id })
|
|
return await getFileInfoFactory({ db: projectDb })({ fileId: args.id })
|
|
}
|
|
},
|
|
Project: {
|
|
async pendingImportedModels(parent, args) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.id })
|
|
return await getStreamPendingModelsFactory({ db: projectDb })(parent.id, args)
|
|
}
|
|
},
|
|
Model: {
|
|
async pendingImportedVersions(parent, args) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.streamId })
|
|
return await getBranchPendingVersionsFactory({ db: projectDb })(
|
|
parent.streamId,
|
|
parent.name,
|
|
args
|
|
)
|
|
},
|
|
async uploads(parent, args) {
|
|
const projectDb = await getProjectDbClient({ projectId: parent.streamId })
|
|
const getModelUploads = getModelUploadsFactory({
|
|
getModelUploadsItems: getModelUploadsItemsFactory({ db: projectDb }),
|
|
getModelUploadsTotalCount: getModelUploadsTotalCountFactory({ db: projectDb })
|
|
})
|
|
|
|
return await getModelUploads({
|
|
modelId: parent.id,
|
|
projectId: parent.streamId,
|
|
limit: args.input?.limit ?? 25,
|
|
cursor: args.input?.cursor
|
|
})
|
|
}
|
|
},
|
|
FileUpload: {
|
|
projectId: (parent) => ('streamId' in parent ? parent.streamId : parent.projectId),
|
|
streamId: (parent) => ('streamId' in parent ? parent.streamId : parent.projectId),
|
|
modelName: async (parent, _args, ctx) => {
|
|
if ('branchName' in parent) return parent.branchName
|
|
return (await getFileUploadModel({ upload: parent, ctx }))?.name
|
|
},
|
|
branchName: async (parent, _args, ctx) => {
|
|
if ('branchName' in parent) return parent.branchName
|
|
return (await getFileUploadModel({ upload: parent, ctx }))?.name
|
|
},
|
|
convertedVersionId: (parent) => parent.convertedCommitId,
|
|
async model(parent, _args, ctx) {
|
|
return await getFileUploadModel({ upload: parent, ctx })
|
|
},
|
|
updatedAt: (parent) => {
|
|
return parent.convertedLastUpdate || parent.uploadDate
|
|
}
|
|
},
|
|
Mutation: {
|
|
fileUploadMutations: () => ({})
|
|
},
|
|
FileUploadMutations: {
|
|
...fileUploadMutations
|
|
},
|
|
Subscription: {
|
|
projectPendingModelsUpdated: {
|
|
subscribe: filteredSubscribe(
|
|
FileImportSubscriptions.ProjectPendingModelsUpdated,
|
|
async (payload, args, ctx) => {
|
|
const { id: projectId } = args
|
|
if (payload.projectId !== projectId) return false
|
|
|
|
throwIfResourceAccessNotAllowed({
|
|
resourceId: payload.projectId,
|
|
resourceType: TokenResourceIdentifierType.Project,
|
|
resourceAccessRules: ctx.resourceAccessRules
|
|
})
|
|
const canRead = await ctx.authPolicies.project.canRead({
|
|
userId: ctx.userId!,
|
|
projectId: payload.projectId
|
|
})
|
|
throwIfAuthNotOk(canRead)
|
|
|
|
return true
|
|
}
|
|
)
|
|
},
|
|
projectPendingVersionsUpdated: {
|
|
subscribe: filteredSubscribe(
|
|
FileImportSubscriptions.ProjectPendingVersionsUpdated,
|
|
async (payload, args, ctx) => {
|
|
const { id: projectId } = args
|
|
if (payload.projectId !== projectId) return false
|
|
|
|
throwIfResourceAccessNotAllowed({
|
|
resourceId: payload.projectId,
|
|
resourceType: TokenResourceIdentifierType.Project,
|
|
resourceAccessRules: ctx.resourceAccessRules
|
|
})
|
|
const canRead = await ctx.authPolicies.project.canRead({
|
|
userId: ctx.userId!,
|
|
projectId: payload.projectId
|
|
})
|
|
throwIfAuthNotOk(canRead)
|
|
|
|
return true
|
|
}
|
|
)
|
|
},
|
|
projectFileImportUpdated: {
|
|
subscribe: filteredSubscribe(
|
|
FileImportSubscriptions.ProjectFileImportUpdated,
|
|
async (payload, args, ctx) => {
|
|
const { id: projectId } = args
|
|
if (payload.projectId !== projectId) return false
|
|
|
|
throwIfResourceAccessNotAllowed({
|
|
resourceId: payload.projectId,
|
|
resourceType: TokenResourceIdentifierType.Project,
|
|
resourceAccessRules: ctx.resourceAccessRules
|
|
})
|
|
const canRead = await ctx.authPolicies.project.canRead({
|
|
userId: ctx.userId!,
|
|
projectId: payload.projectId
|
|
})
|
|
throwIfAuthNotOk(canRead)
|
|
|
|
return true
|
|
}
|
|
)
|
|
}
|
|
}
|
|
} as Resolvers
|