@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,543 @@
1
+ "use strict";
2
+ /**
3
+ * Seed **v1** (`OD3_Maintenance_*`) test data, for exercising the v1 → v2 migration.
4
+ *
5
+ * DEV ONLY. Never run this against production.
6
+ *
7
+ * OD_MIGRATION_MODE=1 PARSE_APP_ID=… PARSE_MAINTENANCE_KEY=… \
8
+ * node dist/scripts/seed-v1-testdata.js [--purge]
9
+ *
10
+ * ## Why the maintenance key
11
+ *
12
+ * Realistic timestamps are the whole point. The migration's fold picks the newest
13
+ * side-table row and its replay builds a timeline from them — neither can be tested
14
+ * against data where every row was created in the same second. Historical
15
+ * `createdAt` / `updatedAt` require the maintenance key, so this seeder needs the
16
+ * same setup as the migration. That makes it a useful smoke test of the write path
17
+ * in its own right: if this runs, the migration's transport works.
18
+ *
19
+ * ## Why OD_MIGRATION_MODE
20
+ *
21
+ * All 29 v1 hooks call `defaultHandler`, which throws on a request with no user —
22
+ * and maintenance-key requests have none. The flag gates that off, so this seeder
23
+ * sets `tenant`, `user` and ACLs itself, exactly as the migration does.
24
+ *
25
+ * ## What it deliberately includes
26
+ *
27
+ * Every case the migration audits turned up, so a dry run has something to say:
28
+ *
29
+ * - two tenants, each with their own kanban states and meta fields
30
+ * - a ticket with no kanban state at all (inbox fallback)
31
+ * - a ticket with `enabled: false`, and one with `enabled` *unset*
32
+ * - side-table rows where `updatedAt` disagrees with `createdAt` ordering
33
+ * - a completed todo, and a todo whose title is an i18n key
34
+ * - a comment with an i18n-key title, and one with no author but an attachment
35
+ * - a source with no tag, and a source tree needing ancestors
36
+ * - an item with a German-decimal price and one with an unparseable price
37
+ * - a two-level issue-category tree (closure expansion)
38
+ * - an assignment batch (three rows, one `batchStamp`)
39
+ * - a message referencing a non-ticket object (orphan skip)
40
+ * - assets on a real and on a `temp-` media context
41
+ */
42
+ var __importDefault = (this && this.__importDefault) || function (mod) {
43
+ return (mod && mod.__esModule) ? mod : { "default": mod };
44
+ };
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ const node_1 = __importDefault(require("parse/node"));
47
+ const acl_js_1 = require("./migration/lib/acl.js");
48
+ const env_js_1 = require("./migration/lib/env.js");
49
+ const rest_js_1 = require("./migration/lib/rest.js");
50
+ // ── helpers ────────────────────────────────────────────────────────────────
51
+ const ptr = (className, objectId) => ({
52
+ __type: "Pointer",
53
+ className,
54
+ objectId,
55
+ });
56
+ const dateVal = (d) => ({ __type: "Date", iso: d.toISOString() });
57
+ /** Days before now, at a fixed time of day so runs are comparable. */
58
+ function daysAgo(days, hour = 9) {
59
+ const d = new Date();
60
+ d.setDate(d.getDate() - days);
61
+ d.setHours(hour, 0, 0, 0);
62
+ return d;
63
+ }
64
+ /** Marker so `--purge` can find exactly what this seeder created. */
65
+ const SEED_MARKER = "seed:v1-testdata";
66
+ async function create(ctx, className, payload, createdAt, updatedAt) {
67
+ const tenantId = payload.tenant?.objectId;
68
+ const userId = payload.user?.objectId;
69
+ const body = {
70
+ ...payload,
71
+ ACL: (0, acl_js_1.migrationAcl)({
72
+ tenantId,
73
+ userId,
74
+ allowTenantUserWrite: true,
75
+ }).toJSON(),
76
+ };
77
+ const result = await (0, rest_js_1.restCreateRaw)(ctx.config, className, body, createdAt, updatedAt);
78
+ ctx.created.set(className, (ctx.created.get(className) ?? 0) + 1);
79
+ return result.objectId;
80
+ }
81
+ /** Remove a field entirely, which a schema `defaultValue` otherwise prevents. */
82
+ async function unsetField(config, className, objectId, field) {
83
+ const response = await fetch(`${config.parseUrl}/classes/${className}/${objectId}`, {
84
+ method: "PUT",
85
+ headers: {
86
+ "X-Parse-Application-Id": config.appId,
87
+ "X-Parse-Maintenance-Key": config.maintenanceKey,
88
+ "X-Parse-Cloud-Context": JSON.stringify({ skipChangelog: true }),
89
+ "Content-Type": "application/json",
90
+ },
91
+ body: JSON.stringify({ [field]: { __op: "Delete" } }),
92
+ });
93
+ if (!response.ok) {
94
+ throw new Error(`unset ${className}.${field} → ${response.status}: ${(await response.text()).slice(0, 200)}`);
95
+ }
96
+ }
97
+ // ── the data ───────────────────────────────────────────────────────────────
98
+ async function seed(ctx) {
99
+ const { tenantId, tenant2Id, userId, roleId } = ctx;
100
+ const tenant = ptr("OD3_Tenant", tenantId);
101
+ const tenant2 = ptr("OD3_Tenant", tenant2Id);
102
+ const user = ptr("_User", userId);
103
+ // ── sources: a two-level tree, plus one with no tag, plus one nothing uses ──
104
+ const hallId = await create(ctx, "OD3_Source", { name: "Halle B", tag: "halle-b", tenant }, daysAgo(400));
105
+ const cncId = await create(ctx, "OD3_Source", { name: "CNC-04", tag: "cnc-04", parent: ptr("OD3_Source", hallId), tenant }, daysAgo(395));
106
+ // No tag: pass 03 must warn that this equipment cannot match a source.
107
+ const untaggedId = await create(ctx, "OD3_Source", { name: "Presse A (ohne Tag)", parent: ptr("OD3_Source", hallId), tenant }, daysAgo(390));
108
+ // Referenced by nothing: pass 03 must skip it (D3).
109
+ await create(ctx, "OD3_Source", { name: "Sensor XYZ (unreferenziert)", tag: "sensor-xyz", tenant }, daysAgo(380));
110
+ // ── kanban states: tenant 1 reuses the auto-seeded ones, plus a working state.
111
+ const inProgressId = await create(ctx, "OD3_Maintenance_Kanban_State", { label: "In Arbeit", order: 50, color: "#cce5ff", enabled: true, tenant }, daysAgo(370));
112
+ const finishedId = await create(ctx, "OD3_Maintenance_Kanban_State", {
113
+ label: "Erledigt",
114
+ order: 90,
115
+ color: "#d4edda",
116
+ isFinished: true,
117
+ enabled: true,
118
+ description: "Wird beim Abschluss gesetzt",
119
+ tenant,
120
+ }, daysAgo(370));
121
+ const inboxId = await create(ctx, "OD3_Maintenance_Kanban_State", { label: "Eingang", order: 10, isInbox: true, enabled: true, tenant }, daysAgo(370));
122
+ // A disabled state -> deleted: true in v2.
123
+ await create(ctx, "OD3_Maintenance_Kanban_State", { label: "Alt / stillgelegt", order: 99, enabled: false, tenant }, daysAgo(360));
124
+ // Tenant 2 gets its own, so the multi-tenant path is exercised.
125
+ await create(ctx, "OD3_Maintenance_Kanban_State", { label: "Offen (T2)", order: 10, isInbox: true, enabled: true, tenant: tenant2 }, daysAgo(360));
126
+ // ── issue categories: two levels, so the closure has something to expand ──
127
+ const mechId = await create(ctx, "OD3_Maintenance_Issuecategory", { name: "Mechanik", enabled: true, issuecode: "100", icon: "fa:gear", tenant }, daysAgo(350));
128
+ const bearingId = await create(ctx, "OD3_Maintenance_Issuecategory", {
129
+ name: "Lagerschaden",
130
+ parent: ptr("OD3_Maintenance_Issuecategory", mechId),
131
+ enabled: true,
132
+ issuecode: "110",
133
+ tenant,
134
+ }, daysAgo(349));
135
+ // ── projects: a parent/child pair, one finished ───────────────────────────
136
+ const projectId = await create(ctx, "OD3_Maintenance_Project", {
137
+ name: "Instandhaltung 2025",
138
+ description: "Laufende Instandhaltung",
139
+ color: "#4f7f2c",
140
+ start: dateVal(daysAgo(330)),
141
+ user,
142
+ tenant,
143
+ }, daysAgo(330));
144
+ await create(ctx, "OD3_Maintenance_Project", {
145
+ name: "Umbau Halle B (abgeschlossen)",
146
+ parent: ptr("OD3_Maintenance_Project", projectId),
147
+ finished: true,
148
+ user,
149
+ tenant,
150
+ }, daysAgo(320));
151
+ // ── items: German decimal, unparseable, and a source link ────────────────
152
+ const itemDichtung = await create(ctx, "OD3_Maintenance_Item", {
153
+ name: "Dichtung 12mm",
154
+ price: "12,50 €",
155
+ stock: 42,
156
+ stockUnit: "Stk",
157
+ internalNumber: "D-12",
158
+ storageLocation: "Regal 3",
159
+ supplier: "Muster GmbH",
160
+ tenant,
161
+ // v1 linked an item to the sources it belongs to via a Relation. Pass 05
162
+ // resolves those through the pass-03 map and stores the migrated Equipment
163
+ // ids on the article meta entry. Seeded so that path is covered — the
164
+ // comment above promised a source link that was never actually created.
165
+ sources: {
166
+ __op: "AddRelation",
167
+ objects: [
168
+ { __type: "Pointer", className: "OD3_Source", objectId: cncId },
169
+ ],
170
+ },
171
+ }, daysAgo(300));
172
+ const itemOel = await create(ctx, "OD3_Maintenance_Item", { name: "Spezialöl", price: "auf Anfrage", stock: 3, tenant }, daysAgo(299));
173
+ // ── ticket 1: the rich one. History across several side tables. ───────────
174
+ const t1 = await create(ctx, "OD3_Maintenance_Ticket", {
175
+ title: "Lager läuft heiß",
176
+ description: "Temperatur an Spindel steigt seit Tagen.",
177
+ enabled: true,
178
+ latestPriorityValue: 80,
179
+ project: ptr("OD3_Maintenance_Project", projectId),
180
+ // `kostenstelle` and `schicht` are defined by the form config below;
181
+ // `altfeld` is NOT — it only exists here, which is exactly the case pass 07
182
+ // unions in from tickets so pass 09 does not write an undefined-able value.
183
+ meta: {
184
+ fields: [
185
+ { name: "kostenstelle", label: "Kostenstelle", type: "string", value: "4711" },
186
+ { name: "schicht", label: "Schicht", type: "select", options: ["Früh", "Spät", "Nacht"], value: "Spät" },
187
+ { name: "altfeld", label: "Altes Feld", type: "string", value: "nur am Ticket" },
188
+ ],
189
+ },
190
+ user,
191
+ tenant,
192
+ }, daysAgo(60, 8));
193
+ // Priority raised over time -> a real timeline.
194
+ await create(ctx, "OD3_Maintenance_Priority", { ticket: ptr("OD3_Maintenance_Ticket", t1), value: 20, user, tenant }, daysAgo(60, 9));
195
+ await create(ctx, "OD3_Maintenance_Priority", { ticket: ptr("OD3_Maintenance_Ticket", t1), value: 60, user, tenant }, daysAgo(40, 9));
196
+ await create(ctx, "OD3_Maintenance_Priority", { ticket: ptr("OD3_Maintenance_Ticket", t1), value: 80, user, tenant }, daysAgo(10, 9));
197
+ // THE `effectiveAt` CASE: created earlier but edited later, so `updatedAt`
198
+ // ordering differs from `createdAt` ordering. The fold and the replay must agree
199
+ // on which of these two wins.
200
+ await create(ctx, "OD3_Maintenance_Downtime", { ticket: ptr("OD3_Maintenance_Ticket", t1), value: 30, user, tenant }, daysAgo(50, 9), daysAgo(5, 9));
201
+ await create(ctx, "OD3_Maintenance_Downtime", { ticket: ptr("OD3_Maintenance_Ticket", t1), value: 15, user, tenant }, daysAgo(20, 9), daysAgo(20, 9));
202
+ await create(ctx, "OD3_Maintenance_Frequency", { ticket: ptr("OD3_Maintenance_Ticket", t1), value: 40, user, tenant }, daysAgo(45, 9));
203
+ await create(ctx, "OD3_Maintenance_Restriction", { ticket: ptr("OD3_Maintenance_Ticket", t1), value: 60, user, tenant }, daysAgo(45, 10));
204
+ await create(ctx, "OD3_Maintenance_Duedate", {
205
+ ticket: ptr("OD3_Maintenance_Ticket", t1),
206
+ start: dateVal(daysAgo(30)),
207
+ end: dateVal(daysAgo(-14)),
208
+ startformat: "date",
209
+ endformat: "date",
210
+ user,
211
+ tenant,
212
+ }, daysAgo(30, 11));
213
+ // Title changed after creation -> the fold must prefer the override.
214
+ await create(ctx, "OD3_Maintenance_Ticket_Title", { ticket: ptr("OD3_Maintenance_Ticket", t1), title: "Lager läuft heiß (Spindel 2)", user, tenant }, daysAgo(35, 9));
215
+ await create(ctx, "OD3_Maintenance_Ticket_Source", { ticket: ptr("OD3_Maintenance_Ticket", t1), source: ptr("OD3_Source", cncId), user, tenant }, daysAgo(60, 9));
216
+ await create(ctx, "OD3_Maintenance_Ticket_Project", { ticket: ptr("OD3_Maintenance_Ticket", t1), project: ptr("OD3_Maintenance_Project", projectId), user, tenant }, daysAgo(60, 9));
217
+ await create(ctx, "OD3_Maintenance_Ticket_Issuecategory", { ticket: ptr("OD3_Maintenance_Ticket", t1), issuecategory: ptr("OD3_Maintenance_Issuecategory", bearingId), user, tenant }, daysAgo(59, 9));
218
+ // Status history: inbox -> in progress. No finished row, so `closedAt` stays null.
219
+ await create(ctx, "OD3_Maintenance_Ticket_Kanban_State", { ticket: ptr("OD3_Maintenance_Ticket", t1), state: ptr("OD3_Maintenance_Kanban_State", inboxId), user, tenant }, daysAgo(60, 9));
220
+ await create(ctx, "OD3_Maintenance_Ticket_Kanban_State", { ticket: ptr("OD3_Maintenance_Ticket", t1), state: ptr("OD3_Maintenance_Kanban_State", inProgressId), user, tenant }, daysAgo(38, 9));
221
+ await create(ctx, "OD3_Maintenance_Ticket_Kanban_State_Current", { ticket: ptr("OD3_Maintenance_Ticket", t1), state: ptr("OD3_Maintenance_Kanban_State", inProgressId), tenant }, daysAgo(38, 9));
222
+ // An assignment batch: several rows sharing one `batchStamp`, which the replay
223
+ // must collapse into a SINGLE timeline event rather than one per assignee.
224
+ const batchStamp = dateVal(daysAgo(37, 14));
225
+ await create(ctx, "OD3_Maintenance_Ticket_Assignment", { ticket: ptr("OD3_Maintenance_Ticket", t1), assignedUser: user, batchStamp, user, tenant }, daysAgo(37, 14));
226
+ if (ctx.roleId) {
227
+ await create(ctx, "OD3_Maintenance_Ticket_Assignment", {
228
+ ticket: ptr("OD3_Maintenance_Ticket", t1),
229
+ assignedRole: ptr("_Role", ctx.roleId),
230
+ batchStamp,
231
+ user,
232
+ tenant,
233
+ }, daysAgo(37, 14));
234
+ }
235
+ // A second, later batch -> a second event, so the timeline shows a reassignment.
236
+ await create(ctx, "OD3_Maintenance_Ticket_Assignment", {
237
+ ticket: ptr("OD3_Maintenance_Ticket", t1),
238
+ assignedUser: user,
239
+ batchStamp: dateVal(daysAgo(8, 11)),
240
+ user,
241
+ tenant,
242
+ }, daysAgo(8, 11));
243
+ // Material: an append-only side table where each row carries the WHOLE list,
244
+ // so two rows means one edit. Covers all three cases the migration handles:
245
+ // a catalogue item (maps to an Article), an ad-hoc line v1 allowed with a
246
+ // synthetic id, and a quantity changed later so the fold must pick row 2.
247
+ await create(ctx, "OD3_Maintenance_Ticket_Material", {
248
+ ticket: ptr("OD3_Maintenance_Ticket", t1),
249
+ itemList: [
250
+ { id: itemDichtung, name: "Dichtung 12mm", quantity: 3, unit: "Stk" },
251
+ ],
252
+ user,
253
+ tenant,
254
+ }, daysAgo(12, 9));
255
+ await create(ctx, "OD3_Maintenance_Ticket_Material", {
256
+ ticket: ptr("OD3_Maintenance_Ticket", t1),
257
+ itemList: [
258
+ { id: itemDichtung, name: "Dichtung 12mm", quantity: 5, unit: "Stk" },
259
+ { id: itemOel, name: "Spezialöl", quantity: 1, unit: "L" },
260
+ { id: "manual-1", name: "Kabelbinder (Restbestand)", quantity: 10, unit: "Stk" },
261
+ ],
262
+ user,
263
+ tenant,
264
+ }, daysAgo(9, 14));
265
+ // ── comments + todos on ticket 1 ─────────────────────────────────────────
266
+ await create(ctx, "OD3_Maintenance_Message", {
267
+ classname: "OD3_Maintenance_Ticket",
268
+ referencedObjectId: t1,
269
+ title: "Erste Sichtung",
270
+ content: "Lager sichtbar verfärbt, Austausch nötig.",
271
+ data: { type: "userinput" },
272
+ displayAt: dateVal(daysAgo(58)),
273
+ user,
274
+ tenant,
275
+ }, daysAgo(58, 10));
276
+ // Title is an i18n KEY (user left the subject blank) -> must not be rendered.
277
+ await create(ctx, "OD3_Maintenance_Message", {
278
+ classname: "OD3_Maintenance_Ticket",
279
+ referencedObjectId: t1,
280
+ title: "maintenance:article.create.default_subject.userinput",
281
+ content: "Ersatzteil bestellt, Lieferung nächste Woche.",
282
+ data: { type: "userinput" },
283
+ user,
284
+ tenant,
285
+ }, daysAgo(30, 10));
286
+ // A completed todo, and one still open with an i18n-key title.
287
+ await create(ctx, "OD3_Maintenance_Message", {
288
+ classname: "OD3_Maintenance_Ticket",
289
+ referencedObjectId: t1,
290
+ title: "Lager ausmessen",
291
+ content: "Innendurchmesser prüfen",
292
+ data: { type: "todo", todoCompleted: true },
293
+ user,
294
+ tenant,
295
+ }, daysAgo(50, 10));
296
+ await create(ctx, "OD3_Maintenance_Message", {
297
+ classname: "OD3_Maintenance_Ticket",
298
+ referencedObjectId: t1,
299
+ title: "maintenance:article.create.default_subject.todo",
300
+ content: "Schmierplan anpassen",
301
+ data: { type: "todo", todoCompleted: false },
302
+ user,
303
+ tenant,
304
+ }, daysAgo(20, 10));
305
+ // ── ticket 2: no kanban state at all -> inbox fallback ───────────────────
306
+ const t2 = await create(ctx, "OD3_Maintenance_Ticket", { title: "Kühlmittel nachfüllen", enabled: true, user, tenant }, daysAgo(25, 8));
307
+ await create(ctx, "OD3_Maintenance_Ticket_Source", { ticket: ptr("OD3_Maintenance_Ticket", t2), source: ptr("OD3_Source", untaggedId), user, tenant }, daysAgo(25, 9));
308
+ // ── ticket 2b: closed, so `closedAt` can be derived ─────────────────────
309
+ // v1 has no closedAt column; pass 08 infers it from the newest state-history row
310
+ // whose state is `isFinished`. Without a ticket like this that logic is untested.
311
+ const tClosed = await create(ctx, "OD3_Maintenance_Ticket", {
312
+ title: "Riemen getauscht",
313
+ description: "Antriebsriemen war gerissen.",
314
+ enabled: true,
315
+ latestPriorityValue: 40,
316
+ user,
317
+ tenant,
318
+ }, daysAgo(120, 8));
319
+ await create(ctx, "OD3_Maintenance_Ticket_Source", { ticket: ptr("OD3_Maintenance_Ticket", tClosed), source: ptr("OD3_Source", cncId), user, tenant }, daysAgo(120, 9));
320
+ await create(ctx, "OD3_Maintenance_Ticket_Kanban_State", { ticket: ptr("OD3_Maintenance_Ticket", tClosed), state: ptr("OD3_Maintenance_Kanban_State", inboxId), user, tenant }, daysAgo(120, 9));
321
+ await create(ctx, "OD3_Maintenance_Ticket_Kanban_State", { ticket: ptr("OD3_Maintenance_Ticket", tClosed), state: ptr("OD3_Maintenance_Kanban_State", inProgressId), user, tenant }, daysAgo(118, 9));
322
+ // The finished row: its updatedAt is what `closedAt` must end up as.
323
+ await create(ctx, "OD3_Maintenance_Ticket_Kanban_State", { ticket: ptr("OD3_Maintenance_Ticket", tClosed), state: ptr("OD3_Maintenance_Kanban_State", finishedId), user, tenant }, daysAgo(115, 16));
324
+ await create(ctx, "OD3_Maintenance_Ticket_Kanban_State_Current", { ticket: ptr("OD3_Maintenance_Ticket", tClosed), state: ptr("OD3_Maintenance_Kanban_State", finishedId), tenant }, daysAgo(115, 16));
325
+ // ── ticket 3: disabled -> deleted: true ──────────────────────────────────
326
+ await create(ctx, "OD3_Maintenance_Ticket", { title: "Storniert: Fehlalarm", enabled: false, user, tenant }, daysAgo(80, 8));
327
+ // ── ticket 4: `enabled` UNSET -> must also become deleted: true ───────────
328
+ const t4 = await create(ctx, "OD3_Maintenance_Ticket", { title: "Altlast ohne enabled-Flag", enabled: true, user, tenant }, daysAgo(500, 8));
329
+ await unsetField(ctx.config, "OD3_Maintenance_Ticket", t4, "enabled");
330
+ // ── ticket 5: tenant 2, so the multi-tenant path has data ────────────────
331
+ await create(ctx, "OD3_Maintenance_Ticket", {
332
+ title: "T2: Förderband prüfen",
333
+ enabled: true,
334
+ meta: {
335
+ fields: [
336
+ { name: "kostenstelle", label: "Kostenstelle", type: "string", value: "T2-4711" },
337
+ ],
338
+ },
339
+ user,
340
+ tenant: tenant2,
341
+ }, daysAgo(15, 8));
342
+ // ── a message pointing at something that is not a ticket -> orphan skip ──
343
+ await create(ctx, "OD3_Maintenance_Message", {
344
+ classname: "OD3_Source",
345
+ referencedObjectId: cncId,
346
+ title: "Maschinennotiz",
347
+ content: "Gehört zu einer Source, nicht zu einem Ticket.",
348
+ data: { type: "userinput" },
349
+ user,
350
+ tenant,
351
+ }, daysAgo(45, 10));
352
+ // ── form config, incl. meta field definitions and priority labels ─────────
353
+ await create(ctx, "OD3_Maintenance_Ticket_FormConfig", {
354
+ view: "formular",
355
+ label: "Standardformular",
356
+ config: {
357
+ config: [
358
+ { name: "title", required: true, label: "Kurzbeschreibung" },
359
+ { name: "description", label: "-" },
360
+ {
361
+ name: "priority",
362
+ required: true,
363
+ customLabels: [
364
+ { label: "verylow", value: "Sehr niedrig" },
365
+ { label: "low", value: "Niedrig" },
366
+ { label: "mid", value: "Mittel" },
367
+ { label: "high", value: "Hoch" },
368
+ { label: "ultra", value: "Kritisch" },
369
+ ],
370
+ },
371
+ { name: "media", label: "Fotos" },
372
+ { name: "material", label: "Material" },
373
+ { name: "source", required: true },
374
+ { name: "issuecategory", help: "Bitte möglichst genau wählen" },
375
+ {
376
+ name: "meta",
377
+ fields: [
378
+ { name: "kostenstelle", label: "Kostenstelle", type: "string" },
379
+ { name: "stillstand", label: "Stillstand?", type: "boolean" },
380
+ {
381
+ name: "schicht",
382
+ label: "Schicht",
383
+ type: "select",
384
+ options: ["Früh", "Spät", "Nacht"],
385
+ },
386
+ ],
387
+ },
388
+ ],
389
+ },
390
+ tenant,
391
+ // v1 stored this as a Parse Relation. Pass 06 reads it with relationIds()
392
+ // and writes a plain id array, which is what the v2 FormConfig.roles
393
+ // column is — seeded so that conversion is actually covered.
394
+ ...(roleId
395
+ ? {
396
+ roles: {
397
+ __op: "AddRelation",
398
+ objects: [{ __type: "Pointer", className: "_Role", objectId: roleId }],
399
+ },
400
+ }
401
+ : {}),
402
+ }, daysAgo(340));
403
+ // ── assets: one real ticket-media context, one abandoned temp context ────
404
+ await create(ctx, "OD3_Assets", {
405
+ context: `ticket-media-${t1}`,
406
+ description: "Foto Lager",
407
+ file: { __type: "File", name: "lager.txt", url: "https://example.invalid/lager.txt" },
408
+ user,
409
+ tenant,
410
+ }, daysAgo(57, 10));
411
+ await create(ctx, "OD3_Assets", {
412
+ context: "ticket-media-temp-abandoned-uuid",
413
+ description: "Verwaister Upload",
414
+ file: { __type: "File", name: "temp.txt", url: "https://example.invalid/temp.txt" },
415
+ user,
416
+ tenant,
417
+ }, daysAgo(56, 10));
418
+ }
419
+ // ── purge ──────────────────────────────────────────────────────────────────
420
+ const SEEDED_CLASSES = [
421
+ "OD3_Maintenance_Ticket_Assignment",
422
+ "OD3_Maintenance_Ticket_Material",
423
+ "OD3_Maintenance_Ticket_Kanban_State_Current",
424
+ "OD3_Maintenance_Ticket_Kanban_State",
425
+ "OD3_Maintenance_Ticket_Issuecategory",
426
+ "OD3_Maintenance_Ticket_Project",
427
+ "OD3_Maintenance_Ticket_Source",
428
+ "OD3_Maintenance_Ticket_Title",
429
+ "OD3_Maintenance_Duedate",
430
+ "OD3_Maintenance_Restriction",
431
+ "OD3_Maintenance_Frequency",
432
+ "OD3_Maintenance_Downtime",
433
+ "OD3_Maintenance_Priority",
434
+ "OD3_Maintenance_Message",
435
+ "OD3_Maintenance_Ticket",
436
+ "OD3_Maintenance_Ticket_FormConfig",
437
+ "OD3_Maintenance_Item",
438
+ "OD3_Maintenance_Project",
439
+ "OD3_Maintenance_Issuecategory",
440
+ "OD3_Maintenance_Kanban_State",
441
+ ];
442
+ /**
443
+ * Delete everything in the v1 classes this seeder touches.
444
+ *
445
+ * Deliberately blunt: this is a dev-only reset, and the seeder's whole point is a
446
+ * known starting state. It does **not** touch `OD3_Source`, `OD3_Assets`,
447
+ * `OD3_Tenant` or `_User`, because those are shared with the rest of the stack.
448
+ */
449
+ async function purge() {
450
+ for (const className of SEEDED_CLASSES) {
451
+ let removed = 0;
452
+ for (;;) {
453
+ const page = await new node_1.default.Query(className).limit(200).find(env_js_1.MK);
454
+ if (page.length === 0)
455
+ break;
456
+ await node_1.default.Object.destroyAll(page, {
457
+ context: { skipChangelog: true },
458
+ });
459
+ removed += page.length;
460
+ }
461
+ if (removed)
462
+ console.log(` purged ${removed.toString().padStart(4)} ${className}`);
463
+ }
464
+ console.log(`\n (OD3_Source, OD3_Assets, OD3_Tenant and _User left alone — shared with the stack)\n`);
465
+ }
466
+ // ── main ───────────────────────────────────────────────────────────────────
467
+ async function main() {
468
+ let config;
469
+ try {
470
+ config = (0, env_js_1.readConfig)(process.argv.slice(2));
471
+ }
472
+ catch (error) {
473
+ if (error instanceof env_js_1.MigrationConfigError) {
474
+ console.error(`\n✗ ${error.message}\n`);
475
+ return 2;
476
+ }
477
+ throw error;
478
+ }
479
+ (0, env_js_1.initParse)(config);
480
+ console.log(`\nv1 test-data seeder (DEV ONLY)`);
481
+ console.log(` server ${config.parseUrl}`);
482
+ if (process.argv.includes("--purge")) {
483
+ console.log(`\npurging v1 classes…\n`);
484
+ await purge();
485
+ return 0;
486
+ }
487
+ // Attach to whatever tenant/user the stack already has rather than inventing one.
488
+ const tenants = await new node_1.default.Query("OD3_Tenant").ascending("createdAt").find(env_js_1.MK);
489
+ const users = await new node_1.default.Query(node_1.default.User).ascending("createdAt").find(env_js_1.MK);
490
+ if (tenants.length === 0 || users.length === 0 || !tenants[0].id || !users[0].id) {
491
+ console.error(`\n✗ Need at least one OD3_Tenant and one _User to attach the test data to.\n` +
492
+ ` Found ${tenants.length} tenant(s), ${users.length} user(s).\n`);
493
+ return 2;
494
+ }
495
+ // Tenant 1 is the one the seeding user actually belongs to, not simply the
496
+ // oldest. Everything seeded here gets tenant-role ACLs, and master data
497
+ // (Status, Equipment) grants roles ONLY — no per-user grant — so data written
498
+ // into a tenant the user is not a member of is invisible in the UI. That reads
499
+ // exactly like a broken migration: tickets show up (they also grant their
500
+ // creator) while their status and equipment render as blank or raw ids.
501
+ //
502
+ // The oldest-first ordering made this worse: the synthetic tenant below is
503
+ // backdated, so on every run after the first it sorted ahead of the real one.
504
+ const userTenantId = users[0].get("tenant")?.id;
505
+ const tenantId = userTenantId && tenants.some((t) => t.id === userTenantId)
506
+ ? userTenantId
507
+ : tenants[0].id;
508
+ let tenant2Id = tenants.find((t) => t.id !== tenantId)?.id ?? "";
509
+ if (!tenant2Id) {
510
+ // The multi-tenant cases need a second tenant; create one if the stack has
511
+ // only the default.
512
+ const second = await (0, rest_js_1.restCreateRaw)(config, "OD3_Tenant", { label: `Testmandant 2 (${SEED_MARKER})` }, daysAgo(600));
513
+ tenant2Id = second.objectId;
514
+ console.log(` created a second tenant for the multi-tenant cases: ${tenant2Id}`);
515
+ }
516
+ console.log(` tenant 1 ${tenantId}`);
517
+ console.log(` tenant 2 ${tenant2Id}`);
518
+ console.log(` user ${users[0].id}\n`);
519
+ // Prefer a tenant-scoped role, so the assignment looks like real data.
520
+ const roles = await new node_1.default.Query(node_1.default.Role).find(env_js_1.MK);
521
+ const roleId = roles.find((r) => r.getName() === `od-tenant-user-${tenantId}`)?.id ??
522
+ roles[0]?.id;
523
+ const ctx = {
524
+ config,
525
+ tenantId,
526
+ tenant2Id,
527
+ userId: users[0].id,
528
+ roleId,
529
+ created: new Map(),
530
+ };
531
+ await seed(ctx);
532
+ console.log(`created:`);
533
+ for (const [className, count] of [...ctx.created].sort()) {
534
+ console.log(` ${count.toString().padStart(4)} ${className}`);
535
+ }
536
+ console.log(`\n✓ seeded. Next:\n` +
537
+ ` node dist/scripts/migration/run.js --dry-run\n`);
538
+ return 0;
539
+ }
540
+ main().then((code) => process.exit(code), (error) => {
541
+ console.error(error);
542
+ process.exit(1);
543
+ });
@@ -10,6 +10,7 @@ export interface BDE_ArticleAttributes {
10
10
  isDeleted: boolean;
11
11
  targettime?: number | undefined;
12
12
  tenant?: Tenant | undefined;
13
+ type?: string | undefined;
13
14
  unit?: string | undefined;
14
15
  }
15
16
  export declare class BDE_Article extends Parse.Object<BDE_ArticleAttributes> {
@@ -25,6 +26,8 @@ export declare class BDE_Article extends Parse.Object<BDE_ArticleAttributes> {
25
26
  set targettime(value: number | undefined);
26
27
  get tenant(): Tenant | undefined;
27
28
  set tenant(value: Tenant | undefined);
29
+ get type(): string | undefined;
30
+ set type(value: string | undefined);
28
31
  get unit(): string | undefined;
29
32
  set unit(value: string | undefined);
30
33
  }
@@ -40,6 +40,12 @@ class BDE_Article extends node_1.default.Object {
40
40
  set tenant(value) {
41
41
  super.set("tenant", value);
42
42
  }
43
+ get type() {
44
+ return super.get("type");
45
+ }
46
+ set type(value) {
47
+ super.set("type", value);
48
+ }
43
49
  get unit() {
44
50
  return super.get("unit");
45
51
  }
@@ -0,0 +1,49 @@
1
+ import Parse from "parse/node";
2
+ import type { _User } from "./_User";
3
+ import type { Tenant } from "./Tenant";
4
+ export interface Chat_MessageAttributes {
5
+ id: string;
6
+ objectId: string;
7
+ createdAt: Date;
8
+ updatedAt: Date;
9
+ attachments?: any[] | undefined;
10
+ author?: _User | undefined;
11
+ authorName?: string | undefined;
12
+ chatId?: string | undefined;
13
+ context?: string | undefined;
14
+ deleted: boolean;
15
+ editedAt?: Date | undefined;
16
+ mentions?: any[] | undefined;
17
+ parent?: Chat_Message | undefined;
18
+ reactions?: any | undefined;
19
+ tenant?: Tenant | undefined;
20
+ text: string;
21
+ }
22
+ export declare class Chat_Message extends Parse.Object<Chat_MessageAttributes> {
23
+ static className: string;
24
+ constructor(data?: Partial<Chat_MessageAttributes>);
25
+ get attachments(): any[] | undefined;
26
+ set attachments(value: any[] | undefined);
27
+ get author(): _User | undefined;
28
+ set author(value: _User | undefined);
29
+ get authorName(): string | undefined;
30
+ set authorName(value: string | undefined);
31
+ get chatId(): string | undefined;
32
+ set chatId(value: string | undefined);
33
+ get context(): string | undefined;
34
+ set context(value: string | undefined);
35
+ get deleted(): boolean;
36
+ set deleted(value: boolean);
37
+ get editedAt(): Date | undefined;
38
+ set editedAt(value: Date | undefined);
39
+ get mentions(): any[] | undefined;
40
+ set mentions(value: any[] | undefined);
41
+ get parent(): Chat_Message | undefined;
42
+ set parent(value: Chat_Message | undefined);
43
+ get reactions(): any | undefined;
44
+ set reactions(value: any | undefined);
45
+ get tenant(): Tenant | undefined;
46
+ set tenant(value: Tenant | undefined);
47
+ get text(): string;
48
+ set text(value: string);
49
+ }