@openinc/parse-server-opendash 4.1.8 → 4.2.1

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 (57) hide show
  1. package/dist/features/openservice/init.js +4 -0
  2. package/dist/features/openservice/schedules/calendarSync/types/Event.d.ts +2 -0
  3. package/dist/features/openservice/serviceSchedules/acl/assignmentAcl.d.ts +30 -0
  4. package/dist/features/openservice/serviceSchedules/acl/assignmentAcl.js +85 -0
  5. package/dist/features/openservice/serviceSchedules/acl/index.d.ts +1 -0
  6. package/dist/features/openservice/serviceSchedules/acl/index.js +6 -0
  7. package/dist/features/openservice/serviceSchedules/calendarSync/functions/eventToPlan.d.ts +50 -0
  8. package/dist/features/openservice/serviceSchedules/calendarSync/functions/eventToPlan.js +115 -0
  9. package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.d.ts +59 -0
  10. package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.js +353 -0
  11. package/dist/features/openservice/serviceSchedules/calendarSync/functions/planToEvent.d.ts +27 -0
  12. package/dist/features/openservice/serviceSchedules/calendarSync/functions/planToEvent.js +145 -0
  13. package/dist/features/openservice/serviceSchedules/calendarSync/index.d.ts +8 -0
  14. package/dist/features/openservice/serviceSchedules/calendarSync/index.js +28 -0
  15. package/dist/features/openservice/serviceSchedules/calendarSync/service/ServiceCalendarSyncPoller.d.ts +54 -0
  16. package/dist/features/openservice/serviceSchedules/calendarSync/service/ServiceCalendarSyncPoller.js +248 -0
  17. package/dist/features/openservice/serviceSchedules/calendarSync/service/serviceCalendarMeta.d.ts +55 -0
  18. package/dist/features/openservice/serviceSchedules/calendarSync/service/serviceCalendarMeta.js +109 -0
  19. package/dist/features/openservice/serviceSchedules/calendarSync/service/syncPlanToCalendar.d.ts +26 -0
  20. package/dist/features/openservice/serviceSchedules/calendarSync/service/syncPlanToCalendar.js +122 -0
  21. package/dist/features/openservice/serviceSchedules/calendarSync/types/ServiceSyncTypes.d.ts +55 -0
  22. package/dist/features/openservice/serviceSchedules/calendarSync/types/ServiceSyncTypes.js +8 -0
  23. package/dist/features/openservice/serviceSchedules/initServiceSchedulesFeature.d.ts +10 -0
  24. package/dist/features/openservice/serviceSchedules/initServiceSchedulesFeature.js +39 -0
  25. package/dist/features/openservice/serviceTickets/alarmActions/functions/alarmTicketFields.d.ts +35 -0
  26. package/dist/features/openservice/serviceTickets/alarmActions/functions/alarmTicketFields.js +103 -0
  27. package/dist/features/openservice/serviceTickets/alarmActions/index.d.ts +7 -0
  28. package/dist/features/openservice/serviceTickets/alarmActions/index.js +16 -0
  29. package/dist/features/openservice/serviceTickets/alarmActions/service/createAlarmTicket.d.ts +33 -0
  30. package/dist/features/openservice/serviceTickets/alarmActions/service/createAlarmTicket.js +179 -0
  31. package/dist/features/openservice/serviceTickets/alarmActions/service/ensureAlarmTicketAction.d.ts +22 -0
  32. package/dist/features/openservice/serviceTickets/alarmActions/service/ensureAlarmTicketAction.js +127 -0
  33. package/dist/features/openservice/serviceTickets/alarmActions/types/AlarmTicketTypes.d.ts +80 -0
  34. package/dist/features/openservice/serviceTickets/alarmActions/types/AlarmTicketTypes.js +28 -0
  35. package/dist/features/permissions/types/Permissions.d.ts +1 -2
  36. package/dist/features/permissions/types/Permissions.js +8 -2
  37. package/dist/functions/openinc-openservice-alarm-create-ticket.d.ts +25 -0
  38. package/dist/functions/openinc-openservice-alarm-create-ticket.js +63 -0
  39. package/dist/functions/openinc-openservice-notify-contacts.d.ts +16 -0
  40. package/dist/functions/openinc-openservice-notify-contacts.js +82 -0
  41. package/dist/functions/openinc-openservice-schedule-calendar-sync.d.ts +15 -0
  42. package/dist/functions/openinc-openservice-schedule-calendar-sync.js +60 -0
  43. package/dist/hooks/Service_Schedule.js +89 -16
  44. package/dist/hooks/Service_Schedule_Template.js +28 -7
  45. package/dist/hooks/Service_Ticket.js +2 -2
  46. package/dist/types/Service_Schedule.d.ts +12 -0
  47. package/dist/types/Service_Schedule.js +21 -0
  48. package/dist/types/Service_Schedule_Execution.d.ts +6 -3
  49. package/dist/types/Service_Schedule_Execution.js +6 -0
  50. package/dist/types/Service_Schedule_Template.d.ts +7 -0
  51. package/dist/types/Service_Schedule_Template.js +12 -0
  52. package/package.json +5 -4
  53. package/schema/Service_Schedule.json +17 -0
  54. package/schema/Service_Schedule_Execution.json +5 -0
  55. package/schema/Service_Schedule_Template.json +9 -0
  56. package/dist/features/ruleset/applyRuleSet.d.ts +0 -41
  57. package/dist/features/ruleset/applyRuleSet.js +0 -81
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Raise an open.SERVICE ticket for a fired alarm.
3
+ *
4
+ * The receiving end of the `openservice-create-ticket` alarm action: an alarm
5
+ * watches a sensor, the rule breaks, and whatever executes alarm actions calls
6
+ * this with the alarm's data and the settings someone filled into the action's
7
+ * form.
8
+ *
9
+ * **Authentication** — one of:
10
+ *
11
+ * - the **master key** (server-to-server inside the stack), then `tenantId`
12
+ * must be given;
13
+ * - a **shared secret** matching an `OPENSERVICE_ALARM_TICKET_SECRET` config
14
+ * row, which also establishes the tenant. An external executor is not
15
+ * trusted to name a tenant, so the secret does it.
16
+ *
17
+ * A logged-in user is deliberately *not* enough: this is a machine endpoint,
18
+ * and a browser session should not be able to fabricate alarm tickets.
19
+ *
20
+ * Params: `{ event, params?, secret?, tenantId? }`.
21
+ * Returns `{ ticketId, created, origin, equipmentId? }` — `created: false`
22
+ * means an open ticket for this alarm already existed (see
23
+ * `createAlarmTicket`).
24
+ */
25
+ export declare function init(name: string): Promise<void>;
@@ -0,0 +1,63 @@
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/openservice/serviceTickets/alarmActions/index.js");
9
+ const index_js_2 = require("../types/index.js");
10
+ /**
11
+ * Raise an open.SERVICE ticket for a fired alarm.
12
+ *
13
+ * The receiving end of the `openservice-create-ticket` alarm action: an alarm
14
+ * watches a sensor, the rule breaks, and whatever executes alarm actions calls
15
+ * this with the alarm's data and the settings someone filled into the action's
16
+ * form.
17
+ *
18
+ * **Authentication** — one of:
19
+ *
20
+ * - the **master key** (server-to-server inside the stack), then `tenantId`
21
+ * must be given;
22
+ * - a **shared secret** matching an `OPENSERVICE_ALARM_TICKET_SECRET` config
23
+ * row, which also establishes the tenant. An external executor is not
24
+ * trusted to name a tenant, so the secret does it.
25
+ *
26
+ * A logged-in user is deliberately *not* enough: this is a machine endpoint,
27
+ * and a browser session should not be able to fabricate alarm tickets.
28
+ *
29
+ * Params: `{ event, params?, secret?, tenantId? }`.
30
+ * Returns `{ ticketId, created, origin, equipmentId? }` — `created: false`
31
+ * means an open ticket for this alarm already existed (see
32
+ * `createAlarmTicket`).
33
+ */
34
+ async function init(name) {
35
+ node_1.default.Cloud.define(name, async (request) => {
36
+ const { event, params, secret, tenantId } = request.params;
37
+ if (!event || typeof event !== "object") {
38
+ throw new node_1.default.Error(node_1.default.Error.INVALID_QUERY, `${name} needs an "event" object describing the alarm`);
39
+ }
40
+ const tenant = request.master
41
+ ? await tenantById(tenantId)
42
+ : await (0, index_js_1.tenantForSecret)(typeof secret === "string" ? secret : "");
43
+ if (!tenant) {
44
+ // Same answer for "wrong secret" and "no such tenant": a caller
45
+ // guessing secrets learns nothing from the difference.
46
+ throw new node_1.default.Error(node_1.default.Error.OPERATION_FORBIDDEN, `${name}: unknown tenant or invalid secret`);
47
+ }
48
+ const result = await (0, index_js_1.createAlarmTicket)(tenant, event, params ?? {});
49
+ console.log(`[${name}] ${result.created ? "created" : "reused"} ticket ` +
50
+ `${result.ticketId} for ${result.origin}` +
51
+ (result.equipmentId ? ` (machine ${result.equipmentId})` : ""));
52
+ return result;
53
+ },
54
+ // No `requireUser`: the callers are machines, authenticated above.
55
+ {});
56
+ }
57
+ async function tenantById(tenantId) {
58
+ if (!tenantId)
59
+ return undefined;
60
+ return ((await new node_1.default.Query(index_js_2.Tenant)
61
+ .equalTo("objectId", tenantId)
62
+ .first({ useMasterKey: true })) ?? undefined);
63
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Mail the external contacts of a maintenance notification rule.
3
+ *
4
+ * **Why this lives here.** The scheduler evaluates the plans' notification
5
+ * rules (`OD3_Service_Schedule.notifications`), but it has no mail transport —
6
+ * that belongs to this server, which owns `Core_Email` and the SMTP config.
7
+ * Users are reached through `OD3_Notification` (the hook there sends mail and
8
+ * web push); contacts have no account, so they are reached directly.
9
+ *
10
+ * **Master key only.** A browser session must not be able to mail arbitrary
11
+ * contacts with arbitrary text.
12
+ *
13
+ * Params: `contactIds`, `subject`, `body`, plus `scheduleId` / `ruleId` /
14
+ * `occurrence` for the log line. Returns `{ sent, skipped, withoutEmail }`.
15
+ */
16
+ export declare function init(name: string): Promise<void>;
@@ -0,0 +1,82 @@
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/notifications/index.js");
9
+ const index_js_2 = require("../types/index.js");
10
+ /** How far back a duplicate is looked for (see `alreadySent`). */
11
+ const DEDUP_WINDOW_DAYS = 30;
12
+ /**
13
+ * Mail the external contacts of a maintenance notification rule.
14
+ *
15
+ * **Why this lives here.** The scheduler evaluates the plans' notification
16
+ * rules (`OD3_Service_Schedule.notifications`), but it has no mail transport —
17
+ * that belongs to this server, which owns `Core_Email` and the SMTP config.
18
+ * Users are reached through `OD3_Notification` (the hook there sends mail and
19
+ * web push); contacts have no account, so they are reached directly.
20
+ *
21
+ * **Master key only.** A browser session must not be able to mail arbitrary
22
+ * contacts with arbitrary text.
23
+ *
24
+ * Params: `contactIds`, `subject`, `body`, plus `scheduleId` / `ruleId` /
25
+ * `occurrence` for the log line. Returns `{ sent, skipped, withoutEmail }`.
26
+ */
27
+ async function init(name) {
28
+ node_1.default.Cloud.define(name, async (request) => {
29
+ if (!request.master) {
30
+ throw new node_1.default.Error(node_1.default.Error.OPERATION_FORBIDDEN, `${name} requires the master key`);
31
+ }
32
+ const { contactIds, subject, body, scheduleId, ruleId, occurrence } = request.params;
33
+ if (!contactIds?.length || !subject || !body) {
34
+ throw new node_1.default.Error(node_1.default.Error.INVALID_QUERY, `${name} needs contactIds, subject and body`);
35
+ }
36
+ const contacts = await new node_1.default.Query(index_js_2.Contact)
37
+ .containedIn("objectId", contactIds)
38
+ .find({ useMasterKey: true });
39
+ let sent = 0;
40
+ let skipped = 0;
41
+ let withoutEmail = 0;
42
+ for (const contact of contacts) {
43
+ const to = contact.get("email");
44
+ if (!to) {
45
+ withoutEmail++;
46
+ continue;
47
+ }
48
+ if (await alreadySent(to, body)) {
49
+ skipped++;
50
+ continue;
51
+ }
52
+ await (0, index_js_1.sendSimpleEmail)(to, subject, body);
53
+ sent++;
54
+ }
55
+ console.log(`[${name}] plan ${scheduleId} rule ${ruleId} occurrence ${occurrence}: ` +
56
+ `sent ${sent}, skipped ${skipped}, without email ${withoutEmail}`);
57
+ return { sent, skipped, withoutEmail };
58
+ },
59
+ // No `requireUser`: this is a machine-to-machine call from the scheduler.
60
+ {});
61
+ }
62
+ /**
63
+ * Whether this exact message already went to this address recently.
64
+ *
65
+ * The idempotency key is the **message body**, because it already identifies
66
+ * the occurrence (it carries plan, description and the due date) and because
67
+ * `Core_Email` has no free column to put a marker in. That makes a restart
68
+ * inside the same tick harmless, while a genuinely different occurrence — a
69
+ * different date in the text — still goes out.
70
+ */
71
+ async function alreadySent(to, body) {
72
+ const since = new Date(Date.now() - DEDUP_WINDOW_DAYS * 24 * 3600 * 1000);
73
+ // Dot access into the `payload` Object column: Mongo resolves it, but the
74
+ // generated class type only knows top-level columns — hence the untyped
75
+ // query handle.
76
+ const query = new node_1.default.Query(index_js_2.Core_Email);
77
+ query.equalTo("payload.to", to);
78
+ query.equalTo("payload.text", body);
79
+ query.greaterThan("createdAt", since);
80
+ const existing = await query.first({ useMasterKey: true });
81
+ return !!existing;
82
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Push maintenance plans into the tenant's Outlook calendar, on request.
3
+ *
4
+ * Normally nobody needs this: saving a plan syncs it (the `Service_Schedule`
5
+ * afterSave hook). It exists for the plans that already existed when a tenant
6
+ * switched the calendar connection on — they have never been written to the
7
+ * calendar, and nothing about them is going to change just to trigger it.
8
+ *
9
+ * **Authorization is the plan's own ACL**: the plans are loaded with the
10
+ * caller's session token, so this can only sync what the caller may read. A
11
+ * plan they cannot see comes back as "not found", like everywhere else.
12
+ *
13
+ * Params: `scheduleIds`. Returns `{ synced, failed }`.
14
+ */
15
+ export declare function init(name: string): Promise<void>;
@@ -0,0 +1,60 @@
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/openservice/serviceSchedules/calendarSync/index.js");
9
+ const index_js_2 = require("../types/index.js");
10
+ /** No more than this many plans per call — each one is a Graph round trip. */
11
+ const MAX_PLANS = 50;
12
+ /**
13
+ * Push maintenance plans into the tenant's Outlook calendar, on request.
14
+ *
15
+ * Normally nobody needs this: saving a plan syncs it (the `Service_Schedule`
16
+ * afterSave hook). It exists for the plans that already existed when a tenant
17
+ * switched the calendar connection on — they have never been written to the
18
+ * calendar, and nothing about them is going to change just to trigger it.
19
+ *
20
+ * **Authorization is the plan's own ACL**: the plans are loaded with the
21
+ * caller's session token, so this can only sync what the caller may read. A
22
+ * plan they cannot see comes back as "not found", like everywhere else.
23
+ *
24
+ * Params: `scheduleIds`. Returns `{ synced, failed }`.
25
+ */
26
+ async function init(name) {
27
+ node_1.default.Cloud.define(name, async (request) => {
28
+ const { scheduleIds } = request.params;
29
+ if (!Array.isArray(scheduleIds) || scheduleIds.length === 0) {
30
+ throw new node_1.default.Error(node_1.default.Error.INVALID_QUERY, `${name} needs scheduleIds`);
31
+ }
32
+ if (scheduleIds.length > MAX_PLANS) {
33
+ throw new node_1.default.Error(node_1.default.Error.INVALID_QUERY, `${name} accepts at most ${MAX_PLANS} plans per call`);
34
+ }
35
+ const plans = await new node_1.default.Query(index_js_2.Service_Schedule)
36
+ .containedIn("objectId", scheduleIds)
37
+ .include("equipment")
38
+ .limit(MAX_PLANS)
39
+ .find({ sessionToken: request.user?.getSessionToken() ?? undefined });
40
+ let synced = 0;
41
+ const failed = [];
42
+ for (const plan of plans) {
43
+ try {
44
+ // Master key from here on: writing the calendar and the meta entry is
45
+ // the server's business, not something the caller needs rights for.
46
+ await (0, index_js_1.syncPlanToCalendar)(plan);
47
+ synced++;
48
+ }
49
+ catch (error) {
50
+ console.error(`[${name}] Plan ${plan.id} failed:`, error);
51
+ failed.push(plan.id);
52
+ }
53
+ }
54
+ return {
55
+ synced,
56
+ failed,
57
+ notFound: scheduleIds.filter((id) => !plans.some((plan) => plan.id === id)),
58
+ };
59
+ }, { requireUser: true });
60
+ }
@@ -6,27 +6,71 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.init = init;
7
7
  const node_1 = __importDefault(require("parse/node"));
8
8
  const index_js_1 = require("../features/schema/index.js");
9
- const index_js_2 = require("../types/index.js");
9
+ const Permissions_js_1 = require("../features/permissions/types/Permissions.js");
10
+ const index_js_2 = require("../features/permissions/index.js");
11
+ const index_js_3 = require("../features/openservice/serviceSchedules/acl/index.js");
12
+ const index_js_4 = require("../features/openservice/serviceSchedules/calendarSync/index.js");
13
+ const index_js_5 = require("../types/index.js");
10
14
  async function init() {
11
- (0, index_js_1.beforeSaveHook)(index_js_2.Service_Schedule, async (request) => {
15
+ (0, index_js_1.beforeSaveHook)(index_js_5.Service_Schedule, async (request) => {
12
16
  const { object, original, user } = request;
13
17
  await (0, index_js_1.defaultHandler)(request);
14
- // `allowCustomACL` keeps an ACL set by the client instead of resetting it on
15
- // every save. The frontend uses it for the per-record write lock ("Rechte
16
- // nur für Ersteller", `openservice-schedule/utils/acl.ts`): the creator gets
17
- // an explicit user grant, everyone else keeps read only. The handler still
18
- // re-adds `od-admin` / `od-tenant-admin-<tenant>` write and
19
- // `od-tenant-user-<tenant>` read afterwards, so a lock can never lock
20
- // administrators out.
21
- await (0, index_js_1.defaultAclHandler)(request, { allowCustomACL: true });
22
- // TODO
18
+ // The creator, recorded once and never rewritten: it is who the plan
19
+ // belongs to, and `defaultAclHandler` turns it into a read+write grant.
20
+ if (!original && user && !object.get("user")) {
21
+ object.set("user", user);
22
+ }
23
+ // `denyTenantUserRead` is what makes a plan private. Without it every user
24
+ // of the tenant reads every plan, which is the opposite of what assigning
25
+ // roles and users to a plan is supposed to mean.
26
+ //
27
+ // `allowCustomACL` keeps an ACL the client set instead of resetting it on
28
+ // every save — the assignment sync in `afterSave` depends on its ACL
29
+ // surviving the next save. The handler still re-adds `od-admin` /
30
+ // `od-tenant-admin-<tenant>`, so a plan can never lock administrators out.
31
+ await (0, index_js_1.defaultAclHandler)(request, {
32
+ allowCustomACL: true,
33
+ denyTenantUserRead: true,
34
+ });
23
35
  });
24
- (0, index_js_1.afterSaveHook)(index_js_2.Service_Schedule, async (request) => {
25
- const { object, original, user } = request;
26
- // TODO
36
+ (0, index_js_1.afterSaveHook)(index_js_5.Service_Schedule, async (request) => {
37
+ const { object, original, context } = request;
38
+ // Our own ACL write — the assignments already match, nothing to redo.
39
+ if (context?.[index_js_3.ASSIGNMENT_ACL_CONTEXT_FLAG])
40
+ return;
41
+ // Assignments are relations: only readable now that the save applied them.
42
+ await (0, index_js_3.syncAssignmentAcl)(object, {
43
+ roleRelations: ["assignedRoles", "escalationRoles"],
44
+ userRelations: ["assignedUsers"],
45
+ }).catch((error) => {
46
+ console.error(`[ServiceScheduleAcl] Plan ${object.id}: could not sync ACL from assignments:`, error);
47
+ });
48
+ // A change that came *from* the calendar must not be written back to it:
49
+ // that would create a new version there, which the delta feed reports as a
50
+ // foreign change, which we would import again — an endless echo. The
51
+ // poller marks its own writes with this flag.
52
+ if (context?.[index_js_4.CALENDAR_SYNC_CONTEXT_FLAG])
53
+ return;
54
+ // Seed data belongs to a demo tenant, not in anybody's calendar.
55
+ if (object.get("seed"))
56
+ return;
57
+ if (!original || calendarRelevantChange(object, original)) {
58
+ // Deliberately not awaited: the calendar is a downstream system, and a
59
+ // Graph round trip (token + write) would otherwise be part of every save
60
+ // the user waits for. Failures are logged, and the next relevant save
61
+ // retries — as does the delta poller from the other side.
62
+ (0, index_js_4.syncPlanToCalendar)(object).catch((error) => {
63
+ console.error(`[ServiceCalendarSync] Plan ${object.id} could not be synced:`, error);
64
+ });
65
+ }
27
66
  });
28
- (0, index_js_1.beforeDeleteHook)(index_js_2.Service_Schedule, async (request) => {
67
+ (0, index_js_1.beforeDeleteHook)(index_js_5.Service_Schedule, async (request) => {
29
68
  const { object } = request;
69
+ // Parse has no delete right of its own — an ACL `write` grant covers update
70
+ // *and* delete, so anyone allowed to edit a plan could otherwise remove it.
71
+ // Hiding the button in the UI does not change that; this is what makes the
72
+ // capability real.
73
+ await (0, index_js_2.requirePermission)(request, Permissions_js_1.Permissions.SERVICE.schedule_delete, "delete maintenance schedule");
30
74
  // A protocol points at the plan it documents. Destroying the plan leaves
31
75
  // that pointer dangling and the protocol — the record that the work
32
76
  // happened — unreachable. Retiring a plan is done by setting `deletedAt`
@@ -35,7 +79,7 @@ async function init() {
35
79
  //
36
80
  // A plan without any execution carries no history, so removing it stays
37
81
  // allowed — that keeps mistakenly created plans cleanable.
38
- const executions = await new node_1.default.Query(index_js_2.Service_Schedule_Execution)
82
+ const executions = await new node_1.default.Query(index_js_5.Service_Schedule_Execution)
39
83
  .equalTo("schedule", object)
40
84
  .count({ useMasterKey: true });
41
85
  if (executions > 0) {
@@ -43,3 +87,32 @@ async function init() {
43
87
  }
44
88
  });
45
89
  }
90
+ /**
91
+ * Whether a saved plan changed anything the calendar shows.
92
+ *
93
+ * Plans are saved for many reasons that mean nothing to Outlook (an execution
94
+ * counter, an ACL, a tag). Only these fields reach the appointment — and
95
+ * `active` / `deletedAt` because retiring a plan removes it from the calendar.
96
+ */
97
+ function calendarRelevantChange(object, original) {
98
+ if (object.get("title") !== original.get("title") ||
99
+ object.get("active") !== original.get("active") ||
100
+ dateValue(object.get("deletedAt")) !== dateValue(original.get("deletedAt"))) {
101
+ return true;
102
+ }
103
+ const equipmentChanged = object.get("equipment")?.id !==
104
+ original.get("equipment")?.id;
105
+ if (equipmentChanged)
106
+ return true;
107
+ // JSON columns: compared by value, because a save always hands us new object
108
+ // identities even when nothing inside them moved.
109
+ return (!sameJson(object.get("cadence"), original.get("cadence")) ||
110
+ !sameJson(object.get("notifications"), original.get("notifications")) ||
111
+ !sameJson(object.get("steps"), original.get("steps")));
112
+ }
113
+ function dateValue(value) {
114
+ return value ? value.getTime() : undefined;
115
+ }
116
+ function sameJson(a, b) {
117
+ return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
118
+ }
@@ -2,16 +2,37 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.init = init;
4
4
  const index_js_1 = require("../features/schema/index.js");
5
- const index_js_2 = require("../types/index.js");
5
+ const index_js_2 = require("../features/openservice/serviceSchedules/acl/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.Service_Schedule_Template, async (request) => {
8
+ (0, index_js_1.beforeSaveHook)(index_js_3.Service_Schedule_Template, async (request) => {
8
9
  const { object, original, user } = request;
9
10
  await (0, index_js_1.defaultHandler)(request);
10
- await (0, index_js_1.defaultAclHandler)(request);
11
- // TODO
11
+ // The creator, recorded once and never rewritten — a template stays visible
12
+ // to whoever built it, whatever happens to its assigned roles later.
13
+ if (!original && user && !object.get("user")) {
14
+ object.set("user", user);
15
+ }
16
+ // A template is private like a plan: its creator, the roles it is assigned
17
+ // to, and administrators. Without `denyTenantUserRead` the whole tenant
18
+ // reads every template and assigning roles would mean nothing.
19
+ await (0, index_js_1.defaultAclHandler)(request, {
20
+ allowCustomACL: true,
21
+ denyTenantUserRead: true,
22
+ });
12
23
  });
13
- (0, index_js_1.afterSaveHook)(index_js_2.Service_Schedule_Template, async (request) => {
14
- const { object, original, user } = request;
15
- // TODO
24
+ (0, index_js_1.afterSaveHook)(index_js_3.Service_Schedule_Template, async (request) => {
25
+ const { object, context } = request;
26
+ // Our own ACL write — the assignments already match.
27
+ if (context?.[index_js_2.ASSIGNMENT_ACL_CONTEXT_FLAG])
28
+ return;
29
+ // `assignedRoles` is a relation, readable only after the save applied it.
30
+ // Templates carry no `assignedUsers`: they are shared by role, and the
31
+ // creator keeps their own grant.
32
+ await (0, index_js_2.syncAssignmentAcl)(object, {
33
+ roleRelations: ["assignedRoles"],
34
+ }).catch((error) => {
35
+ console.error(`[ServiceScheduleAcl] Template ${object.id}: could not sync ACL from assignments:`, error);
36
+ });
16
37
  });
17
38
  }
@@ -43,7 +43,7 @@ const index_js_1 = require("../features/schema/index.js");
43
43
  const appLinks_js_1 = require("../helper/appLinks.js");
44
44
  const getUserLanguage_js_1 = require("../helper/getUserLanguage.js");
45
45
  const index_js_2 = require("../types/index.js");
46
- const applyRuleSet_js_1 = require("../features/ruleset/applyRuleSet.js");
46
+ const ruleset_core_1 = require("@opendash/ruleset-core");
47
47
  async function init() {
48
48
  // Soft delete: a ticket is referenced by chat messages, meta entries,
49
49
  // notifications and child tickets, so the frontend archives instead of
@@ -266,7 +266,7 @@ async function notifyWatchers(ticket, notified, assignerName) {
266
266
  for (const rule of rules) {
267
267
  if (rule.function !== "notify" || !rule.ruleset)
268
268
  continue;
269
- if (!(0, applyRuleSet_js_1.applyRuleSet)(rule.ruleset, getValue))
269
+ if (!(0, ruleset_core_1.applyRuleSet)(rule.ruleset, getValue))
270
270
  continue;
271
271
  const assignees = Array.isArray(rule.params?.assignees)
272
272
  ? rule.params.assignees
@@ -1,5 +1,6 @@
1
1
  import Parse from "parse/node";
2
2
  import type { _Role } from "./_Role";
3
+ import type { _User } from "./_User";
3
4
  import type { Service_Equipment } from "./Service_Equipment";
4
5
  import type { Tenant } from "./Tenant";
5
6
  export interface Service_ScheduleAttributes {
@@ -10,8 +11,10 @@ export interface Service_ScheduleAttributes {
10
11
  active: boolean;
11
12
  asset?: any | undefined;
12
13
  assignedRoles: Parse.Relation<Service_Schedule, _Role>;
14
+ assignedUsers: Parse.Relation<Service_Schedule, _User>;
13
15
  cadence: any;
14
16
  cadenceExceptions?: any[] | undefined;
17
+ color?: string | undefined;
15
18
  coversTasks: any[];
16
19
  deletedAt?: Date | undefined;
17
20
  equipment?: Service_Equipment | undefined;
@@ -19,12 +22,14 @@ export interface Service_ScheduleAttributes {
19
22
  escalationRoles: Parse.Relation<Service_Schedule, _Role>;
20
23
  graceDays?: number | undefined;
21
24
  leadDays?: number | undefined;
25
+ notifications: any[];
22
26
  pausedUntil?: Date | undefined;
23
27
  seed?: boolean | undefined;
24
28
  serviceProviders?: any[] | undefined;
25
29
  steps: any[];
26
30
  tenant: Tenant;
27
31
  title: string;
32
+ user?: _User | undefined;
28
33
  }
29
34
  export declare class Service_Schedule extends Parse.Object<Service_ScheduleAttributes> {
30
35
  static className: string;
@@ -34,10 +39,13 @@ export declare class Service_Schedule extends Parse.Object<Service_ScheduleAttri
34
39
  get asset(): any | undefined;
35
40
  set asset(value: any | undefined);
36
41
  get assignedRoles(): Parse.Relation<Service_Schedule, _Role>;
42
+ get assignedUsers(): Parse.Relation<Service_Schedule, _User>;
37
43
  get cadence(): any;
38
44
  set cadence(value: any);
39
45
  get cadenceExceptions(): any[] | undefined;
40
46
  set cadenceExceptions(value: any[] | undefined);
47
+ get color(): string | undefined;
48
+ set color(value: string | undefined);
41
49
  get coversTasks(): any[];
42
50
  set coversTasks(value: any[]);
43
51
  get deletedAt(): Date | undefined;
@@ -51,6 +59,8 @@ export declare class Service_Schedule extends Parse.Object<Service_ScheduleAttri
51
59
  set graceDays(value: number | undefined);
52
60
  get leadDays(): number | undefined;
53
61
  set leadDays(value: number | undefined);
62
+ get notifications(): any[];
63
+ set notifications(value: any[]);
54
64
  get pausedUntil(): Date | undefined;
55
65
  set pausedUntil(value: Date | undefined);
56
66
  get seed(): boolean | undefined;
@@ -63,4 +73,6 @@ export declare class Service_Schedule extends Parse.Object<Service_ScheduleAttri
63
73
  set tenant(value: Tenant);
64
74
  get title(): string;
65
75
  set title(value: string);
76
+ get user(): _User | undefined;
77
+ set user(value: _User | undefined);
66
78
  }
@@ -25,6 +25,9 @@ class Service_Schedule extends node_1.default.Object {
25
25
  get assignedRoles() {
26
26
  return super.relation("assignedRoles");
27
27
  }
28
+ get assignedUsers() {
29
+ return super.relation("assignedUsers");
30
+ }
28
31
  get cadence() {
29
32
  return super.get("cadence");
30
33
  }
@@ -37,6 +40,12 @@ class Service_Schedule extends node_1.default.Object {
37
40
  set cadenceExceptions(value) {
38
41
  super.set("cadenceExceptions", value);
39
42
  }
43
+ get color() {
44
+ return super.get("color");
45
+ }
46
+ set color(value) {
47
+ super.set("color", value);
48
+ }
40
49
  get coversTasks() {
41
50
  return super.get("coversTasks");
42
51
  }
@@ -76,6 +85,12 @@ class Service_Schedule extends node_1.default.Object {
76
85
  set leadDays(value) {
77
86
  super.set("leadDays", value);
78
87
  }
88
+ get notifications() {
89
+ return super.get("notifications");
90
+ }
91
+ set notifications(value) {
92
+ super.set("notifications", value);
93
+ }
79
94
  get pausedUntil() {
80
95
  return super.get("pausedUntil");
81
96
  }
@@ -112,6 +127,12 @@ class Service_Schedule extends node_1.default.Object {
112
127
  set title(value) {
113
128
  super.set("title", value);
114
129
  }
130
+ get user() {
131
+ return super.get("user");
132
+ }
133
+ set user(value) {
134
+ super.set("user", value);
135
+ }
115
136
  }
116
137
  exports.Service_Schedule = Service_Schedule;
117
138
  node_1.default.Object.registerSubclass("OD3_Service_Schedule", Service_Schedule);
@@ -12,9 +12,10 @@ export interface Service_Schedule_ExecutionAttributes {
12
12
  coverageOrigin?: any | undefined;
13
13
  performedBy?: _User | undefined;
14
14
  schedule: Service_Schedule;
15
- signature?: any | undefined;
15
+ signature?: string | undefined;
16
16
  stepResults?: any[] | undefined;
17
17
  tenant: Tenant;
18
+ user?: _User | undefined;
18
19
  }
19
20
  export declare class Service_Schedule_Execution extends Parse.Object<Service_Schedule_ExecutionAttributes> {
20
21
  static className: string;
@@ -29,10 +30,12 @@ export declare class Service_Schedule_Execution extends Parse.Object<Service_Sch
29
30
  set performedBy(value: _User | undefined);
30
31
  get schedule(): Service_Schedule;
31
32
  set schedule(value: Service_Schedule);
32
- get signature(): any | undefined;
33
- set signature(value: any | undefined);
33
+ get signature(): string | undefined;
34
+ set signature(value: string | undefined);
34
35
  get stepResults(): any[] | undefined;
35
36
  set stepResults(value: any[] | undefined);
36
37
  get tenant(): Tenant;
37
38
  set tenant(value: Tenant);
39
+ get user(): _User | undefined;
40
+ set user(value: _User | undefined);
38
41
  }
@@ -58,6 +58,12 @@ class Service_Schedule_Execution extends node_1.default.Object {
58
58
  set tenant(value) {
59
59
  super.set("tenant", value);
60
60
  }
61
+ get user() {
62
+ return super.get("user");
63
+ }
64
+ set user(value) {
65
+ super.set("user", value);
66
+ }
61
67
  }
62
68
  exports.Service_Schedule_Execution = Service_Schedule_Execution;
63
69
  node_1.default.Object.registerSubclass("OD3_Service_Schedule_Execution", Service_Schedule_Execution);
@@ -1,6 +1,7 @@
1
1
  import Parse from "parse/node";
2
2
  import type { _Role } from "./_Role";
3
3
  import type { Tenant } from "./Tenant";
4
+ import type { _User } from "./_User";
4
5
  export interface Service_Schedule_TemplateAttributes {
5
6
  id: string;
6
7
  objectId: string;
@@ -10,10 +11,12 @@ export interface Service_Schedule_TemplateAttributes {
10
11
  cadence: any;
11
12
  deletedAt?: Date | undefined;
12
13
  description?: string | undefined;
14
+ notifications: any[];
13
15
  serviceProviders: any[];
14
16
  steps: any[];
15
17
  tenant: Tenant;
16
18
  title: string;
19
+ user?: _User | undefined;
17
20
  }
18
21
  export declare class Service_Schedule_Template extends Parse.Object<Service_Schedule_TemplateAttributes> {
19
22
  static className: string;
@@ -25,6 +28,8 @@ export declare class Service_Schedule_Template extends Parse.Object<Service_Sche
25
28
  set deletedAt(value: Date | undefined);
26
29
  get description(): string | undefined;
27
30
  set description(value: string | undefined);
31
+ get notifications(): any[];
32
+ set notifications(value: any[]);
28
33
  get serviceProviders(): any[];
29
34
  set serviceProviders(value: any[]);
30
35
  get steps(): any[];
@@ -33,4 +38,6 @@ export declare class Service_Schedule_Template extends Parse.Object<Service_Sche
33
38
  set tenant(value: Tenant);
34
39
  get title(): string;
35
40
  set title(value: string);
41
+ get user(): _User | undefined;
42
+ set user(value: _User | undefined);
36
43
  }