@openinc/parse-server-opendash 4.0.29 → 4.0.31

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.
@@ -5,7 +5,13 @@ export type ScheduleCronTimestamp = {
5
5
  number: number;
6
6
  unit: "days" | "weeks" | "months";
7
7
  };
8
- export declare function eventToSchedule(event: GraphEventResponse): {
9
- timestamp: ScheduleCronTimestamp;
8
+ export type NotifyBeforeDue = {
9
+ value: number;
10
+ unit: "minutes" | "hours" | "days" | "weeks";
11
+ };
12
+ export type EventToScheduleResult = {
10
13
  schedule_type: "timestamp";
11
- } | undefined;
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
- if (!recurrence)
7
- return undefined;
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
- // Prefer explicit range.startDate, fallback to event.start.dateTime
32
- let startDate;
33
- if (range?.startDate) {
34
- // range.startDate is YYYY-MM-DD
35
- startDate = new Date(range.startDate + "T00:00:00Z");
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?.type === "endDate" && range?.endDate) {
43
- endDate = new Date(range.endDate + "T00:00:00Z");
79
+ if (range.type === "endDate") {
80
+ endDate = parseDateOnly(range.endDate);
44
81
  }
45
- if (!startDate)
46
- return undefined;
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: interval,
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
- async function scheduleToEvent(schedule) {
9
- console.log("Converting Schedule to Event...");
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("Error fetching template:", 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
- // for single-day/all-day events we use date-only; keep full ISO otherwise
35
- dateTime: start.toISOString().slice(0, 19),
36
- timeZone: "UTC",
37
- },
38
- end: {
39
- dateTime: (0, dayjs_1.default)(start).add(1, "day").toISOString().slice(0, 19),
40
- timeZone: "UTC",
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
- pattern,
88
- range,
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: string;
7
- clientID: string;
8
- clientSecret: string;
9
- userMail: string;
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 = index_js_1.ConfigInstance.getInstance().get("MICROSOFT_TENANT_ID") ||
10
- index_js_1.ConfigInstance.getInstance().get("OI_MICROSOFT_TENANT_ID");
11
- clientID = index_js_1.ConfigInstance.getInstance().get("MICROSOFT_CLIENT_ID") ||
12
- index_js_1.ConfigInstance.getInstance().get("OI_MICROSOFT_APP_ID");
13
- clientSecret = index_js_1.ConfigInstance.getInstance().get("MICROSOFT_CLIENT_SECRET") ||
14
- index_js_1.ConfigInstance.getInstance().get("OI_MICROSOFT_CLIENT_SECRET");
15
- userMail = index_js_1.ConfigInstance.getInstance().get("MICROSOFT_USER_EMAIL") ||
16
- index_js_1.ConfigInstance.getInstance().get("OI_MICROSOFT_USER_EMAIL");
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
- console.log("Access token fetched successfully:", data);
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
- const data = (await res.json());
77
- console.log("Event creation response:", data);
78
- return data;
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
- const data = (await res.json());
99
- console.log("Event update response:", data);
100
- return data;
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
- console.log("Event deletion response:", res);
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
- const data = await res.json();
145
- console.log("Calendar events:", data);
146
- return data;
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 || // treat empty string same as missing
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
- console.log("Delta fetch result:", { events, newDeltaLink });
177
- if (!newDeltaLink)
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.isValid !== true) {
22
- console.log("[CalendarSyncPoller] Not started — missing Microsoft credentials.", this.manager.isValid);
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.fetchDelta(deltaLink ?? undefined);
45
+ const result = await this.manager?.fetchDelta(deltaLink);
36
46
  if (!result) {
37
- console.log("[CalendarSyncPoller] Failed to fetch calendar delta.");
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.fetchEvent(event.id);
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.fetchEvent(event.id) ?? event;
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
- schedule.set("cron", cronData);
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
- if (!parsed) {
94
- console.log(`[CalendarSyncPoller] Skipping event "${event.subject}" (${event.id}) — subject does not match expected format: '"<source>" - <title>'.`);
95
- return;
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
- console.log(`[CalendarSyncPoller] Skipping event "${event.subject}" (${event.id}) — no Source found with name "${parsed.sourceName}".`);
102
- return;
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", source.get("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
- schedule.set("cron", cronData);
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, source.get("tenant"), event);
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
- try {
139
- const config = await node_1.default.Config.get({ useMasterKey: true });
140
- return config.get(DELTA_LINK_CONFIG_KEY) || null;
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 node_1.default.Config.save({ [DELTA_LINK_CONFIG_KEY]: link }, { useMasterKey: true });
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;
@@ -0,0 +1,8 @@
1
+ export type ConfigValues = {
2
+ tenant_id: string;
3
+ client_id: string;
4
+ client_secret: string;
5
+ user_email: string;
6
+ default_source_id?: string;
7
+ delta_link?: string;
8
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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 CalendarSyncPoller_js_1.CalendarSyncPoller().start(1000 * 60);
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 new CalendarManager_1.CalendarManager().deleteEvent(eventId);
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 event = await (0, scheduleToEvent_1.scheduleToEvent)(schedule);
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 = new CalendarManager_1.CalendarManager();
96
- const metaEntry = await (0, calendarMetaHelper_1.findCalendarMetaForSchedule)(schedule.id);
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 = new CalendarManager_1.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.29",
3
+ "version": "4.0.31",
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.6",
76
- "i18next-fs-backend": "^2.6.4",
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.5",
79
+ "nodemailer": "^8.0.7",
80
80
  "nunjucks": "^3.2.4",
81
- "parse": "8.5.0",
82
- "parse-server": "9.8.0",
81
+ "parse": "^8.5.0",
82
+ "parse-server": "^9.8.0",
83
83
  "pdf-img-convert": "2.0.0",
84
84
  "rimraf": "^6.1.3",
85
85
  "table": "^6.9.0",