@openinc/parse-server-opendash 4.1.1 → 4.1.3

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 (164) 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/isClassEnabled.js +2 -0
  15. package/dist/features/schema/helper/migrationMode.d.ts +26 -0
  16. package/dist/features/schema/helper/migrationMode.js +48 -0
  17. package/dist/features/schema/index.d.ts +1 -0
  18. package/dist/features/schema/index.js +3 -1
  19. package/dist/features/schema/services/beforeSaveHook.js +21 -0
  20. package/dist/features/translations/services/initTranslation.js +1 -1
  21. package/dist/hooks/Chat_Message.d.ts +1 -0
  22. package/dist/hooks/Chat_Message.js +179 -0
  23. package/dist/hooks/Meta_Config.js +19 -10
  24. package/dist/hooks/Meta_Context.d.ts +6 -0
  25. package/dist/hooks/Meta_Context.js +16 -0
  26. package/dist/hooks/Meta_Entry.js +20 -10
  27. package/dist/hooks/Service_Equipment.d.ts +1 -0
  28. package/dist/hooks/Service_Equipment.js +38 -0
  29. package/dist/hooks/Service_FormConfig.d.ts +1 -0
  30. package/dist/hooks/Service_FormConfig.js +17 -0
  31. package/dist/hooks/Service_Project.d.ts +1 -0
  32. package/dist/hooks/Service_Project.js +17 -0
  33. package/dist/hooks/Service_Schedule.d.ts +1 -0
  34. package/dist/hooks/Service_Schedule.js +24 -0
  35. package/dist/hooks/Service_Schedule_Execution.d.ts +1 -0
  36. package/dist/hooks/Service_Schedule_Execution.js +17 -0
  37. package/dist/hooks/Service_Schedule_Template.d.ts +1 -0
  38. package/dist/hooks/Service_Schedule_Template.js +17 -0
  39. package/dist/hooks/Service_Status.d.ts +1 -0
  40. package/dist/hooks/Service_Status.js +32 -0
  41. package/dist/hooks/Service_Tag.d.ts +1 -0
  42. package/dist/hooks/Service_Tag.js +168 -0
  43. package/dist/hooks/Service_Ticket.d.ts +1 -0
  44. package/dist/hooks/Service_Ticket.js +433 -0
  45. package/dist/hooks/Service_TriggerState.d.ts +1 -0
  46. package/dist/hooks/Service_TriggerState.js +17 -0
  47. package/dist/hooks/_ChangeLog.js +27 -6
  48. package/dist/i18n/de-DE/chat.json +18 -0
  49. package/dist/i18n/de-DE/openservice.json +14 -0
  50. package/dist/i18n/en-US/chat.json +18 -0
  51. package/dist/i18n/en-US/openservice.json +14 -0
  52. package/dist/scripts/backfill-meta-contexts.d.ts +1 -0
  53. package/dist/scripts/backfill-meta-contexts.js +100 -0
  54. package/dist/scripts/migration/lib/acl.d.ts +52 -0
  55. package/dist/scripts/migration/lib/acl.js +79 -0
  56. package/dist/scripts/migration/lib/articleMeta.d.ts +47 -0
  57. package/dist/scripts/migration/lib/articleMeta.js +132 -0
  58. package/dist/scripts/migration/lib/env.d.ts +56 -0
  59. package/dist/scripts/migration/lib/env.js +106 -0
  60. package/dist/scripts/migration/lib/latest.d.ts +47 -0
  61. package/dist/scripts/migration/lib/latest.js +115 -0
  62. package/dist/scripts/migration/lib/map.d.ts +53 -0
  63. package/dist/scripts/migration/lib/map.js +113 -0
  64. package/dist/scripts/migration/lib/material.d.ts +47 -0
  65. package/dist/scripts/migration/lib/material.js +51 -0
  66. package/dist/scripts/migration/lib/message.d.ts +78 -0
  67. package/dist/scripts/migration/lib/message.js +115 -0
  68. package/dist/scripts/migration/lib/pass.d.ts +67 -0
  69. package/dist/scripts/migration/lib/pass.js +89 -0
  70. package/dist/scripts/migration/lib/ptr.d.ts +32 -0
  71. package/dist/scripts/migration/lib/ptr.js +38 -0
  72. package/dist/scripts/migration/lib/report.d.ts +41 -0
  73. package/dist/scripts/migration/lib/report.js +78 -0
  74. package/dist/scripts/migration/lib/rest.d.ts +47 -0
  75. package/dist/scripts/migration/lib/rest.js +68 -0
  76. package/dist/scripts/migration/lib/ticket.d.ts +72 -0
  77. package/dist/scripts/migration/lib/ticket.js +192 -0
  78. package/dist/scripts/migration/passes/01-statuses.d.ts +12 -0
  79. package/dist/scripts/migration/passes/01-statuses.js +122 -0
  80. package/dist/scripts/migration/passes/02-tags.d.ts +13 -0
  81. package/dist/scripts/migration/passes/02-tags.js +150 -0
  82. package/dist/scripts/migration/passes/03-equipment.d.ts +14 -0
  83. package/dist/scripts/migration/passes/03-equipment.js +176 -0
  84. package/dist/scripts/migration/passes/04-projects.d.ts +27 -0
  85. package/dist/scripts/migration/passes/04-projects.js +241 -0
  86. package/dist/scripts/migration/passes/05-articles.d.ts +25 -0
  87. package/dist/scripts/migration/passes/05-articles.js +203 -0
  88. package/dist/scripts/migration/passes/06-formconfigs.d.ts +17 -0
  89. package/dist/scripts/migration/passes/06-formconfigs.js +166 -0
  90. package/dist/scripts/migration/passes/07-meta-configs.d.ts +46 -0
  91. package/dist/scripts/migration/passes/07-meta-configs.js +185 -0
  92. package/dist/scripts/migration/passes/08-tickets.d.ts +27 -0
  93. package/dist/scripts/migration/passes/08-tickets.js +353 -0
  94. package/dist/scripts/migration/passes/09-ticket-meta.d.ts +12 -0
  95. package/dist/scripts/migration/passes/09-ticket-meta.js +136 -0
  96. package/dist/scripts/migration/passes/10-chat.d.ts +17 -0
  97. package/dist/scripts/migration/passes/10-chat.js +230 -0
  98. package/dist/scripts/migration/passes/11-assets.d.ts +17 -0
  99. package/dist/scripts/migration/passes/11-assets.js +93 -0
  100. package/dist/scripts/migration/passes/12-history.d.ts +24 -0
  101. package/dist/scripts/migration/passes/12-history.js +332 -0
  102. package/dist/scripts/migration/run.d.ts +1 -0
  103. package/dist/scripts/migration/run.js +148 -0
  104. package/dist/scripts/migration/verify.d.ts +1 -0
  105. package/dist/scripts/migration/verify.js +620 -0
  106. package/dist/scripts/seed-v1-testdata.d.ts +41 -0
  107. package/dist/scripts/seed-v1-testdata.js +543 -0
  108. package/dist/types/BDE_Article.d.ts +3 -0
  109. package/dist/types/BDE_Article.js +6 -0
  110. package/dist/types/Chat_Message.d.ts +49 -0
  111. package/dist/types/Chat_Message.js +87 -0
  112. package/dist/types/MES_RessourceOverview.d.ts +38 -0
  113. package/dist/types/MES_RessourceOverview.js +63 -0
  114. package/dist/types/Meta_Context.d.ts +43 -0
  115. package/dist/types/Meta_Context.js +75 -0
  116. package/dist/types/Service_Equipment.d.ts +33 -0
  117. package/dist/types/Service_Equipment.js +57 -0
  118. package/dist/types/Service_FormConfig.d.ts +33 -0
  119. package/dist/types/Service_FormConfig.js +57 -0
  120. package/dist/types/Service_MeterReading.d.ts +35 -0
  121. package/dist/types/Service_MeterReading.js +57 -0
  122. package/dist/types/Service_Project.d.ts +53 -0
  123. package/dist/types/Service_Project.js +93 -0
  124. package/dist/types/Service_Schedule.d.ts +66 -0
  125. package/dist/types/Service_Schedule.js +117 -0
  126. package/dist/types/Service_Schedule_Execution.d.ts +38 -0
  127. package/dist/types/Service_Schedule_Execution.js +63 -0
  128. package/dist/types/Service_Schedule_Template.d.ts +36 -0
  129. package/dist/types/Service_Schedule_Template.js +60 -0
  130. package/dist/types/Service_Status.d.ts +30 -0
  131. package/dist/types/Service_Status.js +51 -0
  132. package/dist/types/Service_Tag.d.ts +30 -0
  133. package/dist/types/Service_Tag.js +51 -0
  134. package/dist/types/Service_Ticket.d.ts +92 -0
  135. package/dist/types/Service_Ticket.js +165 -0
  136. package/dist/types/Service_TicketCounter.d.ts +18 -0
  137. package/dist/types/Service_TicketCounter.js +27 -0
  138. package/dist/types/Service_TriggerState.d.ts +31 -0
  139. package/dist/types/Service_TriggerState.js +51 -0
  140. package/dist/types/_Session.d.ts +0 -1
  141. package/dist/types/_User.d.ts +2 -6
  142. package/dist/types/custom/MetaField.d.ts +9 -0
  143. package/dist/types/custom/TicketMaterial.d.ts +32 -0
  144. package/dist/types/custom/TicketMaterial.js +2 -0
  145. package/dist/types/index.d.ts +30 -0
  146. package/dist/types/index.js +33 -3
  147. package/package.json +17 -14
  148. package/schema/BDE_Article.json +3 -0
  149. package/schema/Changelog.json +3 -6
  150. package/schema/Chat_Message.json +88 -0
  151. package/schema/Meta_Context.json +78 -0
  152. package/schema/Service_Equipment.json +55 -0
  153. package/schema/Service_FormConfig.json +66 -0
  154. package/schema/Service_MeterReading.json +54 -0
  155. package/schema/Service_MigrationMap.json +55 -0
  156. package/schema/Service_Project.json +77 -0
  157. package/schema/Service_Schedule.json +101 -0
  158. package/schema/Service_Schedule_Execution.json +58 -0
  159. package/schema/Service_Schedule_Template.json +73 -0
  160. package/schema/Service_Status.json +50 -0
  161. package/schema/Service_Tag.json +50 -0
  162. package/schema/Service_Ticket.json +114 -0
  163. package/schema/Service_TicketCounter.json +38 -0
  164. package/schema/Service_TriggerState.json +50 -0
@@ -0,0 +1,18 @@
1
+ {
2
+ "message": {
3
+ "mention": {
4
+ "user": {
5
+ "notification": {
6
+ "title": "Sie wurden in einer Nachricht erwähnt",
7
+ "description": "{{username}} hat Sie in einer Nachricht erwähnt: {{- messageText}}"
8
+ }
9
+ },
10
+ "role": {
11
+ "notification": {
12
+ "title": "Ihre Rolle wurde in einer Nachricht erwähnt",
13
+ "description": "{{username}} hat die Rolle {{role}} in einer Nachricht erwähnt: {{- messageText}}"
14
+ }
15
+ }
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "ticket": {
3
+ "assignment": {
4
+ "assigned": {
5
+ "title": "Ihnen wurde ein Ticket zugewiesen",
6
+ "description": "{{username}} hat Ihnen das Ticket {{ticketName}} zugewiesen.\n\nLink: {{- ticketLink}}"
7
+ },
8
+ "watcher": {
9
+ "title": "Ein Ticket wurde zugewiesen",
10
+ "description": "{{username}} hat das Ticket {{ticketName}} zugewiesen.\n\nLink: {{- ticketLink}}"
11
+ }
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "message": {
3
+ "mention": {
4
+ "user": {
5
+ "notification": {
6
+ "title": "You were mentioned in a message",
7
+ "description": "{{username}} mentioned you in a message: {{- messageText}}"
8
+ }
9
+ },
10
+ "role": {
11
+ "notification": {
12
+ "title": "Your role was mentioned in a message",
13
+ "description": "{{username}} mentioned the role {{role}} in a message: {{- messageText}}"
14
+ }
15
+ }
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "ticket": {
3
+ "assignment": {
4
+ "assigned": {
5
+ "title": "You were assigned to a ticket",
6
+ "description": "{{username}} assigned you to ticket {{ticketName}}.\n\nLink: {{- ticketLink}}"
7
+ },
8
+ "watcher": {
9
+ "title": "A ticket was assigned",
10
+ "description": "{{username}} assigned ticket {{ticketName}}.\n\nLink: {{- ticketLink}}"
11
+ }
12
+ }
13
+ }
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,100 @@
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("../features/meta/index.js");
8
+ const index_js_2 = require("../types/index.js");
9
+ /**
10
+ * One-off backfill for the `OD3_Meta_Context` directory.
11
+ *
12
+ * From here on the `Meta_Config` / `Meta_Entry` afterSave hooks keep the
13
+ * directory current, but they only see writes — everything already in the
14
+ * database predates them. This walks both classes once and registers what it
15
+ * finds, so the directory starts complete rather than filling in as rows happen
16
+ * to be touched.
17
+ *
18
+ * PARSE_URL=… PARSE_APP_ID=… PARSE_MASTER_KEY=… \
19
+ * node dist/scripts/backfill-meta-contexts.js [--dry-run]
20
+ *
21
+ * Safe to re-run: `registerMetaContext` upserts and only writes on a change.
22
+ */
23
+ const BATCH_SIZE = 500;
24
+ function requireEnv(name) {
25
+ const value = process.env[name];
26
+ if (!value) {
27
+ console.error(`Missing required environment variable ${name}.`);
28
+ process.exit(1);
29
+ }
30
+ return value;
31
+ }
32
+ /** Iterate a class in `createdAt` order, page by page — no skip, no cursor drift. */
33
+ async function* pages(ctor) {
34
+ let after;
35
+ for (;;) {
36
+ const query = new node_1.default.Query(ctor)
37
+ .ascending("createdAt")
38
+ .limit(BATCH_SIZE);
39
+ // `createdAt` is not part of the generic attribute map, hence the cast.
40
+ if (after)
41
+ query.greaterThan("createdAt", after);
42
+ const results = await query.find({ useMasterKey: true });
43
+ if (results.length === 0)
44
+ return;
45
+ yield results;
46
+ after = results[results.length - 1].createdAt;
47
+ if (results.length < BATCH_SIZE)
48
+ return;
49
+ }
50
+ }
51
+ async function main() {
52
+ const dryRun = process.argv.includes("--dry-run");
53
+ node_1.default.initialize(requireEnv("PARSE_APP_ID"), undefined, requireEnv("PARSE_MASTER_KEY"));
54
+ node_1.default.serverURL =
55
+ process.env.PARSE_URL ?? "http://localhost:1337/parse";
56
+ const seen = new Set();
57
+ let configs = 0;
58
+ let entries = 0;
59
+ // Configs first: they carry label/description, so a context that has one is
60
+ // named from the start rather than being created bare by an entry.
61
+ for await (const page of pages(index_js_2.Meta_Config)) {
62
+ for (const config of page) {
63
+ const context = config.context;
64
+ if (!context)
65
+ continue;
66
+ configs++;
67
+ seen.add(context);
68
+ if (dryRun)
69
+ continue;
70
+ await (0, index_js_1.registerMetaContext)({
71
+ context,
72
+ tenant: config.tenant,
73
+ config,
74
+ });
75
+ }
76
+ }
77
+ for await (const page of pages(index_js_2.Meta_Entry)) {
78
+ for (const entry of page) {
79
+ const context = entry.context;
80
+ if (!context)
81
+ continue;
82
+ entries++;
83
+ seen.add(context);
84
+ if (dryRun)
85
+ continue;
86
+ await (0, index_js_1.registerMetaContext)({
87
+ context,
88
+ tenant: entry.tenant,
89
+ entryClassName: entry.entryClassName,
90
+ });
91
+ }
92
+ }
93
+ console.log(`${dryRun ? "[dry run] " : ""}scanned ${configs} configs and ${entries} entries → ${seen.size} distinct contexts`);
94
+ for (const context of [...seen].sort())
95
+ console.log(` ${context}`);
96
+ }
97
+ main().catch((error) => {
98
+ console.error(error);
99
+ process.exit(1);
100
+ });
@@ -0,0 +1,52 @@
1
+ import type { MigrationConfig } from "./env.js";
2
+ /**
3
+ * Replicates `defaultAclHandler` for migration writes.
4
+ *
5
+ * The migration runs with `OD_MIGRATION_MODE=1`, which makes that handler return
6
+ * early (it would otherwise fight the historical owner/tenant the migration is
7
+ * reconstructing). So the ACLs it would have applied have to be applied here
8
+ * instead — deliberately mirroring
9
+ * `src/features/schema/helper/defaultHandler.ts` rather than inventing a policy.
10
+ *
11
+ * Keep these two in sync: if the default handler's grants change, this changes.
12
+ */
13
+ export declare function migrationAcl(options: {
14
+ /** The object's tenant, if it has one. */
15
+ tenantId?: string;
16
+ /** The object's owning user, if it has one (v1 `user` / v2 `createdBy`). */
17
+ userId?: string;
18
+ /** Mirrors `defaultAclHandler`'s `allowTenantUserWrite` option. */
19
+ allowTenantUserWrite?: boolean;
20
+ }): Parse.ACL;
21
+ /**
22
+ * Save options for SDK-path writes (updates).
23
+ *
24
+ * `skipChangelog` matters as much as the key: `hooks/_ChangeLog.ts` registers an
25
+ * afterSave on *every* class, so without this each migrated object would generate
26
+ * a `masterkey`-flagged, author-less, `createdAt = now` changelog entry — six
27
+ * figures of noise burying the reconstructed history it sits beside. The hook
28
+ * already honours this context flag, so no new switch is needed.
29
+ */
30
+ export declare const MIGRATION_SAVE: {
31
+ readonly context: {
32
+ readonly skipChangelog: true;
33
+ };
34
+ };
35
+ /**
36
+ * The single write path every pass must use.
37
+ *
38
+ * Routes by operation, because the two need different transports:
39
+ *
40
+ * - **Create** goes over REST (`lib/rest.ts`), because the SDK silently discards
41
+ * `createdAt` / `updatedAt` and would leave every migrated row stamped `now`.
42
+ * - **Update** goes over the SDK, which is fine — Parse ignores `createdAt` on
43
+ * update anyway, and the object already has its id.
44
+ *
45
+ * Returns the object's id so callers can record the mapping.
46
+ */
47
+ export declare function saveMigrated(config: MigrationConfig, object: Parse.Object, options: {
48
+ className: string;
49
+ isNew: boolean;
50
+ createdAt?: Date;
51
+ updatedAt?: Date;
52
+ }): Promise<string>;
@@ -0,0 +1,79 @@
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.MIGRATION_SAVE = void 0;
7
+ exports.migrationAcl = migrationAcl;
8
+ exports.saveMigrated = saveMigrated;
9
+ const node_1 = __importDefault(require("parse/node"));
10
+ const rest_js_1 = require("./rest.js");
11
+ /**
12
+ * Replicates `defaultAclHandler` for migration writes.
13
+ *
14
+ * The migration runs with `OD_MIGRATION_MODE=1`, which makes that handler return
15
+ * early (it would otherwise fight the historical owner/tenant the migration is
16
+ * reconstructing). So the ACLs it would have applied have to be applied here
17
+ * instead — deliberately mirroring
18
+ * `src/features/schema/helper/defaultHandler.ts` rather than inventing a policy.
19
+ *
20
+ * Keep these two in sync: if the default handler's grants change, this changes.
21
+ */
22
+ function migrationAcl(options) {
23
+ const acl = new node_1.default.ACL();
24
+ acl.setRoleReadAccess("od-admin", true);
25
+ acl.setRoleWriteAccess("od-admin", true);
26
+ if (options.userId) {
27
+ acl.setReadAccess(options.userId, true);
28
+ acl.setWriteAccess(options.userId, true);
29
+ }
30
+ if (options.tenantId) {
31
+ acl.setRoleReadAccess(`od-tenant-user-${options.tenantId}`, true);
32
+ if (options.allowTenantUserWrite) {
33
+ acl.setRoleWriteAccess(`od-tenant-user-${options.tenantId}`, true);
34
+ }
35
+ acl.setRoleReadAccess(`od-tenant-admin-${options.tenantId}`, true);
36
+ acl.setRoleWriteAccess(`od-tenant-admin-${options.tenantId}`, true);
37
+ }
38
+ return acl;
39
+ }
40
+ /**
41
+ * Save options for SDK-path writes (updates).
42
+ *
43
+ * `skipChangelog` matters as much as the key: `hooks/_ChangeLog.ts` registers an
44
+ * afterSave on *every* class, so without this each migrated object would generate
45
+ * a `masterkey`-flagged, author-less, `createdAt = now` changelog entry — six
46
+ * figures of noise burying the reconstructed history it sits beside. The hook
47
+ * already honours this context flag, so no new switch is needed.
48
+ */
49
+ exports.MIGRATION_SAVE = {
50
+ // No `useMaintenanceKey` here: the key travels as a default header (see
51
+ // initParse). Passing the option would append `_MaintenanceKey` to the payload,
52
+ // which Parse rejects on reads and ignores on writes.
53
+ context: { skipChangelog: true },
54
+ };
55
+ /**
56
+ * The single write path every pass must use.
57
+ *
58
+ * Routes by operation, because the two need different transports:
59
+ *
60
+ * - **Create** goes over REST (`lib/rest.ts`), because the SDK silently discards
61
+ * `createdAt` / `updatedAt` and would leave every migrated row stamped `now`.
62
+ * - **Update** goes over the SDK, which is fine — Parse ignores `createdAt` on
63
+ * update anyway, and the object already has its id.
64
+ *
65
+ * Returns the object's id so callers can record the mapping.
66
+ */
67
+ async function saveMigrated(config, object, options) {
68
+ if (!options.isNew) {
69
+ await object.save(null, exports.MIGRATION_SAVE);
70
+ if (!object.id)
71
+ throw new Error("update returned no objectId");
72
+ return object.id;
73
+ }
74
+ const created = await (0, rest_js_1.restCreate)(config, options.className, object, options.createdAt, options.updatedAt);
75
+ // Keep the in-memory object consistent with what the server stored, so callers
76
+ // can read `object.id` as usual.
77
+ object.id = created.objectId;
78
+ return created.objectId;
79
+ }
@@ -0,0 +1,47 @@
1
+ import { Meta_Entry } from "../../../types/index.js";
2
+ import type { MetaFieldConfig } from "../../../types/custom/MetaField.js";
3
+ import type { MigrationConfig } from "./env.js";
4
+ /**
5
+ * Stock-keeping data for an article, kept as plugin-meta rather than columns.
6
+ *
7
+ * v1's `Maintenance_Item` tracked price, stock levels, storage location and
8
+ * supplier. The v2 catalogue is `OD3_BDE_Article`, which is **shared with BDE
9
+ * and its ABAS sync** and deliberately lean — widening it is not ours to do, and
10
+ * the sync overwrites the columns it owns on every run.
11
+ *
12
+ * So the data lands in a `Meta_Entry` instead, under the same
13
+ * `["service", <entity>, "meta"]` context every other core master-data entity
14
+ * uses (`constants.metaContext`). That makes it editable through the article
15
+ * admin view in `openservice-core` with no extra machinery.
16
+ *
17
+ * **Deliberately not `["bde","article","custom"]`.** That context belongs to the
18
+ * ABAS sync (`node-stack-scheduler`, `syncArticle.ts`), which replaces `config`
19
+ * and `values` wholesale on every run and fetches its entries with
20
+ * `.limit(<number of ABAS articles>)`. Sharing it would let migrated entries
21
+ * crowd real ABAS entries out of that limit — and any article present in both
22
+ * would have its values clobbered on the next sync.
23
+ */
24
+ export declare const ARTICLE_META_CONTEXT: string[];
25
+ /** The field definitions written to the shared `Meta_Config`. */
26
+ export declare const ARTICLE_META_FIELDS: MetaFieldConfig[];
27
+ /**
28
+ * v1 stores `price` as a String, and these are German-language instances.
29
+ *
30
+ * Returns `undefined` for anything unparseable so the caller can report it —
31
+ * silently nulling a price is exactly the kind of loss that surfaces months
32
+ * later as "the migration ate our prices".
33
+ */
34
+ export declare function parsePrice(raw: unknown): number | undefined;
35
+ /**
36
+ * Ensure the shared `Meta_Config` exists for a tenant, returning its id.
37
+ *
38
+ * Scoped by tenant deliberately. `getMetaConfig()` in the meta plugin filters by
39
+ * *context only* and relies on Parse ACLs to show each tenant its own record —
40
+ * but the migration reads with the maintenance key, which bypasses ACL entirely.
41
+ * An unscoped `.first()` would find some other tenant's config and overwrite it.
42
+ */
43
+ export declare function ensureArticleMetaConfig(config: MigrationConfig, tenantId: string): Promise<string | undefined>;
44
+ /** Existing article meta entries, keyed by the article they belong to. */
45
+ export declare function loadArticleMetaEntries(config: MigrationConfig): Promise<Map<string, Meta_Entry>>;
46
+ /** The Meta_Config ids already present, keyed by tenant. */
47
+ export declare function loadArticleMetaConfigs(config: MigrationConfig): Promise<Map<string, string>>;
@@ -0,0 +1,132 @@
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.ARTICLE_META_FIELDS = exports.ARTICLE_META_CONTEXT = void 0;
7
+ exports.parsePrice = parsePrice;
8
+ exports.ensureArticleMetaConfig = ensureArticleMetaConfig;
9
+ exports.loadArticleMetaEntries = loadArticleMetaEntries;
10
+ exports.loadArticleMetaConfigs = loadArticleMetaConfigs;
11
+ const node_1 = __importDefault(require("parse/node"));
12
+ const index_js_1 = require("../../../types/index.js");
13
+ const acl_js_1 = require("./acl.js");
14
+ const ptr_js_1 = require("./ptr.js");
15
+ const pass_js_1 = require("./pass.js");
16
+ /**
17
+ * Stock-keeping data for an article, kept as plugin-meta rather than columns.
18
+ *
19
+ * v1's `Maintenance_Item` tracked price, stock levels, storage location and
20
+ * supplier. The v2 catalogue is `OD3_BDE_Article`, which is **shared with BDE
21
+ * and its ABAS sync** and deliberately lean — widening it is not ours to do, and
22
+ * the sync overwrites the columns it owns on every run.
23
+ *
24
+ * So the data lands in a `Meta_Entry` instead, under the same
25
+ * `["service", <entity>, "meta"]` context every other core master-data entity
26
+ * uses (`constants.metaContext`). That makes it editable through the article
27
+ * admin view in `openservice-core` with no extra machinery.
28
+ *
29
+ * **Deliberately not `["bde","article","custom"]`.** That context belongs to the
30
+ * ABAS sync (`node-stack-scheduler`, `syncArticle.ts`), which replaces `config`
31
+ * and `values` wholesale on every run and fetches its entries with
32
+ * `.limit(<number of ABAS articles>)`. Sharing it would let migrated entries
33
+ * crowd real ABAS entries out of that limit — and any article present in both
34
+ * would have its values clobbered on the next sync.
35
+ */
36
+ exports.ARTICLE_META_CONTEXT = ["service", "article", "meta"];
37
+ /** The field definitions written to the shared `Meta_Config`. */
38
+ exports.ARTICLE_META_FIELDS = [
39
+ { type: "number", name: "price", label: "Preis" },
40
+ { type: "number", name: "stock", label: "Bestand" },
41
+ { type: "number", name: "stockAlarmAt", label: "Meldebestand" },
42
+ { type: "string", name: "stockUnit", label: "Lagereinheit" },
43
+ { type: "string", name: "storageLocation", label: "Lagerort" },
44
+ { type: "string", name: "supplier", label: "Lieferant" },
45
+ { type: "string", name: "category", label: "Kategorie" },
46
+ { type: "string", name: "orderNumber", label: "Bestellnummer" },
47
+ {
48
+ type: "select_parse",
49
+ name: "equipment",
50
+ label: "Equipment",
51
+ className: "OD3_Service_Equipment",
52
+ display: "label",
53
+ multiple: true,
54
+ },
55
+ ];
56
+ /**
57
+ * v1 stores `price` as a String, and these are German-language instances.
58
+ *
59
+ * Returns `undefined` for anything unparseable so the caller can report it —
60
+ * silently nulling a price is exactly the kind of loss that surfaces months
61
+ * later as "the migration ate our prices".
62
+ */
63
+ function parsePrice(raw) {
64
+ if (raw === undefined || raw === null)
65
+ return undefined;
66
+ const cleaned = String(raw)
67
+ .replace(/[^\d,.-]/g, "")
68
+ .replace(/\.(?=\d{3}\b)/g, "") // thousands separator
69
+ .replace(",", ".");
70
+ if (cleaned === "" || cleaned === "-")
71
+ return undefined;
72
+ const value = Number(cleaned);
73
+ return Number.isFinite(value) ? value : undefined;
74
+ }
75
+ /**
76
+ * Ensure the shared `Meta_Config` exists for a tenant, returning its id.
77
+ *
78
+ * Scoped by tenant deliberately. `getMetaConfig()` in the meta plugin filters by
79
+ * *context only* and relies on Parse ACLs to show each tenant its own record —
80
+ * but the migration reads with the maintenance key, which bypasses ACL entirely.
81
+ * An unscoped `.first()` would find some other tenant's config and overwrite it.
82
+ */
83
+ async function ensureArticleMetaConfig(config, tenantId) {
84
+ const contextString = (0, ptr_js_1.contextToString)(exports.ARTICLE_META_CONTEXT);
85
+ const query = new node_1.default.Query(index_js_1.Meta_Config).equalTo("context", contextString);
86
+ if (tenantId)
87
+ query.equalTo("tenant", (0, ptr_js_1.tenantPtr)(tenantId));
88
+ else
89
+ query.doesNotExist("tenant");
90
+ const existing = await query.first({});
91
+ const target = existing ?? new index_js_1.Meta_Config();
92
+ target.set("context", contextString);
93
+ target.set("config", { fields: exports.ARTICLE_META_FIELDS });
94
+ if (tenantId)
95
+ target.set("tenant", (0, ptr_js_1.tenantPtr)(tenantId));
96
+ if (!existing)
97
+ target.setACL((0, acl_js_1.migrationAcl)({ tenantId }));
98
+ if (config.dryRun)
99
+ return existing?.id;
100
+ // No historical timestamp to preserve: the shared config is a v2 artefact
101
+ // synthesised by the migration, not a migrated row.
102
+ await (0, acl_js_1.saveMigrated)(config, target, {
103
+ className: index_js_1.Meta_Config.className,
104
+ isNew: !existing,
105
+ });
106
+ return target.id;
107
+ }
108
+ /** Existing article meta entries, keyed by the article they belong to. */
109
+ async function loadArticleMetaEntries(config) {
110
+ const byArticle = new Map();
111
+ const contextString = (0, ptr_js_1.contextToString)(exports.ARTICLE_META_CONTEXT);
112
+ for await (const page of (0, pass_js_1.pages)(() => new node_1.default.Query(index_js_1.Meta_Entry)
113
+ .equalTo("context", contextString)
114
+ .equalTo("entryClassName", index_js_1.BDE_Article.className), config.batchSize)) {
115
+ for (const row of page) {
116
+ const objectId = row.get("entryObjectId");
117
+ if (objectId)
118
+ byArticle.set(objectId, row);
119
+ }
120
+ }
121
+ return byArticle;
122
+ }
123
+ /** The Meta_Config ids already present, keyed by tenant. */
124
+ async function loadArticleMetaConfigs(config) {
125
+ const byTenant = new Map();
126
+ const contextString = (0, ptr_js_1.contextToString)(exports.ARTICLE_META_CONTEXT);
127
+ for (const row of await (0, pass_js_1.all)(() => new node_1.default.Query(index_js_1.Meta_Config).equalTo("context", contextString), config.batchSize)) {
128
+ if (row.id)
129
+ byTenant.set((0, pass_js_1.refId)(row.get("tenant")) ?? "", row.id);
130
+ }
131
+ return byTenant;
132
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Runtime configuration for the openservice v1 → v2 migration.
3
+ *
4
+ * See `plugins/openservice/MIGRATION_PLAN.md` in the opendash repo for the full
5
+ * plan; §3 covers why this runs on the maintenance key.
6
+ */
7
+ export interface MigrationConfig {
8
+ parseUrl: string;
9
+ appId: string;
10
+ maintenanceKey: string;
11
+ /** Restrict the run to one tenant's data. Omit to process every tenant. */
12
+ tenantId?: string;
13
+ /** Run only these passes (by id, e.g. `["01", "08"]`). Omit to run all. */
14
+ only?: string[];
15
+ /** Read and report, write nothing. */
16
+ dryRun: boolean;
17
+ /** Reuse a previous run id so its MigrationMap rows are recognised. */
18
+ runId: string;
19
+ /** Rows fetched per query page. */
20
+ batchSize: number;
21
+ }
22
+ export declare class MigrationConfigError extends Error {
23
+ }
24
+ /**
25
+ * Build the config from argv + env, failing loudly on anything missing.
26
+ *
27
+ * The maintenance key is deliberately required rather than falling back to the
28
+ * master key: a master-key write silently stamps `createdAt = now`
29
+ * (`parse-server/lib/RestWrite.js` — the custom-timestamp branch is guarded by
30
+ * `auth.isMaintenance`), which would destroy ticket age and the reconstructed
31
+ * history without raising a single error. Failing to start is far better than
32
+ * completing with wrong timestamps.
33
+ */
34
+ export declare function readConfig(argv: string[]): MigrationConfig;
35
+ /**
36
+ * Point the Parse SDK at the target server, authenticating as maintenance.
37
+ *
38
+ * The key goes in a **default request header**, not through the SDK's
39
+ * `useMaintenanceKey` option. That option puts `_MaintenanceKey` into the request
40
+ * payload — which for a GET becomes a query parameter, and Parse Server rejects it
41
+ * outright: `Invalid parameter for query: _MaintenanceKey`
42
+ * (`Routers/ClassesRouter.js`). So every read would fail. The header form is what
43
+ * `middlewares.js` actually reads (`X-Parse-Maintenance-Key`), and it applies
44
+ * uniformly to reads and writes.
45
+ */
46
+ export declare function initParse(config: MigrationConfig): void;
47
+ /**
48
+ * Read options for every query in the migration.
49
+ *
50
+ * Deliberately empty: authentication rides on the default header set in
51
+ * {@link initParse}, because the SDK's `useMaintenanceKey` option would be sent as
52
+ * a query parameter and rejected. Kept as a named constant so call sites still
53
+ * document that they are reading with elevated rights — RestQuery treats
54
+ * `isMaintenance` alongside `isMaster`, so these reads bypass ACL and CLP.
55
+ */
56
+ export declare const MK: {};
@@ -0,0 +1,106 @@
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.MK = exports.MigrationConfigError = void 0;
7
+ exports.readConfig = readConfig;
8
+ exports.initParse = initParse;
9
+ const node_1 = __importDefault(require("parse/node"));
10
+ class MigrationConfigError extends Error {
11
+ }
12
+ exports.MigrationConfigError = MigrationConfigError;
13
+ function requireEnv(name, hint) {
14
+ const value = process.env[name];
15
+ if (!value) {
16
+ throw new MigrationConfigError(`Missing required environment variable ${name}.\n ${hint}`);
17
+ }
18
+ return value;
19
+ }
20
+ /**
21
+ * Build the config from argv + env, failing loudly on anything missing.
22
+ *
23
+ * The maintenance key is deliberately required rather than falling back to the
24
+ * master key: a master-key write silently stamps `createdAt = now`
25
+ * (`parse-server/lib/RestWrite.js` — the custom-timestamp branch is guarded by
26
+ * `auth.isMaintenance`), which would destroy ticket age and the reconstructed
27
+ * history without raising a single error. Failing to start is far better than
28
+ * completing with wrong timestamps.
29
+ */
30
+ function readConfig(argv) {
31
+ const flag = (name) => {
32
+ const hit = argv.find((a) => a.startsWith(`--${name}=`));
33
+ return hit ? hit.slice(name.length + 3) : undefined;
34
+ };
35
+ const has = (name) => argv.includes(`--${name}`);
36
+ if (process.env.OD_MIGRATION_MODE !== "1") {
37
+ throw new MigrationConfigError("OD_MIGRATION_MODE must be set to 1 on BOTH this process and the Parse " +
38
+ "Server it talks to.\n" +
39
+ " Server-side it gates defaultHandler/defaultAclHandler (without it every " +
40
+ "maintenance-key write fails on the missing user) and suppresses assignment " +
41
+ "notifications.\n" +
42
+ " See src/features/schema/helper/migrationMode.ts");
43
+ }
44
+ const only = flag("only");
45
+ return {
46
+ parseUrl: process.env.PARSE_URL ?? "http://localhost:1337/parse",
47
+ appId: requireEnv("PARSE_APP_ID", "The Parse application id, e.g. openinc-node-docker-dev."),
48
+ maintenanceKey: requireEnv("PARSE_MAINTENANCE_KEY", "Parse Server's `maintenanceKey` — the only auth level allowed to write a " +
49
+ "historical createdAt. Set PARSE_SERVER_MAINTENANCE_KEY on the server, and " +
50
+ "note maintenanceKeyIps defaults to localhost, so run this ON the Parse host."),
51
+ tenantId: flag("tenant"),
52
+ only: only ? only.split(",").map((s) => s.trim()) : undefined,
53
+ dryRun: has("dry-run"),
54
+ runId: flag("resume") ?? `run-${new Date().toISOString()}`,
55
+ batchSize: readBatchSize(flag("batch")),
56
+ };
57
+ }
58
+ /**
59
+ * Page size, validated rather than trusted.
60
+ *
61
+ * `Number("abc")` is `NaN`, and `.limit(NaN)` produces a query that fails in a way
62
+ * that points nowhere near the flag that caused it. The upper bound is a guard
63
+ * against Parse's `maxLimit` clamping: asking for more than the server allows makes
64
+ * every page come back short, which is safe for the paging loop (it stops only on
65
+ * an empty page) but wastes the request — so refuse the obviously-wrong value
66
+ * instead of quietly doing something else.
67
+ */
68
+ function readBatchSize(raw) {
69
+ if (raw === undefined)
70
+ return 500;
71
+ const value = Number(raw);
72
+ if (!Number.isInteger(value) || value < 1 || value > 1000) {
73
+ throw new MigrationConfigError(`--batch must be an integer between 1 and 1000 (got "${raw}").\n` +
74
+ ` 1000 is Parse's practical page ceiling; the default of 500 is fine at ` +
75
+ `this data volume.`);
76
+ }
77
+ return value;
78
+ }
79
+ /**
80
+ * Point the Parse SDK at the target server, authenticating as maintenance.
81
+ *
82
+ * The key goes in a **default request header**, not through the SDK's
83
+ * `useMaintenanceKey` option. That option puts `_MaintenanceKey` into the request
84
+ * payload — which for a GET becomes a query parameter, and Parse Server rejects it
85
+ * outright: `Invalid parameter for query: _MaintenanceKey`
86
+ * (`Routers/ClassesRouter.js`). So every read would fail. The header form is what
87
+ * `middlewares.js` actually reads (`X-Parse-Maintenance-Key`), and it applies
88
+ * uniformly to reads and writes.
89
+ */
90
+ function initParse(config) {
91
+ node_1.default.initialize(config.appId);
92
+ node_1.default.serverURL = config.parseUrl;
93
+ node_1.default.CoreManager.set("REQUEST_HEADERS", {
94
+ "X-Parse-Maintenance-Key": config.maintenanceKey,
95
+ });
96
+ }
97
+ /**
98
+ * Read options for every query in the migration.
99
+ *
100
+ * Deliberately empty: authentication rides on the default header set in
101
+ * {@link initParse}, because the SDK's `useMaintenanceKey` option would be sent as
102
+ * a query parameter and rejected. Kept as a named constant so call sites still
103
+ * document that they are reading with elevated rights — RestQuery treats
104
+ * `isMaintenance` alongside `isMaster`, so these reads bypass ACL and CLP.
105
+ */
106
+ exports.MK = {};