Files
speckle-server/packages/server/modules/workspaces/services/projectRegions.ts
T
Daniel Gak Anagrov 399c998fd7 feat(multiregion): apply prepared transactions to projects (#5322)
* feat(multiregion): replace user replication

* chore(multiregion): optimise replication

* maybe it's this

* postgres is fun

* once more

* chore(multiregion): only replicate test user creation during multiregion tests

* feat: improved replicate_query logic

* fix: minor

* fix: starting issue

* feat: included user create and delete specs to multiregion

* feat: removed console logs

* fix: user defaults

* fix: multiregion test helper

* fix: update scenarios for users

* refactor(multiregion): swap replicateQuery concept to asMultiregionOperation (#5301)

feat(multiregion): introduced asMultregionOperator, refactor test to user builder classes

* chore: renamings

* fix: remove comments

* feat: remove user replication

* refactor: simplified spec usages

* chore: comments

* chore: branches and favs

* chore: more tests

* chore: more tests

* fix linting

* fix tests

* feat: dropping replication

* refactor: moved project delete to service

* fix: comment

* feat: updateStreamFactory and updateProjectFacotry

* deleteProjectFactory + replicateFactory

* deleteWorkspaceFactory

* fix: selector

* fix: tests

* fix tests, finished createStreamFactory

* feat: simplify changes

* fix: remove comment

* fix: minor strucutres

* fix: moveProjectToRegion

* fix: moved branch creation outside of multiregion scope

* fix: branch creation

* fix: tests

* fix: ci tests

* fix: removed log form test

* fix: on specs, no random regionKeys

* review fixes

* fix: mr comments

* feat: removed test

---------

Co-authored-by: Charles Driesler <chuck@speckle.systems>
2025-09-04 13:07:19 +02:00

163 lines
5.5 KiB
TypeScript

import type { GetProject } from '@/modules/core/domain/projects/operations'
import type {
CopyProjectAutomations,
CopyProjectBlobs,
CopyProjectComments,
CopyProjectModels,
CopyProjectObjects,
CopyProjects,
CopyProjectVersions,
CopyProjectWebhooks,
CopyWorkspace,
CountProjectAutomations,
CountProjectComments,
CountProjectModels,
CountProjectObjects,
CountProjectVersions,
CountProjectWebhooks,
GetAvailableRegions,
MoveProjectToRegion,
ValidateProjectRegionCopy
} from '@/modules/workspaces/domain/operations'
import { ProjectRegionAssignmentError } from '@/modules/workspaces/errors/regions'
import { logger } from '@/observability/logging'
export const moveProjectToRegionFactory =
(deps: {
getProject: GetProject
getAvailableRegions: GetAvailableRegions
copyWorkspace: CopyWorkspace
copyProjects: CopyProjects
copyProjectModels: CopyProjectModels
copyProjectVersions: CopyProjectVersions
copyProjectObjects: CopyProjectObjects
copyProjectAutomations: CopyProjectAutomations
copyProjectComments: CopyProjectComments
copyProjectWebhooks: CopyProjectWebhooks
copyProjectBlobs: CopyProjectBlobs
validateProjectRegionCopy: ValidateProjectRegionCopy
}): MoveProjectToRegion =>
async (params) => {
const { projectId, regionKey } = params
const project = await deps.getProject({ projectId })
if (!project) {
throw new ProjectRegionAssignmentError('Project not found', {
info: { params }
})
}
if (!project.workspaceId) {
throw new ProjectRegionAssignmentError('Project not a part of a workspace', {
info: { params }
})
}
const availableRegions = await deps.getAvailableRegions({
workspaceId: project.workspaceId
})
if (!availableRegions.find((region) => region.key === regionKey)) {
throw new ProjectRegionAssignmentError(
'Specified region not available for workspace',
{
info: {
params,
workspaceId: project.workspaceId
}
}
)
}
// Move workspace
await deps.copyWorkspace({ workspaceId: project.workspaceId })
// Move commits
const projectIds = await deps.copyProjects({ projectIds: [projectId] })
const copiedModelCount = await deps.copyProjectModels({ projectIds })
const copiedVersionCount = await deps.copyProjectVersions({ projectIds })
// Move objects
const copiedObjectCount = await deps.copyProjectObjects({ projectIds })
// Move automations
const copiedAutomationCount = await deps.copyProjectAutomations({ projectIds })
// Move comments
const copiedCommentCount = await deps.copyProjectComments({ projectIds })
// Move webhooks
const copiedWebhookCount = await deps.copyProjectWebhooks({ projectIds })
// Move file blobs
await deps.copyProjectBlobs({ projectIds })
// Validate that state after move captures latest state of project
const targetProjectResources = {
models: copiedModelCount[projectId] ?? 0,
versions: copiedVersionCount[projectId] ?? 0,
objects: copiedObjectCount[projectId] ?? 0,
automations: copiedAutomationCount[projectId] ?? 0,
comments: copiedCommentCount[projectId] ?? 0,
webhooks: copiedWebhookCount[projectId] ?? 0
}
const [isValidCopy, sourceProjectResources] = await deps.validateProjectRegionCopy({
projectId,
copiedRowCount: targetProjectResources
})
if (!isValidCopy) {
// TODO: Move failed or source project added data while changing regions. Retry move.
logger.error(
{
sourceData: sourceProjectResources,
targetData: targetProjectResources
},
'Failed to copy all project resources during project region move.'
)
throw new ProjectRegionAssignmentError(
'Missing data from source project in target region copy after move.'
)
}
}
export const validateProjectRegionCopyFactory =
(deps: {
countProjectModels: CountProjectModels
countProjectVersions: CountProjectVersions
countProjectObjects: CountProjectObjects
countProjectAutomations: CountProjectAutomations
countProjectComments: CountProjectComments
countProjectWebhooks: CountProjectWebhooks
}): ValidateProjectRegionCopy =>
async ({ projectId, copiedRowCount }) => {
const sourceProjectModelCount = await deps.countProjectModels({ projectId })
const sourceProjectVersionCount = await deps.countProjectVersions({ projectId })
const sourceProjectObjectCount = await deps.countProjectObjects({ projectId })
const sourceProjectAutomationCount = await deps.countProjectAutomations({
projectId
})
const sourceProjectCommentCount = await deps.countProjectComments({ projectId })
const sourceProjectWebhooksCount = await deps.countProjectWebhooks({ projectId })
const tests = [
copiedRowCount.models === sourceProjectModelCount,
copiedRowCount.versions === sourceProjectVersionCount,
copiedRowCount.objects === sourceProjectObjectCount,
copiedRowCount.automations === sourceProjectAutomationCount,
copiedRowCount.comments === sourceProjectCommentCount,
copiedRowCount.webhooks === sourceProjectWebhooksCount
]
return [
tests.every((test) => !!test),
{
models: sourceProjectModelCount,
versions: sourceProjectVersionCount,
objects: sourceProjectObjectCount,
automations: sourceProjectAutomationCount,
comments: sourceProjectCommentCount,
webhooks: sourceProjectWebhooksCount
}
]
}