From 2c122a138db0c5154a36ffd114de341bc1a489e0 Mon Sep 17 00:00:00 2001 From: Daniel Gak Anagrov Date: Thu, 11 Sep 2025 09:08:26 +0100 Subject: [PATCH] feat(workspaces): apply prepared transactions to workspaces (#5383) * 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 * feat: workspace replciation * fix: mr comments * feat: removed test * fix: worksapce test creation * fix: mr issues * updated mutations * feat: drop workspace random defaults --------- Co-authored-by: Charles Driesler --- .../server/modules/core/tests/users.spec.ts | 10 +- .../modules/core/tests/usersAdmin.spec.ts | 4 +- .../integration/billingRepositories.spec.ts | 14 +- .../modules/multiregion/tests/helpers.ts | 2 +- .../workspaces/graph/resolvers/regions.ts | 11 +- .../workspaces/graph/resolvers/workspaces.ts | 154 +++++++++--------- .../modules/workspaces/services/projects.ts | 19 --- .../modules/workspaces/services/regions.ts | 7 +- .../workspaces/tests/helpers/creation.ts | 124 ++++++++------ .../tests/integration/repositories.spec.ts | 30 +++- .../workspacesCreationState.spec.ts | 8 +- .../20250905074349_drop_workspace_defaults.ts | 20 +++ packages/server/test/authHelper.ts | 4 +- 13 files changed, 231 insertions(+), 176 deletions(-) create mode 100644 packages/server/modules/workspacesCore/migrations/20250905074349_drop_workspace_defaults.ts diff --git a/packages/server/modules/core/tests/users.spec.ts b/packages/server/modules/core/tests/users.spec.ts index b2c2eff13..485504777 100644 --- a/packages/server/modules/core/tests/users.spec.ts +++ b/packages/server/modules/core/tests/users.spec.ts @@ -107,7 +107,7 @@ import { deleteProjectAndCommitsFactory, queryAllProjectsFactory } from '@/modules/core/services/projects' -import { getTestRegionClients } from '@/modules/multiregion/tests/helpers' +import { getAllRegisteredTestDbs } from '@/modules/multiregion/tests/helpers' import { asMultiregionalOperation, replicateFactory } from '@/modules/shared/command' import type { ChangeUserPassword, @@ -187,7 +187,7 @@ const createUser: CreateValidatedUser = async (...input) => return createUser(...input) }, { - dbs: await getTestRegionClients(), + dbs: await getAllRegisteredTestDbs(), name: 'create user spec', logger: dbLogger } @@ -218,7 +218,7 @@ const updateUser: UpdateUserAndNotify = async (...input) => { logger: dbLogger, name: 'update user and notify spec', - dbs: await getTestRegionClients() + dbs: await getAllRegisteredTestDbs() } ) @@ -241,7 +241,7 @@ const updateUserPassword: ChangeUserPassword = async (...input) => { logger: dbLogger, name: 'update user password spec', - dbs: await getTestRegionClients() + dbs: await getAllRegisteredTestDbs() } ) @@ -283,7 +283,7 @@ const deleteUser: DeleteUser = async (...input) => { logger: dbLogger, name: 'delete user spec', - dbs: await getTestRegionClients() + dbs: await getAllRegisteredTestDbs() } ) diff --git a/packages/server/modules/core/tests/usersAdmin.spec.ts b/packages/server/modules/core/tests/usersAdmin.spec.ts index 368a0730d..54ad10744 100644 --- a/packages/server/modules/core/tests/usersAdmin.spec.ts +++ b/packages/server/modules/core/tests/usersAdmin.spec.ts @@ -55,7 +55,7 @@ import { deleteProjectCommitsFactory } from '@/modules/core/repositories/commits import { deleteProjectFactory } from '@/modules/core/repositories/projects' import type { DeleteUser } from '@/modules/core/domain/users/operations' import { asMultiregionalOperation, replicateFactory } from '@/modules/shared/command' -import { getTestRegionClients } from '@/modules/multiregion/tests/helpers' +import { getAllRegisteredTestDbs } from '@/modules/multiregion/tests/helpers' const getUsers = legacyGetPaginatedUsersFactory({ db }) const countUsers = legacyGetPaginatedUsersCountFactory({ db }) @@ -124,7 +124,7 @@ const deleteUser: DeleteUser = async (...input) => { logger: dbLogger, name: 'delete user spec', - dbs: await getTestRegionClients() + dbs: await getAllRegisteredTestDbs() } ) diff --git a/packages/server/modules/gatekeeper/tests/integration/billingRepositories.spec.ts b/packages/server/modules/gatekeeper/tests/integration/billingRepositories.spec.ts index 47b4094f7..e980ea98f 100644 --- a/packages/server/modules/gatekeeper/tests/integration/billingRepositories.spec.ts +++ b/packages/server/modules/gatekeeper/tests/integration/billingRepositories.spec.ts @@ -18,14 +18,26 @@ import { createTestSubscriptionData, createTestWorkspaceSubscription } from '@/modules/gatekeeper/tests/helpers' +import { getAllRegisteredDbs } from '@/modules/multiregion/utils/dbSelector' +import { asMultiregionalOperation, replicateFactory } from '@/modules/shared/command' +import type { UpsertWorkspace } from '@/modules/workspaces/domain/operations' import { upsertWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces' +import { logger } from '@/observability/logging' import { truncateTables } from '@/test/hooks' import { createAndStoreTestWorkspaceFactory } from '@/test/speckle-helpers/workspaces' import { PaidWorkspacePlans, WorkspaceFeatureFlags } from '@speckle/shared' import { expect } from 'chai' import cryptoRandomString from 'crypto-random-string' -const upsertWorkspace = upsertWorkspaceFactory({ db }) +const upsertWorkspace: UpsertWorkspace = async (...args) => + asMultiregionalOperation( + ({ allDbs }) => replicateFactory(allDbs, upsertWorkspaceFactory)(...args), + { + logger, + name: 'delete workspace spec', + dbs: await getAllRegisteredDbs() + } + ) const createAndStoreTestWorkspace = createAndStoreTestWorkspaceFactory({ upsertWorkspace }) diff --git a/packages/server/modules/multiregion/tests/helpers.ts b/packages/server/modules/multiregion/tests/helpers.ts index 3eae3a7d7..0fc8990cc 100644 --- a/packages/server/modules/multiregion/tests/helpers.ts +++ b/packages/server/modules/multiregion/tests/helpers.ts @@ -6,7 +6,7 @@ import { import { isMultiRegionTestMode } from '@/test/speckle-helpers/regions' import type { Knex } from 'knex' -export async function getTestRegionClients(): Promise<[Knex, ...Knex[]]> { +export async function getAllRegisteredTestDbs(): Promise<[Knex, ...Knex[]]> { if (!isMultiRegionTestMode()) return [db] const regionClients = await getRegisteredRegionClients() diff --git a/packages/server/modules/workspaces/graph/resolvers/regions.ts b/packages/server/modules/workspaces/graph/resolvers/regions.ts index c84ccb26b..8884f6c97 100644 --- a/packages/server/modules/workspaces/graph/resolvers/regions.ts +++ b/packages/server/modules/workspaces/graph/resolvers/regions.ts @@ -2,17 +2,13 @@ import { db } from '@/db/knex' import type { Resolvers } from '@/modules/core/graph/generated/graphql' import { getWorkspacePlanFactory } from '@/modules/gatekeeper/repositories/billing' import { canWorkspaceUseRegionsFactory } from '@/modules/gatekeeper/services/featureAuthorization' -import { getDb } from '@/modules/multiregion/utils/dbSelector' import { getRegionsFactory } from '@/modules/multiregion/repositories' import { authorizeResolver } from '@/modules/shared' import { getDefaultRegionFactory, upsertRegionAssignmentFactory } from '@/modules/workspaces/repositories/regions' -import { - getWorkspaceFactory, - upsertWorkspaceFactory -} from '@/modules/workspaces/repositories/workspaces' +import { getWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces' import { assignWorkspaceRegionFactory, getAvailableRegionsFactory @@ -51,8 +47,6 @@ export default { regionKey }) - const regionDb = await getDb({ regionKey }) - const assignRegion = assignWorkspaceRegionFactory({ getAvailableRegions: getAvailableRegionsFactory({ getRegions: getRegionsFactory({ db }), @@ -62,8 +56,7 @@ export default { }), upsertRegionAssignment: upsertRegionAssignmentFactory({ db }), getDefaultRegion: getDefaultRegionFactory({ db }), - getWorkspace: getWorkspaceFactory({ db }), - insertRegionWorkspace: upsertWorkspaceFactory({ db: regionDb }) + getWorkspace: getWorkspaceFactory({ db }) }) await withOperationLogging( async () => await assignRegion({ workspaceId, regionKey }), diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts index 96e80171c..832ae8f28 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts @@ -677,62 +677,64 @@ export default FF_WORKSPACES_MODULE_ENABLED const logger = context.log - return await asOperation( - async ({ db, emit }) => { + return await asMultiregionalOperation( + async ({ mainDb, allDbs, emit }) => { const createWorkspace = createWorkspaceFactory({ validateSlug: validateSlugFactory({ - getWorkspaceBySlug: getWorkspaceBySlugFactory({ db }) + getWorkspaceBySlug: getWorkspaceBySlugFactory({ db: mainDb }) }), generateValidSlug: generateValidSlugFactory({ - getWorkspaceBySlug: getWorkspaceBySlugFactory({ db }) + getWorkspaceBySlug: getWorkspaceBySlugFactory({ db: mainDb }) }), - upsertWorkspace: upsertWorkspaceFactory({ db }), + upsertWorkspace: replicateFactory(allDbs, upsertWorkspaceFactory), emitWorkspaceEvent: emit, addOrUpdateWorkspaceRole: addOrUpdateWorkspaceRoleFactory({ - getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }), - findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ - db + getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ + db: mainDb }), - getWorkspaceRoles: getWorkspaceRolesFactory({ db }), - upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }), + findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ + db: mainDb + }), + getWorkspaceRoles: getWorkspaceRolesFactory({ db: mainDb }), + upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db: mainDb }), emitWorkspaceEvent: emit, ensureValidWorkspaceRoleSeat: ensureValidWorkspaceRoleSeatFactory({ - createWorkspaceSeat: createWorkspaceSeatFactory({ db }), - getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db }), + createWorkspaceSeat: createWorkspaceSeatFactory({ db: mainDb }), + getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db: mainDb }), getWorkspaceDefaultSeatType: getWorkspaceDefaultSeatTypeFactory({ - getWorkspace: getWorkspaceFactory({ db }) + getWorkspace: getWorkspaceFactory({ db: mainDb }) }), eventEmit: emit }), assignWorkspaceSeat: assignWorkspaceSeatFactory({ - createWorkspaceSeat: createWorkspaceSeatFactory({ db }), + createWorkspaceSeat: createWorkspaceSeatFactory({ db: mainDb }), getWorkspaceRoleForUser: getWorkspaceRoleForUserFactory({ - db + db: mainDb }), eventEmit: emit, - getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db }) + getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db: mainDb }) }) }) }) const updateWorkspace = updateWorkspaceFactory({ validateSlug: validateSlugFactory({ - getWorkspaceBySlug: getWorkspaceBySlugFactory({ db }) + getWorkspaceBySlug: getWorkspaceBySlugFactory({ db: mainDb }) }), - getWorkspace: getWorkspaceWithDomainsFactory({ db }), + getWorkspace: getWorkspaceWithDomainsFactory({ db: mainDb }), getWorkspaceSsoProviderRecord: getWorkspaceSsoProviderFactory({ - db, + db: mainDb, decrypt: getDecryptor() }), - upsertWorkspace: upsertWorkspaceFactory({ db }), + upsertWorkspace: replicateFactory(allDbs, upsertWorkspaceFactory), emitWorkspaceEvent: emit }) const addDomain = addDomainToWorkspaceFactory({ - getWorkspace: getWorkspaceFactory({ db }), - findEmailsByUserId: findEmailsByUserIdFactory({ db }), - storeWorkspaceDomain: storeWorkspaceDomainFactory({ db }), - getDomains: getWorkspaceDomainsFactory({ db }), + getWorkspace: getWorkspaceFactory({ db: mainDb }), + findEmailsByUserId: findEmailsByUserIdFactory({ db: mainDb }), + storeWorkspaceDomain: storeWorkspaceDomainFactory({ db: mainDb }), + getDomains: getWorkspaceDomainsFactory({ db: mainDb }), emitWorkspaceEvent: emit }) @@ -769,7 +771,7 @@ export default FF_WORKSPACES_MODULE_ENABLED logger, name: 'createWorkspace', description: 'Create workspace', - transaction: true + dbs: await getAllRegisteredDbs() } ) }, @@ -865,19 +867,6 @@ export default FF_WORKSPACES_MODULE_ENABLED workspaceId }) - const updateWorkspace = updateWorkspaceFactory({ - validateSlug: validateSlugFactory({ - getWorkspaceBySlug: getWorkspaceBySlugFactory({ db }) - }), - getWorkspace: getWorkspaceWithDomainsFactory({ db }), - getWorkspaceSsoProviderRecord: getWorkspaceSsoProviderFactory({ - db, - decrypt: getDecryptor() - }), - upsertWorkspace: upsertWorkspaceFactory({ db }), - emitWorkspaceEvent: getEventBus().emit - }) - if (workspaceInput.isExclusive) { const canMakeWorkspaceExclusive = await context.authPolicies.workspace.canUseWorkspacePlanFeature({ @@ -888,18 +877,33 @@ export default FF_WORKSPACES_MODULE_ENABLED throwIfAuthNotOk(canMakeWorkspaceExclusive) } - const workspace = await withOperationLogging( - async () => - await updateWorkspace({ + const workspace = await asMultiregionalOperation( + async ({ allDbs, mainDb, emit }) => { + const updateWorkspace = updateWorkspaceFactory({ + validateSlug: validateSlugFactory({ + getWorkspaceBySlug: getWorkspaceBySlugFactory({ db: mainDb }) + }), + getWorkspace: getWorkspaceWithDomainsFactory({ db: mainDb }), + getWorkspaceSsoProviderRecord: getWorkspaceSsoProviderFactory({ + db: mainDb, + decrypt: getDecryptor() + }), + upsertWorkspace: replicateFactory(allDbs, upsertWorkspaceFactory), + emitWorkspaceEvent: emit + }) + + return updateWorkspace({ workspaceId, workspaceInput: { ...omit(workspaceInput, ['defaultProjectRole']) } - }), + }) + }, { logger, - operationName: 'updateWorkspace', - operationDescription: 'Update workspace' + name: 'updateWorkspace', + description: 'Update workspace', + dbs: await getAllRegisteredDbs() } ) @@ -1048,32 +1052,34 @@ export default FF_WORKSPACES_MODULE_ENABLED workspaceId }) - const deleteWorkspaceDomain = deleteWorkspaceDomainFactory({ - deleteWorkspaceDomain: repoDeleteWorkspaceDomainFactory({ db }), - countDomainsByWorkspaceId: countDomainsByWorkspaceIdFactory({ - db - }), - updateWorkspace: updateWorkspaceFactory({ - validateSlug: validateSlugFactory({ - getWorkspaceBySlug: getWorkspaceBySlugFactory({ db }) - }), - getWorkspace: getWorkspaceWithDomainsFactory({ db }), - getWorkspaceSsoProviderRecord: getWorkspaceSsoProviderFactory({ - db, - decrypt: getDecryptor() - }), - upsertWorkspace: upsertWorkspaceFactory({ db }), - emitWorkspaceEvent: getEventBus().emit - }) - }) + await asMultiregionalOperation( + async ({ allDbs, mainDb, emit }) => { + const deleteWorkspaceDomain = deleteWorkspaceDomainFactory({ + deleteWorkspaceDomain: repoDeleteWorkspaceDomainFactory({ db: mainDb }), + countDomainsByWorkspaceId: countDomainsByWorkspaceIdFactory({ + db: mainDb + }), + updateWorkspace: updateWorkspaceFactory({ + validateSlug: validateSlugFactory({ + getWorkspaceBySlug: getWorkspaceBySlugFactory({ db: mainDb }) + }), + getWorkspace: getWorkspaceWithDomainsFactory({ db: mainDb }), + getWorkspaceSsoProviderRecord: getWorkspaceSsoProviderFactory({ + db: mainDb, + decrypt: getDecryptor() + }), + upsertWorkspace: replicateFactory(allDbs, upsertWorkspaceFactory), + emitWorkspaceEvent: emit + }) + }) - await withOperationLogging( - async () => - await deleteWorkspaceDomain({ workspaceId, domainId: args.input.id }), + return deleteWorkspaceDomain({ workspaceId, domainId: args.input.id }) + }, { logger, - operationName: 'deleteWorkspaceDomain', - operationDescription: 'Delete domain from workspace' + name: 'deleteWorkspaceDomain', + description: 'Delete domain from workspace', + dbs: await getAllRegisteredDbs() } ) @@ -1168,18 +1174,18 @@ export default FF_WORKSPACES_MODULE_ENABLED const logger = context.log.child({ workspaceId }) - return await asOperation( - async ({ db, emit }) => { + return await asMultiregionalOperation( + async ({ mainDb, allDbs, emit }) => { const workspace = await updateWorkspaceFactory({ validateSlug: validateSlugFactory({ - getWorkspaceBySlug: getWorkspaceBySlugFactory({ db }) + getWorkspaceBySlug: getWorkspaceBySlugFactory({ db: mainDb }) }), - getWorkspace: getWorkspaceWithDomainsFactory({ db }), + getWorkspace: getWorkspaceWithDomainsFactory({ db: mainDb }), getWorkspaceSsoProviderRecord: getWorkspaceSsoProviderFactory({ - db, + db: mainDb, decrypt: getDecryptor() }), - upsertWorkspace: upsertWorkspaceFactory({ db }), + upsertWorkspace: replicateFactory(allDbs, upsertWorkspaceFactory), emitWorkspaceEvent: emit })({ workspaceId, @@ -1197,7 +1203,7 @@ export default FF_WORKSPACES_MODULE_ENABLED name: 'updateWorkspaceEmbedOptions', description: 'Update workspace-level configuration for the embedded viewer', - transaction: true + dbs: await getAllRegisteredDbs() } ) }, diff --git a/packages/server/modules/workspaces/services/projects.ts b/packages/server/modules/workspaces/services/projects.ts index f26978da8..861343063 100644 --- a/packages/server/modules/workspaces/services/projects.ts +++ b/packages/server/modules/workspaces/services/projects.ts @@ -29,7 +29,6 @@ import type { import { ProjectNotFoundError } from '@/modules/core/errors/projects' import type { WorkspaceProjectCreateInput } from '@/modules/core/graph/generated/graphql' import { - getDb, getReplicationDbs, getValidDefaultProjectRegionKey } from '@/modules/multiregion/utils/dbSelector' @@ -38,11 +37,6 @@ import { storeProjectFactory, storeProjectRoleFactory } from '@/modules/core/repositories/projects' -import { mainDb } from '@/db/knex' -import { - getWorkspaceFactory, - upsertWorkspaceFactory -} from '@/modules/workspaces/repositories/workspaces' import type { GetWorkspaceRoleAndSeat, GetWorkspaceRolesAndSeats, @@ -318,20 +312,7 @@ export const createWorkspaceProjectFactory = }) const regionKey = workspaceDefaultRegion?.key ?? (await getValidDefaultProjectRegionKey()) - const projectDb = await getDb({ regionKey }) - const db = mainDb - const regionalWorkspace = await getWorkspaceFactory({ db: projectDb })({ - workspaceId: input.workspaceId - }) - - if (!regionalWorkspace) { - const workspace = await getWorkspaceFactory({ db })({ - workspaceId: input.workspaceId - }) - if (!workspace) throw new WorkspaceNotFoundError() - await upsertWorkspaceFactory({ db: projectDb })({ workspace }) - } const project = await asMultiregionalOperation( async ({ allDbs, mainDb, emit }) => { const createNewProject = createNewProjectFactory({ diff --git a/packages/server/modules/workspaces/services/regions.ts b/packages/server/modules/workspaces/services/regions.ts index d0b42a588..5e9097541 100644 --- a/packages/server/modules/workspaces/services/regions.ts +++ b/packages/server/modules/workspaces/services/regions.ts @@ -5,8 +5,7 @@ import type { GetAvailableRegions, GetDefaultRegion, GetWorkspace, - UpsertRegionAssignment, - UpsertWorkspace + UpsertRegionAssignment } from '@/modules/workspaces/domain/operations' import { WorkspaceRegionAssignmentError } from '@/modules/workspaces/errors/regions' @@ -31,7 +30,6 @@ export const assignWorkspaceRegionFactory = upsertRegionAssignment: UpsertRegionAssignment getDefaultRegion: GetDefaultRegion getWorkspace: GetWorkspace - insertRegionWorkspace: UpsertWorkspace }): AssignWorkspaceRegion => async (params) => { const { workspaceId, regionKey } = params @@ -65,7 +63,4 @@ export const assignWorkspaceRegionFactory = // Set up region await deps.upsertRegionAssignment({ workspaceId, regionKey }) - - // Copy workspace into region db - await deps.insertRegionWorkspace({ workspace }) } diff --git a/packages/server/modules/workspaces/tests/helpers/creation.ts b/packages/server/modules/workspaces/tests/helpers/creation.ts index d72d69001..b58c36848 100644 --- a/packages/server/modules/workspaces/tests/helpers/creation.ts +++ b/packages/server/modules/workspaces/tests/helpers/creation.ts @@ -90,7 +90,6 @@ import { getDefaultRegionFactory, upsertRegionAssignmentFactory } from '@/modules/workspaces/repositories/regions' -import { getDb } from '@/modules/multiregion/utils/dbSelector' import { WorkspaceSeatType } from '@/modules/gatekeeper/domain/billing' import { assignWorkspaceSeatFactory, @@ -135,6 +134,10 @@ import type { WorkspaceWithOptionalRole } from '@/modules/workspacesCore/domain/types' import { WorkspaceRole } from '@/modules/core/graph/generated/graphql' +import { asMultiregionalOperation, replicateFactory } from '@/modules/shared/command' +import { logger } from '@/observability/logging' +import type { UpdateWorkspace } from '@/modules/workspaces/domain/operations' +import { getAllRegisteredTestDbs } from '@/modules/multiregion/tests/helpers' const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags() @@ -191,41 +194,54 @@ export const createTestWorkspace = async ( } const upsertWorkspacePlan = upsertWorkspacePlanFactory({ db }) - const createWorkspace = createWorkspaceFactory({ - validateSlug: validateSlugFactory({ - getWorkspaceBySlug: getWorkspaceBySlugFactory({ db }) - }), - generateValidSlug: generateValidSlugFactory({ - getWorkspaceBySlug: getWorkspaceBySlugFactory({ db }) - }), - upsertWorkspace: upsertWorkspaceFactory({ db }), - emitWorkspaceEvent: (...args) => getEventBus().emit(...args), - addOrUpdateWorkspaceRole: addOrUpdateWorkspaceRoleFactory({ - getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }), - findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ - db - }), - getWorkspaceRoles: getWorkspaceRolesFactory({ db }), - upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }), - emitWorkspaceEvent: getEventBus().emit, - ensureValidWorkspaceRoleSeat: ensureValidWorkspaceRoleSeatFactory({ - createWorkspaceSeat: createWorkspaceSeatFactory({ db }), - getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db }), - getWorkspaceDefaultSeatType: getWorkspaceDefaultSeatTypeFactory({ - getWorkspace: getWorkspaceFactory({ db }) - }), - eventEmit: getEventBus().emit - }), - assignWorkspaceSeat: assignWorkspaceSeatFactory({ - createWorkspaceSeat: createWorkspaceSeatFactory({ db }), - getWorkspaceRoleForUser: getWorkspaceRoleForUserFactory({ - db - }), - eventEmit: getEventBus().emit, - getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db }) - }) - }) - }) + const createWorkspace: ReturnType = async (...args) => + asMultiregionalOperation( + async ({ allDbs, mainDb, emit }) => { + const createWorkspace = createWorkspaceFactory({ + validateSlug: validateSlugFactory({ + getWorkspaceBySlug: getWorkspaceBySlugFactory({ db: mainDb }) + }), + generateValidSlug: generateValidSlugFactory({ + getWorkspaceBySlug: getWorkspaceBySlugFactory({ db: mainDb }) + }), + upsertWorkspace: replicateFactory(allDbs, upsertWorkspaceFactory), + emitWorkspaceEvent: emit, + addOrUpdateWorkspaceRole: addOrUpdateWorkspaceRoleFactory({ + getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db: mainDb }), + findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ + db: mainDb + }), + getWorkspaceRoles: getWorkspaceRolesFactory({ db: mainDb }), + upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db: mainDb }), + emitWorkspaceEvent: emit, + ensureValidWorkspaceRoleSeat: ensureValidWorkspaceRoleSeatFactory({ + createWorkspaceSeat: createWorkspaceSeatFactory({ db: mainDb }), + getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db: mainDb }), + getWorkspaceDefaultSeatType: getWorkspaceDefaultSeatTypeFactory({ + getWorkspace: getWorkspaceFactory({ db: mainDb }) + }), + eventEmit: emit + }), + assignWorkspaceSeat: assignWorkspaceSeatFactory({ + createWorkspaceSeat: createWorkspaceSeatFactory({ db: mainDb }), + getWorkspaceRoleForUser: getWorkspaceRoleForUserFactory({ + db: mainDb + }), + eventEmit: emit, + getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db: mainDb }) + }) + }) + }) + + return createWorkspace(...args) + }, + { + logger, + name: 'create workspace spec', + dbs: await getAllRegisteredTestDbs() + } + ) + const upsertSubscription = upsertWorkspaceSubscriptionFactory({ db }) const newWorkspace = await createWorkspace({ @@ -305,7 +321,6 @@ export const createTestWorkspace = async ( } if (useRegion) { - const regionDb = await getDb({ regionKey }) const assignRegion = assignWorkspaceRegionFactory({ getAvailableRegions: getAvailableRegionsFactory({ getRegions: getRegionsFactory({ db }), @@ -315,8 +330,7 @@ export const createTestWorkspace = async ( }), upsertRegionAssignment: upsertRegionAssignmentFactory({ db }), getDefaultRegion: getDefaultRegionFactory({ db }), - getWorkspace: getWorkspaceFactory({ db }), - insertRegionWorkspace: upsertWorkspaceFactory({ db: regionDb }) + getWorkspace: getWorkspaceFactory({ db }) }) await assignRegion({ workspaceId: newWorkspace.id, @@ -335,15 +349,29 @@ export const createTestWorkspace = async ( }) } - const updateWorkspace = updateWorkspaceFactory({ - validateSlug: validateSlugFactory({ - getWorkspaceBySlug: getWorkspaceBySlugFactory({ db }) - }), - getWorkspace: getWorkspaceWithDomainsFactory({ db }), - getWorkspaceSsoProviderRecord: getWorkspaceSsoProviderRecordFactory({ db }), - upsertWorkspace: upsertWorkspaceFactory({ db }), - emitWorkspaceEvent: (...args) => getEventBus().emit(...args) - }) + const updateWorkspace: UpdateWorkspace = async (...args) => + asMultiregionalOperation( + ({ allDbs, mainDb, emit }) => { + const updateWorkspace = updateWorkspaceFactory({ + validateSlug: validateSlugFactory({ + getWorkspaceBySlug: getWorkspaceBySlugFactory({ db: mainDb }) + }), + getWorkspace: getWorkspaceWithDomainsFactory({ db: mainDb }), + getWorkspaceSsoProviderRecord: getWorkspaceSsoProviderRecordFactory({ + db: mainDb + }), + upsertWorkspace: replicateFactory(allDbs, upsertWorkspaceFactory), + emitWorkspaceEvent: emit + }) + + return updateWorkspace(...args) + }, + { + logger, + name: 'updateWorkspace spec', + dbs: await getAllRegisteredTestDbs() + } + ) if (workspace.discoverabilityEnabled || workspace.discoverabilityAutoJoinEnabled) { if (!domain) throw new Error('Domain is needed for discoverability') diff --git a/packages/server/modules/workspaces/tests/integration/repositories.spec.ts b/packages/server/modules/workspaces/tests/integration/repositories.spec.ts index 469f7369d..8f38f7e05 100644 --- a/packages/server/modules/workspaces/tests/integration/repositories.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/repositories.spec.ts @@ -50,11 +50,28 @@ import { omit } from 'lodash-es' import { createAndStoreTestWorkspaceFactory } from '@/test/speckle-helpers/workspaces' import { WorkspaceJoinRequests } from '@/modules/workspacesCore/helpers/db' import { insertInviteAndDeleteOldFactory } from '@/modules/serverinvites/repositories/serverInvites' +import type { + DeleteWorkspace, + UpsertWorkspace +} from '@/modules/workspaces/domain/operations' +import { asMultiregionalOperation, replicateFactory } from '@/modules/shared/command' +import { getAllRegisteredDbs } from '@/modules/multiregion/utils/dbSelector' +import { logger } from '@/observability/logging' + const getWorkspace = getWorkspaceFactory({ db }) const getWorkspaces = getWorkspacesFactory({ db }) const getWorkspaceBySlug = getWorkspaceBySlugFactory({ db }) const getWorkspaceCollaborators = getWorkspaceCollaboratorsFactory({ db }) -const deleteWorkspace = deleteWorkspaceFactory({ db }) +const deleteWorkspace: DeleteWorkspace = async (...args) => + asMultiregionalOperation( + ({ allDbs }) => replicateFactory(allDbs, deleteWorkspaceFactory)(...args), + { + logger, + name: 'delete workspace spec', + dbs: await getAllRegisteredDbs() + } + ) + const deleteWorkspaceRole = deleteWorkspaceRoleFactory({ db }) const getWorkspaceRoles = getWorkspaceRolesFactory({ db }) const getWorkspaceRoleForUser = getWorkspaceRoleForUserFactory({ db }) @@ -67,7 +84,16 @@ const getUserDiscoverableWorkspaces = getUserDiscoverableWorkspacesFactory({ db const getUserEligibleWorkspaces = getUserEligibleWorkspacesFactory({ db }) const upsertProjectRole = upsertProjectRoleFactory({ db }) const grantStreamPermissions = grantStreamPermissionsFactory({ db }) -const upsertWorkspace = upsertWorkspaceFactory({ db }) +const upsertWorkspace: UpsertWorkspace = async (...args) => + asMultiregionalOperation( + ({ allDbs }) => replicateFactory(allDbs, upsertWorkspaceFactory)(...args), + { + logger, + name: 'delete workspace spec', + dbs: await getAllRegisteredDbs() + } + ) + const insertInviteAndDeleteOld = insertInviteAndDeleteOldFactory({ db }) const createAndStoreTestWorkspace = createAndStoreTestWorkspaceFactory({ diff --git a/packages/server/modules/workspaces/tests/integration/workspacesCreationState.spec.ts b/packages/server/modules/workspaces/tests/integration/workspacesCreationState.spec.ts index 50aa5f697..312cad442 100644 --- a/packages/server/modules/workspaces/tests/integration/workspacesCreationState.spec.ts +++ b/packages/server/modules/workspaces/tests/integration/workspacesCreationState.spec.ts @@ -49,13 +49,7 @@ describe('WorkspaceCreationState services', () => { const deleteWorkspacesNonComplete = deleteWorkspacesNonCompleteFactory({ getWorkspacesNonComplete: getWorkspacesNonCompleteFactory({ db: mainDb }), deleteWorkspace: deleteWorkspaceFactory({ - deleteWorkspace: async (...input) => { - const [res] = await Promise.all( - allDbs.map((db) => repoDeleteWorkspaceFactory({ db })(...input)) - ) - - return res - }, + deleteWorkspace: replicateFactory(allDbs, repoDeleteWorkspaceFactory), deleteProjectAndCommits: deleteProjectAndCommitsFactory({ deleteProject: replicateFactory(allDbs, deleteProjectFactory), deleteProjectCommits: replicateFactory( diff --git a/packages/server/modules/workspacesCore/migrations/20250905074349_drop_workspace_defaults.ts b/packages/server/modules/workspacesCore/migrations/20250905074349_drop_workspace_defaults.ts new file mode 100644 index 000000000..52a7d0841 --- /dev/null +++ b/packages/server/modules/workspacesCore/migrations/20250905074349_drop_workspace_defaults.ts @@ -0,0 +1,20 @@ +import type { Knex } from 'knex' + +const tableName = 'workspaces' +const colSlug = 'slug' + +export async function up(knex: Knex): Promise { + await knex.schema.raw(` + ALTER TABLE "${tableName}" + ALTER COLUMN "${colSlug}" DROP DEFAULT; + `) +} + +export async function down(knex: Knex): Promise { + await knex.schema.alterTable(tableName, (table) => { + table + .string(colSlug) + .defaultTo(knex.raw('substring(md5(random()::text), 0, 15)')) + .alter() + }) +} diff --git a/packages/server/test/authHelper.ts b/packages/server/test/authHelper.ts index e6d23e0f8..8e59b02c5 100644 --- a/packages/server/test/authHelper.ts +++ b/packages/server/test/authHelper.ts @@ -29,7 +29,7 @@ import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repos import { renderEmail } from '@/modules/emails/services/emailRendering' import { sendEmail } from '@/modules/emails/services/sending' import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request' -import { getTestRegionClients } from '@/modules/multiregion/tests/helpers' +import { getAllRegisteredTestDbs } from '@/modules/multiregion/tests/helpers' import { deleteServerOnlyInvitesFactory, updateAllInviteTargetsFactory @@ -158,7 +158,7 @@ export async function createTestUser(userObj?: Partial) { }) }, { - dbs: await getTestRegionClients(), + dbs: await getAllRegisteredTestDbs(), logger, name: 'createUser' }