@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,17 @@
1
+ import { type Pass } from "../lib/pass.js";
2
+ /**
3
+ * Pass 06 — `OD3_Maintenance_Ticket_FormConfig` → `OD3_Service_FormConfig`.
4
+ *
5
+ * Two representation changes: v1 stored an `Object` column, v2 stores
6
+ * JSON-serialised strings (`fields`, `rules`); and v1's `roles` Relation becomes a
7
+ * plain array of role ids.
8
+ *
9
+ * `view` ("steps" | "formular" | "compact") has no v2 counterpart — v2 renders one
10
+ * form — so it is dropped with a note.
11
+ *
12
+ * The v1 `config` shape is only partly knowable from the type definitions, so this
13
+ * pass is defensive: anything it does not recognise is reported with the raw key
14
+ * rather than dropped silently. Expect to revisit it after the first dry run
15
+ * against real data.
16
+ */
17
+ export declare const pass06: Pass;
@@ -0,0 +1,166 @@
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.pass06 = void 0;
7
+ const node_1 = __importDefault(require("parse/node"));
8
+ const index_js_1 = require("../../../types/index.js");
9
+ const acl_js_1 = require("../lib/acl.js");
10
+ const latest_js_1 = require("../lib/latest.js");
11
+ const ptr_js_1 = require("../lib/ptr.js");
12
+ const pass_js_1 = require("../lib/pass.js");
13
+ /**
14
+ * v1 field key → v2 `FieldConfigEntry.key`.
15
+ *
16
+ * v1 configured the *form input*; v2 configures the *ticket field*, so a couple of
17
+ * v1 keys fan out (`duedate` covered both ends of a range; `assignedTo` covered
18
+ * users and roles together) and two have no v2 field at all.
19
+ */
20
+ const FIELD_KEY_MAP = {
21
+ title: ["title"],
22
+ description: ["description"],
23
+ priority: ["priority"],
24
+ duedate: ["dueDate", "startDate"],
25
+ state: ["status"],
26
+ source: ["equipment"],
27
+ issuecategory: ["tags"],
28
+ project: ["project"],
29
+ assignedTo: ["assignedUsers", "assignedRoles"],
30
+ material: ["material"],
31
+ // `media` is now the asset library (no per-field config) and is reported
32
+ // rather than mapped. `material` used to be dropped with it; it now has a
33
+ // real v2 column, so the form-config entry carries across.
34
+ };
35
+ /**
36
+ * Pass 06 — `OD3_Maintenance_Ticket_FormConfig` → `OD3_Service_FormConfig`.
37
+ *
38
+ * Two representation changes: v1 stored an `Object` column, v2 stores
39
+ * JSON-serialised strings (`fields`, `rules`); and v1's `roles` Relation becomes a
40
+ * plain array of role ids.
41
+ *
42
+ * `view` ("steps" | "formular" | "compact") has no v2 counterpart — v2 renders one
43
+ * form — so it is dropped with a note.
44
+ *
45
+ * The v1 `config` shape is only partly knowable from the type definitions, so this
46
+ * pass is defensive: anything it does not recognise is reported with the raw key
47
+ * rather than dropped silently. Expect to revisit it after the first dry run
48
+ * against real data.
49
+ */
50
+ exports.pass06 = {
51
+ id: "06",
52
+ title: "formconfigs",
53
+ needs: [],
54
+ async run({ config, map, report, entry }) {
55
+ const sources = await (0, pass_js_1.all)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Maintenance_Ticket_FormConfig), config.tenantId), config.batchSize);
56
+ if (sources.length === 0)
57
+ return;
58
+ for (const source of sources) {
59
+ const sourceId = source.id;
60
+ if (!sourceId)
61
+ continue;
62
+ const existingId = map.get(index_js_1.Maintenance_Ticket_FormConfig.className, sourceId);
63
+ const target = existingId
64
+ ? new index_js_1.Service_FormConfig({ objectId: existingId })
65
+ : new index_js_1.Service_FormConfig();
66
+ const tenantId = (0, pass_js_1.refId)(source.get("tenant"));
67
+ const view = source.get("view");
68
+ // v1's `config` is `{ config: FieldConfigUnion[] }` in the UI types, but the
69
+ // column has been written by more than one generation of the form editor —
70
+ // accept either the wrapper or a bare array.
71
+ const raw = source.get("config");
72
+ const v1Fields = Array.isArray(raw)
73
+ ? raw
74
+ : Array.isArray(raw?.config)
75
+ ? raw.config
76
+ : [];
77
+ if (raw && v1Fields.length === 0) {
78
+ report.warn(entry, `FormConfig ${sourceId} has a config column this pass did not recognise: ` +
79
+ `${JSON.stringify(raw).slice(0, 200)}`);
80
+ }
81
+ const v2Fields = [];
82
+ for (const field of v1Fields) {
83
+ if (!field?.name)
84
+ continue;
85
+ const targetKeys = FIELD_KEY_MAP[field.name];
86
+ if (!targetKeys) {
87
+ report.warn(entry, `FormConfig ${sourceId} field "${field.name}" has no per-field v2 ` +
88
+ `config (media → asset library, meta → Meta_Config in pass 07) and was ` +
89
+ `skipped`);
90
+ continue;
91
+ }
92
+ // Priority's custom labels were keyed by v1 threshold label; v2 keys
93
+ // `valueLabels` by bucket value ("75" | "50" | "25" | "0"). The v1 labels
94
+ // are i18n keys, so a faithful remap needs the v1 threshold order — which
95
+ // is per-tenant config. Carry them across positionally and flag it.
96
+ let valueLabels;
97
+ if (field.name === "priority" && field.customLabels?.length) {
98
+ const buckets = ["0", "25", "50", "75"];
99
+ valueLabels = {};
100
+ field.customLabels.forEach((entryLabel, index) => {
101
+ const bucket = buckets[Math.min(index, buckets.length - 1)];
102
+ valueLabels[bucket] = entryLabel.value;
103
+ });
104
+ report.warn(entry, `FormConfig ${sourceId} priority labels were remapped positionally onto ` +
105
+ `v2's four buckets — check them by hand (v1 had five levels)`);
106
+ }
107
+ for (const key of targetKeys) {
108
+ const converted = { key };
109
+ if (field.hidden !== undefined)
110
+ converted.hidden = field.hidden;
111
+ if (field.required !== undefined)
112
+ converted.required = field.required;
113
+ // v1 used "-" to mean "render no label at all".
114
+ if (field.label && field.label !== "-")
115
+ converted.label = field.label;
116
+ if (valueLabels)
117
+ converted.valueLabels = valueLabels;
118
+ v2Fields.push(converted);
119
+ }
120
+ if (field.help) {
121
+ report.warn(entry, `FormConfig ${sourceId} field "${field.name}" drops help text ` +
122
+ `("${field.help}") — v2 FieldConfigEntry has no help property`);
123
+ }
124
+ }
125
+ target.set("label", source.get("label") || (view ? `Formular (${view})` : `Formular ${sourceId}`));
126
+ target.set("fields", JSON.stringify(v2Fields));
127
+ if (tenantId)
128
+ target.set("tenant", (0, ptr_js_1.tenantPtr)(tenantId));
129
+ // `roles` Relation → array of role ids.
130
+ const roleIds = await (0, latest_js_1.relationIds)(source, "roles", config.batchSize);
131
+ if (roleIds.length)
132
+ target.set("roles", roleIds);
133
+ if (view) {
134
+ report.warn(entry, `FormConfig ${sourceId} drops view "${view}" — v2 renders a single form layout`);
135
+ }
136
+ if (!existingId) {
137
+ target.setACL((0, acl_js_1.migrationAcl)({ tenantId }));
138
+ }
139
+ if (config.dryRun) {
140
+ existingId ? entry.updated++ : entry.created++;
141
+ continue;
142
+ }
143
+ try {
144
+ const newId = await (0, acl_js_1.saveMigrated)(config, target, {
145
+ className: index_js_1.Service_FormConfig.className,
146
+ isNew: !existingId,
147
+ createdAt: source.createdAt,
148
+ updatedAt: source.updatedAt,
149
+ });
150
+ if (existingId) {
151
+ entry.updated++;
152
+ }
153
+ else {
154
+ entry.created++;
155
+ await map.record(index_js_1.Maintenance_Ticket_FormConfig.className, sourceId, index_js_1.Service_FormConfig.className, newId);
156
+ }
157
+ }
158
+ catch (error) {
159
+ report.error(entry, `FormConfig ${sourceId} failed: ${error.message}`);
160
+ }
161
+ }
162
+ report.info(entry, `save-time rule sets live in OD3_Config[OPENSERVICE_RULESET_FUNCTIONS] and are ` +
163
+ `NOT migrated by this pass — v2 stores them per FormConfig in \`rules\`. ` +
164
+ `Port them by hand or extend this pass once the mapping is agreed.`);
165
+ },
166
+ };
@@ -0,0 +1,46 @@
1
+ import { type Pass } from "../lib/pass.js";
2
+ /** Context the ticket meta config and its entries live under in v2. */
3
+ export declare const TICKET_META_CONTEXT: string[];
4
+ /** A v1 ticket meta field definition (also carries its value, per ticket). */
5
+ export interface V1MetaField {
6
+ name?: string;
7
+ label?: string;
8
+ description?: string;
9
+ type?: string;
10
+ className?: string;
11
+ display?: string;
12
+ multiple?: boolean;
13
+ options?: string[];
14
+ min?: number;
15
+ max?: number;
16
+ value?: unknown;
17
+ }
18
+ /** A plugin-meta `MetaFieldConfig`. */
19
+ interface V2MetaField {
20
+ name: string;
21
+ label: string;
22
+ type: string;
23
+ description?: string;
24
+ multiline?: boolean;
25
+ min?: number;
26
+ max?: number;
27
+ options?: Array<{
28
+ label: string;
29
+ value: string;
30
+ }>;
31
+ multiple?: boolean;
32
+ className?: string;
33
+ display?: string;
34
+ }
35
+ /**
36
+ * v1 meta field → plugin-meta field config.
37
+ *
38
+ * The type vocabularies line up almost exactly — `string | number | boolean | date
39
+ * | select | select_parse` — and plugin-meta still accepts `select_parse` as a
40
+ * deprecated alias of `select_entity`, so it is carried across unchanged rather
41
+ * than rewritten. That keeps the migration reversible and lets the meta plugin's
42
+ * own read-time alias do the work.
43
+ */
44
+ export declare function toMetaFieldConfig(field: V1MetaField): V2MetaField | undefined;
45
+ export declare const pass07: Pass;
46
+ export {};
@@ -0,0 +1,185 @@
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.pass07 = exports.TICKET_META_CONTEXT = void 0;
7
+ exports.toMetaFieldConfig = toMetaFieldConfig;
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 env_js_1 = require("../lib/env.js");
12
+ const ptr_js_1 = require("../lib/ptr.js");
13
+ const pass_js_1 = require("../lib/pass.js");
14
+ /** Context the ticket meta config and its entries live under in v2. */
15
+ exports.TICKET_META_CONTEXT = ["service", "ticket", "meta"];
16
+ /**
17
+ * v1 meta field → plugin-meta field config.
18
+ *
19
+ * The type vocabularies line up almost exactly — `string | number | boolean | date
20
+ * | select | select_parse` — and plugin-meta still accepts `select_parse` as a
21
+ * deprecated alias of `select_entity`, so it is carried across unchanged rather
22
+ * than rewritten. That keeps the migration reversible and lets the meta plugin's
23
+ * own read-time alias do the work.
24
+ */
25
+ function toMetaFieldConfig(field) {
26
+ const name = field.name?.trim();
27
+ if (!name)
28
+ return undefined;
29
+ const converted = {
30
+ name,
31
+ label: field.label?.trim() || name,
32
+ type: field.type ?? "string",
33
+ };
34
+ if (field.description)
35
+ converted.description = field.description;
36
+ if (typeof field.min === "number")
37
+ converted.min = field.min;
38
+ if (typeof field.max === "number")
39
+ converted.max = field.max;
40
+ if (field.multiple !== undefined)
41
+ converted.multiple = field.multiple;
42
+ if (field.type === "select" && field.options?.length) {
43
+ // v1 stored plain strings; plugin-meta wants {label, value} pairs.
44
+ converted.options = field.options.map((option) => ({
45
+ label: option,
46
+ value: option,
47
+ }));
48
+ }
49
+ if (field.type === "select_parse") {
50
+ if (field.className)
51
+ converted.className = field.className;
52
+ if (field.display)
53
+ converted.display = field.display;
54
+ }
55
+ return converted;
56
+ }
57
+ /**
58
+ * Pass 07 — v1 ticket meta field definitions → one `OD3_Meta_Config`.
59
+ *
60
+ * v1 denormalised config *and* value together into `Maintenance_Ticket.meta` on
61
+ * every ticket, plus a definition list on the form config. v2 splits them: one
62
+ * shared `Meta_Config` under context `["service","ticket","meta"]`, and one
63
+ * `Meta_Entry` per ticket (pass 09).
64
+ *
65
+ * Because no single v1 record holds the authoritative definition list, this pass
66
+ * unions every definition it can see — from the form configs and from the tickets
67
+ * themselves — and de-duplicates by field name. First definition wins; conflicting
68
+ * re-definitions are reported.
69
+ */
70
+ /** Key used to bucket definitions per tenant. `""` means "no tenant". */
71
+ const NO_TENANT = "";
72
+ /**
73
+ * Look up the Meta_Config for one tenant.
74
+ *
75
+ * Scoped by tenant deliberately. `getMetaConfig()` in the meta plugin filters by
76
+ * *context only* and relies on Parse ACLs to show each tenant its own record — but
77
+ * this pass reads with the maintenance key, which bypasses ACL entirely. An
78
+ * unscoped `.first()` here would therefore find some other tenant's config and
79
+ * overwrite it, destroying that tenant's field definitions.
80
+ */
81
+ async function findConfig(contextString, tenantId) {
82
+ const query = new node_1.default.Query(index_js_1.Meta_Config).equalTo("context", contextString);
83
+ if (tenantId)
84
+ query.equalTo("tenant", (0, ptr_js_1.tenantPtr)(tenantId));
85
+ else
86
+ query.doesNotExist("tenant");
87
+ return (await query.first(env_js_1.MK)) ?? undefined;
88
+ }
89
+ exports.pass07 = {
90
+ id: "07",
91
+ title: "meta-configs",
92
+ needs: [],
93
+ async run({ config, report, entry }) {
94
+ // Definitions are collected **per tenant**: one shared Meta_Config per tenant,
95
+ // matching how the meta plugin reads them back. Merging every tenant into one
96
+ // record would leak one customer's custom fields into another's ticket form.
97
+ const byTenant = new Map();
98
+ const conflicts = [];
99
+ const bucket = (tenantId) => {
100
+ const existing = byTenant.get(tenantId);
101
+ if (existing)
102
+ return existing;
103
+ const created = new Map();
104
+ byTenant.set(tenantId, created);
105
+ return created;
106
+ };
107
+ const absorb = (field, tenantId, origin) => {
108
+ const converted = toMetaFieldConfig(field);
109
+ if (!converted)
110
+ return;
111
+ const target = bucket(tenantId);
112
+ const existing = target.get(converted.name);
113
+ if (!existing) {
114
+ target.set(converted.name, converted);
115
+ return;
116
+ }
117
+ if (existing.type !== converted.type) {
118
+ conflicts.push(`"${converted.name}" is ${existing.type} elsewhere but ${converted.type} in ${origin}`);
119
+ }
120
+ };
121
+ // Definitions as configured on the form.
122
+ for (const formConfig of await (0, pass_js_1.all)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Maintenance_Ticket_FormConfig), config.tenantId), config.batchSize)) {
123
+ const raw = formConfig.get("config");
124
+ const fields = Array.isArray(raw) ? raw : raw?.config;
125
+ const metaEntry = fields?.find((f) => f?.name === "meta");
126
+ const tenantId = (0, pass_js_1.refId)(formConfig.get("tenant")) ?? NO_TENANT;
127
+ for (const field of metaEntry?.fields ?? []) {
128
+ absorb(field, tenantId, `FormConfig ${formConfig.id}`);
129
+ }
130
+ }
131
+ // Definitions as they actually appear on tickets. The form config can have been
132
+ // edited since, so a ticket may carry a field the config no longer defines —
133
+ // and pass 09 would then write a value with no definition behind it.
134
+ let ticketsWithMeta = 0;
135
+ for (const ticket of await (0, pass_js_1.all)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Maintenance_Ticket), config.tenantId).exists("meta"), config.batchSize)) {
136
+ const meta = ticket.get("meta");
137
+ if (!meta?.fields?.length)
138
+ continue;
139
+ ticketsWithMeta += 1;
140
+ const tenantId = (0, pass_js_1.refId)(ticket.get("tenant")) ?? NO_TENANT;
141
+ for (const field of meta.fields) {
142
+ absorb(field, tenantId, `Ticket ${ticket.id}`);
143
+ }
144
+ }
145
+ for (const conflict of conflicts) {
146
+ report.warn(entry, `meta field type conflict: ${conflict} — first wins`);
147
+ }
148
+ if (byTenant.size === 0) {
149
+ report.warn(entry, "no ticket meta fields found in any form config or ticket — nothing to configure");
150
+ return;
151
+ }
152
+ const contextString = (0, ptr_js_1.contextToString)(exports.TICKET_META_CONTEXT);
153
+ for (const [tenantId, fieldsByName] of byTenant) {
154
+ const fields = [...fieldsByName.values()];
155
+ const label = tenantId || "(no tenant)";
156
+ report.info(entry, `tenant ${label}: ${fields.length} meta field definition(s) collected ` +
157
+ `(from ${ticketsWithMeta} ticket(s) with meta across all tenants)`);
158
+ const existing = await findConfig(contextString, tenantId);
159
+ const target = existing ?? new index_js_1.Meta_Config();
160
+ target.set("context", contextString);
161
+ target.set("config", { fields });
162
+ if (tenantId)
163
+ target.set("tenant", (0, ptr_js_1.tenantPtr)(tenantId));
164
+ if (!existing) {
165
+ target.setACL((0, acl_js_1.migrationAcl)({ tenantId: tenantId || undefined }));
166
+ }
167
+ if (config.dryRun) {
168
+ existing ? entry.updated++ : entry.created++;
169
+ continue;
170
+ }
171
+ try {
172
+ // No historical timestamp to preserve: the shared config is a v2 artefact
173
+ // synthesised from many v1 records, not a migrated row.
174
+ await (0, acl_js_1.saveMigrated)(config, target, {
175
+ className: index_js_1.Meta_Config.className,
176
+ isNew: !existing,
177
+ });
178
+ existing ? entry.updated++ : entry.created++;
179
+ }
180
+ catch (error) {
181
+ report.error(entry, `Meta_Config for tenant ${label} failed: ${error.message}`);
182
+ }
183
+ }
184
+ },
185
+ };
@@ -0,0 +1,27 @@
1
+ import { type Pass } from "../lib/pass.js";
2
+ /** Marker written to `Ticket.origin` so a v2 ticket can be traced back. */
3
+ export declare const legacyOrigin: (oldTicketId: string) => string;
4
+ /**
5
+ * The ancestor closure of a tag, as v2 stores it.
6
+ *
7
+ * v2 entities store the picked tag *plus every ancestor*, which turns "has tag X"
8
+ * into a flat indexable query that also matches descendants. Writing only the
9
+ * picked tag would silently break every hierarchical filter.
10
+ */
11
+ export declare function closureOf(tagId: string, parentOf: Map<string, string | undefined>): string[];
12
+ /**
13
+ * Pass 08 — the fold. `OD3_Maintenance_Ticket` (+ ~10 side tables) →
14
+ * `OD3_Service_Ticket`.
15
+ *
16
+ * Two things about the ordering are load-bearing:
17
+ *
18
+ * 1. Tickets are inserted in **ascending `createdAt`**, because insert order
19
+ * becomes numbering order and chronological insertion is what reproduces a
20
+ * sensible sequence. The backend `beforeSave` that normally assigns `number`
21
+ * is not registered in migration mode, so `TicketNumberer` (lib/ticket.ts)
22
+ * hands the numbers out in-process and flushes `Project.ticketCounter` once
23
+ * at the end of this pass.
24
+ * 2. Every side table is loaded **once** and grouped in memory (see `lib/latest`),
25
+ * not queried per ticket.
26
+ */
27
+ export declare const pass08: Pass;