@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,353 @@
|
|
|
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.pass08 = exports.legacyOrigin = void 0;
|
|
7
|
+
exports.closureOf = closureOf;
|
|
8
|
+
const node_1 = __importDefault(require("parse/node"));
|
|
9
|
+
const index_js_1 = require("../../../types/index.js");
|
|
10
|
+
const acl_js_1 = require("../lib/acl.js");
|
|
11
|
+
const map_js_1 = require("../lib/map.js");
|
|
12
|
+
const ticket_js_1 = require("../lib/ticket.js");
|
|
13
|
+
const latest_js_1 = require("../lib/latest.js");
|
|
14
|
+
const material_js_1 = require("../lib/material.js");
|
|
15
|
+
const message_js_1 = require("../lib/message.js");
|
|
16
|
+
const ptr_js_1 = require("../lib/ptr.js");
|
|
17
|
+
const pass_js_1 = require("../lib/pass.js");
|
|
18
|
+
/** Marker written to `Ticket.origin` so a v2 ticket can be traced back. */
|
|
19
|
+
const legacyOrigin = (oldTicketId) => `legacy:${oldTicketId}`;
|
|
20
|
+
exports.legacyOrigin = legacyOrigin;
|
|
21
|
+
/**
|
|
22
|
+
* The ancestor closure of a tag, as v2 stores it.
|
|
23
|
+
*
|
|
24
|
+
* v2 entities store the picked tag *plus every ancestor*, which turns "has tag X"
|
|
25
|
+
* into a flat indexable query that also matches descendants. Writing only the
|
|
26
|
+
* picked tag would silently break every hierarchical filter.
|
|
27
|
+
*/
|
|
28
|
+
function closureOf(tagId, parentOf) {
|
|
29
|
+
const closure = [];
|
|
30
|
+
let cursor = tagId;
|
|
31
|
+
const guard = new Set();
|
|
32
|
+
while (cursor && !guard.has(cursor)) {
|
|
33
|
+
guard.add(cursor);
|
|
34
|
+
closure.push(cursor);
|
|
35
|
+
cursor = parentOf.get(cursor);
|
|
36
|
+
}
|
|
37
|
+
return closure;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Pass 08 — the fold. `OD3_Maintenance_Ticket` (+ ~10 side tables) →
|
|
41
|
+
* `OD3_Service_Ticket`.
|
|
42
|
+
*
|
|
43
|
+
* Two things about the ordering are load-bearing:
|
|
44
|
+
*
|
|
45
|
+
* 1. Tickets are inserted in **ascending `createdAt`**, because insert order
|
|
46
|
+
* becomes numbering order and chronological insertion is what reproduces a
|
|
47
|
+
* sensible sequence. The backend `beforeSave` that normally assigns `number`
|
|
48
|
+
* is not registered in migration mode, so `TicketNumberer` (lib/ticket.ts)
|
|
49
|
+
* hands the numbers out in-process and flushes `Project.ticketCounter` once
|
|
50
|
+
* at the end of this pass.
|
|
51
|
+
* 2. Every side table is loaded **once** and grouped in memory (see `lib/latest`),
|
|
52
|
+
* not queried per ticket.
|
|
53
|
+
*/
|
|
54
|
+
exports.pass08 = {
|
|
55
|
+
id: "08",
|
|
56
|
+
title: "tickets",
|
|
57
|
+
needs: ["01", "02", "03", "04", "05"],
|
|
58
|
+
async run({ config, map, report, entry }) {
|
|
59
|
+
const { batchSize, tenantId } = config;
|
|
60
|
+
// ── Fallback status, per tenant ───────────────────────────────────────────
|
|
61
|
+
// `Ticket.status` is required, so a ticket that never had a kanban row still
|
|
62
|
+
// needs one, and the inbox status is the honest default.
|
|
63
|
+
//
|
|
64
|
+
// Resolved **per tenant**, not once per run. A single fallback was only ever
|
|
65
|
+
// correct when `--tenant` scoped the whole run; on a whole-database run it
|
|
66
|
+
// picked the globally-oldest inbox status and put it on every tenant's
|
|
67
|
+
// unstatused tickets — a cross-tenant reference that the ticket's own tenant
|
|
68
|
+
// could not even read, so the status column rendered empty in the UI.
|
|
69
|
+
const inboxByTenant = new Map();
|
|
70
|
+
for (const status of await (0, pass_js_1.all)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Service_Status).equalTo("category", "inbox"), tenantId), batchSize)) {
|
|
71
|
+
const statusTenantId = (0, pass_js_1.refId)(status.get("tenant"));
|
|
72
|
+
// Oldest wins, matching pass 01's "picks the oldest as the fallback" note.
|
|
73
|
+
if (statusTenantId && status.id && !inboxByTenant.has(statusTenantId)) {
|
|
74
|
+
inboxByTenant.set(statusTenantId, status.id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (inboxByTenant.size === 0 && !config.dryRun) {
|
|
78
|
+
report.error(entry, "no inbox Status exists — every ticket without a kanban row would be " +
|
|
79
|
+
"unwritable. Run pass 01 first, or mark one v1 kanban state as isInbox.");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (inboxByTenant.size === 0) {
|
|
83
|
+
// A dry run against a database where v2 is still empty legitimately has no
|
|
84
|
+
// Status yet — pass 01 wrote nothing. Aborting here would make the full
|
|
85
|
+
// `--dry-run` (the review step this whole design leans on) impossible, so
|
|
86
|
+
// carry on with a placeholder and say so once.
|
|
87
|
+
report.warn(entry, "dry run: no inbox Status exists yet, using a placeholder. Run passes 01-07 " +
|
|
88
|
+
"for real before judging this pass's output.");
|
|
89
|
+
}
|
|
90
|
+
// ── Numbering + role names ───────────────────────────────────────────────
|
|
91
|
+
// beforeSave is not registered in migration mode (it is what would strip the
|
|
92
|
+
// historical createdAt), so the numbering and ACL work that hook normally does
|
|
93
|
+
// happens here instead. See lib/ticket.ts.
|
|
94
|
+
const numberer = new ticket_js_1.TicketNumberer(config);
|
|
95
|
+
await numberer.load();
|
|
96
|
+
const roleNames = await (0, ticket_js_1.loadRoleNames)(config);
|
|
97
|
+
// ── Tag parent map, for closure expansion ────────────────────────────────
|
|
98
|
+
const parentOf = new Map();
|
|
99
|
+
for (const tag of await (0, pass_js_1.all)(() => new node_1.default.Query(index_js_1.Service_Tag), batchSize)) {
|
|
100
|
+
if (tag.id)
|
|
101
|
+
parentOf.set(tag.id, (0, pass_js_1.refId)(tag.get("parent")));
|
|
102
|
+
}
|
|
103
|
+
// ── Side tables, one query each ──────────────────────────────────────────
|
|
104
|
+
const scoped = (cls) => () => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(cls), tenantId);
|
|
105
|
+
const titles = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Ticket_Title), batchSize);
|
|
106
|
+
const priorities = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Priority), batchSize);
|
|
107
|
+
const downtimes = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Downtime), batchSize);
|
|
108
|
+
const frequencies = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Frequency), batchSize);
|
|
109
|
+
const restrictions = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Restriction), batchSize);
|
|
110
|
+
const duedates = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Duedate), batchSize);
|
|
111
|
+
const sources = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Ticket_Source), batchSize);
|
|
112
|
+
const projectRows = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Ticket_Project), batchSize);
|
|
113
|
+
const categories = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Ticket_Issuecategory), batchSize);
|
|
114
|
+
const stateHistory = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Ticket_Kanban_State), batchSize);
|
|
115
|
+
const currentStates = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Ticket_Kanban_State_Current), batchSize);
|
|
116
|
+
const materials = await (0, latest_js_1.groupByTicket)(scoped(index_js_1.Maintenance_Ticket_Material), batchSize);
|
|
117
|
+
// Which kanban states count as finished, for deriving `closedAt`.
|
|
118
|
+
const finishedStateIds = new Set();
|
|
119
|
+
for (const state of await (0, pass_js_1.all)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Maintenance_Kanban_State), tenantId), batchSize)) {
|
|
120
|
+
if (state.id && state.get("isFinished"))
|
|
121
|
+
finishedStateIds.add(state.id);
|
|
122
|
+
}
|
|
123
|
+
// Todo messages become the checklist; comments are pass 10's job.
|
|
124
|
+
const messages = new Map();
|
|
125
|
+
for await (const page of (0, pass_js_1.pages)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Maintenance_Message), tenantId), batchSize)) {
|
|
126
|
+
for (const message of page) {
|
|
127
|
+
const ticketId = message.get("referencedObjectId");
|
|
128
|
+
if (!ticketId)
|
|
129
|
+
continue;
|
|
130
|
+
const bucket = messages.get(ticketId);
|
|
131
|
+
if (bucket)
|
|
132
|
+
bucket.push(message);
|
|
133
|
+
else
|
|
134
|
+
messages.set(ticketId, [message]);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ── Fold, ticket by ticket, oldest first ─────────────────────────────────
|
|
138
|
+
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)) {
|
|
139
|
+
for (const ticket of page) {
|
|
140
|
+
const sourceId = ticket.id;
|
|
141
|
+
if (!sourceId)
|
|
142
|
+
continue;
|
|
143
|
+
const ticketTenantId = (0, pass_js_1.refId)(ticket.get("tenant"));
|
|
144
|
+
if (!ticketTenantId) {
|
|
145
|
+
report.error(entry, `ticket ${sourceId} has no tenant; Ticket.tenant is required — skipped`);
|
|
146
|
+
entry.skipped++;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const existingId = map.get(index_js_1.Maintenance_Ticket.className, sourceId);
|
|
150
|
+
const target = existingId
|
|
151
|
+
? new index_js_1.Service_Ticket({ objectId: existingId })
|
|
152
|
+
: new index_js_1.Service_Ticket();
|
|
153
|
+
// ── title: latest override, else the ticket's own ────────────────────
|
|
154
|
+
const latestTitle = (0, latest_js_1.latestOf)(titles.get(sourceId));
|
|
155
|
+
const title = latestTitle?.get("title") ?? ticket.get("title") ?? `Ticket ${sourceId}`;
|
|
156
|
+
target.set("title", title);
|
|
157
|
+
// ── flat scalars ────────────────────────────────────────────────────
|
|
158
|
+
if (ticket.get("description")) {
|
|
159
|
+
target.set("description", ticket.get("description"));
|
|
160
|
+
}
|
|
161
|
+
// v1 lists only `enabled === true` tickets (see
|
|
162
|
+
// functions/openinc-openservice-ticket-data.ts), so `undefined` means hidden
|
|
163
|
+
// there too. `!== true` reproduces that; `=== false` would resurrect records
|
|
164
|
+
// v1 deliberately hid. Disabled tickets migrate as deleted rather than being
|
|
165
|
+
// skipped, so nothing is lost.
|
|
166
|
+
target.set("deleted", ticket.get("enabled") !== true);
|
|
167
|
+
target.set("tenant", (0, ptr_js_1.tenantPtr)(ticketTenantId));
|
|
168
|
+
target.set("origin", (0, exports.legacyOrigin)(sourceId));
|
|
169
|
+
// Priority is already 0–100 in v1 and v1's five thresholds
|
|
170
|
+
// (20/40/60/80/100) each land in a sensible v2 bucket, so it copies
|
|
171
|
+
// straight across with no rescaling.
|
|
172
|
+
const priority = ticket.get("latestPriorityValue") ??
|
|
173
|
+
(0, latest_js_1.latestOf)(priorities.get(sourceId))?.get("value");
|
|
174
|
+
// A v1 ticket could carry no priority at all. v2 renders an unset
|
|
175
|
+
// priority as "-", which reads as missing data rather than as a value,
|
|
176
|
+
// so it defaults to the bottom bucket (0 = the lowest of v2s four
|
|
177
|
+
// levels) instead of being left absent.
|
|
178
|
+
target.set("priority", typeof priority === "number" ? priority : 0);
|
|
179
|
+
const downtime = (0, latest_js_1.latestOf)(downtimes.get(sourceId))?.get("value");
|
|
180
|
+
if (typeof downtime === "number")
|
|
181
|
+
target.set("downtime", downtime);
|
|
182
|
+
const frequency = (0, latest_js_1.latestOf)(frequencies.get(sourceId))?.get("value");
|
|
183
|
+
if (typeof frequency === "number")
|
|
184
|
+
target.set("frequency", frequency);
|
|
185
|
+
const restriction = (0, latest_js_1.latestOf)(restrictions.get(sourceId))?.get("value");
|
|
186
|
+
if (typeof restriction === "number")
|
|
187
|
+
target.set("restriction", restriction);
|
|
188
|
+
// ── dates ───────────────────────────────────────────────────────────
|
|
189
|
+
const duedate = (0, latest_js_1.latestOf)(duedates.get(sourceId));
|
|
190
|
+
if (duedate?.get("end"))
|
|
191
|
+
target.set("dueDate", duedate.get("end"));
|
|
192
|
+
if (duedate?.get("start")) {
|
|
193
|
+
target.set("startDate", duedate.get("start"));
|
|
194
|
+
}
|
|
195
|
+
else if (ticket.get("displayAt")) {
|
|
196
|
+
// D5: displayAt meant "hide until"; startDate is the closest v2 field,
|
|
197
|
+
// and only used where a real start date is absent.
|
|
198
|
+
target.set("startDate", ticket.get("displayAt"));
|
|
199
|
+
}
|
|
200
|
+
// ── status (required) ───────────────────────────────────────────────
|
|
201
|
+
const currentStateRow = (0, latest_js_1.latestOf)(currentStates.get(sourceId));
|
|
202
|
+
const historicStateRow = (0, latest_js_1.latestOf)(stateHistory.get(sourceId));
|
|
203
|
+
const v1StateId = (0, pass_js_1.refId)(currentStateRow?.get("state")) ?? (0, pass_js_1.refId)(historicStateRow?.get("state"));
|
|
204
|
+
const statusId = v1StateId
|
|
205
|
+
? map.get(index_js_1.Maintenance_Kanban_State.className, v1StateId)
|
|
206
|
+
: undefined;
|
|
207
|
+
// The fallback must belong to *this ticket's* tenant.
|
|
208
|
+
const fallbackStatusId = inboxByTenant.get(ticketTenantId) ??
|
|
209
|
+
map_js_1.MigrationMap.dryId(`inbox-status-${ticketTenantId}`);
|
|
210
|
+
if (!statusId && !inboxByTenant.has(ticketTenantId) && !config.dryRun) {
|
|
211
|
+
report.error(entry, `ticket ${sourceId} needs the inbox fallback but tenant ${ticketTenantId} ` +
|
|
212
|
+
`has no inbox Status — mark one of its v1 kanban states as isInbox and ` +
|
|
213
|
+
`re-run passes 01 and 08`);
|
|
214
|
+
entry.skipped++;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
target.set("status", (0, ptr_js_1.maybePtr)(index_js_1.Service_Status.className, statusId ?? fallbackStatusId));
|
|
218
|
+
if (v1StateId && !statusId) {
|
|
219
|
+
report.warn(entry, `ticket ${sourceId} referenced kanban state ${v1StateId}, which has no ` +
|
|
220
|
+
`Status — fell back to inbox`);
|
|
221
|
+
}
|
|
222
|
+
// ── closedAt, derived from the state history ────────────────────────
|
|
223
|
+
// v1 has no closedAt: the closest truth is when the ticket last entered a
|
|
224
|
+
// finished state.
|
|
225
|
+
const finishedEntry = (stateHistory.get(sourceId) ?? [])
|
|
226
|
+
.filter((row) => {
|
|
227
|
+
const id = (0, pass_js_1.refId)(row.get("state"));
|
|
228
|
+
return id ? finishedStateIds.has(id) : false;
|
|
229
|
+
})
|
|
230
|
+
.sort((a, b) => (a.updatedAt?.getTime() ?? 0) - (b.updatedAt?.getTime() ?? 0))
|
|
231
|
+
.pop();
|
|
232
|
+
if (finishedEntry?.updatedAt) {
|
|
233
|
+
target.set("closedAt", finishedEntry.updatedAt);
|
|
234
|
+
}
|
|
235
|
+
// ── equipment, project, tags ────────────────────────────────────────
|
|
236
|
+
const v1SourceId = (0, pass_js_1.refId)((0, latest_js_1.latestOf)(sources.get(sourceId))?.get("source"));
|
|
237
|
+
const equipmentId = v1SourceId
|
|
238
|
+
? map.get(index_js_1.Source.className, v1SourceId)
|
|
239
|
+
: undefined;
|
|
240
|
+
const equipmentPtr = (0, ptr_js_1.maybePtr)(index_js_1.Service_Equipment.className, equipmentId);
|
|
241
|
+
if (equipmentPtr)
|
|
242
|
+
target.set("equipment", equipmentPtr);
|
|
243
|
+
else if (v1SourceId) {
|
|
244
|
+
report.warn(entry, `ticket ${sourceId} referenced source ${v1SourceId}, which has no Equipment`);
|
|
245
|
+
}
|
|
246
|
+
// v1 wrote both the pointer and a side-table row; the pointer wins.
|
|
247
|
+
const v1ProjectId = (0, pass_js_1.refId)(ticket.get("project")) ??
|
|
248
|
+
(0, pass_js_1.refId)((0, latest_js_1.latestOf)(projectRows.get(sourceId))?.get("project"));
|
|
249
|
+
const projectId = v1ProjectId
|
|
250
|
+
? map.get(index_js_1.Maintenance_Project.className, v1ProjectId)
|
|
251
|
+
: undefined;
|
|
252
|
+
const projectPtr = (0, ptr_js_1.maybePtr)(index_js_1.Service_Project.className, projectId);
|
|
253
|
+
if (projectPtr)
|
|
254
|
+
target.set("project", projectPtr);
|
|
255
|
+
const v1CategoryId = (0, pass_js_1.refId)((0, latest_js_1.latestOf)(categories.get(sourceId))?.get("issuecategory"));
|
|
256
|
+
const tagId = v1CategoryId
|
|
257
|
+
? map.get(index_js_1.Maintenance_Issuecategory.className, v1CategoryId)
|
|
258
|
+
: undefined;
|
|
259
|
+
if (tagId)
|
|
260
|
+
target.set("tags", closureOf(tagId, parentOf));
|
|
261
|
+
// ── assignees ───────────────────────────────────────────────────────
|
|
262
|
+
// v2 stores arrays of plain ids; the assignment *history* is pass 12's job.
|
|
263
|
+
const [userIds, roleIds] = await Promise.all([
|
|
264
|
+
(0, latest_js_1.relationIds)(ticket, "assignedusers"),
|
|
265
|
+
(0, latest_js_1.relationIds)(ticket, "assignedroles"),
|
|
266
|
+
]);
|
|
267
|
+
if (userIds.length)
|
|
268
|
+
target.set("assignedUsers", userIds);
|
|
269
|
+
if (roleIds.length)
|
|
270
|
+
target.set("assignedRoles", roleIds);
|
|
271
|
+
const creatorId = (0, pass_js_1.refId)(ticket.get("user"));
|
|
272
|
+
if (creatorId)
|
|
273
|
+
target.set("createdBy", (0, ptr_js_1.userPtr)(creatorId));
|
|
274
|
+
// ── checklist, from todo messages ───────────────────────────────────
|
|
275
|
+
// `isTodoDone` reads `data.todoCompleted` (v1's actual field) and
|
|
276
|
+
// `checklistLabel` skips v1's i18n-key placeholder titles — see lib/message.
|
|
277
|
+
const checklist = (messages.get(sourceId) ?? [])
|
|
278
|
+
.filter((m) => (0, message_js_1.isTodo)(m))
|
|
279
|
+
.sort((a, b) => (a.createdAt?.getTime() ?? 0) - (b.createdAt?.getTime() ?? 0))
|
|
280
|
+
.map((m) => ({ label: (0, message_js_1.checklistLabel)(m), done: (0, message_js_1.isTodoDone)(m) }))
|
|
281
|
+
.filter((item) => item.label !== undefined);
|
|
282
|
+
if (checklist.length)
|
|
283
|
+
target.set("checklist", checklist);
|
|
284
|
+
// ── material, from the newest Ticket_Material row ───────────────────
|
|
285
|
+
// Each v1 row holds the whole list, so the fold is the latest row
|
|
286
|
+
// rather than a merge. Shared with pass 12 via lib/material.
|
|
287
|
+
const materialRow = (0, latest_js_1.latestOf)(materials.get(sourceId));
|
|
288
|
+
if (materialRow) {
|
|
289
|
+
const { entries, unresolved } = (0, material_js_1.toMaterial)(materialRow.get("itemList"), map);
|
|
290
|
+
if (entries.length)
|
|
291
|
+
target.set("material", entries);
|
|
292
|
+
if (unresolved.length) {
|
|
293
|
+
report.warn(entry, `ticket ${sourceId} material references item(s) ` +
|
|
294
|
+
`${unresolved.join(", ")} with no migrated Article — kept as ` +
|
|
295
|
+
`free-text line(s)`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!existingId) {
|
|
299
|
+
// Numbering: normally the backend hook's atomic increment. Safe to do
|
|
300
|
+
// in-process because the migration is the only writer (lib/ticket.ts).
|
|
301
|
+
const number = numberer.next(projectId, ticketTenantId);
|
|
302
|
+
if (number !== undefined)
|
|
303
|
+
target.set("number", number);
|
|
304
|
+
else {
|
|
305
|
+
report.warn(entry, `ticket ${sourceId} has neither project nor tenant scope — left unnumbered`);
|
|
306
|
+
}
|
|
307
|
+
// The full ticket ACL, including what `setTicketACL` would have added.
|
|
308
|
+
target.setACL((0, ticket_js_1.ticketAcl)({
|
|
309
|
+
tenantId: ticketTenantId,
|
|
310
|
+
creatorId,
|
|
311
|
+
assignedUserIds: userIds,
|
|
312
|
+
assignedRoleIds: roleIds,
|
|
313
|
+
roleNames,
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
if (config.dryRun) {
|
|
317
|
+
existingId ? entry.updated++ : entry.created++;
|
|
318
|
+
// Passes 09-12 are all keyed by the *new* ticket id. Without a
|
|
319
|
+
// placeholder a full dry run would report an error per ticket in each of
|
|
320
|
+
// them, drowning the findings the dry run is for.
|
|
321
|
+
if (!existingId) {
|
|
322
|
+
await map.record(index_js_1.Maintenance_Ticket.className, sourceId, index_js_1.Service_Ticket.className, map_js_1.MigrationMap.dryId(sourceId));
|
|
323
|
+
}
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
const newId = await (0, acl_js_1.saveMigrated)(config, target, {
|
|
328
|
+
className: index_js_1.Service_Ticket.className,
|
|
329
|
+
isNew: !existingId,
|
|
330
|
+
createdAt: ticket.createdAt,
|
|
331
|
+
updatedAt: ticket.updatedAt,
|
|
332
|
+
});
|
|
333
|
+
if (existingId) {
|
|
334
|
+
entry.updated++;
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
entry.created++;
|
|
338
|
+
await map.record(index_js_1.Maintenance_Ticket.className, sourceId, index_js_1.Service_Ticket.className, newId);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
report.error(entry, `ticket ${sourceId} failed: ${error.message}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Always flush, even after errors: counters that lag behind the numbers already
|
|
347
|
+
// handed out would make the next run reuse them and produce duplicates.
|
|
348
|
+
if (!config.dryRun) {
|
|
349
|
+
const flushed = await numberer.flush();
|
|
350
|
+
report.info(entry, `counters updated: ${flushed.projects} project(s), ${flushed.tenants} tenant counter(s)`);
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type Pass } from "../lib/pass.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pass 09 — per-ticket meta values → `OD3_Meta_Entry`.
|
|
4
|
+
*
|
|
5
|
+
* v1 kept the field definition and its value together in
|
|
6
|
+
* `Maintenance_Ticket.meta = { fields: [{ name, type, label, …, value }] }`. v2
|
|
7
|
+
* separates them: pass 07 wrote the shared definition list, this pass writes each
|
|
8
|
+
* ticket's values.
|
|
9
|
+
*
|
|
10
|
+
* `entryObjectId` must be the **new** ticket id, so this depends on pass 08.
|
|
11
|
+
*/
|
|
12
|
+
export declare const pass09: Pass;
|
|
@@ -0,0 +1,136 @@
|
|
|
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.pass09 = 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 ptr_js_1 = require("../lib/ptr.js");
|
|
11
|
+
const pass_js_1 = require("../lib/pass.js");
|
|
12
|
+
const _07_meta_configs_js_1 = require("./07-meta-configs.js");
|
|
13
|
+
/** Addressed by constant so the REST create path gets the right class name. */
|
|
14
|
+
const MetaEntryClass = index_js_1.Meta_Entry.className;
|
|
15
|
+
/**
|
|
16
|
+
* Pass 09 — per-ticket meta values → `OD3_Meta_Entry`.
|
|
17
|
+
*
|
|
18
|
+
* v1 kept the field definition and its value together in
|
|
19
|
+
* `Maintenance_Ticket.meta = { fields: [{ name, type, label, …, value }] }`. v2
|
|
20
|
+
* separates them: pass 07 wrote the shared definition list, this pass writes each
|
|
21
|
+
* ticket's values.
|
|
22
|
+
*
|
|
23
|
+
* `entryObjectId` must be the **new** ticket id, so this depends on pass 08.
|
|
24
|
+
*/
|
|
25
|
+
exports.pass09 = {
|
|
26
|
+
id: "09",
|
|
27
|
+
title: "ticket-meta",
|
|
28
|
+
needs: ["07", "08"],
|
|
29
|
+
async run({ config, map, report, entry }) {
|
|
30
|
+
const contextString = (0, ptr_js_1.contextToString)(_07_meta_configs_js_1.TICKET_META_CONTEXT);
|
|
31
|
+
// Pass 07 writes one Meta_Config **per tenant**, so `origin` has to resolve
|
|
32
|
+
// per tenant too — pointing every entry at whichever config happened to come
|
|
33
|
+
// back first would cross-link tenants. Read with the maintenance key, which
|
|
34
|
+
// bypasses the ACLs that normally do this scoping.
|
|
35
|
+
const configByTenant = new Map();
|
|
36
|
+
for await (const page of (0, pass_js_1.pages)(() => new node_1.default.Query(index_js_1.Meta_Config).equalTo("context", contextString), config.batchSize)) {
|
|
37
|
+
for (const row of page) {
|
|
38
|
+
if (row.id)
|
|
39
|
+
configByTenant.set((0, pass_js_1.refId)(row.get("tenant")) ?? "", row.id);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (configByTenant.size === 0) {
|
|
43
|
+
report.warn(entry, "no Meta_Config for the ticket context — entries will be written without " +
|
|
44
|
+
"an `origin` (run pass 07 first)");
|
|
45
|
+
}
|
|
46
|
+
// Existing entries, so a re-run updates rather than duplicates. Keyed by the
|
|
47
|
+
// ticket the entry belongs to.
|
|
48
|
+
const existingByTicket = new Map();
|
|
49
|
+
for await (const page of (0, pass_js_1.pages)(() => new node_1.default.Query(index_js_1.Meta_Entry)
|
|
50
|
+
.equalTo("context", contextString)
|
|
51
|
+
.equalTo("entryClassName", index_js_1.Service_Ticket.className), config.batchSize)) {
|
|
52
|
+
for (const row of page) {
|
|
53
|
+
const objectId = row.get("entryObjectId");
|
|
54
|
+
if (objectId)
|
|
55
|
+
existingByTicket.set(objectId, row);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
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), config.tenantId).exists("meta"), config.batchSize)) {
|
|
59
|
+
for (const ticket of page) {
|
|
60
|
+
const sourceId = ticket.id;
|
|
61
|
+
if (!sourceId)
|
|
62
|
+
continue;
|
|
63
|
+
const meta = ticket.get("meta");
|
|
64
|
+
const fields = meta?.fields ?? [];
|
|
65
|
+
if (fields.length === 0)
|
|
66
|
+
continue;
|
|
67
|
+
const newTicketId = map.get(index_js_1.Maintenance_Ticket.className, sourceId);
|
|
68
|
+
if (!newTicketId) {
|
|
69
|
+
report.error(entry, `ticket ${sourceId} has meta but no migrated ticket — run pass 08 first`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// Values only; the definitions are the shared config's job. A field whose
|
|
73
|
+
// value was never set is skipped rather than written as null.
|
|
74
|
+
const values = {};
|
|
75
|
+
const snapshot = [];
|
|
76
|
+
for (const field of fields) {
|
|
77
|
+
const converted = (0, _07_meta_configs_js_1.toMetaFieldConfig)(field);
|
|
78
|
+
if (!converted)
|
|
79
|
+
continue;
|
|
80
|
+
snapshot.push(converted);
|
|
81
|
+
if (field.value !== undefined && field.value !== null) {
|
|
82
|
+
values[converted.name] = field.value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (Object.keys(values).length === 0) {
|
|
86
|
+
entry.skipped++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const existing = existingByTicket.get(newTicketId);
|
|
90
|
+
const target = existing ?? new index_js_1.Meta_Entry();
|
|
91
|
+
const ticketTenantId = (0, pass_js_1.refId)(ticket.get("tenant"));
|
|
92
|
+
target.set("context", contextString);
|
|
93
|
+
target.set("entryClassName", index_js_1.Service_Ticket.className);
|
|
94
|
+
target.set("entryObjectId", newTicketId);
|
|
95
|
+
target.set("values", values);
|
|
96
|
+
// The meta plugin stores a config snapshot alongside the values so an entry
|
|
97
|
+
// still renders after the shared config changes. Mirror that.
|
|
98
|
+
target.set("config", { fields: snapshot });
|
|
99
|
+
const originPtr = (0, ptr_js_1.maybePtr)(index_js_1.Meta_Config.className, configByTenant.get(ticketTenantId ?? "") ?? configByTenant.get(""));
|
|
100
|
+
if (originPtr)
|
|
101
|
+
target.set("origin", originPtr);
|
|
102
|
+
else if (configByTenant.size > 0) {
|
|
103
|
+
report.warn(entry, `ticket ${sourceId} (tenant ${ticketTenantId ?? "none"}) has no ` +
|
|
104
|
+
`Meta_Config for its tenant — entry written without an origin`);
|
|
105
|
+
}
|
|
106
|
+
if (ticketTenantId) {
|
|
107
|
+
target.set("tenant", (0, ptr_js_1.tenantPtr)(ticketTenantId));
|
|
108
|
+
}
|
|
109
|
+
if (!existing) {
|
|
110
|
+
target.setACL((0, acl_js_1.migrationAcl)({
|
|
111
|
+
tenantId: ticketTenantId,
|
|
112
|
+
allowTenantUserWrite: true,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
if (config.dryRun) {
|
|
116
|
+
existing ? entry.updated++ : entry.created++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
// The entry is as old as the ticket it describes — v1 kept no separate
|
|
121
|
+
// timestamp for it.
|
|
122
|
+
await (0, acl_js_1.saveMigrated)(config, target, {
|
|
123
|
+
className: MetaEntryClass,
|
|
124
|
+
isNew: !existing,
|
|
125
|
+
createdAt: ticket.createdAt,
|
|
126
|
+
updatedAt: ticket.updatedAt,
|
|
127
|
+
});
|
|
128
|
+
existing ? entry.updated++ : entry.created++;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
report.error(entry, `meta entry for ticket ${sourceId} failed: ${error.message}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Pass } from "../lib/pass.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pass 10 — v1 comments → `OD3_Chat_Message`, with their media as attachments.
|
|
4
|
+
*
|
|
5
|
+
* `Maintenance_Message` splits two ways: rows whose `data.type` is `"todo"` became
|
|
6
|
+
* `Ticket.checklist` in pass 08; everything else is a comment and lands here.
|
|
7
|
+
*
|
|
8
|
+
* Attachments: `Maintenance_Media` rows are bare Parse Files with no `OD3_Assets`
|
|
9
|
+
* row behind them. Rather than leaving them as URL-only attachments, this pass
|
|
10
|
+
* creates an `OD3_Assets` row per file in the ticket's shared context, so a legacy
|
|
11
|
+
* comment attachment also shows up in the detail page's asset library — the same
|
|
12
|
+
* place pass 11 puts the form-uploaded files. One destination regardless of which
|
|
13
|
+
* v1 store held the file.
|
|
14
|
+
*
|
|
15
|
+
* Ordering: top-level messages are written before replies, so `parent` resolves.
|
|
16
|
+
*/
|
|
17
|
+
export declare const pass10: Pass;
|