@openinc/parse-server-opendash 4.2.0 → 4.2.2
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/serviceSchedules/acl/assignmentAcl.d.ts +30 -0
- package/dist/features/openservice/serviceSchedules/acl/assignmentAcl.js +85 -0
- package/dist/features/openservice/serviceSchedules/acl/index.d.ts +1 -0
- package/dist/features/openservice/serviceSchedules/acl/index.js +6 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.d.ts +20 -0
- package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.js +50 -7
- package/dist/features/openservice/serviceSchedules/calendarSync/service/syncPlanToCalendar.js +8 -0
- package/dist/features/permissions/types/Permissions.d.ts +5 -6
- package/dist/features/permissions/types/Permissions.js +13 -8
- package/dist/hooks/Service_Schedule.js +43 -17
- package/dist/hooks/Service_Schedule_Template.js +28 -7
- package/dist/hooks/Service_Ticket.js +2 -2
- package/dist/types/Service_Schedule.d.ts +9 -3
- package/dist/types/Service_Schedule.js +9 -0
- package/dist/types/Service_Schedule_Execution.d.ts +6 -3
- package/dist/types/Service_Schedule_Execution.js +6 -0
- package/dist/types/Service_Schedule_Template.d.ts +7 -3
- package/dist/types/Service_Schedule_Template.js +6 -0
- package/package.json +5 -4
- package/schema/Service_Schedule.json +8 -0
- package/schema/Service_Schedule_Execution.json +5 -0
- package/schema/Service_Schedule_Template.json +4 -0
- package/dist/features/ruleset/applyRuleSet.d.ts +0 -41
- package/dist/features/ruleset/applyRuleSet.js +0 -81
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marks a save this module made itself, so the `afterSave` that follows it does
|
|
3
|
+
* not start over. Without it every ACL write would trigger another pass — the
|
|
4
|
+
* second one is a no-op and would stop, but the round trip is pure waste.
|
|
5
|
+
*/
|
|
6
|
+
export declare const ASSIGNMENT_ACL_CONTEXT_FLAG = "fromAssignmentAcl";
|
|
7
|
+
/** Which relations grant read access on this class. */
|
|
8
|
+
export interface AssignmentAclOptions {
|
|
9
|
+
/** Relation of `_Role`s that may read the record. */
|
|
10
|
+
roleRelations: string[];
|
|
11
|
+
/** Relation of `_User`s that may read the record. */
|
|
12
|
+
userRelations?: string[];
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Who may see a maintenance plan (or a template): its creator, the roles and
|
|
16
|
+
* users assigned to it, and the administrators — nobody else.
|
|
17
|
+
*
|
|
18
|
+
* **Why this is rebuilt rather than amended.** Un-assigning somebody has to take
|
|
19
|
+
* their access away again, and an ACL carries no record of *why* a grant is
|
|
20
|
+
* there. Adding grants on every save would therefore be a one-way door: a role
|
|
21
|
+
* removed from the plan would keep reading it forever. The ACL is derived from
|
|
22
|
+
* the assignments instead, so it always says exactly what the assignments say.
|
|
23
|
+
*
|
|
24
|
+
* `od-admin` and `od-tenant-admin-<tenant>` are re-added by `defaultAclHandler`
|
|
25
|
+
* on the save that follows, so an assignment can never lock administrators out.
|
|
26
|
+
*
|
|
27
|
+
* Runs in `afterSave`, not `beforeSave`: relations are only queryable once the
|
|
28
|
+
* pending add/remove operations have been applied, which happens on save.
|
|
29
|
+
*/
|
|
30
|
+
export declare function syncAssignmentAcl(object: Parse.Object, options: AssignmentAclOptions): Promise<void>;
|
|
@@ -0,0 +1,85 @@
|
|
|
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.ASSIGNMENT_ACL_CONTEXT_FLAG = void 0;
|
|
7
|
+
exports.syncAssignmentAcl = syncAssignmentAcl;
|
|
8
|
+
const node_1 = __importDefault(require("parse/node"));
|
|
9
|
+
/**
|
|
10
|
+
* Marks a save this module made itself, so the `afterSave` that follows it does
|
|
11
|
+
* not start over. Without it every ACL write would trigger another pass — the
|
|
12
|
+
* second one is a no-op and would stop, but the round trip is pure waste.
|
|
13
|
+
*/
|
|
14
|
+
exports.ASSIGNMENT_ACL_CONTEXT_FLAG = "fromAssignmentAcl";
|
|
15
|
+
/**
|
|
16
|
+
* Who may see a maintenance plan (or a template): its creator, the roles and
|
|
17
|
+
* users assigned to it, and the administrators — nobody else.
|
|
18
|
+
*
|
|
19
|
+
* **Why this is rebuilt rather than amended.** Un-assigning somebody has to take
|
|
20
|
+
* their access away again, and an ACL carries no record of *why* a grant is
|
|
21
|
+
* there. Adding grants on every save would therefore be a one-way door: a role
|
|
22
|
+
* removed from the plan would keep reading it forever. The ACL is derived from
|
|
23
|
+
* the assignments instead, so it always says exactly what the assignments say.
|
|
24
|
+
*
|
|
25
|
+
* `od-admin` and `od-tenant-admin-<tenant>` are re-added by `defaultAclHandler`
|
|
26
|
+
* on the save that follows, so an assignment can never lock administrators out.
|
|
27
|
+
*
|
|
28
|
+
* Runs in `afterSave`, not `beforeSave`: relations are only queryable once the
|
|
29
|
+
* pending add/remove operations have been applied, which happens on save.
|
|
30
|
+
*/
|
|
31
|
+
async function syncAssignmentAcl(object, options) {
|
|
32
|
+
const acl = new node_1.default.ACL();
|
|
33
|
+
// Administrators. Mirrors `defaultAclHandler` so the ACL is already complete
|
|
34
|
+
// when it is written, rather than only after the next save.
|
|
35
|
+
acl.setRoleReadAccess("od-admin", true);
|
|
36
|
+
acl.setRoleWriteAccess("od-admin", true);
|
|
37
|
+
const tenant = object.get("tenant");
|
|
38
|
+
if (tenant) {
|
|
39
|
+
acl.setRoleReadAccess(`od-tenant-admin-${tenant.id}`, true);
|
|
40
|
+
acl.setRoleWriteAccess(`od-tenant-admin-${tenant.id}`, true);
|
|
41
|
+
}
|
|
42
|
+
// The creator keeps write — the record is theirs.
|
|
43
|
+
const creator = object.get("user");
|
|
44
|
+
if (creator?.id) {
|
|
45
|
+
acl.setReadAccess(creator.id, true);
|
|
46
|
+
acl.setWriteAccess(creator.id, true);
|
|
47
|
+
}
|
|
48
|
+
for (const relation of options.roleRelations) {
|
|
49
|
+
for (const role of await readRelation(object, relation)) {
|
|
50
|
+
const name = role.get("name");
|
|
51
|
+
// Roles are addressed by name in an ACL, not by id.
|
|
52
|
+
if (name)
|
|
53
|
+
acl.setRoleReadAccess(name, true);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const relation of options.userRelations ?? []) {
|
|
57
|
+
for (const user of await readRelation(object, relation)) {
|
|
58
|
+
if (user.id)
|
|
59
|
+
acl.setReadAccess(user.id, true);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (sameAcl(object.getACL(), acl))
|
|
63
|
+
return;
|
|
64
|
+
object.setACL(acl);
|
|
65
|
+
await object.save(null, {
|
|
66
|
+
useMasterKey: true,
|
|
67
|
+
context: { [exports.ASSIGNMENT_ACL_CONTEXT_FLAG]: true },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/** The members of a relation, or an empty list when it has none. */
|
|
71
|
+
async function readRelation(object, field) {
|
|
72
|
+
try {
|
|
73
|
+
return await object.relation(field).query().find({ useMasterKey: true });
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// A relation that was never written has no backing table yet.
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Whether two ACLs grant exactly the same thing. */
|
|
81
|
+
function sameAcl(current, next) {
|
|
82
|
+
if (!current)
|
|
83
|
+
return false;
|
|
84
|
+
return JSON.stringify(current.toJSON()) === JSON.stringify(next.toJSON());
|
|
85
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ASSIGNMENT_ACL_CONTEXT_FLAG, syncAssignmentAcl, type AssignmentAclOptions, } from "./assignmentAcl.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.syncAssignmentAcl = exports.ASSIGNMENT_ACL_CONTEXT_FLAG = void 0;
|
|
4
|
+
var assignmentAcl_js_1 = require("./assignmentAcl.js");
|
|
5
|
+
Object.defineProperty(exports, "ASSIGNMENT_ACL_CONTEXT_FLAG", { enumerable: true, get: function () { return assignmentAcl_js_1.ASSIGNMENT_ACL_CONTEXT_FLAG; } });
|
|
6
|
+
Object.defineProperty(exports, "syncAssignmentAcl", { enumerable: true, get: function () { return assignmentAcl_js_1.syncAssignmentAcl; } });
|
package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.d.ts
CHANGED
|
@@ -9,6 +9,16 @@ export type RRuleParts = {
|
|
|
9
9
|
bymonth?: number[];
|
|
10
10
|
count?: number;
|
|
11
11
|
wkst?: string;
|
|
12
|
+
/**
|
|
13
|
+
* "The nth of the matching days" — `BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1` is
|
|
14
|
+
* "last workday of the month".
|
|
15
|
+
*
|
|
16
|
+
* Outlook has no equivalent: its `relativeMonthly` pattern takes *one*
|
|
17
|
+
* weekday and a position. With a single weekday the two agree (that is the
|
|
18
|
+
* ordinal `BYDAY` form); with several, the rule cannot be expressed — see
|
|
19
|
+
* {@link calendarUnsupportedReason}.
|
|
20
|
+
*/
|
|
21
|
+
bysetpos?: number;
|
|
12
22
|
};
|
|
13
23
|
/**
|
|
14
24
|
* Read an RRULE string into its parts.
|
|
@@ -29,6 +39,16 @@ export declare function buildRRule(parts: RRuleParts): string;
|
|
|
29
39
|
* created by hand.
|
|
30
40
|
*/
|
|
31
41
|
export declare function eventToCadence(start: Date, recurrence?: Recurrence): ServiceCadence;
|
|
42
|
+
/**
|
|
43
|
+
* Why this cadence cannot be written to a calendar — `undefined` when it can.
|
|
44
|
+
*
|
|
45
|
+
* Outlook's recurrence vocabulary is a **subset** of RFC 5545, and the gap is
|
|
46
|
+
* not cosmetic: a rule we cannot express would have to be approximated, and an
|
|
47
|
+
* approximated maintenance date in someone's calendar is worse than no entry
|
|
48
|
+
* at all. The sync therefore leaves such plans alone instead of guessing (see
|
|
49
|
+
* `syncPlanToCalendar`).
|
|
50
|
+
*/
|
|
51
|
+
export declare function calendarUnsupportedReason(cadence: ServiceCadence | undefined): string | undefined;
|
|
32
52
|
/**
|
|
33
53
|
* A v2 cadence as an Outlook recurrence — `undefined` for a one-off, which
|
|
34
54
|
* Graph expects as an event without a `recurrence` at all.
|
package/dist/features/openservice/serviceSchedules/calendarSync/functions/outlookRecurrence.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.parseRRule = parseRRule;
|
|
4
4
|
exports.buildRRule = buildRRule;
|
|
5
5
|
exports.eventToCadence = eventToCadence;
|
|
6
|
+
exports.calendarUnsupportedReason = calendarUnsupportedReason;
|
|
6
7
|
exports.cadenceToRecurrence = cadenceToRecurrence;
|
|
7
8
|
/**
|
|
8
9
|
* Outlook recurrence ↔ RRULE, both directions.
|
|
@@ -18,12 +19,12 @@ exports.cadenceToRecurrence = cadenceToRecurrence;
|
|
|
18
19
|
* a maintenance date. `pnpm tsx scripts/…` round-trips it (see the module test
|
|
19
20
|
* at the bottom of the file's docs in OPENSERVICE_V2_SCHEDULES.md).
|
|
20
21
|
*
|
|
21
|
-
* **
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* **Where the two vocabularies differ**: Outlook's patterns are a subset of the
|
|
23
|
+
* RRULE grammar. A positional weekday (`BYDAY=+2TU`) maps to `relativeMonthly`
|
|
24
|
+
* and back, but `BYSETPOS` over several weekdays ("last workday"), several
|
|
25
|
+
* days of the month, or several months do not exist there at all —
|
|
26
|
+
* {@link calendarUnsupportedReason} names them, and the sync skips such plans
|
|
27
|
+
* instead of approximating a maintenance date.
|
|
27
28
|
*/
|
|
28
29
|
const WEEKDAY_TO_RRULE = {
|
|
29
30
|
Sunday: "SU",
|
|
@@ -102,6 +103,12 @@ function parseRRule(rrule) {
|
|
|
102
103
|
case "WKST":
|
|
103
104
|
parts.wkst = value;
|
|
104
105
|
break;
|
|
106
|
+
case "BYSETPOS": {
|
|
107
|
+
const parsed = Number(value);
|
|
108
|
+
if (Number.isFinite(parsed) && parsed !== 0)
|
|
109
|
+
parts.bysetpos = parsed;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
105
112
|
}
|
|
106
113
|
}
|
|
107
114
|
return parts;
|
|
@@ -119,6 +126,8 @@ function buildRRule(parts) {
|
|
|
119
126
|
}
|
|
120
127
|
if (parts.bymonth?.length)
|
|
121
128
|
chunks.push(`BYMONTH=${parts.bymonth.join(",")}`);
|
|
129
|
+
if (parts.bysetpos)
|
|
130
|
+
chunks.push(`BYSETPOS=${parts.bysetpos}`);
|
|
122
131
|
if (parts.count)
|
|
123
132
|
chunks.push(`COUNT=${parts.count}`);
|
|
124
133
|
if (parts.wkst && parts.wkst !== "MO")
|
|
@@ -194,6 +203,34 @@ function eventToCadence(start, recurrence) {
|
|
|
194
203
|
}
|
|
195
204
|
return cadence;
|
|
196
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Why this cadence cannot be written to a calendar — `undefined` when it can.
|
|
208
|
+
*
|
|
209
|
+
* Outlook's recurrence vocabulary is a **subset** of RFC 5545, and the gap is
|
|
210
|
+
* not cosmetic: a rule we cannot express would have to be approximated, and an
|
|
211
|
+
* approximated maintenance date in someone's calendar is worse than no entry
|
|
212
|
+
* at all. The sync therefore leaves such plans alone instead of guessing (see
|
|
213
|
+
* `syncPlanToCalendar`).
|
|
214
|
+
*/
|
|
215
|
+
function calendarUnsupportedReason(cadence) {
|
|
216
|
+
if (cadence?.kind !== "rrule" || !cadence.rrule)
|
|
217
|
+
return undefined;
|
|
218
|
+
const parts = parseRRule(cadence.rrule);
|
|
219
|
+
const days = parts.byday?.map(splitByDay) ?? [];
|
|
220
|
+
if (parts.bysetpos !== undefined && days.length > 1) {
|
|
221
|
+
return `BYSETPOS with ${days.length} weekdays has no Outlook equivalent`;
|
|
222
|
+
}
|
|
223
|
+
if (parts.byday && parts.byday.length > 1 && parts.freq !== "WEEKLY") {
|
|
224
|
+
return `several weekdays are only expressible for a weekly series (FREQ=${parts.freq})`;
|
|
225
|
+
}
|
|
226
|
+
if (parts.bymonthday && parts.bymonthday.length > 1) {
|
|
227
|
+
return "several days of the month have no Outlook equivalent";
|
|
228
|
+
}
|
|
229
|
+
if (parts.bymonth && parts.bymonth.length > 1) {
|
|
230
|
+
return "several months have no Outlook equivalent";
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
197
234
|
/**
|
|
198
235
|
* A v2 cadence as an Outlook recurrence — `undefined` for a one-off, which
|
|
199
236
|
* Graph expects as an event without a `recurrence` at all.
|
|
@@ -215,7 +252,13 @@ function cadenceToRecurrence(cadence, start) {
|
|
|
215
252
|
}
|
|
216
253
|
function recurrencePattern(parts, start) {
|
|
217
254
|
const interval = parts.interval && parts.interval > 0 ? parts.interval : 1;
|
|
218
|
-
const
|
|
255
|
+
const days = parts.byday?.map(splitByDay) ?? [];
|
|
256
|
+
// A position comes either as an ordinal on the weekday (`+2TU`) or as
|
|
257
|
+
// BYSETPOS next to a single weekday — both are one Outlook `index`.
|
|
258
|
+
const positional = days.find((day) => !!day.ordinal) ??
|
|
259
|
+
(parts.bysetpos !== undefined && days.length === 1
|
|
260
|
+
? { ordinal: parts.bysetpos, weekday: days[0].weekday }
|
|
261
|
+
: undefined);
|
|
219
262
|
switch (parts.freq ?? "DAILY") {
|
|
220
263
|
case "DAILY":
|
|
221
264
|
return { type: "daily", interval };
|
package/dist/features/openservice/serviceSchedules/calendarSync/service/syncPlanToCalendar.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.connectionForTenant = connectionForTenant;
|
|
|
8
8
|
const node_1 = __importDefault(require("parse/node"));
|
|
9
9
|
const CalendarManager_js_1 = require("../../../schedules/calendarSync/service/CalendarManager.js");
|
|
10
10
|
const index_js_1 = require("../../../../../types/index.js");
|
|
11
|
+
const outlookRecurrence_js_1 = require("../functions/outlookRecurrence.js");
|
|
11
12
|
const planToEvent_js_1 = require("../functions/planToEvent.js");
|
|
12
13
|
const ServiceSyncTypes_js_1 = require("../types/ServiceSyncTypes.js");
|
|
13
14
|
const serviceCalendarMeta_js_1 = require("./serviceCalendarMeta.js");
|
|
@@ -33,6 +34,13 @@ async function syncPlanToCalendar(plan) {
|
|
|
33
34
|
const connection = await connectionForTenant(plan.get("tenant"));
|
|
34
35
|
if (!connection)
|
|
35
36
|
return;
|
|
37
|
+
// A recurrence Outlook cannot express is left untouched rather than
|
|
38
|
+
// approximated — a wrong maintenance date in a calendar is worse than none.
|
|
39
|
+
const unsupported = (0, outlookRecurrence_js_1.calendarUnsupportedReason)(plan.get("cadence"));
|
|
40
|
+
if (unsupported) {
|
|
41
|
+
console.warn(`[ServiceCalendarSync] Plan ${plan.id} not synced: ${unsupported}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
36
44
|
const meta = await (0, serviceCalendarMeta_js_1.findMetaForPlan)(plan.id);
|
|
37
45
|
const values = meta ? (0, serviceCalendarMeta_js_1.metaValues)(meta) : undefined;
|
|
38
46
|
const eventId = values?.microsoftCalendarEventId;
|
|
@@ -24,12 +24,11 @@ export declare namespace Permissions {
|
|
|
24
24
|
ticket_edit = "service:can-edit-ticket",
|
|
25
25
|
ticket_delete = "service:can-delete-ticket",
|
|
26
26
|
ticket_assign = "service:can-assign-ticket",
|
|
27
|
-
|
|
28
|
-
schedule_create = "
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
template_manage = "service:can-manage-template",
|
|
27
|
+
schedule_access = "openservice-schedule:can-access-schedule-plugin",
|
|
28
|
+
schedule_create = "openservice-schedule:can-create-schedule",
|
|
29
|
+
schedule_delete = "openservice-schedule:can-delete-schedule",
|
|
30
|
+
execution_perform = "openservice-schedule:can-perform-execution",
|
|
31
|
+
template_manage = "openservice-schedule:can-manage-template",
|
|
33
32
|
masterdata_manage = "service:can-manage-masterdata",
|
|
34
33
|
formconfig_manage = "service:can-manage-formconfig",
|
|
35
34
|
tag_create = "service:can-create-tag",
|
|
@@ -36,15 +36,20 @@ var Permissions;
|
|
|
36
36
|
SERVICE["ticket_edit"] = "service:can-edit-ticket";
|
|
37
37
|
SERVICE["ticket_delete"] = "service:can-delete-ticket";
|
|
38
38
|
SERVICE["ticket_assign"] = "service:can-assign-ticket";
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
|
|
39
|
+
// Schedule-Plugin
|
|
40
|
+
//
|
|
41
|
+
// No `read` or `edit` here on purpose: both are decided per record by the
|
|
42
|
+
// ACL — creator, assigned roles and users — and a capability that says
|
|
43
|
+
// "may edit" while the ACL says "not this one" only adds a second answer to
|
|
44
|
+
// a question that already has one. `create` stays a capability because
|
|
45
|
+
// there is no record yet whose ACL could be asked, and a class-level
|
|
46
|
+
// permission cannot express it per tenant.
|
|
47
|
+
SERVICE["schedule_access"] = "openservice-schedule:can-access-schedule-plugin";
|
|
48
|
+
SERVICE["schedule_create"] = "openservice-schedule:can-create-schedule";
|
|
49
|
+
SERVICE["schedule_delete"] = "openservice-schedule:can-delete-schedule";
|
|
50
|
+
SERVICE["execution_perform"] = "openservice-schedule:can-perform-execution";
|
|
51
|
+
SERVICE["template_manage"] = "openservice-schedule:can-manage-template";
|
|
46
52
|
// Catalog / master data / form config
|
|
47
|
-
SERVICE["template_manage"] = "service:can-manage-template";
|
|
48
53
|
SERVICE["masterdata_manage"] = "service:can-manage-masterdata";
|
|
49
54
|
SERVICE["formconfig_manage"] = "service:can-manage-formconfig";
|
|
50
55
|
// Narrow master-data capability: create a tag inline from a tag picker,
|
|
@@ -6,29 +6,50 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.init = init;
|
|
7
7
|
const node_1 = __importDefault(require("parse/node"));
|
|
8
8
|
const index_js_1 = require("../features/schema/index.js");
|
|
9
|
-
const
|
|
10
|
-
const
|
|
9
|
+
const Permissions_js_1 = require("../features/permissions/types/Permissions.js");
|
|
10
|
+
const index_js_2 = require("../features/permissions/index.js");
|
|
11
|
+
const index_js_3 = require("../features/openservice/serviceSchedules/acl/index.js");
|
|
12
|
+
const index_js_4 = require("../features/openservice/serviceSchedules/calendarSync/index.js");
|
|
13
|
+
const index_js_5 = require("../types/index.js");
|
|
11
14
|
async function init() {
|
|
12
|
-
(0, index_js_1.beforeSaveHook)(
|
|
15
|
+
(0, index_js_1.beforeSaveHook)(index_js_5.Service_Schedule, async (request) => {
|
|
13
16
|
const { object, original, user } = request;
|
|
14
17
|
await (0, index_js_1.defaultHandler)(request);
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// `
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
//
|
|
18
|
+
// The creator, recorded once and never rewritten: it is who the plan
|
|
19
|
+
// belongs to, and `defaultAclHandler` turns it into a read+write grant.
|
|
20
|
+
if (!original && user && !object.get("user")) {
|
|
21
|
+
object.set("user", user);
|
|
22
|
+
}
|
|
23
|
+
// `denyTenantUserRead` is what makes a plan private. Without it every user
|
|
24
|
+
// of the tenant reads every plan, which is the opposite of what assigning
|
|
25
|
+
// roles and users to a plan is supposed to mean.
|
|
26
|
+
//
|
|
27
|
+
// `allowCustomACL` keeps an ACL the client set instead of resetting it on
|
|
28
|
+
// every save — the assignment sync in `afterSave` depends on its ACL
|
|
29
|
+
// surviving the next save. The handler still re-adds `od-admin` /
|
|
30
|
+
// `od-tenant-admin-<tenant>`, so a plan can never lock administrators out.
|
|
31
|
+
await (0, index_js_1.defaultAclHandler)(request, {
|
|
32
|
+
allowCustomACL: true,
|
|
33
|
+
denyTenantUserRead: true,
|
|
34
|
+
});
|
|
24
35
|
});
|
|
25
|
-
(0, index_js_1.afterSaveHook)(
|
|
36
|
+
(0, index_js_1.afterSaveHook)(index_js_5.Service_Schedule, async (request) => {
|
|
26
37
|
const { object, original, context } = request;
|
|
38
|
+
// Our own ACL write — the assignments already match, nothing to redo.
|
|
39
|
+
if (context?.[index_js_3.ASSIGNMENT_ACL_CONTEXT_FLAG])
|
|
40
|
+
return;
|
|
41
|
+
// Assignments are relations: only readable now that the save applied them.
|
|
42
|
+
await (0, index_js_3.syncAssignmentAcl)(object, {
|
|
43
|
+
roleRelations: ["assignedRoles", "escalationRoles"],
|
|
44
|
+
userRelations: ["assignedUsers"],
|
|
45
|
+
}).catch((error) => {
|
|
46
|
+
console.error(`[ServiceScheduleAcl] Plan ${object.id}: could not sync ACL from assignments:`, error);
|
|
47
|
+
});
|
|
27
48
|
// A change that came *from* the calendar must not be written back to it:
|
|
28
49
|
// that would create a new version there, which the delta feed reports as a
|
|
29
50
|
// foreign change, which we would import again — an endless echo. The
|
|
30
51
|
// poller marks its own writes with this flag.
|
|
31
|
-
if (context?.[
|
|
52
|
+
if (context?.[index_js_4.CALENDAR_SYNC_CONTEXT_FLAG])
|
|
32
53
|
return;
|
|
33
54
|
// Seed data belongs to a demo tenant, not in anybody's calendar.
|
|
34
55
|
if (object.get("seed"))
|
|
@@ -38,13 +59,18 @@ async function init() {
|
|
|
38
59
|
// Graph round trip (token + write) would otherwise be part of every save
|
|
39
60
|
// the user waits for. Failures are logged, and the next relevant save
|
|
40
61
|
// retries — as does the delta poller from the other side.
|
|
41
|
-
(0,
|
|
62
|
+
(0, index_js_4.syncPlanToCalendar)(object).catch((error) => {
|
|
42
63
|
console.error(`[ServiceCalendarSync] Plan ${object.id} could not be synced:`, error);
|
|
43
64
|
});
|
|
44
65
|
}
|
|
45
66
|
});
|
|
46
|
-
(0, index_js_1.beforeDeleteHook)(
|
|
67
|
+
(0, index_js_1.beforeDeleteHook)(index_js_5.Service_Schedule, async (request) => {
|
|
47
68
|
const { object } = request;
|
|
69
|
+
// Parse has no delete right of its own — an ACL `write` grant covers update
|
|
70
|
+
// *and* delete, so anyone allowed to edit a plan could otherwise remove it.
|
|
71
|
+
// Hiding the button in the UI does not change that; this is what makes the
|
|
72
|
+
// capability real.
|
|
73
|
+
await (0, index_js_2.requirePermission)(request, Permissions_js_1.Permissions.SERVICE.schedule_delete, "delete maintenance schedule");
|
|
48
74
|
// A protocol points at the plan it documents. Destroying the plan leaves
|
|
49
75
|
// that pointer dangling and the protocol — the record that the work
|
|
50
76
|
// happened — unreachable. Retiring a plan is done by setting `deletedAt`
|
|
@@ -53,7 +79,7 @@ async function init() {
|
|
|
53
79
|
//
|
|
54
80
|
// A plan without any execution carries no history, so removing it stays
|
|
55
81
|
// allowed — that keeps mistakenly created plans cleanable.
|
|
56
|
-
const executions = await new node_1.default.Query(
|
|
82
|
+
const executions = await new node_1.default.Query(index_js_5.Service_Schedule_Execution)
|
|
57
83
|
.equalTo("schedule", object)
|
|
58
84
|
.count({ useMasterKey: true });
|
|
59
85
|
if (executions > 0) {
|
|
@@ -2,16 +2,37 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.init = init;
|
|
4
4
|
const index_js_1 = require("../features/schema/index.js");
|
|
5
|
-
const index_js_2 = require("../
|
|
5
|
+
const index_js_2 = require("../features/openservice/serviceSchedules/acl/index.js");
|
|
6
|
+
const index_js_3 = require("../types/index.js");
|
|
6
7
|
async function init() {
|
|
7
|
-
(0, index_js_1.beforeSaveHook)(
|
|
8
|
+
(0, index_js_1.beforeSaveHook)(index_js_3.Service_Schedule_Template, async (request) => {
|
|
8
9
|
const { object, original, user } = request;
|
|
9
10
|
await (0, index_js_1.defaultHandler)(request);
|
|
10
|
-
|
|
11
|
-
//
|
|
11
|
+
// The creator, recorded once and never rewritten — a template stays visible
|
|
12
|
+
// to whoever built it, whatever happens to its assigned roles later.
|
|
13
|
+
if (!original && user && !object.get("user")) {
|
|
14
|
+
object.set("user", user);
|
|
15
|
+
}
|
|
16
|
+
// A template is private like a plan: its creator, the roles it is assigned
|
|
17
|
+
// to, and administrators. Without `denyTenantUserRead` the whole tenant
|
|
18
|
+
// reads every template and assigning roles would mean nothing.
|
|
19
|
+
await (0, index_js_1.defaultAclHandler)(request, {
|
|
20
|
+
allowCustomACL: true,
|
|
21
|
+
denyTenantUserRead: true,
|
|
22
|
+
});
|
|
12
23
|
});
|
|
13
|
-
(0, index_js_1.afterSaveHook)(
|
|
14
|
-
const { object,
|
|
15
|
-
//
|
|
24
|
+
(0, index_js_1.afterSaveHook)(index_js_3.Service_Schedule_Template, async (request) => {
|
|
25
|
+
const { object, context } = request;
|
|
26
|
+
// Our own ACL write — the assignments already match.
|
|
27
|
+
if (context?.[index_js_2.ASSIGNMENT_ACL_CONTEXT_FLAG])
|
|
28
|
+
return;
|
|
29
|
+
// `assignedRoles` is a relation, readable only after the save applied it.
|
|
30
|
+
// Templates carry no `assignedUsers`: they are shared by role, and the
|
|
31
|
+
// creator keeps their own grant.
|
|
32
|
+
await (0, index_js_2.syncAssignmentAcl)(object, {
|
|
33
|
+
roleRelations: ["assignedRoles"],
|
|
34
|
+
}).catch((error) => {
|
|
35
|
+
console.error(`[ServiceScheduleAcl] Template ${object.id}: could not sync ACL from assignments:`, error);
|
|
36
|
+
});
|
|
16
37
|
});
|
|
17
38
|
}
|
|
@@ -43,7 +43,7 @@ const index_js_1 = require("../features/schema/index.js");
|
|
|
43
43
|
const appLinks_js_1 = require("../helper/appLinks.js");
|
|
44
44
|
const getUserLanguage_js_1 = require("../helper/getUserLanguage.js");
|
|
45
45
|
const index_js_2 = require("../types/index.js");
|
|
46
|
-
const
|
|
46
|
+
const ruleset_core_1 = require("@opendash/ruleset-core");
|
|
47
47
|
async function init() {
|
|
48
48
|
// Soft delete: a ticket is referenced by chat messages, meta entries,
|
|
49
49
|
// notifications and child tickets, so the frontend archives instead of
|
|
@@ -266,7 +266,7 @@ async function notifyWatchers(ticket, notified, assignerName) {
|
|
|
266
266
|
for (const rule of rules) {
|
|
267
267
|
if (rule.function !== "notify" || !rule.ruleset)
|
|
268
268
|
continue;
|
|
269
|
-
if (!(0,
|
|
269
|
+
if (!(0, ruleset_core_1.applyRuleSet)(rule.ruleset, getValue))
|
|
270
270
|
continue;
|
|
271
271
|
const assignees = Array.isArray(rule.params?.assignees)
|
|
272
272
|
? rule.params.assignees
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Parse from "parse/node";
|
|
2
2
|
import type { _Role } from "./_Role";
|
|
3
|
+
import type { _User } from "./_User";
|
|
3
4
|
import type { Service_Equipment } from "./Service_Equipment";
|
|
4
5
|
import type { Tenant } from "./Tenant";
|
|
5
6
|
export interface Service_ScheduleAttributes {
|
|
@@ -10,6 +11,7 @@ export interface Service_ScheduleAttributes {
|
|
|
10
11
|
active: boolean;
|
|
11
12
|
asset?: any | undefined;
|
|
12
13
|
assignedRoles: Parse.Relation<Service_Schedule, _Role>;
|
|
14
|
+
assignedUsers: Parse.Relation<Service_Schedule, _User>;
|
|
13
15
|
cadence: any;
|
|
14
16
|
cadenceExceptions?: any[] | undefined;
|
|
15
17
|
color?: string | undefined;
|
|
@@ -20,13 +22,14 @@ export interface Service_ScheduleAttributes {
|
|
|
20
22
|
escalationRoles: Parse.Relation<Service_Schedule, _Role>;
|
|
21
23
|
graceDays?: number | undefined;
|
|
22
24
|
leadDays?: number | undefined;
|
|
23
|
-
notifications
|
|
25
|
+
notifications: any[];
|
|
24
26
|
pausedUntil?: Date | undefined;
|
|
25
27
|
seed?: boolean | undefined;
|
|
26
28
|
serviceProviders?: any[] | undefined;
|
|
27
29
|
steps: any[];
|
|
28
30
|
tenant: Tenant;
|
|
29
31
|
title: string;
|
|
32
|
+
user?: _User | undefined;
|
|
30
33
|
}
|
|
31
34
|
export declare class Service_Schedule extends Parse.Object<Service_ScheduleAttributes> {
|
|
32
35
|
static className: string;
|
|
@@ -36,6 +39,7 @@ export declare class Service_Schedule extends Parse.Object<Service_ScheduleAttri
|
|
|
36
39
|
get asset(): any | undefined;
|
|
37
40
|
set asset(value: any | undefined);
|
|
38
41
|
get assignedRoles(): Parse.Relation<Service_Schedule, _Role>;
|
|
42
|
+
get assignedUsers(): Parse.Relation<Service_Schedule, _User>;
|
|
39
43
|
get cadence(): any;
|
|
40
44
|
set cadence(value: any);
|
|
41
45
|
get cadenceExceptions(): any[] | undefined;
|
|
@@ -55,8 +59,8 @@ export declare class Service_Schedule extends Parse.Object<Service_ScheduleAttri
|
|
|
55
59
|
set graceDays(value: number | undefined);
|
|
56
60
|
get leadDays(): number | undefined;
|
|
57
61
|
set leadDays(value: number | undefined);
|
|
58
|
-
get notifications(): any[]
|
|
59
|
-
set notifications(value: any[]
|
|
62
|
+
get notifications(): any[];
|
|
63
|
+
set notifications(value: any[]);
|
|
60
64
|
get pausedUntil(): Date | undefined;
|
|
61
65
|
set pausedUntil(value: Date | undefined);
|
|
62
66
|
get seed(): boolean | undefined;
|
|
@@ -69,4 +73,6 @@ export declare class Service_Schedule extends Parse.Object<Service_ScheduleAttri
|
|
|
69
73
|
set tenant(value: Tenant);
|
|
70
74
|
get title(): string;
|
|
71
75
|
set title(value: string);
|
|
76
|
+
get user(): _User | undefined;
|
|
77
|
+
set user(value: _User | undefined);
|
|
72
78
|
}
|
|
@@ -25,6 +25,9 @@ class Service_Schedule extends node_1.default.Object {
|
|
|
25
25
|
get assignedRoles() {
|
|
26
26
|
return super.relation("assignedRoles");
|
|
27
27
|
}
|
|
28
|
+
get assignedUsers() {
|
|
29
|
+
return super.relation("assignedUsers");
|
|
30
|
+
}
|
|
28
31
|
get cadence() {
|
|
29
32
|
return super.get("cadence");
|
|
30
33
|
}
|
|
@@ -124,6 +127,12 @@ class Service_Schedule extends node_1.default.Object {
|
|
|
124
127
|
set title(value) {
|
|
125
128
|
super.set("title", value);
|
|
126
129
|
}
|
|
130
|
+
get user() {
|
|
131
|
+
return super.get("user");
|
|
132
|
+
}
|
|
133
|
+
set user(value) {
|
|
134
|
+
super.set("user", value);
|
|
135
|
+
}
|
|
127
136
|
}
|
|
128
137
|
exports.Service_Schedule = Service_Schedule;
|
|
129
138
|
node_1.default.Object.registerSubclass("OD3_Service_Schedule", Service_Schedule);
|
|
@@ -12,9 +12,10 @@ export interface Service_Schedule_ExecutionAttributes {
|
|
|
12
12
|
coverageOrigin?: any | undefined;
|
|
13
13
|
performedBy?: _User | undefined;
|
|
14
14
|
schedule: Service_Schedule;
|
|
15
|
-
signature?:
|
|
15
|
+
signature?: string | undefined;
|
|
16
16
|
stepResults?: any[] | undefined;
|
|
17
17
|
tenant: Tenant;
|
|
18
|
+
user?: _User | undefined;
|
|
18
19
|
}
|
|
19
20
|
export declare class Service_Schedule_Execution extends Parse.Object<Service_Schedule_ExecutionAttributes> {
|
|
20
21
|
static className: string;
|
|
@@ -29,10 +30,12 @@ export declare class Service_Schedule_Execution extends Parse.Object<Service_Sch
|
|
|
29
30
|
set performedBy(value: _User | undefined);
|
|
30
31
|
get schedule(): Service_Schedule;
|
|
31
32
|
set schedule(value: Service_Schedule);
|
|
32
|
-
get signature():
|
|
33
|
-
set signature(value:
|
|
33
|
+
get signature(): string | undefined;
|
|
34
|
+
set signature(value: string | undefined);
|
|
34
35
|
get stepResults(): any[] | undefined;
|
|
35
36
|
set stepResults(value: any[] | undefined);
|
|
36
37
|
get tenant(): Tenant;
|
|
37
38
|
set tenant(value: Tenant);
|
|
39
|
+
get user(): _User | undefined;
|
|
40
|
+
set user(value: _User | undefined);
|
|
38
41
|
}
|
|
@@ -58,6 +58,12 @@ class Service_Schedule_Execution extends node_1.default.Object {
|
|
|
58
58
|
set tenant(value) {
|
|
59
59
|
super.set("tenant", value);
|
|
60
60
|
}
|
|
61
|
+
get user() {
|
|
62
|
+
return super.get("user");
|
|
63
|
+
}
|
|
64
|
+
set user(value) {
|
|
65
|
+
super.set("user", value);
|
|
66
|
+
}
|
|
61
67
|
}
|
|
62
68
|
exports.Service_Schedule_Execution = Service_Schedule_Execution;
|
|
63
69
|
node_1.default.Object.registerSubclass("OD3_Service_Schedule_Execution", Service_Schedule_Execution);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Parse from "parse/node";
|
|
2
2
|
import type { _Role } from "./_Role";
|
|
3
3
|
import type { Tenant } from "./Tenant";
|
|
4
|
+
import type { _User } from "./_User";
|
|
4
5
|
export interface Service_Schedule_TemplateAttributes {
|
|
5
6
|
id: string;
|
|
6
7
|
objectId: string;
|
|
@@ -10,11 +11,12 @@ export interface Service_Schedule_TemplateAttributes {
|
|
|
10
11
|
cadence: any;
|
|
11
12
|
deletedAt?: Date | undefined;
|
|
12
13
|
description?: string | undefined;
|
|
13
|
-
notifications
|
|
14
|
+
notifications: any[];
|
|
14
15
|
serviceProviders: any[];
|
|
15
16
|
steps: any[];
|
|
16
17
|
tenant: Tenant;
|
|
17
18
|
title: string;
|
|
19
|
+
user?: _User | undefined;
|
|
18
20
|
}
|
|
19
21
|
export declare class Service_Schedule_Template extends Parse.Object<Service_Schedule_TemplateAttributes> {
|
|
20
22
|
static className: string;
|
|
@@ -26,8 +28,8 @@ export declare class Service_Schedule_Template extends Parse.Object<Service_Sche
|
|
|
26
28
|
set deletedAt(value: Date | undefined);
|
|
27
29
|
get description(): string | undefined;
|
|
28
30
|
set description(value: string | undefined);
|
|
29
|
-
get notifications(): any[]
|
|
30
|
-
set notifications(value: any[]
|
|
31
|
+
get notifications(): any[];
|
|
32
|
+
set notifications(value: any[]);
|
|
31
33
|
get serviceProviders(): any[];
|
|
32
34
|
set serviceProviders(value: any[]);
|
|
33
35
|
get steps(): any[];
|
|
@@ -36,4 +38,6 @@ export declare class Service_Schedule_Template extends Parse.Object<Service_Sche
|
|
|
36
38
|
set tenant(value: Tenant);
|
|
37
39
|
get title(): string;
|
|
38
40
|
set title(value: string);
|
|
41
|
+
get user(): _User | undefined;
|
|
42
|
+
set user(value: _User | undefined);
|
|
39
43
|
}
|
|
@@ -61,6 +61,12 @@ class Service_Schedule_Template extends node_1.default.Object {
|
|
|
61
61
|
set title(value) {
|
|
62
62
|
super.set("title", value);
|
|
63
63
|
}
|
|
64
|
+
get user() {
|
|
65
|
+
return super.get("user");
|
|
66
|
+
}
|
|
67
|
+
set user(value) {
|
|
68
|
+
super.set("user", value);
|
|
69
|
+
}
|
|
64
70
|
}
|
|
65
71
|
exports.Service_Schedule_Template = Service_Schedule_Template;
|
|
66
72
|
node_1.default.Object.registerSubclass("OD3_Service_Schedule_Template", Service_Schedule_Template);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openinc/parse-server-opendash",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.2",
|
|
4
4
|
"description": "Parse Server Cloud Code for open.INC Stack.",
|
|
5
5
|
"packageManager": "pnpm@11.25.0",
|
|
6
6
|
"keywords": [
|
|
@@ -69,14 +69,15 @@
|
|
|
69
69
|
"seed:v1": "node dist/scripts/seed-v1-testdata.js"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
+
"@opendash/ruleset-core": "0.1.1-nightly.1",
|
|
72
73
|
"@openinc/parse-server-schema": "^4.1.1",
|
|
73
74
|
"amqplib": "^0.10.9",
|
|
74
75
|
"cron": "^4.4.0",
|
|
75
76
|
"dayjs": "^1.11.23",
|
|
76
77
|
"dotenv": "^17.4.2",
|
|
77
|
-
"fast-equals": "^6.0.
|
|
78
|
-
"i18next": "^26.4.
|
|
79
|
-
"i18next-fs-backend": "^2.6.
|
|
78
|
+
"fast-equals": "^6.0.3",
|
|
79
|
+
"i18next": "^26.4.2",
|
|
80
|
+
"i18next-fs-backend": "^2.6.8",
|
|
80
81
|
"jsonwebtoken": "^9.0.3",
|
|
81
82
|
"jwks-rsa": "^4.1.0",
|
|
82
83
|
"nodemailer": "^9.1.1",
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
"targetClass": "_Role",
|
|
14
14
|
"required": false
|
|
15
15
|
},
|
|
16
|
+
"assignedUsers": {
|
|
17
|
+
"type": "Relation",
|
|
18
|
+
"targetClass": "_User"
|
|
19
|
+
},
|
|
16
20
|
"cadence": {
|
|
17
21
|
"type": "Object",
|
|
18
22
|
"required": true
|
|
@@ -81,6 +85,10 @@
|
|
|
81
85
|
"title": {
|
|
82
86
|
"type": "String",
|
|
83
87
|
"required": true
|
|
88
|
+
},
|
|
89
|
+
"user": {
|
|
90
|
+
"type": "Pointer",
|
|
91
|
+
"targetClass": "_User"
|
|
84
92
|
}
|
|
85
93
|
},
|
|
86
94
|
"classLevelPermissions": {
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Backend port of the `@opendash/ui` ruleset evaluator
|
|
3
|
-
* (`libs/ui/src/features/ruleset/functions/applyRuleSet.ts`).
|
|
4
|
-
*
|
|
5
|
-
* The frontend authors condition trees with the RuleSet editor and stores them
|
|
6
|
-
* as JSON; notifications must be created server-side, so the same evaluation
|
|
7
|
-
* logic has to run here. This is a faithful, dependency-free copy of the pure
|
|
8
|
-
* evaluation core — keep the operator semantics in sync with the frontend.
|
|
9
|
-
*/
|
|
10
|
-
export type RuleOperator = "equals" | "notEquals" | "contains" | "notContains" | "startsWith" | "endsWith" | "isEmpty" | "isNotEmpty" | "gt" | "gte" | "lt" | "lte" | "between" | "isTrue" | "isFalse" | "before" | "after";
|
|
11
|
-
/** A single comparison against one field. */
|
|
12
|
-
export interface Rule {
|
|
13
|
-
id: string;
|
|
14
|
-
kind: "rule";
|
|
15
|
-
field: string;
|
|
16
|
-
operator: RuleOperator;
|
|
17
|
-
value?: string | number | boolean;
|
|
18
|
-
/** Secondary value — only used for the "between" operator. */
|
|
19
|
-
value2?: number;
|
|
20
|
-
}
|
|
21
|
-
/** A logical group that chains rules and nested groups via AND / OR. */
|
|
22
|
-
export interface RuleGroup {
|
|
23
|
-
id: string;
|
|
24
|
-
kind: "group";
|
|
25
|
-
conjunction: "AND" | "OR";
|
|
26
|
-
rules: Array<Rule | RuleGroup>;
|
|
27
|
-
}
|
|
28
|
-
export type RuleSetValue = RuleGroup;
|
|
29
|
-
/** A saved ruleset-function entry as produced by the RuleSet editor. */
|
|
30
|
-
export interface RuleSetFunctionEntry {
|
|
31
|
-
ruleset: RuleSetValue;
|
|
32
|
-
function: string;
|
|
33
|
-
params?: Record<string, unknown>;
|
|
34
|
-
}
|
|
35
|
-
/** Resolves the current value for a given field name. */
|
|
36
|
-
export type GetValueFn = (fieldName: string) => unknown;
|
|
37
|
-
/**
|
|
38
|
-
* Evaluates a `RuleSetValue` against live data. An empty rule group always
|
|
39
|
-
* returns `true` (matches the frontend semantics).
|
|
40
|
-
*/
|
|
41
|
-
export declare function applyRuleSet(ruleSet: RuleSetValue, getValue: GetValueFn): boolean;
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Backend port of the `@opendash/ui` ruleset evaluator
|
|
4
|
-
* (`libs/ui/src/features/ruleset/functions/applyRuleSet.ts`).
|
|
5
|
-
*
|
|
6
|
-
* The frontend authors condition trees with the RuleSet editor and stores them
|
|
7
|
-
* as JSON; notifications must be created server-side, so the same evaluation
|
|
8
|
-
* logic has to run here. This is a faithful, dependency-free copy of the pure
|
|
9
|
-
* evaluation core — keep the operator semantics in sync with the frontend.
|
|
10
|
-
*/
|
|
11
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.applyRuleSet = applyRuleSet;
|
|
13
|
-
function evaluateRule(rule, getValue) {
|
|
14
|
-
const fieldValue = getValue(rule.field);
|
|
15
|
-
switch (rule.operator) {
|
|
16
|
-
case "equals":
|
|
17
|
-
// eslint-disable-next-line eqeqeq
|
|
18
|
-
return fieldValue == rule.value;
|
|
19
|
-
case "notEquals":
|
|
20
|
-
// eslint-disable-next-line eqeqeq
|
|
21
|
-
return fieldValue != rule.value;
|
|
22
|
-
case "contains":
|
|
23
|
-
return (typeof fieldValue === "string" &&
|
|
24
|
-
fieldValue.includes(String(rule.value ?? "")));
|
|
25
|
-
case "notContains":
|
|
26
|
-
return (typeof fieldValue === "string" &&
|
|
27
|
-
!fieldValue.includes(String(rule.value ?? "")));
|
|
28
|
-
case "startsWith":
|
|
29
|
-
return (typeof fieldValue === "string" &&
|
|
30
|
-
fieldValue.startsWith(String(rule.value ?? "")));
|
|
31
|
-
case "endsWith":
|
|
32
|
-
return (typeof fieldValue === "string" &&
|
|
33
|
-
fieldValue.endsWith(String(rule.value ?? "")));
|
|
34
|
-
case "isEmpty":
|
|
35
|
-
return (fieldValue === null || fieldValue === undefined || fieldValue === "");
|
|
36
|
-
case "isNotEmpty":
|
|
37
|
-
return (fieldValue !== null && fieldValue !== undefined && fieldValue !== "");
|
|
38
|
-
case "gt":
|
|
39
|
-
return Number(fieldValue) > Number(rule.value);
|
|
40
|
-
case "gte":
|
|
41
|
-
return Number(fieldValue) >= Number(rule.value);
|
|
42
|
-
case "lt":
|
|
43
|
-
return Number(fieldValue) < Number(rule.value);
|
|
44
|
-
case "lte":
|
|
45
|
-
return Number(fieldValue) <= Number(rule.value);
|
|
46
|
-
case "between":
|
|
47
|
-
return (Number(fieldValue) >= Number(rule.value) &&
|
|
48
|
-
Number(fieldValue) <= Number(rule.value2));
|
|
49
|
-
case "isTrue":
|
|
50
|
-
return fieldValue === true;
|
|
51
|
-
case "isFalse":
|
|
52
|
-
return fieldValue === false;
|
|
53
|
-
case "before":
|
|
54
|
-
return new Date(String(fieldValue)) < new Date(String(rule.value ?? ""));
|
|
55
|
-
case "after":
|
|
56
|
-
return new Date(String(fieldValue)) > new Date(String(rule.value ?? ""));
|
|
57
|
-
default:
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
function evaluateGroup(group, getValue) {
|
|
62
|
-
if (group.rules.length === 0)
|
|
63
|
-
return true;
|
|
64
|
-
if (group.conjunction === "AND") {
|
|
65
|
-
return group.rules.every((item) => item.kind === "rule"
|
|
66
|
-
? evaluateRule(item, getValue)
|
|
67
|
-
: evaluateGroup(item, getValue));
|
|
68
|
-
}
|
|
69
|
-
return group.rules.some((item) => item.kind === "rule"
|
|
70
|
-
? evaluateRule(item, getValue)
|
|
71
|
-
: evaluateGroup(item, getValue));
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Evaluates a `RuleSetValue` against live data. An empty rule group always
|
|
75
|
-
* returns `true` (matches the frontend semantics).
|
|
76
|
-
*/
|
|
77
|
-
function applyRuleSet(ruleSet, getValue) {
|
|
78
|
-
if (!ruleSet || ruleSet.kind !== "group")
|
|
79
|
-
return false;
|
|
80
|
-
return evaluateGroup(ruleSet, getValue);
|
|
81
|
-
}
|