@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,47 @@
1
+ /**
2
+ * Reading v1's side tables without the N+1.
3
+ *
4
+ * v1 keeps each ticket attribute in its own class as append-only rows, and
5
+ * "current" means the newest row for that ticket. The v1 cloud function
6
+ * (`functions/openinc-openservice-ticket-data.ts`) reads that with one point query
7
+ * per ticket per attribute — roughly ten round-trips per ticket, so ~100k for a
8
+ * 10k-ticket tenant.
9
+ *
10
+ * These helpers load a whole side table in pages once, then group in memory.
11
+ * ~10k rows of a handful of fields is nothing to hold, and it turns the read cost
12
+ * from per-ticket into per-table.
13
+ */
14
+ /** All rows of a side table, grouped by the ticket they belong to. */
15
+ export declare function groupByTicket<T extends Parse.Object>(build: () => Parse.Query<T>, batchSize: number, ticketField?: string): Promise<Map<string, T[]>>;
16
+ /**
17
+ * When a side-table row took effect.
18
+ *
19
+ * v1 resolves "current" with `descending("updatedAt")`, so `updatedAt` — not
20
+ * `createdAt` — is the clock that decides which row wins. A row edited in place
21
+ * beats an older-created row under v1's rules.
22
+ *
23
+ * **The fold (pass 08) and the replay (pass 12) must use this same function.** If
24
+ * the fold picked by `updatedAt` while the replay ordered by `createdAt`, the two
25
+ * would disagree whenever any row was edited in place: the replay's final snapshot
26
+ * would end on a different row than the ticket carries, breaking the
27
+ * replay-reproduces-the-ticket invariant for a reason that is not a real bug.
28
+ */
29
+ export declare function effectiveAt(row: Parse.Object): Date | undefined;
30
+ /** The newest row per ticket, by {@link effectiveAt}. */
31
+ export declare function latestOf<T extends Parse.Object>(rows: T[] | undefined): T | undefined;
32
+ /** Chronological order by {@link effectiveAt} — the sequence the replay walks. */
33
+ export declare function chronological<T extends Parse.Object>(rows: T[]): T[];
34
+ /**
35
+ * Read a Parse Relation completely.
36
+ *
37
+ * Relations cannot be included in a query, so this is unavoidably one round trip
38
+ * per owner (plus one more to confirm the end). Only used where the relation is
39
+ * small and per-owner — assignees, an item's sources, a form config's roles.
40
+ *
41
+ * Pages rather than taking a single `limit(1000)`: a fixed cap would drop the
42
+ * 1001st assignee or source with nothing in the log to say so, which is precisely
43
+ * the silent-loss failure mode this migration is built to avoid.
44
+ */
45
+ export declare function relationObjects(owner: Parse.Object, field: string, batchSize?: number): Promise<Parse.Object[]>;
46
+ /** The objectIds of a relation, read completely. */
47
+ export declare function relationIds(owner: Parse.Object, field: string, batchSize?: number): Promise<string[]>;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.groupByTicket = groupByTicket;
4
+ exports.effectiveAt = effectiveAt;
5
+ exports.latestOf = latestOf;
6
+ exports.chronological = chronological;
7
+ exports.relationObjects = relationObjects;
8
+ exports.relationIds = relationIds;
9
+ const env_js_1 = require("./env.js");
10
+ const pass_js_1 = require("./pass.js");
11
+ /**
12
+ * Reading v1's side tables without the N+1.
13
+ *
14
+ * v1 keeps each ticket attribute in its own class as append-only rows, and
15
+ * "current" means the newest row for that ticket. The v1 cloud function
16
+ * (`functions/openinc-openservice-ticket-data.ts`) reads that with one point query
17
+ * per ticket per attribute — roughly ten round-trips per ticket, so ~100k for a
18
+ * 10k-ticket tenant.
19
+ *
20
+ * These helpers load a whole side table in pages once, then group in memory.
21
+ * ~10k rows of a handful of fields is nothing to hold, and it turns the read cost
22
+ * from per-ticket into per-table.
23
+ */
24
+ /** All rows of a side table, grouped by the ticket they belong to. */
25
+ async function groupByTicket(build, batchSize, ticketField = "ticket") {
26
+ const byTicket = new Map();
27
+ let skip = 0;
28
+ for (;;) {
29
+ const page = await build()
30
+ .limit(batchSize)
31
+ .skip(skip)
32
+ // Ascending, so each ticket's bucket comes out in chronological order —
33
+ // which is exactly what the history replay (pass 12) needs, and the last
34
+ // element is what the fold (pass 08) needs.
35
+ .ascending("createdAt")
36
+ .find(env_js_1.MK);
37
+ if (page.length === 0)
38
+ break;
39
+ for (const row of page) {
40
+ const ticketId = (0, pass_js_1.refId)(row.get(ticketField));
41
+ if (!ticketId)
42
+ continue;
43
+ const bucket = byTicket.get(ticketId);
44
+ if (bucket)
45
+ bucket.push(row);
46
+ else
47
+ byTicket.set(ticketId, [row]);
48
+ }
49
+ // Stop only on an empty page — a short page does not mean the end. See the
50
+ // paging note in lib/pass.ts.
51
+ skip += page.length;
52
+ }
53
+ return byTicket;
54
+ }
55
+ /**
56
+ * When a side-table row took effect.
57
+ *
58
+ * v1 resolves "current" with `descending("updatedAt")`, so `updatedAt` — not
59
+ * `createdAt` — is the clock that decides which row wins. A row edited in place
60
+ * beats an older-created row under v1's rules.
61
+ *
62
+ * **The fold (pass 08) and the replay (pass 12) must use this same function.** If
63
+ * the fold picked by `updatedAt` while the replay ordered by `createdAt`, the two
64
+ * would disagree whenever any row was edited in place: the replay's final snapshot
65
+ * would end on a different row than the ticket carries, breaking the
66
+ * replay-reproduces-the-ticket invariant for a reason that is not a real bug.
67
+ */
68
+ function effectiveAt(row) {
69
+ return row.updatedAt ?? row.createdAt;
70
+ }
71
+ /** The newest row per ticket, by {@link effectiveAt}. */
72
+ function latestOf(rows) {
73
+ if (!rows || rows.length === 0)
74
+ return undefined;
75
+ return rows.reduce((best, row) => {
76
+ const a = effectiveAt(row)?.getTime() ?? 0;
77
+ const b = effectiveAt(best)?.getTime() ?? 0;
78
+ return a > b ? row : best;
79
+ });
80
+ }
81
+ /** Chronological order by {@link effectiveAt} — the sequence the replay walks. */
82
+ function chronological(rows) {
83
+ return [...rows].sort((a, b) => (effectiveAt(a)?.getTime() ?? 0) - (effectiveAt(b)?.getTime() ?? 0));
84
+ }
85
+ /**
86
+ * Read a Parse Relation completely.
87
+ *
88
+ * Relations cannot be included in a query, so this is unavoidably one round trip
89
+ * per owner (plus one more to confirm the end). Only used where the relation is
90
+ * small and per-owner — assignees, an item's sources, a form config's roles.
91
+ *
92
+ * Pages rather than taking a single `limit(1000)`: a fixed cap would drop the
93
+ * 1001st assignee or source with nothing in the log to say so, which is precisely
94
+ * the silent-loss failure mode this migration is built to avoid.
95
+ */
96
+ async function relationObjects(owner, field, batchSize = 500) {
97
+ const relation = owner.relation(field);
98
+ if (!relation)
99
+ return [];
100
+ const out = [];
101
+ let skip = 0;
102
+ for (;;) {
103
+ const page = await relation.query().limit(batchSize).skip(skip).find(env_js_1.MK);
104
+ if (page.length === 0)
105
+ break;
106
+ out.push(...page);
107
+ skip += page.length;
108
+ }
109
+ return out;
110
+ }
111
+ /** The objectIds of a relation, read completely. */
112
+ async function relationIds(owner, field, batchSize = 500) {
113
+ const rows = await relationObjects(owner, field, batchSize);
114
+ return rows.map((r) => r.id).filter((id) => !!id);
115
+ }
@@ -0,0 +1,53 @@
1
+ import { type MigrationConfig } from "./env.js";
2
+ /** Exported so the orchestrator can name it in a connection error. */
3
+ export declare const MIGRATION_MAP_CLASS = "OD3_Service_MigrationMap";
4
+ /**
5
+ * The v1 → v2 id map (`OD3_Service_MigrationMap`).
6
+ *
7
+ * New Parse classes mean new objectIds, and plenty of things reference a ticket by
8
+ * id *string* rather than by pointer — the chat context, meta entries, asset
9
+ * contexts. Those can only be rewritten if we remember what became what.
10
+ *
11
+ * Keeping the map in its own class rather than as a `legacyId` column on every
12
+ * model keeps the domain models clean, makes each pass idempotent (look up by
13
+ * source, update instead of insert) and leaves an audit trail for spot-checks.
14
+ *
15
+ * Tickets are the exception: `Ticket.origin` is already a free-form traceability
16
+ * marker on the v2 model, so they carry `"legacy:<oldId>"` there as well. The map
17
+ * still records them, so one lookup path works for every class.
18
+ *
19
+ * Addressed by raw className because the generated typed subclass only appears
20
+ * after `pnpm schema-ts` runs against a server that has the class.
21
+ */
22
+ export declare class MigrationMap {
23
+ private readonly config;
24
+ /** `sourceClass|sourceId` → target objectId, loaded up front. */
25
+ private readonly cache;
26
+ private loaded;
27
+ constructor(config: MigrationConfig);
28
+ private static key;
29
+ /**
30
+ * Load the whole map into memory once.
31
+ *
32
+ * Safe at this scale: ~10k rows per class per tenant means the map tops out in
33
+ * the low hundreds of thousands of short strings, and every pass would otherwise
34
+ * do a point query per row.
35
+ */
36
+ load(): Promise<void>;
37
+ /** The v2 objectId a v1 object already became, if any. */
38
+ get(sourceClass: string, sourceId: string): string | undefined;
39
+ /** Convenience: resolve to a pointer of the target class, or undefined. */
40
+ pointer(sourceClass: string, sourceId: string | undefined, targetClass: string): Parse.Pointer | undefined;
41
+ /**
42
+ * Placeholder target id used during a dry run.
43
+ *
44
+ * A dry run writes nothing, so no real target id exists — but later passes
45
+ * resolve their references *through this map*, and without an entry every one of
46
+ * them would report "no migrated counterpart" for every single row. That would
47
+ * bury the genuine findings the dry run exists to surface. A recognisable
48
+ * placeholder keeps the pipeline resolvable and the report readable.
49
+ */
50
+ static dryId(sourceId: string): string;
51
+ /** Record a mapping. No-op in dry-run mode beyond the in-memory cache. */
52
+ record(sourceClass: string, sourceId: string, targetClass: string, targetId: string): Promise<void>;
53
+ }
@@ -0,0 +1,113 @@
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.MigrationMap = exports.MIGRATION_MAP_CLASS = void 0;
7
+ const node_1 = __importDefault(require("parse/node"));
8
+ const env_js_1 = require("./env.js");
9
+ /** Exported so the orchestrator can name it in a connection error. */
10
+ exports.MIGRATION_MAP_CLASS = "OD3_Service_MigrationMap";
11
+ const CLASS_NAME = exports.MIGRATION_MAP_CLASS;
12
+ /**
13
+ * The v1 → v2 id map (`OD3_Service_MigrationMap`).
14
+ *
15
+ * New Parse classes mean new objectIds, and plenty of things reference a ticket by
16
+ * id *string* rather than by pointer — the chat context, meta entries, asset
17
+ * contexts. Those can only be rewritten if we remember what became what.
18
+ *
19
+ * Keeping the map in its own class rather than as a `legacyId` column on every
20
+ * model keeps the domain models clean, makes each pass idempotent (look up by
21
+ * source, update instead of insert) and leaves an audit trail for spot-checks.
22
+ *
23
+ * Tickets are the exception: `Ticket.origin` is already a free-form traceability
24
+ * marker on the v2 model, so they carry `"legacy:<oldId>"` there as well. The map
25
+ * still records them, so one lookup path works for every class.
26
+ *
27
+ * Addressed by raw className because the generated typed subclass only appears
28
+ * after `pnpm schema-ts` runs against a server that has the class.
29
+ */
30
+ class MigrationMap {
31
+ config;
32
+ /** `sourceClass|sourceId` → target objectId, loaded up front. */
33
+ cache = new Map();
34
+ loaded = false;
35
+ constructor(config) {
36
+ this.config = config;
37
+ }
38
+ static key(sourceClass, sourceId) {
39
+ return `${sourceClass}|${sourceId}`;
40
+ }
41
+ /**
42
+ * Load the whole map into memory once.
43
+ *
44
+ * Safe at this scale: ~10k rows per class per tenant means the map tops out in
45
+ * the low hundreds of thousands of short strings, and every pass would otherwise
46
+ * do a point query per row.
47
+ */
48
+ async load() {
49
+ if (this.loaded)
50
+ return;
51
+ let skip = 0;
52
+ for (;;) {
53
+ const page = await new node_1.default.Query(CLASS_NAME)
54
+ .limit(1000)
55
+ .skip(skip)
56
+ .ascending("createdAt")
57
+ .find(env_js_1.MK);
58
+ if (page.length === 0)
59
+ break;
60
+ for (const row of page) {
61
+ this.cache.set(MigrationMap.key(row.get("sourceClass"), row.get("sourceId")), row.get("targetId"));
62
+ }
63
+ // Stop only on an empty page — a short page does not mean the end. See the
64
+ // paging note in lib/pass.ts.
65
+ skip += page.length;
66
+ }
67
+ this.loaded = true;
68
+ console.log(` map: ${this.cache.size} existing mapping(s) loaded`);
69
+ }
70
+ /** The v2 objectId a v1 object already became, if any. */
71
+ get(sourceClass, sourceId) {
72
+ return this.cache.get(MigrationMap.key(sourceClass, sourceId));
73
+ }
74
+ /** Convenience: resolve to a pointer of the target class, or undefined. */
75
+ pointer(sourceClass, sourceId, targetClass) {
76
+ if (!sourceId)
77
+ return undefined;
78
+ const targetId = this.get(sourceClass, sourceId);
79
+ if (!targetId)
80
+ return undefined;
81
+ return {
82
+ __type: "Pointer",
83
+ className: targetClass,
84
+ objectId: targetId,
85
+ };
86
+ }
87
+ /**
88
+ * Placeholder target id used during a dry run.
89
+ *
90
+ * A dry run writes nothing, so no real target id exists — but later passes
91
+ * resolve their references *through this map*, and without an entry every one of
92
+ * them would report "no migrated counterpart" for every single row. That would
93
+ * bury the genuine findings the dry run exists to surface. A recognisable
94
+ * placeholder keeps the pipeline resolvable and the report readable.
95
+ */
96
+ static dryId(sourceId) {
97
+ return `dry-${sourceId}`;
98
+ }
99
+ /** Record a mapping. No-op in dry-run mode beyond the in-memory cache. */
100
+ async record(sourceClass, sourceId, targetClass, targetId) {
101
+ this.cache.set(MigrationMap.key(sourceClass, sourceId), targetId);
102
+ if (this.config.dryRun)
103
+ return;
104
+ const row = new node_1.default.Object(CLASS_NAME);
105
+ row.set("run", this.config.runId);
106
+ row.set("sourceClass", sourceClass);
107
+ row.set("sourceId", sourceId);
108
+ row.set("targetClass", targetClass);
109
+ row.set("targetId", targetId);
110
+ await row.save(null, { ...env_js_1.MK, context: { skipChangelog: true } });
111
+ }
112
+ }
113
+ exports.MigrationMap = MigrationMap;
@@ -0,0 +1,47 @@
1
+ import type { TicketMaterial } from "../../../types/custom/TicketMaterial.js";
2
+ import type { MigrationMap } from "./map.js";
3
+ /**
4
+ * Reading v1's `OD3_Maintenance_Ticket_Material.itemList`.
5
+ *
6
+ * v1 stored consumed material as an append-only side table like every other
7
+ * ticket attribute, except that each row carries the **whole list** as a JSON
8
+ * blob rather than a single value. So "current" is the newest row's `itemList`,
9
+ * and the history is the sequence of whole-list snapshots — which is exactly the
10
+ * shape `Ticket.material` and the changelog both want.
11
+ *
12
+ * Shared by pass 08 (the fold) and pass 12 (the replay) so the two cannot drift:
13
+ * if they converted differently, the replay's final snapshot would not equal the
14
+ * migrated ticket and `verify.ts` would fail for a reason that is not a bug.
15
+ */
16
+ /** One entry of a v1 `itemList`. */
17
+ export interface LegacyMaterial {
18
+ /** `Maintenance_Item` objectId, or a synthetic marker for an ad-hoc line. */
19
+ id?: string | number;
20
+ name?: string;
21
+ quantity?: number;
22
+ unit?: string;
23
+ }
24
+ export interface MaterialConversion {
25
+ entries: TicketMaterial[];
26
+ /**
27
+ * v1 ids that named something with no migrated `Article`. Not an error — v1
28
+ * let users type material that was never in the item catalogue, and those
29
+ * carry markers like `"manual-1"`. They become ad-hoc lines; the caller
30
+ * reports the count so a real broken reference is still visible.
31
+ */
32
+ unresolved: string[];
33
+ }
34
+ /**
35
+ * Convert a v1 `itemList` into `Ticket.material`.
36
+ *
37
+ * The entry is deliberately a **snapshot**: `name` and `unit` are copied across
38
+ * rather than looked up from the article at read time, because changelog entries
39
+ * render the stored value directly and a bare id would show as an opaque string
40
+ * in the timeline. See `types/custom/TicketMaterial.ts`.
41
+ *
42
+ * An entry with no usable name is dropped — v1 allowed a blank row, and carrying
43
+ * it over would put an empty line in the ticket.
44
+ */
45
+ export declare function toMaterial(itemList: unknown, map: MigrationMap): MaterialConversion;
46
+ /** The class the resolved ids point at — kept here so call sites stay honest. */
47
+ export declare const MATERIAL_TARGET_CLASS: string;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MATERIAL_TARGET_CLASS = void 0;
4
+ exports.toMaterial = toMaterial;
5
+ const index_js_1 = require("../../../types/index.js");
6
+ /**
7
+ * Convert a v1 `itemList` into `Ticket.material`.
8
+ *
9
+ * The entry is deliberately a **snapshot**: `name` and `unit` are copied across
10
+ * rather than looked up from the article at read time, because changelog entries
11
+ * render the stored value directly and a bare id would show as an opaque string
12
+ * in the timeline. See `types/custom/TicketMaterial.ts`.
13
+ *
14
+ * An entry with no usable name is dropped — v1 allowed a blank row, and carrying
15
+ * it over would put an empty line in the ticket.
16
+ */
17
+ function toMaterial(itemList, map) {
18
+ if (!Array.isArray(itemList))
19
+ return { entries: [], unresolved: [] };
20
+ const entries = [];
21
+ const unresolved = [];
22
+ for (const raw of itemList) {
23
+ if (!raw || typeof raw !== "object")
24
+ continue;
25
+ const name = typeof raw.name === "string" ? raw.name.trim() : "";
26
+ if (!name)
27
+ continue;
28
+ const legacyId = raw.id === undefined || raw.id === null ? "" : String(raw.id);
29
+ // Pass 05 maps Maintenance_Item onto OD3_BDE_Article, so a v1 item id
30
+ // resolves to an article id through the same map as everything else.
31
+ const articleId = legacyId
32
+ ? map.get(index_js_1.Maintenance_Item.className, legacyId)
33
+ : undefined;
34
+ if (legacyId && !articleId)
35
+ unresolved.push(legacyId);
36
+ const entry = {
37
+ name,
38
+ // v1's column is a free-form blob, so a non-numeric quantity is possible.
39
+ quantity: typeof raw.quantity === "number" ? raw.quantity : 0,
40
+ };
41
+ if (articleId)
42
+ entry.article = articleId;
43
+ const unit = typeof raw.unit === "string" ? raw.unit.trim() : "";
44
+ if (unit)
45
+ entry.unit = unit;
46
+ entries.push(entry);
47
+ }
48
+ return { entries, unresolved };
49
+ }
50
+ /** The class the resolved ids point at — kept here so call sites stay honest. */
51
+ exports.MATERIAL_TARGET_CLASS = index_js_1.BDE_Article.className;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Reading a v1 `OD3_Maintenance_Message`.
3
+ *
4
+ * Two traps, both of which produce visibly wrong output if ignored — and both are
5
+ * shared by the comment pass (10) and the checklist part of the ticket pass (08),
6
+ * which is why this lives here rather than in either of them.
7
+ *
8
+ * **1. `title` is often an i18n key, not text.** When the user left the subject
9
+ * blank, `ArticleCollapsable` stores the literal string
10
+ * `"maintenance:article.create.default_subject.todo"` (or `…userinput`), and the v1
11
+ * UI renders it through `t(...)`. Copying it verbatim would put that key on screen
12
+ * as a checklist label or a comment heading.
13
+ *
14
+ * **2. completion lives in `data.todoCompleted`**, not `data.done` — see
15
+ * `features/tickets/messages/hooks/useTodoMessage.ts`.
16
+ */
17
+ /** v1's `MessageData` blob, as far as the migration cares. */
18
+ export interface LegacyMessageData {
19
+ type?: "userinput" | "system" | "todo" | "media";
20
+ todoCompleted?: boolean;
21
+ /** Set on machine-generated messages: the side-table row that caused it. */
22
+ origin?: {
23
+ id?: string | number;
24
+ classname?: string;
25
+ };
26
+ }
27
+ export declare function messageData(message: Parse.Object): LegacyMessageData;
28
+ export declare function isTodo(message: Parse.Object): boolean;
29
+ /**
30
+ * A machine-generated change-log message, not something a person wrote.
31
+ *
32
+ * v1 recorded its own edit history *as messages*: every side-table write triggered
33
+ * an afterSave that wrote a `Maintenance_Message` with `data.type: "system"`,
34
+ * `data.origin` naming the row that caused it, and **i18n keys in both `title` and
35
+ * `content`** — e.g. `maintenance:ticket.message.priority.changed.content`, with
36
+ * the actual values in `data.translation`.
37
+ *
38
+ * These are deliberately **not** migrated to chat. v2 expresses that same history
39
+ * as `OD3_Changelog` entries, which pass 12 reconstructs from the side tables
40
+ * directly — with structured before/after values, the real author and the original
41
+ * timestamp. Carrying the messages across as well would duplicate the timeline in a
42
+ * strictly worse form and fill the ticket's comment thread with untranslated keys.
43
+ *
44
+ * Found by running against real data: a hand-written seed had readable content, so
45
+ * this only showed up once v1's own hooks had generated the messages.
46
+ */
47
+ export declare function isSystemGenerated(message: Parse.Object): boolean;
48
+ export declare function isTodoDone(message: Parse.Object): boolean;
49
+ /**
50
+ * An i18n key rather than human text.
51
+ *
52
+ * v1's keys are all `namespace:dotted.path`, and a real subject line would not
53
+ * normally contain a colon followed immediately by a lowercase dotted path — so
54
+ * this is narrow enough not to eat legitimate titles like `"Fehler: Lager heiß"`
55
+ * (space after the colon).
56
+ */
57
+ export declare function looksLikeI18nKey(value: unknown): boolean;
58
+ /**
59
+ * The human-readable title of a message, or undefined when v1 only had a
60
+ * placeholder key there.
61
+ */
62
+ export declare function realTitle(message: Parse.Object): string | undefined;
63
+ /**
64
+ * Label for a checklist item derived from a todo message.
65
+ *
66
+ * Prefers a real subject, falls back to the body (which is what the user actually
67
+ * typed when they left the subject blank), and returns undefined when there is
68
+ * nothing usable — the caller drops those rather than creating a blank item.
69
+ */
70
+ export declare function checklistLabel(message: Parse.Object): string | undefined;
71
+ /**
72
+ * Body text for a chat message derived from a v1 comment.
73
+ *
74
+ * A real subject becomes a bold first line; a placeholder key is dropped rather
75
+ * than rendered. Returns undefined when there is no text at all — `Chat_Message.text`
76
+ * is required, so the caller must skip those.
77
+ */
78
+ export declare function chatText(message: Parse.Object): string | undefined;
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ /**
3
+ * Reading a v1 `OD3_Maintenance_Message`.
4
+ *
5
+ * Two traps, both of which produce visibly wrong output if ignored — and both are
6
+ * shared by the comment pass (10) and the checklist part of the ticket pass (08),
7
+ * which is why this lives here rather than in either of them.
8
+ *
9
+ * **1. `title` is often an i18n key, not text.** When the user left the subject
10
+ * blank, `ArticleCollapsable` stores the literal string
11
+ * `"maintenance:article.create.default_subject.todo"` (or `…userinput`), and the v1
12
+ * UI renders it through `t(...)`. Copying it verbatim would put that key on screen
13
+ * as a checklist label or a comment heading.
14
+ *
15
+ * **2. completion lives in `data.todoCompleted`**, not `data.done` — see
16
+ * `features/tickets/messages/hooks/useTodoMessage.ts`.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.messageData = messageData;
20
+ exports.isTodo = isTodo;
21
+ exports.isSystemGenerated = isSystemGenerated;
22
+ exports.isTodoDone = isTodoDone;
23
+ exports.looksLikeI18nKey = looksLikeI18nKey;
24
+ exports.realTitle = realTitle;
25
+ exports.checklistLabel = checklistLabel;
26
+ exports.chatText = chatText;
27
+ function messageData(message) {
28
+ return message.get("data") ?? {};
29
+ }
30
+ function isTodo(message) {
31
+ return messageData(message).type === "todo";
32
+ }
33
+ /**
34
+ * A machine-generated change-log message, not something a person wrote.
35
+ *
36
+ * v1 recorded its own edit history *as messages*: every side-table write triggered
37
+ * an afterSave that wrote a `Maintenance_Message` with `data.type: "system"`,
38
+ * `data.origin` naming the row that caused it, and **i18n keys in both `title` and
39
+ * `content`** — e.g. `maintenance:ticket.message.priority.changed.content`, with
40
+ * the actual values in `data.translation`.
41
+ *
42
+ * These are deliberately **not** migrated to chat. v2 expresses that same history
43
+ * as `OD3_Changelog` entries, which pass 12 reconstructs from the side tables
44
+ * directly — with structured before/after values, the real author and the original
45
+ * timestamp. Carrying the messages across as well would duplicate the timeline in a
46
+ * strictly worse form and fill the ticket's comment thread with untranslated keys.
47
+ *
48
+ * Found by running against real data: a hand-written seed had readable content, so
49
+ * this only showed up once v1's own hooks had generated the messages.
50
+ */
51
+ function isSystemGenerated(message) {
52
+ return messageData(message).type === "system";
53
+ }
54
+ function isTodoDone(message) {
55
+ return messageData(message).todoCompleted === true;
56
+ }
57
+ /**
58
+ * An i18n key rather than human text.
59
+ *
60
+ * v1's keys are all `namespace:dotted.path`, and a real subject line would not
61
+ * normally contain a colon followed immediately by a lowercase dotted path — so
62
+ * this is narrow enough not to eat legitimate titles like `"Fehler: Lager heiß"`
63
+ * (space after the colon).
64
+ */
65
+ function looksLikeI18nKey(value) {
66
+ return typeof value === "string" && /^[a-z0-9_-]+:[a-z0-9_.-]+$/i.test(value.trim());
67
+ }
68
+ /**
69
+ * The human-readable title of a message, or undefined when v1 only had a
70
+ * placeholder key there.
71
+ */
72
+ function realTitle(message) {
73
+ const title = message.get("title");
74
+ if (typeof title !== "string")
75
+ return undefined;
76
+ const trimmed = title.trim();
77
+ if (!trimmed || looksLikeI18nKey(trimmed))
78
+ return undefined;
79
+ return trimmed;
80
+ }
81
+ /**
82
+ * Label for a checklist item derived from a todo message.
83
+ *
84
+ * Prefers a real subject, falls back to the body (which is what the user actually
85
+ * typed when they left the subject blank), and returns undefined when there is
86
+ * nothing usable — the caller drops those rather than creating a blank item.
87
+ */
88
+ function checklistLabel(message) {
89
+ const title = realTitle(message);
90
+ if (title)
91
+ return title;
92
+ const content = message.get("content");
93
+ if (typeof content === "string" && content.trim())
94
+ return content.trim();
95
+ return undefined;
96
+ }
97
+ /**
98
+ * Body text for a chat message derived from a v1 comment.
99
+ *
100
+ * A real subject becomes a bold first line; a placeholder key is dropped rather
101
+ * than rendered. Returns undefined when there is no text at all — `Chat_Message.text`
102
+ * is required, so the caller must skip those.
103
+ */
104
+ function chatText(message) {
105
+ const title = realTitle(message);
106
+ const content = message.get("content");
107
+ const body = typeof content === "string" ? content.trim() : "";
108
+ if (title && body)
109
+ return `**${title}**\n\n${body}`;
110
+ if (title)
111
+ return title;
112
+ if (body)
113
+ return body;
114
+ return undefined;
115
+ }