@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.
Files changed (163) hide show
  1. package/dist/featuremap.json +27 -12
  2. package/dist/features/config/helper/customOptions.js +9 -0
  3. package/dist/features/config/helper/isFeatureEnabled.d.ts +1 -1
  4. package/dist/features/config/helper/isFeatureEnabled.js +2 -0
  5. package/dist/features/meta/index.d.ts +1 -0
  6. package/dist/features/meta/index.js +5 -0
  7. package/dist/features/meta/services/registerMetaContext.d.ts +29 -0
  8. package/dist/features/meta/services/registerMetaContext.js +208 -0
  9. package/dist/features/permissions/types/Permissions.d.ts +24 -1
  10. package/dist/features/permissions/types/Permissions.js +34 -0
  11. package/dist/features/ruleset/applyRuleSet.d.ts +41 -0
  12. package/dist/features/ruleset/applyRuleSet.js +81 -0
  13. package/dist/features/schema/helper/defaultHandler.js +13 -0
  14. package/dist/features/schema/helper/migrationMode.d.ts +26 -0
  15. package/dist/features/schema/helper/migrationMode.js +48 -0
  16. package/dist/features/schema/index.d.ts +1 -0
  17. package/dist/features/schema/index.js +3 -1
  18. package/dist/features/schema/services/beforeSaveHook.js +21 -0
  19. package/dist/features/translations/services/initTranslation.js +1 -1
  20. package/dist/hooks/Chat_Message.d.ts +1 -0
  21. package/dist/hooks/Chat_Message.js +179 -0
  22. package/dist/hooks/Meta_Config.js +19 -10
  23. package/dist/hooks/Meta_Context.d.ts +6 -0
  24. package/dist/hooks/Meta_Context.js +16 -0
  25. package/dist/hooks/Meta_Entry.js +20 -10
  26. package/dist/hooks/Service_Equipment.d.ts +1 -0
  27. package/dist/hooks/Service_Equipment.js +38 -0
  28. package/dist/hooks/Service_FormConfig.d.ts +1 -0
  29. package/dist/hooks/Service_FormConfig.js +17 -0
  30. package/dist/hooks/Service_Project.d.ts +1 -0
  31. package/dist/hooks/Service_Project.js +17 -0
  32. package/dist/hooks/Service_Schedule.d.ts +1 -0
  33. package/dist/hooks/Service_Schedule.js +24 -0
  34. package/dist/hooks/Service_Schedule_Execution.d.ts +1 -0
  35. package/dist/hooks/Service_Schedule_Execution.js +17 -0
  36. package/dist/hooks/Service_Schedule_Template.d.ts +1 -0
  37. package/dist/hooks/Service_Schedule_Template.js +17 -0
  38. package/dist/hooks/Service_Status.d.ts +1 -0
  39. package/dist/hooks/Service_Status.js +32 -0
  40. package/dist/hooks/Service_Tag.d.ts +1 -0
  41. package/dist/hooks/Service_Tag.js +168 -0
  42. package/dist/hooks/Service_Ticket.d.ts +1 -0
  43. package/dist/hooks/Service_Ticket.js +433 -0
  44. package/dist/hooks/Service_TriggerState.d.ts +1 -0
  45. package/dist/hooks/Service_TriggerState.js +17 -0
  46. package/dist/hooks/_ChangeLog.js +27 -6
  47. package/dist/i18n/de-DE/chat.json +18 -0
  48. package/dist/i18n/de-DE/openservice.json +14 -0
  49. package/dist/i18n/en-US/chat.json +18 -0
  50. package/dist/i18n/en-US/openservice.json +14 -0
  51. package/dist/scripts/backfill-meta-contexts.d.ts +1 -0
  52. package/dist/scripts/backfill-meta-contexts.js +100 -0
  53. package/dist/scripts/migration/lib/acl.d.ts +52 -0
  54. package/dist/scripts/migration/lib/acl.js +79 -0
  55. package/dist/scripts/migration/lib/articleMeta.d.ts +47 -0
  56. package/dist/scripts/migration/lib/articleMeta.js +132 -0
  57. package/dist/scripts/migration/lib/env.d.ts +56 -0
  58. package/dist/scripts/migration/lib/env.js +106 -0
  59. package/dist/scripts/migration/lib/latest.d.ts +47 -0
  60. package/dist/scripts/migration/lib/latest.js +115 -0
  61. package/dist/scripts/migration/lib/map.d.ts +53 -0
  62. package/dist/scripts/migration/lib/map.js +113 -0
  63. package/dist/scripts/migration/lib/material.d.ts +47 -0
  64. package/dist/scripts/migration/lib/material.js +51 -0
  65. package/dist/scripts/migration/lib/message.d.ts +78 -0
  66. package/dist/scripts/migration/lib/message.js +115 -0
  67. package/dist/scripts/migration/lib/pass.d.ts +67 -0
  68. package/dist/scripts/migration/lib/pass.js +89 -0
  69. package/dist/scripts/migration/lib/ptr.d.ts +32 -0
  70. package/dist/scripts/migration/lib/ptr.js +38 -0
  71. package/dist/scripts/migration/lib/report.d.ts +41 -0
  72. package/dist/scripts/migration/lib/report.js +78 -0
  73. package/dist/scripts/migration/lib/rest.d.ts +47 -0
  74. package/dist/scripts/migration/lib/rest.js +68 -0
  75. package/dist/scripts/migration/lib/ticket.d.ts +72 -0
  76. package/dist/scripts/migration/lib/ticket.js +192 -0
  77. package/dist/scripts/migration/passes/01-statuses.d.ts +12 -0
  78. package/dist/scripts/migration/passes/01-statuses.js +122 -0
  79. package/dist/scripts/migration/passes/02-tags.d.ts +13 -0
  80. package/dist/scripts/migration/passes/02-tags.js +150 -0
  81. package/dist/scripts/migration/passes/03-equipment.d.ts +14 -0
  82. package/dist/scripts/migration/passes/03-equipment.js +176 -0
  83. package/dist/scripts/migration/passes/04-projects.d.ts +27 -0
  84. package/dist/scripts/migration/passes/04-projects.js +241 -0
  85. package/dist/scripts/migration/passes/05-articles.d.ts +25 -0
  86. package/dist/scripts/migration/passes/05-articles.js +203 -0
  87. package/dist/scripts/migration/passes/06-formconfigs.d.ts +17 -0
  88. package/dist/scripts/migration/passes/06-formconfigs.js +166 -0
  89. package/dist/scripts/migration/passes/07-meta-configs.d.ts +46 -0
  90. package/dist/scripts/migration/passes/07-meta-configs.js +185 -0
  91. package/dist/scripts/migration/passes/08-tickets.d.ts +27 -0
  92. package/dist/scripts/migration/passes/08-tickets.js +353 -0
  93. package/dist/scripts/migration/passes/09-ticket-meta.d.ts +12 -0
  94. package/dist/scripts/migration/passes/09-ticket-meta.js +136 -0
  95. package/dist/scripts/migration/passes/10-chat.d.ts +17 -0
  96. package/dist/scripts/migration/passes/10-chat.js +230 -0
  97. package/dist/scripts/migration/passes/11-assets.d.ts +17 -0
  98. package/dist/scripts/migration/passes/11-assets.js +93 -0
  99. package/dist/scripts/migration/passes/12-history.d.ts +24 -0
  100. package/dist/scripts/migration/passes/12-history.js +332 -0
  101. package/dist/scripts/migration/run.d.ts +1 -0
  102. package/dist/scripts/migration/run.js +148 -0
  103. package/dist/scripts/migration/verify.d.ts +1 -0
  104. package/dist/scripts/migration/verify.js +620 -0
  105. package/dist/scripts/seed-v1-testdata.d.ts +41 -0
  106. package/dist/scripts/seed-v1-testdata.js +543 -0
  107. package/dist/types/BDE_Article.d.ts +3 -0
  108. package/dist/types/BDE_Article.js +6 -0
  109. package/dist/types/Chat_Message.d.ts +49 -0
  110. package/dist/types/Chat_Message.js +87 -0
  111. package/dist/types/MES_RessourceOverview.d.ts +38 -0
  112. package/dist/types/MES_RessourceOverview.js +63 -0
  113. package/dist/types/Meta_Context.d.ts +43 -0
  114. package/dist/types/Meta_Context.js +75 -0
  115. package/dist/types/Service_Equipment.d.ts +33 -0
  116. package/dist/types/Service_Equipment.js +57 -0
  117. package/dist/types/Service_FormConfig.d.ts +33 -0
  118. package/dist/types/Service_FormConfig.js +57 -0
  119. package/dist/types/Service_MeterReading.d.ts +35 -0
  120. package/dist/types/Service_MeterReading.js +57 -0
  121. package/dist/types/Service_Project.d.ts +53 -0
  122. package/dist/types/Service_Project.js +93 -0
  123. package/dist/types/Service_Schedule.d.ts +66 -0
  124. package/dist/types/Service_Schedule.js +117 -0
  125. package/dist/types/Service_Schedule_Execution.d.ts +38 -0
  126. package/dist/types/Service_Schedule_Execution.js +63 -0
  127. package/dist/types/Service_Schedule_Template.d.ts +36 -0
  128. package/dist/types/Service_Schedule_Template.js +60 -0
  129. package/dist/types/Service_Status.d.ts +30 -0
  130. package/dist/types/Service_Status.js +51 -0
  131. package/dist/types/Service_Tag.d.ts +30 -0
  132. package/dist/types/Service_Tag.js +51 -0
  133. package/dist/types/Service_Ticket.d.ts +92 -0
  134. package/dist/types/Service_Ticket.js +165 -0
  135. package/dist/types/Service_TicketCounter.d.ts +18 -0
  136. package/dist/types/Service_TicketCounter.js +27 -0
  137. package/dist/types/Service_TriggerState.d.ts +31 -0
  138. package/dist/types/Service_TriggerState.js +51 -0
  139. package/dist/types/_Session.d.ts +0 -1
  140. package/dist/types/_User.d.ts +2 -6
  141. package/dist/types/custom/MetaField.d.ts +9 -0
  142. package/dist/types/custom/TicketMaterial.d.ts +32 -0
  143. package/dist/types/custom/TicketMaterial.js +2 -0
  144. package/dist/types/index.d.ts +30 -0
  145. package/dist/types/index.js +33 -3
  146. package/package.json +6 -3
  147. package/schema/BDE_Article.json +3 -0
  148. package/schema/Changelog.json +3 -6
  149. package/schema/Chat_Message.json +88 -0
  150. package/schema/Meta_Context.json +78 -0
  151. package/schema/Service_Equipment.json +55 -0
  152. package/schema/Service_FormConfig.json +66 -0
  153. package/schema/Service_MeterReading.json +54 -0
  154. package/schema/Service_MigrationMap.json +55 -0
  155. package/schema/Service_Project.json +77 -0
  156. package/schema/Service_Schedule.json +101 -0
  157. package/schema/Service_Schedule_Execution.json +58 -0
  158. package/schema/Service_Schedule_Template.json +73 -0
  159. package/schema/Service_Status.json +50 -0
  160. package/schema/Service_Tag.json +50 -0
  161. package/schema/Service_Ticket.json +114 -0
  162. package/schema/Service_TicketCounter.json +38 -0
  163. 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;