Files
speckle-server/packages/server/modules/fileuploads/repositories/fileUploads.ts
T
Kristaps Fabians Geikins bde148f286 chore(server): migrating fully to ESM (#5042)
* 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
2025-07-14 10:26:19 +03:00

335 lines
9.3 KiB
TypeScript

import { Branches, FileUploads } from '@/modules/core/dbSchema'
import {
GarbageCollectPendingUploadedFiles,
GetFileInfo,
SaveUploadFile,
SaveUploadFileV2,
SaveUploadFileInput,
SaveUploadFileInputV2,
GetFileInfoV2,
UpdateFileUpload,
GetModelUploadsItems,
GetModelUploadsBaseArgs,
GetModelUploadsTotalCount,
UpdateFileStatus
} from '@/modules/fileuploads/domain/operations'
import {
FileUploadConvertedStatus,
FileUploadRecord,
FileUploadRecordV2
} from '@/modules/fileuploads/helpers/types'
import { Knex } from 'knex'
import { FileImportJobNotFoundError } from '@/modules/fileuploads/helpers/errors'
import { compositeCursorTools } from '@/modules/shared/helpers/dbHelper'
import { clamp } from 'lodash-es'
const tables = {
fileUploads: (db: Knex) => db<FileUploadRecord>(FileUploads.name)
}
const getCursorTools = () =>
compositeCursorTools({
schema: FileUploads,
cols: ['convertedLastUpdate', 'id']
})
export const getFileInfoFactory =
(deps: { db: Knex }): GetFileInfo =>
async (params) => {
const { fileId } = params
const fileInfo = await tables
.fileUploads(deps.db)
.where({ [FileUploads.col.id]: fileId })
.select<FileUploadRecord[]>('*')
.first()
return fileInfo
}
export const getFileInfoFactoryV2 =
(deps: { db: Knex }): GetFileInfoV2 =>
async (params) => {
const { fileId, projectId } = params
const q = tables
.fileUploads(deps.db)
.where({ [FileUploads.col.id]: fileId })
.select<FileUploadRecord[]>('*')
if (projectId) q.andWhere(FileUploads.col.streamId, projectId)
const fileInfo = await q.first()
if (!fileInfo) return undefined
return { ...fileInfo, projectId: fileInfo.streamId } satisfies FileUploadRecordV2
}
export const getStreamFileUploadsFactory =
(deps: { db: Knex }) => async (params: { streamId: string }) => {
const { streamId } = params
const fileInfos = await tables
.fileUploads(deps.db)
.select<FileUploadRecord[]>('*')
.where({ [FileUploads.col.streamId]: streamId })
.andWhere((q1) => {
q1.orWhereIn(FileUploads.col.convertedStatus, [
FileUploadConvertedStatus.Completed,
FileUploadConvertedStatus.Error
]).orWhere(
FileUploads.col.uploadDate,
'>=',
deps.db.raw(`now()-'1 day'::interval`)
)
})
.orderBy([
{ column: FileUploads.withoutTablePrefix.col.uploadDate, order: 'desc' }
])
return fileInfos
}
// While we haven't fully migrated to new endpoint
const mapFileUploadRecordToV2 = (record: FileUploadRecord): FileUploadRecordV2 => {
return {
id: record.id,
projectId: record.streamId,
modelId: record.modelId,
userId: record.userId,
fileName: record.fileName,
fileType: record.fileType,
fileSize: record.fileSize,
uploadComplete: record.uploadComplete,
uploadDate: record.uploadDate,
convertedStatus: record.convertedStatus,
convertedLastUpdate: record.convertedLastUpdate,
convertedMessage: record.convertedMessage,
convertedCommitId: record.convertedCommitId
} as FileUploadRecordV2
}
export const saveUploadFileFactory =
(deps: { db: Knex }): SaveUploadFile =>
async ({
fileId,
streamId,
branchName,
userId,
fileName,
fileType,
fileSize,
modelId
}: SaveUploadFileInput) => {
const dbFile: Partial<FileUploadRecord> = {
id: fileId,
streamId,
branchName,
userId,
fileName,
fileType,
fileSize,
uploadComplete: true,
modelId
}
const [newRecord] = await tables.fileUploads(deps.db).insert(dbFile, '*')
return newRecord as FileUploadRecord
}
export const saveUploadFileFactoryV2 =
(deps: { db: Knex }): SaveUploadFileV2 =>
async ({
fileId,
projectId,
modelId,
userId,
fileName,
fileType,
fileSize,
modelName
}: SaveUploadFileInputV2) => {
const dbFile: Partial<SaveUploadFileV2> = {
id: fileId,
streamId: projectId,
branchName: modelName, // @deprecated
userId,
modelId,
fileName,
fileType,
fileSize,
uploadComplete: true
}
const [newRecord] = await tables.fileUploads(deps.db).insert(dbFile, '*')
return mapFileUploadRecordToV2(newRecord)
}
export const expireOldPendingUploadsFactory =
(deps: { db: Knex }): GarbageCollectPendingUploadedFiles =>
async (params: { timeoutThresholdSeconds: number }) => {
const updatedRows = await deps
.db(FileUploads.name)
.whereIn(FileUploads.withoutTablePrefix.col.convertedStatus, [
FileUploadConvertedStatus.Converting
])
.andWhere(
FileUploads.withoutTablePrefix.col.convertedLastUpdate,
'<',
deps.db.raw(`now() - interval '${params.timeoutThresholdSeconds} seconds'`)
)
.update({
[FileUploads.withoutTablePrefix.col.convertedStatus]:
FileUploadConvertedStatus.Error,
[FileUploads.withoutTablePrefix.col.convertedMessage]:
'File import job timed out',
[FileUploads.withoutTablePrefix.col.convertedLastUpdate]: deps.db.fn.now()
})
.returning<FileUploadRecord[]>('*')
return updatedRows
}
const getPendingUploadsBaseQueryFactory =
(deps: { db: Knex }) =>
(streamId: string, options?: Partial<{ ignoreOld: boolean; limit: number }>) => {
const { ignoreOld = true, limit } = options || {}
const q = tables
.fileUploads(deps.db)
.where(FileUploads.col.streamId, streamId)
.whereIn(FileUploads.col.convertedStatus, [
FileUploadConvertedStatus.Queued,
FileUploadConvertedStatus.Converting
])
.orderBy(FileUploads.col.uploadDate, 'desc')
if (ignoreOld) {
q.andWhere(
FileUploads.col.uploadDate,
'>=',
deps.db.raw(`now()-'1 day'::interval`)
)
}
if (limit) {
q.limit(limit)
}
return q
}
export const getStreamPendingModelsFactory =
(deps: { db: Knex }) =>
async (
streamId: string,
options?: Partial<{ limit: number; branchNamePattern: string }>
) => {
const q = getPendingUploadsBaseQueryFactory(deps)(streamId, {
limit: options?.limit
}).whereNotIn(
FileUploads.col.branchName,
Branches.knex().select(Branches.col.name).where(Branches.col.streamId, streamId)
)
if (options?.branchNamePattern) {
q.whereRaw(
deps.db.raw(`?? ~* ?`, [FileUploads.col.branchName, options.branchNamePattern])
)
}
return await q
}
export const getBranchPendingVersionsFactory =
(deps: { db: Knex }) =>
async (
streamId: string,
branchName: string,
options?: Partial<{ limit: number }>
) => {
const q = getPendingUploadsBaseQueryFactory(deps)(streamId, {
limit: options?.limit
})
.where(FileUploads.col.branchName, branchName)
.whereIn(
FileUploads.col.branchName,
Branches.knex().select(Branches.col.name).where(Branches.col.streamId, streamId)
)
return await q
}
export const updateFileUploadFactory =
(deps: { db: Knex }): UpdateFileUpload =>
async (params) => {
const { id, upload } = params
const updatedFile = await tables
.fileUploads(deps.db)
.update(upload)
.where({ [FileUploads.col.id]: id })
.returning<FileUploadRecord[]>('*')
if (updatedFile.length === 0) {
throw new FileImportJobNotFoundError(`File with id ${id} not found`)
}
return updatedFile[0]
}
export const updateFileStatusFactory =
(deps: { db: Knex }): UpdateFileStatus =>
async (params) => {
const updatedFile = await tables
.fileUploads(deps.db)
.update({
[FileUploads.withoutTablePrefix.col.convertedStatus]: params.status,
[FileUploads.withoutTablePrefix.col.convertedMessage]: params.convertedMessage,
[FileUploads.withoutTablePrefix.col.convertedCommitId]:
params.convertedCommitId,
[FileUploads.withoutTablePrefix.col.convertedLastUpdate]: deps.db.fn.now()
})
.where({
[FileUploads.withoutTablePrefix.col.id]: params.fileId,
[FileUploads.withoutTablePrefix.col.streamId]: params.projectId
})
.returning<FileUploadRecord[]>('*')
if (updatedFile.length === 0) {
throw new FileImportJobNotFoundError(`File with id ${params.fileId} not found`)
}
return updatedFile[0]
}
const getModelUploadsBaseQueryFactory =
(deps: { db: Knex }) => (params: GetModelUploadsBaseArgs) => {
const { projectId, modelId } = params
const q = tables
.fileUploads(deps.db)
.where(FileUploads.col.streamId, projectId)
.andWhere(FileUploads.col.modelId, modelId)
return q
}
export const getModelUploadsItemsFactory =
(deps: { db: Knex }): GetModelUploadsItems =>
async (params) => {
const limit = clamp(params.limit || 0, 0, 100)
const { applyCursorSortAndFilter, resolveNewCursor } = getCursorTools()
const q = getModelUploadsBaseQueryFactory(deps)(params).limit(limit)
applyCursorSortAndFilter({
query: q,
cursor: params.cursor
})
const rows = await q
const newCursor = resolveNewCursor(rows)
return {
items: rows,
cursor: newCursor
}
}
export const getModelUploadsTotalCountFactory =
(deps: { db: Knex }): GetModelUploadsTotalCount =>
async (params) => {
const q = getModelUploadsBaseQueryFactory(deps)(params)
const [{ count }] = await q.count()
return parseInt(count + '')
}