@openinc/parse-server-opendash 4.1.7 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/features/openservice/init.js +4 -0
  2. package/dist/features/openservice/schedules/calendarSync/types/Event.d.ts +2 -0
  3. package/dist/features/openservice/serviceSchedules/calendarSync/functions/eventToPlan.d.ts +50 -0
  4. package/dist/features/openservice/serviceSchedules/calendarSync/functions/eventToPlan.js +115 -0
  5. package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.d.ts +39 -0
  6. package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.js +310 -0
  7. package/dist/features/openservice/serviceSchedules/calendarSync/functions/planToEvent.d.ts +27 -0
  8. package/dist/features/openservice/serviceSchedules/calendarSync/functions/planToEvent.js +145 -0
  9. package/dist/features/openservice/serviceSchedules/calendarSync/index.d.ts +8 -0
  10. package/dist/features/openservice/serviceSchedules/calendarSync/index.js +28 -0
  11. package/dist/features/openservice/serviceSchedules/calendarSync/service/ServiceCalendarSyncPoller.d.ts +54 -0
  12. package/dist/features/openservice/serviceSchedules/calendarSync/service/ServiceCalendarSyncPoller.js +248 -0
  13. package/dist/features/openservice/serviceSchedules/calendarSync/service/serviceCalendarMeta.d.ts +55 -0
  14. package/dist/features/openservice/serviceSchedules/calendarSync/service/serviceCalendarMeta.js +109 -0
  15. package/dist/features/openservice/serviceSchedules/calendarSync/service/syncPlanToCalendar.d.ts +26 -0
  16. package/dist/features/openservice/serviceSchedules/calendarSync/service/syncPlanToCalendar.js +114 -0
  17. package/dist/features/openservice/serviceSchedules/calendarSync/types/ServiceSyncTypes.d.ts +55 -0
  18. package/dist/features/openservice/serviceSchedules/calendarSync/types/ServiceSyncTypes.js +8 -0
  19. package/dist/features/openservice/serviceSchedules/initServiceSchedulesFeature.d.ts +10 -0
  20. package/dist/features/openservice/serviceSchedules/initServiceSchedulesFeature.js +39 -0
  21. package/dist/features/openservice/serviceTickets/alarmActions/functions/alarmTicketFields.d.ts +35 -0
  22. package/dist/features/openservice/serviceTickets/alarmActions/functions/alarmTicketFields.js +103 -0
  23. package/dist/features/openservice/serviceTickets/alarmActions/index.d.ts +7 -0
  24. package/dist/features/openservice/serviceTickets/alarmActions/index.js +16 -0
  25. package/dist/features/openservice/serviceTickets/alarmActions/service/createAlarmTicket.d.ts +33 -0
  26. package/dist/features/openservice/serviceTickets/alarmActions/service/createAlarmTicket.js +179 -0
  27. package/dist/features/openservice/serviceTickets/alarmActions/service/ensureAlarmTicketAction.d.ts +22 -0
  28. package/dist/features/openservice/serviceTickets/alarmActions/service/ensureAlarmTicketAction.js +127 -0
  29. package/dist/features/openservice/serviceTickets/alarmActions/types/AlarmTicketTypes.d.ts +80 -0
  30. package/dist/features/openservice/serviceTickets/alarmActions/types/AlarmTicketTypes.js +28 -0
  31. package/dist/functions/openinc-openservice-alarm-create-ticket.d.ts +25 -0
  32. package/dist/functions/openinc-openservice-alarm-create-ticket.js +63 -0
  33. package/dist/functions/openinc-openservice-notify-contacts.d.ts +16 -0
  34. package/dist/functions/openinc-openservice-notify-contacts.js +82 -0
  35. package/dist/functions/openinc-openservice-schedule-calendar-sync.d.ts +15 -0
  36. package/dist/functions/openinc-openservice-schedule-calendar-sync.js +60 -0
  37. package/dist/hooks/Service_Schedule.js +73 -5
  38. package/dist/types/Service_Schedule.d.ts +6 -0
  39. package/dist/types/Service_Schedule.js +12 -0
  40. package/dist/types/Service_Schedule_Template.d.ts +3 -0
  41. package/dist/types/Service_Schedule_Template.js +6 -0
  42. package/package.json +1 -1
  43. package/schema/Service_Schedule.json +9 -0
  44. package/schema/Service_Schedule_Template.json +5 -0
@@ -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,114 @@
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 planToEvent_js_1 = require("../functions/planToEvent.js");
12
+ const ServiceSyncTypes_js_1 = require("../types/ServiceSyncTypes.js");
13
+ const serviceCalendarMeta_js_1 = require("./serviceCalendarMeta.js");
14
+ /**
15
+ * Write a maintenance plan into the tenant's Outlook calendar.
16
+ *
17
+ * The counterpart of `ServiceCalendarSyncPoller`; called from the
18
+ * `Service_Schedule` afterSave hook. Silent when the tenant has no calendar
19
+ * connection — the sync is opt-in per tenant, and a missing connection is not
20
+ * an error.
21
+ *
22
+ * Four cases, in this order:
23
+ *
24
+ * 1. plan archived or inactive → the appointment is removed; maintenance that
25
+ * no longer happens must not sit in someone's calendar;
26
+ * 2. plan has no date (manual, sensor- or meter-driven) → same, there is
27
+ * nothing to show;
28
+ * 3. plan already linked → the event is updated in place, keeping the time of
29
+ * day someone gave it in Outlook;
30
+ * 4. otherwise → a new event, and a meta entry that links the two.
31
+ */
32
+ async function syncPlanToCalendar(plan) {
33
+ const connection = await connectionForTenant(plan.get("tenant"));
34
+ if (!connection)
35
+ return;
36
+ const meta = await (0, serviceCalendarMeta_js_1.findMetaForPlan)(plan.id);
37
+ const values = meta ? (0, serviceCalendarMeta_js_1.metaValues)(meta) : undefined;
38
+ const eventId = values?.microsoftCalendarEventId;
39
+ const retired = !!plan.get("deletedAt") || plan.get("active") === false;
40
+ const event = retired
41
+ ? undefined
42
+ : (0, planToEvent_js_1.planToEvent)({
43
+ title: plan.get("title"),
44
+ cadence: plan.get("cadence"),
45
+ steps: plan.get("steps"),
46
+ notifications: plan.get("notifications"),
47
+ equipmentName: await equipmentLabel(plan),
48
+ snapshot: values?.microsoftCalendarEvent,
49
+ });
50
+ if (!event) {
51
+ if (meta && eventId) {
52
+ await connection.manager.deleteEvent(eventId);
53
+ await meta.destroy({ useMasterKey: true });
54
+ }
55
+ return;
56
+ }
57
+ if (meta && eventId) {
58
+ const updated = await connection.manager.updateEvent(eventId, event);
59
+ if (updated?.changeKey) {
60
+ await (0, serviceCalendarMeta_js_1.updateChangeKey)(meta, updated.changeKey, updated);
61
+ }
62
+ return;
63
+ }
64
+ const created = await connection.manager.createEvent(event);
65
+ if (!created?.id)
66
+ return;
67
+ await (0, serviceCalendarMeta_js_1.saveMeta)(plan.id, created.id, created.changeKey, plan.get("tenant"), created);
68
+ }
69
+ /** The machine's label, for the subject prefix — `undefined` if it has none. */
70
+ async function equipmentLabel(plan) {
71
+ const equipment = plan.get("equipment");
72
+ if (!equipment)
73
+ return undefined;
74
+ // A pointer straight from a save is unfetched; the label lives on the object.
75
+ if (!equipment.get("label")) {
76
+ try {
77
+ await equipment.fetch({ useMasterKey: true });
78
+ }
79
+ catch {
80
+ return undefined;
81
+ }
82
+ }
83
+ return equipment.get("label") || undefined;
84
+ }
85
+ async function connectionForTenant(tenant) {
86
+ if (!tenant)
87
+ return null;
88
+ const config = await new node_1.default.Query(index_js_1.Config)
89
+ .equalTo("key", ServiceSyncTypes_js_1.OUTLOOK_CONFIG_KEY)
90
+ .equalTo("tenant", tenant)
91
+ .first({ useMasterKey: true });
92
+ if (!config)
93
+ return null;
94
+ let values;
95
+ try {
96
+ values = JSON.parse(config.get("value"));
97
+ }
98
+ catch {
99
+ console.error(`[ServiceCalendarSync] Config ${config.id} does not contain valid JSON`);
100
+ return null;
101
+ }
102
+ const manager = new CalendarManager_js_1.CalendarManager({
103
+ tenantId: values.tenant_id,
104
+ clientID: values.client_id,
105
+ clientSecret: values.client_secret,
106
+ userMail: values.user_email,
107
+ });
108
+ const valid = manager.isValid;
109
+ if (valid !== true) {
110
+ console.warn(`[ServiceCalendarSync] Connection unusable: ${valid}`);
111
+ return null;
112
+ }
113
+ return { manager, values };
114
+ }
@@ -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
+ }
@@ -0,0 +1,35 @@
1
+ import { type AlarmEvent, type AlarmTicketParams } from "../types/AlarmTicketTypes.js";
2
+ /**
3
+ * Alarm + action parameters → the text fields of a ticket.
4
+ *
5
+ * Pure, so the wording and — more importantly — the `origin` marker can be
6
+ * checked without a database. The marker is what makes the whole thing
7
+ * idempotent: it identifies the alarm, and one open ticket per marker is the
8
+ * default.
9
+ */
10
+ /** Title, description and origin of the ticket a fired alarm produces. */
11
+ export interface AlarmTicketFields {
12
+ title: string;
13
+ description: string;
14
+ origin: string;
15
+ }
16
+ export declare function alarmTicketFields(event: AlarmEvent, params?: AlarmTicketParams): AlarmTicketFields;
17
+ /**
18
+ * The `origin` marker: `alarm:<key>`.
19
+ *
20
+ * Same idea as the schedule plugin's `schedule:<id>:<occurrence>` — a
21
+ * free-form, greppable string that says where a ticket came from and can be
22
+ * queried back (`equalTo("origin", …)`). The key is the alarm's id when the
23
+ * executor knows it, otherwise source and sensor, which stay stable across
24
+ * firings of the same alarm.
25
+ */
26
+ export declare function alarmOrigin(event: AlarmEvent): string;
27
+ /**
28
+ * A stable identity for the alarm — the one thing a payload must carry.
29
+ *
30
+ * Returns `undefined` when the executor reports neither an alarm id nor a
31
+ * sensor: such a call cannot be deduplicated and is rejected rather than
32
+ * silently creating a ticket on every firing.
33
+ */
34
+ export declare function alarmKey(event: AlarmEvent): string | undefined;
35
+ export declare function firedAt(event: AlarmEvent): Date;
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.alarmTicketFields = alarmTicketFields;
4
+ exports.alarmOrigin = alarmOrigin;
5
+ exports.alarmKey = alarmKey;
6
+ exports.firedAt = firedAt;
7
+ const AlarmTicketTypes_js_1 = require("../types/AlarmTicketTypes.js");
8
+ function alarmTicketFields(event, params = {}) {
9
+ return {
10
+ title: renderTitle(event, params),
11
+ description: renderDescription(event, params),
12
+ origin: alarmOrigin(event),
13
+ };
14
+ }
15
+ /**
16
+ * The `origin` marker: `alarm:<key>`.
17
+ *
18
+ * Same idea as the schedule plugin's `schedule:<id>:<occurrence>` — a
19
+ * free-form, greppable string that says where a ticket came from and can be
20
+ * queried back (`equalTo("origin", …)`). The key is the alarm's id when the
21
+ * executor knows it, otherwise source and sensor, which stay stable across
22
+ * firings of the same alarm.
23
+ */
24
+ function alarmOrigin(event) {
25
+ return `alarm:${alarmKey(event)}`;
26
+ }
27
+ /**
28
+ * A stable identity for the alarm — the one thing a payload must carry.
29
+ *
30
+ * Returns `undefined` when the executor reports neither an alarm id nor a
31
+ * sensor: such a call cannot be deduplicated and is rejected rather than
32
+ * silently creating a ticket on every firing.
33
+ */
34
+ function alarmKey(event) {
35
+ if (event.alarmId)
36
+ return event.alarmId;
37
+ const sensor = [event.item_source, event.item_id].filter(Boolean).join("/");
38
+ if (!sensor)
39
+ return undefined;
40
+ return event.item_dimension != null
41
+ ? `${sensor}#${event.item_dimension}`
42
+ : sensor;
43
+ }
44
+ /** `{{alarm}}` / `{{sensor}}` / `{{value}}` in the configured title. */
45
+ function renderTitle(event, params) {
46
+ const template = params.title?.trim();
47
+ const sensor = sensorLabel(event);
48
+ if (!template) {
49
+ // No template: the alarm's own name is the most recognisable thing there
50
+ // is, and a ticket must have a title.
51
+ return event.name || (sensor ? `Alarm: ${sensor}` : "Alarm");
52
+ }
53
+ return template
54
+ .replace(/\{\{\s*alarm\s*\}\}/gi, event.name ?? "")
55
+ .replace(/\{\{\s*sensor\s*\}\}/gi, sensor)
56
+ .replace(/\{\{\s*value\s*\}\}/gi, formatValue(event.value))
57
+ .replace(/\s+/g, " ")
58
+ .trim();
59
+ }
60
+ /**
61
+ * The configured note plus what the alarm actually reported.
62
+ *
63
+ * The details are appended rather than templated, because the person handling
64
+ * the ticket needs to know which sensor, which value and when — and forgetting
65
+ * a placeholder in the form should not lose that.
66
+ */
67
+ function renderDescription(event, params) {
68
+ const lines = [];
69
+ const note = params.description?.trim();
70
+ if (note)
71
+ lines.push(note, "");
72
+ const sensor = sensorLabel(event);
73
+ if (event.name)
74
+ lines.push(`Alarm: ${event.name}`);
75
+ if (sensor)
76
+ lines.push(`Sensor: ${sensor}`);
77
+ if (event.value !== undefined)
78
+ lines.push(`Wert: ${formatValue(event.value)}`);
79
+ if (event.condition)
80
+ lines.push(`Regel: ${event.condition}`);
81
+ lines.push(`Ausgelöst: ${firedAt(event).toISOString()}`);
82
+ lines.push(`Quelle: ${AlarmTicketTypes_js_1.ALARM_TICKET_ACTION_ID}`);
83
+ return lines.join("\n");
84
+ }
85
+ function firedAt(event) {
86
+ if (event.firedAt) {
87
+ const parsed = new Date(event.firedAt);
88
+ if (!Number.isNaN(parsed.getTime()))
89
+ return parsed;
90
+ }
91
+ return new Date();
92
+ }
93
+ function sensorLabel(event) {
94
+ const sensor = [event.item_source, event.item_id].filter(Boolean).join("/");
95
+ return event.item_dimension != null && sensor
96
+ ? `${sensor} [${event.item_dimension}]`
97
+ : sensor;
98
+ }
99
+ function formatValue(value) {
100
+ if (value === undefined || value === null)
101
+ return "";
102
+ return String(value);
103
+ }
@@ -0,0 +1,7 @@
1
+ export { alarmKey, alarmOrigin, alarmTicketFields, firedAt, } from "./functions/alarmTicketFields.js";
2
+ export type { AlarmTicketFields } from "./functions/alarmTicketFields.js";
3
+ export { createAlarmTicket, tenantForSecret, } from "./service/createAlarmTicket.js";
4
+ export type { AlarmTicketResult } from "./service/createAlarmTicket.js";
5
+ export { ensureAlarmTicketAction } from "./service/ensureAlarmTicketAction.js";
6
+ export { ALARM_TICKET_ACTION_ID, ALARM_TICKET_SECRET_CONFIG_KEY, } from "./types/AlarmTicketTypes.js";
7
+ export type { AlarmEvent, AlarmTicketParams, } from "./types/AlarmTicketTypes.js";
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ALARM_TICKET_SECRET_CONFIG_KEY = exports.ALARM_TICKET_ACTION_ID = exports.ensureAlarmTicketAction = exports.tenantForSecret = exports.createAlarmTicket = exports.firedAt = exports.alarmTicketFields = exports.alarmOrigin = exports.alarmKey = void 0;
4
+ var alarmTicketFields_js_1 = require("./functions/alarmTicketFields.js");
5
+ Object.defineProperty(exports, "alarmKey", { enumerable: true, get: function () { return alarmTicketFields_js_1.alarmKey; } });
6
+ Object.defineProperty(exports, "alarmOrigin", { enumerable: true, get: function () { return alarmTicketFields_js_1.alarmOrigin; } });
7
+ Object.defineProperty(exports, "alarmTicketFields", { enumerable: true, get: function () { return alarmTicketFields_js_1.alarmTicketFields; } });
8
+ Object.defineProperty(exports, "firedAt", { enumerable: true, get: function () { return alarmTicketFields_js_1.firedAt; } });
9
+ var createAlarmTicket_js_1 = require("./service/createAlarmTicket.js");
10
+ Object.defineProperty(exports, "createAlarmTicket", { enumerable: true, get: function () { return createAlarmTicket_js_1.createAlarmTicket; } });
11
+ Object.defineProperty(exports, "tenantForSecret", { enumerable: true, get: function () { return createAlarmTicket_js_1.tenantForSecret; } });
12
+ var ensureAlarmTicketAction_js_1 = require("./service/ensureAlarmTicketAction.js");
13
+ Object.defineProperty(exports, "ensureAlarmTicketAction", { enumerable: true, get: function () { return ensureAlarmTicketAction_js_1.ensureAlarmTicketAction; } });
14
+ var AlarmTicketTypes_js_1 = require("./types/AlarmTicketTypes.js");
15
+ Object.defineProperty(exports, "ALARM_TICKET_ACTION_ID", { enumerable: true, get: function () { return AlarmTicketTypes_js_1.ALARM_TICKET_ACTION_ID; } });
16
+ Object.defineProperty(exports, "ALARM_TICKET_SECRET_CONFIG_KEY", { enumerable: true, get: function () { return AlarmTicketTypes_js_1.ALARM_TICKET_SECRET_CONFIG_KEY; } });
@@ -0,0 +1,33 @@
1
+ import { Tenant } from "../../../../../types/index.js";
2
+ import { type AlarmEvent, type AlarmTicketParams } from "../types/AlarmTicketTypes.js";
3
+ /** Outcome of one fired alarm. */
4
+ export interface AlarmTicketResult {
5
+ ticketId: string;
6
+ /** `false` when an open ticket for this alarm already existed. */
7
+ created: boolean;
8
+ origin: string;
9
+ equipmentId?: string;
10
+ }
11
+ /**
12
+ * Create the ticket a fired alarm asks for — or return the one that is already
13
+ * open for it.
14
+ *
15
+ * **One open ticket per alarm** is the default (`params.duplicate` opts out).
16
+ * A sensor that crosses its threshold every few minutes would otherwise bury
17
+ * the inbox, and the person on shift needs one work item, not a log. Closing
18
+ * the ticket arms the alarm again, which is the behaviour people expect from
19
+ * every other alerting tool.
20
+ *
21
+ * Deliberately thin: number, ACL, assignment notifications and the tenant's
22
+ * watcher rules all happen in the `Service_Ticket` hooks, on every write path.
23
+ */
24
+ export declare function createAlarmTicket(tenant: Tenant, event: AlarmEvent, params?: AlarmTicketParams): Promise<AlarmTicketResult>;
25
+ /**
26
+ * The tenant a call belongs to, identified by its shared secret.
27
+ *
28
+ * The executor lives outside this server and is not trusted to name a tenant —
29
+ * so the secret does it. One `OD3_Config` row per tenant
30
+ * (`OPENSERVICE_ALARM_TICKET_SECRET`); no row means the tenant has not switched
31
+ * the endpoint on, and the call is refused.
32
+ */
33
+ export declare function tenantForSecret(secret: string): Promise<Tenant | undefined>;
@@ -0,0 +1,179 @@
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.createAlarmTicket = createAlarmTicket;
7
+ exports.tenantForSecret = tenantForSecret;
8
+ const node_1 = __importDefault(require("parse/node"));
9
+ const index_js_1 = require("../../../../../types/index.js");
10
+ const alarmTicketFields_js_1 = require("../functions/alarmTicketFields.js");
11
+ const AlarmTicketTypes_js_1 = require("../types/AlarmTicketTypes.js");
12
+ /**
13
+ * Create the ticket a fired alarm asks for — or return the one that is already
14
+ * open for it.
15
+ *
16
+ * **One open ticket per alarm** is the default (`params.duplicate` opts out).
17
+ * A sensor that crosses its threshold every few minutes would otherwise bury
18
+ * the inbox, and the person on shift needs one work item, not a log. Closing
19
+ * the ticket arms the alarm again, which is the behaviour people expect from
20
+ * every other alerting tool.
21
+ *
22
+ * Deliberately thin: number, ACL, assignment notifications and the tenant's
23
+ * watcher rules all happen in the `Service_Ticket` hooks, on every write path.
24
+ */
25
+ async function createAlarmTicket(tenant, event, params = {}) {
26
+ if (!(0, alarmTicketFields_js_1.alarmKey)(event)) {
27
+ throw new node_1.default.Error(node_1.default.Error.INVALID_QUERY, "The alarm payload identifies no alarm: pass alarmId, or item_source/item_id.");
28
+ }
29
+ const fields = (0, alarmTicketFields_js_1.alarmTicketFields)(event, params);
30
+ if (!params.duplicate) {
31
+ const open = await findOpenTicket(tenant, fields.origin);
32
+ if (open) {
33
+ return {
34
+ ticketId: open.id,
35
+ created: false,
36
+ origin: fields.origin,
37
+ equipmentId: open.get("equipment")?.id,
38
+ };
39
+ }
40
+ }
41
+ const status = await resolveDefaultStatus(tenant);
42
+ if (!status) {
43
+ throw new node_1.default.Error(node_1.default.Error.INVALID_QUERY, `No "${AlarmTicketTypes_js_1.DEFAULT_STATUS_CATEGORY}" status configured for this tenant — cannot create a ticket.`);
44
+ }
45
+ const equipment = await resolveEquipment(tenant, event, params);
46
+ const ticket = new index_js_1.Service_Ticket();
47
+ ticket.set("deleted", false);
48
+ ticket.set("tenant", tenant);
49
+ ticket.set("status", status);
50
+ ticket.set("title", fields.title);
51
+ ticket.set("description", fields.description);
52
+ ticket.set("origin", fields.origin);
53
+ // The moment the alarm fired, not the moment we got the call — a queued or
54
+ // retried webhook must not move the incident.
55
+ ticket.set("startDate", (0, alarmTicketFields_js_1.firedAt)(event));
56
+ if (equipment)
57
+ ticket.set("equipment", equipment);
58
+ if (params.priority != null)
59
+ ticket.set("priority", params.priority);
60
+ if (params.projectId) {
61
+ const project = new index_js_1.Service_Project();
62
+ project.id = params.projectId;
63
+ ticket.set("project", project);
64
+ }
65
+ // Assignment arrays hold plain ids in this schema (see the `pointerIds`
66
+ // helper in the ticket hook, which accepts both).
67
+ if (params.assignedUserIds?.length) {
68
+ ticket.set("assignedUsers", params.assignedUserIds);
69
+ }
70
+ if (params.assignedRoleIds?.length) {
71
+ ticket.set("assignedRoles", params.assignedRoleIds);
72
+ }
73
+ if (params.tagIds?.length)
74
+ ticket.set("tags", params.tagIds);
75
+ await ticket.save(null, { useMasterKey: true });
76
+ return {
77
+ ticketId: ticket.id,
78
+ created: true,
79
+ origin: fields.origin,
80
+ equipmentId: equipment?.id,
81
+ };
82
+ }
83
+ /**
84
+ * An unfinished ticket of this alarm, if there is one.
85
+ *
86
+ * "Unfinished" is decided by the **status category**, not by `closedAt`: a
87
+ * tenant can name its statuses whatever it likes, the category is the contract
88
+ * (`done` / `cancelled` — the same list core uses).
89
+ */
90
+ async function findOpenTicket(tenant, origin) {
91
+ const tickets = await new node_1.default.Query(index_js_1.Service_Ticket)
92
+ .equalTo("tenant", tenant)
93
+ .equalTo("origin", origin)
94
+ .equalTo("deleted", false)
95
+ .include("status")
96
+ .descending("createdAt")
97
+ .limit(20)
98
+ .find({ useMasterKey: true });
99
+ return tickets.find((ticket) => {
100
+ const category = ticket.get("status")?.get("category");
101
+ return !AlarmTicketTypes_js_1.DONE_STATUS_CATEGORIES.includes(category);
102
+ });
103
+ }
104
+ /** The tenant's inbox status, else a global one — never another tenant's. */
105
+ async function resolveDefaultStatus(tenant) {
106
+ const statuses = await new node_1.default.Query(index_js_1.Service_Status)
107
+ .equalTo("category", AlarmTicketTypes_js_1.DEFAULT_STATUS_CATEGORY)
108
+ .equalTo("deleted", false)
109
+ .ascending("order")
110
+ .find({ useMasterKey: true });
111
+ return (statuses.find((status) => status.get("tenant")?.id === tenant.id) ?? statuses.find((status) => !status.get("tenant")));
112
+ }
113
+ /**
114
+ * The machine the ticket belongs to.
115
+ *
116
+ * 1. what was configured on the alarm — the only reliable source, because
117
+ * **nothing in the data model links a sensor to a machine**;
118
+ * 2. otherwise a machine whose `sourceTags` name the sensor or its source.
119
+ * That is a convention, not a relation, so it is a fallback: `sourceTags`
120
+ * was meant for source scoping, and a site-level tag can match several
121
+ * machines — the first one wins, which is a guess.
122
+ *
123
+ * No machine is not an error: `equipment` is optional on a ticket, and an
124
+ * unassigned alarm ticket in the inbox is better than one on the wrong machine.
125
+ */
126
+ async function resolveEquipment(tenant, event, params) {
127
+ if (params.equipmentId) {
128
+ const explicit = await new node_1.default.Query(index_js_1.Service_Equipment)
129
+ .equalTo("objectId", params.equipmentId)
130
+ .first({ useMasterKey: true });
131
+ if (explicit)
132
+ return explicit;
133
+ console.warn(`[AlarmTicket] Configured machine ${params.equipmentId} does not exist`);
134
+ }
135
+ const candidates = [event.item_id, event.item_source].filter((value) => !!value);
136
+ if (candidates.length === 0)
137
+ return undefined;
138
+ const query = new node_1.default.Query(index_js_1.Service_Equipment)
139
+ .equalTo("tenant", tenant)
140
+ .notEqualTo("deleted", true)
141
+ // `sourceTags` is an Array column: Mongo reads "contained in" as "the array
142
+ // holds one of these". The generated type only knows `any[]`, hence the cast.
143
+ .containedIn("sourceTags", candidates);
144
+ return (await query.first({ useMasterKey: true })) ?? undefined;
145
+ }
146
+ /**
147
+ * The tenant a call belongs to, identified by its shared secret.
148
+ *
149
+ * The executor lives outside this server and is not trusted to name a tenant —
150
+ * so the secret does it. One `OD3_Config` row per tenant
151
+ * (`OPENSERVICE_ALARM_TICKET_SECRET`); no row means the tenant has not switched
152
+ * the endpoint on, and the call is refused.
153
+ */
154
+ async function tenantForSecret(secret) {
155
+ if (!secret)
156
+ return undefined;
157
+ const configs = await new node_1.default.Query(index_js_1.Config)
158
+ .equalTo("key", AlarmTicketTypes_js_1.ALARM_TICKET_SECRET_CONFIG_KEY)
159
+ .include("tenant")
160
+ .find({ useMasterKey: true });
161
+ const match = configs.find((config) => {
162
+ const value = config.get("value");
163
+ return typeof value === "string" && constantTimeEqual(value.trim(), secret);
164
+ });
165
+ return match?.get("tenant") ?? undefined;
166
+ }
167
+ /**
168
+ * Compare without leaking the answer through timing — the caller controls one
169
+ * side of this comparison and may try many.
170
+ */
171
+ function constantTimeEqual(a, b) {
172
+ if (a.length !== b.length)
173
+ return false;
174
+ let diff = 0;
175
+ for (let i = 0; i < a.length; i++) {
176
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
177
+ }
178
+ return diff === 0;
179
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Make "create a ticket" selectable in the alarm dialog.
3
+ *
4
+ * Alarm actions are **data**, not code: `OD3_AlarmAction` rows that the
5
+ * monitoring adapter loads into the frontend, where the chosen row's
6
+ * `formFields` are rendered as the action's settings form (see
7
+ * `plugins/monitoring/src/components/AlarmCreate.tsx`). There is no plugin-side
8
+ * `registerAlarmAction`, so the row is seeded here — find-or-create by `topic`,
9
+ * like the other open.SERVICE bootstraps.
10
+ *
11
+ * The row is **global** (no tenant): the action makes sense for every tenant,
12
+ * and the tenant of a fired alarm is established by its secret, not by this
13
+ * row.
14
+ *
15
+ * ⚠️ `type` / `target` / `payload` are the executor's half of the contract —
16
+ * the component that evaluates alarms lives outside this server. The values
17
+ * below assume a webhook-style executor that POSTs `payload` (placeholders
18
+ * filled from the alarm) to `target`. If the executor expects something else,
19
+ * this row is where it is corrected; the receiving endpoint accepts any caller
20
+ * that brings the secret.
21
+ */
22
+ export declare function ensureAlarmTicketAction(): Promise<void>;