Merge branch 'develop' into staging-new

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2025-02-11 18:04:05 +07:00
10 changed files with 331 additions and 300 deletions
+188 -179
View File
@@ -1,5 +1,12 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { type AccountDB, type Workspace, getAccount, getWorkspaceById } from '@hcengineering/account'
import {
type AccountDB,
type MongoAccountDB,
type Workspace,
getAccount,
getWorkspaceById,
getWorkspaces
} from '@hcengineering/account'
import {
systemAccountUuid,
type BackupClient,
@@ -174,191 +181,193 @@ export async function moveAccountDbFromMongoToPG (
mongoDb: AccountDB,
pgDb: AccountDB
): Promise<void> {
// TODO: FIXME
throw new Error('Not implemented')
// [accountId, workspaceId]
// const workspaceAssignments: [string, WorkspaceUuid][] = []
// const accounts = await listAccounts(mongoDb)
// const workspaces = await listWorkspacesPure(mongoDb)
// const invites = await listInvites(mongoDb)
const mdb = mongoDb as MongoAccountDB
// for (const mongoAccount of accounts) {
// const pgAccount = {
// ...mongoAccount,
// _id: mongoAccount._id.toString()
// }
// delete (pgAccount as any).workspaces
// if (pgAccount.createdOn == null) {
// pgAccount.createdOn = Date.now()
// }
// if (pgAccount.first == null) {
// pgAccount.first = 'NotSet'
// }
// if (pgAccount.last == null) {
// pgAccount.last = 'NotSet'
// }
// for (const workspaceString of new Set(mongoAccount.workspaces.map((w) => w.toString()))) {
// workspaceAssignments.push([pgAccount._id, workspaceString])
// }
// const exists = await getAccount(pgDb, pgAccount.email)
// if (exists === null) {
// await pgDb.account.insertOne(pgAccount)
// ctx.info('Moved account', { email: pgAccount.email })
// }
// }
// for (const mongoWorkspace of workspaces) {
// const pgWorkspace = {
// ...mongoWorkspace,
// _id: mongoWorkspace._id.toString()
// }
// if (pgWorkspace.createdOn == null) {
// pgWorkspace.createdOn = Date.now()
// }
// // delete deprecated fields
// delete (pgWorkspace as any).createProgress
// delete (pgWorkspace as any).creating
// delete (pgWorkspace as any).productId
// delete (pgWorkspace as any).organisation
// // assigned separately
// delete (pgWorkspace as any).accounts
// const exists = await getWorkspaceById(pgDb, pgWorkspace.workspace)
// if (exists === null) {
// await pgDb.workspace.insertOne(pgWorkspace)
// ctx.info('Moved workspace', {
// workspace: pgWorkspace.workspace,
// workspaceName: pgWorkspace.workspaceName,
// workspaceUrl: pgWorkspace.workspaceUrl
// })
// }
// }
// for (const mongoInvite of invites) {
// const pgInvite = {
// ...mongoInvite,
// _id: mongoInvite._id.toString()
// }
// const exists = await pgDb.invite.findOne({ _id: pgInvite._id })
// if (exists === null) {
// await pgDb.invite.insertOne(pgInvite)
// }
// }
// const pgAssignments = (await listAccounts(pgDb)).reduce<Record<ObjectId, ObjectId[]>>((assignments, acc) => {
// assignments[acc._id] = acc.workspaces
// return assignments
// }, {})
// const assignmentsToInsert = workspaceAssignments.filter(
// ([accountId, workspaceId]) =>
// pgAssignments[accountId] === undefined || !pgAssignments[accountId].includes(workspaceId)
// )
// for (const [accountId, workspaceId] of assignmentsToInsert) {
// await pgDb.assignWorkspace(accountId, workspaceId)
// }
// ctx.info('Assignments made', { count: assignmentsToInsert.length })
}
export async function generateUuidMissingWorkspaces (
ctx: MeasureMetricsContext,
db: AccountDB,
dryRun = false
): Promise<void> {
// TODO: FIXME
throw new Error('Not implemented')
// const workspaces = await listWorkspacesPure(db)
// let updated = 0
// for (const ws of workspaces) {
// if (ws.uuid !== undefined) continue
// const uuid = new UUID().toJSON()
// if (!dryRun) {
// await db.workspace.updateOne({ _id: ws._id }, { uuid })
// }
// updated++
// }
// ctx.info('Assigned uuids to workspaces', { updated, total: workspaces.length })
}
export async function updateDataWorkspaceIdToUuid (
ctx: MeasureMetricsContext,
accountDb: AccountDB,
dbUrl: string | undefined,
dryRun = false
): Promise<void> {
if (dbUrl === undefined) {
throw new Error('dbUrl is required')
}
const pg = getDBClient(sharedPipelineContextVars, dbUrl)
ctx.info('Starting migration of persons...')
const personsCursor = mdb.person.findCursor({})
try {
const pgClient = await pg.getClient()
let personsCount = 0
while (await personsCursor.hasNext()) {
const person = await personsCursor.next()
if (person == null) break
// Generate uuids for all workspaces or verify they exist
await generateUuidMissingWorkspaces(ctx, accountDb, dryRun)
const exists = await pgDb.person.findOne({ uuid: person.uuid })
if (exists == null) {
if (person.firstName == null) {
person.firstName = 'n/a'
}
const workspaces: Workspace[] = [] // TODO: FIXME await listWorkspacesPure(accountDb)
// const noUuidWss = workspaces.filter((ws) => ws.uuid === undefined)
// if (noUuidWss.length > 0) {
// ctx.error('Workspace uuid is required but not defined', { workspaces: noUuidWss.map((it) => it.workspace) })
// throw new Error('workspace uuid is required but not defined')
// }
if (person.lastName == null) {
person.lastName = 'n/a'
}
const res = await pgClient`select t.table_name from information_schema.columns as c
join information_schema.tables as t on
c.table_catalog = t.table_catalog and
c.table_schema = t.table_schema and
c.table_name = t.table_name
where t.table_type = 'BASE TABLE' and t.table_schema = 'public' and c.column_name = 'workspaceId' and c.data_type <> 'uuid'`
const tables: string[] = res.map((r) => r.table_name)
ctx.info('Tables to be updated: ', { tables })
for (const table of tables) {
ctx.info('Altering table workspaceId type to uuid', { table })
if (!dryRun) {
await retryTxn(pgClient, async (client) => {
await client`ALTER TABLE ${client(table)} RENAME COLUMN "workspaceId" TO "workspaceIdOld"`
await client`ALTER TABLE ${client(table)} ADD COLUMN "workspaceId" UUID`
})
await retryTxn(pgClient, async (client) => {
for (const ws of workspaces) {
if (ws.dataId === undefined) continue
const uuid = ws.uuid
await client`UPDATE ${client(table)} SET "workspaceId" = ${uuid} WHERE "workspaceIdOld" = ${ws.dataId} OR "workspaceIdOld" = ${uuid}`
}
})
await retryTxn(pgClient, async (client) => {
await client`ALTER TABLE ${client(table)} ALTER COLUMN "workspaceId" SET NOT NULL`
})
await retryTxn(pgClient, async (client) => {
await client`ALTER TABLE ${client(table)} DROP CONSTRAINT ${client(`${table}_pkey`)}`
await client`ALTER TABLE ${client(table)} ADD CONSTRAINT ${client(`${table}_pkey`)} PRIMARY KEY ("workspaceId", _id)`
})
await pgDb.person.insertOne(person)
personsCount++
if (personsCount % 100 === 0) {
ctx.info(`Migrated ${personsCount} persons...`)
}
}
}
ctx.info('Done updating workspaceId to uuid')
ctx.info(`Migrated ${personsCount} persons`)
} finally {
pg.close()
await personsCursor.close()
}
ctx.info('Starting migration of accounts...')
const accountsCursor = mdb.account.findCursor({})
try {
let accountsCount = 0
while (await accountsCursor.hasNext()) {
const account = await accountsCursor.next()
if (account == null) break
const exists = await pgDb.account.findOne({ uuid: account.uuid })
if (exists == null) {
const { hash, salt } = account
delete account.hash
delete account.salt
await pgDb.account.insertOne(account)
if (hash != null && salt != null) {
await pgDb.setPassword(account.uuid, hash, salt)
}
accountsCount++
if (accountsCount % 100 === 0) {
ctx.info(`Migrated ${accountsCount} accounts...`)
}
}
}
ctx.info(`Migrated ${accountsCount} accounts`)
} finally {
await accountsCursor.close()
}
ctx.info('Starting migration of social IDs...')
const socialIdsCursor = mdb.socialId.findCursor({})
try {
let socialIdsCount = 0
while (await socialIdsCursor.hasNext()) {
const socialId = await socialIdsCursor.next()
if (socialId == null) break
const exists = await pgDb.socialId.findOne({ key: socialId.key })
if (exists == null) {
delete (socialId as any).key
await pgDb.socialId.insertOne(socialId)
socialIdsCount++
if (socialIdsCount % 100 === 0) {
ctx.info(`Migrated ${socialIdsCount} social IDs...`)
}
}
}
ctx.info(`Migrated ${socialIdsCount} social IDs`)
} finally {
await socialIdsCursor.close()
}
ctx.info('Starting migration of account events...')
const accountEventsCursor = mdb.accountEvent.findCursor({})
try {
let eventsCount = 0
while (await accountEventsCursor.hasNext()) {
const accountEvent = await accountEventsCursor.next()
if (accountEvent == null) break
const exists = await pgDb.accountEvent.findOne({
accountUuid: accountEvent.accountUuid,
eventType: accountEvent.eventType,
time: accountEvent.time
})
if (exists == null) {
await pgDb.accountEvent.insertOne(accountEvent)
eventsCount++
if (eventsCount % 100 === 0) {
ctx.info(`Migrated ${eventsCount} account events...`)
}
}
}
ctx.info(`Migrated ${eventsCount} account events`)
} finally {
await accountEventsCursor.close()
}
ctx.info('Starting migration of workspaces...')
const workspacesCursor = mdb.workspace.findCursor({})
try {
let workspacesCount = 0
let membersCount = 0
while (await workspacesCursor.hasNext()) {
const workspace = await workspacesCursor.next()
if (workspace == null) break
const exists = await pgDb.workspace.findOne({ uuid: workspace.uuid })
if (exists != null) continue
const status = workspace.status
if (status == null) continue
delete (workspace as any).status
if (workspace.createdBy === 'N/A') {
delete workspace.createdBy
}
if (workspace.billingAccount === 'N/A') {
delete workspace.billingAccount
}
if (workspace.createdOn == null) {
delete workspace.createdOn
}
await pgDb.createWorkspace(workspace, status)
workspacesCount++
const members = await mdb.getWorkspaceMembers(workspace.uuid)
for (const member of members) {
const alreadyAssigned = await pgDb.getWorkspaceRole(member.person, workspace.uuid)
if (alreadyAssigned != null) continue
await pgDb.assignWorkspace(member.person, workspace.uuid, member.role)
membersCount++
}
if (workspacesCount % 100 === 0) {
ctx.info(`Migrated ${workspacesCount} invites...`)
}
}
ctx.info(`Migrated ${workspacesCount} workspaces with ${membersCount} member assignments`)
} finally {
await workspacesCursor.close()
}
ctx.info('Starting migration of invites...')
const invitesCursor = mdb.invite.findCursor({})
try {
let invitesCount = 0
while (await invitesCursor.hasNext()) {
const invite = await invitesCursor.next()
if (invite == null) break
if (invite.migratedFrom == null) {
invite.migratedFrom = invite.id
}
delete (invite as any).id
const exists = await pgDb.invite.findOne({ migratedFrom: invite.migratedFrom })
if (exists == null) {
await pgDb.invite.insertOne(invite)
invitesCount++
if (invitesCount % 100 === 0) {
ctx.info(`Migrated ${invitesCount} invites...`)
}
}
}
ctx.info(`Migrated ${invitesCount} invites`)
} finally {
await invitesCursor.close()
}
ctx.info('Account database migration completed')
}
+14 -36
View File
@@ -68,12 +68,13 @@ import {
shutdownPostgres
} from '@hcengineering/postgres'
import type { StorageAdapter } from '@hcengineering/server-core'
import { getAccountDBUrl } from './__start'
import { getAccountDBUrl, getMongoDBUrl } from './__start'
// import { fillGithubUsers, fixAccountEmails, renameAccount } from './account'
import { changeConfiguration } from './configuration'
import { reindexWorkspace } from './fulltext'
import { getToolToken, getWorkspace, getWorkspaceTransactorEndpoint } from './utils'
import { moveAccountDbFromMongoToPG } from './db'
const colorConstants = {
colorRed: '\u001b[31m',
@@ -2094,20 +2095,20 @@ export function devTool (
// }
// )
// program.command('move-account-db-to-pg').action(async () => {
// const { dbUrl } = prepareTools()
// const mongodbUri = getMongoDBUrl()
program.command('move-account-db-to-pg').action(async () => {
const { dbUrl } = prepareTools()
const mongodbUri = getMongoDBUrl()
// if (mongodbUri === dbUrl) {
// throw new Error('MONGO_URL and DB_URL are the same')
// }
if (mongodbUri === dbUrl) {
throw new Error('MONGO_URL and DB_URL are the same')
}
// await withAccountDatabase(async (pgDb) => {
// await withAccountDatabase(async (mongoDb) => {
// await moveAccountDbFromMongoToPG(toolCtx, mongoDb, pgDb)
// }, mongodbUri)
// }, dbUrl)
// })
await withAccountDatabase(async (pgDb) => {
await withAccountDatabase(async (mongoDb) => {
await moveAccountDbFromMongoToPG(toolCtx, mongoDb, pgDb)
}, mongodbUri)
}, dbUrl)
})
// program
// .command('perfomance')
@@ -2166,29 +2167,6 @@ export function devTool (
// })
// })
// program
// .command('generate-uuid-workspaces')
// .description('generate uuids for all workspaces which are missing it')
// .option('-d, --dryrun', 'Dry run', false)
// .action(async (cmd: { dryrun: boolean }) => {
// await withAccountDatabase(async (db) => {
// console.log('generate uuids for all workspaces which are missing it')
// await generateUuidMissingWorkspaces(toolCtx, db, cmd.dryrun)
// })
// })
// program
// .command('update-data-wsid-to-uuid')
// .description('updates workspaceId in pg/cr to uuid')
// .option('-d, --dryrun', 'Dry run', false)
// .action(async (cmd: { dryrun: boolean }) => {
// await withAccountDatabase(async (db) => {
// console.log('updates workspaceId in pg/cr to uuid')
// const { dbUrl } = prepareTools()
// await updateDataWorkspaceIdToUuid(toolCtx, db, dbUrl, cmd.dryrun)
// })
// })
// program
// .command('add-controlled-doc-rank-mongo')
// .description('add rank to controlled documents')
+3 -3
View File
@@ -794,8 +794,8 @@ export interface WorkspaceInfo {
region?: string
branding?: string
createdOn: number
createdBy: PersonUuid
billingAccount: PersonUuid
createdBy?: PersonUuid // Should always be set for NEW workspaces
billingAccount?: PersonUuid // Should always be set for NEW workspaces
}
export interface BackupStatus {
@@ -820,7 +820,7 @@ export interface WorkspaceInfoWithStatus extends WorkspaceInfo {
}
export interface WorkspaceMemberInfo {
person: string
person: PersonUuid
role: AccountRole
}
@@ -45,45 +45,53 @@ export async function migrateFromOldAccounts (oldAccsUrl: string, accountDB: Acc
console.log('Migrating accounts database from old accounts')
let accountsProcessed = 0
const accountsCursor = oldAccountDb.account.findCursor({})
while (await accountsCursor.hasNext()) {
const account = await accountsCursor.next()
if (account == null) {
break
}
try {
while (await accountsCursor.hasNext()) {
const account = await accountsCursor.next()
if (account == null) {
break
}
const accountUuid = await migrateAccount(account, accountDB)
if (accountUuid == null) {
console.log('Account not migrated', account)
continue
}
accountsIdToUuid[account._id.toString()] = accountUuid
accountsEmailToUuid[account.email] = accountUuid
const accountUuid = await migrateAccount(account, accountDB)
if (accountUuid == null) {
console.log('Account not migrated', account)
continue
}
accountsIdToUuid[account._id.toString()] = accountUuid
accountsEmailToUuid[account.email] = accountUuid
accountsProcessed++
if (accountsProcessed % 100 === 0) {
console.log('Processed accounts:', accountsProcessed)
accountsProcessed++
if (accountsProcessed % 100 === 0) {
console.log('Processed accounts:', accountsProcessed)
}
}
} finally {
await accountsCursor.close()
}
console.log('Total accounts processed:', accountsProcessed)
let processedWorkspaces = 0
const workspacesCursor = oldAccountDb.workspace.findCursor({})
while (await workspacesCursor.hasNext()) {
const workspace = await workspacesCursor.next()
if (workspace == null) {
break
}
try {
while (await workspacesCursor.hasNext()) {
const workspace = await workspacesCursor.next()
if (workspace == null) {
break
}
const workspaceUuid = await migrateWorkspace(workspace, accountDB, accountsIdToUuid, accountsEmailToUuid)
const workspaceUuid = await migrateWorkspace(workspace, accountDB, accountsIdToUuid, accountsEmailToUuid)
if (workspaceUuid !== undefined) {
workspacesIdToUuid[workspace.workspace] = workspaceUuid
}
processedWorkspaces++
if (processedWorkspaces % 100 === 0) {
console.log('Processed workspaces:', processedWorkspaces)
if (workspaceUuid !== undefined) {
workspacesIdToUuid[workspace.workspace] = workspaceUuid
}
processedWorkspaces++
if (processedWorkspaces % 100 === 0) {
console.log('Processed workspaces:', processedWorkspaces)
}
}
} finally {
await workspacesCursor.close()
}
console.log('Total workspaces processed:', processedWorkspaces)
@@ -91,32 +99,37 @@ export async function migrateFromOldAccounts (oldAccsUrl: string, accountDB: Acc
let invitesProcessed = 0
const invitesCursor = oldAccountDb.invite.findCursor({})
while (await invitesCursor.hasNext()) {
const invite = await invitesCursor.next()
if (invite == null) {
break
}
try {
while (await invitesCursor.hasNext()) {
const invite = await invitesCursor.next()
if (invite == null) {
break
}
const workspaceUuid = workspacesIdToUuid[invite.workspace.name]
if (workspaceUuid === undefined) {
console.log('No workspace with id', invite.workspace.name, 'found for invite', invite._id)
continue
}
const workspaceUuid = workspacesIdToUuid[invite.workspace.name]
if (workspaceUuid === undefined) {
console.log('No workspace with id', invite.workspace.name, 'found for invite', invite._id)
continue
}
await accountDB.invite.insertOne({
migratedFrom: invite._id.toString(),
workspaceUuid,
expiresOn: invite.exp,
emailPattern: invite.emailMask,
remainingUses: invite.limit,
role: invite.role ?? AccountRole.User
})
await accountDB.invite.insertOne({
migratedFrom: invite._id.toString(),
workspaceUuid,
expiresOn: invite.exp,
emailPattern: invite.emailMask,
remainingUses: invite.limit,
role: invite.role ?? AccountRole.User
})
invitesProcessed++
if (invitesProcessed % 100 === 0) {
console.log('Processed invites:', invitesProcessed)
invitesProcessed++
if (invitesProcessed % 100 === 0) {
console.log('Processed invites:', invitesProcessed)
}
}
} finally {
await invitesCursor.close()
}
console.log('Total invites processed:', invitesProcessed)
await oldAccountDb.migration.insertOne({ key: migrationKey, completed: true })
console.log('Migration of accounts database from old accounts COMPLETED')
+18 -2
View File
@@ -13,7 +13,15 @@
// limitations under the License.
//
import { UUID } from 'mongodb'
import type { Collection, CreateIndexesOptions, Db, Filter, OptionalUnlessRequiredId, Sort as RawSort } from 'mongodb'
import type {
Collection,
CreateIndexesOptions,
Db,
Filter,
FindCursor,
OptionalUnlessRequiredId,
Sort as RawSort
} from 'mongodb'
import {
type Person,
type WorkspaceMemberInfo,
@@ -111,6 +119,10 @@ implements DbCollection<T> {
}
async find (query: Query<T>, sort?: Sort<T>, limit?: number): Promise<T[]> {
return await this.findCursor(query, sort, limit).toArray()
}
findCursor (query: Query<T>, sort?: Sort<T>, limit?: number): FindCursor<T> {
const cursor = this.collection.find<T>(query as Filter<T>)
if (sort !== undefined) {
@@ -121,7 +133,11 @@ implements DbCollection<T> {
cursor.limit(limit)
}
return await cursor.toArray()
return cursor.map((doc) => {
delete doc._id
return doc
})
}
async findOne (query: Query<T>): Promise<T | null> {
+8 -6
View File
@@ -612,7 +612,7 @@ export class PostgresAccountDB implements AccountDB {
async setPassword (accountUuid: PersonUuid, hash: Buffer, salt: Buffer): Promise<void> {
await this
.client`UPSERT INTO ${this.client(this.account.getPasswordsTableName())} (account_uuid, hash, salt) VALUES (${accountUuid}, ${hash as unknown as Uint8Array}, ${salt as unknown as Uint8Array})`
.client`UPSERT INTO ${this.client(this.account.getPasswordsTableName())} (account_uuid, hash, salt) VALUES (${accountUuid}, ${hash.buffer as any}::bytea, ${salt.buffer as any}::bytea)`
}
async resetPassword (accountUuid: PersonUuid): Promise<void> {
@@ -639,9 +639,9 @@ export class PostgresAccountDB implements AccountDB {
$$ LANGUAGE SQL;
/* ======= T Y P E S ======= */
CREATE TYPE global_account.social_id_type AS ENUM ('email', 'github', 'google', 'phone', 'oidc', 'huly', 'telegram');
CREATE TYPE global_account.location AS ENUM ('kv', 'weur', 'eeur', 'wnam', 'enam', 'apac');
CREATE TYPE global_account.workspace_role AS ENUM ('OWNER', 'MAINTAINER', 'USER', 'GUEST', 'DOCGUEST');
CREATE TYPE IF NOT EXISTS global_account.social_id_type AS ENUM ('email', 'github', 'google', 'phone', 'oidc', 'huly', 'telegram');
CREATE TYPE IF NOT EXISTS global_account.location AS ENUM ('kv', 'weur', 'eeur', 'wnam', 'enam', 'apac');
CREATE TYPE IF NOT EXISTS global_account.workspace_role AS ENUM ('OWNER', 'MAINTAINER', 'USER', 'GUEST', 'DOCGUEST');
/* ======= P E R S O N ======= */
CREATE TABLE IF NOT EXISTS global_account.person (
@@ -702,9 +702,9 @@ export class PostgresAccountDB implements AccountDB {
branding STRING,
location global_account.location,
region STRING,
created_by UUID NOT NULL, -- account uuid
created_by UUID, -- account uuid
created_on BIGINT NOT NULL DEFAULT current_epoch_ms(),
billing_account UUID NOT NULL,
billing_account UUID,
CONSTRAINT workspace_pk PRIMARY KEY (uuid),
CONSTRAINT workspace_url_unique UNIQUE (url),
CONSTRAINT workspace_created_by_fk FOREIGN KEY (created_by) REFERENCES global_account.account(uuid),
@@ -759,8 +759,10 @@ export class PostgresAccountDB implements AccountDB {
email_pattern STRING,
remaining_uses INT2,
role global_account.workspace_role NOT NULL DEFAULT 'USER',
migrated_from STRING,
CONSTRAINT invite_pk PRIMARY KEY (id),
INDEX workspace_invite_idx (workspace_uuid),
INDEX migrated_from_idx (migrated_from),
CONSTRAINT invite_workspace_fk FOREIGN KEY (workspace_uuid) REFERENCES global_account.workspace(uuid)
);
`
+1
View File
@@ -19,4 +19,5 @@ export * from './operations'
export * from './plugin'
export * from './utils'
export * from './types'
export type { MongoAccountDB } from './collections/mongo'
export default accountPlugin
+3 -24
View File
@@ -85,7 +85,8 @@ import {
selectWorkspace,
doJoinByInvite,
getWorkspacesInfoWithStatusByIds,
getSocialIdByKey
getSocialIdByKey,
getWorkspaces
} from './utils'
// Move to config?
@@ -842,29 +843,7 @@ export async function listWorkspaces (
throw new PlatformError(new Status(Severity.ERROR, platform.status.Forbidden, {}))
}
const statuses = await db.workspaceStatus.find({})
const statusesMap = statuses.reduce<Record<string, WorkspaceStatus>>((sm, s) => {
sm[s.workspaceUuid] = s
return sm
}, {})
const workspaces = (await db.workspace.find(region != null ? { region } : {})).filter((it) => {
const status = statusesMap[it.uuid]
if (status.isDisabled) {
return false
}
if (mode != null) {
return status.mode === mode
}
return true
})
return workspaces.map((it) => ({
...it,
status: statusesMap[it.uuid]
}))
return await getWorkspaces(db, false, region, mode)
}
export async function performWorkspaceOperation (
+2 -2
View File
@@ -97,8 +97,8 @@ export interface Workspace {
branding?: string
location?: Location
region?: string
createdBy: PersonUuid
billingAccount: PersonUuid
createdBy?: PersonUuid
billingAccount?: PersonUuid
createdOn?: Timestamp
}
+33
View File
@@ -1088,3 +1088,36 @@ export function flattenStatus (ws: WorkspaceInfoWithStatus): WorkspaceInfoWithSt
export async function cleanExpiredOtp (db: AccountDB): Promise<void> {
await db.otp.deleteMany({ expiresOn: { $lte: Date.now() } })
}
export async function getWorkspaces (
db: AccountDB,
isDisabled?: boolean | null,
region?: string | null,
mode?: WorkspaceMode | null
): Promise<WorkspaceInfoWithStatus[]> {
const statuses = await db.workspaceStatus.find({})
const statusesMap = statuses.reduce<Record<string, WorkspaceStatus>>((sm, s) => {
sm[s.workspaceUuid] = s
return sm
}, {})
const workspaces = (await db.workspace.find(region != null ? { region } : {})).filter((it) => {
const status = statusesMap[it.uuid]
if (isDisabled === true) {
return status.isDisabled
} else if (isDisabled === false) {
return !status.isDisabled
}
if (mode != null) {
return status.mode === mode
}
return true
})
return workspaces.map((it) => ({
...it,
status: statusesMap[it.uuid]
}))
}