Files
speckle-server/packages/server/modules/workspaces/index.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

149 lines
5.9 KiB
TypeScript

import type cron from 'node-cron'
import type { Logger } from '@/observability/logging'
import { moduleLogger } from '@/observability/logging'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { registerOrUpdateScopeFactory } from '@/modules/shared/repositories/scopes'
import db from '@/db/knex'
import type { Optional, SpeckleModule } from '@/modules/shared/helpers/typeHelper'
import { workspaceRoles } from '@/modules/workspaces/roles'
import { workspaceScopes } from '@/modules/workspaces/scopes'
import { registerOrUpdateRole } from '@/modules/shared/repositories/roles'
import { initializeEventListenersFactory } from '@/modules/workspaces/events/eventListener'
import { validateModuleLicense } from '@/modules/gatekeeper/services/validateLicense'
import { getSsoRouter } from '@/modules/workspaces/rest/sso'
import { InvalidLicenseError } from '@/modules/gatekeeper/errors/license'
import type { ScheduleExecution } from '@/modules/core/domain/scheduledTasks/operations'
import { scheduleExecutionFactory } from '@/modules/core/services/taskScheduler'
import {
acquireTaskLockFactory,
releaseTaskLockFactory
} from '@/modules/core/repositories/scheduledTasks'
import { getWorkspacesNonCompleteFactory } from '@/modules/workspaces/repositories/workspaces'
import { deleteWorkspacesNonCompleteFactory } from '@/modules/workspaces/services/workspaceCreationState'
import { getExplicitProjects } from '@/modules/core/repositories/streams'
import { deleteSsoProviderFactory } from '@/modules/workspaces/repositories/sso'
import { deleteAllResourceInvitesFactory } from '@/modules/serverinvites/repositories/serverInvites'
import { deleteWorkspaceFactory as repoDeleteWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces'
import { deleteWorkspaceFactory } from '@/modules/workspaces/services/management'
import { scheduleUpdateAllWorkspacesTracking } from '@/modules/workspaces/services/tracking'
import { getClient } from '@/modules/shared/utils/mixpanel'
import {
deleteProjectAndCommitsFactory,
queryAllProjectsFactory
} from '@/modules/core/services/projects'
import { deleteProjectFactory } from '@/modules/core/repositories/projects'
import { deleteProjectCommitsFactory } from '@/modules/core/repositories/commits'
import { asMultiregionalOperation, replicateFactory } from '@/modules/shared/command'
import { getAllRegisteredDbs } from '@/modules/multiregion/utils/dbSelector'
const {
FF_WORKSPACES_MODULE_ENABLED,
FF_WORKSPACES_SSO_ENABLED,
FF_BILLING_INTEGRATION_ENABLED
} = getFeatureFlags()
let quitListeners: Optional<() => void> = undefined
let scheduledTasks: cron.ScheduledTask[] = []
const initScopes = async () => {
const registerFunc = registerOrUpdateScopeFactory({ db })
await Promise.all(workspaceScopes.map((scope) => registerFunc({ scope })))
}
const initRoles = async () => {
const registerFunc = registerOrUpdateRole({ db })
await Promise.all(workspaceRoles.map((role) => registerFunc({ role })))
}
const scheduleDeleteWorkspacesNonComplete = ({
scheduleExecution
}: {
scheduleExecution: ScheduleExecution
}) => {
const deleteWorkspacesNonComplete = async ({ logger }: { logger: Logger }) =>
asMultiregionalOperation(
({ allDbs, mainDb, emit }) => {
const deleteWorkspacesNonComplete = deleteWorkspacesNonCompleteFactory({
getWorkspacesNonComplete: getWorkspacesNonCompleteFactory({ db: mainDb }),
deleteWorkspace: deleteWorkspaceFactory({
deleteWorkspace: replicateFactory(allDbs, repoDeleteWorkspaceFactory),
deleteProjectAndCommits: deleteProjectAndCommitsFactory({
deleteProject: replicateFactory(allDbs, deleteProjectFactory),
deleteProjectCommits: replicateFactory(
allDbs,
deleteProjectCommitsFactory
)
}),
deleteAllResourceInvites: deleteAllResourceInvitesFactory({
db: mainDb
}),
queryAllProjects: queryAllProjectsFactory({
getExplicitProjects: getExplicitProjects({ db: mainDb })
}),
deleteSsoProvider: deleteSsoProviderFactory({ db: mainDb }),
emitWorkspaceEvent: emit
})
})
return deleteWorkspacesNonComplete({ logger })
},
{
logger,
name: 'deleteWorkspacesNonComplete',
dbs: await getAllRegisteredDbs()
}
)
const every30Mins = '*/30 * * * *'
return scheduleExecution(
every30Mins,
'DeleteWorkspaceNonComplete',
async (_scheduledTime, { logger }) => {
await Promise.all([deleteWorkspacesNonComplete({ logger })])
}
)
}
const workspacesModule: SpeckleModule = {
async init({ app, isInitial }) {
if (!FF_WORKSPACES_MODULE_ENABLED) return
const isWorkspaceLicenseValid = await validateModuleLicense({
requiredModules: ['workspaces']
})
if (!isWorkspaceLicenseValid)
throw new InvalidLicenseError(
'The workspaces module needs a valid license to run, contact Speckle to get one.'
)
moduleLogger.info('⚒️ Init workspaces module')
if (FF_WORKSPACES_SSO_ENABLED) app.use(getSsoRouter())
if (isInitial) {
const mixpanel = getClient()
const scheduleExecution = scheduleExecutionFactory({
acquireTaskLock: acquireTaskLockFactory({ db }),
releaseTaskLock: releaseTaskLockFactory({ db })
})
scheduledTasks = [scheduleDeleteWorkspacesNonComplete({ scheduleExecution })]
if (FF_BILLING_INTEGRATION_ENABLED && mixpanel)
scheduledTasks.push(
scheduleUpdateAllWorkspacesTracking({ scheduleExecution, mixpanel })
)
quitListeners = initializeEventListenersFactory({ db })()
}
await Promise.all([initScopes(), initRoles()])
},
shutdown() {
if (!FF_WORKSPACES_MODULE_ENABLED) return
quitListeners?.()
scheduledTasks.forEach((task) => {
task.stop()
})
}
}
export default workspacesModule