@openinc/parse-server-opendash 4.1.1 → 4.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/dist/featuremap.json +27 -12
  2. package/dist/features/config/helper/customOptions.js +9 -0
  3. package/dist/features/config/helper/isFeatureEnabled.d.ts +1 -1
  4. package/dist/features/config/helper/isFeatureEnabled.js +2 -0
  5. package/dist/features/meta/index.d.ts +1 -0
  6. package/dist/features/meta/index.js +5 -0
  7. package/dist/features/meta/services/registerMetaContext.d.ts +29 -0
  8. package/dist/features/meta/services/registerMetaContext.js +208 -0
  9. package/dist/features/permissions/types/Permissions.d.ts +24 -1
  10. package/dist/features/permissions/types/Permissions.js +34 -0
  11. package/dist/features/ruleset/applyRuleSet.d.ts +41 -0
  12. package/dist/features/ruleset/applyRuleSet.js +81 -0
  13. package/dist/features/schema/helper/defaultHandler.js +13 -0
  14. package/dist/features/schema/helper/migrationMode.d.ts +26 -0
  15. package/dist/features/schema/helper/migrationMode.js +48 -0
  16. package/dist/features/schema/index.d.ts +1 -0
  17. package/dist/features/schema/index.js +3 -1
  18. package/dist/features/schema/services/beforeSaveHook.js +21 -0
  19. package/dist/features/translations/services/initTranslation.js +1 -1
  20. package/dist/hooks/Chat_Message.d.ts +1 -0
  21. package/dist/hooks/Chat_Message.js +179 -0
  22. package/dist/hooks/Meta_Config.js +19 -10
  23. package/dist/hooks/Meta_Context.d.ts +6 -0
  24. package/dist/hooks/Meta_Context.js +16 -0
  25. package/dist/hooks/Meta_Entry.js +20 -10
  26. package/dist/hooks/Service_Equipment.d.ts +1 -0
  27. package/dist/hooks/Service_Equipment.js +38 -0
  28. package/dist/hooks/Service_FormConfig.d.ts +1 -0
  29. package/dist/hooks/Service_FormConfig.js +17 -0
  30. package/dist/hooks/Service_Project.d.ts +1 -0
  31. package/dist/hooks/Service_Project.js +17 -0
  32. package/dist/hooks/Service_Schedule.d.ts +1 -0
  33. package/dist/hooks/Service_Schedule.js +24 -0
  34. package/dist/hooks/Service_Schedule_Execution.d.ts +1 -0
  35. package/dist/hooks/Service_Schedule_Execution.js +17 -0
  36. package/dist/hooks/Service_Schedule_Template.d.ts +1 -0
  37. package/dist/hooks/Service_Schedule_Template.js +17 -0
  38. package/dist/hooks/Service_Status.d.ts +1 -0
  39. package/dist/hooks/Service_Status.js +32 -0
  40. package/dist/hooks/Service_Tag.d.ts +1 -0
  41. package/dist/hooks/Service_Tag.js +168 -0
  42. package/dist/hooks/Service_Ticket.d.ts +1 -0
  43. package/dist/hooks/Service_Ticket.js +433 -0
  44. package/dist/hooks/Service_TriggerState.d.ts +1 -0
  45. package/dist/hooks/Service_TriggerState.js +17 -0
  46. package/dist/hooks/_ChangeLog.js +27 -6
  47. package/dist/i18n/de-DE/chat.json +18 -0
  48. package/dist/i18n/de-DE/openservice.json +14 -0
  49. package/dist/i18n/en-US/chat.json +18 -0
  50. package/dist/i18n/en-US/openservice.json +14 -0
  51. package/dist/scripts/backfill-meta-contexts.d.ts +1 -0
  52. package/dist/scripts/backfill-meta-contexts.js +100 -0
  53. package/dist/scripts/migration/lib/acl.d.ts +52 -0
  54. package/dist/scripts/migration/lib/acl.js +79 -0
  55. package/dist/scripts/migration/lib/articleMeta.d.ts +47 -0
  56. package/dist/scripts/migration/lib/articleMeta.js +132 -0
  57. package/dist/scripts/migration/lib/env.d.ts +56 -0
  58. package/dist/scripts/migration/lib/env.js +106 -0
  59. package/dist/scripts/migration/lib/latest.d.ts +47 -0
  60. package/dist/scripts/migration/lib/latest.js +115 -0
  61. package/dist/scripts/migration/lib/map.d.ts +53 -0
  62. package/dist/scripts/migration/lib/map.js +113 -0
  63. package/dist/scripts/migration/lib/material.d.ts +47 -0
  64. package/dist/scripts/migration/lib/material.js +51 -0
  65. package/dist/scripts/migration/lib/message.d.ts +78 -0
  66. package/dist/scripts/migration/lib/message.js +115 -0
  67. package/dist/scripts/migration/lib/pass.d.ts +67 -0
  68. package/dist/scripts/migration/lib/pass.js +89 -0
  69. package/dist/scripts/migration/lib/ptr.d.ts +32 -0
  70. package/dist/scripts/migration/lib/ptr.js +38 -0
  71. package/dist/scripts/migration/lib/report.d.ts +41 -0
  72. package/dist/scripts/migration/lib/report.js +78 -0
  73. package/dist/scripts/migration/lib/rest.d.ts +47 -0
  74. package/dist/scripts/migration/lib/rest.js +68 -0
  75. package/dist/scripts/migration/lib/ticket.d.ts +72 -0
  76. package/dist/scripts/migration/lib/ticket.js +192 -0
  77. package/dist/scripts/migration/passes/01-statuses.d.ts +12 -0
  78. package/dist/scripts/migration/passes/01-statuses.js +122 -0
  79. package/dist/scripts/migration/passes/02-tags.d.ts +13 -0
  80. package/dist/scripts/migration/passes/02-tags.js +150 -0
  81. package/dist/scripts/migration/passes/03-equipment.d.ts +14 -0
  82. package/dist/scripts/migration/passes/03-equipment.js +176 -0
  83. package/dist/scripts/migration/passes/04-projects.d.ts +27 -0
  84. package/dist/scripts/migration/passes/04-projects.js +241 -0
  85. package/dist/scripts/migration/passes/05-articles.d.ts +25 -0
  86. package/dist/scripts/migration/passes/05-articles.js +203 -0
  87. package/dist/scripts/migration/passes/06-formconfigs.d.ts +17 -0
  88. package/dist/scripts/migration/passes/06-formconfigs.js +166 -0
  89. package/dist/scripts/migration/passes/07-meta-configs.d.ts +46 -0
  90. package/dist/scripts/migration/passes/07-meta-configs.js +185 -0
  91. package/dist/scripts/migration/passes/08-tickets.d.ts +27 -0
  92. package/dist/scripts/migration/passes/08-tickets.js +353 -0
  93. package/dist/scripts/migration/passes/09-ticket-meta.d.ts +12 -0
  94. package/dist/scripts/migration/passes/09-ticket-meta.js +136 -0
  95. package/dist/scripts/migration/passes/10-chat.d.ts +17 -0
  96. package/dist/scripts/migration/passes/10-chat.js +230 -0
  97. package/dist/scripts/migration/passes/11-assets.d.ts +17 -0
  98. package/dist/scripts/migration/passes/11-assets.js +93 -0
  99. package/dist/scripts/migration/passes/12-history.d.ts +24 -0
  100. package/dist/scripts/migration/passes/12-history.js +332 -0
  101. package/dist/scripts/migration/run.d.ts +1 -0
  102. package/dist/scripts/migration/run.js +148 -0
  103. package/dist/scripts/migration/verify.d.ts +1 -0
  104. package/dist/scripts/migration/verify.js +620 -0
  105. package/dist/scripts/seed-v1-testdata.d.ts +41 -0
  106. package/dist/scripts/seed-v1-testdata.js +543 -0
  107. package/dist/types/BDE_Article.d.ts +3 -0
  108. package/dist/types/BDE_Article.js +6 -0
  109. package/dist/types/Chat_Message.d.ts +49 -0
  110. package/dist/types/Chat_Message.js +87 -0
  111. package/dist/types/MES_RessourceOverview.d.ts +38 -0
  112. package/dist/types/MES_RessourceOverview.js +63 -0
  113. package/dist/types/Meta_Context.d.ts +43 -0
  114. package/dist/types/Meta_Context.js +75 -0
  115. package/dist/types/Service_Equipment.d.ts +33 -0
  116. package/dist/types/Service_Equipment.js +57 -0
  117. package/dist/types/Service_FormConfig.d.ts +33 -0
  118. package/dist/types/Service_FormConfig.js +57 -0
  119. package/dist/types/Service_MeterReading.d.ts +35 -0
  120. package/dist/types/Service_MeterReading.js +57 -0
  121. package/dist/types/Service_Project.d.ts +53 -0
  122. package/dist/types/Service_Project.js +93 -0
  123. package/dist/types/Service_Schedule.d.ts +66 -0
  124. package/dist/types/Service_Schedule.js +117 -0
  125. package/dist/types/Service_Schedule_Execution.d.ts +38 -0
  126. package/dist/types/Service_Schedule_Execution.js +63 -0
  127. package/dist/types/Service_Schedule_Template.d.ts +36 -0
  128. package/dist/types/Service_Schedule_Template.js +60 -0
  129. package/dist/types/Service_Status.d.ts +30 -0
  130. package/dist/types/Service_Status.js +51 -0
  131. package/dist/types/Service_Tag.d.ts +30 -0
  132. package/dist/types/Service_Tag.js +51 -0
  133. package/dist/types/Service_Ticket.d.ts +92 -0
  134. package/dist/types/Service_Ticket.js +165 -0
  135. package/dist/types/Service_TicketCounter.d.ts +18 -0
  136. package/dist/types/Service_TicketCounter.js +27 -0
  137. package/dist/types/Service_TriggerState.d.ts +31 -0
  138. package/dist/types/Service_TriggerState.js +51 -0
  139. package/dist/types/_Session.d.ts +0 -1
  140. package/dist/types/_User.d.ts +2 -6
  141. package/dist/types/custom/MetaField.d.ts +9 -0
  142. package/dist/types/custom/TicketMaterial.d.ts +32 -0
  143. package/dist/types/custom/TicketMaterial.js +2 -0
  144. package/dist/types/index.d.ts +30 -0
  145. package/dist/types/index.js +33 -3
  146. package/package.json +6 -3
  147. package/schema/BDE_Article.json +3 -0
  148. package/schema/Changelog.json +3 -6
  149. package/schema/Chat_Message.json +88 -0
  150. package/schema/Meta_Context.json +78 -0
  151. package/schema/Service_Equipment.json +55 -0
  152. package/schema/Service_FormConfig.json +66 -0
  153. package/schema/Service_MeterReading.json +54 -0
  154. package/schema/Service_MigrationMap.json +55 -0
  155. package/schema/Service_Project.json +77 -0
  156. package/schema/Service_Schedule.json +101 -0
  157. package/schema/Service_Schedule_Execution.json +58 -0
  158. package/schema/Service_Schedule_Template.json +73 -0
  159. package/schema/Service_Status.json +50 -0
  160. package/schema/Service_Tag.json +50 -0
  161. package/schema/Service_Ticket.json +114 -0
  162. package/schema/Service_TicketCounter.json +38 -0
  163. package/schema/Service_TriggerState.json +50 -0
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ /**
3
+ * Migration mode — an explicit escape hatch for the openservice v1 → v2 data
4
+ * migration (see `plugins/openservice/MIGRATION_PLAN.md` in the opendash repo).
5
+ *
6
+ * Why this exists: the migration writes with the **maintenance** key, because that
7
+ * is the only auth level Parse Server lets set a historical `createdAt`
8
+ * (`RestWrite.js`: `if (this.auth.isMaintenance && this.data.createdAt …)`). But
9
+ * `Auth.maintenance()` leaves `isMaster: false`, and `getRequestObject()` derives
10
+ * `request.master` from `isMaster` only — so triggers see `master === false`.
11
+ * `defaultHandler` therefore does *not* take its master-key shortcut, reaches
12
+ * `if (!request.user) throw`, and every maintenance-key write fails.
13
+ *
14
+ * So `request.master` cannot serve as an "is this the migration?" signal, and
15
+ * sniffing for a missing user would silently change behaviour for ordinary
16
+ * requests. An explicit env flag keeps the exception auditable, greppable, and
17
+ * impossible to hit by accident.
18
+ *
19
+ * While enabled, the caller takes over responsibility for everything it disables:
20
+ * `tenant`, `user` and the ACL on every object written.
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.isMigrationMode = isMigrationMode;
24
+ let warned = false;
25
+ /**
26
+ * True when `OD_MIGRATION_MODE=1`. Logs a loud one-time banner the first time it
27
+ * is observed, so a server accidentally left in migration mode is obvious in the
28
+ * logs rather than silently permissive.
29
+ */
30
+ function isMigrationMode() {
31
+ const enabled = process.env.OD_MIGRATION_MODE === "1";
32
+ if (enabled && !warned) {
33
+ warned = true;
34
+ console.warn([
35
+ "",
36
+ "!".repeat(80),
37
+ "[@openinc/parse-server-opendash] OD_MIGRATION_MODE=1 — MIGRATION MODE ACTIVE",
38
+ " · defaultHandler / defaultAclHandler are bypassed (no tenant, user or ACL",
39
+ " defaults are applied — the writer must set them itself)",
40
+ " · assignment / watcher notifications are suppressed",
41
+ " · tag closure reconciliation is suppressed",
42
+ " This must NOT be set during normal operation.",
43
+ "!".repeat(80),
44
+ "",
45
+ ].join("\n"));
46
+ }
47
+ return enabled;
48
+ }
@@ -5,6 +5,7 @@ export { beforeSaveHook } from "./services/beforeSaveHook.js";
5
5
  export * from "./types/hookTypes.js";
6
6
  export { defaultAclHandler, defaultHandler } from "./helper/defaultHandler.js";
7
7
  export { immutableField } from "./helper/immutableField.js";
8
+ export { isMigrationMode } from "./helper/migrationMode.js";
8
9
  export { ensureRole } from "./helper/ensureRole.js";
9
10
  export { ensureUserRole } from "./helper/ensureUserRole.js";
10
11
  export { initSchema } from "./services/initSchema.js";
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.initDefaultRoles = exports.initDefaultData = exports.initSchema = exports.ensureUserRole = exports.ensureRole = exports.immutableField = exports.defaultHandler = exports.defaultAclHandler = exports.beforeSaveHook = exports.beforeDeleteHook = exports.afterSaveHook = exports.afterDeleteHook = void 0;
17
+ exports.initDefaultRoles = exports.initDefaultData = exports.initSchema = exports.ensureUserRole = exports.ensureRole = exports.isMigrationMode = exports.immutableField = exports.defaultHandler = exports.defaultAclHandler = exports.beforeSaveHook = exports.beforeDeleteHook = exports.afterSaveHook = exports.afterDeleteHook = void 0;
18
18
  var afterDeleteHook_js_1 = require("./services/afterDeleteHook.js");
19
19
  Object.defineProperty(exports, "afterDeleteHook", { enumerable: true, get: function () { return afterDeleteHook_js_1.afterDeleteHook; } });
20
20
  var afterSaveHook_js_1 = require("./services/afterSaveHook.js");
@@ -29,6 +29,8 @@ Object.defineProperty(exports, "defaultAclHandler", { enumerable: true, get: fun
29
29
  Object.defineProperty(exports, "defaultHandler", { enumerable: true, get: function () { return defaultHandler_js_1.defaultHandler; } });
30
30
  var immutableField_js_1 = require("./helper/immutableField.js");
31
31
  Object.defineProperty(exports, "immutableField", { enumerable: true, get: function () { return immutableField_js_1.immutableField; } });
32
+ var migrationMode_js_1 = require("./helper/migrationMode.js");
33
+ Object.defineProperty(exports, "isMigrationMode", { enumerable: true, get: function () { return migrationMode_js_1.isMigrationMode; } });
32
34
  var ensureRole_js_1 = require("./helper/ensureRole.js");
33
35
  Object.defineProperty(exports, "ensureRole", { enumerable: true, get: function () { return ensureRole_js_1.ensureRole; } });
34
36
  var ensureUserRole_js_1 = require("./helper/ensureUserRole.js");
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.beforeSaveHook = beforeSaveHook;
7
7
  const node_1 = __importDefault(require("parse/node"));
8
+ const migrationMode_js_1 = require("../helper/migrationMode.js");
8
9
  const beforeSaveHooks = {};
9
10
  /**
10
11
  * Registers a beforeSave hook for a Parse class. In comparison to Parse.Cloud.beforeSave(), this function allows multiple beforeSave hooks to be registered for the same class.
@@ -17,6 +18,26 @@ const beforeSaveHooks = {};
17
18
  function beforeSaveHook(target, callback) {
18
19
  // @ts-ignore
19
20
  const className = typeof target === "string" ? target : target.className;
21
+ // In migration mode, do not register a beforeSave trigger at all.
22
+ //
23
+ // This is not an optimisation — it is the only way historical timestamps can be
24
+ // written. Parse Server only honours a supplied `createdAt` in
25
+ // `setRequiredFieldsIfNeeded()`, which runs *after* `runBeforeSaveTrigger()`,
26
+ // and that trigger path does `this.data = response.object` unconditionally
27
+ // (`parse-server/lib/RestWrite.js`). `response.object` has been round-tripped
28
+ // through a `Parse.Object`, whose `set()` silently discards `createdAt` /
29
+ // `updatedAt` — so the field is gone by the time the maintenance-key branch
30
+ // looks for it. Verified empirically: an identical write to a class with no
31
+ // beforeSave trigger keeps the historical date; to a class with one, it does not.
32
+ //
33
+ // The migration therefore takes over what these hooks would have done —
34
+ // `tenant`, `user`, ACLs and ticket numbering — which it was already doing for
35
+ // the parts `OD_MIGRATION_MODE` gates off.
36
+ if ((0, migrationMode_js_1.isMigrationMode)()) {
37
+ console.warn(`[@openinc/parse-server-opendash] OD_MIGRATION_MODE: NOT registering ` +
38
+ `beforeSave for ${className} — historical timestamps require no trigger`);
39
+ return;
40
+ }
20
41
  if (!beforeSaveHooks[className]) {
21
42
  beforeSaveHooks[className] = [];
22
43
  node_1.default.Cloud.beforeSave(className, async function beforeSaveHookFunction(request) {
@@ -62,7 +62,7 @@ async function initTranslations() {
62
62
  fallbackLng: "en-US",
63
63
  // Specify available languages to match your directories
64
64
  supportedLngs: ["de-DE", "en-US"],
65
- ns: ["server", "maintenance"],
65
+ ns: ["server", "maintenance", "chat", "openservice"],
66
66
  defaultNS: "server",
67
67
  debug: false,
68
68
  // Load resources asynchronously (await init promise)
@@ -0,0 +1 @@
1
+ export declare function init(): Promise<void>;
@@ -0,0 +1,179 @@
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.init = init;
7
+ const node_1 = __importDefault(require("parse/node"));
8
+ const i18next_1 = __importDefault(require("i18next"));
9
+ const index_js_1 = require("../features/schema/index.js");
10
+ const getUserLanguage_js_1 = require("../helper/getUserLanguage.js");
11
+ const index_js_2 = require("../types/index.js");
12
+ async function init() {
13
+ (0, index_js_1.beforeSaveHook)(index_js_2.Chat_Message, async (request) => {
14
+ await (0, index_js_1.defaultHandler)(request);
15
+ await (0, index_js_1.defaultAclHandler)(request);
16
+ });
17
+ (0, index_js_1.afterSaveHook)(index_js_2.Chat_Message, async ({ object, original }) => {
18
+ try {
19
+ await notifyMentions(object, original);
20
+ }
21
+ catch (error) {
22
+ console.error("[@openinc/parse-server-opendash][Chat_Message] Failed to notify mentioned users", error);
23
+ }
24
+ });
25
+ }
26
+ /**
27
+ * Notifies the users and roles mentioned in a chat message.
28
+ *
29
+ * Only *newly added* mentions are notified: we diff the message's current
30
+ * `mentions` against the previous save (`original`). On creation `original` is
31
+ * undefined, so every mention is new; on an edit that leaves the mentions
32
+ * untouched (the common case — most edits only change the text around existing
33
+ * mentions, react, or soft-delete) nothing is sent, so repeated edits never
34
+ * re-notify. The only case that re-notifies is removing a mention and re-adding
35
+ * it across separate edits, which is rare and harmless.
36
+ *
37
+ * Dedup + precedence: a user is notified at most once. A directly mentioned
38
+ * user wins over a role mention (`_User` > `_Role`) — they get the personal
39
+ * "you were mentioned" notification and are skipped when expanding roles. The
40
+ * message author is never notified for mentioning themselves.
41
+ */
42
+ async function notifyMentions(object, original) {
43
+ // A soft-delete is just an update; never notify for it.
44
+ if (object.get("deleted"))
45
+ return;
46
+ const previousKeys = new Set(normalizeMentions(original?.get("mentions")).map(mentionKey));
47
+ const newMentions = normalizeMentions(object.get("mentions")).filter((mention) => !previousKeys.has(mentionKey(mention)));
48
+ if (newMentions.length === 0)
49
+ return;
50
+ const username = object.get("authorName") || "System";
51
+ const messageText = messageSnippet(object.get("text"));
52
+ const url = getMessageLink(object.get("context"));
53
+ // Users already handled — seeded with the author so they never self-notify.
54
+ const notified = new Set();
55
+ const authorId = object.get("author")?.id;
56
+ if (authorId)
57
+ notified.add(authorId);
58
+ // 1. Directly mentioned users (take precedence over role membership).
59
+ const userIds = newMentions
60
+ .filter((mention) => mention.className === "_User")
61
+ .map((mention) => mention.id);
62
+ const users = userIds.length
63
+ ? await new node_1.default.Query(node_1.default.User)
64
+ .containedIn("objectId", userIds)
65
+ .findAll({ useMasterKey: true })
66
+ : [];
67
+ for (const user of users) {
68
+ if (!user.id || notified.has(user.id))
69
+ continue;
70
+ notified.add(user.id);
71
+ await sendMentionNotification(user, "user", object, {
72
+ username,
73
+ messageText,
74
+ url,
75
+ });
76
+ }
77
+ // 2. Mentioned roles → their members, skipping anyone already notified.
78
+ const roleIds = newMentions
79
+ .filter((mention) => mention.className === "_Role")
80
+ .map((mention) => mention.id);
81
+ const roles = roleIds.length
82
+ ? await new node_1.default.Query(node_1.default.Role)
83
+ .containedIn("objectId", roleIds)
84
+ .findAll({ useMasterKey: true })
85
+ : [];
86
+ for (const role of roles) {
87
+ const members = await role
88
+ .relation("users")
89
+ .query()
90
+ .findAll({ useMasterKey: true });
91
+ for (const member of members) {
92
+ if (!member.id || notified.has(member.id))
93
+ continue;
94
+ notified.add(member.id);
95
+ await sendMentionNotification(member, "role", object, {
96
+ username,
97
+ messageText,
98
+ url,
99
+ role: role.get("label"),
100
+ });
101
+ }
102
+ }
103
+ }
104
+ /**
105
+ * Builds and saves a single mention notification, translated into the
106
+ * recipient's preferred language. Delivery (email + web push) is handled
107
+ * automatically by the {@link Notification} afterSave hook.
108
+ */
109
+ async function sendMentionNotification(user, kind, message, vars) {
110
+ await i18next_1.default.changeLanguage(await (0, getUserLanguage_js_1.getUserLanguage)(user));
111
+ const base = `chat:message.mention.${kind}.notification`;
112
+ await new index_js_2.Notification({
113
+ title: i18next_1.default.t(`${base}.title`),
114
+ description: i18next_1.default.t(`${base}.description`, {
115
+ username: vars.username,
116
+ messageText: vars.messageText,
117
+ role: vars.role,
118
+ }),
119
+ data: {
120
+ url: vars.url,
121
+ messageId: message.id,
122
+ context: message.get("context"),
123
+ translation: {
124
+ username: vars.username,
125
+ messageText: vars.messageText,
126
+ role: vars.role,
127
+ },
128
+ },
129
+ user: user,
130
+ isSent: false,
131
+ }).save(null, { useMasterKey: true });
132
+ }
133
+ /** Coerces the raw `mentions` field into a clean list of valid mentions. */
134
+ function normalizeMentions(value) {
135
+ if (!Array.isArray(value))
136
+ return [];
137
+ return value.filter((entry) => !!entry &&
138
+ typeof entry.id === "string" &&
139
+ typeof entry.className === "string");
140
+ }
141
+ /** Stable identity of a mention, used for de-duplication and diffing. */
142
+ function mentionKey(mention) {
143
+ return `${mention.className}:${mention.id}`;
144
+ }
145
+ /**
146
+ * Derives a deep-link path from the chat's `context`. The context is the
147
+ * JSON-stringified address tuple (e.g. `["service","ticket","<id>"]`). Only the
148
+ * service-ticket context is linkable today; anything else yields no link.
149
+ */
150
+ function getMessageLink(context) {
151
+ if (typeof context !== "string")
152
+ return undefined;
153
+ try {
154
+ const parts = JSON.parse(context);
155
+ if (Array.isArray(parts) &&
156
+ parts[0] === "service" &&
157
+ parts[1] === "ticket" &&
158
+ parts[2]) {
159
+ return `/service/tickets/${parts[2]}`;
160
+ }
161
+ }
162
+ catch {
163
+ // context is not a JSON tuple — no link
164
+ }
165
+ return undefined;
166
+ }
167
+ const SNIPPET_MAX_LENGTH = 140;
168
+ /** Inline mention token: `[label](#mention:_User:<id>)` → keep just `label`. */
169
+ const MENTION_TOKEN = /\[([^\]]*)\]\(#mention:[^)]*\)/g;
170
+ /** Builds a short, plain-text preview of the message for the notification body:
171
+ * mention tokens collapse to their label and whitespace is normalized. */
172
+ function messageSnippet(text) {
173
+ if (typeof text !== "string")
174
+ return "";
175
+ const plain = text.replace(MENTION_TOKEN, "$1").replace(/\s+/g, " ").trim();
176
+ return plain.length > SNIPPET_MAX_LENGTH
177
+ ? `${plain.slice(0, SNIPPET_MAX_LENGTH - 1).trimEnd()}…`
178
+ : plain;
179
+ }
@@ -1,17 +1,26 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.init = init;
4
- const index_js_1 = require("../features/schema/index.js");
5
- const index_js_2 = require("../types/index.js");
4
+ const index_js_1 = require("../features/meta/index.js");
5
+ const index_js_2 = require("../features/schema/index.js");
6
+ const index_js_3 = require("../types/index.js");
6
7
  async function init() {
7
- (0, index_js_1.beforeSaveHook)(index_js_2.Meta_Config, async (request) => {
8
- const { object, original, user } = request;
9
- await (0, index_js_1.defaultHandler)(request);
10
- await (0, index_js_1.defaultAclHandler)(request);
11
- // TODO
8
+ (0, index_js_2.beforeSaveHook)(index_js_3.Meta_Config, async (request) => {
9
+ await (0, index_js_2.defaultHandler)(request);
10
+ await (0, index_js_2.defaultAclHandler)(request);
12
11
  });
13
- (0, index_js_1.afterSaveHook)(index_js_2.Meta_Config, async (request) => {
14
- const { object, original, user } = request;
15
- // TODO
12
+ (0, index_js_2.afterSaveHook)(index_js_3.Meta_Config, async ({ object }) => {
13
+ // Keeps the context directory current: existence, label and description all
14
+ // come from here, so a config save is what makes a context "named".
15
+ try {
16
+ await (0, index_js_1.registerMetaContext)({
17
+ context: object.context ?? "",
18
+ tenant: object.tenant,
19
+ config: object,
20
+ });
21
+ }
22
+ catch (error) {
23
+ console.error("[@openinc/parse-server-opendash][Meta_Config] Failed to register meta context", error);
24
+ }
16
25
  });
17
26
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The context directory itself. Rows are written by `registerMetaContext` from
3
+ * the `Meta_Config` / `Meta_Entry` hooks with the master key, which skips these
4
+ * handlers — they exist for the rare direct edit (renaming a label by hand).
5
+ */
6
+ export declare function init(): Promise<void>;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.init = init;
4
+ const index_js_1 = require("../features/schema/index.js");
5
+ const index_js_2 = require("../types/index.js");
6
+ /**
7
+ * The context directory itself. Rows are written by `registerMetaContext` from
8
+ * the `Meta_Config` / `Meta_Entry` hooks with the master key, which skips these
9
+ * handlers — they exist for the rare direct edit (renaming a label by hand).
10
+ */
11
+ async function init() {
12
+ (0, index_js_1.beforeSaveHook)(index_js_2.Meta_Context, async (request) => {
13
+ await (0, index_js_1.defaultHandler)(request);
14
+ await (0, index_js_1.defaultAclHandler)(request);
15
+ });
16
+ }
@@ -1,17 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.init = init;
4
- const index_js_1 = require("../features/schema/index.js");
5
- const index_js_2 = require("../types/index.js");
4
+ const index_js_1 = require("../features/meta/index.js");
5
+ const index_js_2 = require("../features/schema/index.js");
6
+ const index_js_3 = require("../types/index.js");
6
7
  async function init() {
7
- (0, index_js_1.beforeSaveHook)(index_js_2.Meta_Entry, async (request) => {
8
- const { object, original, user } = request;
9
- await (0, index_js_1.defaultHandler)(request);
10
- await (0, index_js_1.defaultAclHandler)(request);
11
- // TODO
8
+ (0, index_js_2.beforeSaveHook)(index_js_3.Meta_Entry, async (request) => {
9
+ await (0, index_js_2.defaultHandler)(request);
10
+ await (0, index_js_2.defaultAclHandler)(request);
12
11
  });
13
- (0, index_js_1.afterSaveHook)(index_js_2.Meta_Entry, async (request) => {
14
- const { object, original, user } = request;
15
- // TODO
12
+ (0, index_js_2.afterSaveHook)(index_js_3.Meta_Entry, async ({ object }) => {
13
+ // An entry is the only trace of a context that has no config behind it —
14
+ // a `customConfig` form leaves `origin` empty and creates no Meta_Config,
15
+ // so without this the context would exist in the data and nowhere else.
16
+ try {
17
+ await (0, index_js_1.registerMetaContext)({
18
+ context: object.context ?? "",
19
+ tenant: object.tenant,
20
+ entryClassName: object.entryClassName,
21
+ });
22
+ }
23
+ catch (error) {
24
+ console.error("[@openinc/parse-server-opendash][Meta_Entry] Failed to register meta context", error);
25
+ }
16
26
  });
17
27
  }
@@ -0,0 +1 @@
1
+ export declare function init(): Promise<void>;
@@ -0,0 +1,38 @@
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.init = init;
7
+ const node_1 = __importDefault(require("parse/node"));
8
+ const index_js_1 = require("../features/schema/index.js");
9
+ const index_js_2 = require("../types/index.js");
10
+ async function init() {
11
+ (0, index_js_1.beforeSaveHook)(index_js_2.Service_Equipment, async (request) => {
12
+ const { object, original, user } = request;
13
+ await (0, index_js_1.defaultHandler)(request);
14
+ await (0, index_js_1.defaultAclHandler)(request);
15
+ // TODO
16
+ });
17
+ (0, index_js_1.afterSaveHook)(index_js_2.Service_Equipment, async (request) => {
18
+ const { object, original, user } = request;
19
+ // TODO
20
+ });
21
+ // Block deleting equipment that is still referenced where it matters:
22
+ // active tickets, and meter readings (whose `equipment` pointer is required,
23
+ // so orphaning it would break them).
24
+ (0, index_js_1.beforeDeleteHook)(index_js_2.Service_Equipment, async ({ object }) => {
25
+ const [tickets, readings] = await Promise.all([
26
+ new node_1.default.Query(index_js_2.Service_Ticket)
27
+ .equalTo("equipment", object)
28
+ .notEqualTo("deleted", true)
29
+ .count({ useMasterKey: true }),
30
+ new node_1.default.Query("OD3_Service_MeterReading")
31
+ .equalTo("equipment", object)
32
+ .count({ useMasterKey: true }),
33
+ ]);
34
+ if (tickets > 0 || readings > 0) {
35
+ throw new node_1.default.Error(node_1.default.Error.OPERATION_FORBIDDEN, `Cannot delete equipment: referenced by ${tickets} active ticket(s) and ${readings} meter reading(s).`);
36
+ }
37
+ });
38
+ }
@@ -0,0 +1 @@
1
+ export declare function init(): Promise<void>;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.init = init;
4
+ const index_js_1 = require("../features/schema/index.js");
5
+ const index_js_2 = require("../types/index.js");
6
+ async function init() {
7
+ (0, index_js_1.beforeSaveHook)(index_js_2.Service_FormConfig, async (request) => {
8
+ const { object, original, user } = request;
9
+ await (0, index_js_1.defaultHandler)(request);
10
+ await (0, index_js_1.defaultAclHandler)(request);
11
+ // TODO
12
+ });
13
+ (0, index_js_1.afterSaveHook)(index_js_2.Service_FormConfig, async (request) => {
14
+ const { object, original, user } = request;
15
+ // TODO
16
+ });
17
+ }
@@ -0,0 +1 @@
1
+ export declare function init(): Promise<void>;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.init = init;
4
+ const index_js_1 = require("../features/schema/index.js");
5
+ const index_js_2 = require("../types/index.js");
6
+ async function init() {
7
+ (0, index_js_1.beforeSaveHook)(index_js_2.Service_Project, async (request) => {
8
+ const { object, original, user } = request;
9
+ await (0, index_js_1.defaultHandler)(request);
10
+ await (0, index_js_1.defaultAclHandler)(request);
11
+ // TODO
12
+ });
13
+ (0, index_js_1.afterSaveHook)(index_js_2.Service_Project, async (request) => {
14
+ const { object, original, user } = request;
15
+ // TODO
16
+ });
17
+ }
@@ -0,0 +1 @@
1
+ export declare function init(): Promise<void>;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.init = init;
4
+ const index_js_1 = require("../features/schema/index.js");
5
+ const index_js_2 = require("../types/index.js");
6
+ async function init() {
7
+ (0, index_js_1.beforeSaveHook)(index_js_2.Service_Schedule, async (request) => {
8
+ const { object, original, user } = request;
9
+ await (0, index_js_1.defaultHandler)(request);
10
+ // `allowCustomACL` keeps an ACL set by the client instead of resetting it on
11
+ // every save. The frontend uses it for the per-record write lock ("Rechte
12
+ // nur für Ersteller", `openservice-schedule/utils/acl.ts`): the creator gets
13
+ // an explicit user grant, everyone else keeps read only. The handler still
14
+ // re-adds `od-admin` / `od-tenant-admin-<tenant>` write and
15
+ // `od-tenant-user-<tenant>` read afterwards, so a lock can never lock
16
+ // administrators out.
17
+ await (0, index_js_1.defaultAclHandler)(request, { allowCustomACL: true });
18
+ // TODO
19
+ });
20
+ (0, index_js_1.afterSaveHook)(index_js_2.Service_Schedule, async (request) => {
21
+ const { object, original, user } = request;
22
+ // TODO
23
+ });
24
+ }
@@ -0,0 +1 @@
1
+ export declare function init(): Promise<void>;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.init = init;
4
+ const index_js_1 = require("../features/schema/index.js");
5
+ const index_js_2 = require("../types/index.js");
6
+ async function init() {
7
+ (0, index_js_1.beforeSaveHook)(index_js_2.Service_Schedule_Execution, async (request) => {
8
+ const { object, original, user } = request;
9
+ await (0, index_js_1.defaultHandler)(request);
10
+ await (0, index_js_1.defaultAclHandler)(request);
11
+ // TODO
12
+ });
13
+ (0, index_js_1.afterSaveHook)(index_js_2.Service_Schedule_Execution, async (request) => {
14
+ const { object, original, user } = request;
15
+ // TODO
16
+ });
17
+ }
@@ -0,0 +1 @@
1
+ export declare function init(): Promise<void>;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.init = init;
4
+ const index_js_1 = require("../features/schema/index.js");
5
+ const index_js_2 = require("../types/index.js");
6
+ async function init() {
7
+ (0, index_js_1.beforeSaveHook)(index_js_2.Service_Schedule_Template, async (request) => {
8
+ const { object, original, user } = request;
9
+ await (0, index_js_1.defaultHandler)(request);
10
+ await (0, index_js_1.defaultAclHandler)(request);
11
+ // TODO
12
+ });
13
+ (0, index_js_1.afterSaveHook)(index_js_2.Service_Schedule_Template, async (request) => {
14
+ const { object, original, user } = request;
15
+ // TODO
16
+ });
17
+ }
@@ -0,0 +1 @@
1
+ export declare function init(): Promise<void>;
@@ -0,0 +1,32 @@
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.init = init;
7
+ const node_1 = __importDefault(require("parse/node"));
8
+ const index_js_1 = require("../features/schema/index.js");
9
+ const index_js_2 = require("../types/index.js");
10
+ async function init() {
11
+ (0, index_js_1.beforeSaveHook)(index_js_2.Service_Status, async (request) => {
12
+ const { object, original, user } = request;
13
+ await (0, index_js_1.defaultHandler)(request);
14
+ await (0, index_js_1.defaultAclHandler)(request);
15
+ // TODO
16
+ });
17
+ (0, index_js_1.afterSaveHook)(index_js_2.Service_Status, async (request) => {
18
+ const { object, original, user } = request;
19
+ // TODO
20
+ });
21
+ // A ticket's `status` is required — deleting a status still in use by active
22
+ // tickets would orphan that required pointer. Block it.
23
+ (0, index_js_1.beforeDeleteHook)(index_js_2.Service_Status, async ({ object }) => {
24
+ const inUse = await new node_1.default.Query(index_js_2.Service_Ticket)
25
+ .equalTo("status", object)
26
+ .notEqualTo("deleted", true)
27
+ .count({ useMasterKey: true });
28
+ if (inUse > 0) {
29
+ throw new node_1.default.Error(node_1.default.Error.OPERATION_FORBIDDEN, `Cannot delete status: still used by ${inUse} active ticket(s).`);
30
+ }
31
+ });
32
+ }
@@ -0,0 +1 @@
1
+ export declare function init(): Promise<void>;