@openinc/parse-server-opendash 4.1.1 → 4.1.3
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/featuremap.json +27 -12
- package/dist/features/config/helper/customOptions.js +9 -0
- package/dist/features/config/helper/isFeatureEnabled.d.ts +1 -1
- package/dist/features/config/helper/isFeatureEnabled.js +2 -0
- package/dist/features/meta/index.d.ts +1 -0
- package/dist/features/meta/index.js +5 -0
- package/dist/features/meta/services/registerMetaContext.d.ts +29 -0
- package/dist/features/meta/services/registerMetaContext.js +208 -0
- package/dist/features/permissions/types/Permissions.d.ts +24 -1
- package/dist/features/permissions/types/Permissions.js +34 -0
- package/dist/features/ruleset/applyRuleSet.d.ts +41 -0
- package/dist/features/ruleset/applyRuleSet.js +81 -0
- package/dist/features/schema/helper/defaultHandler.js +13 -0
- package/dist/features/schema/helper/isClassEnabled.js +2 -0
- package/dist/features/schema/helper/migrationMode.d.ts +26 -0
- package/dist/features/schema/helper/migrationMode.js +48 -0
- package/dist/features/schema/index.d.ts +1 -0
- package/dist/features/schema/index.js +3 -1
- package/dist/features/schema/services/beforeSaveHook.js +21 -0
- package/dist/features/translations/services/initTranslation.js +1 -1
- package/dist/hooks/Chat_Message.d.ts +1 -0
- package/dist/hooks/Chat_Message.js +179 -0
- package/dist/hooks/Meta_Config.js +19 -10
- package/dist/hooks/Meta_Context.d.ts +6 -0
- package/dist/hooks/Meta_Context.js +16 -0
- package/dist/hooks/Meta_Entry.js +20 -10
- package/dist/hooks/Service_Equipment.d.ts +1 -0
- package/dist/hooks/Service_Equipment.js +38 -0
- package/dist/hooks/Service_FormConfig.d.ts +1 -0
- package/dist/hooks/Service_FormConfig.js +17 -0
- package/dist/hooks/Service_Project.d.ts +1 -0
- package/dist/hooks/Service_Project.js +17 -0
- package/dist/hooks/Service_Schedule.d.ts +1 -0
- package/dist/hooks/Service_Schedule.js +24 -0
- package/dist/hooks/Service_Schedule_Execution.d.ts +1 -0
- package/dist/hooks/Service_Schedule_Execution.js +17 -0
- package/dist/hooks/Service_Schedule_Template.d.ts +1 -0
- package/dist/hooks/Service_Schedule_Template.js +17 -0
- package/dist/hooks/Service_Status.d.ts +1 -0
- package/dist/hooks/Service_Status.js +32 -0
- package/dist/hooks/Service_Tag.d.ts +1 -0
- package/dist/hooks/Service_Tag.js +168 -0
- package/dist/hooks/Service_Ticket.d.ts +1 -0
- package/dist/hooks/Service_Ticket.js +433 -0
- package/dist/hooks/Service_TriggerState.d.ts +1 -0
- package/dist/hooks/Service_TriggerState.js +17 -0
- package/dist/hooks/_ChangeLog.js +27 -6
- package/dist/i18n/de-DE/chat.json +18 -0
- package/dist/i18n/de-DE/openservice.json +14 -0
- package/dist/i18n/en-US/chat.json +18 -0
- package/dist/i18n/en-US/openservice.json +14 -0
- package/dist/scripts/backfill-meta-contexts.d.ts +1 -0
- package/dist/scripts/backfill-meta-contexts.js +100 -0
- package/dist/scripts/migration/lib/acl.d.ts +52 -0
- package/dist/scripts/migration/lib/acl.js +79 -0
- package/dist/scripts/migration/lib/articleMeta.d.ts +47 -0
- package/dist/scripts/migration/lib/articleMeta.js +132 -0
- package/dist/scripts/migration/lib/env.d.ts +56 -0
- package/dist/scripts/migration/lib/env.js +106 -0
- package/dist/scripts/migration/lib/latest.d.ts +47 -0
- package/dist/scripts/migration/lib/latest.js +115 -0
- package/dist/scripts/migration/lib/map.d.ts +53 -0
- package/dist/scripts/migration/lib/map.js +113 -0
- package/dist/scripts/migration/lib/material.d.ts +47 -0
- package/dist/scripts/migration/lib/material.js +51 -0
- package/dist/scripts/migration/lib/message.d.ts +78 -0
- package/dist/scripts/migration/lib/message.js +115 -0
- package/dist/scripts/migration/lib/pass.d.ts +67 -0
- package/dist/scripts/migration/lib/pass.js +89 -0
- package/dist/scripts/migration/lib/ptr.d.ts +32 -0
- package/dist/scripts/migration/lib/ptr.js +38 -0
- package/dist/scripts/migration/lib/report.d.ts +41 -0
- package/dist/scripts/migration/lib/report.js +78 -0
- package/dist/scripts/migration/lib/rest.d.ts +47 -0
- package/dist/scripts/migration/lib/rest.js +68 -0
- package/dist/scripts/migration/lib/ticket.d.ts +72 -0
- package/dist/scripts/migration/lib/ticket.js +192 -0
- package/dist/scripts/migration/passes/01-statuses.d.ts +12 -0
- package/dist/scripts/migration/passes/01-statuses.js +122 -0
- package/dist/scripts/migration/passes/02-tags.d.ts +13 -0
- package/dist/scripts/migration/passes/02-tags.js +150 -0
- package/dist/scripts/migration/passes/03-equipment.d.ts +14 -0
- package/dist/scripts/migration/passes/03-equipment.js +176 -0
- package/dist/scripts/migration/passes/04-projects.d.ts +27 -0
- package/dist/scripts/migration/passes/04-projects.js +241 -0
- package/dist/scripts/migration/passes/05-articles.d.ts +25 -0
- package/dist/scripts/migration/passes/05-articles.js +203 -0
- package/dist/scripts/migration/passes/06-formconfigs.d.ts +17 -0
- package/dist/scripts/migration/passes/06-formconfigs.js +166 -0
- package/dist/scripts/migration/passes/07-meta-configs.d.ts +46 -0
- package/dist/scripts/migration/passes/07-meta-configs.js +185 -0
- package/dist/scripts/migration/passes/08-tickets.d.ts +27 -0
- package/dist/scripts/migration/passes/08-tickets.js +353 -0
- package/dist/scripts/migration/passes/09-ticket-meta.d.ts +12 -0
- package/dist/scripts/migration/passes/09-ticket-meta.js +136 -0
- package/dist/scripts/migration/passes/10-chat.d.ts +17 -0
- package/dist/scripts/migration/passes/10-chat.js +230 -0
- package/dist/scripts/migration/passes/11-assets.d.ts +17 -0
- package/dist/scripts/migration/passes/11-assets.js +93 -0
- package/dist/scripts/migration/passes/12-history.d.ts +24 -0
- package/dist/scripts/migration/passes/12-history.js +332 -0
- package/dist/scripts/migration/run.d.ts +1 -0
- package/dist/scripts/migration/run.js +148 -0
- package/dist/scripts/migration/verify.d.ts +1 -0
- package/dist/scripts/migration/verify.js +620 -0
- package/dist/scripts/seed-v1-testdata.d.ts +41 -0
- package/dist/scripts/seed-v1-testdata.js +543 -0
- package/dist/types/BDE_Article.d.ts +3 -0
- package/dist/types/BDE_Article.js +6 -0
- package/dist/types/Chat_Message.d.ts +49 -0
- package/dist/types/Chat_Message.js +87 -0
- package/dist/types/MES_RessourceOverview.d.ts +38 -0
- package/dist/types/MES_RessourceOverview.js +63 -0
- package/dist/types/Meta_Context.d.ts +43 -0
- package/dist/types/Meta_Context.js +75 -0
- package/dist/types/Service_Equipment.d.ts +33 -0
- package/dist/types/Service_Equipment.js +57 -0
- package/dist/types/Service_FormConfig.d.ts +33 -0
- package/dist/types/Service_FormConfig.js +57 -0
- package/dist/types/Service_MeterReading.d.ts +35 -0
- package/dist/types/Service_MeterReading.js +57 -0
- package/dist/types/Service_Project.d.ts +53 -0
- package/dist/types/Service_Project.js +93 -0
- package/dist/types/Service_Schedule.d.ts +66 -0
- package/dist/types/Service_Schedule.js +117 -0
- package/dist/types/Service_Schedule_Execution.d.ts +38 -0
- package/dist/types/Service_Schedule_Execution.js +63 -0
- package/dist/types/Service_Schedule_Template.d.ts +36 -0
- package/dist/types/Service_Schedule_Template.js +60 -0
- package/dist/types/Service_Status.d.ts +30 -0
- package/dist/types/Service_Status.js +51 -0
- package/dist/types/Service_Tag.d.ts +30 -0
- package/dist/types/Service_Tag.js +51 -0
- package/dist/types/Service_Ticket.d.ts +92 -0
- package/dist/types/Service_Ticket.js +165 -0
- package/dist/types/Service_TicketCounter.d.ts +18 -0
- package/dist/types/Service_TicketCounter.js +27 -0
- package/dist/types/Service_TriggerState.d.ts +31 -0
- package/dist/types/Service_TriggerState.js +51 -0
- package/dist/types/_Session.d.ts +0 -1
- package/dist/types/_User.d.ts +2 -6
- package/dist/types/custom/MetaField.d.ts +9 -0
- package/dist/types/custom/TicketMaterial.d.ts +32 -0
- package/dist/types/custom/TicketMaterial.js +2 -0
- package/dist/types/index.d.ts +30 -0
- package/dist/types/index.js +33 -3
- package/package.json +17 -14
- package/schema/BDE_Article.json +3 -0
- package/schema/Changelog.json +3 -6
- package/schema/Chat_Message.json +88 -0
- package/schema/Meta_Context.json +78 -0
- package/schema/Service_Equipment.json +55 -0
- package/schema/Service_FormConfig.json +66 -0
- package/schema/Service_MeterReading.json +54 -0
- package/schema/Service_MigrationMap.json +55 -0
- package/schema/Service_Project.json +77 -0
- package/schema/Service_Schedule.json +101 -0
- package/schema/Service_Schedule_Execution.json +58 -0
- package/schema/Service_Schedule_Template.json +73 -0
- package/schema/Service_Status.json +50 -0
- package/schema/Service_Tag.json +50 -0
- package/schema/Service_Ticket.json +114 -0
- package/schema/Service_TicketCounter.json +38 -0
- package/schema/Service_TriggerState.json +50 -0
|
@@ -0,0 +1,168 @@
|
|
|
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/schema/index.js");
|
|
9
|
+
const index_js_2 = require("../types/index.js");
|
|
10
|
+
async function init() {
|
|
11
|
+
(0, index_js_1.beforeSaveHook)(index_js_2.Service_Tag, async (request) => {
|
|
12
|
+
const { object, original, user } = request;
|
|
13
|
+
await (0, index_js_1.defaultHandler)(request);
|
|
14
|
+
await (0, index_js_1.defaultAclHandler)(request);
|
|
15
|
+
// TODO
|
|
16
|
+
});
|
|
17
|
+
(0, index_js_1.afterSaveHook)(index_js_2.Service_Tag, async ({ object, original }) => {
|
|
18
|
+
// Entities store the *ancestor closure* of the tags picked on them (see the
|
|
19
|
+
// tags README). Re-parenting a tag changes those ancestors, so every
|
|
20
|
+
// equipment/ticket whose closure includes this tag must be rebuilt.
|
|
21
|
+
// A brand-new tag (no `original`) has nothing referencing it yet.
|
|
22
|
+
if (!original)
|
|
23
|
+
return;
|
|
24
|
+
// A migration re-run can re-parent an already-imported tag; rebuilding
|
|
25
|
+
// closures mid-import would race the pass that is still writing them. The
|
|
26
|
+
// migration rebuilds once at the end instead. See helper/migrationMode.ts.
|
|
27
|
+
if ((0, index_js_1.isMigrationMode)())
|
|
28
|
+
return;
|
|
29
|
+
const oldParentId = getParentId(original);
|
|
30
|
+
const newParentId = getParentId(object);
|
|
31
|
+
if (oldParentId === newParentId)
|
|
32
|
+
return; // parent unchanged → closures fine
|
|
33
|
+
await reconcileClosures(object, oldParentId, newParentId);
|
|
34
|
+
});
|
|
35
|
+
(0, index_js_1.afterDeleteHook)(index_js_2.Service_Tag, async (request) => {
|
|
36
|
+
const { object } = request;
|
|
37
|
+
await cleanupTagArrays(object);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async function cleanupTagArrays(object) {
|
|
41
|
+
if (!object.id)
|
|
42
|
+
return;
|
|
43
|
+
await Promise.all([
|
|
44
|
+
removeTagFromClass(index_js_2.Service_Ticket, object),
|
|
45
|
+
removeTagFromClass(index_js_2.Service_Equipment, object),
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
48
|
+
async function removeTagFromClass(ObjectClass, tag) {
|
|
49
|
+
const query = new node_1.default.Query(ObjectClass);
|
|
50
|
+
query.equalTo("tags", tag.id);
|
|
51
|
+
// A tag can only be referenced by objects of the same tenant, so we narrow
|
|
52
|
+
// the query to avoid scanning (and touching) other tenants' data.
|
|
53
|
+
if (tag.tenant) {
|
|
54
|
+
query.equalTo("tenant", tag.tenant);
|
|
55
|
+
}
|
|
56
|
+
const affected = await query.findAll({ useMasterKey: true });
|
|
57
|
+
for (const object of affected) {
|
|
58
|
+
object.remove("tags", tag.id);
|
|
59
|
+
}
|
|
60
|
+
if (affected.length > 0) {
|
|
61
|
+
await node_1.default.Object.saveAll(affected, { useMasterKey: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function getParentId(tag) {
|
|
65
|
+
return tag.get("parent")?.id ?? null;
|
|
66
|
+
}
|
|
67
|
+
/** Ancestor ids, nearest first; stops on an unknown parent, breaks cycles. */
|
|
68
|
+
function ancestorsOf(id, parents) {
|
|
69
|
+
const out = [];
|
|
70
|
+
const seen = new Set([id]);
|
|
71
|
+
let cur = parents.get(id) ?? null;
|
|
72
|
+
while (cur && !seen.has(cur)) {
|
|
73
|
+
seen.add(cur);
|
|
74
|
+
out.push(cur);
|
|
75
|
+
cur = parents.get(cur) ?? null;
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
/** Every id plus all of its ancestors, de-duplicated, order preserved. */
|
|
80
|
+
function closureOf(ids, parents) {
|
|
81
|
+
const out = [];
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
const add = (id) => {
|
|
84
|
+
if (seen.has(id))
|
|
85
|
+
return;
|
|
86
|
+
seen.add(id);
|
|
87
|
+
out.push(id);
|
|
88
|
+
};
|
|
89
|
+
for (const id of ids) {
|
|
90
|
+
add(id);
|
|
91
|
+
for (const anc of ancestorsOf(id, parents))
|
|
92
|
+
add(anc);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
/** Collapse a closure back to only its deepest members. Unknown ids are kept. */
|
|
97
|
+
function deepestOf(ids, parents) {
|
|
98
|
+
const set = new Set(ids);
|
|
99
|
+
const impliedByChild = new Set();
|
|
100
|
+
for (const id of ids) {
|
|
101
|
+
for (const anc of ancestorsOf(id, parents)) {
|
|
102
|
+
if (set.has(anc))
|
|
103
|
+
impliedByChild.add(anc);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const seen = new Set();
|
|
107
|
+
return ids.filter((id) => {
|
|
108
|
+
if (impliedByChild.has(id) || seen.has(id))
|
|
109
|
+
return false;
|
|
110
|
+
seen.add(id);
|
|
111
|
+
return true;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function sameMembers(a, b) {
|
|
115
|
+
if (a.length !== b.length)
|
|
116
|
+
return false;
|
|
117
|
+
const set = new Set(a);
|
|
118
|
+
return b.every((x) => set.has(x));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Rebuild the ancestor closures stored on equipment/tickets after `tag` was
|
|
122
|
+
* re-parented. Only this tag moved, so the "old tree" is the current tree with
|
|
123
|
+
* this tag's parent reverted — recovering the user's real (deepest) picks under
|
|
124
|
+
* the old tree lets us drop now-invalid ancestors when re-expanding under the
|
|
125
|
+
* new tree.
|
|
126
|
+
*/
|
|
127
|
+
async function reconcileClosures(tag, oldParentId, newParentId) {
|
|
128
|
+
const tagId = tag.id;
|
|
129
|
+
if (!tagId)
|
|
130
|
+
return;
|
|
131
|
+
const tenant = tag.tenant;
|
|
132
|
+
const query = new node_1.default.Query(index_js_2.Service_Tag);
|
|
133
|
+
if (tenant)
|
|
134
|
+
query.equalTo("tenant", tenant);
|
|
135
|
+
const allTags = await query.findAll({ useMasterKey: true });
|
|
136
|
+
const newParents = new Map();
|
|
137
|
+
for (const t of allTags) {
|
|
138
|
+
if (t.id)
|
|
139
|
+
newParents.set(t.id, getParentId(t));
|
|
140
|
+
}
|
|
141
|
+
newParents.set(tagId, newParentId);
|
|
142
|
+
const oldParents = new Map(newParents);
|
|
143
|
+
oldParents.set(tagId, oldParentId);
|
|
144
|
+
await Promise.all([
|
|
145
|
+
reconcileClass(index_js_2.Service_Ticket, tagId, tenant, oldParents, newParents),
|
|
146
|
+
reconcileClass(index_js_2.Service_Equipment, tagId, tenant, oldParents, newParents),
|
|
147
|
+
]);
|
|
148
|
+
}
|
|
149
|
+
async function reconcileClass(ObjectClass, tagId, tenant, oldParents, newParents) {
|
|
150
|
+
const query = new node_1.default.Query(ObjectClass);
|
|
151
|
+
query.equalTo("tags", tagId);
|
|
152
|
+
// A tag is only referenced by objects of the same tenant.
|
|
153
|
+
if (tenant)
|
|
154
|
+
query.equalTo("tenant", tenant);
|
|
155
|
+
const affected = await query.findAll({ useMasterKey: true });
|
|
156
|
+
const toSave = [];
|
|
157
|
+
for (const object of affected) {
|
|
158
|
+
const current = object.get("tags") ?? [];
|
|
159
|
+
const rebuilt = closureOf(deepestOf(current, oldParents), newParents);
|
|
160
|
+
if (!sameMembers(current, rebuilt)) {
|
|
161
|
+
object.set("tags", rebuilt);
|
|
162
|
+
toSave.push(object);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (toSave.length > 0) {
|
|
166
|
+
await node_1.default.Object.saveAll(toSave, { useMasterKey: true });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function init(): Promise<void>;
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.init = init;
|
|
40
|
+
const node_1 = __importStar(require("parse/node"));
|
|
41
|
+
const i18next_1 = __importDefault(require("i18next"));
|
|
42
|
+
const index_js_1 = require("../features/schema/index.js");
|
|
43
|
+
const getUserLanguage_js_1 = require("../helper/getUserLanguage.js");
|
|
44
|
+
const index_js_2 = require("../types/index.js");
|
|
45
|
+
const index_js_3 = require("../features/config/index.js");
|
|
46
|
+
const applyRuleSet_js_1 = require("../features/ruleset/applyRuleSet.js");
|
|
47
|
+
async function init() {
|
|
48
|
+
(0, index_js_1.beforeSaveHook)("OD3_Service_Ticket", async (request) => {
|
|
49
|
+
const { object, original, user } = request;
|
|
50
|
+
const isNew = object.isNew();
|
|
51
|
+
if (isNew && !object.get("createdBy") && user)
|
|
52
|
+
object.set("createdBy", user);
|
|
53
|
+
await (0, index_js_1.defaultHandler)(request);
|
|
54
|
+
await (0, index_js_1.defaultAclHandler)(request, {
|
|
55
|
+
allowCustomACL: true,
|
|
56
|
+
allowTenantUserWrite: true,
|
|
57
|
+
});
|
|
58
|
+
await setTicketACL(object);
|
|
59
|
+
// Assign a sequential ticket number on creation. Done here (backend) rather
|
|
60
|
+
// than in the frontend because every write path — the create form, the
|
|
61
|
+
// cross-plugin service, and the generic entity admin — funnels through this
|
|
62
|
+
// beforeSave, and the counter increment is atomic (no client race).
|
|
63
|
+
//
|
|
64
|
+
// Also re-number when an existing ticket's `project` changes: numbers are a
|
|
65
|
+
// per-project sequence, so a ticket keeping its old-project number after a
|
|
66
|
+
// move could collide with an existing number in the new project. Drawing a
|
|
67
|
+
// fresh number from the destination keeps each project's sequence unique
|
|
68
|
+
// (the source sequence simply gains a gap, which is fine).
|
|
69
|
+
const projectChanged = !isNew && object.dirty("project");
|
|
70
|
+
if ((isNew && object.get("number") == null) || projectChanged) {
|
|
71
|
+
await assignTicketNumber(object);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
(0, index_js_1.afterDeleteHook)(index_js_2.Service_Ticket, async ({ object }) => {
|
|
75
|
+
if (object.id)
|
|
76
|
+
await cleanupTicketRelations(object.id);
|
|
77
|
+
});
|
|
78
|
+
(0, index_js_1.afterSaveHook)(index_js_2.Service_Ticket, async ({ object, original, user }) => {
|
|
79
|
+
// A bulk import would otherwise notify every assignee once per migrated
|
|
80
|
+
// ticket (~10k per tenant). See helper/migrationMode.ts.
|
|
81
|
+
if ((0, index_js_1.isMigrationMode)())
|
|
82
|
+
return;
|
|
83
|
+
try {
|
|
84
|
+
// Shared across the assignee + watcher passes so a user who is both an
|
|
85
|
+
// assignee and a matched watcher is notified only once (assignee first).
|
|
86
|
+
const notified = new Set();
|
|
87
|
+
if (user?.id)
|
|
88
|
+
notified.add(user.id); // never notify the acting user
|
|
89
|
+
const assignerName = getUsername(user);
|
|
90
|
+
await notifyNewAssignees(object, original, notified, assignerName);
|
|
91
|
+
// Conditional "watcher" rules are evaluated on ticket creation only for
|
|
92
|
+
// now — re-evaluating them on every edit (and tracking who was already
|
|
93
|
+
// notified across edits) is deferred.
|
|
94
|
+
if (!original) {
|
|
95
|
+
await notifyWatchers(object, notified, assignerName);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
console.error("[@openinc/parse-server-opendash][Service_Ticket] Failed to notify on assignment", error);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** Upper bound on related records deleted per ticket — far above any realistic
|
|
104
|
+
* meta/chat/notification count for a single ticket. */
|
|
105
|
+
const RELATED_DELETE_LIMIT = 10000;
|
|
106
|
+
/**
|
|
107
|
+
* Removes the data attached to a ticket after it is deleted. Parse cannot
|
|
108
|
+
* cascade these on its own: meta head-data ({@link Meta_Entry}) and chat
|
|
109
|
+
* messages ({@link Chat_Message}) link to the ticket by a plain string key
|
|
110
|
+
* rather than a pointer, and notifications reference it inside their JSON `data`
|
|
111
|
+
* blob. Without this hook all three would be orphaned.
|
|
112
|
+
*
|
|
113
|
+
* Child tickets (the `parent` self-pointer) are intentionally left untouched.
|
|
114
|
+
*
|
|
115
|
+
* This is the single cleanup path for both single deletes and the dev
|
|
116
|
+
* "delete all" command, so the frontend no longer needs to delete related data
|
|
117
|
+
* itself.
|
|
118
|
+
*/
|
|
119
|
+
async function cleanupTicketRelations(ticketId) {
|
|
120
|
+
const [metaEntries, chatMessages, notifications] = await Promise.all([
|
|
121
|
+
new node_1.default.Query(index_js_2.Meta_Entry)
|
|
122
|
+
.equalTo("entryClassName", index_js_2.Service_Ticket.className)
|
|
123
|
+
.equalTo("entryObjectId", ticketId)
|
|
124
|
+
.limit(RELATED_DELETE_LIMIT)
|
|
125
|
+
.find({ useMasterKey: true }),
|
|
126
|
+
// Chat context is stored as `contextToString(["service","ticket",id])`,
|
|
127
|
+
// i.e. a JSON.stringify of the tuple — match it exactly.
|
|
128
|
+
new node_1.default.Query(index_js_2.Chat_Message)
|
|
129
|
+
.equalTo("context", JSON.stringify(["service", "ticket", ticketId]))
|
|
130
|
+
.limit(RELATED_DELETE_LIMIT)
|
|
131
|
+
.find({ useMasterKey: true }),
|
|
132
|
+
// `data` is a JSON Object column; Parse supports dot-notation into it. Query
|
|
133
|
+
// by class name (untyped) so the `data.ticketId` path key is accepted.
|
|
134
|
+
new node_1.default.Query(index_js_2.Notification.className)
|
|
135
|
+
.equalTo("data.ticketId", ticketId)
|
|
136
|
+
.limit(RELATED_DELETE_LIMIT)
|
|
137
|
+
.find({ useMasterKey: true }),
|
|
138
|
+
]);
|
|
139
|
+
await node_1.default.Object.destroyAll([...metaEntries, ...chatMessages, ...notifications], { useMasterKey: true });
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Assigns a sequential `number` to a new ticket.
|
|
143
|
+
*
|
|
144
|
+
* - With a project: atomically increments that project's `ticketCounter` and
|
|
145
|
+
* uses the result, so numbers are per-project sequences.
|
|
146
|
+
* - Without a project: falls back to a per-tenant counter
|
|
147
|
+
* (`OD3_Service_TicketCounter`, one row per tenant), so every ticket still
|
|
148
|
+
* gets a number.
|
|
149
|
+
*
|
|
150
|
+
* The increments use Parse's atomic `$inc`, so concurrent creates never collide.
|
|
151
|
+
*/
|
|
152
|
+
async function assignTicketNumber(ticket) {
|
|
153
|
+
const project = ticket.get("project");
|
|
154
|
+
if (project) {
|
|
155
|
+
await project.fetch({ useMasterKey: true });
|
|
156
|
+
project.increment("ticketCounter", 1);
|
|
157
|
+
await project.save(null, { useMasterKey: true });
|
|
158
|
+
ticket.set("number", project.get("ticketCounter"));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const tenant = ticket.get("tenant");
|
|
162
|
+
if (!tenant)
|
|
163
|
+
return; // no scope to count within — leave unnumbered
|
|
164
|
+
ticket.set("number", await nextTenantTicketNumber(tenant));
|
|
165
|
+
}
|
|
166
|
+
/** Find-or-create the tenant's ticket counter and return the next value. */
|
|
167
|
+
async function nextTenantTicketNumber(tenant) {
|
|
168
|
+
const existing = await new node_1.default.Query("OD3_Service_TicketCounter")
|
|
169
|
+
.equalTo("tenant", tenant)
|
|
170
|
+
.first({ useMasterKey: true });
|
|
171
|
+
if (existing) {
|
|
172
|
+
existing.increment("value", 1);
|
|
173
|
+
await existing.save(null, { useMasterKey: true });
|
|
174
|
+
return existing.get("value");
|
|
175
|
+
}
|
|
176
|
+
const Counter = node_1.default.Object.extend("OD3_Service_TicketCounter");
|
|
177
|
+
const counter = new Counter();
|
|
178
|
+
counter.set("tenant", tenant);
|
|
179
|
+
counter.set("value", 1);
|
|
180
|
+
await counter.save(null, { useMasterKey: true });
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Sets the ticket ACL from `createdBy`, `assignedUsers` and `assignedRoles` —
|
|
185
|
+
* each gets read/write. `assignedUsers`/`assignedRoles` are arrays of Parse
|
|
186
|
+
* pointers, so we iterate the entries (by object id) rather than the array
|
|
187
|
+
* indices. Parse ACL role grants require the role *name*, not its id, so the
|
|
188
|
+
* assigned role pointers are resolved to names first.
|
|
189
|
+
*/
|
|
190
|
+
async function setTicketACL(object) {
|
|
191
|
+
const acl = object.getACL() ?? new node_1.default.ACL();
|
|
192
|
+
// deny global read / write
|
|
193
|
+
acl.setPublicReadAccess(false);
|
|
194
|
+
acl.setPublicWriteAccess(false);
|
|
195
|
+
// creator read/write
|
|
196
|
+
const creatorId = object.get("createdBy")?.id;
|
|
197
|
+
if (creatorId) {
|
|
198
|
+
acl.setReadAccess(creatorId, true);
|
|
199
|
+
acl.setWriteAccess(creatorId, true);
|
|
200
|
+
}
|
|
201
|
+
// assigned users read/write
|
|
202
|
+
for (const userId of pointerIds(object.get("assignedUsers"))) {
|
|
203
|
+
acl.setReadAccess(userId, true);
|
|
204
|
+
acl.setWriteAccess(userId, true);
|
|
205
|
+
}
|
|
206
|
+
// assigned roles read/write (resolve pointer ids → role names)
|
|
207
|
+
const roleIds = pointerIds(object.get("assignedRoles"));
|
|
208
|
+
if (roleIds.length > 0) {
|
|
209
|
+
const roles = await new node_1.default.Query(node_1.default.Role)
|
|
210
|
+
.containedIn("objectId", roleIds)
|
|
211
|
+
.find({ useMasterKey: true });
|
|
212
|
+
for (const role of roles) {
|
|
213
|
+
const name = role.getName();
|
|
214
|
+
if (name) {
|
|
215
|
+
acl.setRoleReadAccess(name, true);
|
|
216
|
+
acl.setRoleWriteAccess(name, true);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
object.setACL(acl);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Notifies the users and roles that were newly assigned to a ticket.
|
|
224
|
+
*
|
|
225
|
+
* Runs on every save and diffs the current `assignedUsers`/`assignedRoles`
|
|
226
|
+
* (arrays of pointers) against the previous version (`original`). On creation
|
|
227
|
+
* `original` is undefined, so everyone assigned is new; on an edit only the
|
|
228
|
+
* added assignees are notified, so re-saving a ticket never re-notifies people
|
|
229
|
+
* who were already assigned.
|
|
230
|
+
*
|
|
231
|
+
* Dedup + precedence mirror the chat-mention feature: one notification per user,
|
|
232
|
+
* directly assigned users win over role membership (`users` > `roles`), and the
|
|
233
|
+
* acting user (whoever performed the save) is never notified.
|
|
234
|
+
*/
|
|
235
|
+
async function notifyNewAssignees(ticket, original, notified, assignerName) {
|
|
236
|
+
const newUserIds = addedIds(ticket.get("assignedUsers"), original?.get("assignedUsers"));
|
|
237
|
+
const newRoleIds = addedIds(ticket.get("assignedRoles"), original?.get("assignedRoles"));
|
|
238
|
+
if (newUserIds.length === 0 && newRoleIds.length === 0)
|
|
239
|
+
return;
|
|
240
|
+
await notifyRecipients(newUserIds, newRoleIds, notified, ticket, {
|
|
241
|
+
variant: "assigned",
|
|
242
|
+
assignerName,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/** OD3_Config key holding the per-tenant assignment-notification rules. */
|
|
246
|
+
const ASSIGNMENT_NOTIFICATION_CONFIG_KEY = "OPENSERVICE_ASSIGNMENT_NOTIFICATIONS";
|
|
247
|
+
/**
|
|
248
|
+
* Notifies the configured "watchers": users/roles that a conditional ruleset
|
|
249
|
+
* says should hear about a ticket, independent of assignment. The rules live in
|
|
250
|
+
* a per-tenant {@link Config} row (`OPENSERVICE_ASSIGNMENT_NOTIFICATIONS`) as a
|
|
251
|
+
* JSON array of RuleSet function entries authored in the frontend admin. Each
|
|
252
|
+
* matching rule's assignees (`user:<id>` / `role:<id>`) are notified, deduped
|
|
253
|
+
* against the shared `notified` set (so assignees already handled win).
|
|
254
|
+
*/
|
|
255
|
+
async function notifyWatchers(ticket, notified, assignerName) {
|
|
256
|
+
const rules = await loadWatcherRules(ticket.get("tenant"));
|
|
257
|
+
if (rules.length === 0)
|
|
258
|
+
return;
|
|
259
|
+
const getValue = (field) => ticket.get(field);
|
|
260
|
+
for (const rule of rules) {
|
|
261
|
+
if (rule.function !== "notify" || !rule.ruleset)
|
|
262
|
+
continue;
|
|
263
|
+
if (!(0, applyRuleSet_js_1.applyRuleSet)(rule.ruleset, getValue))
|
|
264
|
+
continue;
|
|
265
|
+
const assignees = Array.isArray(rule.params?.assignees)
|
|
266
|
+
? rule.params.assignees
|
|
267
|
+
: [];
|
|
268
|
+
const userIds = assignees
|
|
269
|
+
.filter((a) => a.startsWith("user:"))
|
|
270
|
+
.map((a) => a.slice(5));
|
|
271
|
+
const roleIds = assignees
|
|
272
|
+
.filter((a) => a.startsWith("role:"))
|
|
273
|
+
.map((a) => a.slice(5));
|
|
274
|
+
await notifyRecipients(userIds, roleIds, notified, ticket, {
|
|
275
|
+
variant: "watcher",
|
|
276
|
+
assignerName,
|
|
277
|
+
frontpage: rule.params?.frontpage === true,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/** Loads and parses the tenant's assignment-notification rules. */
|
|
282
|
+
async function loadWatcherRules(tenant) {
|
|
283
|
+
if (!tenant)
|
|
284
|
+
return [];
|
|
285
|
+
const config = await new node_1.default.Query(index_js_2.Config)
|
|
286
|
+
.equalTo("key", ASSIGNMENT_NOTIFICATION_CONFIG_KEY)
|
|
287
|
+
.equalTo("tenant", tenant)
|
|
288
|
+
.first({ useMasterKey: true });
|
|
289
|
+
const raw = config?.get("value");
|
|
290
|
+
if (typeof raw !== "string")
|
|
291
|
+
return [];
|
|
292
|
+
try {
|
|
293
|
+
const parsed = JSON.parse(raw);
|
|
294
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Resolves a set of user ids + role ids to concrete recipients and sends one
|
|
302
|
+
* notification each, skipping anyone already in `notified`. Directly listed
|
|
303
|
+
* users take precedence over users reached via a role. `notified` is mutated so
|
|
304
|
+
* callers can chain multiple passes (assignees, then watchers) with shared
|
|
305
|
+
* de-duplication.
|
|
306
|
+
*/
|
|
307
|
+
async function notifyRecipients(userIds, roleIds, notified, ticket, options) {
|
|
308
|
+
const users = userIds.length
|
|
309
|
+
? await new node_1.default.Query(node_1.default.User)
|
|
310
|
+
.containedIn("objectId", userIds)
|
|
311
|
+
.findAll({ useMasterKey: true })
|
|
312
|
+
: [];
|
|
313
|
+
for (const user of users) {
|
|
314
|
+
if (!user.id || notified.has(user.id))
|
|
315
|
+
continue;
|
|
316
|
+
notified.add(user.id);
|
|
317
|
+
await sendAssignmentNotification(user, ticket, options);
|
|
318
|
+
}
|
|
319
|
+
const roles = roleIds.length
|
|
320
|
+
? await new node_1.default.Query(node_1.Role)
|
|
321
|
+
.containedIn("objectId", roleIds)
|
|
322
|
+
.findAll({ useMasterKey: true })
|
|
323
|
+
: [];
|
|
324
|
+
for (const role of roles) {
|
|
325
|
+
const members = await role
|
|
326
|
+
.relation("users")
|
|
327
|
+
.query()
|
|
328
|
+
.findAll({ useMasterKey: true });
|
|
329
|
+
for (const member of members) {
|
|
330
|
+
if (!member.id || notified.has(member.id))
|
|
331
|
+
continue;
|
|
332
|
+
notified.add(member.id);
|
|
333
|
+
await sendAssignmentNotification(member, ticket, options);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Builds and saves a single assignment notification, translated into the
|
|
339
|
+
* recipient's preferred language. Delivery (email + web push) is handled
|
|
340
|
+
* automatically by the {@link Notification} afterSave hook.
|
|
341
|
+
*/
|
|
342
|
+
async function sendAssignmentNotification(user, ticket, options) {
|
|
343
|
+
await i18next_1.default.changeLanguage(await (0, getUserLanguage_js_1.getUserLanguage)(user));
|
|
344
|
+
const ticketLink = await getTicketLink(ticket, user);
|
|
345
|
+
const base = `openservice:ticket.assignment.${options.variant}`;
|
|
346
|
+
const ticketName = ticket.get("title");
|
|
347
|
+
await new index_js_2.Notification({
|
|
348
|
+
title: i18next_1.default.t(`${base}.title`),
|
|
349
|
+
description: i18next_1.default.t(`${base}.description`, {
|
|
350
|
+
username: options.assignerName,
|
|
351
|
+
ticketName,
|
|
352
|
+
ticketLink,
|
|
353
|
+
}),
|
|
354
|
+
type: options.frontpage ? "frontpage" : "mailbox",
|
|
355
|
+
data: {
|
|
356
|
+
ticketId: ticket.id,
|
|
357
|
+
translation: {
|
|
358
|
+
username: options.assignerName,
|
|
359
|
+
ticketName,
|
|
360
|
+
ticketId: ticket.id,
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
user: user,
|
|
364
|
+
isSent: false,
|
|
365
|
+
}).save(null, { useMasterKey: true });
|
|
366
|
+
}
|
|
367
|
+
/** Ids present in `current` (array of pointers) but not in `previous`. */
|
|
368
|
+
function addedIds(current, previous) {
|
|
369
|
+
const previousIds = new Set(pointerIds(previous));
|
|
370
|
+
return pointerIds(current).filter((id) => !previousIds.has(id));
|
|
371
|
+
}
|
|
372
|
+
/** Extracts the object ids from an array of Parse pointers, tolerating
|
|
373
|
+
* undefined / malformed entries. */
|
|
374
|
+
function pointerIds(value) {
|
|
375
|
+
if (!Array.isArray(value))
|
|
376
|
+
return [];
|
|
377
|
+
return value
|
|
378
|
+
.map((entry) => {
|
|
379
|
+
// The entity adapter stores array-of-ref fields as plain id
|
|
380
|
+
// STRINGS, not Parse.Objects. Reading `.id` off them returned
|
|
381
|
+
// undefined, so setTicketACL granted assignees nothing and
|
|
382
|
+
// notifyNewAssignees found nobody — silently, for every ticket.
|
|
383
|
+
if (typeof entry === "string")
|
|
384
|
+
return entry;
|
|
385
|
+
if (entry && typeof entry === "object") {
|
|
386
|
+
const o = entry;
|
|
387
|
+
return o.id ?? o.objectId;
|
|
388
|
+
}
|
|
389
|
+
return undefined;
|
|
390
|
+
})
|
|
391
|
+
.filter((id) => typeof id === "string");
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Returns the link to a ticket.
|
|
395
|
+
* @param ticket
|
|
396
|
+
* @returns
|
|
397
|
+
*/
|
|
398
|
+
async function getTicketLink(ticket, user) {
|
|
399
|
+
//Get hostname from tenant or environment variable or default to localhost
|
|
400
|
+
let hostname = "localhost";
|
|
401
|
+
const tenant = (await user.fetchWithInclude("tenant", { useMasterKey: true })).get("tenant");
|
|
402
|
+
try {
|
|
403
|
+
const baseurl = tenant?.get("baseUrl");
|
|
404
|
+
const publicurl = index_js_3.ConfigInstance.getInstance()
|
|
405
|
+
.get("PARSE_PUBLIC_SERVER_URL")
|
|
406
|
+
.replace("/parse", "");
|
|
407
|
+
if (baseurl !== undefined && baseurl !== "") {
|
|
408
|
+
hostname = baseurl;
|
|
409
|
+
}
|
|
410
|
+
else if (publicurl) {
|
|
411
|
+
hostname = publicurl;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
console.warn("[@openinc/parse-server-opendash] Could not get a hostname, defaulting to localhost");
|
|
416
|
+
}
|
|
417
|
+
return `${hostname}/service/tickets/${ticket.id}`;
|
|
418
|
+
}
|
|
419
|
+
function getUsername(user) {
|
|
420
|
+
if (!user)
|
|
421
|
+
return "System";
|
|
422
|
+
let username = "";
|
|
423
|
+
if (user?.get("firstname") && user?.get("lastname")) {
|
|
424
|
+
username = user?.get("firstname") + " " + user?.get("lastname");
|
|
425
|
+
}
|
|
426
|
+
else if (user?.get("name")) {
|
|
427
|
+
username = user.get("name");
|
|
428
|
+
}
|
|
429
|
+
else if (user?.getUsername()) {
|
|
430
|
+
username = user?.getUsername();
|
|
431
|
+
}
|
|
432
|
+
return username ?? "System";
|
|
433
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function init(): Promise<void>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.init = init;
|
|
4
|
+
const index_js_1 = require("../features/schema/index.js");
|
|
5
|
+
const index_js_2 = require("../types/index.js");
|
|
6
|
+
async function init() {
|
|
7
|
+
(0, index_js_1.beforeSaveHook)(index_js_2.Service_TriggerState, async (request) => {
|
|
8
|
+
const { object, original, user } = request;
|
|
9
|
+
await (0, index_js_1.defaultHandler)(request);
|
|
10
|
+
await (0, index_js_1.defaultAclHandler)(request);
|
|
11
|
+
// TODO
|
|
12
|
+
});
|
|
13
|
+
(0, index_js_1.afterSaveHook)(index_js_2.Service_TriggerState, async (request) => {
|
|
14
|
+
const { object, original, user } = request;
|
|
15
|
+
// TODO
|
|
16
|
+
});
|
|
17
|
+
}
|
package/dist/hooks/_ChangeLog.js
CHANGED
|
@@ -23,10 +23,24 @@ async function init() {
|
|
|
23
23
|
];
|
|
24
24
|
console.log(`ChangeLog blacklisted classes: ${blacklistedClasses.join(", ") || "None"}`);
|
|
25
25
|
(0, index_js_1.beforeSaveHook)(index_js_3.Changelog.className, async (request) => {
|
|
26
|
-
const { object,
|
|
26
|
+
const { object, user } = request;
|
|
27
27
|
await (0, index_js_1.defaultHandler)(request);
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// Mirror the ACL of the changed object instead of locking the changelog to
|
|
29
|
+
// the od-admin role: whoever can see the underlying object can see its
|
|
30
|
+
// history. The object snapshot lives in `value` for create/update and in
|
|
31
|
+
// `original` for delete (both are `Parse.Object.toJSON()` output, which
|
|
32
|
+
// carries the object's ACL under the `ACL` key).
|
|
33
|
+
const snapshot = object.get("value") ?? object.get("original");
|
|
34
|
+
const sourceAcl = snapshot?.ACL;
|
|
35
|
+
if (sourceAcl) {
|
|
36
|
+
object.setACL(new node_1.default.ACL(sourceAcl));
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
// No ACL on the source (e.g. public or masterkey-only object): fall back
|
|
40
|
+
// to the restrictive default rather than leaving the changelog public.
|
|
41
|
+
await (0, index_js_1.defaultAclHandler)(request);
|
|
42
|
+
}
|
|
43
|
+
if (user && user.id && object.getACL()) {
|
|
30
44
|
object.getACL().setReadAccess(user.id, true);
|
|
31
45
|
}
|
|
32
46
|
});
|
|
@@ -77,9 +91,16 @@ async function init() {
|
|
|
77
91
|
(0, index_js_1.beforeDeleteHook)(classSchema.className, async (request) => {
|
|
78
92
|
console.log(`Creating ChangeLog entry for DELETE on class ${classSchema.className} and object ${request.object.id}`);
|
|
79
93
|
try {
|
|
80
|
-
const { object, user, master } = request;
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
const { object, user, master, context } = request;
|
|
95
|
+
// `context.skipChangelog` is honoured here as well as on afterSave. The
|
|
96
|
+
// two were asymmetric: a caller could suppress the create/update entry but
|
|
97
|
+
// not the delete entry, so a bulk cleanup (a migration rollback, a dev
|
|
98
|
+
// purge) wrote one delete entry per row into the very table it was trying
|
|
99
|
+
// to clear. Parse does pass `context` to beforeDelete, and the JS SDK
|
|
100
|
+
// forwards it on `destroy`/`destroyAll`, so nothing else was missing.
|
|
101
|
+
if (context?.skipChangelog ||
|
|
102
|
+
blacklistedClasses.includes(classSchema.className)) {
|
|
103
|
+
console.log(`Skipping ChangeLog entry for class ${classSchema.className} and object ${object.id} due to ${context?.skipChangelog ? "context flag" : "blacklisted class"}.`);
|
|
83
104
|
return;
|
|
84
105
|
}
|
|
85
106
|
const changelog = new index_js_3.Changelog();
|