diff --git a/dev/tool/src/clean.ts b/dev/tool/src/clean.ts index 59dc19c0f2..35d6fe9d20 100644 --- a/dev/tool/src/clean.ts +++ b/dev/tool/src/clean.ts @@ -30,13 +30,18 @@ import core, { type Domain, type Ref, type TxCreateDoc, - type WorkspaceId + type WorkspaceId, + type StatusCategory, + type TxMixin, + type TxCUD } from '@hcengineering/core' import { getWorkspaceDB } from '@hcengineering/mongo' import recruit from '@hcengineering/recruit' +import recruitModel from '@hcengineering/model-recruit' import { type StorageAdapter } from '@hcengineering/server-core' import { connect } from '@hcengineering/server-tool' import tags, { type TagCategory, type TagElement, type TagReference } from '@hcengineering/tags' +import task, { type ProjectType, type TaskType } from '@hcengineering/task' import tracker from '@hcengineering/tracker' import { deepEqual } from 'fast-equals' import { MongoClient } from 'mongodb' @@ -641,3 +646,167 @@ function groupBy (docs: T[], key: string): Record { return storage }, {}) } + +export async function restoreRecruitingTaskTypes ( + mongoUrl: string, + workspaceId: WorkspaceId, + transactorUrl: string +): Promise { + const connection = (await connect(transactorUrl, workspaceId, undefined, { + mode: 'backup', + model: 'upgrade' + })) as unknown as CoreClient & BackupClient + const client = new MongoClient(mongoUrl) + try { + await client.connect() + const db = getWorkspaceDB(client, workspaceId) + + // Query all vacancy project types creations (in Model) + // We only update new project types in model here and not old ones in spaces + const vacancyTypes = await connection.findAll>(core.class.TxCreateDoc, { + objectClass: task.class.ProjectType, + objectSpace: core.space.Model, + 'attributes.descriptor': recruit.descriptors.VacancyType + }) + + console.log('Found ', vacancyTypes.length, ' vacancy types to check') + + if (vacancyTypes.length === 0) { + return + } + + const descr = connection.getModel().getObject(recruit.descriptors.VacancyType) + const knownCategories = [ + task.statusCategory.UnStarted, + task.statusCategory.Active, + task.statusCategory.Won, + task.statusCategory.Lost + ] + + function compareCategories (a: Ref, b: Ref): number { + const indexOfA = knownCategories.indexOf(a) + const indexOfB = knownCategories.indexOf(b) + + return indexOfA - indexOfB + } + + for (const vacType of vacancyTypes) { + for (const taskTypeId of vacType.attributes.tasks) { + // Check if task type create TX exists + const createTx = ( + await connection.findAll>(core.class.TxCreateDoc, { + objectClass: task.class.TaskType, + objectSpace: core.space.Model, + objectId: taskTypeId + }) + )[0] + + console.log('####################################') + console.log('Checking vacancy type: ', vacType.attributes.name) + + if (createTx !== undefined) { + console.log('Task type already exists in model') + continue + } + + console.log('Restoring task type: ', taskTypeId, ' in vacancy type: ', vacType.attributes.name) + + // Restore create task type tx + + // Get target class mixin + + const typeMixin = ( + await connection.findAll>(core.class.TxMixin, { + mixin: task.mixin.TaskTypeClass, + 'attributes.projectType': vacType.objectId, + 'attributes.taskType': taskTypeId + }) + )[0] + + if (typeMixin === undefined) { + console.error(new Error('No type mixin found for the task type being restored')) + continue + } + + // Get statuses and categories + const statusesIds = vacType.attributes.statuses.filter((s) => s.taskType === taskTypeId).map((s) => s._id) + if (statusesIds.length === 0) { + console.error(new Error('No statuses defined for the task type being restored')) + continue + } + + const statuses = await connection.findAll(core.class.Status, { + _id: { $in: statusesIds } + }) + const categoriesIds = new Set>() + + statuses.forEach((st) => { + if (st.category !== undefined) { + categoriesIds.add(st.category) + } + }) + + if (categoriesIds.size === 0) { + console.error(new Error('No categories found for the task type being restored')) + continue + } + + const statusCategories = Array.from(categoriesIds) + + statusCategories.sort(compareCategories) + + const createTxNew: TxCreateDoc = { + _id: generateId(), + _class: core.class.TxCreateDoc, + space: core.space.Tx, + objectId: taskTypeId, + objectClass: task.class.TaskType, + objectSpace: core.space.Model, + modifiedBy: core.account.ConfigUser, // So it's not removed during the next migration + modifiedOn: vacType.modifiedOn, + createdOn: vacType.createdOn, + attributes: { + name: 'Applicant', + descriptor: recruitModel.descriptors.Application, + ofClass: recruit.class.Applicant, + targetClass: typeMixin.objectId, + statusClass: core.class.Status, + allowedAsChildOf: [taskTypeId], + statuses: statusesIds, + statusCategories, + parent: vacType.objectId, + kind: 'both', + icon: descr.icon + } + } + + await db.collection(DOMAIN_TX).insertOne(createTxNew) + console.log('Successfully created new task type: ') + console.log(createTxNew) + + // If there were updates to the task type - move them to the model + const updateTxes = ( + await connection.findAll(core.class.TxCUD, { + objectClass: task.class.TaskType, + objectSpace: vacType.objectId as any, + objectId: taskTypeId + }) + ).filter((tx) => [core.class.TxUpdateDoc, core.class.TxRemoveDoc].includes(tx._class)) + + for (const updTx of updateTxes) { + await db.collection>(DOMAIN_TX).insertOne({ + ...updTx, + _id: generateId(), + objectSpace: core.space.Model + }) + } + console.log('Successfully restored ', updateTxes.length, ' CUD transactions') + } + } + } catch (err: any) { + console.trace(err) + } finally { + await client.close() + await connection.close() + } +} diff --git a/dev/tool/src/index.ts b/dev/tool/src/index.ts index 092451bd01..1bd5e747dc 100644 --- a/dev/tool/src/index.ts +++ b/dev/tool/src/index.ts @@ -75,6 +75,7 @@ import { fixCommentDoubleIdCreate, fixMinioBW, fixSkills, + restoreRecruitingTaskTypes, optimizeModel } from './clean' import { checkOrphanWorkspaces } from './cleanOrphan' @@ -797,6 +798,15 @@ export function devTool ( await fixSkills(mongodbUri, getWorkspaceId(workspace, productId), transactorUrl, step) }) + program + .command('restore-ats-types ') + .description('Restore recruiting task types for workspace') + .action(async (workspace: string, step: string) => { + const { mongodbUri } = prepareTools() + console.log('Restoring recruiting task types in workspace ', workspace, '...') + await restoreRecruitingTaskTypes(mongodbUri, getWorkspaceId(workspace, productId), transactorUrl) + }) + program .command('change-field ') .description('change field value for the object')