@openinc/parse-server-opendash 4.0.29 → 4.0.30
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/schedules/calendarSync/functions/eventToSchedule.d.ts +9 -3
- package/dist/features/openservice/schedules/calendarSync/functions/eventToSchedule.js +62 -20
- package/dist/features/openservice/schedules/calendarSync/functions/scheduleToEvent.d.ts +2 -1
- package/dist/features/openservice/schedules/calendarSync/functions/scheduleToEvent.js +43 -29
- package/dist/features/openservice/schedules/calendarSync/service/CalendarManager.d.ts +10 -24
- package/dist/features/openservice/schedules/calendarSync/service/CalendarManager.js +40 -46
- package/dist/features/openservice/schedules/calendarSync/service/CalendarSyncPoller.d.ts +4 -1
- package/dist/features/openservice/schedules/calendarSync/service/CalendarSyncPoller.js +64 -32
- package/dist/features/openservice/schedules/calendarSync/types/ConfigValues.d.ts +8 -0
- package/dist/features/openservice/schedules/calendarSync/types/ConfigValues.js +2 -0
- package/dist/features/openservice/schedules/initSchedulesFeature.js +5 -1
- package/dist/hooks/Config_open_service.js +2 -0
- package/dist/hooks/Maintenance_Schedule.js +31 -8
- package/package.json +5 -5
|
@@ -5,7 +5,13 @@ export type ScheduleCronTimestamp = {
|
|
|
5
5
|
number: number;
|
|
6
6
|
unit: "days" | "weeks" | "months";
|
|
7
7
|
};
|
|
8
|
-
export
|
|
9
|
-
|
|
8
|
+
export type NotifyBeforeDue = {
|
|
9
|
+
value: number;
|
|
10
|
+
unit: "minutes" | "hours" | "days" | "weeks";
|
|
11
|
+
};
|
|
12
|
+
export type EventToScheduleResult = {
|
|
10
13
|
schedule_type: "timestamp";
|
|
11
|
-
|
|
14
|
+
timestamp: ScheduleCronTimestamp;
|
|
15
|
+
notifyBeforeDue?: NotifyBeforeDue;
|
|
16
|
+
};
|
|
17
|
+
export declare function eventToSchedule(event: GraphEventResponse): EventToScheduleResult | undefined;
|
|
@@ -1,10 +1,47 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.eventToSchedule = eventToSchedule;
|
|
7
|
+
const dayjs_1 = __importDefault(require("dayjs"));
|
|
8
|
+
// Parses only the date portion of an Outlook dateTime string to avoid
|
|
9
|
+
// timezone misinterpretation (Outlook dateTime is local to the event's
|
|
10
|
+
// timezone, not UTC).
|
|
11
|
+
function parseDateOnly(dateTime) {
|
|
12
|
+
return (0, dayjs_1.default)(dateTime.slice(0, 10));
|
|
13
|
+
}
|
|
14
|
+
function minutesToNotifyBeforeDue(minutes) {
|
|
15
|
+
if (minutes % (60 * 24 * 7) === 0)
|
|
16
|
+
return { value: minutes / (60 * 24 * 7), unit: "weeks" };
|
|
17
|
+
if (minutes % (60 * 24) === 0)
|
|
18
|
+
return { value: minutes / (60 * 24), unit: "days" };
|
|
19
|
+
if (minutes % 60 === 0)
|
|
20
|
+
return { value: minutes / 60, unit: "hours" };
|
|
21
|
+
return { value: minutes, unit: "minutes" };
|
|
22
|
+
}
|
|
4
23
|
function eventToSchedule(event) {
|
|
24
|
+
const notifyBeforeDue = event.isReminderOn && typeof event.reminderMinutesBeforeStart === "number"
|
|
25
|
+
? minutesToNotifyBeforeDue(event.reminderMinutesBeforeStart)
|
|
26
|
+
: undefined;
|
|
5
27
|
const recurrence = event?.recurrence;
|
|
6
|
-
|
|
7
|
-
|
|
28
|
+
// Single (non-recurring) event: treat as one-day schedule regardless of
|
|
29
|
+
// the event's time or duration — our schedules are always all-day.
|
|
30
|
+
if (!recurrence) {
|
|
31
|
+
if (!event.start?.dateTime)
|
|
32
|
+
return undefined;
|
|
33
|
+
const date = parseDateOnly(event.start.dateTime).toDate();
|
|
34
|
+
return {
|
|
35
|
+
schedule_type: "timestamp",
|
|
36
|
+
timestamp: {
|
|
37
|
+
startDate: date,
|
|
38
|
+
endDate: date,
|
|
39
|
+
number: 1,
|
|
40
|
+
unit: "days",
|
|
41
|
+
},
|
|
42
|
+
notifyBeforeDue,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
8
45
|
const pattern = recurrence.pattern;
|
|
9
46
|
const range = recurrence.range;
|
|
10
47
|
if (!pattern || !range || !pattern.type)
|
|
@@ -21,36 +58,41 @@ function eventToSchedule(event) {
|
|
|
21
58
|
case "relativeMonthly":
|
|
22
59
|
unit = "months";
|
|
23
60
|
break;
|
|
61
|
+
case "absoluteYearly":
|
|
62
|
+
case "relativeYearly":
|
|
63
|
+
// Map yearly recurrence to a 12-month interval
|
|
64
|
+
unit = "months";
|
|
65
|
+
break;
|
|
24
66
|
default:
|
|
25
|
-
// unsupported recurrence type
|
|
26
67
|
return undefined;
|
|
27
68
|
}
|
|
28
69
|
const interval = typeof pattern.interval === "number" && pattern.interval > 0
|
|
29
70
|
? pattern.interval
|
|
30
71
|
: 1;
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
else if (event.start?.dateTime) {
|
|
38
|
-
startDate = new Date(event.start.dateTime);
|
|
39
|
-
}
|
|
40
|
-
// For end, prefer range.endDate. If range.type === 'noEnd' leave undefined.
|
|
72
|
+
// Yearly patterns don't carry an interval in Outlook (always 1 year)
|
|
73
|
+
const effectiveInterval = pattern.type === "absoluteYearly" || pattern.type === "relativeYearly"
|
|
74
|
+
? 12
|
|
75
|
+
: interval;
|
|
76
|
+
// range.startDate is always YYYY-MM-DD
|
|
77
|
+
const startDate = parseDateOnly(range.startDate);
|
|
41
78
|
let endDate;
|
|
42
|
-
if (range
|
|
43
|
-
endDate =
|
|
79
|
+
if (range.type === "endDate") {
|
|
80
|
+
endDate = parseDateOnly(range.endDate);
|
|
44
81
|
}
|
|
45
|
-
if (
|
|
46
|
-
|
|
82
|
+
else if (range.type === "numbered") {
|
|
83
|
+
// Compute end date from occurrence count
|
|
84
|
+
const occurrences = range.numberOfOccurrences ?? 1;
|
|
85
|
+
endDate = startDate.add((occurrences - 1) * effectiveInterval, unit);
|
|
86
|
+
}
|
|
87
|
+
// range.type === "noEnd" → endDate stays undefined
|
|
47
88
|
return {
|
|
48
89
|
schedule_type: "timestamp",
|
|
49
90
|
timestamp: {
|
|
50
|
-
startDate,
|
|
51
|
-
endDate,
|
|
52
|
-
number:
|
|
91
|
+
startDate: startDate.toDate(),
|
|
92
|
+
endDate: endDate?.toDate(),
|
|
93
|
+
number: effectiveInterval,
|
|
53
94
|
unit,
|
|
54
95
|
},
|
|
96
|
+
notifyBeforeDue,
|
|
55
97
|
};
|
|
56
98
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { Maintenance_Schedule } from "../../../../../types/index.js";
|
|
2
|
+
import { CalendarEventSnapshot } from "../service/calendarMetaHelper.js";
|
|
2
3
|
import { GraphEvent } from "../types/Event.js";
|
|
3
|
-
export declare function scheduleToEvent(schedule: Maintenance_Schedule): Promise<GraphEvent | undefined>;
|
|
4
|
+
export declare function scheduleToEvent(schedule: Maintenance_Schedule, originalEvent?: CalendarEventSnapshot): Promise<GraphEvent | undefined>;
|
|
@@ -5,15 +5,32 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.scheduleToEvent = scheduleToEvent;
|
|
7
7
|
const dayjs_1 = __importDefault(require("dayjs"));
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
function notifyBeforeDueToMinutes(notify) {
|
|
9
|
+
switch (notify.unit) {
|
|
10
|
+
case "weeks":
|
|
11
|
+
return notify.value * 60 * 24 * 7;
|
|
12
|
+
case "days":
|
|
13
|
+
return notify.value * 60 * 24;
|
|
14
|
+
case "hours":
|
|
15
|
+
return notify.value * 60;
|
|
16
|
+
case "minutes":
|
|
17
|
+
return notify.value;
|
|
18
|
+
case "seconds":
|
|
19
|
+
return Math.round(notify.value / 60);
|
|
20
|
+
case "milliseconds":
|
|
21
|
+
return Math.round(notify.value / 60000);
|
|
22
|
+
default:
|
|
23
|
+
return notify.value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function scheduleToEvent(schedule, originalEvent) {
|
|
10
27
|
try {
|
|
11
28
|
await schedule.fetchWithInclude(["template"], {
|
|
12
29
|
useMasterKey: true,
|
|
13
30
|
});
|
|
14
31
|
}
|
|
15
32
|
catch (error) {
|
|
16
|
-
console.error(
|
|
33
|
+
console.error(`[scheduleToEvent] Failed to fetch template for schedule ${schedule.id}:`, error);
|
|
17
34
|
}
|
|
18
35
|
const template = schedule.get("template");
|
|
19
36
|
const templateCron = template?.get("cron");
|
|
@@ -23,34 +40,38 @@ async function scheduleToEvent(schedule) {
|
|
|
23
40
|
const interval = scheduleCron?.timestamp?.number ?? templateCron?.timestamp?.number;
|
|
24
41
|
const unit = scheduleCron?.timestamp?.unit ?? templateCron?.timestamp?.unit;
|
|
25
42
|
if (!start || !interval || !unit) {
|
|
26
|
-
console.log("Insufficient data to create event from schedule.");
|
|
27
43
|
return;
|
|
28
44
|
}
|
|
29
45
|
const description = schedule.get("description");
|
|
46
|
+
const notifyBeforeDue = schedule.get("notifyBeforeDue");
|
|
47
|
+
// If the original Outlook event had specific times (not all-day), preserve
|
|
48
|
+
// them so we don't silently convert a timed event to all-day on update.
|
|
49
|
+
const preserveTimes = originalEvent != null && !originalEvent.isAllDay;
|
|
50
|
+
const toDateOnly = (d) => d.toISOString().slice(0, 10);
|
|
30
51
|
const event = {
|
|
31
52
|
subject: schedule.get("title"),
|
|
32
53
|
body: { contentType: "Text", content: description ?? "" },
|
|
33
|
-
start:
|
|
34
|
-
|
|
35
|
-
dateTime: start
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
isAllDay: true,
|
|
54
|
+
start: preserveTimes
|
|
55
|
+
? originalEvent.start
|
|
56
|
+
: { dateTime: toDateOnly(start), timeZone: "UTC" },
|
|
57
|
+
end: preserveTimes
|
|
58
|
+
? originalEvent.end
|
|
59
|
+
: {
|
|
60
|
+
dateTime: toDateOnly((0, dayjs_1.default)(start).add(1, "day").toDate()),
|
|
61
|
+
timeZone: "UTC",
|
|
62
|
+
},
|
|
63
|
+
isAllDay: preserveTimes ? undefined : true,
|
|
64
|
+
isReminderOn: notifyBeforeDue != null,
|
|
65
|
+
reminderMinutesBeforeStart: notifyBeforeDue
|
|
66
|
+
? notifyBeforeDueToMinutes(notifyBeforeDue)
|
|
67
|
+
: undefined,
|
|
43
68
|
};
|
|
44
|
-
// Helper to format date as YYYY-MM-DD for recurrence range
|
|
45
|
-
const toDateOnly = (d) => d.toISOString().slice(0, 10);
|
|
46
|
-
// determine pattern type and additional fields
|
|
47
69
|
let pattern = { interval };
|
|
48
70
|
if (unit === "days") {
|
|
49
71
|
pattern.type = "daily";
|
|
50
72
|
}
|
|
51
73
|
else if (unit === "weeks") {
|
|
52
74
|
pattern.type = "weekly";
|
|
53
|
-
// include the weekday of the start date so weekly recurrences occur on that weekday
|
|
54
75
|
const dayNames = [
|
|
55
76
|
"Sunday",
|
|
56
77
|
"Monday",
|
|
@@ -65,11 +86,9 @@ async function scheduleToEvent(schedule) {
|
|
|
65
86
|
}
|
|
66
87
|
else if (unit === "months") {
|
|
67
88
|
pattern.type = "absoluteMonthly";
|
|
68
|
-
// Graph uses dayOfMonth for absoluteMonthly
|
|
69
89
|
pattern.dayOfMonth = start.getUTCDate();
|
|
70
90
|
}
|
|
71
91
|
else {
|
|
72
|
-
// unsupported unit -> skip recurrence
|
|
73
92
|
pattern = null;
|
|
74
93
|
}
|
|
75
94
|
if (pattern) {
|
|
@@ -83,21 +102,16 @@ async function scheduleToEvent(schedule) {
|
|
|
83
102
|
type: "noEnd",
|
|
84
103
|
startDate: toDateOnly(start),
|
|
85
104
|
};
|
|
86
|
-
event.recurrence = {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// if event is all-day, set start/end to date-only values (Graph expects dates) => always true for schedules
|
|
91
|
-
if (event.isAllDay) {
|
|
105
|
+
event.recurrence = { pattern, range };
|
|
106
|
+
// All-day events need date-only strings for start/end in the Graph payload.
|
|
107
|
+
// Timed events already have their original times set above — don't touch them.
|
|
108
|
+
if (!preserveTimes) {
|
|
92
109
|
event.start.dateTime = toDateOnly(start);
|
|
93
|
-
// Graph's endDate in event object is exclusive for all-day events; keep as provided for recurrence range
|
|
94
|
-
// Set end to next day to represent an all-day event that begins and ends the same day
|
|
95
110
|
const endForEvent = new Date(start);
|
|
96
111
|
endForEvent.setUTCDate(endForEvent.getUTCDate() + 1);
|
|
97
112
|
if (event.end)
|
|
98
113
|
event.end.dateTime = toDateOnly(endForEvent);
|
|
99
114
|
}
|
|
100
115
|
}
|
|
101
|
-
console.log("Schedule converted to Event:", event);
|
|
102
116
|
return event;
|
|
103
117
|
}
|
|
@@ -3,36 +3,22 @@ import { GraphEvent, GraphEventResponse } from "../types/Event.js";
|
|
|
3
3
|
* Manager for calendar operations via Microsoft Graph API
|
|
4
4
|
*/
|
|
5
5
|
export declare class CalendarManager {
|
|
6
|
-
tenantID
|
|
7
|
-
clientID
|
|
8
|
-
clientSecret
|
|
9
|
-
userMail
|
|
6
|
+
tenantID?: string;
|
|
7
|
+
clientID?: string;
|
|
8
|
+
clientSecret?: string;
|
|
9
|
+
userMail?: string;
|
|
10
|
+
constructor(config: {
|
|
11
|
+
tenantId: string;
|
|
12
|
+
clientID: string;
|
|
13
|
+
clientSecret: string;
|
|
14
|
+
userMail: string;
|
|
15
|
+
});
|
|
10
16
|
get isValid(): true | string;
|
|
11
17
|
private getAccess;
|
|
12
|
-
/**
|
|
13
|
-
* Create a new event in the calendar
|
|
14
|
-
* @param event The event data to create
|
|
15
|
-
* @returns the created event response
|
|
16
|
-
*/
|
|
17
18
|
createEvent(event: GraphEvent): Promise<GraphEventResponse | undefined>;
|
|
18
|
-
/**
|
|
19
|
-
* Update an event in the calendar
|
|
20
|
-
* @param eventId The ID of the event to update
|
|
21
|
-
* @param event The updated event data
|
|
22
|
-
* @returns
|
|
23
|
-
*/
|
|
24
19
|
updateEvent(eventId: string, event: Partial<GraphEvent>): Promise<GraphEventResponse | undefined>;
|
|
25
|
-
/**
|
|
26
|
-
* Delete an event from the calendar
|
|
27
|
-
* @param eventId The ID of the event to delete
|
|
28
|
-
* @returns whether the deletion was successful
|
|
29
|
-
*/
|
|
30
20
|
deleteEvent(eventId: string): Promise<boolean | undefined>;
|
|
31
21
|
fetchEvent(eventId: string): Promise<GraphEventResponse | null>;
|
|
32
|
-
/**
|
|
33
|
-
* Fetch all events in calendar
|
|
34
|
-
* @returns the list of events
|
|
35
|
-
*/
|
|
36
22
|
fetchEvents(): Promise<{
|
|
37
23
|
value: GraphEventResponse[];
|
|
38
24
|
} | undefined>;
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CalendarManager = void 0;
|
|
4
|
-
const index_js_1 = require("../../../../config/index.js");
|
|
5
4
|
/**
|
|
6
5
|
* Manager for calendar operations via Microsoft Graph API
|
|
7
6
|
*/
|
|
8
7
|
class CalendarManager {
|
|
9
|
-
tenantID
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
tenantID;
|
|
9
|
+
clientID;
|
|
10
|
+
clientSecret;
|
|
11
|
+
userMail;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.tenantID = config.tenantId;
|
|
14
|
+
this.clientID = config.clientID;
|
|
15
|
+
this.clientSecret = config.clientSecret;
|
|
16
|
+
this.userMail = config.userMail;
|
|
17
|
+
}
|
|
17
18
|
get isValid() {
|
|
18
19
|
if (!this.tenantID)
|
|
19
20
|
return "Missing Microsoft Tenant ID";
|
|
@@ -28,7 +29,6 @@ class CalendarManager {
|
|
|
28
29
|
async getAccess() {
|
|
29
30
|
if (this.isValid !== true)
|
|
30
31
|
return null;
|
|
31
|
-
console.log("Getting access token...");
|
|
32
32
|
try {
|
|
33
33
|
const params = new URLSearchParams();
|
|
34
34
|
params.append("client_id", this.clientID);
|
|
@@ -43,19 +43,17 @@ class CalendarManager {
|
|
|
43
43
|
body: params.toString(),
|
|
44
44
|
});
|
|
45
45
|
const data = await response.json();
|
|
46
|
-
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
console.error(`[CalendarManager] Failed to obtain access token: HTTP ${response.status}`);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
47
50
|
return data;
|
|
48
51
|
}
|
|
49
52
|
catch (error) {
|
|
50
|
-
console.error("Error fetching access token:", error);
|
|
53
|
+
console.error("[CalendarManager] Error fetching access token:", error);
|
|
51
54
|
return null;
|
|
52
55
|
}
|
|
53
56
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Create a new event in the calendar
|
|
56
|
-
* @param event The event data to create
|
|
57
|
-
* @returns the created event response
|
|
58
|
-
*/
|
|
59
57
|
async createEvent(event) {
|
|
60
58
|
if (!this.userMail ||
|
|
61
59
|
!this.tenantID ||
|
|
@@ -73,16 +71,12 @@ class CalendarManager {
|
|
|
73
71
|
},
|
|
74
72
|
body: JSON.stringify(event),
|
|
75
73
|
});
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
console.error(`[CalendarManager] Failed to create event: HTTP ${res.status}`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
return (await res.json());
|
|
79
79
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Update an event in the calendar
|
|
82
|
-
* @param eventId The ID of the event to update
|
|
83
|
-
* @param event The updated event data
|
|
84
|
-
* @returns
|
|
85
|
-
*/
|
|
86
80
|
async updateEvent(eventId, event) {
|
|
87
81
|
const accessData = await this.getAccess();
|
|
88
82
|
if (!accessData?.access_token)
|
|
@@ -95,15 +89,12 @@ class CalendarManager {
|
|
|
95
89
|
},
|
|
96
90
|
body: JSON.stringify(event),
|
|
97
91
|
});
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
console.error(`[CalendarManager] Failed to update event ${eventId}: HTTP ${res.status}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
return (await res.json());
|
|
101
97
|
}
|
|
102
|
-
/**
|
|
103
|
-
* Delete an event from the calendar
|
|
104
|
-
* @param eventId The ID of the event to delete
|
|
105
|
-
* @returns whether the deletion was successful
|
|
106
|
-
*/
|
|
107
98
|
async deleteEvent(eventId) {
|
|
108
99
|
const accessData = await this.getAccess();
|
|
109
100
|
if (!accessData?.access_token)
|
|
@@ -115,7 +106,9 @@ class CalendarManager {
|
|
|
115
106
|
Authorization: `Bearer ${accessData.access_token}`,
|
|
116
107
|
},
|
|
117
108
|
});
|
|
118
|
-
|
|
109
|
+
if (!res.ok) {
|
|
110
|
+
console.error(`[CalendarManager] Failed to delete event ${eventId}: HTTP ${res.status}`);
|
|
111
|
+
}
|
|
119
112
|
return res.ok;
|
|
120
113
|
}
|
|
121
114
|
async fetchEvent(eventId) {
|
|
@@ -123,14 +116,12 @@ class CalendarManager {
|
|
|
123
116
|
if (!accessData?.access_token)
|
|
124
117
|
return null;
|
|
125
118
|
const res = await fetch(`https://graph.microsoft.com/v1.0/users/${encodeURIComponent(this.userMail)}/events/${eventId}`, { headers: { Authorization: `Bearer ${accessData.access_token}` } });
|
|
126
|
-
if (!res.ok)
|
|
119
|
+
if (!res.ok) {
|
|
120
|
+
console.error(`[CalendarManager] Failed to fetch event ${eventId}: HTTP ${res.status}`);
|
|
127
121
|
return null;
|
|
122
|
+
}
|
|
128
123
|
return (await res.json());
|
|
129
124
|
}
|
|
130
|
-
/**
|
|
131
|
-
* Fetch all events in calendar
|
|
132
|
-
* @returns the list of events
|
|
133
|
-
*/
|
|
134
125
|
async fetchEvents() {
|
|
135
126
|
const accessData = await this.getAccess();
|
|
136
127
|
if (!accessData?.access_token)
|
|
@@ -141,9 +132,11 @@ class CalendarManager {
|
|
|
141
132
|
Authorization: `Bearer ${accessData.access_token}`,
|
|
142
133
|
},
|
|
143
134
|
});
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
135
|
+
if (!res.ok) {
|
|
136
|
+
console.error(`[CalendarManager] Failed to fetch events: HTTP ${res.status}`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
return (await res.json());
|
|
147
140
|
}
|
|
148
141
|
/**
|
|
149
142
|
* Fetch changed events since the last delta sync.
|
|
@@ -156,7 +149,7 @@ class CalendarManager {
|
|
|
156
149
|
if (!accessData?.access_token)
|
|
157
150
|
return null;
|
|
158
151
|
const events = [];
|
|
159
|
-
let nextUrl = deltaLink ||
|
|
152
|
+
let nextUrl = deltaLink ||
|
|
160
153
|
`https://graph.microsoft.com/v1.0/users/${encodeURIComponent(this.userMail)}/events/delta`;
|
|
161
154
|
let newDeltaLink = "";
|
|
162
155
|
while (nextUrl) {
|
|
@@ -173,9 +166,10 @@ class CalendarManager {
|
|
|
173
166
|
if (data["@odata.deltaLink"])
|
|
174
167
|
newDeltaLink = data["@odata.deltaLink"];
|
|
175
168
|
}
|
|
176
|
-
|
|
177
|
-
|
|
169
|
+
if (!newDeltaLink) {
|
|
170
|
+
console.error("[CalendarManager] fetchDelta did not return a deltaLink.");
|
|
178
171
|
return null;
|
|
172
|
+
}
|
|
179
173
|
return { events, deltaLink: newDeltaLink };
|
|
180
174
|
}
|
|
181
175
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { Config } from "../../../../../types/index.js";
|
|
1
2
|
export declare class CalendarSyncPoller {
|
|
2
|
-
private manager;
|
|
3
3
|
private timer;
|
|
4
|
+
private config;
|
|
5
|
+
constructor(config: Config);
|
|
6
|
+
private get manager();
|
|
4
7
|
/**
|
|
5
8
|
* Start polling the Microsoft Calendar delta feed.
|
|
6
9
|
* @param intervalMs polling interval in milliseconds (default: 5 minutes)
|
|
@@ -9,17 +9,28 @@ const eventToSchedule_js_1 = require("../functions/eventToSchedule.js");
|
|
|
9
9
|
const calendarMetaHelper_js_1 = require("./calendarMetaHelper.js");
|
|
10
10
|
const CalendarManager_js_1 = require("./CalendarManager.js");
|
|
11
11
|
const index_js_1 = require("../../../../../types/index.js");
|
|
12
|
-
const DELTA_LINK_CONFIG_KEY = "microsoftCalendarDeltaLink";
|
|
13
12
|
class CalendarSyncPoller {
|
|
14
|
-
manager = new CalendarManager_js_1.CalendarManager();
|
|
15
13
|
timer = null;
|
|
14
|
+
config;
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
}
|
|
18
|
+
get manager() {
|
|
19
|
+
const configValues = JSON.parse(this.config.get("value"));
|
|
20
|
+
return new CalendarManager_js_1.CalendarManager({
|
|
21
|
+
tenantId: configValues.tenant_id,
|
|
22
|
+
clientID: configValues.client_id,
|
|
23
|
+
clientSecret: configValues.client_secret,
|
|
24
|
+
userMail: configValues.user_email,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
16
27
|
/**
|
|
17
28
|
* Start polling the Microsoft Calendar delta feed.
|
|
18
29
|
* @param intervalMs polling interval in milliseconds (default: 5 minutes)
|
|
19
30
|
*/
|
|
20
31
|
start(intervalMs = 5 * 60 * 1000) {
|
|
21
|
-
if (this.manager
|
|
22
|
-
console.
|
|
32
|
+
if (this.manager?.isValid !== true) {
|
|
33
|
+
console.error(`[CalendarSyncPoller] Not started — invalid configuration: ${this.manager?.isValid}`);
|
|
23
34
|
return;
|
|
24
35
|
}
|
|
25
36
|
this.poll().catch(console.error);
|
|
@@ -30,11 +41,10 @@ class CalendarSyncPoller {
|
|
|
30
41
|
clearInterval(this.timer);
|
|
31
42
|
}
|
|
32
43
|
async poll() {
|
|
33
|
-
console.log("[CalendarSyncPoller] Polling calendar delta...");
|
|
34
44
|
const deltaLink = await this.loadDeltaLink();
|
|
35
|
-
const result = await this.manager
|
|
45
|
+
const result = await this.manager?.fetchDelta(deltaLink);
|
|
36
46
|
if (!result) {
|
|
37
|
-
console.
|
|
47
|
+
console.error("[CalendarSyncPoller] Failed to fetch calendar delta.");
|
|
38
48
|
return;
|
|
39
49
|
}
|
|
40
50
|
for (const event of result.events) {
|
|
@@ -52,6 +62,10 @@ class CalendarSyncPoller {
|
|
|
52
62
|
await this.handleDeleted(event.id);
|
|
53
63
|
return;
|
|
54
64
|
}
|
|
65
|
+
// Exception events are individual moved/edited occurrences of a series.
|
|
66
|
+
// The series master handles the recurrence as a whole; exceptions are noise.
|
|
67
|
+
if (event.type === "exception")
|
|
68
|
+
return;
|
|
55
69
|
const metaEntry = await (0, calendarMetaHelper_js_1.findCalendarMetaByEventId)(event.id);
|
|
56
70
|
if (metaEntry) {
|
|
57
71
|
const storedChangeKey = metaEntry.get("values")?.microsoftCalendarChangeKey;
|
|
@@ -62,7 +76,7 @@ class CalendarSyncPoller {
|
|
|
62
76
|
}
|
|
63
77
|
else {
|
|
64
78
|
// Delta responses omit most properties; fetch the full event before creating a schedule
|
|
65
|
-
const fullEvent = await this.manager
|
|
79
|
+
const fullEvent = await this.manager?.fetchEvent(event.id);
|
|
66
80
|
if (!fullEvent)
|
|
67
81
|
return;
|
|
68
82
|
await this.createScheduleFromEvent(fullEvent);
|
|
@@ -73,15 +87,19 @@ class CalendarSyncPoller {
|
|
|
73
87
|
if (!scheduleId)
|
|
74
88
|
return;
|
|
75
89
|
// Delta responses omit most fields; fetch the full event for title/body/cron
|
|
76
|
-
const fullEvent = await this.manager
|
|
90
|
+
const fullEvent = (await this.manager?.fetchEvent(event.id)) ?? event;
|
|
77
91
|
const schedule = await new node_1.default.Query(index_js_1.Maintenance_Schedule).get(scheduleId, { useMasterKey: true });
|
|
78
92
|
if (fullEvent.subject)
|
|
79
93
|
schedule.set("title", fullEvent.subject);
|
|
80
94
|
if (fullEvent.bodyPreview)
|
|
81
95
|
schedule.set("description", fullEvent.bodyPreview);
|
|
82
96
|
const cronData = (0, eventToSchedule_js_1.eventToSchedule)(fullEvent);
|
|
83
|
-
if (cronData)
|
|
84
|
-
|
|
97
|
+
if (cronData) {
|
|
98
|
+
const { notifyBeforeDue, ...cron } = cronData;
|
|
99
|
+
schedule.set("cron", cron);
|
|
100
|
+
if (notifyBeforeDue != null)
|
|
101
|
+
schedule.set("notifyBeforeDue", notifyBeforeDue);
|
|
102
|
+
}
|
|
85
103
|
// useMasterKey prevents afterSaveHook from re-syncing back to calendar
|
|
86
104
|
await schedule.save(null, { useMasterKey: true });
|
|
87
105
|
if (event.changeKey) {
|
|
@@ -90,31 +108,45 @@ class CalendarSyncPoller {
|
|
|
90
108
|
}
|
|
91
109
|
async createScheduleFromEvent(event) {
|
|
92
110
|
const parsed = parseEventSubject(event.subject);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
111
|
+
let source;
|
|
112
|
+
const tenant = this.config.get("tenant");
|
|
113
|
+
if (parsed) {
|
|
114
|
+
source = await new node_1.default.Query(index_js_1.Source)
|
|
115
|
+
.equalTo("name", parsed.sourceName)
|
|
116
|
+
.equalTo("tenant", tenant)
|
|
117
|
+
.first({ useMasterKey: true });
|
|
96
118
|
}
|
|
97
|
-
const source = await new node_1.default.Query(index_js_1.Source)
|
|
98
|
-
.equalTo("name", parsed.sourceName)
|
|
99
|
-
.first({ useMasterKey: true });
|
|
100
119
|
if (!source) {
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
const configValues = JSON.parse(this.config.get("value"));
|
|
121
|
+
if (!configValues.default_source_id) {
|
|
122
|
+
console.error(`[CalendarSyncPoller] Skipping event ${event.id} — no source name in subject and no default_source_id configured.`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
source = await new node_1.default.Query(index_js_1.Source)
|
|
126
|
+
.equalTo("objectId", configValues.default_source_id)
|
|
127
|
+
.first({ useMasterKey: true });
|
|
128
|
+
if (!source) {
|
|
129
|
+
console.error(`[CalendarSyncPoller] Skipping event ${event.id} — default source "${configValues.default_source_id}" not found.`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
103
132
|
}
|
|
104
133
|
const schedule = new index_js_1.Maintenance_Schedule();
|
|
105
134
|
schedule.set("title", event.subject);
|
|
106
135
|
schedule.set("source", source);
|
|
107
136
|
schedule.set("enabled", true);
|
|
108
|
-
schedule.set("tenant",
|
|
137
|
+
schedule.set("tenant", tenant);
|
|
109
138
|
if (event.bodyPreview)
|
|
110
139
|
schedule.set("description", event.bodyPreview);
|
|
111
140
|
const cronData = (0, eventToSchedule_js_1.eventToSchedule)(event);
|
|
112
|
-
if (cronData)
|
|
113
|
-
|
|
141
|
+
if (cronData) {
|
|
142
|
+
const { notifyBeforeDue, ...cron } = cronData;
|
|
143
|
+
schedule.set("cron", cron);
|
|
144
|
+
if (notifyBeforeDue != null)
|
|
145
|
+
schedule.set("notifyBeforeDue", notifyBeforeDue);
|
|
146
|
+
}
|
|
114
147
|
// useMasterKey prevents afterSaveHook from syncing back to calendar
|
|
115
148
|
await schedule.save(null, { useMasterKey: true });
|
|
116
|
-
await (0, calendarMetaHelper_js_1.saveCalendarMeta)(schedule.id, event.id, event.changeKey,
|
|
117
|
-
console.log(`[CalendarSyncPoller] Created schedule "${event.subject}" for source "${parsed.sourceName}" from calendar event ${event.id}.`);
|
|
149
|
+
await (0, calendarMetaHelper_js_1.saveCalendarMeta)(schedule.id, event.id, event.changeKey, tenant, event);
|
|
118
150
|
}
|
|
119
151
|
async handleDeleted(eventId) {
|
|
120
152
|
const metaEntry = await (0, calendarMetaHelper_js_1.findCalendarMetaByEventId)(eventId);
|
|
@@ -135,16 +167,16 @@ class CalendarSyncPoller {
|
|
|
135
167
|
await metaEntry.destroy({ useMasterKey: true });
|
|
136
168
|
}
|
|
137
169
|
async loadDeltaLink() {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
170
|
+
this.config = await this.config.fetch({ useMasterKey: true });
|
|
171
|
+
const values = JSON.parse(this.config.get("value"));
|
|
172
|
+
return values.delta_link;
|
|
145
173
|
}
|
|
146
174
|
async saveDeltaLink(link) {
|
|
147
|
-
await
|
|
175
|
+
this.config = await this.config.fetch({ useMasterKey: true });
|
|
176
|
+
const values = JSON.parse(this.config.get("value"));
|
|
177
|
+
values.delta_link = link;
|
|
178
|
+
this.config.set("value", JSON.stringify(values));
|
|
179
|
+
await this.config.save(null, { useMasterKey: true });
|
|
148
180
|
}
|
|
149
181
|
}
|
|
150
182
|
exports.CalendarSyncPoller = CalendarSyncPoller;
|
|
@@ -9,7 +9,11 @@ const index_js_1 = require("../../../types/index.js");
|
|
|
9
9
|
const CalendarSyncPoller_js_1 = require("./calendarSync/service/CalendarSyncPoller.js");
|
|
10
10
|
async function initSchedulesFeature() {
|
|
11
11
|
updateFiinishedSchedules();
|
|
12
|
-
new
|
|
12
|
+
const configs = await new node_1.default.Query(index_js_1.Config)
|
|
13
|
+
.equalTo("key", "OPENSERVICE_MICROSOFT_OUTLOOK_INTEGRATION")
|
|
14
|
+
.includeAll()
|
|
15
|
+
.findAll({ useMasterKey: true });
|
|
16
|
+
const pollers = configs.map((config) => new CalendarSyncPoller_js_1.CalendarSyncPoller(config).start(1000 * 60));
|
|
13
17
|
}
|
|
14
18
|
async function updateFiinishedSchedules() {
|
|
15
19
|
const executionsToUpdate = await new node_1.default.Query(index_js_1.Maintenance_Schedule_Execution)
|
|
@@ -45,6 +45,8 @@ async function init() {
|
|
|
45
45
|
//object.value can look like this: [{"label":"PSA","allowedValues":[],"required":false},{"label":"Typ","allowedValues":[],"required":false,"onlyOneAllowedValue":false},{"label":"Material","allowedValues":["@OD3_Maintenance_Item"],"required":false,"onlyOneAllowedValue":false},{"label":"Dienstleister","allowedValues":["@OD3_COMPANY","@OD3_CONTACT"],"required":false,"onlyOneAllowedValue":false}]
|
|
46
46
|
//fields in Maintenance_Step can look like this: [{"value":"Handschuhe","metafield":{"label":"PSA","allowedValues":[],"required":false}},{"value":"Auffüllen","metafield":{"label":"Typ","allowedValues":[],"required":false,"onlyOneAllowedValue":false}}]
|
|
47
47
|
const metafieldsconfig = JSON.parse(object.get("value"));
|
|
48
|
+
if (!Array.isArray(metafieldsconfig))
|
|
49
|
+
return;
|
|
48
50
|
const [maintenanceStepQueryError, maintenanceStepQuery] = await (0, catchError_js_1.catchError)(new node_1.default.Query(index_js_2.Maintenance_Schedule_Step).find({
|
|
49
51
|
useMasterKey: true,
|
|
50
52
|
}));
|
|
@@ -52,7 +52,7 @@ async function init() {
|
|
|
52
52
|
});
|
|
53
53
|
(0, schema_1.beforeDeleteHook)(types_1.Maintenance_Schedule, async (request) => {
|
|
54
54
|
const { object } = request;
|
|
55
|
-
const scheduleFetched = await object?.fetchWithInclude(["template", "source"], {
|
|
55
|
+
const scheduleFetched = await object?.fetchWithInclude(["template", "source", "tenant"], {
|
|
56
56
|
useMasterKey: true,
|
|
57
57
|
});
|
|
58
58
|
const template = scheduleFetched?.get("template");
|
|
@@ -64,7 +64,10 @@ async function init() {
|
|
|
64
64
|
if (metaEntry) {
|
|
65
65
|
const eventId = metaEntry.get("values")?.microsoftCalendarEventId;
|
|
66
66
|
if (eventId) {
|
|
67
|
-
await
|
|
67
|
+
const calendarManager = await getScheduleCalendarManager(object);
|
|
68
|
+
if (!calendarManager)
|
|
69
|
+
return;
|
|
70
|
+
await calendarManager.deleteEvent(eventId);
|
|
68
71
|
}
|
|
69
72
|
await metaEntry.destroy({ useMasterKey: true });
|
|
70
73
|
}
|
|
@@ -89,14 +92,16 @@ async function addToTemplateSources(schedule) {
|
|
|
89
92
|
await template.save(null, { useMasterKey: true });
|
|
90
93
|
}
|
|
91
94
|
async function updateCalendarEvent(schedule) {
|
|
92
|
-
const
|
|
95
|
+
const metaEntry = await (0, calendarMetaHelper_1.findCalendarMetaForSchedule)(schedule.id);
|
|
96
|
+
const snapshot = metaEntry?.get("values")?.microsoftCalendarEvent;
|
|
97
|
+
const event = await (0, scheduleToEvent_1.scheduleToEvent)(schedule, snapshot);
|
|
93
98
|
if (!event)
|
|
94
99
|
return;
|
|
95
|
-
const calendarManager =
|
|
96
|
-
|
|
100
|
+
const calendarManager = await getScheduleCalendarManager(schedule);
|
|
101
|
+
if (!calendarManager)
|
|
102
|
+
return;
|
|
97
103
|
const eventId = metaEntry?.get("values")?.microsoftCalendarEventId;
|
|
98
104
|
if (eventId) {
|
|
99
|
-
console.log("Updating schedule in calendar...");
|
|
100
105
|
const updatedEvent = await calendarManager.updateEvent(eventId, event);
|
|
101
106
|
if (updatedEvent?.changeKey && metaEntry) {
|
|
102
107
|
await (0, calendarMetaHelper_1.updateCalendarChangeKey)(metaEntry, updatedEvent.changeKey, updatedEvent);
|
|
@@ -111,13 +116,31 @@ async function updateCalendarEvent(schedule) {
|
|
|
111
116
|
* @param schedule
|
|
112
117
|
*/
|
|
113
118
|
async function addToCalendar(schedule) {
|
|
114
|
-
console.log("Creating new calendar event for schedule...");
|
|
115
119
|
const event = await (0, scheduleToEvent_1.scheduleToEvent)(schedule);
|
|
116
120
|
if (!event)
|
|
117
121
|
return;
|
|
118
|
-
const calendarManager =
|
|
122
|
+
const calendarManager = await getScheduleCalendarManager(schedule);
|
|
123
|
+
if (!calendarManager)
|
|
124
|
+
return;
|
|
119
125
|
const createdEvent = await calendarManager.createEvent(event);
|
|
120
126
|
if (createdEvent?.id) {
|
|
121
127
|
await (0, calendarMetaHelper_1.saveCalendarMeta)(schedule.id, createdEvent.id, createdEvent.changeKey, schedule.get("tenant"), createdEvent);
|
|
122
128
|
}
|
|
123
129
|
}
|
|
130
|
+
async function getScheduleCalendarManager(schedule) {
|
|
131
|
+
const config = await new node_1.default.Query(types_1.Config)
|
|
132
|
+
.equalTo("key", "OPENSERVICE_MICROSOFT_OUTLOOK_INTEGRATION")
|
|
133
|
+
.equalTo("tenant", schedule.get("tenant"))
|
|
134
|
+
.first({ useMasterKey: true });
|
|
135
|
+
if (!config) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const configValues = JSON.parse(config.get("value"));
|
|
139
|
+
const calendarManager = new CalendarManager_1.CalendarManager({
|
|
140
|
+
tenantId: configValues.tenant_id,
|
|
141
|
+
clientID: configValues.client_id,
|
|
142
|
+
clientSecret: configValues.client_secret,
|
|
143
|
+
userMail: configValues.user_email,
|
|
144
|
+
});
|
|
145
|
+
return calendarManager;
|
|
146
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openinc/parse-server-opendash",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.30",
|
|
4
4
|
"description": "Parse Server Cloud Code for open.INC Stack.",
|
|
5
5
|
"packageManager": "pnpm@10.33.0",
|
|
6
6
|
"keywords": [
|
|
@@ -72,14 +72,14 @@
|
|
|
72
72
|
"dayjs": "^1.11.20",
|
|
73
73
|
"dotenv": "^17.4.2",
|
|
74
74
|
"fast-equals": "^6.0.0",
|
|
75
|
-
"i18next": "^26.0.
|
|
76
|
-
"i18next-fs-backend": "^2.6.
|
|
75
|
+
"i18next": "^26.0.10",
|
|
76
|
+
"i18next-fs-backend": "^2.6.5",
|
|
77
77
|
"jsonwebtoken": "^9.0.3",
|
|
78
78
|
"jwks-rsa": "^4.0.1",
|
|
79
|
-
"nodemailer": "^8.0.
|
|
79
|
+
"nodemailer": "^8.0.7",
|
|
80
80
|
"nunjucks": "^3.2.4",
|
|
81
81
|
"parse": "8.5.0",
|
|
82
|
-
"parse-server": "9.
|
|
82
|
+
"parse-server": "^9.9.0",
|
|
83
83
|
"pdf-img-convert": "2.0.0",
|
|
84
84
|
"rimraf": "^6.1.3",
|
|
85
85
|
"table": "^6.9.0",
|