@oondemand/oon-core-back 0.3.46 → 0.3.48

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.
@@ -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 applyDomainMutation({ rules, current = null, changes = {}, op = "update" } = {}) {
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
- for (const validation of domainRules.validations || []) {
286
- const context = {
287
- field: validation.field,
288
- rule: validation.name || validation.code || "validation",
289
- };
290
- if (validation.when
291
- && !Boolean(evaluateDomainExpression(validation.when, consolidated, context))) {
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
+ };