@cullet/erp-core 1.0.9 → 1.0.11
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/index.d.ts +123 -2
- package/dist/index.js +103 -1
- package/dist/index.js.map +1 -1
- package/dist/policy-service.d.ts +99 -0
- package/dist/policy-service.js +73 -0
- package/dist/policy-service.js.map +1 -1
- package/dist/validation-code.js +33 -0
- package/dist/validation-code.js.map +1 -1
- package/dist/validation-error.d.ts +202 -0
- package/dist/validation-error.js +164 -0
- package/dist/validation-error.js.map +1 -1
- package/meta.json +12 -8
- package/package.json +1 -1
- package/src/core/domain/entity.ts +71 -0
- package/src/core/domain/value-object.ts +51 -0
- package/src/core/errors/app-error.ts +33 -0
- package/src/core/errors/authorization-error.ts +43 -0
- package/src/core/errors/conflict-error.ts +69 -0
- package/src/core/errors/integration-error.ts +25 -0
- package/src/core/errors/not-found-error.ts +14 -0
- package/src/core/errors/validation-error.ts +18 -0
- package/src/core/policies/catalog/policy-catalog.ts +36 -0
- package/src/core/policies/resolver/policy-resolver.ts +10 -0
- package/src/core/policies/service/policy-service.ts +53 -0
- package/src/version.ts +1 -1
package/dist/validation-error.js
CHANGED
|
@@ -80,6 +80,19 @@ function compactMetadata$1(input) {
|
|
|
80
80
|
}
|
|
81
81
|
//#endregion
|
|
82
82
|
//#region src/core/errors/authorization-error.ts
|
|
83
|
+
/**
|
|
84
|
+
* Raised when an authenticated actor is not allowed to perform a business
|
|
85
|
+
* action — the "you may not" error, distinct from authentication ("who are
|
|
86
|
+
* you"). Maps to an HTTP 403 at the edge.
|
|
87
|
+
*
|
|
88
|
+
* The discriminating {@link reason} records *why* access was denied (a flat
|
|
89
|
+
* forbid, a missing role/capability, an out-of-scope target, or a policy
|
|
90
|
+
* decision) so the boundary can shape the response without re-deriving it. The
|
|
91
|
+
* metadata is deliberately built around a stable business `action` and a
|
|
92
|
+
* type/id resource reference rather than an HTTP route or full payload, keeping
|
|
93
|
+
* sensitive data out of logs. Instances are frozen. Construct through the
|
|
94
|
+
* static factories, never directly.
|
|
95
|
+
*/
|
|
83
96
|
var AuthorizationError = class AuthorizationError extends AppError {
|
|
84
97
|
constructor(params) {
|
|
85
98
|
super(params.message, params.code, {
|
|
@@ -96,6 +109,12 @@ var AuthorizationError = class AuthorizationError extends AppError {
|
|
|
96
109
|
this.reason = params.reason;
|
|
97
110
|
Object.freeze(this);
|
|
98
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* A flat denial with no more specific reason — the actor simply may not
|
|
114
|
+
* perform this action. Reach for a more precise factory
|
|
115
|
+
* ({@link AuthorizationError.missingCapability}, {@link AuthorizationError.outOfScope},
|
|
116
|
+
* {@link AuthorizationError.policyDenied}) when the cause is known.
|
|
117
|
+
*/
|
|
99
118
|
static forbidden(input) {
|
|
100
119
|
return new AuthorizationError({
|
|
101
120
|
message: "Action not allowed",
|
|
@@ -108,6 +127,15 @@ var AuthorizationError = class AuthorizationError extends AppError {
|
|
|
108
127
|
...extractAppErrorOptions(input)
|
|
109
128
|
});
|
|
110
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* The action was denied by an evaluated policy. Captures the deciding
|
|
132
|
+
* policy's id, version, and evaluation instant into the metadata (with
|
|
133
|
+
* `decision: "deny"`) so the denial is auditable back to the exact policy
|
|
134
|
+
* that produced it.
|
|
135
|
+
*
|
|
136
|
+
* @param input - Factory options plus the required `policyId`,
|
|
137
|
+
* `policyVersion`, and `evaluatedAtIso` of the deciding policy.
|
|
138
|
+
*/
|
|
111
139
|
static policyDenied(input) {
|
|
112
140
|
return new AuthorizationError({
|
|
113
141
|
message: "Action denied by policy",
|
|
@@ -121,6 +149,13 @@ var AuthorizationError = class AuthorizationError extends AppError {
|
|
|
121
149
|
...extractAppErrorOptions(input)
|
|
122
150
|
});
|
|
123
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* The actor lacks a required role or capability. The expected
|
|
154
|
+
* {@link AuthorizationRequirement} is recorded so the boundary can tell the
|
|
155
|
+
* caller precisely what grant is missing.
|
|
156
|
+
*
|
|
157
|
+
* @param input - Factory options plus the `required` role/capability/scope.
|
|
158
|
+
*/
|
|
124
159
|
static missingCapability(input) {
|
|
125
160
|
return new AuthorizationError({
|
|
126
161
|
message: "Insufficient capability to perform the action",
|
|
@@ -133,6 +168,14 @@ var AuthorizationError = class AuthorizationError extends AppError {
|
|
|
133
168
|
...extractAppErrorOptions(input)
|
|
134
169
|
});
|
|
135
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* The actor may perform the action in general, but not on *this* target —
|
|
173
|
+
* the resource falls outside the actor's permitted scope (e.g. a different
|
|
174
|
+
* school or tenant than the one they are bound to).
|
|
175
|
+
*
|
|
176
|
+
* @param input - Factory options plus the optional `required` scope that the
|
|
177
|
+
* target failed to satisfy.
|
|
178
|
+
*/
|
|
136
179
|
static outOfScope(input) {
|
|
137
180
|
return new AuthorizationError({
|
|
138
181
|
message: "Action outside the allowed scope",
|
|
@@ -182,6 +225,17 @@ var BusinessRuleViolationError = class extends AppError {
|
|
|
182
225
|
};
|
|
183
226
|
//#endregion
|
|
184
227
|
//#region src/core/errors/conflict-error.ts
|
|
228
|
+
/**
|
|
229
|
+
* Base for errors that signal a state conflict — the request collides with data
|
|
230
|
+
* that already exists. Common at an HTTP 409 boundary.
|
|
231
|
+
*
|
|
232
|
+
* The discriminating {@link kind} lets a handler branch on *why* it conflicted
|
|
233
|
+
* (an already-existing record, a domain duplicate, or a raw storage uniqueness
|
|
234
|
+
* breach) without `instanceof` chains. Abstract: construct one of the concrete
|
|
235
|
+
* subclasses through its `detected(...)` factory, which fills in the right code,
|
|
236
|
+
* message, and metadata. Instances are frozen, so a conflict error can be shared
|
|
237
|
+
* and rethrown without risk of tampering.
|
|
238
|
+
*/
|
|
185
239
|
var ConflictError = class extends AppError {
|
|
186
240
|
constructor(params) {
|
|
187
241
|
super(params.message, params.code, {
|
|
@@ -195,6 +249,11 @@ var ConflictError = class extends AppError {
|
|
|
195
249
|
Object.freeze(this);
|
|
196
250
|
}
|
|
197
251
|
};
|
|
252
|
+
/**
|
|
253
|
+
* The operation cannot proceed because a matching record already exists —
|
|
254
|
+
* e.g. creating something whose natural key is already taken. Construct via
|
|
255
|
+
* {@link AlreadyExistsError.detected}.
|
|
256
|
+
*/
|
|
198
257
|
var AlreadyExistsError = class AlreadyExistsError extends ConflictError {
|
|
199
258
|
constructor(input) {
|
|
200
259
|
super({
|
|
@@ -204,6 +263,14 @@ var AlreadyExistsError = class AlreadyExistsError extends ConflictError {
|
|
|
204
263
|
...input
|
|
205
264
|
});
|
|
206
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Builds an {@link AlreadyExistsError} for a detected collision. `operation`
|
|
268
|
+
* defaults to `"create"` and a generic retry `hint` is supplied when none is
|
|
269
|
+
* given, so the error is actionable even from a minimal call site.
|
|
270
|
+
*
|
|
271
|
+
* @param input - The conflicting entity plus optional non-sensitive context
|
|
272
|
+
* (operation, field, existing id, value hash/preview, correlation ids).
|
|
273
|
+
*/
|
|
207
274
|
static detected(input) {
|
|
208
275
|
return new AlreadyExistsError({
|
|
209
276
|
metadata: {
|
|
@@ -223,6 +290,13 @@ var AlreadyExistsError = class AlreadyExistsError extends ConflictError {
|
|
|
223
290
|
});
|
|
224
291
|
}
|
|
225
292
|
};
|
|
293
|
+
/**
|
|
294
|
+
* A domain-level duplicate: the data being written would duplicate an existing
|
|
295
|
+
* record under the model's own uniqueness rules. Use this when the duplication
|
|
296
|
+
* is recognized in the domain, as opposed to a raw DB constraint
|
|
297
|
+
* ({@link UniqueConstraintViolationError}). Construct via
|
|
298
|
+
* {@link DuplicateError.detected}.
|
|
299
|
+
*/
|
|
226
300
|
var DuplicateError = class DuplicateError extends ConflictError {
|
|
227
301
|
constructor(input) {
|
|
228
302
|
super({
|
|
@@ -232,6 +306,13 @@ var DuplicateError = class DuplicateError extends ConflictError {
|
|
|
232
306
|
...input
|
|
233
307
|
});
|
|
234
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Builds a {@link DuplicateError} for a detected duplicate, defaulting to a
|
|
311
|
+
* "adjust the data so it does not duplicate" hint when none is provided.
|
|
312
|
+
*
|
|
313
|
+
* @param input - The conflicting entity plus optional non-sensitive context
|
|
314
|
+
* (field, constraint name, existing id, value hash/preview, correlation ids).
|
|
315
|
+
*/
|
|
235
316
|
static detected(input) {
|
|
236
317
|
return new DuplicateError({
|
|
237
318
|
metadata: {
|
|
@@ -252,6 +333,14 @@ var DuplicateError = class DuplicateError extends ConflictError {
|
|
|
252
333
|
});
|
|
253
334
|
}
|
|
254
335
|
};
|
|
336
|
+
/**
|
|
337
|
+
* A uniqueness breach surfaced by the storage layer itself (a database unique
|
|
338
|
+
* index), carrying the raw {@link UniqueConstraintViolation} (constraint, table,
|
|
339
|
+
* columns). Keep this distinct from {@link DuplicateError}: this one is the
|
|
340
|
+
* infrastructure signal; translate it into a domain duplicate at the boundary
|
|
341
|
+
* with {@link translateUniqueViolationToDuplicate} when the model should own the
|
|
342
|
+
* message. Construct via {@link UniqueConstraintViolationError.detected}.
|
|
343
|
+
*/
|
|
255
344
|
var UniqueConstraintViolationError = class UniqueConstraintViolationError extends ConflictError {
|
|
256
345
|
constructor(input) {
|
|
257
346
|
super({
|
|
@@ -261,6 +350,13 @@ var UniqueConstraintViolationError = class UniqueConstraintViolationError extend
|
|
|
261
350
|
...input
|
|
262
351
|
});
|
|
263
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Builds a {@link UniqueConstraintViolationError} from the constraint details
|
|
355
|
+
* a driver reports, packaging them into a nested `violation` payload.
|
|
356
|
+
*
|
|
357
|
+
* @param input - The entity plus the offending constraint name, table, and
|
|
358
|
+
* columns, and optional correlation ids.
|
|
359
|
+
*/
|
|
264
360
|
static detected(input) {
|
|
265
361
|
return new UniqueConstraintViolationError({
|
|
266
362
|
metadata: {
|
|
@@ -283,6 +379,17 @@ var UniqueConstraintViolationError = class UniqueConstraintViolationError extend
|
|
|
283
379
|
});
|
|
284
380
|
}
|
|
285
381
|
};
|
|
382
|
+
/**
|
|
383
|
+
* Translates a raw storage {@link UniqueConstraintViolation} into a domain-level
|
|
384
|
+
* {@link DuplicateError}. This is the seam where an infrastructure concern (a DB
|
|
385
|
+
* unique index firing) is re-expressed in the language of the model, so callers
|
|
386
|
+
* upstream catch a `DuplicateError` and never have to know a database was
|
|
387
|
+
* involved.
|
|
388
|
+
*
|
|
389
|
+
* @param input - The entity, the driver-reported `violation`, and optional
|
|
390
|
+
* request context (`ctx`) carrying correlation ids and a clock instant.
|
|
391
|
+
* @returns A {@link DuplicateError} carrying the constraint name as its field hint.
|
|
392
|
+
*/
|
|
286
393
|
function translateUniqueViolationToDuplicate(input) {
|
|
287
394
|
return DuplicateError.detected({
|
|
288
395
|
entity: input.entity,
|
|
@@ -453,6 +560,18 @@ function resolveKeyIdentity(key, keyHash, keyPreview) {
|
|
|
453
560
|
}
|
|
454
561
|
//#endregion
|
|
455
562
|
//#region src/core/errors/integration-error.ts
|
|
563
|
+
/**
|
|
564
|
+
* Raised when a call to an external provider fails — a timeout, an unreachable
|
|
565
|
+
* endpoint, or a malformed response. This is the boundary error that separates
|
|
566
|
+
* "our code is fine, the outside world misbehaved" from internal faults, which
|
|
567
|
+
* matters for retry and alerting decisions.
|
|
568
|
+
*
|
|
569
|
+
* Every instance records the `provider`, the `operation`, and timing
|
|
570
|
+
* (`startedAtIso` / `durationMs`) so failures are correlatable across services
|
|
571
|
+
* and a slow dependency is visible in the metadata. The discriminating
|
|
572
|
+
* {@link reason} lets callers decide whether a failure is worth retrying.
|
|
573
|
+
* Instances are frozen; construct through the static factories.
|
|
574
|
+
*/
|
|
456
575
|
var IntegrationError = class IntegrationError extends AppError {
|
|
457
576
|
constructor(params) {
|
|
458
577
|
const { message, code, metadata, ...rest } = params;
|
|
@@ -463,6 +582,10 @@ var IntegrationError = class IntegrationError extends AppError {
|
|
|
463
582
|
this.reason = params.reason;
|
|
464
583
|
Object.freeze(this);
|
|
465
584
|
}
|
|
585
|
+
/**
|
|
586
|
+
* The provider did not respond within the allotted time. Usually retryable,
|
|
587
|
+
* since a timeout leaves the outcome unknown rather than known-failed.
|
|
588
|
+
*/
|
|
466
589
|
static timeout(options) {
|
|
467
590
|
return new IntegrationError({
|
|
468
591
|
message: "Timeout while integrating with external provider",
|
|
@@ -475,6 +598,10 @@ var IntegrationError = class IntegrationError extends AppError {
|
|
|
475
598
|
...pickAppErrorFields(options)
|
|
476
599
|
});
|
|
477
600
|
}
|
|
601
|
+
/**
|
|
602
|
+
* The provider could not be reached at all (connection refused, DNS
|
|
603
|
+
* failure, network partition) — the request never landed.
|
|
604
|
+
*/
|
|
478
605
|
static unreachable(options) {
|
|
479
606
|
return new IntegrationError({
|
|
480
607
|
message: "Provider unavailable",
|
|
@@ -487,6 +614,11 @@ var IntegrationError = class IntegrationError extends AppError {
|
|
|
487
614
|
...pickAppErrorFields(options)
|
|
488
615
|
});
|
|
489
616
|
}
|
|
617
|
+
/**
|
|
618
|
+
* The provider answered, but with something the system cannot use — an
|
|
619
|
+
* unexpected status, an unparsable body, or a contract mismatch. Unlike a
|
|
620
|
+
* timeout this is a definite failure, so blind retries rarely help.
|
|
621
|
+
*/
|
|
490
622
|
static badResponse(options) {
|
|
491
623
|
return new IntegrationError({
|
|
492
624
|
message: "Unexpected response from provider",
|
|
@@ -547,7 +679,21 @@ var LegacyIncompatibleError = class extends AppError {
|
|
|
547
679
|
};
|
|
548
680
|
//#endregion
|
|
549
681
|
//#region src/core/errors/not-found-error.ts
|
|
682
|
+
/**
|
|
683
|
+
* Raised when a requested resource does not exist. Maps naturally onto an HTTP
|
|
684
|
+
* 404 at the edge, but stays transport-agnostic here.
|
|
685
|
+
*
|
|
686
|
+
* The `resource` name builds the message (`"<resource> not found"`) and, with
|
|
687
|
+
* the optional lookup `criteria`, lands in the metadata so logs show *what* was
|
|
688
|
+
* searched for without the caller having to restate it in the message.
|
|
689
|
+
*/
|
|
550
690
|
var NotFoundError = class extends AppError {
|
|
691
|
+
/**
|
|
692
|
+
* @param resource - Human-readable name of the missing resource (e.g. `"Order"`).
|
|
693
|
+
* @param criteria - Optional lookup keys used in the search; recorded in
|
|
694
|
+
* metadata to aid debugging. Avoid putting sensitive values here.
|
|
695
|
+
* @param options - Optional cause, correlation ids, and extra metadata.
|
|
696
|
+
*/
|
|
551
697
|
constructor(resource, criteria, options) {
|
|
552
698
|
const baseMetadata = criteria ? {
|
|
553
699
|
resource,
|
|
@@ -735,7 +881,25 @@ function evaluateTemporalWindow(input) {
|
|
|
735
881
|
}
|
|
736
882
|
//#endregion
|
|
737
883
|
//#region src/core/errors/validation-error.ts
|
|
884
|
+
/**
|
|
885
|
+
* Raised when a single input fails a validation rule — the canonical
|
|
886
|
+
* "this field is wrong, and here is why" error.
|
|
887
|
+
*
|
|
888
|
+
* It pins the offending `field` and a machine-readable `validationCode` into
|
|
889
|
+
* the metadata (merged ahead of any caller-supplied metadata) so an API layer
|
|
890
|
+
* can map the failure straight onto a form field without parsing the message.
|
|
891
|
+
* The fixed {@link ErrorCodes.validation} code lets callers catch all
|
|
892
|
+
* validation failures uniformly while still distinguishing the specific rule
|
|
893
|
+
* through `validationCode`.
|
|
894
|
+
*/
|
|
738
895
|
var ValidationError = class extends AppError {
|
|
896
|
+
/**
|
|
897
|
+
* @param field - The input that failed validation; surfaced as `metadata.field`.
|
|
898
|
+
* @param code - The specific rule that was violated; surfaced as `metadata.validationCode`.
|
|
899
|
+
* @param message - The developer-facing description of the failure.
|
|
900
|
+
* @param options - Optional cause, correlation ids, and extra metadata
|
|
901
|
+
* (merged over the field/code metadata, never overwriting it by accident).
|
|
902
|
+
*/
|
|
739
903
|
constructor(field, code, message, options) {
|
|
740
904
|
const baseMetadata = {
|
|
741
905
|
field: field.value,
|