Files
speckle-server/packages/server/modules/workspaces/repositories/projectRegions.ts
T
2025-05-07 16:47:49 +01:00

707 lines
22 KiB
TypeScript

import {
AutomationFunctionRuns,
AutomationRevisionFunctions,
AutomationRevisions,
AutomationRuns,
AutomationRunTriggers,
Automations,
AutomationTokens,
AutomationTriggers,
BranchCommits,
Branches,
CommentLinks,
Comments,
CommentViews,
Commits,
FileUploads,
Objects,
StreamCommits,
StreamFavorites,
Streams,
StreamsMeta
} from '@/modules/core/dbSchema'
import { Branch } from '@/modules/core/domain/branches/types'
import { Commit } from '@/modules/core/domain/commits/types'
import { Stream } from '@/modules/core/domain/streams/types'
import {
BranchCommitRecord,
ObjectRecord,
CommitRecord,
StreamCommitRecord,
StreamFavoriteRecord,
StreamRecord
} from '@/modules/core/helpers/types'
import { executeBatchedSelect } from '@/modules/shared/helpers/dbHelper'
import {
CopyProjectAutomations,
CopyProjectBlobs,
CopyProjectComments,
CopyProjectModels,
CopyProjectObjects,
CopyProjects,
CopyProjectVersions,
CopyProjectWebhooks,
CopyWorkspace,
CountProjectAutomations,
CountProjectComments,
CountProjectModels,
CountProjectObjects,
CountProjectVersions,
CountProjectWebhooks
} from '@/modules/workspaces/domain/operations'
import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace'
import { Knex } from 'knex'
import { Workspace } from '@/modules/workspacesCore/domain/types'
import { Workspaces } from '@/modules/workspacesCore/helpers/db'
import { ObjectPreview } from '@/modules/previews/domain/types'
import {
AutomationFunctionRunRecord,
AutomationRecord,
AutomationRevisionFunctionRecord,
AutomationRevisionRecord,
AutomationRunRecord,
AutomationRunTriggerRecord,
AutomationTokenRecord,
AutomationTriggerDefinitionRecord
} from '@/modules/automate/helpers/types'
import {
CommentLinkRecord,
CommentRecord,
CommentViewRecord
} from '@/modules/comments/helpers/types'
import { Webhook, WebhookEvent } from '@/modules/webhooks/domain/types'
import { ObjectStorage } from '@/modules/blobstorage/clients/objectStorage'
import { BlobStorage } from '@/modules/blobstorage/repositories'
import { BlobStorageItem } from '@/modules/blobstorage/domain/types'
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'
import { FileUploadRecord } from '@/modules/fileuploads/helpers/types'
import { getObjectKey } from '@/modules/blobstorage/helpers/blobs'
const tables = {
workspaces: (db: Knex) => db<Workspace>(Workspaces.name),
projects: (db: Knex) => db<Stream>(Streams.name),
models: (db: Knex) => db<Branch>(Branches.name),
versions: (db: Knex) => db<Commit>(Commits.name),
branchCommits: (db: Knex) => db<BranchCommitRecord>(BranchCommits.name),
streamCommits: (db: Knex) => db<StreamCommitRecord>(StreamCommits.name),
streamFavorites: (db: Knex) => db<StreamFavoriteRecord>(StreamFavorites.name),
streamsMeta: (db: Knex) => db(StreamsMeta.name),
objects: (db: Knex) => db<ObjectRecord>(Objects.name),
objectPreviews: (db: Knex) => db<ObjectPreview>('object_preview'),
automations: (db: Knex) => db<AutomationRecord>(Automations.name),
automationTokens: (db: Knex) => db<AutomationTokenRecord>(AutomationTokens.name),
automationRevisions: (db: Knex) =>
db<AutomationRevisionRecord>(AutomationRevisions.name),
automationTriggers: (db: Knex) =>
db<AutomationTriggerDefinitionRecord>(AutomationTriggers.name),
automationRevisionFunctions: (db: Knex) =>
db<AutomationRevisionFunctionRecord>(AutomationRevisionFunctions.name),
automationRuns: (db: Knex) => db<AutomationRunRecord>(AutomationRuns.name),
automationRunTriggers: (db: Knex) =>
db<AutomationRunTriggerRecord>(AutomationRunTriggers.name),
automationFunctionRuns: (db: Knex) =>
db<AutomationFunctionRunRecord>(AutomationFunctionRuns.name),
comments: (db: Knex) => db.table<CommentRecord>(Comments.name),
commentViews: (db: Knex) => db.table<CommentViewRecord>(CommentViews.name),
commentLinks: (db: Knex) => db.table<CommentLinkRecord>(CommentLinks.name),
webhooks: (db: Knex) => db.table<Webhook>('webhooks_config'),
webhookEvents: (db: Knex) => db.table<WebhookEvent>('webhooks_events'),
fileUploads: (db: Knex) => db.table<FileUploadRecord>(FileUploads.name),
blobStorage: (db: Knex) => db.table<BlobStorageItem>(BlobStorage.name)
}
const getCountObject = (projectIds: string[]) => {
const countObject: Record<string, number> = {}
for (const projectId of projectIds) {
countObject[projectId] = 0
}
return countObject
}
/**
* Copies rows from the following tables:
* - workspaces
*/
export const copyWorkspaceFactory =
(deps: { sourceDb: Knex; targetDb: Knex }): CopyWorkspace =>
async ({ workspaceId }) => {
const workspace = await tables
.workspaces(deps.sourceDb)
.select('*')
.where({ id: workspaceId })
if (!workspace) {
throw new WorkspaceNotFoundError()
}
await tables
.workspaces(deps.targetDb)
.insert(workspace)
.onConflict(Workspaces.withoutTablePrefix.col.id)
.ignore()
return workspaceId
}
/**
* Copies rows from the following tables:
* - streams
* - streams_meta
* - stream_favorites
*/
export const copyProjectsFactory =
(deps: { sourceDb: Knex; targetDb: Knex }): CopyProjects =>
async ({ projectIds }) => {
const selectProjects = tables
.projects(deps.sourceDb)
.select('*')
.whereIn(Streams.col.id, projectIds)
const copiedProjectIds: string[] = []
// Copy project record
for await (const projects of executeBatchedSelect(selectProjects)) {
const projectIds = projects.map((project) => project.id)
copiedProjectIds.push(...projectIds)
// Copy `streams` rows to target db
await tables
.projects(deps.targetDb)
.insert(projects)
.onConflict(Streams.withoutTablePrefix.col.id)
.merge(Streams.withoutTablePrefix.cols as (keyof StreamRecord)[])
// Fetch `stream_favorites` rows for projects in batch
const selectStreamFavorites = tables
.streamFavorites(deps.sourceDb)
.select('*')
.whereIn(StreamFavorites.col.streamId, projectIds)
for await (const streamFavorites of executeBatchedSelect(selectStreamFavorites)) {
// Copy `stream_favorites` rows to target db
await tables
.streamFavorites(deps.targetDb)
.insert(streamFavorites)
.onConflict()
.ignore()
}
// Fetch `streams_meta` rows for projects in batch
const selectStreamsMetadata = tables
.streamsMeta(deps.sourceDb)
.select('*')
.whereIn(StreamsMeta.col.streamId, projectIds)
for await (const streamsMetadataBatch of executeBatchedSelect(
selectStreamsMetadata
)) {
// Copy `streams_meta` rows to target db
await tables
.streamsMeta(deps.targetDb)
.insert(streamsMetadataBatch)
.onConflict()
.ignore()
}
}
return copiedProjectIds
}
/**
* Copies rows from the following tables:
* - branches
*/
export const copyProjectModelsFactory =
(deps: { sourceDb: Knex; targetDb: Knex }): CopyProjectModels =>
async ({ projectIds }) => {
const copiedModelCountByProjectId = getCountObject(projectIds)
// Fetch `branches` rows for projects in batch
const selectModels = tables
.models(deps.sourceDb)
.select('*')
.whereIn(Branches.col.streamId, projectIds)
for await (const models of executeBatchedSelect(selectModels)) {
// Copy `branches` rows to target db
await tables.models(deps.targetDb).insert(models).onConflict().ignore()
for (const model of models) {
copiedModelCountByProjectId[model.streamId]++
}
}
return copiedModelCountByProjectId
}
/**
* Copies rows from the following tables:
* - commits
* - branch_commits
* - stream_commits
*/
export const copyProjectVersionsFactory =
(deps: { sourceDb: Knex; targetDb: Knex }): CopyProjectVersions =>
async ({ projectIds }) => {
const copiedVersionCountByProjectId = getCountObject(projectIds)
const selectVersions = tables
.streamCommits(deps.sourceDb)
.select('*')
.join<StreamCommitRecord & Commit>(
Commits.name,
Commits.col.id,
StreamCommits.col.commitId
)
.whereIn(StreamCommits.col.streamId, projectIds)
for await (const versions of executeBatchedSelect(selectVersions)) {
const { commitIds, commits } = versions.reduce(
(all, version) => {
const { commitId, streamId, ...commit } = version
all.commitIds.push(commitId)
all.streamIds.push(streamId)
all.commits.push(commit)
return all
},
{ commitIds: [], streamIds: [], commits: [] } as {
commitIds: string[]
streamIds: string[]
commits: CommitRecord[]
}
)
// Copy `commits` rows to target db
await tables.versions(deps.targetDb).insert(commits).onConflict().ignore()
for (const version of versions) {
copiedVersionCountByProjectId[version.streamId]++
}
// Fetch `branch_commits` rows for versions in batch
const selectBranchCommits = tables
.branchCommits(deps.sourceDb)
.select('*')
.whereIn(BranchCommits.col.commitId, commitIds)
for await (const branchCommits of executeBatchedSelect(selectBranchCommits)) {
// Copy `branch_commits` row to target db
await tables
.branchCommits(deps.targetDb)
.insert(branchCommits)
.onConflict()
.ignore()
}
// Fetch `stream_commits` rows for versions in batch
const selectStreamCommits = tables
.streamCommits(deps.sourceDb)
.select('*')
.whereIn(StreamCommits.col.commitId, commitIds)
for await (const streamCommits of executeBatchedSelect(selectStreamCommits)) {
// Copy `stream_commits` row to target db
await tables
.streamCommits(deps.targetDb)
.insert(streamCommits)
.onConflict()
.ignore()
}
}
return copiedVersionCountByProjectId
}
/**
* Copies rows from the following tables:
* - objects
* - object_preview
*/
export const copyProjectObjectsFactory =
(deps: { sourceDb: Knex; targetDb: Knex }): CopyProjectObjects =>
async ({ projectIds }) => {
const copiedObjectCountByProjectId = getCountObject(projectIds)
// Copy `objects` table rows in batches
const selectObjects = tables
.objects(deps.sourceDb)
.select<ObjectRecord[]>('*')
.whereIn(Objects.col.streamId, projectIds)
.orderBy(Objects.col.id)
for await (const objects of executeBatchedSelect(selectObjects)) {
// Write `objects` table rows to target db
await tables.objects(deps.targetDb).insert(objects).onConflict().ignore()
for (const object of objects) {
copiedObjectCountByProjectId[object.streamId]++
}
}
// Copy `object_preview` rows in batches
const selectObjectPreviews = tables
.objectPreviews(deps.sourceDb)
.select<ObjectPreview[]>('*')
.whereIn('streamId', projectIds)
for await (const previews of executeBatchedSelect(selectObjectPreviews)) {
// Write `object_preview` rows to target db
await tables.objectPreviews(deps.targetDb).insert(previews).onConflict().ignore()
}
return copiedObjectCountByProjectId
}
/**
* Copies rows from the following tables:
* - automations
* - automation_tokens
* - automation_revisions
* - automation_triggers
* - automation_revision_functions
* - automation_runs
* - automation_run_triggers
* - automation_function_runs
*/
export const copyProjectAutomationsFactory =
(deps: { sourceDb: Knex; targetDb: Knex }): CopyProjectAutomations =>
async ({ projectIds }) => {
const copiedAutomationCountByProjectId = getCountObject(projectIds)
// Copy `automations` table rows in batches
const selectAutomations = tables
.automations(deps.sourceDb)
.select('*')
.whereIn(Automations.col.projectId, projectIds)
for await (const automations of executeBatchedSelect(selectAutomations)) {
const automationIds = automations.map((automation) => automation.id)
// Write `automations` table rows to target db
await tables
.automations(deps.targetDb)
// Cast ignores unexpected behavior in how knex handles object union types
.insert(automations as unknown as AutomationRecord)
.onConflict()
.ignore()
for (const automation of automations) {
copiedAutomationCountByProjectId[automation.projectId]++
}
// Copy `automation_tokens` rows for automation
const selectAutomationTokens = tables
.automationTokens(deps.sourceDb)
.select('*')
.whereIn(AutomationTokens.col.automationId, automationIds)
for await (const tokens of executeBatchedSelect(selectAutomationTokens)) {
// Write `automation_tokens` row to target db
await tables
.automationTokens(deps.targetDb)
.insert(tokens)
.onConflict()
.ignore()
}
// Copy `automation_revisions` rows for automation
const selectAutomationRevisions = tables
.automationRevisions(deps.sourceDb)
.select('*')
.whereIn(AutomationRevisions.col.automationId, automationIds)
for await (const automationRevisions of executeBatchedSelect(
selectAutomationRevisions
)) {
const automationRevisionIds = automationRevisions.map((revision) => revision.id)
// Write `automation_revisions` rows to target db
await tables
.automationRevisions(deps.targetDb)
.insert(automationRevisions)
.onConflict()
.ignore()
// Copy `automation_triggers` rows for automation revisions
const automationTriggers = await tables
.automationTriggers(deps.sourceDb)
.select('*')
.whereIn(AutomationTriggers.col.automationRevisionId, automationRevisionIds)
await tables
.automationTriggers(deps.targetDb)
.insert(automationTriggers)
.onConflict()
.ignore()
// Copy `automation_revision_functions` rows for automation revisions
const automationRevisionFunctions = await tables
.automationRevisionFunctions(deps.sourceDb)
.select('*')
.whereIn(
AutomationRevisionFunctions.col.automationRevisionId,
automationRevisionIds
)
await tables
.automationRevisionFunctions(deps.targetDb)
.insert(automationRevisionFunctions)
.onConflict()
.ignore()
// Copy `automation_runs` rows for automation revision
const selectAutomationRuns = tables
.automationRuns(deps.sourceDb)
.select('*')
.whereIn(AutomationRuns.col.automationRevisionId, automationRevisionIds)
for await (const automationRuns of executeBatchedSelect(selectAutomationRuns)) {
const automationRunIds = automationRuns.map((run) => run.id)
// Write `automation_runs` row to target db
await tables
.automationRuns(deps.targetDb)
.insert(automationRuns)
.onConflict()
.ignore()
// Copy `automation_run_triggers` rows for automation run
const automationRunTriggers = await tables
.automationRunTriggers(deps.sourceDb)
.select('*')
.whereIn(AutomationRunTriggers.col.automationRunId, automationRunIds)
await tables
.automationRunTriggers(deps.targetDb)
.insert(automationRunTriggers)
.onConflict()
.ignore()
// Copy `automation_function_runs` rows for automation run
const automationFunctionRuns = await tables
.automationFunctionRuns(deps.sourceDb)
.select('*')
.whereIn(AutomationFunctionRuns.col.runId, automationRunIds)
await tables
.automationFunctionRuns(deps.targetDb)
.insert(automationFunctionRuns)
.onConflict()
.ignore()
}
}
}
return copiedAutomationCountByProjectId
}
/**
* Copies rows from the following tables:
* - comments
* - comment_views
* - comment_links
*/
export const copyProjectCommentsFactory =
(deps: { sourceDb: Knex; targetDb: Knex }): CopyProjectComments =>
async ({ projectIds }) => {
const copiedCommentCountByProjectId = getCountObject(projectIds)
// Copy `comments` table rows in batches
const selectComments = tables
.comments(deps.sourceDb)
.select('*')
.whereIn(Comments.col.streamId, projectIds)
for await (const comments of executeBatchedSelect(selectComments)) {
const commentIds = comments.map((comment) => comment.id)
// Write `comments` rows to target db
await tables.comments(deps.targetDb).insert(comments).onConflict().ignore()
for (const comment of comments) {
copiedCommentCountByProjectId[comment.streamId]++
}
// Copy `comment_views` table rows
const commentViews = await tables
.commentViews(deps.sourceDb)
.select('*')
.whereIn(CommentViews.col.commentId, commentIds)
await tables
.commentViews(deps.targetDb)
.insert(commentViews)
.onConflict()
.ignore()
// Copy `comment_links` table rows
const commentLinks = await tables
.commentLinks(deps.sourceDb)
.select('*')
.whereIn(CommentLinks.col.commentId, commentIds)
await tables
.commentLinks(deps.targetDb)
.insert(commentLinks)
.onConflict()
.ignore()
}
return copiedCommentCountByProjectId
}
/**
* Copies rows from the following tables:
* - webhooks_config
* - webhooks_events
*/
export const copyProjectWebhooksFactory =
(deps: { sourceDb: Knex; targetDb: Knex }): CopyProjectWebhooks =>
async ({ projectIds }) => {
const copiedWebhookCountByProjectId = getCountObject(projectIds)
// Copy `webhooks_config` table rows in batches
const selectWebhooks = tables
.webhooks(deps.sourceDb)
.select('*')
.whereIn('streamId', projectIds)
for await (const webhooks of executeBatchedSelect(selectWebhooks)) {
const webhookIds = webhooks.map((webhook) => webhook.id)
// Write `webhooks_config` rows to target db
await tables.webhooks(deps.targetDb).insert(webhooks).onConflict().ignore()
for (const webhook of webhooks) {
copiedWebhookCountByProjectId[webhook.streamId]++
}
// Copy `webhooks_events` table rows in batches
const selectWebhookEvents = tables
.webhookEvents(deps.sourceDb)
.select('*')
.whereIn('webhookId', webhookIds)
for await (const webhookEvents of executeBatchedSelect(selectWebhookEvents)) {
// Write `webhooks_events` rows to target db
await tables
.webhookEvents(deps.targetDb)
.insert(webhookEvents)
.onConflict()
.ignore()
}
}
return copiedWebhookCountByProjectId
}
/**
* Copies rows from the following tables:
* - file_uploads
* - blob_storage
* Also copies blobs in storage from one region to the other.
*/
export const copyProjectBlobs =
(deps: {
sourceDb: Knex
sourceObjectStorage: ObjectStorage
targetDb: Knex
targetObjectStorage: ObjectStorage
}): CopyProjectBlobs =>
async ({ projectIds }) => {
const copiedBlobsCountByProjectId = getCountObject(projectIds)
// Copy `blob_storage` table rows in batches
const selectBlobs = tables
.blobStorage(deps.sourceDb)
.select('*')
.whereIn(BlobStorage.col.streamId, projectIds)
for await (const blobs of executeBatchedSelect(selectBlobs)) {
// Write `blob_storage` rows to target db
await tables.blobStorage(deps.targetDb).insert(blobs).onConflict().ignore()
for (const blob of blobs) {
copiedBlobsCountByProjectId[blob.streamId]++
// Copy file blob from one regional storage to the other
const objectKey = getObjectKey(blob.streamId, blob.id)
const sourceBlob = await deps.sourceObjectStorage.client.send(
new GetObjectCommand({
Bucket: deps.sourceObjectStorage.bucket,
Key: objectKey
})
)
await deps.targetObjectStorage.client.send(
new PutObjectCommand({
Bucket: deps.targetObjectStorage.bucket,
Key: objectKey,
Body: sourceBlob.Body,
ContentLength: sourceBlob.ContentLength
})
)
}
}
// Copy `file_uploads` table rows in batches
const selectFileUploads = tables
.fileUploads(deps.sourceDb)
.select('*')
.whereIn(FileUploads.col.streamId, projectIds)
for await (const fileUploads of executeBatchedSelect(selectFileUploads)) {
// Write `file_uploads` rows to target db
await tables.fileUploads(deps.targetDb).insert(fileUploads).onConflict().ignore()
}
return copiedBlobsCountByProjectId
}
export const countProjectModelsFactory =
(deps: { db: Knex }): CountProjectModels =>
async ({ projectId }) => {
const [res] = await tables.models(deps.db).where({ streamId: projectId }).count()
return parseInt(res?.count?.toString() ?? '0')
}
export const countProjectVersionsFactory =
(deps: { db: Knex }): CountProjectVersions =>
async ({ projectId }) => {
const [res] = await tables
.streamCommits(deps.db)
.where({ streamId: projectId })
.count()
return parseInt(res?.count?.toString() ?? '0')
}
export const countProjectObjectsFactory =
(deps: { db: Knex }): CountProjectObjects =>
async ({ projectId }) => {
const [res] = await tables.objects(deps.db).where({ streamId: projectId }).count()
return parseInt(res?.count?.toString() ?? '0')
}
export const countProjectAutomationsFactory =
(deps: { db: Knex }): CountProjectAutomations =>
async ({ projectId }) => {
const [res] = await tables.automations(deps.db).where({ projectId }).count()
return parseInt(res?.count?.toString() ?? '0')
}
export const countProjectCommentsFactory =
(deps: { db: Knex }): CountProjectComments =>
async ({ projectId }) => {
const [res] = await tables.comments(deps.db).where({ streamId: projectId }).count()
return parseInt(res?.count?.toString() ?? '0')
}
export const countProjectWebhooksFactory =
(deps: { db: Knex }): CountProjectWebhooks =>
async ({ projectId }) => {
const [res] = await tables.webhooks(deps.db).where({ streamId: projectId }).count()
return parseInt(res?.count?.toString() ?? '0')
}