import { AccountClient } from '@hcengineering/account-client' import { Analytics } from '@hcengineering/analytics' import core, { Class, Client, DOMAIN_MIGRATION, DOMAIN_TX, Data, Doc, DocumentQuery, Domain, FindOptions, Hierarchy, MigrationState, ModelDb, ObjQueryType, Rank, Ref, SortingOrder, Space, TxOperations, UnsetOptions, WorkspaceIds, generateId } from '@hcengineering/core' import { makeRank } from '@hcengineering/rank' import { StorageAdapter } from '@hcengineering/storage' import { ModelLogger } from './utils' /** * @public */ export type MigrateUpdate = Partial & UnsetOptions & Record /** * @public */ export interface MigrationResult { matched: number updated: number } /** * @public */ export type MigrationDocumentQuery = { [P in keyof T]?: ObjQueryType | null } & { $search?: string // support nested queries e.g. 'user.friends.name' // this will mark all unrecognized properties as any (including nested queries) [key: string]: any } /** * @public */ export interface MigrationIterator { next: (count: number) => Promise close: () => Promise } /** * @public * Client to perform model upgrades */ export interface MigrationClient { // Raw collection operations // Raw FIND, allow to find documents inside domain. find: ( domain: Domain, query: MigrationDocumentQuery, options?: Omit, 'lookup'> ) => Promise // Raw group by, allow to group documents inside domain. groupBy: (domain: Domain, field: string, query?: DocumentQuery

) => Promise> // Traverse documents traverse: ( domain: Domain, query: MigrationDocumentQuery, options?: Pick, 'sort' | 'limit' | 'projection'> ) => Promise> // Allow to raw update documents inside domain. update: ( domain: Domain, query: MigrationDocumentQuery, operations: MigrateUpdate ) => Promise bulk: ( domain: Domain, operations: { filter: MigrationDocumentQuery, update: MigrateUpdate }[] ) => Promise // Move documents per domain move: ( sourceDomain: Domain, query: DocumentQuery, targetDomain: Domain, size?: number ) => Promise create: (domain: Domain, doc: T | T[]) => Promise delete: (domain: Domain, _id: Ref) => Promise deleteMany: (domain: Domain, query: DocumentQuery) => Promise hierarchy: Hierarchy model: ModelDb migrateState: Map> storageAdapter: StorageAdapter accountClient: AccountClient wsIds: WorkspaceIds } /** * @public */ export type MigrationUpgradeClient = Client /** * @public */ export interface MigrateOperation { // Perform low level migration prior to the model update preMigrate?: (client: MigrationClient, logger: ModelLogger) => Promise // Perform low level migration migrate: (client: MigrationClient, logger: ModelLogger) => Promise // Perform high level upgrade operations. upgrade: ( state: Map>, client: () => Promise, logger: ModelLogger ) => Promise } /** * @public */ export interface Migrations { state: string func: (client: MigrationClient) => Promise } /** * @public */ export interface UpgradeOperations { state: string func: (client: MigrationUpgradeClient) => Promise } /** * @public */ export async function tryMigrate (client: MigrationClient, plugin: string, migrations: Migrations[]): Promise { const states = client.migrateState.get(plugin) ?? new Set() for (const migration of migrations) { if (states.has(migration.state)) continue try { console.log('running migration', plugin, migration.state) await migration.func(client) } catch (err: any) { console.error(err) Analytics.handleError(err) continue } const st: MigrationState = { plugin, state: migration.state, space: core.space.Configuration, modifiedBy: core.account.System, modifiedOn: Date.now(), _class: core.class.MigrationState, _id: generateId() } await client.create(DOMAIN_MIGRATION, st) } } /** * @public */ export async function tryUpgrade ( state: Map>, client: () => Promise, plugin: string, migrations: UpgradeOperations[] ): Promise { const states = state.get(plugin) ?? new Set() for (const migration of migrations) { if (states.has(migration.state)) continue const _client = await client() try { await migration.func(_client) } catch (err: any) { console.error(err) Analytics.handleError(err) continue } const st: Data = { plugin, state: migration.state } const tx = new TxOperations(_client, core.account.System) await tx.createDoc(core.class.MigrationState, core.space.Configuration, st) } } type DefaultSpaceData = Pick type RequiredData = Omit, keyof DefaultSpaceData> & Partial> /** * @public */ export async function createDefaultSpace ( client: MigrationUpgradeClient, _id: Ref, props: RequiredData, _class: Ref> = core.class.SystemSpace ): Promise { const defaults: DefaultSpaceData = { description: '', private: false, archived: false, members: [] } const data: Data = { ...defaults, ...props } const tx = new TxOperations(client, core.account.System) const current = await tx.findOne(core.class.Space, { _id }) if (current === undefined || current._class !== _class) { if (current !== undefined && current._class !== _class) { await tx.remove(current) } await tx.createDoc(_class, core.space.Space, data, _id) } } /** * @public */ export async function migrateSpace ( client: MigrationClient, from: Ref, to: Ref, domains: Domain[] ): Promise { for (const domain of domains) { await client.update(domain, { space: from }, { space: to }) } await client.update(DOMAIN_TX, { objectSpace: from }, { objectSpace: to }) } export async function migrateSpaceRanks (client: MigrationClient, domain: Domain, space: Space): Promise { type WithRank = Doc & { rank: Rank } const iterator = await client.traverse( domain, { space: space._id, rank: { $exists: true } }, { sort: { rank: SortingOrder.Ascending } } ) try { let rank = '0|100000:' while (true) { const docs = await iterator.next(1000) if (docs === null || docs.length === 0) { break } const updates: { filter: MigrationDocumentQuery>, update: MigrateUpdate> }[] = [] for (const doc of docs) { rank = makeRank(rank, undefined) updates.push({ filter: { _id: doc._id }, update: { rank } }) } await client.bulk(domain, updates) } } finally { await iterator.close() } }