PSA/server/migrations/20260314133000_surface_unresolved_ticket_status_references.cjs
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

169 lines
4.8 KiB
JavaScript

function isPlainObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
function collectLegacyTicketStatusReferences(value, legacyStatusIds, results, context) {
if (Array.isArray(value)) {
value.forEach((item, index) => {
collectLegacyTicketStatusReferences(item, legacyStatusIds, results, {
...context,
inputPath: `${context.inputPath}[${index}]`
});
});
return;
}
if (!isPlainObject(value)) {
return;
}
if (typeof value.status_id === 'string' && legacyStatusIds.has(value.status_id)) {
if (typeof value.board_id !== 'string') {
results.push({
workflowId: context.workflowId,
tableName: context.tableName,
stepPath: context.stepPath,
actionId: context.actionId,
inputPath: `${context.inputPath}.status_id`,
legacyStatusId: value.status_id
});
}
}
for (const [key, child] of Object.entries(value)) {
collectLegacyTicketStatusReferences(child, legacyStatusIds, results, {
...context,
inputPath: `${context.inputPath}.${key}`
});
}
}
function collectWorkflowStepReferences(step, legacyStatusIds, results, context) {
if (!isPlainObject(step)) {
return;
}
if (isPlainObject(step.config) && isPlainObject(step.config.inputMapping)) {
collectLegacyTicketStatusReferences(step.config.inputMapping, legacyStatusIds, results, {
...context,
actionId: typeof step.config.actionId === 'string' ? step.config.actionId : null,
inputPath: 'inputMapping'
});
}
if (Array.isArray(step.then)) {
step.then.forEach((child, index) => {
collectWorkflowStepReferences(child, legacyStatusIds, results, {
...context,
stepPath: `${context.stepPath}.then[${index}]`
});
});
}
if (Array.isArray(step.else)) {
step.else.forEach((child, index) => {
collectWorkflowStepReferences(child, legacyStatusIds, results, {
...context,
stepPath: `${context.stepPath}.else[${index}]`
});
});
}
if (Array.isArray(step.body)) {
step.body.forEach((child, index) => {
collectWorkflowStepReferences(child, legacyStatusIds, results, {
...context,
stepPath: `${context.stepPath}.body[${index}]`
});
});
}
if (Array.isArray(step.try)) {
step.try.forEach((child, index) => {
collectWorkflowStepReferences(child, legacyStatusIds, results, {
...context,
stepPath: `${context.stepPath}.try[${index}]`
});
});
}
if (Array.isArray(step.catch)) {
step.catch.forEach((child, index) => {
collectWorkflowStepReferences(child, legacyStatusIds, results, {
...context,
stepPath: `${context.stepPath}.catch[${index}]`
});
});
}
}
async function findUnresolvedWorkflowTicketStatusReferences(knex) {
const legacyStatusRows = await knex('statuses')
.where({ status_type: 'ticket' })
.whereNull('board_id')
.select('status_id');
const legacyStatusIds = new Set(legacyStatusRows.map((row) => row.status_id));
if (legacyStatusIds.size === 0) {
return [];
}
const results = [];
const tables = [
{ tableName: 'workflow_definitions', jsonColumn: 'draft_definition' },
{ tableName: 'workflow_definition_versions', jsonColumn: 'definition_json' }
];
for (const { tableName, jsonColumn } of tables) {
const records = await knex(tableName).select('workflow_id', jsonColumn);
for (const record of records) {
const definition = record[jsonColumn];
if (!isPlainObject(definition) || !Array.isArray(definition.steps)) {
continue;
}
definition.steps.forEach((step, index) => {
collectWorkflowStepReferences(step, legacyStatusIds, results, {
workflowId: record.workflow_id,
tableName,
stepPath: `steps[${index}]`,
actionId: null,
inputPath: 'inputMapping'
});
});
}
}
return results;
}
exports.up = async function up(knex) {
const unresolved = await findUnresolvedWorkflowTicketStatusReferences(knex);
if (unresolved.length === 0) {
return;
}
const detailLines = unresolved.map((entry) =>
[
`${entry.tableName}:${entry.workflowId}`,
`step=${entry.stepPath}`,
`action=${entry.actionId ?? 'unknown'}`,
`path=${entry.inputPath}`,
`legacy_status_id=${entry.legacyStatusId}`
].join(' ')
);
throw new Error(
[
'Unresolved legacy ticket status references remain in workflow definitions without literal board context.',
'Update these workflow steps to use board-owned status ids or add explicit board context before rerunning the migration.',
...detailLines
].join('\n')
);
};
exports.down = async function down() {
// Guard migration only; nothing to roll back.
};