@openinc/parse-server-opendash 4.1.1 → 4.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/featuremap.json +27 -12
- package/dist/features/config/helper/customOptions.js +9 -0
- package/dist/features/config/helper/isFeatureEnabled.d.ts +1 -1
- package/dist/features/config/helper/isFeatureEnabled.js +2 -0
- package/dist/features/meta/index.d.ts +1 -0
- package/dist/features/meta/index.js +5 -0
- package/dist/features/meta/services/registerMetaContext.d.ts +29 -0
- package/dist/features/meta/services/registerMetaContext.js +208 -0
- package/dist/features/permissions/types/Permissions.d.ts +24 -1
- package/dist/features/permissions/types/Permissions.js +34 -0
- package/dist/features/ruleset/applyRuleSet.d.ts +41 -0
- package/dist/features/ruleset/applyRuleSet.js +81 -0
- package/dist/features/schema/helper/defaultHandler.js +13 -0
- package/dist/features/schema/helper/migrationMode.d.ts +26 -0
- package/dist/features/schema/helper/migrationMode.js +48 -0
- package/dist/features/schema/index.d.ts +1 -0
- package/dist/features/schema/index.js +3 -1
- package/dist/features/schema/services/beforeSaveHook.js +21 -0
- package/dist/features/translations/services/initTranslation.js +1 -1
- package/dist/hooks/Chat_Message.d.ts +1 -0
- package/dist/hooks/Chat_Message.js +179 -0
- package/dist/hooks/Meta_Config.js +19 -10
- package/dist/hooks/Meta_Context.d.ts +6 -0
- package/dist/hooks/Meta_Context.js +16 -0
- package/dist/hooks/Meta_Entry.js +20 -10
- package/dist/hooks/Service_Equipment.d.ts +1 -0
- package/dist/hooks/Service_Equipment.js +38 -0
- package/dist/hooks/Service_FormConfig.d.ts +1 -0
- package/dist/hooks/Service_FormConfig.js +17 -0
- package/dist/hooks/Service_Project.d.ts +1 -0
- package/dist/hooks/Service_Project.js +17 -0
- package/dist/hooks/Service_Schedule.d.ts +1 -0
- package/dist/hooks/Service_Schedule.js +24 -0
- package/dist/hooks/Service_Schedule_Execution.d.ts +1 -0
- package/dist/hooks/Service_Schedule_Execution.js +17 -0
- package/dist/hooks/Service_Schedule_Template.d.ts +1 -0
- package/dist/hooks/Service_Schedule_Template.js +17 -0
- package/dist/hooks/Service_Status.d.ts +1 -0
- package/dist/hooks/Service_Status.js +32 -0
- package/dist/hooks/Service_Tag.d.ts +1 -0
- package/dist/hooks/Service_Tag.js +168 -0
- package/dist/hooks/Service_Ticket.d.ts +1 -0
- package/dist/hooks/Service_Ticket.js +433 -0
- package/dist/hooks/Service_TriggerState.d.ts +1 -0
- package/dist/hooks/Service_TriggerState.js +17 -0
- package/dist/hooks/_ChangeLog.js +27 -6
- package/dist/i18n/de-DE/chat.json +18 -0
- package/dist/i18n/de-DE/openservice.json +14 -0
- package/dist/i18n/en-US/chat.json +18 -0
- package/dist/i18n/en-US/openservice.json +14 -0
- package/dist/scripts/backfill-meta-contexts.d.ts +1 -0
- package/dist/scripts/backfill-meta-contexts.js +100 -0
- package/dist/scripts/migration/lib/acl.d.ts +52 -0
- package/dist/scripts/migration/lib/acl.js +79 -0
- package/dist/scripts/migration/lib/articleMeta.d.ts +47 -0
- package/dist/scripts/migration/lib/articleMeta.js +132 -0
- package/dist/scripts/migration/lib/env.d.ts +56 -0
- package/dist/scripts/migration/lib/env.js +106 -0
- package/dist/scripts/migration/lib/latest.d.ts +47 -0
- package/dist/scripts/migration/lib/latest.js +115 -0
- package/dist/scripts/migration/lib/map.d.ts +53 -0
- package/dist/scripts/migration/lib/map.js +113 -0
- package/dist/scripts/migration/lib/material.d.ts +47 -0
- package/dist/scripts/migration/lib/material.js +51 -0
- package/dist/scripts/migration/lib/message.d.ts +78 -0
- package/dist/scripts/migration/lib/message.js +115 -0
- package/dist/scripts/migration/lib/pass.d.ts +67 -0
- package/dist/scripts/migration/lib/pass.js +89 -0
- package/dist/scripts/migration/lib/ptr.d.ts +32 -0
- package/dist/scripts/migration/lib/ptr.js +38 -0
- package/dist/scripts/migration/lib/report.d.ts +41 -0
- package/dist/scripts/migration/lib/report.js +78 -0
- package/dist/scripts/migration/lib/rest.d.ts +47 -0
- package/dist/scripts/migration/lib/rest.js +68 -0
- package/dist/scripts/migration/lib/ticket.d.ts +72 -0
- package/dist/scripts/migration/lib/ticket.js +192 -0
- package/dist/scripts/migration/passes/01-statuses.d.ts +12 -0
- package/dist/scripts/migration/passes/01-statuses.js +122 -0
- package/dist/scripts/migration/passes/02-tags.d.ts +13 -0
- package/dist/scripts/migration/passes/02-tags.js +150 -0
- package/dist/scripts/migration/passes/03-equipment.d.ts +14 -0
- package/dist/scripts/migration/passes/03-equipment.js +176 -0
- package/dist/scripts/migration/passes/04-projects.d.ts +27 -0
- package/dist/scripts/migration/passes/04-projects.js +241 -0
- package/dist/scripts/migration/passes/05-articles.d.ts +25 -0
- package/dist/scripts/migration/passes/05-articles.js +203 -0
- package/dist/scripts/migration/passes/06-formconfigs.d.ts +17 -0
- package/dist/scripts/migration/passes/06-formconfigs.js +166 -0
- package/dist/scripts/migration/passes/07-meta-configs.d.ts +46 -0
- package/dist/scripts/migration/passes/07-meta-configs.js +185 -0
- package/dist/scripts/migration/passes/08-tickets.d.ts +27 -0
- package/dist/scripts/migration/passes/08-tickets.js +353 -0
- package/dist/scripts/migration/passes/09-ticket-meta.d.ts +12 -0
- package/dist/scripts/migration/passes/09-ticket-meta.js +136 -0
- package/dist/scripts/migration/passes/10-chat.d.ts +17 -0
- package/dist/scripts/migration/passes/10-chat.js +230 -0
- package/dist/scripts/migration/passes/11-assets.d.ts +17 -0
- package/dist/scripts/migration/passes/11-assets.js +93 -0
- package/dist/scripts/migration/passes/12-history.d.ts +24 -0
- package/dist/scripts/migration/passes/12-history.js +332 -0
- package/dist/scripts/migration/run.d.ts +1 -0
- package/dist/scripts/migration/run.js +148 -0
- package/dist/scripts/migration/verify.d.ts +1 -0
- package/dist/scripts/migration/verify.js +620 -0
- package/dist/scripts/seed-v1-testdata.d.ts +41 -0
- package/dist/scripts/seed-v1-testdata.js +543 -0
- package/dist/types/BDE_Article.d.ts +3 -0
- package/dist/types/BDE_Article.js +6 -0
- package/dist/types/Chat_Message.d.ts +49 -0
- package/dist/types/Chat_Message.js +87 -0
- package/dist/types/MES_RessourceOverview.d.ts +38 -0
- package/dist/types/MES_RessourceOverview.js +63 -0
- package/dist/types/Meta_Context.d.ts +43 -0
- package/dist/types/Meta_Context.js +75 -0
- package/dist/types/Service_Equipment.d.ts +33 -0
- package/dist/types/Service_Equipment.js +57 -0
- package/dist/types/Service_FormConfig.d.ts +33 -0
- package/dist/types/Service_FormConfig.js +57 -0
- package/dist/types/Service_MeterReading.d.ts +35 -0
- package/dist/types/Service_MeterReading.js +57 -0
- package/dist/types/Service_Project.d.ts +53 -0
- package/dist/types/Service_Project.js +93 -0
- package/dist/types/Service_Schedule.d.ts +66 -0
- package/dist/types/Service_Schedule.js +117 -0
- package/dist/types/Service_Schedule_Execution.d.ts +38 -0
- package/dist/types/Service_Schedule_Execution.js +63 -0
- package/dist/types/Service_Schedule_Template.d.ts +36 -0
- package/dist/types/Service_Schedule_Template.js +60 -0
- package/dist/types/Service_Status.d.ts +30 -0
- package/dist/types/Service_Status.js +51 -0
- package/dist/types/Service_Tag.d.ts +30 -0
- package/dist/types/Service_Tag.js +51 -0
- package/dist/types/Service_Ticket.d.ts +92 -0
- package/dist/types/Service_Ticket.js +165 -0
- package/dist/types/Service_TicketCounter.d.ts +18 -0
- package/dist/types/Service_TicketCounter.js +27 -0
- package/dist/types/Service_TriggerState.d.ts +31 -0
- package/dist/types/Service_TriggerState.js +51 -0
- package/dist/types/_Session.d.ts +0 -1
- package/dist/types/_User.d.ts +2 -6
- package/dist/types/custom/MetaField.d.ts +9 -0
- package/dist/types/custom/TicketMaterial.d.ts +32 -0
- package/dist/types/custom/TicketMaterial.js +2 -0
- package/dist/types/index.d.ts +30 -0
- package/dist/types/index.js +33 -3
- package/package.json +6 -3
- package/schema/BDE_Article.json +3 -0
- package/schema/Changelog.json +3 -6
- package/schema/Chat_Message.json +88 -0
- package/schema/Meta_Context.json +78 -0
- package/schema/Service_Equipment.json +55 -0
- package/schema/Service_FormConfig.json +66 -0
- package/schema/Service_MeterReading.json +54 -0
- package/schema/Service_MigrationMap.json +55 -0
- package/schema/Service_Project.json +77 -0
- package/schema/Service_Schedule.json +101 -0
- package/schema/Service_Schedule_Execution.json +58 -0
- package/schema/Service_Schedule_Template.json +73 -0
- package/schema/Service_Status.json +50 -0
- package/schema/Service_Tag.json +50 -0
- package/schema/Service_Ticket.json +114 -0
- package/schema/Service_TicketCounter.json +38 -0
- package/schema/Service_TriggerState.json +50 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.pass12 = void 0;
|
|
7
|
+
const node_1 = __importDefault(require("parse/node"));
|
|
8
|
+
const index_js_1 = require("../../../types/index.js");
|
|
9
|
+
const acl_js_1 = require("../lib/acl.js");
|
|
10
|
+
const latest_js_1 = require("../lib/latest.js");
|
|
11
|
+
const material_js_1 = require("../lib/material.js");
|
|
12
|
+
const ptr_js_1 = require("../lib/ptr.js");
|
|
13
|
+
const pass_js_1 = require("../lib/pass.js");
|
|
14
|
+
const ticket_js_1 = require("../lib/ticket.js");
|
|
15
|
+
const _08_tickets_js_1 = require("./08-tickets.js");
|
|
16
|
+
function sortEvents(events) {
|
|
17
|
+
return [...events].sort((a, b) => a.at.getTime() - b.at.getTime());
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Pass 12 — replay the v1 side tables into `OD3_Changelog`.
|
|
21
|
+
*
|
|
22
|
+
* The v2 ticket detail page already aggregates changelog entries for
|
|
23
|
+
* `OD3_Service_Ticket` by objectId and renders each field through that field's own
|
|
24
|
+
* Display component, so entries written here need no UI work to show up.
|
|
25
|
+
*
|
|
26
|
+
* Three things this depends on and cannot work around:
|
|
27
|
+
*
|
|
28
|
+
* - **Historical `createdAt`.** The timeline sorts, groups and labels purely by
|
|
29
|
+
* `entry.createdAt`. Under the master key Parse would stamp `now` on every entry
|
|
30
|
+
* and the whole trail would collapse onto the migration date.
|
|
31
|
+
* - **`ACL` inside the snapshot.** `beforeSave(OD3_Changelog)` mirrors the changed
|
|
32
|
+
* object's ACL, read from `value.ACL` / `original.ACL`. Omit it and history ends
|
|
33
|
+
* up more restrictive than the ticket it describes.
|
|
34
|
+
* - **Parse wire types in the snapshot.** `{__type:"Pointer"}` / `{__type:"Date"}`
|
|
35
|
+
* are what make the diff render reference pills and dates instead of raw JSON.
|
|
36
|
+
*
|
|
37
|
+
* By construction the final snapshot of the replay equals the flat ticket pass 08
|
|
38
|
+
* wrote — which `verify.ts` checks, and which is the strongest signal in the whole
|
|
39
|
+
* migration that both halves agree.
|
|
40
|
+
*/
|
|
41
|
+
exports.pass12 = {
|
|
42
|
+
id: "12",
|
|
43
|
+
title: "history",
|
|
44
|
+
needs: ["08"],
|
|
45
|
+
async run({ config, map, report, entry }) {
|
|
46
|
+
const { batchSize, tenantId } = config;
|
|
47
|
+
const scoped = (cls) => () => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(cls), tenantId);
|
|
48
|
+
// ── ACLs of the migrated tickets ─────────────────────────────────────────
|
|
49
|
+
// `beforeSave(OD3_Changelog)` normally mirrors the changed object's ACL onto
|
|
50
|
+
// the entry, but beforeSave is not registered in migration mode — so each entry
|
|
51
|
+
// must carry it explicitly, and the snapshot should show the v2 ACL rather than
|
|
52
|
+
// the v1 one it used to.
|
|
53
|
+
const ticketAcls = await (0, ticket_js_1.loadTicketAcls)(config);
|
|
54
|
+
// ── Tag parents, for closure expansion in the tag events ─────────────────
|
|
55
|
+
const parentOf = new Map();
|
|
56
|
+
for (const tag of await (0, pass_js_1.all)(() => new node_1.default.Query(index_js_1.Service_Tag), batchSize)) {
|
|
57
|
+
if (tag.id)
|
|
58
|
+
parentOf.set(tag.id, (0, pass_js_1.refId)(tag.get("parent")));
|
|
59
|
+
}
|
|
60
|
+
// ── Every history table, one query each ──────────────────────────────────
|
|
61
|
+
const titles = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Ticket_Title), batchSize);
|
|
62
|
+
const priorities = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Priority), batchSize);
|
|
63
|
+
const downtimes = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Downtime), batchSize);
|
|
64
|
+
const frequencies = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Frequency), batchSize);
|
|
65
|
+
const restrictions = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Restriction), batchSize);
|
|
66
|
+
const duedates = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Duedate), batchSize);
|
|
67
|
+
const sourceRows = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Ticket_Source), batchSize);
|
|
68
|
+
const projectRows = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Ticket_Project), batchSize);
|
|
69
|
+
const categoryRows = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Ticket_Issuecategory), batchSize);
|
|
70
|
+
const stateRows = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Ticket_Kanban_State), batchSize);
|
|
71
|
+
const assignmentRows = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Ticket_Assignment), batchSize);
|
|
72
|
+
const materialRows = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Ticket_Material), batchSize);
|
|
73
|
+
// Which tickets already have reconstructed history, so a re-run does not
|
|
74
|
+
// duplicate a trail. Resumability is per ticket, not per batch — this pass
|
|
75
|
+
// writes 50k–200k rows and a mid-pass failure must not restart it.
|
|
76
|
+
const alreadyDone = new Set();
|
|
77
|
+
for await (const page of (0, pass_js_1.pages)(() => new node_1.default.Query(index_js_1.Changelog)
|
|
78
|
+
.equalTo("nameOfClass", index_js_1.Service_Ticket.className)
|
|
79
|
+
.equalTo("operation", "create"), batchSize)) {
|
|
80
|
+
for (const row of page) {
|
|
81
|
+
const context = row.get("context");
|
|
82
|
+
if (context?.migrated)
|
|
83
|
+
alreadyDone.add(row.get("changedObject"));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (alreadyDone.size > 0) {
|
|
87
|
+
report.info(entry, `${alreadyDone.size} ticket(s) already have reconstructed history — skipping them`);
|
|
88
|
+
}
|
|
89
|
+
let totalEntries = 0;
|
|
90
|
+
for await (const page of (0, pass_js_1.pages)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Maintenance_Ticket), tenantId), batchSize)) {
|
|
91
|
+
for (const ticket of page) {
|
|
92
|
+
const oldId = ticket.id;
|
|
93
|
+
if (!oldId)
|
|
94
|
+
continue;
|
|
95
|
+
const newId = map.get(index_js_1.Maintenance_Ticket.className, oldId);
|
|
96
|
+
if (!newId) {
|
|
97
|
+
report.error(entry, `ticket ${oldId} has no migrated counterpart — run pass 08 first`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (alreadyDone.has(newId)) {
|
|
101
|
+
entry.skipped++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const ticketTenantId = (0, pass_js_1.refId)(ticket.get("tenant"));
|
|
105
|
+
const creatorId = (0, pass_js_1.refId)(ticket.get("user"));
|
|
106
|
+
// ── Build the event list ─────────────────────────────────────────────
|
|
107
|
+
const events = [];
|
|
108
|
+
const push = (row, changes, legacyClass) => {
|
|
109
|
+
// Same clock the fold uses, or the replay ends on a different row than
|
|
110
|
+
// the ticket carries. See lib/latest.ts `effectiveAt`.
|
|
111
|
+
const at = (0, latest_js_1.effectiveAt)(row);
|
|
112
|
+
if (!at || !row.id)
|
|
113
|
+
return;
|
|
114
|
+
// A row whose value is unset is not a change. The fold treats it the same
|
|
115
|
+
// way (`latest?.get(x) ?? fallback`), so emitting `{title: undefined}`
|
|
116
|
+
// here would make the trail claim an edit the ticket never took.
|
|
117
|
+
const real = Object.fromEntries(Object.entries(changes).filter(([, v]) => v !== undefined && v !== null));
|
|
118
|
+
if (Object.keys(real).length === 0)
|
|
119
|
+
return;
|
|
120
|
+
events.push({
|
|
121
|
+
at,
|
|
122
|
+
by: (0, pass_js_1.refId)(row.get("user")),
|
|
123
|
+
changes: real,
|
|
124
|
+
legacyClass,
|
|
125
|
+
legacyId: row.id,
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
for (const row of titles.get(oldId) ?? []) {
|
|
129
|
+
push(row, { title: row.get("title") }, index_js_1.Maintenance_Ticket_Title.className);
|
|
130
|
+
}
|
|
131
|
+
for (const row of priorities.get(oldId) ?? []) {
|
|
132
|
+
push(row, { priority: row.get("value") }, index_js_1.Maintenance_Priority.className);
|
|
133
|
+
}
|
|
134
|
+
for (const row of downtimes.get(oldId) ?? []) {
|
|
135
|
+
push(row, { downtime: row.get("value") }, index_js_1.Maintenance_Downtime.className);
|
|
136
|
+
}
|
|
137
|
+
for (const row of frequencies.get(oldId) ?? []) {
|
|
138
|
+
push(row, { frequency: row.get("value") }, index_js_1.Maintenance_Frequency.className);
|
|
139
|
+
}
|
|
140
|
+
for (const row of restrictions.get(oldId) ?? []) {
|
|
141
|
+
push(row, { restriction: row.get("value") }, index_js_1.Maintenance_Restriction.className);
|
|
142
|
+
}
|
|
143
|
+
for (const row of duedates.get(oldId) ?? []) {
|
|
144
|
+
// One v1 row carries both ends of the range, so this is a single event
|
|
145
|
+
// that changes two fields — not two events.
|
|
146
|
+
const changes = {};
|
|
147
|
+
const end = row.get("end");
|
|
148
|
+
const start = row.get("start");
|
|
149
|
+
if (end)
|
|
150
|
+
changes.dueDate = (0, ptr_js_1.dateVal)(end);
|
|
151
|
+
if (start)
|
|
152
|
+
changes.startDate = (0, ptr_js_1.dateVal)(start);
|
|
153
|
+
push(row, changes, index_js_1.Maintenance_Duedate.className);
|
|
154
|
+
}
|
|
155
|
+
for (const row of sourceRows.get(oldId) ?? []) {
|
|
156
|
+
const equipmentId = map.get(index_js_1.Source.className, (0, pass_js_1.refId)(row.get("source")) ?? "");
|
|
157
|
+
if (equipmentId) {
|
|
158
|
+
push(row, { equipment: (0, ptr_js_1.ptr)(index_js_1.Service_Equipment.className, equipmentId) }, index_js_1.Maintenance_Ticket_Source.className);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const row of projectRows.get(oldId) ?? []) {
|
|
162
|
+
const projectId = map.get(index_js_1.Maintenance_Project.className, (0, pass_js_1.refId)(row.get("project")) ?? "");
|
|
163
|
+
if (projectId) {
|
|
164
|
+
push(row, { project: (0, ptr_js_1.ptr)(index_js_1.Service_Project.className, projectId) }, index_js_1.Maintenance_Ticket_Project.className);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
for (const row of categoryRows.get(oldId) ?? []) {
|
|
168
|
+
const tagId = map.get(index_js_1.Maintenance_Issuecategory.className, (0, pass_js_1.refId)(row.get("issuecategory")) ?? "");
|
|
169
|
+
if (tagId) {
|
|
170
|
+
// Stored as the closure, matching what the ticket itself carries.
|
|
171
|
+
push(row, { tags: (0, _08_tickets_js_1.closureOf)(tagId, parentOf) }, index_js_1.Maintenance_Ticket_Issuecategory.className);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const row of stateRows.get(oldId) ?? []) {
|
|
175
|
+
const statusId = map.get(index_js_1.Maintenance_Kanban_State.className, (0, pass_js_1.refId)(row.get("state")) ?? "");
|
|
176
|
+
if (statusId) {
|
|
177
|
+
push(row, { status: (0, ptr_js_1.ptr)(index_js_1.Service_Status.className, statusId) }, index_js_1.Maintenance_Ticket_Kanban_State.className);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const row of materialRows.get(oldId) ?? []) {
|
|
181
|
+
// Each v1 row carries the WHOLE list, so every row is a complete
|
|
182
|
+
// new value for the column — the same conversion the fold uses, so
|
|
183
|
+
// the replay ends on exactly what pass 08 wrote.
|
|
184
|
+
//
|
|
185
|
+
// An empty result is emitted as an empty array rather than skipped:
|
|
186
|
+
// clearing the last material line is a real edit, and `push` drops
|
|
187
|
+
// undefined/null but keeps `[]`.
|
|
188
|
+
const { entries } = (0, material_js_1.toMaterial)(row.get("itemList"), map);
|
|
189
|
+
push(row, { material: entries }, index_js_1.Maintenance_Ticket_Material.className);
|
|
190
|
+
}
|
|
191
|
+
// ── Assignments: group by batchStamp ────────────────────────────────
|
|
192
|
+
// `assignUsersAndRoles` writes one row *per assignee* per change, which is
|
|
193
|
+
// exactly what `batchStamp` exists to tie back together. Ungrouped, a
|
|
194
|
+
// three-assignee change would render as three separate timeline events.
|
|
195
|
+
const batches = new Map();
|
|
196
|
+
for (const row of assignmentRows.get(oldId) ?? []) {
|
|
197
|
+
const stamp = row.get("batchStamp");
|
|
198
|
+
if (!stamp)
|
|
199
|
+
continue;
|
|
200
|
+
const key = stamp.toISOString();
|
|
201
|
+
const bucket = batches.get(key);
|
|
202
|
+
if (bucket)
|
|
203
|
+
bucket.push(row);
|
|
204
|
+
else
|
|
205
|
+
batches.set(key, [row]);
|
|
206
|
+
}
|
|
207
|
+
for (const [stampIso, rows] of batches) {
|
|
208
|
+
const users = rows
|
|
209
|
+
.map((r) => (0, pass_js_1.refId)(r.get("assignedUser")))
|
|
210
|
+
.filter((id) => !!id);
|
|
211
|
+
const roles = rows
|
|
212
|
+
.map((r) => (0, pass_js_1.refId)(r.get("assignedRole")))
|
|
213
|
+
.filter((id) => !!id);
|
|
214
|
+
const changes = {};
|
|
215
|
+
if (users.length)
|
|
216
|
+
changes.assignedUsers = users;
|
|
217
|
+
if (roles.length)
|
|
218
|
+
changes.assignedRoles = roles;
|
|
219
|
+
if (Object.keys(changes).length === 0)
|
|
220
|
+
continue;
|
|
221
|
+
events.push({
|
|
222
|
+
at: new Date(stampIso),
|
|
223
|
+
// The acting user is who *made* the assignment, not who was assigned.
|
|
224
|
+
by: (0, pass_js_1.refId)(rows[0].get("user")),
|
|
225
|
+
changes,
|
|
226
|
+
legacyClass: index_js_1.Maintenance_Ticket_Assignment.className,
|
|
227
|
+
legacyId: rows.map((r) => r.id).join(","),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
// ── Replay ──────────────────────────────────────────────────────────
|
|
231
|
+
const ordered = sortEvents(events);
|
|
232
|
+
// The creation snapshot: what the ticket looked like before any side-table
|
|
233
|
+
// row existed. Anything only ever set by a side table is absent here and
|
|
234
|
+
// appears as its first event, which is the honest reading.
|
|
235
|
+
const v2Acl = ticketAcls.get(newId);
|
|
236
|
+
const aclJson = v2Acl?.toJSON() ?? ticket.getACL()?.toJSON();
|
|
237
|
+
const snapshot = {
|
|
238
|
+
objectId: newId,
|
|
239
|
+
title: ticket.get("title") ?? "",
|
|
240
|
+
deleted: ticket.get("enabled") !== true,
|
|
241
|
+
origin: `legacy:${oldId}`,
|
|
242
|
+
};
|
|
243
|
+
if (ticket.get("description")) {
|
|
244
|
+
snapshot.description = ticket.get("description");
|
|
245
|
+
}
|
|
246
|
+
if (ticketTenantId)
|
|
247
|
+
snapshot.tenant = (0, ptr_js_1.tenantPtr)(ticketTenantId);
|
|
248
|
+
if (creatorId)
|
|
249
|
+
snapshot.createdBy = (0, ptr_js_1.userPtr)(creatorId);
|
|
250
|
+
if (aclJson)
|
|
251
|
+
snapshot.ACL = aclJson;
|
|
252
|
+
// Captured before the walk mutates `snapshot`.
|
|
253
|
+
const createEntry = {
|
|
254
|
+
operation: "create",
|
|
255
|
+
original: null,
|
|
256
|
+
value: { ...snapshot },
|
|
257
|
+
at: ticket.createdAt ?? ordered[0]?.at ?? new Date(0),
|
|
258
|
+
by: creatorId,
|
|
259
|
+
legacyClass: index_js_1.Maintenance_Ticket.className,
|
|
260
|
+
legacyId: oldId,
|
|
261
|
+
};
|
|
262
|
+
const toWrite = [];
|
|
263
|
+
for (const event of ordered) {
|
|
264
|
+
const before = { ...snapshot };
|
|
265
|
+
Object.assign(snapshot, event.changes);
|
|
266
|
+
toWrite.push({
|
|
267
|
+
operation: "update",
|
|
268
|
+
original: before,
|
|
269
|
+
value: { ...snapshot },
|
|
270
|
+
at: event.at,
|
|
271
|
+
by: event.by,
|
|
272
|
+
legacyClass: event.legacyClass,
|
|
273
|
+
legacyId: event.legacyId,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
// The `create` entry goes LAST on purpose. `alreadyDone` above treats its
|
|
277
|
+
// presence as "this ticket's trail is complete", so writing it first would
|
|
278
|
+
// make a run that died mid-ticket look finished on resume and leave the
|
|
279
|
+
// trail permanently truncated. Written last, it is a commit marker.
|
|
280
|
+
// Storage order is irrelevant — the timeline sorts by createdAt.
|
|
281
|
+
toWrite.push(createEntry);
|
|
282
|
+
if (config.dryRun) {
|
|
283
|
+
entry.created += toWrite.length;
|
|
284
|
+
totalEntries += toWrite.length;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
let wrote = 0;
|
|
288
|
+
for (const item of toWrite) {
|
|
289
|
+
const changelog = new index_js_1.Changelog();
|
|
290
|
+
changelog.set("nameOfClass", index_js_1.Service_Ticket.className);
|
|
291
|
+
changelog.set("changedObject", newId);
|
|
292
|
+
changelog.set("operation", item.operation);
|
|
293
|
+
changelog.set("masterkey", false);
|
|
294
|
+
if (item.by)
|
|
295
|
+
changelog.set("actingUser", (0, ptr_js_1.userPtr)(item.by));
|
|
296
|
+
changelog.set("original", item.original);
|
|
297
|
+
changelog.set("value", item.value);
|
|
298
|
+
// Provenance: distinguishes reconstructed history from history the app
|
|
299
|
+
// wrote, and lets a re-run recognise what it already did.
|
|
300
|
+
changelog.set("context", {
|
|
301
|
+
migrated: true,
|
|
302
|
+
run: config.runId,
|
|
303
|
+
legacyClass: item.legacyClass,
|
|
304
|
+
legacyId: item.legacyId,
|
|
305
|
+
});
|
|
306
|
+
// Whoever can see the ticket can see its history — the grant the
|
|
307
|
+
// suppressed changelog hook would have applied.
|
|
308
|
+
if (v2Acl)
|
|
309
|
+
changelog.setACL(v2Acl);
|
|
310
|
+
try {
|
|
311
|
+
// The historical timestamp IS the entry: the changelog timeline sorts,
|
|
312
|
+
// groups and labels purely by createdAt.
|
|
313
|
+
await (0, acl_js_1.saveMigrated)(config, changelog, {
|
|
314
|
+
className: index_js_1.Changelog.className,
|
|
315
|
+
isNew: true,
|
|
316
|
+
createdAt: item.at,
|
|
317
|
+
updatedAt: item.at,
|
|
318
|
+
});
|
|
319
|
+
wrote += 1;
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
report.error(entry, `changelog for ticket ${oldId} (${item.legacyClass}) failed: ` +
|
|
323
|
+
`${error.message}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
entry.created += wrote;
|
|
327
|
+
totalEntries += wrote;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
report.info(entry, `${totalEntries} changelog entr(ies) reconstructed`);
|
|
331
|
+
},
|
|
332
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const env_js_1 = require("./lib/env.js");
|
|
4
|
+
const map_js_1 = require("./lib/map.js");
|
|
5
|
+
const report_js_1 = require("./lib/report.js");
|
|
6
|
+
const _01_statuses_js_1 = require("./passes/01-statuses.js");
|
|
7
|
+
const _02_tags_js_1 = require("./passes/02-tags.js");
|
|
8
|
+
const _03_equipment_js_1 = require("./passes/03-equipment.js");
|
|
9
|
+
const _04_projects_js_1 = require("./passes/04-projects.js");
|
|
10
|
+
const _05_articles_js_1 = require("./passes/05-articles.js");
|
|
11
|
+
const _06_formconfigs_js_1 = require("./passes/06-formconfigs.js");
|
|
12
|
+
const _07_meta_configs_js_1 = require("./passes/07-meta-configs.js");
|
|
13
|
+
const _08_tickets_js_1 = require("./passes/08-tickets.js");
|
|
14
|
+
const _09_ticket_meta_js_1 = require("./passes/09-ticket-meta.js");
|
|
15
|
+
const _10_chat_js_1 = require("./passes/10-chat.js");
|
|
16
|
+
const _11_assets_js_1 = require("./passes/11-assets.js");
|
|
17
|
+
const _12_history_js_1 = require("./passes/12-history.js");
|
|
18
|
+
/**
|
|
19
|
+
* openservice v1 → v2 migration orchestrator.
|
|
20
|
+
*
|
|
21
|
+
* OD_MIGRATION_MODE=1 PARSE_APP_ID=… PARSE_MAINTENANCE_KEY=… \
|
|
22
|
+
* node dist/scripts/migration/run.js --tenant=<id> --dry-run
|
|
23
|
+
*
|
|
24
|
+
* Flags: --tenant=<id> --only=01,08 --dry-run --resume=<runId> --batch=500
|
|
25
|
+
*
|
|
26
|
+
* Pass order is a hard dependency chain (equipment before tickets, tickets before
|
|
27
|
+
* anything needing the new ids), so the chain is declared on each pass and checked
|
|
28
|
+
* here rather than left to whoever runs it.
|
|
29
|
+
*
|
|
30
|
+
* Full plan: `plugins/openservice/MIGRATION_PLAN.md` in the opendash repo.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Registered in dependency order.
|
|
34
|
+
*
|
|
35
|
+
* The order is not cosmetic: equipment/tags/statuses/projects must exist before
|
|
36
|
+
* tickets can point at them, and tickets must exist before anything keyed by the
|
|
37
|
+
* new ticket id (meta, chat, assets, history).
|
|
38
|
+
*/
|
|
39
|
+
const PASSES = [
|
|
40
|
+
_01_statuses_js_1.pass01,
|
|
41
|
+
_02_tags_js_1.pass02,
|
|
42
|
+
_03_equipment_js_1.pass03,
|
|
43
|
+
_04_projects_js_1.pass04,
|
|
44
|
+
_05_articles_js_1.pass05,
|
|
45
|
+
_06_formconfigs_js_1.pass06,
|
|
46
|
+
_07_meta_configs_js_1.pass07,
|
|
47
|
+
_08_tickets_js_1.pass08,
|
|
48
|
+
_09_ticket_meta_js_1.pass09,
|
|
49
|
+
_10_chat_js_1.pass10,
|
|
50
|
+
_11_assets_js_1.pass11,
|
|
51
|
+
_12_history_js_1.pass12,
|
|
52
|
+
];
|
|
53
|
+
/** Passes the plan defines but which are deliberately not implemented. */
|
|
54
|
+
const PLANNED = ["13 schedules", "14 executions"];
|
|
55
|
+
function selectPasses(config) {
|
|
56
|
+
if (!config.only)
|
|
57
|
+
return PASSES;
|
|
58
|
+
const wanted = new Set(config.only);
|
|
59
|
+
const unknown = config.only.filter((id) => !PASSES.some((p) => p.id === id));
|
|
60
|
+
if (unknown.length) {
|
|
61
|
+
throw new env_js_1.MigrationConfigError(`Unknown pass id(s): ${unknown.join(", ")}.\n` +
|
|
62
|
+
` Implemented: ${PASSES.map((p) => p.id).join(", ")}\n` +
|
|
63
|
+
` Deferred, not implemented: ${PLANNED.join(", ")}`);
|
|
64
|
+
}
|
|
65
|
+
// A selected pass whose prerequisite is neither selected nor already recorded in
|
|
66
|
+
// the map would read a half-built world. Warn rather than block: re-running a
|
|
67
|
+
// single pass against a completed earlier run is a legitimate thing to do.
|
|
68
|
+
const selected = PASSES.filter((p) => wanted.has(p.id));
|
|
69
|
+
for (const p of selected) {
|
|
70
|
+
const missing = p.needs.filter((n) => !wanted.has(n));
|
|
71
|
+
if (missing.length) {
|
|
72
|
+
console.warn(`⚠ pass ${p.id} (${p.title}) depends on ${missing.join(", ")}, ` +
|
|
73
|
+
`which are not in --only. It will rely on their MigrationMap rows ` +
|
|
74
|
+
`already existing from an earlier run.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return selected;
|
|
78
|
+
}
|
|
79
|
+
async function main() {
|
|
80
|
+
let config;
|
|
81
|
+
let passes;
|
|
82
|
+
try {
|
|
83
|
+
config = (0, env_js_1.readConfig)(process.argv.slice(2));
|
|
84
|
+
passes = selectPasses(config);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof env_js_1.MigrationConfigError) {
|
|
88
|
+
console.error(`\n✗ ${error.message}\n`);
|
|
89
|
+
return 2;
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
console.log(`\nopenservice v1 → v2 migration`);
|
|
94
|
+
console.log(` server ${config.parseUrl}`);
|
|
95
|
+
console.log(` tenant ${config.tenantId ?? "(all)"}`);
|
|
96
|
+
console.log(` run ${config.runId}`);
|
|
97
|
+
console.log(` passes ${passes.map((p) => p.id).join(", ") || "(none)"}`);
|
|
98
|
+
if (config.dryRun)
|
|
99
|
+
console.log(` MODE DRY RUN — nothing will be written`);
|
|
100
|
+
if (PLANNED.length) {
|
|
101
|
+
console.log(`
|
|
102
|
+
deferred (MIGRATION_PLAN §4.5): ${PLANNED.join(", ")}`);
|
|
103
|
+
}
|
|
104
|
+
(0, env_js_1.initParse)(config);
|
|
105
|
+
const report = new report_js_1.Report();
|
|
106
|
+
const map = new map_js_1.MigrationMap(config);
|
|
107
|
+
try {
|
|
108
|
+
await map.load();
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
// Almost always an unreachable server or a rejected key, and the raw Parse
|
|
112
|
+
// error ("Unexpected end of JSON input") says nothing useful about either.
|
|
113
|
+
console.error(`
|
|
114
|
+
✗ Could not read ${map_js_1.MIGRATION_MAP_CLASS} from ${config.parseUrl}.
|
|
115
|
+
` +
|
|
116
|
+
` ${error.message}
|
|
117
|
+
` +
|
|
118
|
+
` Check that Parse Server is reachable there, that PARSE_APP_ID matches, ` +
|
|
119
|
+
`that PARSE_MAINTENANCE_KEY is the server's maintenanceKey, and that this ` +
|
|
120
|
+
`host is allowed by maintenanceKeyIps (default: localhost only).
|
|
121
|
+
`);
|
|
122
|
+
return 2;
|
|
123
|
+
}
|
|
124
|
+
for (const pass of passes) {
|
|
125
|
+
const entry = report.begin(`${pass.id} ${pass.title}`);
|
|
126
|
+
try {
|
|
127
|
+
await pass.run({ config, map, report, entry });
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
report.error(entry, `pass aborted: ${error.message}`);
|
|
131
|
+
}
|
|
132
|
+
report.end();
|
|
133
|
+
}
|
|
134
|
+
report.printSummary(config.runId, config.dryRun);
|
|
135
|
+
if (report.failed) {
|
|
136
|
+
console.error(`✗ run finished with errors. Passes are idempotent — fix the cause and ` +
|
|
137
|
+
`re-run with --resume=${config.runId}.\n`);
|
|
138
|
+
return 1;
|
|
139
|
+
}
|
|
140
|
+
console.log(config.dryRun
|
|
141
|
+
? `✓ dry run clean. Re-run without --dry-run to write.\n`
|
|
142
|
+
: `✓ run complete. Next: verify.ts.\n`);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
main().then((code) => process.exit(code), (error) => {
|
|
146
|
+
console.error(error);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|