@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.
- package/dist/features/openservice/init.js +4 -0
- package/dist/features/openservice/schedules/calendarSync/types/Event.d.ts +2 -0
- package/dist/features/openservice/serviceSchedules/acl/assignmentAcl.d.ts +30 -0
- package/dist/features/openservice/serviceSchedules/acl/assignmentAcl.js +85 -0
- package/dist/features/openservice/serviceSchedules/acl/index.d.ts +1 -0
- package/dist/features/openservice/serviceSchedules/acl/index.js +6 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/eventToPlan.d.ts +50 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/eventToPlan.js +115 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.d.ts +59 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.js +353 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/planToEvent.d.ts +27 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/planToEvent.js +145 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/index.d.ts +8 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/index.js +28 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/ServiceCalendarSyncPoller.d.ts +54 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/ServiceCalendarSyncPoller.js +248 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/serviceCalendarMeta.d.ts +55 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/serviceCalendarMeta.js +109 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/syncPlanToCalendar.d.ts +26 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/syncPlanToCalendar.js +122 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/types/ServiceSyncTypes.d.ts +55 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/types/ServiceSyncTypes.js +8 -0
- package/dist/features/openservice/serviceSchedules/initServiceSchedulesFeature.d.ts +10 -0
- package/dist/features/openservice/serviceSchedules/initServiceSchedulesFeature.js +39 -0
- package/dist/features/openservice/serviceTickets/alarmActions/functions/alarmTicketFields.d.ts +35 -0
- package/dist/features/openservice/serviceTickets/alarmActions/functions/alarmTicketFields.js +103 -0
- package/dist/features/openservice/serviceTickets/alarmActions/index.d.ts +7 -0
- package/dist/features/openservice/serviceTickets/alarmActions/index.js +16 -0
- package/dist/features/openservice/serviceTickets/alarmActions/service/createAlarmTicket.d.ts +33 -0
- package/dist/features/openservice/serviceTickets/alarmActions/service/createAlarmTicket.js +179 -0
- package/dist/features/openservice/serviceTickets/alarmActions/service/ensureAlarmTicketAction.d.ts +22 -0
- package/dist/features/openservice/serviceTickets/alarmActions/service/ensureAlarmTicketAction.js +127 -0
- package/dist/features/openservice/serviceTickets/alarmActions/types/AlarmTicketTypes.d.ts +80 -0
- package/dist/features/openservice/serviceTickets/alarmActions/types/AlarmTicketTypes.js +28 -0
- package/dist/features/permissions/types/Permissions.d.ts +1 -2
- package/dist/features/permissions/types/Permissions.js +8 -2
- package/dist/functions/openinc-openservice-alarm-create-ticket.d.ts +25 -0
- package/dist/functions/openinc-openservice-alarm-create-ticket.js +63 -0
- package/dist/functions/openinc-openservice-notify-contacts.d.ts +16 -0
- package/dist/functions/openinc-openservice-notify-contacts.js +82 -0
- package/dist/functions/openinc-openservice-schedule-calendar-sync.d.ts +15 -0
- package/dist/functions/openinc-openservice-schedule-calendar-sync.js +60 -0
- package/dist/hooks/Service_Schedule.js +89 -16
- package/dist/hooks/Service_Schedule_Template.js +28 -7
- package/dist/hooks/Service_Ticket.js +2 -2
- package/dist/types/Service_Schedule.d.ts +12 -0
- package/dist/types/Service_Schedule.js +21 -0
- package/dist/types/Service_Schedule_Execution.d.ts +6 -3
- package/dist/types/Service_Schedule_Execution.js +6 -0
- package/dist/types/Service_Schedule_Template.d.ts +7 -0
- package/dist/types/Service_Schedule_Template.js +12 -0
- package/package.json +5 -4
- package/schema/Service_Schedule.json +17 -0
- package/schema/Service_Schedule_Execution.json +5 -0
- package/schema/Service_Schedule_Template.json +9 -0
- package/dist/features/ruleset/applyRuleSet.d.ts +0 -41
- package/dist/features/ruleset/applyRuleSet.js +0 -81
package/dist/features/openservice/serviceSchedules/calendarSync/service/ServiceCalendarSyncPoller.js
ADDED
|
@@ -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;
|
package/dist/features/openservice/serviceSchedules/calendarSync/service/serviceCalendarMeta.d.ts
ADDED
|
@@ -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;
|
package/dist/features/openservice/serviceSchedules/calendarSync/service/serviceCalendarMeta.js
ADDED
|
@@ -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
|
+
}
|
package/dist/features/openservice/serviceSchedules/calendarSync/service/syncPlanToCalendar.d.ts
ADDED
|
@@ -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
|
+
}
|