@openinc/parse-server-opendash 4.1.1 → 4.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/featuremap.json +27 -12
- package/dist/features/config/helper/customOptions.js +9 -0
- package/dist/features/config/helper/isFeatureEnabled.d.ts +1 -1
- package/dist/features/config/helper/isFeatureEnabled.js +2 -0
- package/dist/features/meta/index.d.ts +1 -0
- package/dist/features/meta/index.js +5 -0
- package/dist/features/meta/services/registerMetaContext.d.ts +29 -0
- package/dist/features/meta/services/registerMetaContext.js +208 -0
- package/dist/features/permissions/types/Permissions.d.ts +24 -1
- package/dist/features/permissions/types/Permissions.js +34 -0
- package/dist/features/ruleset/applyRuleSet.d.ts +41 -0
- package/dist/features/ruleset/applyRuleSet.js +81 -0
- package/dist/features/schema/helper/defaultHandler.js +13 -0
- package/dist/features/schema/helper/migrationMode.d.ts +26 -0
- package/dist/features/schema/helper/migrationMode.js +48 -0
- package/dist/features/schema/index.d.ts +1 -0
- package/dist/features/schema/index.js +3 -1
- package/dist/features/schema/services/beforeSaveHook.js +21 -0
- package/dist/features/translations/services/initTranslation.js +1 -1
- package/dist/hooks/Chat_Message.d.ts +1 -0
- package/dist/hooks/Chat_Message.js +179 -0
- package/dist/hooks/Meta_Config.js +19 -10
- package/dist/hooks/Meta_Context.d.ts +6 -0
- package/dist/hooks/Meta_Context.js +16 -0
- package/dist/hooks/Meta_Entry.js +20 -10
- package/dist/hooks/Service_Equipment.d.ts +1 -0
- package/dist/hooks/Service_Equipment.js +38 -0
- package/dist/hooks/Service_FormConfig.d.ts +1 -0
- package/dist/hooks/Service_FormConfig.js +17 -0
- package/dist/hooks/Service_Project.d.ts +1 -0
- package/dist/hooks/Service_Project.js +17 -0
- package/dist/hooks/Service_Schedule.d.ts +1 -0
- package/dist/hooks/Service_Schedule.js +24 -0
- package/dist/hooks/Service_Schedule_Execution.d.ts +1 -0
- package/dist/hooks/Service_Schedule_Execution.js +17 -0
- package/dist/hooks/Service_Schedule_Template.d.ts +1 -0
- package/dist/hooks/Service_Schedule_Template.js +17 -0
- package/dist/hooks/Service_Status.d.ts +1 -0
- package/dist/hooks/Service_Status.js +32 -0
- package/dist/hooks/Service_Tag.d.ts +1 -0
- package/dist/hooks/Service_Tag.js +168 -0
- package/dist/hooks/Service_Ticket.d.ts +1 -0
- package/dist/hooks/Service_Ticket.js +433 -0
- package/dist/hooks/Service_TriggerState.d.ts +1 -0
- package/dist/hooks/Service_TriggerState.js +17 -0
- package/dist/hooks/_ChangeLog.js +27 -6
- package/dist/i18n/de-DE/chat.json +18 -0
- package/dist/i18n/de-DE/openservice.json +14 -0
- package/dist/i18n/en-US/chat.json +18 -0
- package/dist/i18n/en-US/openservice.json +14 -0
- package/dist/scripts/backfill-meta-contexts.d.ts +1 -0
- package/dist/scripts/backfill-meta-contexts.js +100 -0
- package/dist/scripts/migration/lib/acl.d.ts +52 -0
- package/dist/scripts/migration/lib/acl.js +79 -0
- package/dist/scripts/migration/lib/articleMeta.d.ts +47 -0
- package/dist/scripts/migration/lib/articleMeta.js +132 -0
- package/dist/scripts/migration/lib/env.d.ts +56 -0
- package/dist/scripts/migration/lib/env.js +106 -0
- package/dist/scripts/migration/lib/latest.d.ts +47 -0
- package/dist/scripts/migration/lib/latest.js +115 -0
- package/dist/scripts/migration/lib/map.d.ts +53 -0
- package/dist/scripts/migration/lib/map.js +113 -0
- package/dist/scripts/migration/lib/material.d.ts +47 -0
- package/dist/scripts/migration/lib/material.js +51 -0
- package/dist/scripts/migration/lib/message.d.ts +78 -0
- package/dist/scripts/migration/lib/message.js +115 -0
- package/dist/scripts/migration/lib/pass.d.ts +67 -0
- package/dist/scripts/migration/lib/pass.js +89 -0
- package/dist/scripts/migration/lib/ptr.d.ts +32 -0
- package/dist/scripts/migration/lib/ptr.js +38 -0
- package/dist/scripts/migration/lib/report.d.ts +41 -0
- package/dist/scripts/migration/lib/report.js +78 -0
- package/dist/scripts/migration/lib/rest.d.ts +47 -0
- package/dist/scripts/migration/lib/rest.js +68 -0
- package/dist/scripts/migration/lib/ticket.d.ts +72 -0
- package/dist/scripts/migration/lib/ticket.js +192 -0
- package/dist/scripts/migration/passes/01-statuses.d.ts +12 -0
- package/dist/scripts/migration/passes/01-statuses.js +122 -0
- package/dist/scripts/migration/passes/02-tags.d.ts +13 -0
- package/dist/scripts/migration/passes/02-tags.js +150 -0
- package/dist/scripts/migration/passes/03-equipment.d.ts +14 -0
- package/dist/scripts/migration/passes/03-equipment.js +176 -0
- package/dist/scripts/migration/passes/04-projects.d.ts +27 -0
- package/dist/scripts/migration/passes/04-projects.js +241 -0
- package/dist/scripts/migration/passes/05-articles.d.ts +25 -0
- package/dist/scripts/migration/passes/05-articles.js +203 -0
- package/dist/scripts/migration/passes/06-formconfigs.d.ts +17 -0
- package/dist/scripts/migration/passes/06-formconfigs.js +166 -0
- package/dist/scripts/migration/passes/07-meta-configs.d.ts +46 -0
- package/dist/scripts/migration/passes/07-meta-configs.js +185 -0
- package/dist/scripts/migration/passes/08-tickets.d.ts +27 -0
- package/dist/scripts/migration/passes/08-tickets.js +353 -0
- package/dist/scripts/migration/passes/09-ticket-meta.d.ts +12 -0
- package/dist/scripts/migration/passes/09-ticket-meta.js +136 -0
- package/dist/scripts/migration/passes/10-chat.d.ts +17 -0
- package/dist/scripts/migration/passes/10-chat.js +230 -0
- package/dist/scripts/migration/passes/11-assets.d.ts +17 -0
- package/dist/scripts/migration/passes/11-assets.js +93 -0
- package/dist/scripts/migration/passes/12-history.d.ts +24 -0
- package/dist/scripts/migration/passes/12-history.js +332 -0
- package/dist/scripts/migration/run.d.ts +1 -0
- package/dist/scripts/migration/run.js +148 -0
- package/dist/scripts/migration/verify.d.ts +1 -0
- package/dist/scripts/migration/verify.js +620 -0
- package/dist/scripts/seed-v1-testdata.d.ts +41 -0
- package/dist/scripts/seed-v1-testdata.js +543 -0
- package/dist/types/BDE_Article.d.ts +3 -0
- package/dist/types/BDE_Article.js +6 -0
- package/dist/types/Chat_Message.d.ts +49 -0
- package/dist/types/Chat_Message.js +87 -0
- package/dist/types/MES_RessourceOverview.d.ts +38 -0
- package/dist/types/MES_RessourceOverview.js +63 -0
- package/dist/types/Meta_Context.d.ts +43 -0
- package/dist/types/Meta_Context.js +75 -0
- package/dist/types/Service_Equipment.d.ts +33 -0
- package/dist/types/Service_Equipment.js +57 -0
- package/dist/types/Service_FormConfig.d.ts +33 -0
- package/dist/types/Service_FormConfig.js +57 -0
- package/dist/types/Service_MeterReading.d.ts +35 -0
- package/dist/types/Service_MeterReading.js +57 -0
- package/dist/types/Service_Project.d.ts +53 -0
- package/dist/types/Service_Project.js +93 -0
- package/dist/types/Service_Schedule.d.ts +66 -0
- package/dist/types/Service_Schedule.js +117 -0
- package/dist/types/Service_Schedule_Execution.d.ts +38 -0
- package/dist/types/Service_Schedule_Execution.js +63 -0
- package/dist/types/Service_Schedule_Template.d.ts +36 -0
- package/dist/types/Service_Schedule_Template.js +60 -0
- package/dist/types/Service_Status.d.ts +30 -0
- package/dist/types/Service_Status.js +51 -0
- package/dist/types/Service_Tag.d.ts +30 -0
- package/dist/types/Service_Tag.js +51 -0
- package/dist/types/Service_Ticket.d.ts +92 -0
- package/dist/types/Service_Ticket.js +165 -0
- package/dist/types/Service_TicketCounter.d.ts +18 -0
- package/dist/types/Service_TicketCounter.js +27 -0
- package/dist/types/Service_TriggerState.d.ts +31 -0
- package/dist/types/Service_TriggerState.js +51 -0
- package/dist/types/_Session.d.ts +0 -1
- package/dist/types/_User.d.ts +2 -6
- package/dist/types/custom/MetaField.d.ts +9 -0
- package/dist/types/custom/TicketMaterial.d.ts +32 -0
- package/dist/types/custom/TicketMaterial.js +2 -0
- package/dist/types/index.d.ts +30 -0
- package/dist/types/index.js +33 -3
- package/package.json +6 -3
- package/schema/BDE_Article.json +3 -0
- package/schema/Changelog.json +3 -6
- package/schema/Chat_Message.json +88 -0
- package/schema/Meta_Context.json +78 -0
- package/schema/Service_Equipment.json +55 -0
- package/schema/Service_FormConfig.json +66 -0
- package/schema/Service_MeterReading.json +54 -0
- package/schema/Service_MigrationMap.json +55 -0
- package/schema/Service_Project.json +77 -0
- package/schema/Service_Schedule.json +101 -0
- package/schema/Service_Schedule_Execution.json +58 -0
- package/schema/Service_Schedule_Template.json +73 -0
- package/schema/Service_Status.json +50 -0
- package/schema/Service_Tag.json +50 -0
- package/schema/Service_Ticket.json +114 -0
- package/schema/Service_TicketCounter.json +38 -0
- package/schema/Service_TriggerState.json +50 -0
|
@@ -0,0 +1,620 @@
|
|
|
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
|
+
const node_1 = __importDefault(require("parse/node"));
|
|
7
|
+
const index_js_1 = require("../../types/index.js");
|
|
8
|
+
const env_js_1 = require("./lib/env.js");
|
|
9
|
+
const message_js_1 = require("./lib/message.js");
|
|
10
|
+
const pass_js_1 = require("./lib/pass.js");
|
|
11
|
+
/** Pointer/array-of-ids fields to resolve, and the class they must resolve into. */
|
|
12
|
+
const REFERENCE_FIELDS = [
|
|
13
|
+
{ field: "status", className: index_js_1.Service_Status.className },
|
|
14
|
+
{ field: "equipment", className: index_js_1.Service_Equipment.className },
|
|
15
|
+
{ field: "project", className: index_js_1.Service_Project.className },
|
|
16
|
+
{ field: "tags", className: index_js_1.Service_Tag.className, array: true },
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* How many objects the migration created for a given v1 class.
|
|
20
|
+
*
|
|
21
|
+
* Comparing raw table counts (`Maintenance_Ticket` vs `Service_Ticket`) looks
|
|
22
|
+
* equivalent but drifts: the aftercare step deliberately *adds* v2 Statuses for the
|
|
23
|
+
* `pending` / `cancelled` categories v1 never had, and any ticket created in v2
|
|
24
|
+
* before this runs inflates the other side. Both would fail a hard check for
|
|
25
|
+
* entirely correct reasons.
|
|
26
|
+
*
|
|
27
|
+
* Counting `OD3_Service_MigrationMap` rows instead asks the question that actually
|
|
28
|
+
* matters — "was every v1 record migrated?" — and is immune to anything added
|
|
29
|
+
* afterwards.
|
|
30
|
+
*/
|
|
31
|
+
async function migratedCount(sourceClass, batchSize) {
|
|
32
|
+
return (0, pass_js_1.countAll)(() => new node_1.default.Query("OD3_Service_MigrationMap").equalTo("sourceClass", sourceClass), batchSize);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Render a snapshot value for comparison.
|
|
36
|
+
*
|
|
37
|
+
* The replay check used to compare with `String(...)`, which is fine for
|
|
38
|
+
* scalars and pointers but collapses every array-of-objects column to
|
|
39
|
+
* "[object Object]" — so a `material` mismatch would have compared equal to
|
|
40
|
+
* itself and the check would have passed while proving nothing.
|
|
41
|
+
*
|
|
42
|
+
* Objects are stringified with their keys sorted, because the fold and the
|
|
43
|
+
* replay build their entries independently and key order carries no meaning.
|
|
44
|
+
*/
|
|
45
|
+
function comparable(value) {
|
|
46
|
+
const seen = new WeakSet();
|
|
47
|
+
const norm = (v) => {
|
|
48
|
+
if (v === null || typeof v !== "object")
|
|
49
|
+
return v;
|
|
50
|
+
if (seen.has(v))
|
|
51
|
+
return "[circular]";
|
|
52
|
+
seen.add(v);
|
|
53
|
+
if (Array.isArray(v))
|
|
54
|
+
return v.map(norm);
|
|
55
|
+
const obj = v;
|
|
56
|
+
// Parse wire types carry their identity in `__type`; compare pointers by id.
|
|
57
|
+
if (obj.__type === "Pointer")
|
|
58
|
+
return `Pointer:${String(obj.objectId)}`;
|
|
59
|
+
if (obj.__type === "Date")
|
|
60
|
+
return `Date:${String(obj.iso)}`;
|
|
61
|
+
return Object.fromEntries(Object.keys(obj)
|
|
62
|
+
.sort()
|
|
63
|
+
.filter((k) => obj[k] !== undefined)
|
|
64
|
+
.map((k) => [k, norm(obj[k])]));
|
|
65
|
+
};
|
|
66
|
+
return JSON.stringify(norm(value));
|
|
67
|
+
}
|
|
68
|
+
const checks = [
|
|
69
|
+
// ── Counts ───────────────────────────────────────────────────────────────
|
|
70
|
+
{
|
|
71
|
+
name: "every v1 ticket was migrated (disabled ones arrive as deleted)",
|
|
72
|
+
hard: true,
|
|
73
|
+
async run(tenantId, batchSize) {
|
|
74
|
+
const [v1, migrated] = await Promise.all([
|
|
75
|
+
(0, pass_js_1.countAll)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Maintenance_Ticket), tenantId), batchSize),
|
|
76
|
+
migratedCount(index_js_1.Maintenance_Ticket.className, batchSize),
|
|
77
|
+
]);
|
|
78
|
+
// Note: the map is not tenant-scoped, so this is only exact for a
|
|
79
|
+
// whole-database run. With --tenant it is a lower bound, which still catches
|
|
80
|
+
// wholesale loss (the failure mode that matters).
|
|
81
|
+
return migrated >= v1
|
|
82
|
+
? undefined
|
|
83
|
+
: `${v1} v1 ticket(s) but only ${migrated} mapping(s) — ${v1 - migrated} not migrated`;
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "every v1 kanban state was migrated",
|
|
88
|
+
hard: true,
|
|
89
|
+
async run(tenantId, batchSize) {
|
|
90
|
+
const [v1, migrated] = await Promise.all([
|
|
91
|
+
(0, pass_js_1.countAll)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Maintenance_Kanban_State), tenantId), batchSize),
|
|
92
|
+
migratedCount(index_js_1.Maintenance_Kanban_State.className, batchSize),
|
|
93
|
+
]);
|
|
94
|
+
return migrated >= v1
|
|
95
|
+
? undefined
|
|
96
|
+
: `${v1} v1 kanban state(s) but only ${migrated} mapping(s)`;
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "comment count matches migrated chat messages",
|
|
101
|
+
hard: false,
|
|
102
|
+
async run(tenantId, batchSize) {
|
|
103
|
+
let comments = 0;
|
|
104
|
+
for await (const page of (0, pass_js_1.pages)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Maintenance_Message), tenantId), batchSize)) {
|
|
105
|
+
// Same predicates pass 10 uses, imported rather than re-inlined — a
|
|
106
|
+
// divergence here would make the check disagree with the pass it verifies.
|
|
107
|
+
for (const m of page) {
|
|
108
|
+
if (!(0, message_js_1.isTodo)(m) && !(0, message_js_1.isSystemGenerated)(m))
|
|
109
|
+
comments += 1;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const migrated = await (0, pass_js_1.countAll)(() => new node_1.default.Query(index_js_1.Chat_Message).startsWith("context", '["service","ticket"'), batchSize);
|
|
113
|
+
// Not a hard check: v1 messages can reference non-ticket objects, which are
|
|
114
|
+
// legitimately skipped.
|
|
115
|
+
return comments === migrated
|
|
116
|
+
? undefined
|
|
117
|
+
: `${comments} human v1 comment(s) vs ${migrated} chat message(s) — the ` +
|
|
118
|
+
`difference should equal pass 10's "not a migrated ticket" skips`;
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
// ── Required fields ──────────────────────────────────────────────────────
|
|
122
|
+
{
|
|
123
|
+
name: "every ticket has status, tenant, title and number",
|
|
124
|
+
hard: true,
|
|
125
|
+
async run(tenantId, batchSize) {
|
|
126
|
+
const missing = [];
|
|
127
|
+
for await (const page of (0, pass_js_1.pages)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Service_Ticket), tenantId), batchSize)) {
|
|
128
|
+
for (const t of page) {
|
|
129
|
+
const gaps = [];
|
|
130
|
+
if (!(0, pass_js_1.refId)(t.get("status")))
|
|
131
|
+
gaps.push("status");
|
|
132
|
+
if (!(0, pass_js_1.refId)(t.get("tenant")))
|
|
133
|
+
gaps.push("tenant");
|
|
134
|
+
if (!t.get("title"))
|
|
135
|
+
gaps.push("title");
|
|
136
|
+
if (typeof t.get("number") !== "number")
|
|
137
|
+
gaps.push("number");
|
|
138
|
+
if (gaps.length && missing.length < 10) {
|
|
139
|
+
missing.push(`${t.id} (${gaps.join(", ")})`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return missing.length ? `e.g. ${missing.join("; ")}` : undefined;
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "project ticketCounter is at least the highest number handed out",
|
|
148
|
+
hard: true,
|
|
149
|
+
async run(tenantId, batchSize) {
|
|
150
|
+
// The migration assigns `number` itself now (beforeSave is suppressed so the
|
|
151
|
+
// historical createdAt survives), so this checks the migration's own
|
|
152
|
+
// bookkeeping: a counter left behind the numbers already used would make the
|
|
153
|
+
// next run reuse them.
|
|
154
|
+
const highest = new Map();
|
|
155
|
+
for await (const page of (0, pass_js_1.pages)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Service_Ticket), tenantId), batchSize)) {
|
|
156
|
+
for (const t of page) {
|
|
157
|
+
const number = t.get("number");
|
|
158
|
+
const projectId = (0, pass_js_1.refId)(t.get("project"));
|
|
159
|
+
if (typeof number !== "number" || !projectId)
|
|
160
|
+
continue;
|
|
161
|
+
highest.set(projectId, Math.max(highest.get(projectId) ?? 0, number));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const behind = [];
|
|
165
|
+
for await (const page of (0, pass_js_1.pages)(() => new node_1.default.Query(index_js_1.Service_Project), batchSize)) {
|
|
166
|
+
for (const project of page) {
|
|
167
|
+
if (!project.id)
|
|
168
|
+
continue;
|
|
169
|
+
const max = highest.get(project.id);
|
|
170
|
+
if (max === undefined)
|
|
171
|
+
continue;
|
|
172
|
+
const counter = project.get("ticketCounter") ?? 0;
|
|
173
|
+
if (counter < max && behind.length < 10) {
|
|
174
|
+
behind.push(`${project.id}: counter ${counter} < highest number ${max}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return behind.length ? behind.join("; ") : undefined;
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "ticket numbers are unique per project",
|
|
183
|
+
hard: true,
|
|
184
|
+
async run(tenantId, batchSize) {
|
|
185
|
+
const seen = new Map();
|
|
186
|
+
const dupes = [];
|
|
187
|
+
for await (const page of (0, pass_js_1.pages)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Service_Ticket), tenantId), batchSize)) {
|
|
188
|
+
for (const t of page) {
|
|
189
|
+
const number = t.get("number");
|
|
190
|
+
if (typeof number !== "number")
|
|
191
|
+
continue;
|
|
192
|
+
// Keyed by tenant *and* project: tickets without a project are numbered
|
|
193
|
+
// from the per-tenant counter, so two tenants legitimately share numbers
|
|
194
|
+
// and a project-only key would report them as duplicates.
|
|
195
|
+
const key = `${(0, pass_js_1.refId)(t.get("tenant")) ?? "-"}/${(0, pass_js_1.refId)(t.get("project")) ?? "(no project)"}`;
|
|
196
|
+
const bucket = seen.get(key) ?? new Set();
|
|
197
|
+
if (bucket.has(number) && dupes.length < 10) {
|
|
198
|
+
dupes.push(`${key} #${number}`);
|
|
199
|
+
}
|
|
200
|
+
bucket.add(number);
|
|
201
|
+
seen.set(key, bucket);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return dupes.length ? `duplicate number(s): ${dupes.join(", ")}` : undefined;
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: "every Status has a category in the v2 domain",
|
|
209
|
+
hard: true,
|
|
210
|
+
async run(tenantId, batchSize) {
|
|
211
|
+
const bad = await (0, pass_js_1.countAll)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Service_Status).notContainedIn("category", [
|
|
212
|
+
"inbox",
|
|
213
|
+
"active",
|
|
214
|
+
"pending",
|
|
215
|
+
"done",
|
|
216
|
+
"cancelled",
|
|
217
|
+
]), tenantId), batchSize);
|
|
218
|
+
return bad > 0 ? `${bad} Status row(s) with an unknown category` : undefined;
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "an inbox Status exists",
|
|
223
|
+
hard: true,
|
|
224
|
+
async run(tenantId, batchSize) {
|
|
225
|
+
const n = await (0, pass_js_1.countAll)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Service_Status).equalTo("category", "inbox"), tenantId), batchSize);
|
|
226
|
+
return n > 0 ? undefined : "no Status with category 'inbox'";
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
// ── Referential integrity ────────────────────────────────────────────────
|
|
230
|
+
{
|
|
231
|
+
name: "every ticket reference resolves to an existing object",
|
|
232
|
+
hard: true,
|
|
233
|
+
async run(tenantId, batchSize) {
|
|
234
|
+
// Load the id sets once rather than probing per ticket.
|
|
235
|
+
const known = new Map();
|
|
236
|
+
for (const { className } of REFERENCE_FIELDS) {
|
|
237
|
+
if (known.has(className))
|
|
238
|
+
continue;
|
|
239
|
+
const ids = new Set();
|
|
240
|
+
for await (const page of (0, pass_js_1.pages)(() => new node_1.default.Query(className), batchSize)) {
|
|
241
|
+
for (const row of page)
|
|
242
|
+
if (row.id)
|
|
243
|
+
ids.add(row.id);
|
|
244
|
+
}
|
|
245
|
+
known.set(className, ids);
|
|
246
|
+
}
|
|
247
|
+
const broken = [];
|
|
248
|
+
for await (const page of (0, pass_js_1.pages)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Service_Ticket), tenantId), batchSize)) {
|
|
249
|
+
for (const t of page) {
|
|
250
|
+
for (const { field, className, array } of REFERENCE_FIELDS) {
|
|
251
|
+
const ids = known.get(className);
|
|
252
|
+
const value = t.get(field);
|
|
253
|
+
if (!value)
|
|
254
|
+
continue;
|
|
255
|
+
const candidates = array
|
|
256
|
+
? (value ?? []).map((v) => (0, pass_js_1.refId)(v))
|
|
257
|
+
: [(0, pass_js_1.refId)(value)];
|
|
258
|
+
for (const id of candidates) {
|
|
259
|
+
if (id && !ids.has(id) && broken.length < 10) {
|
|
260
|
+
broken.push(`${t.id}.${field} → ${id}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return broken.length ? `e.g. ${broken.join("; ")}` : undefined;
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: "every ticket reference belongs to the ticket's own tenant",
|
|
271
|
+
hard: true,
|
|
272
|
+
async run(tenantId, batchSize) {
|
|
273
|
+
// Pass 08 resolved the required-status fallback once per run and only
|
|
274
|
+
// scoped it when --tenant was given, so a whole-database run put one
|
|
275
|
+
// tenant's inbox Status on every other tenant's unstatused tickets. The
|
|
276
|
+
// reference resolved fine (the row exists), so the integrity check above
|
|
277
|
+
// passed — it just belonged to a tenant that could not read it, and the
|
|
278
|
+
// column rendered empty in the UI. This is the check that catches it.
|
|
279
|
+
const tenantOf = new Map();
|
|
280
|
+
for (const { className } of REFERENCE_FIELDS) {
|
|
281
|
+
for await (const page of (0, pass_js_1.pages)(() => new node_1.default.Query(className), batchSize)) {
|
|
282
|
+
for (const row of page) {
|
|
283
|
+
if (row.id)
|
|
284
|
+
tenantOf.set(`${className}|${row.id}`, (0, pass_js_1.refId)(row.get("tenant")));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const crossed = [];
|
|
289
|
+
for await (const page of (0, pass_js_1.pages)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Service_Ticket), tenantId), batchSize)) {
|
|
290
|
+
for (const t of page) {
|
|
291
|
+
const own = (0, pass_js_1.refId)(t.get("tenant"));
|
|
292
|
+
if (!own)
|
|
293
|
+
continue;
|
|
294
|
+
for (const { field, className, array } of REFERENCE_FIELDS) {
|
|
295
|
+
const value = t.get(field);
|
|
296
|
+
if (!value)
|
|
297
|
+
continue;
|
|
298
|
+
const ids = array
|
|
299
|
+
? (value ?? []).map((v) => (0, pass_js_1.refId)(v))
|
|
300
|
+
: [(0, pass_js_1.refId)(value)];
|
|
301
|
+
for (const id of ids) {
|
|
302
|
+
if (!id)
|
|
303
|
+
continue;
|
|
304
|
+
const other = tenantOf.get(`${className}|${id}`);
|
|
305
|
+
// undefined = the row has no tenant at all, which is a different
|
|
306
|
+
// (and legitimate) case — only a *mismatch* is a leak.
|
|
307
|
+
if (other !== undefined && other !== own && crossed.length < 10) {
|
|
308
|
+
crossed.push(`${t.id}.${field} -> ${id} (tenant ${other}, ticket is ${own})`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return crossed.length ? `e.g. ${crossed.join("; ")}` : undefined;
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: "every ticket's tags array is a complete ancestor closure",
|
|
319
|
+
hard: true,
|
|
320
|
+
async run(tenantId, batchSize) {
|
|
321
|
+
const parentOf = new Map();
|
|
322
|
+
for await (const page of (0, pass_js_1.pages)(() => new node_1.default.Query(index_js_1.Service_Tag), batchSize)) {
|
|
323
|
+
for (const tag of page) {
|
|
324
|
+
if (tag.id)
|
|
325
|
+
parentOf.set(tag.id, (0, pass_js_1.refId)(tag.get("parent")));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const bad = [];
|
|
329
|
+
for await (const page of (0, pass_js_1.pages)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Service_Ticket), tenantId), batchSize)) {
|
|
330
|
+
for (const t of page) {
|
|
331
|
+
const tags = t.get("tags") ?? [];
|
|
332
|
+
if (tags.length === 0)
|
|
333
|
+
continue;
|
|
334
|
+
const present = new Set(tags.map((v) => (0, pass_js_1.refId)(v)).filter(Boolean));
|
|
335
|
+
for (const id of present) {
|
|
336
|
+
let cursor = parentOf.get(id);
|
|
337
|
+
const guard = new Set();
|
|
338
|
+
while (cursor && !guard.has(cursor)) {
|
|
339
|
+
guard.add(cursor);
|
|
340
|
+
if (!present.has(cursor) && bad.length < 10) {
|
|
341
|
+
bad.push(`${t.id} has ${id} but not its ancestor ${cursor}`);
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
cursor = parentOf.get(cursor);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return bad.length ? `e.g. ${bad.join("; ")}` : undefined;
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: "no equipment parent chain contains a cycle",
|
|
354
|
+
hard: true,
|
|
355
|
+
async run(_tenantId, batchSize) {
|
|
356
|
+
const parentOf = new Map();
|
|
357
|
+
for await (const page of (0, pass_js_1.pages)(() => new node_1.default.Query(index_js_1.Service_Equipment), batchSize)) {
|
|
358
|
+
for (const e of page) {
|
|
359
|
+
if (e.id)
|
|
360
|
+
parentOf.set(e.id, (0, pass_js_1.refId)(e.get("parent")));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const cyclic = [];
|
|
364
|
+
for (const start of parentOf.keys()) {
|
|
365
|
+
const guard = new Set([start]);
|
|
366
|
+
let cursor = parentOf.get(start);
|
|
367
|
+
while (cursor) {
|
|
368
|
+
if (guard.has(cursor)) {
|
|
369
|
+
if (cyclic.length < 10)
|
|
370
|
+
cyclic.push(start);
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
guard.add(cursor);
|
|
374
|
+
cursor = parentOf.get(cursor);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return cyclic.length ? `equipment in a cycle: ${cyclic.join(", ")}` : undefined;
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
// ── Track F: the reconstructed history ───────────────────────────────────
|
|
381
|
+
{
|
|
382
|
+
name: "replaying each ticket's changelog reproduces the migrated ticket",
|
|
383
|
+
hard: true,
|
|
384
|
+
async run(tenantId, batchSize) {
|
|
385
|
+
// The strongest check in the migration: the final snapshot of the replay IS
|
|
386
|
+
// the flat ticket by construction, so any disagreement means either the fold
|
|
387
|
+
// (pass 08) or the replay (pass 12) is wrong.
|
|
388
|
+
const latestSnapshot = new Map();
|
|
389
|
+
const latestAt = new Map();
|
|
390
|
+
for await (const page of (0, pass_js_1.pages)(() => new node_1.default.Query(index_js_1.Changelog).equalTo("nameOfClass", index_js_1.Service_Ticket.className), batchSize)) {
|
|
391
|
+
for (const row of page) {
|
|
392
|
+
const context = row.get("context");
|
|
393
|
+
if (!context?.migrated)
|
|
394
|
+
continue;
|
|
395
|
+
const ticketId = row.get("changedObject");
|
|
396
|
+
const at = row.createdAt?.getTime() ?? 0;
|
|
397
|
+
if (at >= (latestAt.get(ticketId) ?? -1)) {
|
|
398
|
+
latestAt.set(ticketId, at);
|
|
399
|
+
latestSnapshot.set(ticketId, row.get("value") ?? {});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (latestSnapshot.size === 0) {
|
|
404
|
+
return "no reconstructed history found (has pass 12 run?)";
|
|
405
|
+
}
|
|
406
|
+
// Compare only the fields the replay actually tracks; anything the fold set
|
|
407
|
+
// from a source the replay has no events for is out of scope here.
|
|
408
|
+
const tracked = [
|
|
409
|
+
"title",
|
|
410
|
+
"priority",
|
|
411
|
+
"downtime",
|
|
412
|
+
"frequency",
|
|
413
|
+
"restriction",
|
|
414
|
+
"status",
|
|
415
|
+
"equipment",
|
|
416
|
+
"project",
|
|
417
|
+
"material",
|
|
418
|
+
];
|
|
419
|
+
const mismatches = [];
|
|
420
|
+
for await (const page of (0, pass_js_1.pages)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Service_Ticket), tenantId), batchSize)) {
|
|
421
|
+
for (const t of page) {
|
|
422
|
+
if (!t.id)
|
|
423
|
+
continue;
|
|
424
|
+
const snapshot = latestSnapshot.get(t.id);
|
|
425
|
+
if (!snapshot) {
|
|
426
|
+
if (mismatches.length < 10) {
|
|
427
|
+
mismatches.push(`${t.id} has no reconstructed history`);
|
|
428
|
+
}
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
for (const field of tracked) {
|
|
432
|
+
if (!(field in snapshot))
|
|
433
|
+
continue; // never edited → nothing to compare
|
|
434
|
+
const fromReplay = (0, pass_js_1.refId)(snapshot[field]) ?? snapshot[field];
|
|
435
|
+
const onTicket = (0, pass_js_1.refId)(t.get(field)) ?? t.get(field);
|
|
436
|
+
if (fromReplay !== undefined &&
|
|
437
|
+
onTicket !== undefined &&
|
|
438
|
+
comparable(fromReplay) !== comparable(onTicket) &&
|
|
439
|
+
mismatches.length < 10) {
|
|
440
|
+
mismatches.push(`${t.id}.${field}: replay ${comparable(fromReplay)} vs ` +
|
|
441
|
+
`ticket ${comparable(onTicket)}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return mismatches.length ? `e.g. ${mismatches.join("; ")}` : undefined;
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
name: "reconstructed history kept its historical timestamps",
|
|
451
|
+
hard: true,
|
|
452
|
+
async run(_tenantId, batchSize) {
|
|
453
|
+
// Under the master key Parse silently stamps `createdAt = now`, which would
|
|
454
|
+
// collapse the whole trail onto the migration date. This is the check that
|
|
455
|
+
// catches a run that used the wrong key.
|
|
456
|
+
const cutoff = Date.now() - 60 * 60 * 1000;
|
|
457
|
+
let total = 0;
|
|
458
|
+
let recent = 0;
|
|
459
|
+
for await (const page of (0, pass_js_1.pages)(() => new node_1.default.Query(index_js_1.Changelog).equalTo("nameOfClass", index_js_1.Service_Ticket.className), batchSize)) {
|
|
460
|
+
for (const row of page) {
|
|
461
|
+
const context = row.get("context");
|
|
462
|
+
if (!context?.migrated)
|
|
463
|
+
continue;
|
|
464
|
+
total += 1;
|
|
465
|
+
if ((row.createdAt?.getTime() ?? 0) > cutoff)
|
|
466
|
+
recent += 1;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (total === 0)
|
|
470
|
+
return undefined;
|
|
471
|
+
return recent === total
|
|
472
|
+
? `all ${total} reconstructed entries were created within the last hour — ` +
|
|
473
|
+
`timestamps were NOT preserved (was the maintenance key actually used?)`
|
|
474
|
+
: undefined;
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
name: "no auto-generated changelog entries for migrated objects",
|
|
479
|
+
hard: true,
|
|
480
|
+
async run(_tenantId, batchSize) {
|
|
481
|
+
// Every migration write passes `context: { skipChangelog: true }`. A pass
|
|
482
|
+
// that forgets it leaves a fingerprint nothing in normal operation
|
|
483
|
+
// produces: `_ChangeLog`'s afterSave stamps `masterkey: request.master`,
|
|
484
|
+
// and a maintenance-key write is **not** master — `Auth.maintenance()`
|
|
485
|
+
// leaves `isMaster: false` — nor does it carry a user. So the tell is an
|
|
486
|
+
// entry with no acting user that is not master-key.
|
|
487
|
+
//
|
|
488
|
+
// Explicitly NOT `masterkey === true`, which is what this check looked for
|
|
489
|
+
// originally and could therefore never find: hook-written entries under the
|
|
490
|
+
// master key are legitimate v2 activity, and the migration's own writes
|
|
491
|
+
// never set that flag.
|
|
492
|
+
//
|
|
493
|
+
// Pass 12's entries match the same shape whenever the v1 row had no `user`,
|
|
494
|
+
// so they are excluded by their `context.migrated` marker rather than by
|
|
495
|
+
// their author. Both filters run in memory so an absent `actingUser` and a
|
|
496
|
+
// null one are treated alike.
|
|
497
|
+
const offenders = [];
|
|
498
|
+
let total = 0;
|
|
499
|
+
for await (const page of (0, pass_js_1.pages)(() => new node_1.default.Query(index_js_1.Changelog)
|
|
500
|
+
.containedIn("nameOfClass", [
|
|
501
|
+
index_js_1.Service_Ticket.className,
|
|
502
|
+
index_js_1.Service_Status.className,
|
|
503
|
+
index_js_1.Service_Tag.className,
|
|
504
|
+
index_js_1.Service_Equipment.className,
|
|
505
|
+
index_js_1.Service_Project.className,
|
|
506
|
+
])
|
|
507
|
+
.notEqualTo("masterkey", true), batchSize)) {
|
|
508
|
+
for (const row of page) {
|
|
509
|
+
const context = row.get("context");
|
|
510
|
+
if (context?.migrated)
|
|
511
|
+
continue; // written by pass 12, on purpose
|
|
512
|
+
if (row.get("actingUser"))
|
|
513
|
+
continue; // a real edit by a real user
|
|
514
|
+
total += 1;
|
|
515
|
+
if (offenders.length < 10) {
|
|
516
|
+
offenders.push(`${row.get("nameOfClass")} ${row.get("changedObject")}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return total > 0
|
|
521
|
+
? `${total} auto-generated changelog entr(ies) — a pass is missing ` +
|
|
522
|
+
`skipChangelog. e.g. ${offenders.join("; ")}`
|
|
523
|
+
: undefined;
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
// ── Soft: distribution sanity ────────────────────────────────────────────
|
|
527
|
+
{
|
|
528
|
+
name: "ticket status distribution looks plausible",
|
|
529
|
+
hard: false,
|
|
530
|
+
async run(tenantId, batchSize) {
|
|
531
|
+
const byCategory = new Map();
|
|
532
|
+
const categoryOf = new Map();
|
|
533
|
+
for await (const page of (0, pass_js_1.pages)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Service_Status), tenantId), batchSize)) {
|
|
534
|
+
for (const s of page) {
|
|
535
|
+
if (s.id)
|
|
536
|
+
categoryOf.set(s.id, s.get("category"));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
for await (const page of (0, pass_js_1.pages)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Service_Ticket), tenantId), batchSize)) {
|
|
540
|
+
for (const t of page) {
|
|
541
|
+
const category = categoryOf.get((0, pass_js_1.refId)(t.get("status")) ?? "") ?? "(unknown)";
|
|
542
|
+
byCategory.set(category, (byCategory.get(category) ?? 0) + 1);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const summary = [...byCategory.entries()]
|
|
546
|
+
.sort((a, b) => b[1] - a[1])
|
|
547
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
548
|
+
.join(" ");
|
|
549
|
+
// Informational: always reported so the numbers land in the run log.
|
|
550
|
+
return `distribution: ${summary}`;
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
];
|
|
554
|
+
async function main() {
|
|
555
|
+
let config;
|
|
556
|
+
try {
|
|
557
|
+
config = (0, env_js_1.readConfig)(process.argv.slice(2));
|
|
558
|
+
}
|
|
559
|
+
catch (error) {
|
|
560
|
+
if (error instanceof env_js_1.MigrationConfigError) {
|
|
561
|
+
console.error(`\n✗ ${error.message}\n`);
|
|
562
|
+
return 2;
|
|
563
|
+
}
|
|
564
|
+
throw error;
|
|
565
|
+
}
|
|
566
|
+
(0, env_js_1.initParse)(config);
|
|
567
|
+
// Probe once before running the checks. Without this, an unreachable server or a
|
|
568
|
+
// rejected key makes every check throw, and the summary reads "N hard invariants
|
|
569
|
+
// failed — do not cut over" — which looks like N data problems rather than one
|
|
570
|
+
// connection problem.
|
|
571
|
+
try {
|
|
572
|
+
await new node_1.default.Query(index_js_1.Service_Ticket).limit(1).find(env_js_1.MK);
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
console.error(`\n✗ Could not query ${config.parseUrl}.\n` +
|
|
576
|
+
` ${error.message}\n` +
|
|
577
|
+
` Check that Parse Server is reachable there, that PARSE_APP_ID matches, ` +
|
|
578
|
+
`that PARSE_MAINTENANCE_KEY is the server's maintenanceKey, and that this ` +
|
|
579
|
+
`host is allowed by maintenanceKeyIps (default: localhost only).\n` +
|
|
580
|
+
` Nothing was verified — this is not a data problem.\n`);
|
|
581
|
+
return 2;
|
|
582
|
+
}
|
|
583
|
+
console.log(`\nverifying — tenant ${config.tenantId ?? "(all)"}\n`);
|
|
584
|
+
let hardFailures = 0;
|
|
585
|
+
let softFailures = 0;
|
|
586
|
+
for (const check of checks) {
|
|
587
|
+
let failure;
|
|
588
|
+
try {
|
|
589
|
+
failure = await check.run(config.tenantId, config.batchSize);
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
failure = `check threw: ${error.message}`;
|
|
593
|
+
}
|
|
594
|
+
if (!failure) {
|
|
595
|
+
console.log(` ✓ ${check.name}`);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (check.hard) {
|
|
599
|
+
hardFailures++;
|
|
600
|
+
console.error(` ✗ ${check.name}\n ${failure}`);
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
softFailures++;
|
|
604
|
+
console.warn(` ! ${check.name}\n ${failure}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
console.log("");
|
|
608
|
+
if (hardFailures > 0) {
|
|
609
|
+
console.error(`✗ ${hardFailures} hard invariant(s) failed — do not cut over.\n`);
|
|
610
|
+
return 1;
|
|
611
|
+
}
|
|
612
|
+
console.log(softFailures > 0
|
|
613
|
+
? `✓ hard invariants pass; ${softFailures} soft check(s) want a look.\n`
|
|
614
|
+
: `✓ all checks pass.\n`);
|
|
615
|
+
return 0;
|
|
616
|
+
}
|
|
617
|
+
main().then((code) => process.exit(code), (error) => {
|
|
618
|
+
console.error(error);
|
|
619
|
+
process.exit(1);
|
|
620
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed **v1** (`OD3_Maintenance_*`) test data, for exercising the v1 → v2 migration.
|
|
3
|
+
*
|
|
4
|
+
* DEV ONLY. Never run this against production.
|
|
5
|
+
*
|
|
6
|
+
* OD_MIGRATION_MODE=1 PARSE_APP_ID=… PARSE_MAINTENANCE_KEY=… \
|
|
7
|
+
* node dist/scripts/seed-v1-testdata.js [--purge]
|
|
8
|
+
*
|
|
9
|
+
* ## Why the maintenance key
|
|
10
|
+
*
|
|
11
|
+
* Realistic timestamps are the whole point. The migration's fold picks the newest
|
|
12
|
+
* side-table row and its replay builds a timeline from them — neither can be tested
|
|
13
|
+
* against data where every row was created in the same second. Historical
|
|
14
|
+
* `createdAt` / `updatedAt` require the maintenance key, so this seeder needs the
|
|
15
|
+
* same setup as the migration. That makes it a useful smoke test of the write path
|
|
16
|
+
* in its own right: if this runs, the migration's transport works.
|
|
17
|
+
*
|
|
18
|
+
* ## Why OD_MIGRATION_MODE
|
|
19
|
+
*
|
|
20
|
+
* All 29 v1 hooks call `defaultHandler`, which throws on a request with no user —
|
|
21
|
+
* and maintenance-key requests have none. The flag gates that off, so this seeder
|
|
22
|
+
* sets `tenant`, `user` and ACLs itself, exactly as the migration does.
|
|
23
|
+
*
|
|
24
|
+
* ## What it deliberately includes
|
|
25
|
+
*
|
|
26
|
+
* Every case the migration audits turned up, so a dry run has something to say:
|
|
27
|
+
*
|
|
28
|
+
* - two tenants, each with their own kanban states and meta fields
|
|
29
|
+
* - a ticket with no kanban state at all (inbox fallback)
|
|
30
|
+
* - a ticket with `enabled: false`, and one with `enabled` *unset*
|
|
31
|
+
* - side-table rows where `updatedAt` disagrees with `createdAt` ordering
|
|
32
|
+
* - a completed todo, and a todo whose title is an i18n key
|
|
33
|
+
* - a comment with an i18n-key title, and one with no author but an attachment
|
|
34
|
+
* - a source with no tag, and a source tree needing ancestors
|
|
35
|
+
* - an item with a German-decimal price and one with an unparseable price
|
|
36
|
+
* - a two-level issue-category tree (closure expansion)
|
|
37
|
+
* - an assignment batch (three rows, one `batchStamp`)
|
|
38
|
+
* - a message referencing a non-ticket object (orphan skip)
|
|
39
|
+
* - assets on a real and on a `temp-` media context
|
|
40
|
+
*/
|
|
41
|
+
export {};
|