@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,176 @@
|
|
|
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.pass03 = 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 map_js_1 = require("../lib/map.js");
|
|
11
|
+
const latest_js_1 = require("../lib/latest.js");
|
|
12
|
+
const ptr_js_1 = require("../lib/ptr.js");
|
|
13
|
+
const pass_js_1 = require("../lib/pass.js");
|
|
14
|
+
/**
|
|
15
|
+
* Collect the ids of every `OD3_Source` that v1 data actually points at.
|
|
16
|
+
*
|
|
17
|
+
* D3 decided: referenced sources only, not the whole source tree. A data source is
|
|
18
|
+
* an openware/sensor concept and most of them are not physical equipment; importing
|
|
19
|
+
* all of them would fill the equipment picker with noise. Ancestors are added later
|
|
20
|
+
* regardless, or the hierarchy breaks.
|
|
21
|
+
*/
|
|
22
|
+
async function referencedSourceIds(ctx) {
|
|
23
|
+
const { config } = ctx;
|
|
24
|
+
const ids = new Set();
|
|
25
|
+
// Tickets reference a source through the side table.
|
|
26
|
+
for (const row of await (0, pass_js_1.all)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Maintenance_Ticket_Source), config.tenantId), config.batchSize)) {
|
|
27
|
+
const id = (0, pass_js_1.refId)(row.get("source"));
|
|
28
|
+
if (id)
|
|
29
|
+
ids.add(id);
|
|
30
|
+
}
|
|
31
|
+
// Spare parts (pass 05) map `sources` → `equipment`, so those count too.
|
|
32
|
+
for (const item of await (0, pass_js_1.all)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Maintenance_Item), config.tenantId), config.batchSize)) {
|
|
33
|
+
for (const src of await (0, latest_js_1.relationObjects)(item, "sources", config.batchSize)) {
|
|
34
|
+
if (src.id)
|
|
35
|
+
ids.add(src.id);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Schedules are deferred (MIGRATION_PLAN §4.5) but their sources are included
|
|
39
|
+
// anyway: the equipment they will need should exist before that work starts, and
|
|
40
|
+
// a schedule's source is by definition real equipment.
|
|
41
|
+
for (const schedule of await (0, pass_js_1.all)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Maintenance_Schedule), config.tenantId), config.batchSize)) {
|
|
42
|
+
const id = (0, pass_js_1.refId)(schedule.get("source"));
|
|
43
|
+
if (id)
|
|
44
|
+
ids.add(id);
|
|
45
|
+
}
|
|
46
|
+
return ids;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Pass 03 — `OD3_Source` → `OD3_Service_Equipment`.
|
|
50
|
+
*
|
|
51
|
+
* The one genuine remodelling in the migration. v1 attaches tickets to a data
|
|
52
|
+
* source; v2 attaches them to Equipment, which is only loosely coupled back to
|
|
53
|
+
* sources through `Equipment.sourceTags`.
|
|
54
|
+
*
|
|
55
|
+
* `source.tag` is not unique — several sources can carry the same tag — so
|
|
56
|
+
* Equipment→Source is deliberately not a bijection. Storing the tag string rather
|
|
57
|
+
* than a source id is the documented intent (see the openservice-core equipment
|
|
58
|
+
* README).
|
|
59
|
+
*/
|
|
60
|
+
exports.pass03 = {
|
|
61
|
+
id: "03",
|
|
62
|
+
title: "equipment",
|
|
63
|
+
needs: [],
|
|
64
|
+
async run(ctx) {
|
|
65
|
+
const { config, map, report, entry } = ctx;
|
|
66
|
+
const referenced = await referencedSourceIds(ctx);
|
|
67
|
+
if (referenced.size === 0) {
|
|
68
|
+
report.warn(entry, "no source is referenced by any ticket, item or schedule");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Load every source once; the tree walk needs arbitrary parents anyway.
|
|
72
|
+
const allSources = await (0, pass_js_1.all)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Source), config.tenantId), config.batchSize);
|
|
73
|
+
const byId = new Map();
|
|
74
|
+
for (const s of allSources)
|
|
75
|
+
if (s.id)
|
|
76
|
+
byId.set(s.id, s);
|
|
77
|
+
// Close the referenced set over `parent`, so no equipment is left orphaned.
|
|
78
|
+
const needed = new Set();
|
|
79
|
+
for (const id of referenced) {
|
|
80
|
+
let cursor = id;
|
|
81
|
+
const guard = new Set();
|
|
82
|
+
while (cursor && byId.has(cursor) && !guard.has(cursor)) {
|
|
83
|
+
guard.add(cursor);
|
|
84
|
+
needed.add(cursor);
|
|
85
|
+
cursor = (0, pass_js_1.refId)(byId.get(cursor).get("parent"));
|
|
86
|
+
}
|
|
87
|
+
if (cursor && guard.has(cursor)) {
|
|
88
|
+
report.error(entry, `source ${id} sits in a parent cycle`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const ancestorsAdded = needed.size - referenced.size;
|
|
92
|
+
report.info(entry, `${referenced.size} referenced source(s) + ${ancestorsAdded} ancestor(s) = ` +
|
|
93
|
+
`${needed.size} equipment; ${allSources.length - needed.size} source(s) skipped (D3)`);
|
|
94
|
+
// Parents before children, so `Equipment.parent` can resolve through the map.
|
|
95
|
+
const depth = (id) => {
|
|
96
|
+
let d = 0;
|
|
97
|
+
let cursor = id;
|
|
98
|
+
const guard = new Set();
|
|
99
|
+
while (cursor && byId.has(cursor) && !guard.has(cursor)) {
|
|
100
|
+
guard.add(cursor);
|
|
101
|
+
cursor = (0, pass_js_1.refId)(byId.get(cursor).get("parent"));
|
|
102
|
+
if (cursor && needed.has(cursor))
|
|
103
|
+
d += 1;
|
|
104
|
+
else
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
return d;
|
|
108
|
+
};
|
|
109
|
+
const ordered = [...needed]
|
|
110
|
+
.map((id) => byId.get(id))
|
|
111
|
+
.sort((a, b) => depth(a.id) - depth(b.id));
|
|
112
|
+
for (const source of ordered) {
|
|
113
|
+
const sourceId = source.id;
|
|
114
|
+
if (!sourceId)
|
|
115
|
+
continue;
|
|
116
|
+
const existingId = map.get(index_js_1.Source.className, sourceId);
|
|
117
|
+
const target = existingId
|
|
118
|
+
? new index_js_1.Service_Equipment({ objectId: existingId })
|
|
119
|
+
: new index_js_1.Service_Equipment();
|
|
120
|
+
const tenantId = (0, pass_js_1.refId)(source.get("tenant"));
|
|
121
|
+
if (!tenantId) {
|
|
122
|
+
// `Equipment.tenant` is required, so this row cannot be written.
|
|
123
|
+
report.error(entry, `source ${sourceId} ("${source.get("name")}") has no tenant; ` +
|
|
124
|
+
`Equipment.tenant is required — skipped`);
|
|
125
|
+
entry.skipped++;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const parentTargetId = (() => {
|
|
129
|
+
const parentSourceId = (0, pass_js_1.refId)(source.get("parent"));
|
|
130
|
+
return parentSourceId ? map.get(index_js_1.Source.className, parentSourceId) : undefined;
|
|
131
|
+
})();
|
|
132
|
+
target.set("label", source.get("name") ?? sourceId);
|
|
133
|
+
target.set("deleted", false);
|
|
134
|
+
target.set("tenant", (0, ptr_js_1.tenantPtr)(tenantId));
|
|
135
|
+
// Stored raw as a string array; the tag is what links equipment back to a
|
|
136
|
+
// source at read time.
|
|
137
|
+
target.set("sourceTags", source.get("tag") ? [source.get("tag")] : []);
|
|
138
|
+
const parentPtr = (0, ptr_js_1.maybePtr)(index_js_1.Service_Equipment.className, parentTargetId);
|
|
139
|
+
if (parentPtr)
|
|
140
|
+
target.set("parent", parentPtr);
|
|
141
|
+
if (!source.get("tag")) {
|
|
142
|
+
report.warn(entry, `source ${sourceId} ("${source.get("name")}") has no tag — the equipment ` +
|
|
143
|
+
`will not match any source at read time`);
|
|
144
|
+
}
|
|
145
|
+
if (!existingId) {
|
|
146
|
+
target.setACL((0, acl_js_1.migrationAcl)({ tenantId, allowTenantUserWrite: true }));
|
|
147
|
+
}
|
|
148
|
+
if (config.dryRun) {
|
|
149
|
+
existingId ? entry.updated++ : entry.created++;
|
|
150
|
+
if (!existingId) {
|
|
151
|
+
await map.record(index_js_1.Source.className, sourceId, index_js_1.Service_Equipment.className, map_js_1.MigrationMap.dryId(sourceId));
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const newId = await (0, acl_js_1.saveMigrated)(config, target, {
|
|
157
|
+
className: index_js_1.Service_Equipment.className,
|
|
158
|
+
isNew: !existingId,
|
|
159
|
+
createdAt: source.createdAt,
|
|
160
|
+
updatedAt: source.updatedAt,
|
|
161
|
+
});
|
|
162
|
+
if (existingId) {
|
|
163
|
+
entry.updated++;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
entry.created++;
|
|
167
|
+
await map.record(index_js_1.Source.className, sourceId, index_js_1.Service_Equipment.className, newId);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
report.error(entry, `source ${sourceId} failed: ${error.message}`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Pass } from "../lib/pass.js";
|
|
2
|
+
/**
|
|
3
|
+
* A 3-letter `Project.shortcode`, derived from the label.
|
|
4
|
+
*
|
|
5
|
+
* v1 has no equivalent column, so this is synthesised. Uniqueness is **per
|
|
6
|
+
* tenant**, which is also how the field reads in the UI.
|
|
7
|
+
*
|
|
8
|
+
* Candidates, in order: the first three letters; then the first two plus each
|
|
9
|
+
* later letter (so "Instandhaltung" and "Instandsetzung" become INS and INT
|
|
10
|
+
* rather than INS and INS1); then a numeric suffix as a last resort. Diacritics
|
|
11
|
+
* are folded rather than dropped, or "Öltank" would become "LTA".
|
|
12
|
+
*/
|
|
13
|
+
export declare function shortcodeCandidates(label: string): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Pass 04 — `OD3_Maintenance_Project` → `OD3_Service_Project`.
|
|
16
|
+
*
|
|
17
|
+
* `ticketCounter` starts at 0, and only on a newly created project — a re-run must
|
|
18
|
+
* not reset it. The backend `beforeSave` that normally increments it is not
|
|
19
|
+
* registered in migration mode (it is what would strip the historical `createdAt`),
|
|
20
|
+
* so `TicketNumberer` in pass 08 seeds itself from these values, hands out the
|
|
21
|
+
* numbers in-process and writes the final counter back at the end of that pass.
|
|
22
|
+
*
|
|
23
|
+
* Dropped per MIGRATION_PLAN A4: the `assignedUsers` / `assignedRoles` relations
|
|
24
|
+
* (no equivalent on the v2 Project) and the `tickets` relation (replaced by the
|
|
25
|
+
* inverse `Ticket.project`). `finished` maps to `deleted` (D4).
|
|
26
|
+
*/
|
|
27
|
+
export declare const pass04: Pass;
|
|
@@ -0,0 +1,241 @@
|
|
|
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.pass04 = void 0;
|
|
7
|
+
exports.shortcodeCandidates = shortcodeCandidates;
|
|
8
|
+
const node_1 = __importDefault(require("parse/node"));
|
|
9
|
+
const index_js_1 = require("../../../types/index.js");
|
|
10
|
+
const acl_js_1 = require("../lib/acl.js");
|
|
11
|
+
const map_js_1 = require("../lib/map.js");
|
|
12
|
+
const env_js_1 = require("../lib/env.js");
|
|
13
|
+
const latest_js_1 = require("../lib/latest.js");
|
|
14
|
+
const ptr_js_1 = require("../lib/ptr.js");
|
|
15
|
+
const pass_js_1 = require("../lib/pass.js");
|
|
16
|
+
/**
|
|
17
|
+
* The inbox Status a project should default to.
|
|
18
|
+
*
|
|
19
|
+
* Pass 01 produces exactly one per tenant in the normal case; the oldest wins if a
|
|
20
|
+
* v1 tenant somehow had several inbox states.
|
|
21
|
+
*/
|
|
22
|
+
async function inboxStatusId(tenantId) {
|
|
23
|
+
const query = new node_1.default.Query(index_js_1.Service_Status)
|
|
24
|
+
.equalTo("category", "inbox")
|
|
25
|
+
.ascending("createdAt");
|
|
26
|
+
if (tenantId) {
|
|
27
|
+
query.equalTo("tenant", (0, ptr_js_1.tenantPtr)(tenantId));
|
|
28
|
+
}
|
|
29
|
+
const status = await query.first(env_js_1.MK);
|
|
30
|
+
return status?.id;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A 3-letter `Project.shortcode`, derived from the label.
|
|
34
|
+
*
|
|
35
|
+
* v1 has no equivalent column, so this is synthesised. Uniqueness is **per
|
|
36
|
+
* tenant**, which is also how the field reads in the UI.
|
|
37
|
+
*
|
|
38
|
+
* Candidates, in order: the first three letters; then the first two plus each
|
|
39
|
+
* later letter (so "Instandhaltung" and "Instandsetzung" become INS and INT
|
|
40
|
+
* rather than INS and INS1); then a numeric suffix as a last resort. Diacritics
|
|
41
|
+
* are folded rather than dropped, or "Öltank" would become "LTA".
|
|
42
|
+
*/
|
|
43
|
+
function shortcodeCandidates(label) {
|
|
44
|
+
const cleaned = (label ?? "")
|
|
45
|
+
.normalize("NFD")
|
|
46
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
47
|
+
.replace(/ß/gi, "SS")
|
|
48
|
+
.toUpperCase()
|
|
49
|
+
.replace(/[^A-Z0-9]/g, "");
|
|
50
|
+
// A label with no usable characters at all still needs a code.
|
|
51
|
+
if (cleaned.length === 0) {
|
|
52
|
+
return Array.from({ length: 99 }, (_, i) => `P${String(i + 1).padStart(2, "0")}`);
|
|
53
|
+
}
|
|
54
|
+
const out = [];
|
|
55
|
+
const push = (code) => {
|
|
56
|
+
const c = code.slice(0, 3).padEnd(3, "X");
|
|
57
|
+
if (!out.includes(c))
|
|
58
|
+
out.push(c);
|
|
59
|
+
};
|
|
60
|
+
push(cleaned);
|
|
61
|
+
const head = cleaned.slice(0, 2).padEnd(2, "X");
|
|
62
|
+
for (let i = 3; i < cleaned.length; i++)
|
|
63
|
+
push(head + cleaned[i]);
|
|
64
|
+
for (let n = 1; n <= 9; n++)
|
|
65
|
+
push(head + n);
|
|
66
|
+
for (let n = 10; n <= 99; n++)
|
|
67
|
+
push(cleaned[0] + n);
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
/** First candidate not already taken in this tenant. */
|
|
71
|
+
function pickShortcode(label, taken) {
|
|
72
|
+
for (const candidate of shortcodeCandidates(label)) {
|
|
73
|
+
if (!taken.has(candidate))
|
|
74
|
+
return candidate;
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Pass 04 — `OD3_Maintenance_Project` → `OD3_Service_Project`.
|
|
80
|
+
*
|
|
81
|
+
* `ticketCounter` starts at 0, and only on a newly created project — a re-run must
|
|
82
|
+
* not reset it. The backend `beforeSave` that normally increments it is not
|
|
83
|
+
* registered in migration mode (it is what would strip the historical `createdAt`),
|
|
84
|
+
* so `TicketNumberer` in pass 08 seeds itself from these values, hands out the
|
|
85
|
+
* numbers in-process and writes the final counter back at the end of that pass.
|
|
86
|
+
*
|
|
87
|
+
* Dropped per MIGRATION_PLAN A4: the `assignedUsers` / `assignedRoles` relations
|
|
88
|
+
* (no equivalent on the v2 Project) and the `tickets` relation (replaced by the
|
|
89
|
+
* inverse `Ticket.project`). `finished` maps to `deleted` (D4).
|
|
90
|
+
*/
|
|
91
|
+
exports.pass04 = {
|
|
92
|
+
id: "04",
|
|
93
|
+
title: "projects",
|
|
94
|
+
needs: ["01"],
|
|
95
|
+
async run({ config, map, report, entry }) {
|
|
96
|
+
const projects = await (0, pass_js_1.all)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Maintenance_Project), config.tenantId), config.batchSize);
|
|
97
|
+
if (projects.length === 0)
|
|
98
|
+
return;
|
|
99
|
+
// Shortcodes already in use, per tenant. Seeded from every existing v2
|
|
100
|
+
// project — not just migrated ones — so a code cannot collide with one a
|
|
101
|
+
// user typed in by hand.
|
|
102
|
+
const takenByTenant = new Map();
|
|
103
|
+
const takenFor = (id) => {
|
|
104
|
+
const existing = takenByTenant.get(id);
|
|
105
|
+
if (existing)
|
|
106
|
+
return existing;
|
|
107
|
+
const created = new Set();
|
|
108
|
+
takenByTenant.set(id, created);
|
|
109
|
+
return created;
|
|
110
|
+
};
|
|
111
|
+
for (const existing of await (0, pass_js_1.all)(() => new node_1.default.Query(index_js_1.Service_Project), config.batchSize)) {
|
|
112
|
+
const code = existing.get("shortcode");
|
|
113
|
+
const owner = (0, pass_js_1.refId)(existing.get("tenant"));
|
|
114
|
+
if (typeof code === "string" && code && owner)
|
|
115
|
+
takenFor(owner).add(code);
|
|
116
|
+
}
|
|
117
|
+
const defaultStatusId = await inboxStatusId(config.tenantId);
|
|
118
|
+
if (!defaultStatusId) {
|
|
119
|
+
report.warn(entry, "no inbox Status found — projects will be created without a defaultStatus " +
|
|
120
|
+
"(run pass 01 first)");
|
|
121
|
+
}
|
|
122
|
+
// Parents before children so `Project.parent` resolves through the map.
|
|
123
|
+
const byId = new Map();
|
|
124
|
+
for (const p of projects)
|
|
125
|
+
if (p.id)
|
|
126
|
+
byId.set(p.id, p);
|
|
127
|
+
const depth = (project) => {
|
|
128
|
+
let d = 0;
|
|
129
|
+
let cursor = (0, pass_js_1.refId)(project.get("parent"));
|
|
130
|
+
const guard = new Set();
|
|
131
|
+
while (cursor && byId.has(cursor) && !guard.has(cursor)) {
|
|
132
|
+
guard.add(cursor);
|
|
133
|
+
d += 1;
|
|
134
|
+
cursor = (0, pass_js_1.refId)(byId.get(cursor).get("parent"));
|
|
135
|
+
}
|
|
136
|
+
return d;
|
|
137
|
+
};
|
|
138
|
+
const ordered = [...projects].sort((a, b) => depth(a) - depth(b));
|
|
139
|
+
let droppedAssignments = 0;
|
|
140
|
+
for (const project of ordered) {
|
|
141
|
+
const sourceId = project.id;
|
|
142
|
+
if (!sourceId)
|
|
143
|
+
continue;
|
|
144
|
+
const existingId = map.get(index_js_1.Maintenance_Project.className, sourceId);
|
|
145
|
+
const target = existingId
|
|
146
|
+
? new index_js_1.Service_Project({ objectId: existingId })
|
|
147
|
+
: new index_js_1.Service_Project();
|
|
148
|
+
const tenantId = (0, pass_js_1.refId)(project.get("tenant"));
|
|
149
|
+
if (!tenantId) {
|
|
150
|
+
report.error(entry, `project ${sourceId} ("${project.get("name")}") has no tenant; ` +
|
|
151
|
+
`Project.tenant is required — skipped`);
|
|
152
|
+
entry.skipped++;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const parentSourceId = (0, pass_js_1.refId)(project.get("parent"));
|
|
156
|
+
const parentTargetId = parentSourceId
|
|
157
|
+
? map.get(index_js_1.Maintenance_Project.className, parentSourceId)
|
|
158
|
+
: undefined;
|
|
159
|
+
const ownerId = (0, pass_js_1.refId)(project.get("user"));
|
|
160
|
+
target.set("label", project.get("name"));
|
|
161
|
+
target.set("tenant", (0, ptr_js_1.tenantPtr)(tenantId));
|
|
162
|
+
// v1 had no soft-delete on projects; `finished` is the closest thing and D4
|
|
163
|
+
// chose to reuse `deleted` rather than add a column.
|
|
164
|
+
target.set("deleted", project.get("finished") === true);
|
|
165
|
+
if (!existingId)
|
|
166
|
+
target.set("ticketCounter", 0);
|
|
167
|
+
if (!existingId) {
|
|
168
|
+
// Only on create. On a re-run `target` is a bare stub with no loaded
|
|
169
|
+
// attributes, so setting this would overwrite whatever the project
|
|
170
|
+
// already carries — including a code a user has since edited.
|
|
171
|
+
const taken = takenFor(tenantId);
|
|
172
|
+
const shortcode = pickShortcode(project.get("name") ?? "", taken);
|
|
173
|
+
if (shortcode) {
|
|
174
|
+
target.set("shortcode", shortcode);
|
|
175
|
+
taken.add(shortcode);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
report.warn(entry, `project ${sourceId} ("${project.get("name")}") could not be given a ` +
|
|
179
|
+
`unique shortcode — tenant ${tenantId} has exhausted the candidates`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (project.get("description"))
|
|
183
|
+
target.set("description", project.get("description"));
|
|
184
|
+
if (project.get("color"))
|
|
185
|
+
target.set("color", project.get("color"));
|
|
186
|
+
if (project.get("icon"))
|
|
187
|
+
target.set("icon", project.get("icon"));
|
|
188
|
+
if (project.get("start"))
|
|
189
|
+
target.set("start", project.get("start"));
|
|
190
|
+
if (project.get("end"))
|
|
191
|
+
target.set("end", project.get("end"));
|
|
192
|
+
if (ownerId)
|
|
193
|
+
target.set("manager", (0, ptr_js_1.userPtr)(ownerId));
|
|
194
|
+
const parentPtr = (0, ptr_js_1.maybePtr)(index_js_1.Service_Project.className, parentTargetId);
|
|
195
|
+
if (parentPtr)
|
|
196
|
+
target.set("parent", parentPtr);
|
|
197
|
+
const statusPtr = (0, ptr_js_1.maybePtr)(index_js_1.Service_Status.className, defaultStatusId);
|
|
198
|
+
if (statusPtr)
|
|
199
|
+
target.set("defaultStatus", statusPtr);
|
|
200
|
+
// Report the relations that have nowhere to go, rather than losing them
|
|
201
|
+
// quietly — a customer using project-level assignment will notice.
|
|
202
|
+
for (const field of ["assignedUsers", "assignedRoles"]) {
|
|
203
|
+
const ids = await (0, latest_js_1.relationIds)(project, field, config.batchSize);
|
|
204
|
+
droppedAssignments += ids.length;
|
|
205
|
+
}
|
|
206
|
+
if (!existingId) {
|
|
207
|
+
target.setACL((0, acl_js_1.migrationAcl)({ tenantId, userId: ownerId, allowTenantUserWrite: true }));
|
|
208
|
+
}
|
|
209
|
+
if (config.dryRun) {
|
|
210
|
+
existingId ? entry.updated++ : entry.created++;
|
|
211
|
+
if (!existingId) {
|
|
212
|
+
await map.record(index_js_1.Maintenance_Project.className, sourceId, index_js_1.Service_Project.className, map_js_1.MigrationMap.dryId(sourceId));
|
|
213
|
+
}
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const newId = await (0, acl_js_1.saveMigrated)(config, target, {
|
|
218
|
+
className: index_js_1.Service_Project.className,
|
|
219
|
+
isNew: !existingId,
|
|
220
|
+
createdAt: project.createdAt,
|
|
221
|
+
updatedAt: project.updatedAt,
|
|
222
|
+
});
|
|
223
|
+
if (existingId) {
|
|
224
|
+
entry.updated++;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
entry.created++;
|
|
228
|
+
await map.record(index_js_1.Maintenance_Project.className, sourceId, index_js_1.Service_Project.className, newId);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
report.error(entry, `project ${sourceId} failed: ${error.message}`);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (droppedAssignments > 0) {
|
|
237
|
+
report.info(entry, `${droppedAssignments} project-level user/role assignment(s) dropped — the v2 ` +
|
|
238
|
+
`Project has no equivalent (A4)`);
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type Pass } from "../lib/pass.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pass 05 — `OD3_Maintenance_Item` → `OD3_BDE_Article`.
|
|
4
|
+
*
|
|
5
|
+
* The parts *catalogue*. It used to target `OD3_Service_SparePart`; that class is
|
|
6
|
+
* retired and material now references the shared article catalogue instead, so
|
|
7
|
+
* the same v1 rows land in `OD3_BDE_Article` with `type: "spare_part"`.
|
|
8
|
+
*
|
|
9
|
+
* **The shapes do not line up, so the surplus goes to plugin-meta.**
|
|
10
|
+
* `BDE_Article` is a lean catalogue — `articleNumber`, `description`, `unit`,
|
|
11
|
+
* `type`, `targettime` — and it is shared with BDE and its ABAS sync, so it is
|
|
12
|
+
* not ours to widen. v1's stock-keeping fields (price, stock levels, storage
|
|
13
|
+
* location, supplier, category, order number) and its equipment links are
|
|
14
|
+
* therefore written as a `Meta_Entry` under `["service","article","meta"]`,
|
|
15
|
+
* editable through the article admin view in `openservice-core`. Nothing is
|
|
16
|
+
* dropped. See `lib/articleMeta.ts` for why that context and not the ABAS one.
|
|
17
|
+
*
|
|
18
|
+
* Two naming traps, both inherited from BDE: the label column is `description`,
|
|
19
|
+
* not `name`; and the soft-delete flag is `isDeleted`, not `deleted`.
|
|
20
|
+
*
|
|
21
|
+
* Per-ticket consumption (`OD3_Maintenance_Ticket_Material`) is a separate
|
|
22
|
+
* concern — passes 08 and 12 fold and replay it onto `Ticket.material`, which
|
|
23
|
+
* resolves its article ids through the map this pass writes.
|
|
24
|
+
*/
|
|
25
|
+
export declare const pass05: Pass;
|
|
@@ -0,0 +1,203 @@
|
|
|
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.pass05 = 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 articleMeta_js_1 = require("../lib/articleMeta.js");
|
|
11
|
+
const latest_js_1 = require("../lib/latest.js");
|
|
12
|
+
const map_js_1 = require("../lib/map.js");
|
|
13
|
+
const ptr_js_1 = require("../lib/ptr.js");
|
|
14
|
+
const pass_js_1 = require("../lib/pass.js");
|
|
15
|
+
/**
|
|
16
|
+
* `Article.type` for everything this pass creates.
|
|
17
|
+
*
|
|
18
|
+
* `OD3_BDE_Article` is a shared catalogue holding several kinds of thing —
|
|
19
|
+
* products, spare parts — distinguished by `type` rather than by class. Every v1
|
|
20
|
+
* `Maintenance_Item` was a maintenance part, so they all arrive as this.
|
|
21
|
+
* Mirrors `ARTICLE_TYPE.sparePart` in `@opendash/plugin-openservice-core`.
|
|
22
|
+
*/
|
|
23
|
+
const ARTICLE_TYPE_SPARE_PART = "spare_part";
|
|
24
|
+
/**
|
|
25
|
+
* Pass 05 — `OD3_Maintenance_Item` → `OD3_BDE_Article`.
|
|
26
|
+
*
|
|
27
|
+
* The parts *catalogue*. It used to target `OD3_Service_SparePart`; that class is
|
|
28
|
+
* retired and material now references the shared article catalogue instead, so
|
|
29
|
+
* the same v1 rows land in `OD3_BDE_Article` with `type: "spare_part"`.
|
|
30
|
+
*
|
|
31
|
+
* **The shapes do not line up, so the surplus goes to plugin-meta.**
|
|
32
|
+
* `BDE_Article` is a lean catalogue — `articleNumber`, `description`, `unit`,
|
|
33
|
+
* `type`, `targettime` — and it is shared with BDE and its ABAS sync, so it is
|
|
34
|
+
* not ours to widen. v1's stock-keeping fields (price, stock levels, storage
|
|
35
|
+
* location, supplier, category, order number) and its equipment links are
|
|
36
|
+
* therefore written as a `Meta_Entry` under `["service","article","meta"]`,
|
|
37
|
+
* editable through the article admin view in `openservice-core`. Nothing is
|
|
38
|
+
* dropped. See `lib/articleMeta.ts` for why that context and not the ABAS one.
|
|
39
|
+
*
|
|
40
|
+
* Two naming traps, both inherited from BDE: the label column is `description`,
|
|
41
|
+
* not `name`; and the soft-delete flag is `isDeleted`, not `deleted`.
|
|
42
|
+
*
|
|
43
|
+
* Per-ticket consumption (`OD3_Maintenance_Ticket_Material`) is a separate
|
|
44
|
+
* concern — passes 08 and 12 fold and replay it onto `Ticket.material`, which
|
|
45
|
+
* resolves its article ids through the map this pass writes.
|
|
46
|
+
*/
|
|
47
|
+
exports.pass05 = {
|
|
48
|
+
id: "05",
|
|
49
|
+
title: "articles",
|
|
50
|
+
needs: ["03"],
|
|
51
|
+
async run({ config, map, report, entry }) {
|
|
52
|
+
const items = await (0, pass_js_1.all)(() => (0, pass_js_1.scopeToTenant)(new node_1.default.Query(index_js_1.Maintenance_Item), config.tenantId), config.batchSize);
|
|
53
|
+
if (items.length === 0)
|
|
54
|
+
return;
|
|
55
|
+
// Existing entries, so a re-run updates rather than duplicates. Config ids
|
|
56
|
+
// are resolved per tenant on first use.
|
|
57
|
+
const metaByArticle = await (0, articleMeta_js_1.loadArticleMetaEntries)(config);
|
|
58
|
+
const metaConfigByTenant = new Map();
|
|
59
|
+
const contextString = (0, ptr_js_1.contextToString)(articleMeta_js_1.ARTICLE_META_CONTEXT);
|
|
60
|
+
let withMeta = 0;
|
|
61
|
+
let unparseablePrices = 0;
|
|
62
|
+
for (const item of items) {
|
|
63
|
+
const sourceId = item.id;
|
|
64
|
+
if (!sourceId)
|
|
65
|
+
continue;
|
|
66
|
+
const existingId = map.get(index_js_1.Maintenance_Item.className, sourceId);
|
|
67
|
+
const target = existingId
|
|
68
|
+
? new index_js_1.BDE_Article({ objectId: existingId })
|
|
69
|
+
: new index_js_1.BDE_Article();
|
|
70
|
+
const tenantId = (0, pass_js_1.refId)(item.get("tenant"));
|
|
71
|
+
// `description` is BDE's label column — v1's `name` goes here, not into a
|
|
72
|
+
// `name` field, which does not exist.
|
|
73
|
+
target.set("description", item.get("name"));
|
|
74
|
+
target.set("type", ARTICLE_TYPE_SPARE_PART);
|
|
75
|
+
// Soft delete is `isDeleted` here, not `deleted`.
|
|
76
|
+
target.set("isDeleted", false);
|
|
77
|
+
if (tenantId)
|
|
78
|
+
target.set("tenant", (0, ptr_js_1.tenantPtr)(tenantId));
|
|
79
|
+
if (item.get("internalNumber")) {
|
|
80
|
+
target.set("articleNumber", item.get("internalNumber"));
|
|
81
|
+
}
|
|
82
|
+
if (item.get("stockUnit"))
|
|
83
|
+
target.set("unit", item.get("stockUnit"));
|
|
84
|
+
// Everything v1 tracked that the article catalogue has no room for goes
|
|
85
|
+
// to meta rather than being dropped.
|
|
86
|
+
const metaValues = {};
|
|
87
|
+
for (const field of [
|
|
88
|
+
"storageLocation",
|
|
89
|
+
"supplier",
|
|
90
|
+
"category",
|
|
91
|
+
"orderNumber",
|
|
92
|
+
"stockUnit",
|
|
93
|
+
]) {
|
|
94
|
+
const value = item.get(field);
|
|
95
|
+
if (typeof value === "string" && value.trim()) {
|
|
96
|
+
metaValues[field] = value.trim();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const field of ["stock", "stockAlarmAt"]) {
|
|
100
|
+
const value = item.get(field);
|
|
101
|
+
if (typeof value === "number")
|
|
102
|
+
metaValues[field] = value;
|
|
103
|
+
}
|
|
104
|
+
// v1 stored price as a free-form string; keep the number, report the rest
|
|
105
|
+
// rather than silently discarding it.
|
|
106
|
+
const rawPrice = item.get("price");
|
|
107
|
+
const price = (0, articleMeta_js_1.parsePrice)(rawPrice);
|
|
108
|
+
if (price !== undefined)
|
|
109
|
+
metaValues.price = price;
|
|
110
|
+
else if (rawPrice) {
|
|
111
|
+
unparseablePrices += 1;
|
|
112
|
+
report.warn(entry, `item ${sourceId} ("${item.get("name")}") price "${String(rawPrice)}" ` +
|
|
113
|
+
`is not a number — left unset on the meta entry`);
|
|
114
|
+
}
|
|
115
|
+
// `Item.sources` (Relation<Source>) -> the migrated Equipment ids.
|
|
116
|
+
const equipmentIds = [];
|
|
117
|
+
for (const src of await (0, latest_js_1.relationObjects)(item, "sources", config.batchSize)) {
|
|
118
|
+
if (!src.id)
|
|
119
|
+
continue;
|
|
120
|
+
const equipmentId = map.get(index_js_1.Source.className, src.id);
|
|
121
|
+
if (equipmentId)
|
|
122
|
+
equipmentIds.push(equipmentId);
|
|
123
|
+
else {
|
|
124
|
+
report.warn(entry, `item ${sourceId} references source ${src.id}, which has no ` +
|
|
125
|
+
`Equipment (not referenced by a ticket? see D3)`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (equipmentIds.length)
|
|
129
|
+
metaValues.equipment = equipmentIds;
|
|
130
|
+
if (!existingId) {
|
|
131
|
+
target.setACL((0, acl_js_1.migrationAcl)({ tenantId, allowTenantUserWrite: true }));
|
|
132
|
+
}
|
|
133
|
+
if (config.dryRun) {
|
|
134
|
+
existingId ? entry.updated++ : entry.created++;
|
|
135
|
+
// Pass 08 resolves ticket material through this map. Without a
|
|
136
|
+
// placeholder every material line in a dry run would report an
|
|
137
|
+
// unresolved article, burying the genuine findings.
|
|
138
|
+
if (!existingId) {
|
|
139
|
+
await map.record(index_js_1.Maintenance_Item.className, sourceId, index_js_1.BDE_Article.className, map_js_1.MigrationMap.dryId(sourceId));
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const newId = await (0, acl_js_1.saveMigrated)(config, target, {
|
|
145
|
+
className: index_js_1.BDE_Article.className,
|
|
146
|
+
isNew: !existingId,
|
|
147
|
+
createdAt: item.createdAt,
|
|
148
|
+
updatedAt: item.updatedAt,
|
|
149
|
+
});
|
|
150
|
+
if (existingId) {
|
|
151
|
+
entry.updated++;
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
entry.created++;
|
|
155
|
+
await map.record(index_js_1.Maintenance_Item.className, sourceId, index_js_1.BDE_Article.className, newId);
|
|
156
|
+
}
|
|
157
|
+
// Meta last: it needs the article id, and an entry with no values is
|
|
158
|
+
// not worth a row.
|
|
159
|
+
if (Object.keys(metaValues).length > 0) {
|
|
160
|
+
if (!metaConfigByTenant.has(tenantId ?? "")) {
|
|
161
|
+
metaConfigByTenant.set(tenantId ?? "", await (0, articleMeta_js_1.ensureArticleMetaConfig)(config, tenantId ?? ""));
|
|
162
|
+
}
|
|
163
|
+
const existingMeta = metaByArticle.get(newId);
|
|
164
|
+
const metaEntry = existingMeta ?? new index_js_1.Meta_Entry();
|
|
165
|
+
metaEntry.set("context", contextString);
|
|
166
|
+
metaEntry.set("entryClassName", index_js_1.BDE_Article.className);
|
|
167
|
+
metaEntry.set("entryObjectId", newId);
|
|
168
|
+
metaEntry.set("values", metaValues);
|
|
169
|
+
const originPtr = (0, ptr_js_1.maybePtr)("OD3_Meta_Config", metaConfigByTenant.get(tenantId ?? ""));
|
|
170
|
+
if (originPtr)
|
|
171
|
+
metaEntry.set("origin", originPtr);
|
|
172
|
+
if (tenantId)
|
|
173
|
+
metaEntry.set("tenant", (0, ptr_js_1.tenantPtr)(tenantId));
|
|
174
|
+
if (!existingMeta) {
|
|
175
|
+
metaEntry.setACL((0, acl_js_1.migrationAcl)({ tenantId, allowTenantUserWrite: true }));
|
|
176
|
+
}
|
|
177
|
+
await (0, acl_js_1.saveMigrated)(config, metaEntry, {
|
|
178
|
+
className: index_js_1.Meta_Entry.className,
|
|
179
|
+
isNew: !existingMeta,
|
|
180
|
+
createdAt: item.createdAt,
|
|
181
|
+
updatedAt: item.updatedAt,
|
|
182
|
+
});
|
|
183
|
+
metaByArticle.set(newId, metaEntry);
|
|
184
|
+
withMeta += 1;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
report.error(entry, `item ${sourceId} failed: ${error.message}`);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (withMeta > 0) {
|
|
193
|
+
report.info(entry, `${withMeta} article(s) carry stock-keeping data as meta under ` +
|
|
194
|
+
`["service","article","meta"] — editable in the article admin view`);
|
|
195
|
+
}
|
|
196
|
+
if (unparseablePrices > 0) {
|
|
197
|
+
report.info(entry, `${unparseablePrices} price(s) could not be parsed into a number — see ` +
|
|
198
|
+
`above and fix by hand after the run`);
|
|
199
|
+
}
|
|
200
|
+
report.info(entry, `articles written with type "${ARTICLE_TYPE_SPARE_PART}"; the article ` +
|
|
201
|
+
`catalogue is shared with BDE, so a re-run updates rather than duplicates`);
|
|
202
|
+
},
|
|
203
|
+
};
|