@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,248 @@
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.ServiceCalendarSyncPoller = exports.CALENDAR_SYNC_CONTEXT_FLAG = void 0;
7
+ const crypto_1 = require("crypto");
8
+ const node_1 = __importDefault(require("parse/node"));
9
+ const CalendarManager_js_1 = require("../../../schedules/calendarSync/service/CalendarManager.js");
10
+ const index_js_1 = require("../../../../../types/index.js");
11
+ const eventToPlan_js_1 = require("../functions/eventToPlan.js");
12
+ const serviceCalendarMeta_js_1 = require("./serviceCalendarMeta.js");
13
+ /**
14
+ * Marker on a save that came from the calendar.
15
+ *
16
+ * The outbound side (`syncPlanToCalendar`, called from the `Service_Schedule`
17
+ * afterSave hook) checks it and stays quiet — otherwise every inbound change
18
+ * would be written straight back to Outlook, produce a new `changeKey`, come
19
+ * back through the delta feed as a foreign change, and loop forever. The stored
20
+ * `changeKey` is the second line of defence; this is the first, and unlike v1's
21
+ * `!master` check it does not disable outbound sync for every other
22
+ * master-key writer (the scheduler pausing a plan, a migration, a script).
23
+ */
24
+ exports.CALENDAR_SYNC_CONTEXT_FLAG = "fromCalendarSync";
25
+ /**
26
+ * Pulls Outlook changes into open.SERVICE **v2** maintenance plans.
27
+ *
28
+ * One poller per configured connection, driven by the Graph *delta* feed: the
29
+ * first call reads the whole calendar, each later call only what changed, and
30
+ * the cursor lives in the Config row so a restart resumes instead of
31
+ * re-importing. The v1 poller does the same for `Maintenance_Schedule` from the
32
+ * same connection — the two keep separate cursors (`delta_link` vs
33
+ * `delta_link_v2`) so neither consumes the other's changes.
34
+ */
35
+ class ServiceCalendarSyncPoller {
36
+ timer = null;
37
+ config;
38
+ constructor(config) {
39
+ this.config = config;
40
+ }
41
+ get values() {
42
+ return JSON.parse(this.config.get("value"));
43
+ }
44
+ get manager() {
45
+ const values = this.values;
46
+ return new CalendarManager_js_1.CalendarManager({
47
+ tenantId: values.tenant_id,
48
+ clientID: values.client_id,
49
+ clientSecret: values.client_secret,
50
+ userMail: values.user_email,
51
+ });
52
+ }
53
+ start(intervalMs = 5 * 60 * 1000) {
54
+ const valid = this.manager.isValid;
55
+ if (valid !== true) {
56
+ console.error(`[ServiceCalendarSync] Not started — invalid configuration: ${valid}`);
57
+ return this;
58
+ }
59
+ this.poll().catch(console.error);
60
+ this.timer = setInterval(() => this.poll().catch(console.error), intervalMs);
61
+ return this;
62
+ }
63
+ stop() {
64
+ if (this.timer)
65
+ clearInterval(this.timer);
66
+ this.timer = null;
67
+ }
68
+ async poll() {
69
+ const result = await this.manager.fetchDelta(await this.loadDeltaLink());
70
+ if (!result) {
71
+ console.error("[ServiceCalendarSync] Could not fetch the calendar delta");
72
+ return;
73
+ }
74
+ for (const event of result.events) {
75
+ try {
76
+ await this.processEvent(event);
77
+ }
78
+ catch (error) {
79
+ // One unusable event must not cost us the whole batch — and not the
80
+ // delta cursor either, or it would be retried forever.
81
+ console.error("[ServiceCalendarSync] Event could not be processed:", event.id, error);
82
+ }
83
+ }
84
+ await this.saveDeltaLink(result.deltaLink);
85
+ }
86
+ async processEvent(event) {
87
+ if (event["@removed"]) {
88
+ await this.handleRemoved(event.id);
89
+ return;
90
+ }
91
+ // An "exception" is a single moved or edited occurrence of a series. The
92
+ // plan's own `cadenceExceptions` could hold that, but Graph does not tell
93
+ // us *which* occurrence was moved in a form we can map back reliably — and
94
+ // a wrong exception silently moves a maintenance date. The series master
95
+ // carries the recurrence; exceptions are left to Outlook.
96
+ if (event.type === "exception")
97
+ return;
98
+ const meta = await (0, serviceCalendarMeta_js_1.findMetaForEvent)(event.id);
99
+ if (!meta) {
100
+ // Delta payloads are partial — fetch the whole event before creating.
101
+ const full = await this.manager.fetchEvent(event.id);
102
+ if (full)
103
+ await this.createPlan(full);
104
+ return;
105
+ }
106
+ if (event.changeKey &&
107
+ (0, serviceCalendarMeta_js_1.metaValues)(meta)?.microsoftCalendarChangeKey === event.changeKey) {
108
+ return; // Our own write, echoed back by the delta feed.
109
+ }
110
+ await this.updatePlan(meta, event);
111
+ }
112
+ async createPlan(event) {
113
+ const fields = (0, eventToPlan_js_1.eventToPlanFields)(event);
114
+ if (!fields) {
115
+ console.warn(`[ServiceCalendarSync] Event ${event.id} has no usable start — skipped`);
116
+ return;
117
+ }
118
+ const tenant = this.config.get("tenant");
119
+ const equipment = await this.resolveEquipment(fields.equipmentName, tenant);
120
+ const plan = new index_js_1.Service_Schedule();
121
+ plan.set("title", fields.title);
122
+ plan.set("active", true);
123
+ plan.set("tenant", tenant);
124
+ plan.set("cadence", fields.cadence);
125
+ if (equipment)
126
+ plan.set("equipment", equipment);
127
+ // A plan needs at least one step to be executable. The event's note becomes
128
+ // its description, so what someone wrote in Outlook is not lost — the
129
+ // checklist proper is then built in the plan editor.
130
+ plan.set("steps", [
131
+ {
132
+ id: (0, crypto_1.randomUUID)(),
133
+ order: 0,
134
+ title: fields.title,
135
+ ...(fields.description ? { description: fields.description } : {}),
136
+ required: false,
137
+ },
138
+ ]);
139
+ plan.set("notifications", (0, eventToPlan_js_1.mergeReminderRule)([], fields.notification));
140
+ await plan.save(null, {
141
+ useMasterKey: true,
142
+ context: { [exports.CALENDAR_SYNC_CONTEXT_FLAG]: true },
143
+ });
144
+ await (0, serviceCalendarMeta_js_1.saveMeta)(plan.id, event.id, event.changeKey, tenant, event);
145
+ console.log(`[ServiceCalendarSync] Created plan ${plan.id} from event ${event.id}` +
146
+ (equipment ? ` (machine ${equipment.id})` : " (no machine)"));
147
+ }
148
+ async updatePlan(meta, event) {
149
+ const planId = meta.get("entryObjectId");
150
+ if (!planId)
151
+ return;
152
+ // Delta payloads are partial: without the full event, an unchanged subject
153
+ // would arrive as `undefined` and wipe the plan's title.
154
+ const full = (await this.manager.fetchEvent(event.id)) ?? event;
155
+ const fields = (0, eventToPlan_js_1.eventToPlanFields)(full);
156
+ if (!fields)
157
+ return;
158
+ const plan = await new node_1.default.Query(index_js_1.Service_Schedule)
159
+ .include("equipment")
160
+ .get(planId, { useMasterKey: true });
161
+ plan.set("title", fields.title);
162
+ plan.set("cadence", fields.cadence);
163
+ plan.set("notifications", (0, eventToPlan_js_1.mergeReminderRule)(plan.get("notifications"), fields.notification));
164
+ // The machine only changes if the subject actually names a different one —
165
+ // a subject without the prefix leaves the existing assignment alone rather
166
+ // than falling back to the configured default and moving the plan.
167
+ if (fields.equipmentName) {
168
+ const equipment = await this.findEquipmentByLabel(fields.equipmentName, this.config.get("tenant"));
169
+ if (equipment)
170
+ plan.set("equipment", equipment);
171
+ }
172
+ await plan.save(null, {
173
+ useMasterKey: true,
174
+ context: { [exports.CALENDAR_SYNC_CONTEXT_FLAG]: true },
175
+ });
176
+ if (event.changeKey)
177
+ await (0, serviceCalendarMeta_js_1.updateChangeKey)(meta, event.changeKey, full);
178
+ }
179
+ /**
180
+ * A deleted appointment archives the plan — it never destroys it.
181
+ *
182
+ * Executions document that work happened; deleting the plan they point at
183
+ * would strand that history (the `beforeDelete` hook refuses it, too). The
184
+ * meta entry goes, so the same plan can be linked to a new event later.
185
+ */
186
+ async handleRemoved(eventId) {
187
+ const meta = await (0, serviceCalendarMeta_js_1.findMetaForEvent)(eventId);
188
+ if (!meta)
189
+ return;
190
+ const planId = meta.get("entryObjectId");
191
+ if (planId) {
192
+ try {
193
+ const plan = await new node_1.default.Query(index_js_1.Service_Schedule).get(planId, {
194
+ useMasterKey: true,
195
+ });
196
+ plan.set("active", false);
197
+ plan.set("deletedAt", new Date());
198
+ await plan.save(null, {
199
+ useMasterKey: true,
200
+ context: { [exports.CALENDAR_SYNC_CONTEXT_FLAG]: true },
201
+ });
202
+ }
203
+ catch (error) {
204
+ console.error(`[ServiceCalendarSync] Could not archive plan ${planId} of deleted event ${eventId}:`, error);
205
+ }
206
+ }
207
+ await meta.destroy({ useMasterKey: true });
208
+ }
209
+ /** The machine named in the subject, else the configured fallback. */
210
+ async resolveEquipment(label, tenant) {
211
+ if (label) {
212
+ const found = await this.findEquipmentByLabel(label, tenant);
213
+ if (found)
214
+ return found;
215
+ console.warn(`[ServiceCalendarSync] No machine named "${label}" — falling back to the default`);
216
+ }
217
+ const defaultId = this.values.default_equipment_id;
218
+ if (!defaultId)
219
+ return undefined;
220
+ return ((await new node_1.default.Query(index_js_1.Service_Equipment)
221
+ .equalTo("objectId", defaultId)
222
+ .first({ useMasterKey: true })) ?? undefined);
223
+ }
224
+ async findEquipmentByLabel(label, tenant) {
225
+ const query = new node_1.default.Query(index_js_1.Service_Equipment).equalTo("label", label);
226
+ if (tenant)
227
+ query.equalTo("tenant", tenant);
228
+ query.notEqualTo("deleted", true);
229
+ return (await query.first({ useMasterKey: true })) ?? undefined;
230
+ }
231
+ /**
232
+ * The cursor is re-read from the Config row on every tick, and written back
233
+ * after it: the row is also the admin form's storage, so it can change
234
+ * underneath us.
235
+ */
236
+ async loadDeltaLink() {
237
+ this.config = await this.config.fetch({ useMasterKey: true });
238
+ return this.values.delta_link_v2;
239
+ }
240
+ async saveDeltaLink(link) {
241
+ this.config = await this.config.fetch({ useMasterKey: true });
242
+ const values = this.values;
243
+ values.delta_link_v2 = link;
244
+ this.config.set("value", JSON.stringify(values));
245
+ await this.config.save(null, { useMasterKey: true });
246
+ }
247
+ }
248
+ exports.ServiceCalendarSyncPoller = ServiceCalendarSyncPoller;
@@ -0,0 +1,55 @@
1
+ import type { MetaConfig } from "../../../../../types/custom/MetaField.js";
2
+ import { Meta_Entry, Tenant } from "../../../../../types/index.js";
3
+ import type { GraphEventResponse } from "../../../schedules/calendarSync/types/Event.js";
4
+ /**
5
+ * Which calendar event belongs to which v2 maintenance plan.
6
+ *
7
+ * Same mechanism as v1 (a `Meta_Entry` per plan), but a **separate context**:
8
+ * one Outlook connection serves both generations, and a v1 entry pointing at a
9
+ * `Maintenance_Schedule` must never be mistaken for a v2 one pointing at an
10
+ * `OD3_Service_Schedule`. The stored `changeKey` is what keeps the two sides
11
+ * from echoing each other — see `ServiceCalendarSyncPoller`.
12
+ */
13
+ export declare const SERVICE_CALENDAR_SYNC_CONTEXT: string;
14
+ /**
15
+ * The last known state of the event, as far as the sync needs it.
16
+ *
17
+ * Kept so an outbound update can preserve what the user set in Outlook and this
18
+ * side has no field for — the event's times above all: a plan knows a due
19
+ * *date*, and writing that back must not turn someone's 9:00 appointment into
20
+ * an all-day block.
21
+ */
22
+ export interface ServiceCalendarEventSnapshot {
23
+ id: string;
24
+ subject?: string;
25
+ start: {
26
+ dateTime: string;
27
+ timeZone: string;
28
+ };
29
+ end: {
30
+ dateTime: string;
31
+ timeZone: string;
32
+ };
33
+ isAllDay?: boolean;
34
+ isCancelled?: boolean;
35
+ recurrence?: GraphEventResponse["recurrence"];
36
+ }
37
+ export interface ServiceCalendarMetaValues {
38
+ microsoftCalendarEventId: string;
39
+ microsoftCalendarChangeKey?: string;
40
+ microsoftCalendarEvent?: ServiceCalendarEventSnapshot;
41
+ }
42
+ /** Shown by the meta plugin when the entry is inspected in the frontend. */
43
+ export declare const ServiceCalendarMetaConfig: MetaConfig;
44
+ export declare function findMetaForPlan(scheduleId: string): Promise<Meta_Entry | null>;
45
+ export declare function findMetaForEvent(eventId: string): Promise<Meta_Entry | null>;
46
+ export declare function saveMeta(scheduleId: string, eventId: string, changeKey?: string, tenant?: Tenant, event?: GraphEventResponse): Promise<Meta_Entry>;
47
+ /**
48
+ * Remember the `changeKey` of the version we just wrote.
49
+ *
50
+ * This is the loop breaker: the delta feed will report our own write back to
51
+ * us, and the poller recognises it by this key. Losing it means one harmless
52
+ * extra round trip; writing the wrong one means an endless echo.
53
+ */
54
+ export declare function updateChangeKey(entry: Meta_Entry, changeKey: string, event?: GraphEventResponse): Promise<void>;
55
+ export declare function metaValues(entry: Meta_Entry): ServiceCalendarMetaValues | undefined;
@@ -0,0 +1,109 @@
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.ServiceCalendarMetaConfig = exports.SERVICE_CALENDAR_SYNC_CONTEXT = void 0;
7
+ exports.findMetaForPlan = findMetaForPlan;
8
+ exports.findMetaForEvent = findMetaForEvent;
9
+ exports.saveMeta = saveMeta;
10
+ exports.updateChangeKey = updateChangeKey;
11
+ exports.metaValues = metaValues;
12
+ const node_1 = __importDefault(require("parse/node"));
13
+ const index_js_1 = require("../../../../../types/index.js");
14
+ /**
15
+ * Which calendar event belongs to which v2 maintenance plan.
16
+ *
17
+ * Same mechanism as v1 (a `Meta_Entry` per plan), but a **separate context**:
18
+ * one Outlook connection serves both generations, and a v1 entry pointing at a
19
+ * `Maintenance_Schedule` must never be mistaken for a v2 one pointing at an
20
+ * `OD3_Service_Schedule`. The stored `changeKey` is what keeps the two sides
21
+ * from echoing each other — see `ServiceCalendarSyncPoller`.
22
+ */
23
+ exports.SERVICE_CALENDAR_SYNC_CONTEXT = JSON.stringify([
24
+ "service",
25
+ "schedule",
26
+ "sync",
27
+ "microsoft",
28
+ ]);
29
+ /** Shown by the meta plugin when the entry is inspected in the frontend. */
30
+ exports.ServiceCalendarMetaConfig = {
31
+ fields: [
32
+ {
33
+ name: "microsoftCalendarEventId",
34
+ type: "string",
35
+ label: "Microsoft Calendar Event ID",
36
+ },
37
+ {
38
+ name: "microsoftCalendarChangeKey",
39
+ type: "string",
40
+ label: "Microsoft Calendar Change Key",
41
+ },
42
+ ],
43
+ };
44
+ function toSnapshot(event) {
45
+ return {
46
+ id: event.id,
47
+ subject: event.subject,
48
+ start: event.start,
49
+ end: event.end,
50
+ isAllDay: event.isAllDay,
51
+ isCancelled: event.isCancelled,
52
+ recurrence: event.recurrence ?? undefined,
53
+ };
54
+ }
55
+ async function findMetaForPlan(scheduleId) {
56
+ return ((await new node_1.default.Query(index_js_1.Meta_Entry)
57
+ .equalTo("context", exports.SERVICE_CALENDAR_SYNC_CONTEXT)
58
+ .equalTo("entryClassName", index_js_1.Service_Schedule.className)
59
+ .equalTo("entryObjectId", scheduleId)
60
+ .first({ useMasterKey: true })) ?? null);
61
+ }
62
+ async function findMetaForEvent(eventId) {
63
+ return ((await new node_1.default.Query(index_js_1.Meta_Entry)
64
+ .equalTo("context", exports.SERVICE_CALENDAR_SYNC_CONTEXT)
65
+ .equalTo("entryClassName", index_js_1.Service_Schedule.className)
66
+ // Dot access into the `values` Object column — Mongo resolves it, the
67
+ // generated type only knows top-level columns.
68
+ // @ts-expect-error
69
+ .equalTo("values.microsoftCalendarEventId", eventId)
70
+ .first({ useMasterKey: true })) ?? null);
71
+ }
72
+ async function saveMeta(scheduleId, eventId, changeKey, tenant, event) {
73
+ let entry = await findMetaForPlan(scheduleId);
74
+ if (!entry) {
75
+ entry = new index_js_1.Meta_Entry();
76
+ entry.set("context", exports.SERVICE_CALENDAR_SYNC_CONTEXT);
77
+ entry.set("entryClassName", index_js_1.Service_Schedule.className);
78
+ entry.set("entryObjectId", scheduleId);
79
+ entry.set("tenant", tenant);
80
+ entry.set("config", {});
81
+ }
82
+ const values = {
83
+ microsoftCalendarEventId: eventId,
84
+ };
85
+ if (changeKey)
86
+ values.microsoftCalendarChangeKey = changeKey;
87
+ if (event)
88
+ values.microsoftCalendarEvent = toSnapshot(event);
89
+ entry.set("values", values);
90
+ return entry.save(null, { useMasterKey: true });
91
+ }
92
+ /**
93
+ * Remember the `changeKey` of the version we just wrote.
94
+ *
95
+ * This is the loop breaker: the delta feed will report our own write back to
96
+ * us, and the poller recognises it by this key. Losing it means one harmless
97
+ * extra round trip; writing the wrong one means an endless echo.
98
+ */
99
+ async function updateChangeKey(entry, changeKey, event) {
100
+ entry.set("values", {
101
+ ...entry.get("values"),
102
+ microsoftCalendarChangeKey: changeKey,
103
+ ...(event ? { microsoftCalendarEvent: toSnapshot(event) } : {}),
104
+ });
105
+ await entry.save(null, { useMasterKey: true });
106
+ }
107
+ function metaValues(entry) {
108
+ return entry.get("values");
109
+ }
@@ -0,0 +1,26 @@
1
+ import { CalendarManager } from "../../../schedules/calendarSync/service/CalendarManager.js";
2
+ import { Service_Schedule, Tenant } from "../../../../../types/index.js";
3
+ import type { ServiceSyncConfigValues } from "../types/ServiceSyncTypes.js";
4
+ /**
5
+ * Write a maintenance plan into the tenant's Outlook calendar.
6
+ *
7
+ * The counterpart of `ServiceCalendarSyncPoller`; called from the
8
+ * `Service_Schedule` afterSave hook. Silent when the tenant has no calendar
9
+ * connection — the sync is opt-in per tenant, and a missing connection is not
10
+ * an error.
11
+ *
12
+ * Four cases, in this order:
13
+ *
14
+ * 1. plan archived or inactive → the appointment is removed; maintenance that
15
+ * no longer happens must not sit in someone's calendar;
16
+ * 2. plan has no date (manual, sensor- or meter-driven) → same, there is
17
+ * nothing to show;
18
+ * 3. plan already linked → the event is updated in place, keeping the time of
19
+ * day someone gave it in Outlook;
20
+ * 4. otherwise → a new event, and a meta entry that links the two.
21
+ */
22
+ export declare function syncPlanToCalendar(plan: Service_Schedule): Promise<void>;
23
+ export declare function connectionForTenant(tenant: Tenant | undefined): Promise<{
24
+ manager: CalendarManager;
25
+ values: ServiceSyncConfigValues;
26
+ } | null>;
@@ -0,0 +1,122 @@
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.syncPlanToCalendar = syncPlanToCalendar;
7
+ exports.connectionForTenant = connectionForTenant;
8
+ const node_1 = __importDefault(require("parse/node"));
9
+ const CalendarManager_js_1 = require("../../../schedules/calendarSync/service/CalendarManager.js");
10
+ const index_js_1 = require("../../../../../types/index.js");
11
+ const outlookRecurrence_js_1 = require("../functions/outlookRecurrence.js");
12
+ const planToEvent_js_1 = require("../functions/planToEvent.js");
13
+ const ServiceSyncTypes_js_1 = require("../types/ServiceSyncTypes.js");
14
+ const serviceCalendarMeta_js_1 = require("./serviceCalendarMeta.js");
15
+ /**
16
+ * Write a maintenance plan into the tenant's Outlook calendar.
17
+ *
18
+ * The counterpart of `ServiceCalendarSyncPoller`; called from the
19
+ * `Service_Schedule` afterSave hook. Silent when the tenant has no calendar
20
+ * connection — the sync is opt-in per tenant, and a missing connection is not
21
+ * an error.
22
+ *
23
+ * Four cases, in this order:
24
+ *
25
+ * 1. plan archived or inactive → the appointment is removed; maintenance that
26
+ * no longer happens must not sit in someone's calendar;
27
+ * 2. plan has no date (manual, sensor- or meter-driven) → same, there is
28
+ * nothing to show;
29
+ * 3. plan already linked → the event is updated in place, keeping the time of
30
+ * day someone gave it in Outlook;
31
+ * 4. otherwise → a new event, and a meta entry that links the two.
32
+ */
33
+ async function syncPlanToCalendar(plan) {
34
+ const connection = await connectionForTenant(plan.get("tenant"));
35
+ if (!connection)
36
+ return;
37
+ // A recurrence Outlook cannot express is left untouched rather than
38
+ // approximated — a wrong maintenance date in a calendar is worse than none.
39
+ const unsupported = (0, outlookRecurrence_js_1.calendarUnsupportedReason)(plan.get("cadence"));
40
+ if (unsupported) {
41
+ console.warn(`[ServiceCalendarSync] Plan ${plan.id} not synced: ${unsupported}`);
42
+ return;
43
+ }
44
+ const meta = await (0, serviceCalendarMeta_js_1.findMetaForPlan)(plan.id);
45
+ const values = meta ? (0, serviceCalendarMeta_js_1.metaValues)(meta) : undefined;
46
+ const eventId = values?.microsoftCalendarEventId;
47
+ const retired = !!plan.get("deletedAt") || plan.get("active") === false;
48
+ const event = retired
49
+ ? undefined
50
+ : (0, planToEvent_js_1.planToEvent)({
51
+ title: plan.get("title"),
52
+ cadence: plan.get("cadence"),
53
+ steps: plan.get("steps"),
54
+ notifications: plan.get("notifications"),
55
+ equipmentName: await equipmentLabel(plan),
56
+ snapshot: values?.microsoftCalendarEvent,
57
+ });
58
+ if (!event) {
59
+ if (meta && eventId) {
60
+ await connection.manager.deleteEvent(eventId);
61
+ await meta.destroy({ useMasterKey: true });
62
+ }
63
+ return;
64
+ }
65
+ if (meta && eventId) {
66
+ const updated = await connection.manager.updateEvent(eventId, event);
67
+ if (updated?.changeKey) {
68
+ await (0, serviceCalendarMeta_js_1.updateChangeKey)(meta, updated.changeKey, updated);
69
+ }
70
+ return;
71
+ }
72
+ const created = await connection.manager.createEvent(event);
73
+ if (!created?.id)
74
+ return;
75
+ await (0, serviceCalendarMeta_js_1.saveMeta)(plan.id, created.id, created.changeKey, plan.get("tenant"), created);
76
+ }
77
+ /** The machine's label, for the subject prefix — `undefined` if it has none. */
78
+ async function equipmentLabel(plan) {
79
+ const equipment = plan.get("equipment");
80
+ if (!equipment)
81
+ return undefined;
82
+ // A pointer straight from a save is unfetched; the label lives on the object.
83
+ if (!equipment.get("label")) {
84
+ try {
85
+ await equipment.fetch({ useMasterKey: true });
86
+ }
87
+ catch {
88
+ return undefined;
89
+ }
90
+ }
91
+ return equipment.get("label") || undefined;
92
+ }
93
+ async function connectionForTenant(tenant) {
94
+ if (!tenant)
95
+ return null;
96
+ const config = await new node_1.default.Query(index_js_1.Config)
97
+ .equalTo("key", ServiceSyncTypes_js_1.OUTLOOK_CONFIG_KEY)
98
+ .equalTo("tenant", tenant)
99
+ .first({ useMasterKey: true });
100
+ if (!config)
101
+ return null;
102
+ let values;
103
+ try {
104
+ values = JSON.parse(config.get("value"));
105
+ }
106
+ catch {
107
+ console.error(`[ServiceCalendarSync] Config ${config.id} does not contain valid JSON`);
108
+ return null;
109
+ }
110
+ const manager = new CalendarManager_js_1.CalendarManager({
111
+ tenantId: values.tenant_id,
112
+ clientID: values.client_id,
113
+ clientSecret: values.client_secret,
114
+ userMail: values.user_email,
115
+ });
116
+ const valid = manager.isValid;
117
+ if (valid !== true) {
118
+ console.warn(`[ServiceCalendarSync] Connection unusable: ${valid}`);
119
+ return null;
120
+ }
121
+ return { manager, values };
122
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Config key of the Outlook integration. Shared with v1 — one calendar
3
+ * connection per tenant serves both generations.
4
+ */
5
+ export declare const OUTLOOK_CONFIG_KEY = "OPENSERVICE_MICROSOFT_OUTLOOK_INTEGRATION";
6
+ /**
7
+ * The stored configuration of one tenant's calendar connection.
8
+ *
9
+ * `default_source_id` is v1's fallback (a `Maintenance_Source`);
10
+ * `default_equipment_id` is its v2 counterpart (`OD3_Service_Equipment`). Both
11
+ * live in the same row because the connection is the same — the poller for each
12
+ * generation reads the key it understands.
13
+ */
14
+ export type ServiceSyncConfigValues = {
15
+ tenant_id: string;
16
+ client_id: string;
17
+ client_secret: string;
18
+ user_email: string;
19
+ /** v1 fallback source. */
20
+ default_source_id?: string;
21
+ /** v2 fallback machine for events that name none. */
22
+ default_equipment_id?: string;
23
+ /** Delta cursor of the v1 poller. */
24
+ delta_link?: string;
25
+ /** Delta cursor of the v2 poller — separate, so the two never steal each other's. */
26
+ delta_link_v2?: string;
27
+ };
28
+ /**
29
+ * The recurrence of a v2 maintenance plan, as stored in
30
+ * `OD3_Service_Schedule.cadence`.
31
+ *
32
+ * Mirrored from `@opendash/plugin-openservice-core` (a browser package); the
33
+ * authoritative definition is the Parse schema. Only `rrule` and `manual` are
34
+ * active in v2.0, and only `rrule` can be represented in a calendar.
35
+ */
36
+ export type ServiceCadence = {
37
+ kind: "rrule" | "sensor" | "meter" | "event" | "manual";
38
+ /** RFC 5545 recurrence rule, without the `RRULE:` prefix. */
39
+ rrule?: string;
40
+ /** ISO timestamp of the series start. */
41
+ dtstart?: string;
42
+ /** ISO timestamp the series ends at (inclusive). */
43
+ until?: string;
44
+ };
45
+ /** A notification rule of a plan (`notifications`), as far as the sync needs it. */
46
+ export type ServiceNotificationRule = {
47
+ id: string;
48
+ trigger: "before" | "due";
49
+ offsetDays?: number;
50
+ assignedRoles?: boolean;
51
+ serviceProviders?: boolean;
52
+ userIds?: string[];
53
+ roleIds?: string[];
54
+ contactIds?: string[];
55
+ };
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OUTLOOK_CONFIG_KEY = void 0;
4
+ /**
5
+ * Config key of the Outlook integration. Shared with v1 — one calendar
6
+ * connection per tenant serves both generations.
7
+ */
8
+ exports.OUTLOOK_CONFIG_KEY = "OPENSERVICE_MICROSOFT_OUTLOOK_INTEGRATION";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Start the v2 side of the Outlook sync — one poller per configured tenant.
3
+ *
4
+ * The connections are read once at boot, like v1 does: a tenant that switches
5
+ * the sync on gets it after the next server start. The pollers are kept so a
6
+ * future reload can stop them instead of doubling them up.
7
+ */
8
+ export declare function initServiceSchedulesFeature(): Promise<void>;
9
+ /** Stop every poller — for tests and for a graceful shutdown. */
10
+ export declare function stopServiceSchedulesFeature(): void;
@@ -0,0 +1,39 @@
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.initServiceSchedulesFeature = initServiceSchedulesFeature;
7
+ exports.stopServiceSchedulesFeature = stopServiceSchedulesFeature;
8
+ const node_1 = __importDefault(require("parse/node"));
9
+ const index_js_1 = require("../../../types/index.js");
10
+ const ServiceCalendarSyncPoller_js_1 = require("./calendarSync/service/ServiceCalendarSyncPoller.js");
11
+ const ServiceSyncTypes_js_1 = require("./calendarSync/types/ServiceSyncTypes.js");
12
+ /** How often the delta feed is asked for changes. */
13
+ const POLL_INTERVAL_MS = 60 * 1000;
14
+ const pollers = [];
15
+ /**
16
+ * Start the v2 side of the Outlook sync — one poller per configured tenant.
17
+ *
18
+ * The connections are read once at boot, like v1 does: a tenant that switches
19
+ * the sync on gets it after the next server start. The pollers are kept so a
20
+ * future reload can stop them instead of doubling them up.
21
+ */
22
+ async function initServiceSchedulesFeature() {
23
+ const configs = await new node_1.default.Query(index_js_1.Config)
24
+ .equalTo("key", ServiceSyncTypes_js_1.OUTLOOK_CONFIG_KEY)
25
+ .includeAll()
26
+ .findAll({ useMasterKey: true });
27
+ for (const config of configs) {
28
+ pollers.push(new ServiceCalendarSyncPoller_js_1.ServiceCalendarSyncPoller(config).start(POLL_INTERVAL_MS));
29
+ }
30
+ if (pollers.length > 0) {
31
+ console.log(`[ServiceCalendarSync] Started for ${pollers.length} calendar connection(s)`);
32
+ }
33
+ }
34
+ /** Stop every poller — for tests and for a graceful shutdown. */
35
+ function stopServiceSchedulesFeature() {
36
+ for (const poller of pollers)
37
+ poller.stop();
38
+ pollers.length = 0;
39
+ }