@oondemand/oon-core-back 0.3.45 → 0.3.47
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/README.md +10 -1
- package/package.json +1 -1
- package/src/boot/loadCentral.js +15 -1
- package/src/core/define.js +9 -21
- package/src/core/factories/crudController.js +110 -42
- package/src/core/factories/crudService.js +24 -30
- package/src/delivery/buildImage.js +19 -0
- package/src/index.js +20 -1
- package/src/manifest/domainRules.js +95 -26
- package/src/manifest/processManifest.js +411 -0
- package/src/manifest/processRuntime.js +578 -0
- package/src/manifest/referencePolicies.js +108 -0
|
@@ -23,6 +23,9 @@ const EXPRESSION_OPERATORS = Object.freeze([
|
|
|
23
23
|
"not",
|
|
24
24
|
"present",
|
|
25
25
|
"in",
|
|
26
|
+
"if",
|
|
27
|
+
"concat",
|
|
28
|
+
"formatCurrency",
|
|
26
29
|
]);
|
|
27
30
|
|
|
28
31
|
class DomainRuleError extends GenericError {
|
|
@@ -30,7 +33,7 @@ class DomainRuleError extends GenericError {
|
|
|
30
33
|
const field = options.field || null;
|
|
31
34
|
const rule = options.rule || null;
|
|
32
35
|
super(message, {
|
|
33
|
-
statusCode: 422,
|
|
36
|
+
statusCode: options.statusCode || 422,
|
|
34
37
|
details: {
|
|
35
38
|
field,
|
|
36
39
|
message,
|
|
@@ -66,9 +69,7 @@ function isEmpty(value) {
|
|
|
66
69
|
}
|
|
67
70
|
|
|
68
71
|
function toNumber(value, context = {}) {
|
|
69
|
-
if (isEmpty(value))
|
|
70
|
-
return context.nullAsZero ? 0 : null;
|
|
71
|
-
}
|
|
72
|
+
if (isEmpty(value)) return context.nullAsZero ? 0 : null;
|
|
72
73
|
const numeric = typeof value === "number" ? value : Number(value);
|
|
73
74
|
if (!Number.isFinite(numeric)) {
|
|
74
75
|
throw new DomainRuleError(
|
|
@@ -202,6 +203,36 @@ function evaluateDomainExpression(expression, record = {}, context = {}) {
|
|
|
202
203
|
const list = evaluateDomainExpression(args[1], record, context);
|
|
203
204
|
return Array.isArray(list) && list.some((item) => Object.is(item, value));
|
|
204
205
|
}
|
|
206
|
+
case "if":
|
|
207
|
+
return Boolean(evaluateDomainExpression(args[0], record, context))
|
|
208
|
+
? evaluateDomainExpression(args[1], record, context)
|
|
209
|
+
: evaluateDomainExpression(args[2], record, context);
|
|
210
|
+
case "concat":
|
|
211
|
+
return args
|
|
212
|
+
.map((arg) => evaluateDomainExpression(arg, record, context))
|
|
213
|
+
.filter((value) => value !== null && value !== undefined)
|
|
214
|
+
.join("");
|
|
215
|
+
case "formatCurrency": {
|
|
216
|
+
const value = toNumber(evaluateDomainExpression(args[0], record, context), context);
|
|
217
|
+
if (value === null) return "";
|
|
218
|
+
const currency = evaluateDomainExpression(args[1] || { value: "BRL" }, record, context) || "BRL";
|
|
219
|
+
const locale = evaluateDomainExpression(args[2] || { value: "pt-BR" }, record, context) || "pt-BR";
|
|
220
|
+
try {
|
|
221
|
+
return new Intl.NumberFormat(String(locale), {
|
|
222
|
+
style: "currency",
|
|
223
|
+
currency: String(currency),
|
|
224
|
+
minimumFractionDigits: 2,
|
|
225
|
+
maximumFractionDigits: 2,
|
|
226
|
+
}).format(value);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
throw new DomainRuleError("Não foi possível formatar o valor monetário.", {
|
|
229
|
+
code: "OON_DOMAIN_FORMULA_ERROR",
|
|
230
|
+
field: context.field,
|
|
231
|
+
rule: context.rule,
|
|
232
|
+
details: { currency, locale, cause: error.message },
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
205
236
|
default:
|
|
206
237
|
throw new DomainRuleError(`Operador declarativo não suportado: ${String(operator)}.`, {
|
|
207
238
|
code: "OON_DOMAIN_EXPRESSION_INVALID",
|
|
@@ -221,6 +252,11 @@ function roundValue(value, precision) {
|
|
|
221
252
|
function valuesEquivalent(left, right) {
|
|
222
253
|
if (Object.is(left, right)) return true;
|
|
223
254
|
if (isEmpty(left) && isEmpty(right)) return true;
|
|
255
|
+
if (left instanceof Date || right instanceof Date) {
|
|
256
|
+
const leftDate = new Date(left).getTime();
|
|
257
|
+
const rightDate = new Date(right).getTime();
|
|
258
|
+
return Number.isFinite(leftDate) && Number.isFinite(rightDate) && leftDate === rightDate;
|
|
259
|
+
}
|
|
224
260
|
const leftNumber = Number(left);
|
|
225
261
|
const rightNumber = Number(right);
|
|
226
262
|
return Number.isFinite(leftNumber)
|
|
@@ -228,6 +264,13 @@ function valuesEquivalent(left, right) {
|
|
|
228
264
|
&& Object.is(leftNumber, rightNumber);
|
|
229
265
|
}
|
|
230
266
|
|
|
267
|
+
function detectChangedFields(current = {}, next = {}, candidates = null) {
|
|
268
|
+
const names = candidates
|
|
269
|
+
? [...new Set(candidates)]
|
|
270
|
+
: [...new Set([...Object.keys(current || {}), ...Object.keys(next || {})])];
|
|
271
|
+
return names.filter((field) => !valuesEquivalent(current?.[field], next?.[field]));
|
|
272
|
+
}
|
|
273
|
+
|
|
231
274
|
function readonlyViolation(field, rule) {
|
|
232
275
|
throw new DomainRuleError(`O campo ${field} é somente leitura e não pode ser alterado.`, {
|
|
233
276
|
code: "OON_DOMAIN_READONLY_FIELD",
|
|
@@ -236,10 +279,43 @@ function readonlyViolation(field, rule) {
|
|
|
236
279
|
});
|
|
237
280
|
}
|
|
238
281
|
|
|
239
|
-
function
|
|
282
|
+
function validateDomainRecord(rules, record) {
|
|
283
|
+
const domainRules = rules || {};
|
|
284
|
+
for (const validation of domainRules.validations || []) {
|
|
285
|
+
const context = {
|
|
286
|
+
field: validation.field,
|
|
287
|
+
rule: validation.name || validation.code || "validation",
|
|
288
|
+
};
|
|
289
|
+
if (validation.when
|
|
290
|
+
&& !Boolean(evaluateDomainExpression(validation.when, record, context))) {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (!Boolean(evaluateDomainExpression(validation.assert, record, context))) {
|
|
294
|
+
throw new DomainRuleError(validation.message, {
|
|
295
|
+
code: validation.code || "OON_DOMAIN_VALIDATION_FAILED",
|
|
296
|
+
field: validation.field,
|
|
297
|
+
rule: validation.name,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function applyDomainMutation({
|
|
304
|
+
rules,
|
|
305
|
+
current = null,
|
|
306
|
+
changes = {},
|
|
307
|
+
systemChanges = {},
|
|
308
|
+
op = "update",
|
|
309
|
+
skipValidations = false,
|
|
310
|
+
} = {}) {
|
|
240
311
|
const requestedChanges = changes && typeof changes === "object" && !Array.isArray(changes)
|
|
241
312
|
? { ...changes }
|
|
242
313
|
: {};
|
|
314
|
+
const trustedSystemChanges = systemChanges
|
|
315
|
+
&& typeof systemChanges === "object"
|
|
316
|
+
&& !Array.isArray(systemChanges)
|
|
317
|
+
? { ...systemChanges }
|
|
318
|
+
: {};
|
|
243
319
|
const sanitizedChanges = { ...requestedChanges };
|
|
244
320
|
const currentRecord = current && typeof current === "object" ? { ...current } : {};
|
|
245
321
|
const domainRules = rules || {};
|
|
@@ -257,10 +333,9 @@ function applyDomainMutation({ rules, current = null, changes = {}, op = "update
|
|
|
257
333
|
readonlyViolation(field, "readonly");
|
|
258
334
|
}
|
|
259
335
|
|
|
260
|
-
for (const computed of computedFields)
|
|
261
|
-
delete sanitizedChanges[computed.field];
|
|
262
|
-
}
|
|
336
|
+
for (const computed of computedFields) delete sanitizedChanges[computed.field];
|
|
263
337
|
|
|
338
|
+
Object.assign(sanitizedChanges, trustedSystemChanges);
|
|
264
339
|
const consolidated = { ...currentRecord, ...sanitizedChanges };
|
|
265
340
|
|
|
266
341
|
for (const computed of computedFields) {
|
|
@@ -282,28 +357,20 @@ function applyDomainMutation({ rules, current = null, changes = {}, op = "update
|
|
|
282
357
|
sanitizedChanges[computed.field] = normalized;
|
|
283
358
|
}
|
|
284
359
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
if (!Boolean(evaluateDomainExpression(validation.assert, consolidated, context))) {
|
|
295
|
-
throw new DomainRuleError(validation.message, {
|
|
296
|
-
code: validation.code || "OON_DOMAIN_VALIDATION_FAILED",
|
|
297
|
-
field: validation.field,
|
|
298
|
-
rule: validation.name,
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
}
|
|
360
|
+
if (!skipValidations) validateDomainRecord(domainRules, consolidated);
|
|
361
|
+
|
|
362
|
+
const candidateFields = [
|
|
363
|
+
...Object.keys(sanitizedChanges),
|
|
364
|
+
...Object.keys(trustedSystemChanges),
|
|
365
|
+
];
|
|
366
|
+
const changedFields = detectChangedFields(currentRecord, consolidated, candidateFields);
|
|
302
367
|
|
|
303
368
|
return {
|
|
304
369
|
changes: sanitizedChanges,
|
|
305
370
|
consolidated,
|
|
306
371
|
requestedChanges,
|
|
372
|
+
systemChanges: trustedSystemChanges,
|
|
373
|
+
changedFields,
|
|
307
374
|
};
|
|
308
375
|
}
|
|
309
376
|
|
|
@@ -312,7 +379,9 @@ module.exports = {
|
|
|
312
379
|
DomainRuleError,
|
|
313
380
|
evaluateDomainExpression,
|
|
314
381
|
applyDomainMutation,
|
|
382
|
+
validateDomainRecord,
|
|
383
|
+
detectChangedFields,
|
|
315
384
|
getFieldValue,
|
|
316
385
|
roundValue,
|
|
317
386
|
valuesEquivalent,
|
|
318
|
-
};
|
|
387
|
+
};
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const registry = require("../core/registry");
|
|
6
|
+
const GenericError = require("../core/errors/GenericError");
|
|
7
|
+
const { EXPRESSION_OPERATORS } = require("./domainRules");
|
|
8
|
+
|
|
9
|
+
const PROCESS_MANIFEST_FILENAME = "central.process.json";
|
|
10
|
+
const PROCESS_MANIFEST_SCHEMA_VERSION = 1;
|
|
11
|
+
const BINDING_KINDS = Object.freeze(["lookup", "aggregate", "expression"]);
|
|
12
|
+
const AGGREGATE_OPERATORS = Object.freeze(["sum", "count", "min", "max"]);
|
|
13
|
+
const RECALCULATION_MODES = Object.freeze(["immediate", "async"]);
|
|
14
|
+
const EXPRESSION_OPERATOR_SET = new Set(EXPRESSION_OPERATORS);
|
|
15
|
+
|
|
16
|
+
class ProcessManifestError extends GenericError {
|
|
17
|
+
constructor(message, issues = [], options = {}) {
|
|
18
|
+
super(message, {
|
|
19
|
+
statusCode: 422,
|
|
20
|
+
details: { issues },
|
|
21
|
+
cause: options.cause,
|
|
22
|
+
});
|
|
23
|
+
this.name = "ProcessManifestError";
|
|
24
|
+
this.code = "OON_PROCESS_MANIFEST_INVALID";
|
|
25
|
+
this.issues = issues;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isPlainObject(value) {
|
|
30
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hasOwn(object, key) {
|
|
34
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function addIssue(issues, fieldPath, message) {
|
|
38
|
+
issues.push({ path: fieldPath, message });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function validateText(value, fieldPath, issues) {
|
|
42
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
43
|
+
addIssue(issues, fieldPath, "deve ser texto não vazio.");
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function validateStringList(value, fieldPath, issues) {
|
|
50
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item.trim())) {
|
|
51
|
+
addIssue(issues, fieldPath, "deve ser uma lista de textos não vazios.");
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validateJsonValue(value, fieldPath, issues, seen = new Set()) {
|
|
58
|
+
if (value === null || ["string", "boolean"].includes(typeof value)) return;
|
|
59
|
+
if (typeof value === "number") {
|
|
60
|
+
if (!Number.isFinite(value)) addIssue(issues, fieldPath, "deve ser um número finito.");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (typeof value !== "object" || seen.has(value)) {
|
|
64
|
+
addIssue(issues, fieldPath, "deve conter somente valores JSON.");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
seen.add(value);
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
value.forEach((item, index) => validateJsonValue(item, `${fieldPath}[${index}]`, issues, seen));
|
|
70
|
+
} else {
|
|
71
|
+
Object.entries(value).forEach(([key, item]) => {
|
|
72
|
+
validateJsonValue(item, `${fieldPath}.${key}`, issues, seen);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
seen.delete(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function validateExpression(expression, fieldPath, issues) {
|
|
79
|
+
if (!isPlainObject(expression)) {
|
|
80
|
+
addIssue(issues, fieldPath, "deve ser um objeto de expressão.");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const kinds = ["value", "field", "op"].filter((key) => hasOwn(expression, key));
|
|
84
|
+
if (kinds.length !== 1) {
|
|
85
|
+
addIssue(issues, fieldPath, "deve declarar exatamente um entre value, field ou op.");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (hasOwn(expression, "value")) {
|
|
89
|
+
validateJsonValue(expression.value, `${fieldPath}.value`, issues);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (hasOwn(expression, "field")) {
|
|
93
|
+
validateText(expression.field, `${fieldPath}.field`, issues);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (!EXPRESSION_OPERATOR_SET.has(expression.op)) {
|
|
97
|
+
addIssue(issues, `${fieldPath}.op`, `operador não suportado: ${String(expression.op)}.`);
|
|
98
|
+
}
|
|
99
|
+
if (!Array.isArray(expression.args)) {
|
|
100
|
+
addIssue(issues, `${fieldPath}.args`, "deve ser uma lista.");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
expression.args.forEach((arg, index) => validateExpression(arg, `${fieldPath}.args[${index}]`, issues));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function validateMatch(match, fieldPath, issues) {
|
|
107
|
+
if (!isPlainObject(match)) {
|
|
108
|
+
addIssue(issues, fieldPath, "deve ser um objeto.");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
for (const [field, condition] of Object.entries(match)) {
|
|
112
|
+
if (!field || ["__proto__", "prototype", "constructor"].includes(field)) {
|
|
113
|
+
addIssue(issues, `${fieldPath}.${field}`, "campo inválido.");
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (isPlainObject(condition)) {
|
|
117
|
+
const operators = Object.keys(condition);
|
|
118
|
+
if (operators.length !== 1 || !["eq", "neq", "in", "nin"].includes(operators[0])) {
|
|
119
|
+
addIssue(issues, `${fieldPath}.${field}`, "use somente eq, neq, in ou nin.");
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (["in", "nin"].includes(operators[0]) && !Array.isArray(condition[operators[0]])) {
|
|
123
|
+
addIssue(issues, `${fieldPath}.${field}.${operators[0]}`, "deve ser uma lista.");
|
|
124
|
+
}
|
|
125
|
+
validateJsonValue(condition[operators[0]], `${fieldPath}.${field}.${operators[0]}`, issues);
|
|
126
|
+
} else {
|
|
127
|
+
validateJsonValue(condition, `${fieldPath}.${field}`, issues);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function validateWorkflow(workflow, fieldPath, issues) {
|
|
133
|
+
if (!isPlainObject(workflow)) {
|
|
134
|
+
addIssue(issues, fieldPath, "deve ser um objeto.");
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
validateText(workflow.stageField, `${fieldPath}.stageField`, issues);
|
|
138
|
+
|
|
139
|
+
if (hasOwn(workflow, "transitions")) {
|
|
140
|
+
if (!Array.isArray(workflow.transitions)) {
|
|
141
|
+
addIssue(issues, `${fieldPath}.transitions`, "deve ser uma lista.");
|
|
142
|
+
} else {
|
|
143
|
+
workflow.transitions.forEach((transition, index) => {
|
|
144
|
+
const itemPath = `${fieldPath}.transitions[${index}]`;
|
|
145
|
+
if (!isPlainObject(transition)) {
|
|
146
|
+
addIssue(issues, itemPath, "deve ser um objeto.");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
validateText(transition.from, `${itemPath}.from`, issues);
|
|
150
|
+
validateText(transition.to, `${itemPath}.to`, issues);
|
|
151
|
+
if (transition.when) validateExpression(transition.when, `${itemPath}.when`, issues);
|
|
152
|
+
if (transition.message) validateText(transition.message, `${itemPath}.message`, issues);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (hasOwn(workflow, "lockedFieldsByStage")) {
|
|
158
|
+
if (!isPlainObject(workflow.lockedFieldsByStage)) {
|
|
159
|
+
addIssue(issues, `${fieldPath}.lockedFieldsByStage`, "deve ser um objeto.");
|
|
160
|
+
} else {
|
|
161
|
+
Object.entries(workflow.lockedFieldsByStage).forEach(([stage, fields]) => {
|
|
162
|
+
validateStringList(fields, `${fieldPath}.lockedFieldsByStage.${stage}`, issues);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (hasOwn(workflow, "onEnter")) {
|
|
168
|
+
if (!Array.isArray(workflow.onEnter)) {
|
|
169
|
+
addIssue(issues, `${fieldPath}.onEnter`, "deve ser uma lista.");
|
|
170
|
+
} else {
|
|
171
|
+
workflow.onEnter.forEach((assignment, index) => {
|
|
172
|
+
const itemPath = `${fieldPath}.onEnter[${index}]`;
|
|
173
|
+
if (!isPlainObject(assignment)) {
|
|
174
|
+
addIssue(issues, itemPath, "deve ser um objeto.");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
validateText(assignment.stage, `${itemPath}.stage`, issues);
|
|
178
|
+
if (!isPlainObject(assignment.set)) addIssue(issues, `${itemPath}.set`, "deve ser um objeto.");
|
|
179
|
+
else validateJsonValue(assignment.set, `${itemPath}.set`, issues);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (hasOwn(workflow, "automaticTransitions")) {
|
|
185
|
+
if (!Array.isArray(workflow.automaticTransitions)) {
|
|
186
|
+
addIssue(issues, `${fieldPath}.automaticTransitions`, "deve ser uma lista.");
|
|
187
|
+
} else {
|
|
188
|
+
workflow.automaticTransitions.forEach((transition, index) => {
|
|
189
|
+
const itemPath = `${fieldPath}.automaticTransitions[${index}]`;
|
|
190
|
+
if (!isPlainObject(transition)) {
|
|
191
|
+
addIssue(issues, itemPath, "deve ser um objeto.");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
validateExpression(transition.when, `${itemPath}.when`, issues);
|
|
195
|
+
validateText(transition.to, `${itemPath}.to`, issues);
|
|
196
|
+
if (transition.set) validateJsonValue(transition.set, `${itemPath}.set`, issues);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function validateBinding(binding, fieldPath, issues) {
|
|
203
|
+
if (!isPlainObject(binding)) {
|
|
204
|
+
addIssue(issues, fieldPath, "deve ser um objeto.");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
validateText(binding.field, `${fieldPath}.field`, issues);
|
|
208
|
+
if (!BINDING_KINDS.includes(binding.kind)) {
|
|
209
|
+
addIssue(issues, `${fieldPath}.kind`, `use: ${BINDING_KINDS.join(", ")}.`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (binding.recalculate && !RECALCULATION_MODES.includes(binding.recalculate)) {
|
|
213
|
+
addIssue(issues, `${fieldPath}.recalculate`, `use: ${RECALCULATION_MODES.join(", ")}.`);
|
|
214
|
+
}
|
|
215
|
+
if (binding.watchFields) validateStringList(binding.watchFields, `${fieldPath}.watchFields`, issues);
|
|
216
|
+
if (binding.match) validateMatch(binding.match, `${fieldPath}.match`, issues);
|
|
217
|
+
|
|
218
|
+
if (binding.kind === "lookup") {
|
|
219
|
+
validateText(binding.sourceModel, `${fieldPath}.sourceModel`, issues);
|
|
220
|
+
validateText(binding.localField, `${fieldPath}.localField`, issues);
|
|
221
|
+
validateText(binding.sourceField, `${fieldPath}.sourceField`, issues);
|
|
222
|
+
}
|
|
223
|
+
if (binding.kind === "aggregate") {
|
|
224
|
+
validateText(binding.sourceModel, `${fieldPath}.sourceModel`, issues);
|
|
225
|
+
validateText(binding.foreignField, `${fieldPath}.foreignField`, issues);
|
|
226
|
+
if (!AGGREGATE_OPERATORS.includes(binding.operator)) {
|
|
227
|
+
addIssue(issues, `${fieldPath}.operator`, `use: ${AGGREGATE_OPERATORS.join(", ")}.`);
|
|
228
|
+
}
|
|
229
|
+
if (binding.operator !== "count") validateText(binding.sourceField, `${fieldPath}.sourceField`, issues);
|
|
230
|
+
}
|
|
231
|
+
if (binding.kind === "expression") {
|
|
232
|
+
validateExpression(binding.expression, `${fieldPath}.expression`, issues);
|
|
233
|
+
if (hasOwn(binding, "precision")
|
|
234
|
+
&& (!Number.isInteger(binding.precision) || binding.precision < 0 || binding.precision > 8)) {
|
|
235
|
+
addIssue(issues, `${fieldPath}.precision`, "deve ser inteiro entre 0 e 8.");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function validateDeleteProtection(rule, fieldPath, issues) {
|
|
241
|
+
if (!isPlainObject(rule)) {
|
|
242
|
+
addIssue(issues, fieldPath, "deve ser um objeto.");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
validateText(rule.sourceModel, `${fieldPath}.sourceModel`, issues);
|
|
246
|
+
validateText(rule.foreignField, `${fieldPath}.foreignField`, issues);
|
|
247
|
+
validateText(rule.message, `${fieldPath}.message`, issues);
|
|
248
|
+
if (rule.match) validateMatch(rule.match, `${fieldPath}.match`, issues);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function validateAtomicInvariant(rule, fieldPath, issues) {
|
|
252
|
+
if (!isPlainObject(rule)) {
|
|
253
|
+
addIssue(issues, fieldPath, "deve ser um objeto.");
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
validateText(rule.name, `${fieldPath}.name`, issues);
|
|
257
|
+
if (rule.kind !== "relatedSumLteParentField") {
|
|
258
|
+
addIssue(issues, `${fieldPath}.kind`, "use relatedSumLteParentField.");
|
|
259
|
+
}
|
|
260
|
+
validateText(rule.parentModel, `${fieldPath}.parentModel`, issues);
|
|
261
|
+
validateText(rule.parentLocalField, `${fieldPath}.parentLocalField`, issues);
|
|
262
|
+
validateText(rule.sourceField, `${fieldPath}.sourceField`, issues);
|
|
263
|
+
validateText(rule.parentField, `${fieldPath}.parentField`, issues);
|
|
264
|
+
validateText(rule.message, `${fieldPath}.message`, issues);
|
|
265
|
+
if (rule.match) validateMatch(rule.match, `${fieldPath}.match`, issues);
|
|
266
|
+
if (hasOwn(rule, "tolerance")
|
|
267
|
+
&& (typeof rule.tolerance !== "number" || !Number.isFinite(rule.tolerance) || rule.tolerance < 0)) {
|
|
268
|
+
addIssue(issues, `${fieldPath}.tolerance`, "deve ser número finito maior ou igual a zero.");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function validateModelProcess(config, fieldPath, issues) {
|
|
273
|
+
if (!isPlainObject(config)) {
|
|
274
|
+
addIssue(issues, fieldPath, "deve ser um objeto.");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (config.workflow) validateWorkflow(config.workflow, `${fieldPath}.workflow`, issues);
|
|
278
|
+
for (const [key, validator] of [
|
|
279
|
+
["bindings", validateBinding],
|
|
280
|
+
["deleteProtection", validateDeleteProtection],
|
|
281
|
+
["atomicInvariants", validateAtomicInvariant],
|
|
282
|
+
]) {
|
|
283
|
+
if (!hasOwn(config, key)) continue;
|
|
284
|
+
if (!Array.isArray(config[key])) {
|
|
285
|
+
addIssue(issues, `${fieldPath}.${key}`, "deve ser uma lista.");
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
config[key].forEach((item, index) => validator(item, `${fieldPath}.${key}[${index}]`, issues));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function validateProcessManifest(input) {
|
|
293
|
+
const issues = [];
|
|
294
|
+
if (!isPlainObject(input)) {
|
|
295
|
+
throw new ProcessManifestError("Manifesto de processos inválido.", [
|
|
296
|
+
{ path: "$", message: "o documento deve ser um objeto JSON." },
|
|
297
|
+
]);
|
|
298
|
+
}
|
|
299
|
+
if (input.schemaVersion !== PROCESS_MANIFEST_SCHEMA_VERSION) {
|
|
300
|
+
addIssue(issues, "schemaVersion", `versão não suportada. Use ${PROCESS_MANIFEST_SCHEMA_VERSION}.`);
|
|
301
|
+
}
|
|
302
|
+
if (!isPlainObject(input.models) || Object.keys(input.models).length === 0) {
|
|
303
|
+
addIssue(issues, "models", "deve ser um objeto com pelo menos uma model.");
|
|
304
|
+
} else {
|
|
305
|
+
Object.entries(input.models).forEach(([modelName, config]) => {
|
|
306
|
+
validateText(modelName, `models.${modelName}`, issues);
|
|
307
|
+
validateModelProcess(config, `models.${modelName}`, issues);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if (issues.length) {
|
|
311
|
+
throw new ProcessManifestError(
|
|
312
|
+
`Manifesto de processos inválido: ${issues.length} problema(s) encontrado(s).`,
|
|
313
|
+
issues,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
return JSON.parse(JSON.stringify(input));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function assertRegisteredContract(manifest) {
|
|
320
|
+
const issues = [];
|
|
321
|
+
for (const [modelName, config] of Object.entries(manifest.models)) {
|
|
322
|
+
const entry = registry.getModel(modelName);
|
|
323
|
+
if (!entry) {
|
|
324
|
+
addIssue(issues, `models.${modelName}`, "model não registrada no domínio.");
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const ownFields = new Set(Object.keys(entry.metaFields || {}));
|
|
328
|
+
const assertOwnField = (field, fieldPath) => {
|
|
329
|
+
if (field && !ownFields.has(field)) addIssue(issues, fieldPath, `campo não declarado em ${modelName}.`);
|
|
330
|
+
};
|
|
331
|
+
if (config.workflow) {
|
|
332
|
+
assertOwnField(config.workflow.stageField, `models.${modelName}.workflow.stageField`);
|
|
333
|
+
Object.entries(config.workflow.lockedFieldsByStage || {}).forEach(([stage, fields]) => {
|
|
334
|
+
fields.forEach((field, index) => assertOwnField(
|
|
335
|
+
field,
|
|
336
|
+
`models.${modelName}.workflow.lockedFieldsByStage.${stage}[${index}]`,
|
|
337
|
+
));
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
(config.bindings || []).forEach((binding, index) => {
|
|
341
|
+
const base = `models.${modelName}.bindings[${index}]`;
|
|
342
|
+
assertOwnField(binding.field, `${base}.field`);
|
|
343
|
+
if (binding.localField) assertOwnField(binding.localField, `${base}.localField`);
|
|
344
|
+
if (binding.sourceModel && !registry.getModel(binding.sourceModel)) {
|
|
345
|
+
addIssue(issues, `${base}.sourceModel`, "model de origem não registrada.");
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
(config.deleteProtection || []).forEach((rule, index) => {
|
|
349
|
+
if (!registry.getModel(rule.sourceModel)) {
|
|
350
|
+
addIssue(
|
|
351
|
+
issues,
|
|
352
|
+
`models.${modelName}.deleteProtection[${index}].sourceModel`,
|
|
353
|
+
"model de origem não registrada.",
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
(config.atomicInvariants || []).forEach((rule, index) => {
|
|
358
|
+
const base = `models.${modelName}.atomicInvariants[${index}]`;
|
|
359
|
+
assertOwnField(rule.parentLocalField, `${base}.parentLocalField`);
|
|
360
|
+
assertOwnField(rule.sourceField, `${base}.sourceField`);
|
|
361
|
+
if (!registry.getModel(rule.parentModel)) {
|
|
362
|
+
addIssue(issues, `${base}.parentModel`, "model pai não registrada.");
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
if (issues.length) throw new ProcessManifestError("Manifesto referencia domínio inexistente.", issues);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function registerProcessManifest(input) {
|
|
370
|
+
const manifest = validateProcessManifest(input);
|
|
371
|
+
assertRegisteredContract(manifest);
|
|
372
|
+
const entries = [];
|
|
373
|
+
for (const [modelName, processRules] of Object.entries(manifest.models)) {
|
|
374
|
+
const entry = registry.getModel(modelName);
|
|
375
|
+
entry.definition.processRules = JSON.parse(JSON.stringify(processRules));
|
|
376
|
+
entries.push(entry);
|
|
377
|
+
}
|
|
378
|
+
return entries;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function loadProcessManifest(filePath, options = {}) {
|
|
382
|
+
const resolvedPath = path.resolve(filePath);
|
|
383
|
+
let parsed;
|
|
384
|
+
try {
|
|
385
|
+
parsed = JSON.parse(fs.readFileSync(resolvedPath, "utf8"));
|
|
386
|
+
} catch (error) {
|
|
387
|
+
if (error instanceof SyntaxError) {
|
|
388
|
+
throw new ProcessManifestError(
|
|
389
|
+
`JSON inválido em ${resolvedPath}.`,
|
|
390
|
+
[{ path: "$", message: error.message }],
|
|
391
|
+
{ cause: error },
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
const manifest = validateProcessManifest(parsed);
|
|
397
|
+
const entries = options.register === false ? [] : registerProcessManifest(manifest);
|
|
398
|
+
return { filePath: resolvedPath, manifest, entries };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
module.exports = {
|
|
402
|
+
PROCESS_MANIFEST_FILENAME,
|
|
403
|
+
PROCESS_MANIFEST_SCHEMA_VERSION,
|
|
404
|
+
BINDING_KINDS,
|
|
405
|
+
AGGREGATE_OPERATORS,
|
|
406
|
+
RECALCULATION_MODES,
|
|
407
|
+
ProcessManifestError,
|
|
408
|
+
validateProcessManifest,
|
|
409
|
+
registerProcessManifest,
|
|
410
|
+
loadProcessManifest,
|
|
411
|
+
};
|