@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.
- package/dist/features/openservice/init.js +4 -0
- package/dist/features/openservice/schedules/calendarSync/types/Event.d.ts +2 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/eventToPlan.d.ts +50 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/eventToPlan.js +115 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.d.ts +39 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.js +310 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/planToEvent.d.ts +27 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/planToEvent.js +145 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/index.d.ts +8 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/index.js +28 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/ServiceCalendarSyncPoller.d.ts +54 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/ServiceCalendarSyncPoller.js +248 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/serviceCalendarMeta.d.ts +55 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/serviceCalendarMeta.js +109 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/syncPlanToCalendar.d.ts +26 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/service/syncPlanToCalendar.js +114 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/types/ServiceSyncTypes.d.ts +55 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/types/ServiceSyncTypes.js +8 -0
- package/dist/features/openservice/serviceSchedules/initServiceSchedulesFeature.d.ts +10 -0
- package/dist/features/openservice/serviceSchedules/initServiceSchedulesFeature.js +39 -0
- package/dist/features/openservice/serviceTickets/alarmActions/functions/alarmTicketFields.d.ts +35 -0
- package/dist/features/openservice/serviceTickets/alarmActions/functions/alarmTicketFields.js +103 -0
- package/dist/features/openservice/serviceTickets/alarmActions/index.d.ts +7 -0
- package/dist/features/openservice/serviceTickets/alarmActions/index.js +16 -0
- package/dist/features/openservice/serviceTickets/alarmActions/service/createAlarmTicket.d.ts +33 -0
- package/dist/features/openservice/serviceTickets/alarmActions/service/createAlarmTicket.js +179 -0
- package/dist/features/openservice/serviceTickets/alarmActions/service/ensureAlarmTicketAction.d.ts +22 -0
- package/dist/features/openservice/serviceTickets/alarmActions/service/ensureAlarmTicketAction.js +127 -0
- package/dist/features/openservice/serviceTickets/alarmActions/types/AlarmTicketTypes.d.ts +80 -0
- package/dist/features/openservice/serviceTickets/alarmActions/types/AlarmTicketTypes.js +28 -0
- package/dist/functions/openinc-openservice-alarm-create-ticket.d.ts +25 -0
- package/dist/functions/openinc-openservice-alarm-create-ticket.js +63 -0
- package/dist/functions/openinc-openservice-notify-contacts.d.ts +16 -0
- package/dist/functions/openinc-openservice-notify-contacts.js +82 -0
- package/dist/functions/openinc-openservice-schedule-calendar-sync.d.ts +15 -0
- package/dist/functions/openinc-openservice-schedule-calendar-sync.js +60 -0
- package/dist/hooks/Service_Schedule.js +73 -5
- package/dist/types/Service_Schedule.d.ts +6 -0
- package/dist/types/Service_Schedule.js +12 -0
- package/dist/types/Service_Schedule_Template.d.ts +3 -0
- package/dist/types/Service_Schedule_Template.js +6 -0
- package/package.json +1 -1
- package/schema/Service_Schedule.json +9 -0
- package/schema/Service_Schedule_Template.json +5 -0
package/dist/features/openservice/serviceTickets/alarmActions/service/ensureAlarmTicketAction.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
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.ensureAlarmTicketAction = ensureAlarmTicketAction;
|
|
7
|
+
const node_1 = __importDefault(require("parse/node"));
|
|
8
|
+
const index_js_1 = require("../../../../../types/index.js");
|
|
9
|
+
const AlarmTicketTypes_js_1 = require("../types/AlarmTicketTypes.js");
|
|
10
|
+
/**
|
|
11
|
+
* Make "create a ticket" selectable in the alarm dialog.
|
|
12
|
+
*
|
|
13
|
+
* Alarm actions are **data**, not code: `OD3_AlarmAction` rows that the
|
|
14
|
+
* monitoring adapter loads into the frontend, where the chosen row's
|
|
15
|
+
* `formFields` are rendered as the action's settings form (see
|
|
16
|
+
* `plugins/monitoring/src/components/AlarmCreate.tsx`). There is no plugin-side
|
|
17
|
+
* `registerAlarmAction`, so the row is seeded here — find-or-create by `topic`,
|
|
18
|
+
* like the other open.SERVICE bootstraps.
|
|
19
|
+
*
|
|
20
|
+
* The row is **global** (no tenant): the action makes sense for every tenant,
|
|
21
|
+
* and the tenant of a fired alarm is established by its secret, not by this
|
|
22
|
+
* row.
|
|
23
|
+
*
|
|
24
|
+
* ⚠️ `type` / `target` / `payload` are the executor's half of the contract —
|
|
25
|
+
* the component that evaluates alarms lives outside this server. The values
|
|
26
|
+
* below assume a webhook-style executor that POSTs `payload` (placeholders
|
|
27
|
+
* filled from the alarm) to `target`. If the executor expects something else,
|
|
28
|
+
* this row is where it is corrected; the receiving endpoint accepts any caller
|
|
29
|
+
* that brings the secret.
|
|
30
|
+
*/
|
|
31
|
+
async function ensureAlarmTicketAction() {
|
|
32
|
+
const existing = await new node_1.default.Query(index_js_1.AlarmAction)
|
|
33
|
+
.equalTo("topic", AlarmTicketTypes_js_1.ALARM_TICKET_ACTION_ID)
|
|
34
|
+
.first({ useMasterKey: true });
|
|
35
|
+
if (existing)
|
|
36
|
+
return;
|
|
37
|
+
const action = new index_js_1.AlarmAction();
|
|
38
|
+
action.set("label", "Ticket erstellen (open.SERVICE)");
|
|
39
|
+
action.set("type", "webhook");
|
|
40
|
+
action.set("topic", AlarmTicketTypes_js_1.ALARM_TICKET_ACTION_ID);
|
|
41
|
+
action.set("target", "/parse/functions/openinc-openservice-alarm-create-ticket");
|
|
42
|
+
action.set("templateType", "json");
|
|
43
|
+
action.set("payload", JSON.stringify({
|
|
44
|
+
secret: "{{secret}}",
|
|
45
|
+
event: {
|
|
46
|
+
alarmId: "{{alarm.id}}",
|
|
47
|
+
name: "{{alarm.name}}",
|
|
48
|
+
item_id: "{{item.id}}",
|
|
49
|
+
item_source: "{{item.source}}",
|
|
50
|
+
item_dimension: "{{item.dimension}}",
|
|
51
|
+
value: "{{value}}",
|
|
52
|
+
condition: "{{condition}}",
|
|
53
|
+
firedAt: "{{timestamp}}",
|
|
54
|
+
},
|
|
55
|
+
params: "{{action}}",
|
|
56
|
+
}));
|
|
57
|
+
// Every value type can breach a rule, so every type may raise a ticket.
|
|
58
|
+
action.set("supportedTypes", [
|
|
59
|
+
"Number",
|
|
60
|
+
"String",
|
|
61
|
+
"Boolean",
|
|
62
|
+
"Geo",
|
|
63
|
+
"Object",
|
|
64
|
+
]);
|
|
65
|
+
action.set("formFields", FORM_FIELDS);
|
|
66
|
+
await action.save(null, { useMasterKey: true });
|
|
67
|
+
console.log(`[AlarmTicket] Registered alarm action "${AlarmTicketTypes_js_1.ALARM_TICKET_ACTION_ID}"`);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* What the person configuring an alarm gets to fill in.
|
|
71
|
+
*
|
|
72
|
+
* Field types are the ones `FormGenerator` (in `@opendash/core`) understands;
|
|
73
|
+
* `select.parse` renders an object picker for a Parse class. Keys match
|
|
74
|
+
* `AlarmTicketParams` — the executor hands the form's values back to us as
|
|
75
|
+
* `params`.
|
|
76
|
+
*
|
|
77
|
+
* The machine is a plain picker rather than something derived from the sensor,
|
|
78
|
+
* because nothing in the data model links the two. Whoever sets the alarm up
|
|
79
|
+
* knows which machine it watches; guessing it later is worse than asking now.
|
|
80
|
+
*/
|
|
81
|
+
const FORM_FIELDS = [
|
|
82
|
+
{
|
|
83
|
+
key: "title",
|
|
84
|
+
label: "Titel des Tickets",
|
|
85
|
+
type: "input",
|
|
86
|
+
hint: "Platzhalter: {{alarm}}, {{sensor}}, {{value}}. Leer = Name des Alarms.",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
key: "description",
|
|
90
|
+
label: "Notiz",
|
|
91
|
+
type: "textarea",
|
|
92
|
+
hint: "Wird dem Ticket vorangestellt; Sensor, Wert, Regel und Zeitpunkt kommen automatisch dazu.",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: "equipmentId",
|
|
96
|
+
label: "Maschine",
|
|
97
|
+
type: "select.parse",
|
|
98
|
+
settings: {
|
|
99
|
+
className: "OD3_Service_Equipment",
|
|
100
|
+
displayField: "label",
|
|
101
|
+
mode: "single",
|
|
102
|
+
},
|
|
103
|
+
hint: "Ohne Angabe wird über die Quellen-Tags der Maschine geraten — oder das Ticket bleibt ohne Maschine.",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
key: "projectId",
|
|
107
|
+
label: "Projekt",
|
|
108
|
+
type: "select.parse",
|
|
109
|
+
settings: {
|
|
110
|
+
className: "OD3_Service_Project",
|
|
111
|
+
displayField: "label",
|
|
112
|
+
mode: "single",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
key: "priority",
|
|
117
|
+
label: "Priorität",
|
|
118
|
+
type: "input.number",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
key: "duplicate",
|
|
122
|
+
label: "Bei jedem Auslösen ein neues Ticket",
|
|
123
|
+
type: "switch",
|
|
124
|
+
defaultValue: false,
|
|
125
|
+
hint: "Aus (Standard): solange das Ticket offen ist, entsteht kein zweites.",
|
|
126
|
+
},
|
|
127
|
+
];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The alarm action "create a ticket".
|
|
3
|
+
*
|
|
4
|
+
* An open.DASH alarm watches a sensor value against a rule; when the rule
|
|
5
|
+
* breaks, its configured **action** runs. The actions are rows in
|
|
6
|
+
* `OD3_AlarmAction` (loaded into the frontend by the monitoring adapter), and
|
|
7
|
+
* the executor — the component that evaluates alarms, outside this server —
|
|
8
|
+
* carries them out. This module is the receiving end: it turns one fired alarm
|
|
9
|
+
* into an `OD3_Service_Ticket`.
|
|
10
|
+
*/
|
|
11
|
+
/** Id of the action row, and the marker in a ticket's `origin`. */
|
|
12
|
+
export declare const ALARM_TICKET_ACTION_ID = "openservice-create-ticket";
|
|
13
|
+
/**
|
|
14
|
+
* Config key of the shared secret an executor authenticates with.
|
|
15
|
+
*
|
|
16
|
+
* One row per tenant: `key` = this, `value` = the secret, `tenant` = the
|
|
17
|
+
* tenant's pointer. The secret is what tells us **which tenant** a call belongs
|
|
18
|
+
* to — an alarm executor is not trusted to name it. Without a row, the endpoint
|
|
19
|
+
* only answers to the master key.
|
|
20
|
+
*/
|
|
21
|
+
export declare const ALARM_TICKET_SECRET_CONFIG_KEY = "OPENSERVICE_ALARM_TICKET_SECRET";
|
|
22
|
+
/** Status category a fresh ticket lands in — mirrors the ticket plugin's port. */
|
|
23
|
+
export declare const DEFAULT_STATUS_CATEGORY = "inbox";
|
|
24
|
+
/** Categories in which a ticket counts as finished (mirrors core's `DONE_CATEGORIES`). */
|
|
25
|
+
export declare const DONE_STATUS_CATEGORIES: string[];
|
|
26
|
+
/**
|
|
27
|
+
* What the executor reports about the alarm that fired.
|
|
28
|
+
*
|
|
29
|
+
* Field names follow `OD3_Alarm` so a payload can be assembled from the alarm
|
|
30
|
+
* row without renaming: `item_id` / `item_source` / `item_dimension` identify
|
|
31
|
+
* the sensor, the rest is context for the ticket text. Everything is optional
|
|
32
|
+
* except that *something* must identify the alarm — see `alarmKey`.
|
|
33
|
+
*/
|
|
34
|
+
export interface AlarmEvent {
|
|
35
|
+
/** `OD3_Alarm.objectId`, if the executor knows it. */
|
|
36
|
+
alarmId?: string;
|
|
37
|
+
/** The alarm's name, used in the ticket title when no template is given. */
|
|
38
|
+
name?: string;
|
|
39
|
+
/** Sensor id, e.g. `cnc04.spindle`. */
|
|
40
|
+
item_id?: string;
|
|
41
|
+
/** Source (site/tenant hierarchy) the sensor belongs to. */
|
|
42
|
+
item_source?: string;
|
|
43
|
+
/** Which dimension of the sensor tripped. */
|
|
44
|
+
item_dimension?: number;
|
|
45
|
+
/** The value that broke the rule. */
|
|
46
|
+
value?: number | string | boolean;
|
|
47
|
+
/** Human-readable rule, e.g. `> 80`. */
|
|
48
|
+
condition?: string;
|
|
49
|
+
/** When it fired (ISO). Defaults to now. */
|
|
50
|
+
firedAt?: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* What the person configuring the alarm filled into the action's form
|
|
54
|
+
* (`OD3_AlarmAction.formFields`).
|
|
55
|
+
*/
|
|
56
|
+
export interface AlarmTicketParams {
|
|
57
|
+
/** Ticket title; `{{alarm}}`, `{{sensor}}` and `{{value}}` are replaced. */
|
|
58
|
+
title?: string;
|
|
59
|
+
/** Prepended to the generated alarm details. */
|
|
60
|
+
description?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Machine the ticket belongs to.
|
|
63
|
+
*
|
|
64
|
+
* Set explicitly when configuring the alarm — that is the reliable way,
|
|
65
|
+
* because nothing in the data model links a sensor to a machine. Without it,
|
|
66
|
+
* `Equipment.sourceTags` is tried (see `resolveEquipment`).
|
|
67
|
+
*/
|
|
68
|
+
equipmentId?: string;
|
|
69
|
+
projectId?: string;
|
|
70
|
+
priority?: number;
|
|
71
|
+
assignedUserIds?: string[];
|
|
72
|
+
assignedRoleIds?: string[];
|
|
73
|
+
tagIds?: string[];
|
|
74
|
+
/**
|
|
75
|
+
* Reopen behaviour. `false` (default) keeps **one open ticket per alarm**: a
|
|
76
|
+
* flapping sensor does not produce a hundred tickets, and closing the ticket
|
|
77
|
+
* arms the alarm again. `true` creates one per firing.
|
|
78
|
+
*/
|
|
79
|
+
duplicate?: boolean;
|
|
80
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The alarm action "create a ticket".
|
|
4
|
+
*
|
|
5
|
+
* An open.DASH alarm watches a sensor value against a rule; when the rule
|
|
6
|
+
* breaks, its configured **action** runs. The actions are rows in
|
|
7
|
+
* `OD3_AlarmAction` (loaded into the frontend by the monitoring adapter), and
|
|
8
|
+
* the executor — the component that evaluates alarms, outside this server —
|
|
9
|
+
* carries them out. This module is the receiving end: it turns one fired alarm
|
|
10
|
+
* into an `OD3_Service_Ticket`.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.DONE_STATUS_CATEGORIES = exports.DEFAULT_STATUS_CATEGORY = exports.ALARM_TICKET_SECRET_CONFIG_KEY = exports.ALARM_TICKET_ACTION_ID = void 0;
|
|
14
|
+
/** Id of the action row, and the marker in a ticket's `origin`. */
|
|
15
|
+
exports.ALARM_TICKET_ACTION_ID = "openservice-create-ticket";
|
|
16
|
+
/**
|
|
17
|
+
* Config key of the shared secret an executor authenticates with.
|
|
18
|
+
*
|
|
19
|
+
* One row per tenant: `key` = this, `value` = the secret, `tenant` = the
|
|
20
|
+
* tenant's pointer. The secret is what tells us **which tenant** a call belongs
|
|
21
|
+
* to — an alarm executor is not trusted to name it. Without a row, the endpoint
|
|
22
|
+
* only answers to the master key.
|
|
23
|
+
*/
|
|
24
|
+
exports.ALARM_TICKET_SECRET_CONFIG_KEY = "OPENSERVICE_ALARM_TICKET_SECRET";
|
|
25
|
+
/** Status category a fresh ticket lands in — mirrors the ticket plugin's port. */
|
|
26
|
+
exports.DEFAULT_STATUS_CATEGORY = "inbox";
|
|
27
|
+
/** Categories in which a ticket counts as finished (mirrors core's `DONE_CATEGORIES`). */
|
|
28
|
+
exports.DONE_STATUS_CATEGORIES = ["done", "cancelled"];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raise an open.SERVICE ticket for a fired alarm.
|
|
3
|
+
*
|
|
4
|
+
* The receiving end of the `openservice-create-ticket` alarm action: an alarm
|
|
5
|
+
* watches a sensor, the rule breaks, and whatever executes alarm actions calls
|
|
6
|
+
* this with the alarm's data and the settings someone filled into the action's
|
|
7
|
+
* form.
|
|
8
|
+
*
|
|
9
|
+
* **Authentication** — one of:
|
|
10
|
+
*
|
|
11
|
+
* - the **master key** (server-to-server inside the stack), then `tenantId`
|
|
12
|
+
* must be given;
|
|
13
|
+
* - a **shared secret** matching an `OPENSERVICE_ALARM_TICKET_SECRET` config
|
|
14
|
+
* row, which also establishes the tenant. An external executor is not
|
|
15
|
+
* trusted to name a tenant, so the secret does it.
|
|
16
|
+
*
|
|
17
|
+
* A logged-in user is deliberately *not* enough: this is a machine endpoint,
|
|
18
|
+
* and a browser session should not be able to fabricate alarm tickets.
|
|
19
|
+
*
|
|
20
|
+
* Params: `{ event, params?, secret?, tenantId? }`.
|
|
21
|
+
* Returns `{ ticketId, created, origin, equipmentId? }` — `created: false`
|
|
22
|
+
* means an open ticket for this alarm already existed (see
|
|
23
|
+
* `createAlarmTicket`).
|
|
24
|
+
*/
|
|
25
|
+
export declare function init(name: string): Promise<void>;
|
|
@@ -0,0 +1,63 @@
|
|
|
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.init = init;
|
|
7
|
+
const node_1 = __importDefault(require("parse/node"));
|
|
8
|
+
const index_js_1 = require("../features/openservice/serviceTickets/alarmActions/index.js");
|
|
9
|
+
const index_js_2 = require("../types/index.js");
|
|
10
|
+
/**
|
|
11
|
+
* Raise an open.SERVICE ticket for a fired alarm.
|
|
12
|
+
*
|
|
13
|
+
* The receiving end of the `openservice-create-ticket` alarm action: an alarm
|
|
14
|
+
* watches a sensor, the rule breaks, and whatever executes alarm actions calls
|
|
15
|
+
* this with the alarm's data and the settings someone filled into the action's
|
|
16
|
+
* form.
|
|
17
|
+
*
|
|
18
|
+
* **Authentication** — one of:
|
|
19
|
+
*
|
|
20
|
+
* - the **master key** (server-to-server inside the stack), then `tenantId`
|
|
21
|
+
* must be given;
|
|
22
|
+
* - a **shared secret** matching an `OPENSERVICE_ALARM_TICKET_SECRET` config
|
|
23
|
+
* row, which also establishes the tenant. An external executor is not
|
|
24
|
+
* trusted to name a tenant, so the secret does it.
|
|
25
|
+
*
|
|
26
|
+
* A logged-in user is deliberately *not* enough: this is a machine endpoint,
|
|
27
|
+
* and a browser session should not be able to fabricate alarm tickets.
|
|
28
|
+
*
|
|
29
|
+
* Params: `{ event, params?, secret?, tenantId? }`.
|
|
30
|
+
* Returns `{ ticketId, created, origin, equipmentId? }` — `created: false`
|
|
31
|
+
* means an open ticket for this alarm already existed (see
|
|
32
|
+
* `createAlarmTicket`).
|
|
33
|
+
*/
|
|
34
|
+
async function init(name) {
|
|
35
|
+
node_1.default.Cloud.define(name, async (request) => {
|
|
36
|
+
const { event, params, secret, tenantId } = request.params;
|
|
37
|
+
if (!event || typeof event !== "object") {
|
|
38
|
+
throw new node_1.default.Error(node_1.default.Error.INVALID_QUERY, `${name} needs an "event" object describing the alarm`);
|
|
39
|
+
}
|
|
40
|
+
const tenant = request.master
|
|
41
|
+
? await tenantById(tenantId)
|
|
42
|
+
: await (0, index_js_1.tenantForSecret)(typeof secret === "string" ? secret : "");
|
|
43
|
+
if (!tenant) {
|
|
44
|
+
// Same answer for "wrong secret" and "no such tenant": a caller
|
|
45
|
+
// guessing secrets learns nothing from the difference.
|
|
46
|
+
throw new node_1.default.Error(node_1.default.Error.OPERATION_FORBIDDEN, `${name}: unknown tenant or invalid secret`);
|
|
47
|
+
}
|
|
48
|
+
const result = await (0, index_js_1.createAlarmTicket)(tenant, event, params ?? {});
|
|
49
|
+
console.log(`[${name}] ${result.created ? "created" : "reused"} ticket ` +
|
|
50
|
+
`${result.ticketId} for ${result.origin}` +
|
|
51
|
+
(result.equipmentId ? ` (machine ${result.equipmentId})` : ""));
|
|
52
|
+
return result;
|
|
53
|
+
},
|
|
54
|
+
// No `requireUser`: the callers are machines, authenticated above.
|
|
55
|
+
{});
|
|
56
|
+
}
|
|
57
|
+
async function tenantById(tenantId) {
|
|
58
|
+
if (!tenantId)
|
|
59
|
+
return undefined;
|
|
60
|
+
return ((await new node_1.default.Query(index_js_2.Tenant)
|
|
61
|
+
.equalTo("objectId", tenantId)
|
|
62
|
+
.first({ useMasterKey: true })) ?? undefined);
|
|
63
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mail the external contacts of a maintenance notification rule.
|
|
3
|
+
*
|
|
4
|
+
* **Why this lives here.** The scheduler evaluates the plans' notification
|
|
5
|
+
* rules (`OD3_Service_Schedule.notifications`), but it has no mail transport —
|
|
6
|
+
* that belongs to this server, which owns `Core_Email` and the SMTP config.
|
|
7
|
+
* Users are reached through `OD3_Notification` (the hook there sends mail and
|
|
8
|
+
* web push); contacts have no account, so they are reached directly.
|
|
9
|
+
*
|
|
10
|
+
* **Master key only.** A browser session must not be able to mail arbitrary
|
|
11
|
+
* contacts with arbitrary text.
|
|
12
|
+
*
|
|
13
|
+
* Params: `contactIds`, `subject`, `body`, plus `scheduleId` / `ruleId` /
|
|
14
|
+
* `occurrence` for the log line. Returns `{ sent, skipped, withoutEmail }`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function init(name: string): Promise<void>;
|
|
@@ -0,0 +1,82 @@
|
|
|
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.init = init;
|
|
7
|
+
const node_1 = __importDefault(require("parse/node"));
|
|
8
|
+
const index_js_1 = require("../features/notifications/index.js");
|
|
9
|
+
const index_js_2 = require("../types/index.js");
|
|
10
|
+
/** How far back a duplicate is looked for (see `alreadySent`). */
|
|
11
|
+
const DEDUP_WINDOW_DAYS = 30;
|
|
12
|
+
/**
|
|
13
|
+
* Mail the external contacts of a maintenance notification rule.
|
|
14
|
+
*
|
|
15
|
+
* **Why this lives here.** The scheduler evaluates the plans' notification
|
|
16
|
+
* rules (`OD3_Service_Schedule.notifications`), but it has no mail transport —
|
|
17
|
+
* that belongs to this server, which owns `Core_Email` and the SMTP config.
|
|
18
|
+
* Users are reached through `OD3_Notification` (the hook there sends mail and
|
|
19
|
+
* web push); contacts have no account, so they are reached directly.
|
|
20
|
+
*
|
|
21
|
+
* **Master key only.** A browser session must not be able to mail arbitrary
|
|
22
|
+
* contacts with arbitrary text.
|
|
23
|
+
*
|
|
24
|
+
* Params: `contactIds`, `subject`, `body`, plus `scheduleId` / `ruleId` /
|
|
25
|
+
* `occurrence` for the log line. Returns `{ sent, skipped, withoutEmail }`.
|
|
26
|
+
*/
|
|
27
|
+
async function init(name) {
|
|
28
|
+
node_1.default.Cloud.define(name, async (request) => {
|
|
29
|
+
if (!request.master) {
|
|
30
|
+
throw new node_1.default.Error(node_1.default.Error.OPERATION_FORBIDDEN, `${name} requires the master key`);
|
|
31
|
+
}
|
|
32
|
+
const { contactIds, subject, body, scheduleId, ruleId, occurrence } = request.params;
|
|
33
|
+
if (!contactIds?.length || !subject || !body) {
|
|
34
|
+
throw new node_1.default.Error(node_1.default.Error.INVALID_QUERY, `${name} needs contactIds, subject and body`);
|
|
35
|
+
}
|
|
36
|
+
const contacts = await new node_1.default.Query(index_js_2.Contact)
|
|
37
|
+
.containedIn("objectId", contactIds)
|
|
38
|
+
.find({ useMasterKey: true });
|
|
39
|
+
let sent = 0;
|
|
40
|
+
let skipped = 0;
|
|
41
|
+
let withoutEmail = 0;
|
|
42
|
+
for (const contact of contacts) {
|
|
43
|
+
const to = contact.get("email");
|
|
44
|
+
if (!to) {
|
|
45
|
+
withoutEmail++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (await alreadySent(to, body)) {
|
|
49
|
+
skipped++;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
await (0, index_js_1.sendSimpleEmail)(to, subject, body);
|
|
53
|
+
sent++;
|
|
54
|
+
}
|
|
55
|
+
console.log(`[${name}] plan ${scheduleId} rule ${ruleId} occurrence ${occurrence}: ` +
|
|
56
|
+
`sent ${sent}, skipped ${skipped}, without email ${withoutEmail}`);
|
|
57
|
+
return { sent, skipped, withoutEmail };
|
|
58
|
+
},
|
|
59
|
+
// No `requireUser`: this is a machine-to-machine call from the scheduler.
|
|
60
|
+
{});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether this exact message already went to this address recently.
|
|
64
|
+
*
|
|
65
|
+
* The idempotency key is the **message body**, because it already identifies
|
|
66
|
+
* the occurrence (it carries plan, description and the due date) and because
|
|
67
|
+
* `Core_Email` has no free column to put a marker in. That makes a restart
|
|
68
|
+
* inside the same tick harmless, while a genuinely different occurrence — a
|
|
69
|
+
* different date in the text — still goes out.
|
|
70
|
+
*/
|
|
71
|
+
async function alreadySent(to, body) {
|
|
72
|
+
const since = new Date(Date.now() - DEDUP_WINDOW_DAYS * 24 * 3600 * 1000);
|
|
73
|
+
// Dot access into the `payload` Object column: Mongo resolves it, but the
|
|
74
|
+
// generated class type only knows top-level columns — hence the untyped
|
|
75
|
+
// query handle.
|
|
76
|
+
const query = new node_1.default.Query(index_js_2.Core_Email);
|
|
77
|
+
query.equalTo("payload.to", to);
|
|
78
|
+
query.equalTo("payload.text", body);
|
|
79
|
+
query.greaterThan("createdAt", since);
|
|
80
|
+
const existing = await query.first({ useMasterKey: true });
|
|
81
|
+
return !!existing;
|
|
82
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push maintenance plans into the tenant's Outlook calendar, on request.
|
|
3
|
+
*
|
|
4
|
+
* Normally nobody needs this: saving a plan syncs it (the `Service_Schedule`
|
|
5
|
+
* afterSave hook). It exists for the plans that already existed when a tenant
|
|
6
|
+
* switched the calendar connection on — they have never been written to the
|
|
7
|
+
* calendar, and nothing about them is going to change just to trigger it.
|
|
8
|
+
*
|
|
9
|
+
* **Authorization is the plan's own ACL**: the plans are loaded with the
|
|
10
|
+
* caller's session token, so this can only sync what the caller may read. A
|
|
11
|
+
* plan they cannot see comes back as "not found", like everywhere else.
|
|
12
|
+
*
|
|
13
|
+
* Params: `scheduleIds`. Returns `{ synced, failed }`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function init(name: string): Promise<void>;
|
|
@@ -0,0 +1,60 @@
|
|
|
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.init = init;
|
|
7
|
+
const node_1 = __importDefault(require("parse/node"));
|
|
8
|
+
const index_js_1 = require("../features/openservice/serviceSchedules/calendarSync/index.js");
|
|
9
|
+
const index_js_2 = require("../types/index.js");
|
|
10
|
+
/** No more than this many plans per call — each one is a Graph round trip. */
|
|
11
|
+
const MAX_PLANS = 50;
|
|
12
|
+
/**
|
|
13
|
+
* Push maintenance plans into the tenant's Outlook calendar, on request.
|
|
14
|
+
*
|
|
15
|
+
* Normally nobody needs this: saving a plan syncs it (the `Service_Schedule`
|
|
16
|
+
* afterSave hook). It exists for the plans that already existed when a tenant
|
|
17
|
+
* switched the calendar connection on — they have never been written to the
|
|
18
|
+
* calendar, and nothing about them is going to change just to trigger it.
|
|
19
|
+
*
|
|
20
|
+
* **Authorization is the plan's own ACL**: the plans are loaded with the
|
|
21
|
+
* caller's session token, so this can only sync what the caller may read. A
|
|
22
|
+
* plan they cannot see comes back as "not found", like everywhere else.
|
|
23
|
+
*
|
|
24
|
+
* Params: `scheduleIds`. Returns `{ synced, failed }`.
|
|
25
|
+
*/
|
|
26
|
+
async function init(name) {
|
|
27
|
+
node_1.default.Cloud.define(name, async (request) => {
|
|
28
|
+
const { scheduleIds } = request.params;
|
|
29
|
+
if (!Array.isArray(scheduleIds) || scheduleIds.length === 0) {
|
|
30
|
+
throw new node_1.default.Error(node_1.default.Error.INVALID_QUERY, `${name} needs scheduleIds`);
|
|
31
|
+
}
|
|
32
|
+
if (scheduleIds.length > MAX_PLANS) {
|
|
33
|
+
throw new node_1.default.Error(node_1.default.Error.INVALID_QUERY, `${name} accepts at most ${MAX_PLANS} plans per call`);
|
|
34
|
+
}
|
|
35
|
+
const plans = await new node_1.default.Query(index_js_2.Service_Schedule)
|
|
36
|
+
.containedIn("objectId", scheduleIds)
|
|
37
|
+
.include("equipment")
|
|
38
|
+
.limit(MAX_PLANS)
|
|
39
|
+
.find({ sessionToken: request.user?.getSessionToken() ?? undefined });
|
|
40
|
+
let synced = 0;
|
|
41
|
+
const failed = [];
|
|
42
|
+
for (const plan of plans) {
|
|
43
|
+
try {
|
|
44
|
+
// Master key from here on: writing the calendar and the meta entry is
|
|
45
|
+
// the server's business, not something the caller needs rights for.
|
|
46
|
+
await (0, index_js_1.syncPlanToCalendar)(plan);
|
|
47
|
+
synced++;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.error(`[${name}] Plan ${plan.id} failed:`, error);
|
|
51
|
+
failed.push(plan.id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
synced,
|
|
56
|
+
failed,
|
|
57
|
+
notFound: scheduleIds.filter((id) => !plans.some((plan) => plan.id === id)),
|
|
58
|
+
};
|
|
59
|
+
}, { requireUser: true });
|
|
60
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
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.init = init;
|
|
7
|
+
const node_1 = __importDefault(require("parse/node"));
|
|
4
8
|
const index_js_1 = require("../features/schema/index.js");
|
|
5
|
-
const index_js_2 = require("../
|
|
9
|
+
const index_js_2 = require("../features/openservice/serviceSchedules/calendarSync/index.js");
|
|
10
|
+
const index_js_3 = require("../types/index.js");
|
|
6
11
|
async function init() {
|
|
7
|
-
(0, index_js_1.beforeSaveHook)(
|
|
12
|
+
(0, index_js_1.beforeSaveHook)(index_js_3.Service_Schedule, async (request) => {
|
|
8
13
|
const { object, original, user } = request;
|
|
9
14
|
await (0, index_js_1.defaultHandler)(request);
|
|
10
15
|
// `allowCustomACL` keeps an ACL set by the client instead of resetting it on
|
|
@@ -17,8 +22,71 @@ async function init() {
|
|
|
17
22
|
await (0, index_js_1.defaultAclHandler)(request, { allowCustomACL: true });
|
|
18
23
|
// TODO
|
|
19
24
|
});
|
|
20
|
-
(0, index_js_1.afterSaveHook)(
|
|
21
|
-
const { object, original,
|
|
22
|
-
//
|
|
25
|
+
(0, index_js_1.afterSaveHook)(index_js_3.Service_Schedule, async (request) => {
|
|
26
|
+
const { object, original, context } = request;
|
|
27
|
+
// A change that came *from* the calendar must not be written back to it:
|
|
28
|
+
// that would create a new version there, which the delta feed reports as a
|
|
29
|
+
// foreign change, which we would import again — an endless echo. The
|
|
30
|
+
// poller marks its own writes with this flag.
|
|
31
|
+
if (context?.[index_js_2.CALENDAR_SYNC_CONTEXT_FLAG])
|
|
32
|
+
return;
|
|
33
|
+
// Seed data belongs to a demo tenant, not in anybody's calendar.
|
|
34
|
+
if (object.get("seed"))
|
|
35
|
+
return;
|
|
36
|
+
if (!original || calendarRelevantChange(object, original)) {
|
|
37
|
+
// Deliberately not awaited: the calendar is a downstream system, and a
|
|
38
|
+
// Graph round trip (token + write) would otherwise be part of every save
|
|
39
|
+
// the user waits for. Failures are logged, and the next relevant save
|
|
40
|
+
// retries — as does the delta poller from the other side.
|
|
41
|
+
(0, index_js_2.syncPlanToCalendar)(object).catch((error) => {
|
|
42
|
+
console.error(`[ServiceCalendarSync] Plan ${object.id} could not be synced:`, error);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
(0, index_js_1.beforeDeleteHook)(index_js_3.Service_Schedule, async (request) => {
|
|
47
|
+
const { object } = request;
|
|
48
|
+
// A protocol points at the plan it documents. Destroying the plan leaves
|
|
49
|
+
// that pointer dangling and the protocol — the record that the work
|
|
50
|
+
// happened — unreachable. Retiring a plan is done by setting `deletedAt`
|
|
51
|
+
// (archiving); the whole frontend already does that, including the admin
|
|
52
|
+
// table, which offers "archive" instead of "delete".
|
|
53
|
+
//
|
|
54
|
+
// A plan without any execution carries no history, so removing it stays
|
|
55
|
+
// allowed — that keeps mistakenly created plans cleanable.
|
|
56
|
+
const executions = await new node_1.default.Query(index_js_3.Service_Schedule_Execution)
|
|
57
|
+
.equalTo("schedule", object)
|
|
58
|
+
.count({ useMasterKey: true });
|
|
59
|
+
if (executions > 0) {
|
|
60
|
+
throw new node_1.default.Error(node_1.default.Error.OPERATION_FORBIDDEN, `Cannot delete maintenance schedule: ${executions} execution(s) document it. Set "deletedAt" to archive it instead.`);
|
|
61
|
+
}
|
|
23
62
|
});
|
|
24
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Whether a saved plan changed anything the calendar shows.
|
|
66
|
+
*
|
|
67
|
+
* Plans are saved for many reasons that mean nothing to Outlook (an execution
|
|
68
|
+
* counter, an ACL, a tag). Only these fields reach the appointment — and
|
|
69
|
+
* `active` / `deletedAt` because retiring a plan removes it from the calendar.
|
|
70
|
+
*/
|
|
71
|
+
function calendarRelevantChange(object, original) {
|
|
72
|
+
if (object.get("title") !== original.get("title") ||
|
|
73
|
+
object.get("active") !== original.get("active") ||
|
|
74
|
+
dateValue(object.get("deletedAt")) !== dateValue(original.get("deletedAt"))) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
const equipmentChanged = object.get("equipment")?.id !==
|
|
78
|
+
original.get("equipment")?.id;
|
|
79
|
+
if (equipmentChanged)
|
|
80
|
+
return true;
|
|
81
|
+
// JSON columns: compared by value, because a save always hands us new object
|
|
82
|
+
// identities even when nothing inside them moved.
|
|
83
|
+
return (!sameJson(object.get("cadence"), original.get("cadence")) ||
|
|
84
|
+
!sameJson(object.get("notifications"), original.get("notifications")) ||
|
|
85
|
+
!sameJson(object.get("steps"), original.get("steps")));
|
|
86
|
+
}
|
|
87
|
+
function dateValue(value) {
|
|
88
|
+
return value ? value.getTime() : undefined;
|
|
89
|
+
}
|
|
90
|
+
function sameJson(a, b) {
|
|
91
|
+
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
92
|
+
}
|
|
@@ -12,6 +12,7 @@ export interface Service_ScheduleAttributes {
|
|
|
12
12
|
assignedRoles: Parse.Relation<Service_Schedule, _Role>;
|
|
13
13
|
cadence: any;
|
|
14
14
|
cadenceExceptions?: any[] | undefined;
|
|
15
|
+
color?: string | undefined;
|
|
15
16
|
coversTasks: any[];
|
|
16
17
|
deletedAt?: Date | undefined;
|
|
17
18
|
equipment?: Service_Equipment | undefined;
|
|
@@ -19,6 +20,7 @@ export interface Service_ScheduleAttributes {
|
|
|
19
20
|
escalationRoles: Parse.Relation<Service_Schedule, _Role>;
|
|
20
21
|
graceDays?: number | undefined;
|
|
21
22
|
leadDays?: number | undefined;
|
|
23
|
+
notifications?: any[] | undefined;
|
|
22
24
|
pausedUntil?: Date | undefined;
|
|
23
25
|
seed?: boolean | undefined;
|
|
24
26
|
serviceProviders?: any[] | undefined;
|
|
@@ -38,6 +40,8 @@ export declare class Service_Schedule extends Parse.Object<Service_ScheduleAttri
|
|
|
38
40
|
set cadence(value: any);
|
|
39
41
|
get cadenceExceptions(): any[] | undefined;
|
|
40
42
|
set cadenceExceptions(value: any[] | undefined);
|
|
43
|
+
get color(): string | undefined;
|
|
44
|
+
set color(value: string | undefined);
|
|
41
45
|
get coversTasks(): any[];
|
|
42
46
|
set coversTasks(value: any[]);
|
|
43
47
|
get deletedAt(): Date | undefined;
|
|
@@ -51,6 +55,8 @@ export declare class Service_Schedule extends Parse.Object<Service_ScheduleAttri
|
|
|
51
55
|
set graceDays(value: number | undefined);
|
|
52
56
|
get leadDays(): number | undefined;
|
|
53
57
|
set leadDays(value: number | undefined);
|
|
58
|
+
get notifications(): any[] | undefined;
|
|
59
|
+
set notifications(value: any[] | undefined);
|
|
54
60
|
get pausedUntil(): Date | undefined;
|
|
55
61
|
set pausedUntil(value: Date | undefined);
|
|
56
62
|
get seed(): boolean | undefined;
|
|
@@ -37,6 +37,12 @@ class Service_Schedule extends node_1.default.Object {
|
|
|
37
37
|
set cadenceExceptions(value) {
|
|
38
38
|
super.set("cadenceExceptions", value);
|
|
39
39
|
}
|
|
40
|
+
get color() {
|
|
41
|
+
return super.get("color");
|
|
42
|
+
}
|
|
43
|
+
set color(value) {
|
|
44
|
+
super.set("color", value);
|
|
45
|
+
}
|
|
40
46
|
get coversTasks() {
|
|
41
47
|
return super.get("coversTasks");
|
|
42
48
|
}
|
|
@@ -76,6 +82,12 @@ class Service_Schedule extends node_1.default.Object {
|
|
|
76
82
|
set leadDays(value) {
|
|
77
83
|
super.set("leadDays", value);
|
|
78
84
|
}
|
|
85
|
+
get notifications() {
|
|
86
|
+
return super.get("notifications");
|
|
87
|
+
}
|
|
88
|
+
set notifications(value) {
|
|
89
|
+
super.set("notifications", value);
|
|
90
|
+
}
|
|
79
91
|
get pausedUntil() {
|
|
80
92
|
return super.get("pausedUntil");
|
|
81
93
|
}
|