@entifix/core 0.8.0 → 0.10.0
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 +3 -3
- package/dist/deadline/deadline.d.ts +33 -0
- package/dist/deadline/deadline.d.ts.map +1 -0
- package/dist/deadline/deadline.js +33 -0
- package/dist/deadline/deadline.js.map +1 -0
- package/dist/entity-definition/links/entity-collection-link/entity-collection-link.d.ts +3 -3
- package/dist/entity-definition/links/entity-collection-link/entity-collection-link.d.ts.map +1 -1
- package/dist/entity-definition/links/entity-collection-link/entity-collection-link.js.map +1 -1
- package/dist/entity-definition/links/entity-link/entity-link.d.ts +4 -4
- package/dist/entity-definition/links/entity-link/entity-link.d.ts.map +1 -1
- package/dist/entity-definition/links/entity-link/entity-link.js.map +1 -1
- package/dist/entity-definition/links/entity-link-source/entity-link-source.d.ts +3 -3
- package/dist/entity-definition/links/entity-link-source/entity-link-source.d.ts.map +1 -1
- package/dist/entity-definition/links/entity-link-source/entity-link-source.js.map +1 -1
- package/dist/entity-definition/validate/validate-records.d.ts +23 -6
- package/dist/entity-definition/validate/validate-records.d.ts.map +1 -1
- package/dist/entity-definition/validate/validate-records.js +25 -10
- package/dist/entity-definition/validate/validate-records.js.map +1 -1
- package/dist/envelope/make-envelope.d.ts +7 -1
- package/dist/envelope/make-envelope.d.ts.map +1 -1
- package/dist/envelope/make-envelope.js +12 -0
- package/dist/envelope/make-envelope.js.map +1 -1
- package/dist/envelope/read-envelope.d.ts +14 -7
- package/dist/envelope/read-envelope.d.ts.map +1 -1
- package/dist/envelope/read-envelope.js +36 -25
- package/dist/envelope/read-envelope.js.map +1 -1
- package/dist/envelope/types.d.ts +3 -1
- package/dist/envelope/types.d.ts.map +1 -1
- package/dist/envelope/types.js.map +1 -1
- package/dist/failure/define.d.ts +39 -0
- package/dist/failure/define.d.ts.map +1 -0
- package/dist/failure/define.js +70 -0
- package/dist/failure/define.js.map +1 -0
- package/dist/failure/failure.d.ts +79 -0
- package/dist/failure/failure.d.ts.map +1 -0
- package/dist/failure/failure.js +39 -0
- package/dist/failure/failure.js.map +1 -0
- package/dist/failure/generic.d.ts +141 -0
- package/dist/failure/generic.d.ts.map +1 -0
- package/dist/failure/generic.js +101 -0
- package/dist/failure/generic.js.map +1 -0
- package/dist/failure/index.d.ts +50 -0
- package/dist/failure/index.d.ts.map +1 -0
- package/dist/failure/index.js +47 -0
- package/dist/failure/index.js.map +1 -0
- package/dist/failure/predicates.d.ts +56 -0
- package/dist/failure/predicates.d.ts.map +1 -0
- package/dist/failure/predicates.js +22 -0
- package/dist/failure/predicates.js.map +1 -0
- package/dist/failure/problem.d.ts +49 -0
- package/dist/failure/problem.d.ts.map +1 -0
- package/dist/failure/problem.js +87 -0
- package/dist/failure/problem.js.map +1 -0
- package/dist/failure/registry.d.ts +28 -0
- package/dist/failure/registry.d.ts.map +1 -0
- package/dist/failure/registry.js +36 -0
- package/dist/failure/registry.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/query/fold.d.ts +40 -0
- package/dist/query/fold.d.ts.map +1 -0
- package/dist/query/fold.js +49 -0
- package/dist/query/fold.js.map +1 -0
- package/dist/query/in-memory-query.d.ts +12 -1
- package/dist/query/in-memory-query.d.ts.map +1 -1
- package/dist/query/in-memory-query.js +33 -3
- package/dist/query/in-memory-query.js.map +1 -1
- package/dist/rsql/facet-query-url.d.ts +25 -0
- package/dist/rsql/facet-query-url.d.ts.map +1 -0
- package/dist/rsql/facet-query-url.js +63 -0
- package/dist/rsql/facet-query-url.js.map +1 -0
- package/dist/rsql/index.d.ts +1 -0
- package/dist/rsql/index.d.ts.map +1 -1
- package/dist/rsql/index.js +1 -0
- package/dist/rsql/index.js.map +1 -1
- package/dist/types/EntityFacets.d.ts +30 -0
- package/dist/types/EntityFacets.d.ts.map +1 -0
- package/dist/types/EntityFacets.js +10 -0
- package/dist/types/EntityFacets.js.map +1 -0
- package/package.json +3 -3
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Effect } from 'effect';
|
|
2
|
-
import { EntifixBuildError } from '../base-entities/entifix-error/index.js';
|
|
3
2
|
import type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';
|
|
3
|
+
import { InvalidEnvelope } from '../failure/generic.js';
|
|
4
4
|
import type { Entity, EntityConstructor } from '../types/Entity.js';
|
|
5
|
+
import type { EntityFacetValue } from '../types/EntityFacets.js';
|
|
5
6
|
import type { DomainEvent, EntifixEnvelope, EntifixEnvelopeType } from './types.js';
|
|
6
7
|
/**
|
|
7
8
|
* Structural check only — `meta.type` is validated separately so a mismatch
|
|
@@ -13,14 +14,14 @@ export declare function isEntifixEnvelope(body: unknown): body is EntifixEnvelop
|
|
|
13
14
|
* requiring an entity constructor — for `command`/`transactionRecord` messages
|
|
14
15
|
* whose `data` is not a serialized entity. `label` only sharpens error text.
|
|
15
16
|
*/
|
|
16
|
-
export declare function readEnvelope<TData>(body: unknown, expected: EntifixEnvelopeType, label?: string): Effect.Effect<EntifixEnvelope<TData>,
|
|
17
|
-
export declare const readEntityEnvelope: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, body: unknown) => Effect.Effect<TEntity, EntifixBuildError, never>;
|
|
18
|
-
export declare const readEntityCollectionEnvelope: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, body: unknown) => Effect.Effect<TEntity[], EntifixBuildError, never>;
|
|
17
|
+
export declare function readEnvelope<TData>(body: unknown, expected: EntifixEnvelopeType, label?: string): Effect.Effect<EntifixEnvelope<TData>, InvalidEnvelope>;
|
|
18
|
+
export declare const readEntityEnvelope: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, body: unknown) => Effect.Effect<TEntity, import("../index.js").EntifixBuildError | InvalidEnvelope, never>;
|
|
19
|
+
export declare const readEntityCollectionEnvelope: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, body: unknown) => Effect.Effect<TEntity[], import("../index.js").EntifixBuildError | InvalidEnvelope, never>;
|
|
19
20
|
export declare const readEntityPageEnvelope: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, body: unknown) => Effect.Effect<{
|
|
20
21
|
items: TEntity[];
|
|
21
22
|
total: number;
|
|
22
23
|
request: import("../index.js").EntityLoadRequest<TEntity>;
|
|
23
|
-
}, EntifixBuildError, never>;
|
|
24
|
+
}, import("../index.js").EntifixBuildError | InvalidEnvelope, never>;
|
|
24
25
|
/**
|
|
25
26
|
* Reads a `$metadata` response back into its document.
|
|
26
27
|
*
|
|
@@ -29,7 +30,13 @@ export declare const readEntityPageEnvelope: <TEntity extends Entity>(entityCons
|
|
|
29
30
|
* actions" meant "denied" or "malformed", so a broken payload fails loudly here
|
|
30
31
|
* the way a half-populated entity does.
|
|
31
32
|
*/
|
|
32
|
-
export declare const readEntityMetadataEnvelope: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, body: unknown) => Effect.Effect<EntityMetadataDocument,
|
|
33
|
+
export declare const readEntityMetadataEnvelope: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, body: unknown) => Effect.Effect<EntityMetadataDocument, InvalidEnvelope, never>;
|
|
34
|
+
/**
|
|
35
|
+
* Reads a `$facets` response back into its counts, refusing a body whose lists
|
|
36
|
+
* are not `{ value, count }` rows — a panel that rendered a malformed count
|
|
37
|
+
* would show a number nobody computed.
|
|
38
|
+
*/
|
|
39
|
+
export declare const readEntityFacetsEnvelope: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, body: unknown) => Effect.Effect<Readonly<Record<string, readonly EntityFacetValue[]>>, InvalidEnvelope, never>;
|
|
33
40
|
/**
|
|
34
41
|
* Reads a bus message back into a {@link DomainEvent}.
|
|
35
42
|
*
|
|
@@ -44,5 +51,5 @@ export declare const readEntityMetadataEnvelope: <TEntity extends Entity>(entity
|
|
|
44
51
|
* `catalog.published` payload should look like, and inventing an opinion here
|
|
45
52
|
* is how the bus would start knowing about domains.
|
|
46
53
|
*/
|
|
47
|
-
export declare function readEventEnvelope<TData = unknown>(body: unknown): Effect.Effect<DomainEvent<TData>,
|
|
54
|
+
export declare function readEventEnvelope<TData = unknown>(body: unknown): Effect.Effect<DomainEvent<TData>, InvalidEnvelope>;
|
|
48
55
|
//# sourceMappingURL=read-envelope.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read-envelope.d.ts","sourceRoot":"","sources":["../../src/envelope/read-envelope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"read-envelope.d.ts","sourceRoot":"","sources":["../../src/envelope/read-envelope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAKrF,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EAEV,gBAAgB,EACjB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,KAAK,EACV,WAAW,EACX,eAAe,EACf,mBAAmB,EAGpB,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,eAAe,CAQxE;AA+BD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAChC,IAAI,EAAE,OAAO,EACb,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,SAAY,GAChB,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC,CAgBxD;AAED,eAAO,MAAM,kBAAkB,GAAI,OAAO,SAAS,MAAM,EACvD,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,OAAO,6FAsBX,CAAC;AAEL,eAAO,MAAM,4BAA4B,GAAI,OAAO,SAAS,MAAM,EACjE,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,OAAO,+FAaX,CAAC;AAEL,eAAO,MAAM,sBAAsB,GAAI,OAAO,SAAS,MAAM,EAC3D,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,OAAO;WAYO,OAAO,EAAE;;;oEAI3B,CAAC;AAEL;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,GAAI,OAAO,SAAS,MAAM,EAC/D,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,OAAO,kEAsBX,CAAC;AAUL;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GAAI,OAAO,SAAS,MAAM,EAC7D,mBAAmB,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,OAAO,iGAyBX,CAAC;AAKL;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,GAAG,OAAO,EAC/C,IAAI,EAAE,OAAO,GACZ,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC,CAkBpD"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
2
|
import { Effect } from 'effect';
|
|
3
|
-
import { EntifixBuildError } from '../base-entities/entifix-error/index.js';
|
|
4
3
|
import { deserializeEntityCollection, deserializeSingleEntity } from '../entity-definition/serializer/index.js';
|
|
4
|
+
import { InvalidEnvelope } from '../failure/generic.js';
|
|
5
5
|
import { envelopeEntityName } from './make-envelope.js';
|
|
6
6
|
/**
|
|
7
7
|
* Structural check only — `meta.type` is validated separately so a mismatch
|
|
@@ -15,22 +15,18 @@ import { envelopeEntityName } from './make-envelope.js';
|
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* Narrows an arbitrary body to an envelope of the expected `meta.type`, failing
|
|
18
|
-
* with an {@link
|
|
18
|
+
* with an {@link InvalidEnvelope} otherwise. A wrong shape must never
|
|
19
19
|
* silently deserialize into a half-populated entity.
|
|
20
20
|
*/ function assertEnvelope(entityConstructor, body, expected) {
|
|
21
21
|
const entity = envelopeEntityName(entityConstructor);
|
|
22
22
|
if (!isEntifixEnvelope(body)) {
|
|
23
|
-
return Effect.fail(new
|
|
24
|
-
entity
|
|
25
|
-
expected,
|
|
26
|
-
body
|
|
23
|
+
return Effect.fail(new InvalidEnvelope({
|
|
24
|
+
reason: `Expected an EntifixEnvelope for "${entity}" but the payload carried no meta.type`
|
|
27
25
|
}));
|
|
28
26
|
}
|
|
29
27
|
if (body.meta.type !== expected) {
|
|
30
|
-
return Effect.fail(new
|
|
31
|
-
entity
|
|
32
|
-
expected,
|
|
33
|
-
actual: body.meta.type
|
|
28
|
+
return Effect.fail(new InvalidEnvelope({
|
|
29
|
+
reason: `Expected an EntifixEnvelope of type "${expected}" for "${entity}" but got "${body.meta.type}"`
|
|
34
30
|
}));
|
|
35
31
|
}
|
|
36
32
|
return Effect.succeed(body);
|
|
@@ -41,17 +37,13 @@ import { envelopeEntityName } from './make-envelope.js';
|
|
|
41
37
|
* whose `data` is not a serialized entity. `label` only sharpens error text.
|
|
42
38
|
*/ export function readEnvelope(body, expected, label = 'message') {
|
|
43
39
|
if (!isEntifixEnvelope(body)) {
|
|
44
|
-
return Effect.fail(new
|
|
45
|
-
label
|
|
46
|
-
expected,
|
|
47
|
-
body
|
|
40
|
+
return Effect.fail(new InvalidEnvelope({
|
|
41
|
+
reason: `Expected an EntifixEnvelope for "${label}" but the payload carried no meta.type`
|
|
48
42
|
}));
|
|
49
43
|
}
|
|
50
44
|
if (body.meta.type !== expected) {
|
|
51
|
-
return Effect.fail(new
|
|
52
|
-
label
|
|
53
|
-
expected,
|
|
54
|
-
actual: body.meta.type
|
|
45
|
+
return Effect.fail(new InvalidEnvelope({
|
|
46
|
+
reason: `Expected an EntifixEnvelope of type "${expected}" for "${label}" but got "${body.meta.type}"`
|
|
55
47
|
}));
|
|
56
48
|
}
|
|
57
49
|
return Effect.succeed(body);
|
|
@@ -60,8 +52,8 @@ export const readEntityEnvelope = (entityConstructor, body)=>Effect.gen(function
|
|
|
60
52
|
const envelope = yield* assertEnvelope(entityConstructor, body, 'entity');
|
|
61
53
|
const entity = yield* deserializeSingleEntity(entityConstructor, envelope.data);
|
|
62
54
|
if (entity === undefined) {
|
|
63
|
-
return yield* Effect.fail(new
|
|
64
|
-
|
|
55
|
+
return yield* Effect.fail(new InvalidEnvelope({
|
|
56
|
+
reason: `EntifixEnvelope for "${envelopeEntityName(entityConstructor)}" carried no data`
|
|
65
57
|
}));
|
|
66
58
|
}
|
|
67
59
|
return entity;
|
|
@@ -93,12 +85,32 @@ export const readEntityPageEnvelope = (entityConstructor, body)=>Effect.gen(func
|
|
|
93
85
|
const envelope = yield* assertEnvelope(entityConstructor, body, 'entityMetadata');
|
|
94
86
|
const document = envelope.data;
|
|
95
87
|
if (!Array.isArray(document == null ? void 0 : document.actions) || !Array.isArray(document == null ? void 0 : document.useCases)) {
|
|
96
|
-
return yield* Effect.fail(new
|
|
97
|
-
|
|
88
|
+
return yield* Effect.fail(new InvalidEnvelope({
|
|
89
|
+
reason: `EntifixEnvelope for "${envelopeEntityName(entityConstructor)}" carried no metadata document`
|
|
98
90
|
}));
|
|
99
91
|
}
|
|
100
92
|
return document;
|
|
101
93
|
});
|
|
94
|
+
const isFacetValueRow = (row)=>row != null && typeof row === 'object' && [
|
|
95
|
+
'string',
|
|
96
|
+
'number',
|
|
97
|
+
'boolean'
|
|
98
|
+
].includes(typeof row.value) && typeof row.count === 'number';
|
|
99
|
+
/**
|
|
100
|
+
* Reads a `$facets` response back into its counts, refusing a body whose lists
|
|
101
|
+
* are not `{ value, count }` rows — a panel that rendered a malformed count
|
|
102
|
+
* would show a number nobody computed.
|
|
103
|
+
*/ export const readEntityFacetsEnvelope = (entityConstructor, body)=>Effect.gen(function*() {
|
|
104
|
+
const envelope = yield* assertEnvelope(entityConstructor, body, 'entityFacets');
|
|
105
|
+
const counts = envelope.data;
|
|
106
|
+
const wellFormed = counts != null && typeof counts === 'object' && Object.values(counts).every((rows)=>Array.isArray(rows) && rows.every(isFacetValueRow));
|
|
107
|
+
if (!wellFormed) {
|
|
108
|
+
return yield* Effect.fail(new InvalidEnvelope({
|
|
109
|
+
reason: `EntifixEnvelope for "${envelopeEntityName(entityConstructor)}" carried no facet counts`
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
return counts;
|
|
113
|
+
});
|
|
102
114
|
/** Every member `meta.event` must carry for a message to be routable at all. */ const REQUIRED_EVENT_META = [
|
|
103
115
|
'name',
|
|
104
116
|
'id',
|
|
@@ -124,9 +136,8 @@ export const readEntityPageEnvelope = (entityConstructor, body)=>Effect.gen(func
|
|
|
124
136
|
const event = envelope.meta.event;
|
|
125
137
|
const missing = REQUIRED_EVENT_META.filter((member)=>typeof (event == null ? void 0 : event[member]) !== 'string' || event[member] === '');
|
|
126
138
|
if (missing.length > 0) {
|
|
127
|
-
return yield* Effect.fail(new
|
|
128
|
-
missing,
|
|
129
|
-
body
|
|
139
|
+
return yield* Effect.fail(new InvalidEnvelope({
|
|
140
|
+
reason: `event envelope carried incomplete meta.event (missing ${missing.join(', ')})`
|
|
130
141
|
}));
|
|
131
142
|
}
|
|
132
143
|
return _extends({}, event, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/envelope/read-envelope.ts"],"sourcesContent":["import { Effect } from 'effect';\n\nimport { EntifixBuildError } from '../base-entities/entifix-error/index.js';\nimport type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';\nimport {\n deserializeEntityCollection,\n deserializeSingleEntity,\n} from '../entity-definition/serializer/index.js';\nimport type { Entity, EntityConstructor } from '../types/Entity.js';\nimport type { EntityPage } from '../types/EntityPage.js';\nimport { envelopeEntityName } from './make-envelope.js';\nimport type {\n DomainEvent,\n EntifixEnvelope,\n EntifixEnvelopeType,\n EntifixEventMeta,\n SerializedEntityPage,\n} from './types.js';\n\n/**\n * Structural check only — `meta.type` is validated separately so a mismatch\n * reports the expected/actual types rather than a generic \"not an envelope\".\n */\nexport function isEntifixEnvelope(body: unknown): body is EntifixEnvelope {\n if (body == null || typeof body !== 'object') {\n return false;\n }\n const meta = (body as EntifixEnvelope).meta;\n return (\n meta != null && typeof meta === 'object' && typeof meta.type === 'string'\n );\n}\n\n/**\n * Narrows an arbitrary body to an envelope of the expected `meta.type`, failing\n * with an {@link EntifixBuildError} otherwise. A wrong shape must never\n * silently deserialize into a half-populated entity.\n */\nfunction assertEnvelope<TEntity extends Entity, TData>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n expected: EntifixEnvelopeType,\n): Effect.Effect<EntifixEnvelope<TData>, EntifixBuildError> {\n const entity = envelopeEntityName(entityConstructor);\n\n if (!isEntifixEnvelope(body)) {\n return Effect.fail(\n new EntifixBuildError(\n `Expected an EntifixEnvelope for \"${entity}\" but the payload carried no meta.type`,\n undefined,\n { entity, expected, body },\n ),\n );\n }\n if (body.meta.type !== expected) {\n return Effect.fail(\n new EntifixBuildError(\n `Expected an EntifixEnvelope of type \"${expected}\" for \"${entity}\" but got \"${body.meta.type}\"`,\n undefined,\n { entity, expected, actual: body.meta.type },\n ),\n );\n }\n return Effect.succeed(body as EntifixEnvelope<TData>);\n}\n\n/**\n * Narrows an arbitrary body to an envelope of the expected `meta.type` without\n * requiring an entity constructor — for `command`/`transactionRecord` messages\n * whose `data` is not a serialized entity. `label` only sharpens error text.\n */\nexport function readEnvelope<TData>(\n body: unknown,\n expected: EntifixEnvelopeType,\n label = 'message',\n): Effect.Effect<EntifixEnvelope<TData>, EntifixBuildError> {\n if (!isEntifixEnvelope(body)) {\n return Effect.fail(\n new EntifixBuildError(\n `Expected an EntifixEnvelope for \"${label}\" but the payload carried no meta.type`,\n undefined,\n { label, expected, body },\n ),\n );\n }\n if (body.meta.type !== expected) {\n return Effect.fail(\n new EntifixBuildError(\n `Expected an EntifixEnvelope of type \"${expected}\" for \"${label}\" but got \"${body.meta.type}\"`,\n undefined,\n { label, expected, actual: body.meta.type },\n ),\n );\n }\n return Effect.succeed(body as EntifixEnvelope<TData>);\n}\n\nexport const readEntityEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, unknown>(\n entityConstructor,\n body,\n 'entity',\n );\n const entity = yield* deserializeSingleEntity(\n entityConstructor,\n envelope.data,\n );\n if (entity === undefined) {\n return yield* Effect.fail(\n new EntifixBuildError(\n `EntifixEnvelope for \"${envelopeEntityName(\n entityConstructor,\n )}\" carried no data`,\n undefined,\n { entity: envelopeEntityName(entityConstructor) },\n ),\n );\n }\n return entity;\n });\n\nexport const readEntityCollectionEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, unknown>(\n entityConstructor,\n body,\n 'entityCollection',\n );\n const items = yield* deserializeEntityCollection(\n entityConstructor,\n envelope.data,\n );\n return items as TEntity[];\n });\n\nexport const readEntityPageEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<\n TEntity,\n SerializedEntityPage<TEntity>\n >(entityConstructor, body, 'entityPage');\n const items = yield* deserializeEntityCollection(\n entityConstructor,\n envelope.data?.items,\n );\n return {\n items: items as TEntity[],\n total: envelope.data?.total ?? 0,\n request: envelope.data?.request ?? {},\n } satisfies EntityPage<TEntity>;\n });\n\n/**\n * Reads a `$metadata` response back into its document.\n *\n * No deserialization step, and no defaulting of the members either: an envelope\n * whose `data` is missing a list would leave a form guessing whether \"no\n * actions\" meant \"denied\" or \"malformed\", so a broken payload fails loudly here\n * the way a half-populated entity does.\n */\nexport const readEntityMetadataEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, EntityMetadataDocument>(\n entityConstructor,\n body,\n 'entityMetadata',\n );\n const document = envelope.data;\n if (\n !Array.isArray(document?.actions) ||\n !Array.isArray(document?.useCases)\n ) {\n return yield* Effect.fail(\n new EntifixBuildError(\n `EntifixEnvelope for \"${envelopeEntityName(\n entityConstructor,\n )}\" carried no metadata document`,\n undefined,\n { entity: envelopeEntityName(entityConstructor) },\n ),\n );\n }\n return document;\n });\n\n/** Every member `meta.event` must carry for a message to be routable at all. */\nconst REQUIRED_EVENT_META = ['name', 'id', 'source', 'at'] as const;\n\n/**\n * Reads a bus message back into a {@link DomainEvent}.\n *\n * The metadata is validated rather than defaulted, and that is the point: a\n * message missing `id` cannot be deduplicated and a message missing `name`\n * cannot be routed, so both are failures a consumer must see rather than\n * absences it silently works around. It is the same call\n * {@link readEntityMetadataEnvelope} makes — a broken payload fails loudly\n * instead of arriving half-populated.\n *\n * Note this does not validate `data`. The transport has no idea what a\n * `catalog.published` payload should look like, and inventing an opinion here\n * is how the bus would start knowing about domains.\n */\nexport function readEventEnvelope<TData = unknown>(\n body: unknown,\n): Effect.Effect<DomainEvent<TData>, EntifixBuildError> {\n return Effect.gen(function* () {\n const envelope = yield* readEnvelope<TData>(body, 'event', 'event');\n const event = envelope.meta.event;\n const missing = REQUIRED_EVENT_META.filter(\n member => typeof event?.[member] !== 'string' || event[member] === '',\n );\n if (missing.length > 0) {\n return yield* Effect.fail(\n new EntifixBuildError(\n `event envelope carried incomplete meta.event (missing ${missing.join(\n ', ',\n )})`,\n undefined,\n { missing, body },\n ),\n );\n }\n return { ...(event as EntifixEventMeta), data: envelope.data };\n });\n}\n"],"names":["Effect","EntifixBuildError","deserializeEntityCollection","deserializeSingleEntity","envelopeEntityName","isEntifixEnvelope","body","meta","type","assertEnvelope","entityConstructor","expected","entity","fail","undefined","actual","succeed","readEnvelope","label","readEntityEnvelope","gen","envelope","data","readEntityCollectionEnvelope","items","readEntityPageEnvelope","total","request","readEntityMetadataEnvelope","document","Array","isArray","actions","useCases","REQUIRED_EVENT_META","readEventEnvelope","event","missing","filter","member","length","join"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/envelope/read-envelope.ts"],"sourcesContent":["import { Effect } from 'effect';\n\nimport type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';\nimport {\n deserializeEntityCollection,\n deserializeSingleEntity,\n} from '../entity-definition/serializer/index.js';\nimport { InvalidEnvelope } from '../failure/generic.js';\nimport type { Entity, EntityConstructor } from '../types/Entity.js';\nimport type {\n EntityFacetCounts,\n EntityFacetValue,\n} from '../types/EntityFacets.js';\nimport type { EntityPage } from '../types/EntityPage.js';\nimport { envelopeEntityName } from './make-envelope.js';\nimport type {\n DomainEvent,\n EntifixEnvelope,\n EntifixEnvelopeType,\n EntifixEventMeta,\n SerializedEntityPage,\n} from './types.js';\n\n/**\n * Structural check only — `meta.type` is validated separately so a mismatch\n * reports the expected/actual types rather than a generic \"not an envelope\".\n */\nexport function isEntifixEnvelope(body: unknown): body is EntifixEnvelope {\n if (body == null || typeof body !== 'object') {\n return false;\n }\n const meta = (body as EntifixEnvelope).meta;\n return (\n meta != null && typeof meta === 'object' && typeof meta.type === 'string'\n );\n}\n\n/**\n * Narrows an arbitrary body to an envelope of the expected `meta.type`, failing\n * with an {@link InvalidEnvelope} otherwise. A wrong shape must never\n * silently deserialize into a half-populated entity.\n */\nfunction assertEnvelope<TEntity extends Entity, TData>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n expected: EntifixEnvelopeType,\n): Effect.Effect<EntifixEnvelope<TData>, InvalidEnvelope> {\n const entity = envelopeEntityName(entityConstructor);\n\n if (!isEntifixEnvelope(body)) {\n return Effect.fail(\n new InvalidEnvelope({\n reason: `Expected an EntifixEnvelope for \"${entity}\" but the payload carried no meta.type`,\n }),\n );\n }\n if (body.meta.type !== expected) {\n return Effect.fail(\n new InvalidEnvelope({\n reason: `Expected an EntifixEnvelope of type \"${expected}\" for \"${entity}\" but got \"${body.meta.type}\"`,\n }),\n );\n }\n return Effect.succeed(body as EntifixEnvelope<TData>);\n}\n\n/**\n * Narrows an arbitrary body to an envelope of the expected `meta.type` without\n * requiring an entity constructor — for `command`/`transactionRecord` messages\n * whose `data` is not a serialized entity. `label` only sharpens error text.\n */\nexport function readEnvelope<TData>(\n body: unknown,\n expected: EntifixEnvelopeType,\n label = 'message',\n): Effect.Effect<EntifixEnvelope<TData>, InvalidEnvelope> {\n if (!isEntifixEnvelope(body)) {\n return Effect.fail(\n new InvalidEnvelope({\n reason: `Expected an EntifixEnvelope for \"${label}\" but the payload carried no meta.type`,\n }),\n );\n }\n if (body.meta.type !== expected) {\n return Effect.fail(\n new InvalidEnvelope({\n reason: `Expected an EntifixEnvelope of type \"${expected}\" for \"${label}\" but got \"${body.meta.type}\"`,\n }),\n );\n }\n return Effect.succeed(body as EntifixEnvelope<TData>);\n}\n\nexport const readEntityEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, unknown>(\n entityConstructor,\n body,\n 'entity',\n );\n const entity = yield* deserializeSingleEntity(\n entityConstructor,\n envelope.data,\n );\n if (entity === undefined) {\n return yield* Effect.fail(\n new InvalidEnvelope({\n reason: `EntifixEnvelope for \"${envelopeEntityName(\n entityConstructor,\n )}\" carried no data`,\n }),\n );\n }\n return entity;\n });\n\nexport const readEntityCollectionEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, unknown>(\n entityConstructor,\n body,\n 'entityCollection',\n );\n const items = yield* deserializeEntityCollection(\n entityConstructor,\n envelope.data,\n );\n return items as TEntity[];\n });\n\nexport const readEntityPageEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<\n TEntity,\n SerializedEntityPage<TEntity>\n >(entityConstructor, body, 'entityPage');\n const items = yield* deserializeEntityCollection(\n entityConstructor,\n envelope.data?.items,\n );\n return {\n items: items as TEntity[],\n total: envelope.data?.total ?? 0,\n request: envelope.data?.request ?? {},\n } satisfies EntityPage<TEntity>;\n });\n\n/**\n * Reads a `$metadata` response back into its document.\n *\n * No deserialization step, and no defaulting of the members either: an envelope\n * whose `data` is missing a list would leave a form guessing whether \"no\n * actions\" meant \"denied\" or \"malformed\", so a broken payload fails loudly here\n * the way a half-populated entity does.\n */\nexport const readEntityMetadataEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, EntityMetadataDocument>(\n entityConstructor,\n body,\n 'entityMetadata',\n );\n const document = envelope.data;\n if (\n !Array.isArray(document?.actions) ||\n !Array.isArray(document?.useCases)\n ) {\n return yield* Effect.fail(\n new InvalidEnvelope({\n reason: `EntifixEnvelope for \"${envelopeEntityName(\n entityConstructor,\n )}\" carried no metadata document`,\n }),\n );\n }\n return document;\n });\n\nconst isFacetValueRow = (row: unknown): row is EntityFacetValue =>\n row != null &&\n typeof row === 'object' &&\n ['string', 'number', 'boolean'].includes(\n typeof (row as EntityFacetValue).value,\n ) &&\n typeof (row as EntityFacetValue).count === 'number';\n\n/**\n * Reads a `$facets` response back into its counts, refusing a body whose lists\n * are not `{ value, count }` rows — a panel that rendered a malformed count\n * would show a number nobody computed.\n */\nexport const readEntityFacetsEnvelope = <TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n body: unknown,\n) =>\n Effect.gen(function* () {\n const envelope = yield* assertEnvelope<TEntity, EntityFacetCounts>(\n entityConstructor,\n body,\n 'entityFacets',\n );\n const counts = envelope.data;\n const wellFormed =\n counts != null &&\n typeof counts === 'object' &&\n Object.values(counts).every(\n rows => Array.isArray(rows) && rows.every(isFacetValueRow),\n );\n if (!wellFormed) {\n return yield* Effect.fail(\n new InvalidEnvelope({\n reason: `EntifixEnvelope for \"${envelopeEntityName(\n entityConstructor,\n )}\" carried no facet counts`,\n }),\n );\n }\n return counts;\n });\n\n/** Every member `meta.event` must carry for a message to be routable at all. */\nconst REQUIRED_EVENT_META = ['name', 'id', 'source', 'at'] as const;\n\n/**\n * Reads a bus message back into a {@link DomainEvent}.\n *\n * The metadata is validated rather than defaulted, and that is the point: a\n * message missing `id` cannot be deduplicated and a message missing `name`\n * cannot be routed, so both are failures a consumer must see rather than\n * absences it silently works around. It is the same call\n * {@link readEntityMetadataEnvelope} makes — a broken payload fails loudly\n * instead of arriving half-populated.\n *\n * Note this does not validate `data`. The transport has no idea what a\n * `catalog.published` payload should look like, and inventing an opinion here\n * is how the bus would start knowing about domains.\n */\nexport function readEventEnvelope<TData = unknown>(\n body: unknown,\n): Effect.Effect<DomainEvent<TData>, InvalidEnvelope> {\n return Effect.gen(function* () {\n const envelope = yield* readEnvelope<TData>(body, 'event', 'event');\n const event = envelope.meta.event;\n const missing = REQUIRED_EVENT_META.filter(\n member => typeof event?.[member] !== 'string' || event[member] === '',\n );\n if (missing.length > 0) {\n return yield* Effect.fail(\n new InvalidEnvelope({\n reason: `event envelope carried incomplete meta.event (missing ${missing.join(\n ', ',\n )})`,\n }),\n );\n }\n return { ...(event as EntifixEventMeta), data: envelope.data };\n });\n}\n"],"names":["Effect","deserializeEntityCollection","deserializeSingleEntity","InvalidEnvelope","envelopeEntityName","isEntifixEnvelope","body","meta","type","assertEnvelope","entityConstructor","expected","entity","fail","reason","succeed","readEnvelope","label","readEntityEnvelope","gen","envelope","data","undefined","readEntityCollectionEnvelope","items","readEntityPageEnvelope","total","request","readEntityMetadataEnvelope","document","Array","isArray","actions","useCases","isFacetValueRow","row","includes","value","count","readEntityFacetsEnvelope","counts","wellFormed","Object","values","every","rows","REQUIRED_EVENT_META","readEventEnvelope","event","missing","filter","member","length","join"],"mappings":"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"}
|
package/dist/envelope/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';
|
|
2
2
|
import type { SerializedEntity } from '../entity-definition/serializer/index.js';
|
|
3
3
|
import type { Entity, EntityId } from '../types/Entity.js';
|
|
4
|
+
import type { EntityFacetCounts } from '../types/EntityFacets.js';
|
|
4
5
|
import type { EntityLoadRequest } from '../types/EntityLoadRequest.js';
|
|
5
6
|
/**
|
|
6
7
|
* Discriminates what {@link EntifixEnvelope.data} carries.
|
|
@@ -35,7 +36,7 @@ import type { EntityLoadRequest } from '../types/EntityLoadRequest.js';
|
|
|
35
36
|
* both the service that computes it and the controls that render it are already
|
|
36
37
|
* below the layer that would otherwise host it.
|
|
37
38
|
*/
|
|
38
|
-
export type EntifixEnvelopeType = 'entity' | 'entityCollection' | 'entityPage' | 'command' | 'event' | 'transactionAccepted' | 'transactionRecord' | 'sagaResult' | 'sagaInstance' | 'entityMetadata';
|
|
39
|
+
export type EntifixEnvelopeType = 'entity' | 'entityCollection' | 'entityPage' | 'command' | 'event' | 'transactionAccepted' | 'transactionRecord' | 'sagaResult' | 'sagaInstance' | 'entityMetadata' | 'entityFacets';
|
|
39
40
|
export type EntifixEnvelopeMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
40
41
|
/**
|
|
41
42
|
* A HATEOAS affordance: where the peer can go next from this message. Optional —
|
|
@@ -159,6 +160,7 @@ export type EntityEnvelope = EntifixEnvelope<SerializedEntity>;
|
|
|
159
160
|
export type EntityCollectionEnvelope = EntifixEnvelope<SerializedEntity[]>;
|
|
160
161
|
export type EntityPageEnvelope<TEntity extends Entity = Entity> = EntifixEnvelope<SerializedEntityPage<TEntity>>;
|
|
161
162
|
export type EntityMetadataEnvelope = EntifixEnvelope<EntityMetadataDocument>;
|
|
163
|
+
export type EntityFacetsEnvelope = EntifixEnvelope<EntityFacetCounts>;
|
|
162
164
|
/**
|
|
163
165
|
* A bus message on the wire. `meta.event` is always populated, and
|
|
164
166
|
* `readEventEnvelope` is what proves it before a consumer sees the payload.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/envelope/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AACrF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0CAA0C,CAAC;AACjF,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,MAAM,mBAAmB,GAC3B,QAAQ,GACR,kBAAkB,GAClB,YAAY,GACZ,SAAS,GACT,OAAO,GACP,qBAAqB,GACrB,mBAAmB,GACnB,YAAY,GACZ,cAAc,GACd,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/envelope/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/envelope/types.ts"],"sourcesContent":["import type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';\nimport type { SerializedEntity } from '../entity-definition/serializer/index.js';\nimport type { Entity, EntityId } from '../types/Entity.js';\nimport type { EntityLoadRequest } from '../types/EntityLoadRequest.js';\n\n/**\n * Discriminates what {@link EntifixEnvelope.data} carries.\n *\n * `command` extends the contract for the transactions layer: a write is issued\n * as a `command`, whose `data` shape lives in `@entifix/transactions` —\n * core only owns the discriminant so every artifact agrees on it.\n *\n * `event` is what actually rides the bus, and it is deliberately not named\n * after one publisher's flow: once a message can be `catalog.published` as\n * easily as `transaction.completed`, `transactionEvent` would be the wrong\n * name for it.\n *\n * ⚠️ **The four transaction-shaped discriminants below are four because they\n * carry four shapes.** `transactionEvent` used to mean all of them, and a\n * discriminant that means several things asserts nothing when it is read —\n * which is what `readTransactionEventEnvelope` was doing on the `202`: checking\n * a name that the record route and the event type both also answered to.\n *\n * - `transactionAccepted` — the `202` accept body, `{ transactionId, state }`.\n * Not a record: it has no `createdAt` and names no entity id yet.\n * - `transactionRecord` — the fold a client polls, a `TransactionRecord`.\n * - `sagaResult` — how a settled multi-step walk ended: which steps ran and\n * what each returned.\n * - `sagaInstance` — the durable instance itself, which is what a resumable\n * coordinator reads and what `GET /api/saga/:id` serves. It differs from\n * `sagaResult` in carrying the inputs and the resume count, because those are\n * what an operator needs to answer *why is this one still here*.\n *\n * `entityMetadata` is the same extension made once more, for the action model:\n * its `data` is an {@link EntityMetadataDocument}, which core does own because\n * both the service that computes it and the controls that render it are already\n * below the layer that would otherwise host it.\n */\nexport type EntifixEnvelopeType =\n | 'entity'\n | 'entityCollection'\n | 'entityPage'\n | 'command'\n | 'event'\n | 'transactionAccepted'\n | 'transactionRecord'\n | 'sagaResult'\n | 'sagaInstance'\n | 'entityMetadata';\n\nexport type EntifixEnvelopeMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';\n\n/**\n * A HATEOAS affordance: where the peer can go next from this message. Optional —\n * the envelope stands on its own without links, and only the artifact that owns\n * a route surface (today the REST service) is able to fill them in.\n */\nexport interface EntifixEnvelopeLink {\n rel: string;\n href: string;\n method?: EntifixEnvelopeMethod;\n}\n\n/**\n * The facts a message carries about *itself*, as opposed to about what\n * happened. Present only when the envelope is a bus message.\n *\n * The split rule: `meta` describes the message, `data` describes the\n * occurrence. So `correlationId` belongs here and an outcome's `code` does not.\n *\n * **`source` is never a consumer branch.** It is there for routing,\n * observability and audit. A handler that behaves differently depending on who\n * published re-couples the two services the bus decoupled — which is why .NET's\n * canonical `(sender, eventArgs)` types its sender as bare `object`, making the\n * dependency awkward on purpose. TypeScript cannot reproduce that friction, so\n * here it is a rule rather than a type.\n */\nexport interface EntifixEventMeta {\n /**\n * What happened, in the register's vocabulary: `catalog.published`,\n * `transaction.completed`. Declared in `tools/slices/*.slice.ts` as\n * `publishedEvents`/`subscriptions`, and used verbatim as the AMQP routing\n * key — which is what makes a subscriber's declared interest and its actual\n * queue binding the same string.\n */\n name: string;\n /**\n * Unique per message, and **the** deduplication key. Delivery is\n * at-least-once, so a consumer that must not fold twice keys on this.\n *\n * Not `correlationId`: one flow emits several messages, so correlating and\n * deduplicating are different questions. Keying dedup on the correlation id\n * makes a transaction's `completed` look like a duplicate of its `accepted`.\n */\n id: string;\n /** The emitting **slice** — the unit that owns a store — e.g. `catalog`. */\n source: string;\n /** ISO-8601 emission time. */\n at: string;\n /** Ties every message of one flow together — a transaction id, a saga id. */\n correlationId?: string;\n}\n\nexport interface EntifixEnvelopeMeta {\n type: EntifixEnvelopeType;\n /**\n * The target entity's `key` (falling back to its class name).\n *\n * Optional, because a bus message need not be about an entity at all —\n * `settlement.run.completed` is about a run. What a message *is* lives in\n * {@link EntifixEventMeta.name}; this stays the entity label the HTTP arm\n * routes on.\n */\n entity?: string;\n links?: EntifixEnvelopeLink[];\n /** Present only on a bus message. */\n event?: EntifixEventMeta;\n}\n\n/**\n * One message on the bus: its own metadata, plus the payload describing what\n * happened.\n *\n * Generic in the payload so a publisher keeps its own shape —\n * `DomainEvent<TransactionEvent>` today, `DomainEvent<PublishedOffering>` when\n * a projection's event lands — without the transport learning either.\n */\nexport interface DomainEvent<TData = unknown> extends EntifixEventMeta {\n data: TData;\n}\n\n/**\n * A change to an entity that happened on the server, delivered out-of-band —\n * the `data` of a bus/stream `event` envelope rather than a framing of its own\n *. `entity` is the `key ?? name` an adapter routes on, the same\n * string `entityQueryScope` uses, so a subscriber can invalidate exactly the\n * affected query keys.\n *\n * It lives in core rather than beside the React `ReactiveChannel` that consumes\n * it because both ends need it: the service maps a `TransactionEvent` onto it,\n * and the React packages sit above every tier that does the mapping.\n *\n * Deliberately **no** `transactionId`, timestamp or sequence: all three already\n * exist one level up as `meta.event.correlationId`, `.at` and `.id`, and\n * duplicating them onto the payload is how two sources of the same fact drift.\n */\nexport interface EntityChangeEvent {\n entity: string;\n change: 'created' | 'updated' | 'deleted';\n id: EntityId;\n}\n\n/**\n * The standard message exchanged between entifix artifacts.\n *\n * Every request and response body is an envelope: `meta` describes the payload,\n * `data` is the payload. It is deliberately transport-free — it lives in `core`\n * and knows nothing about HTTP — so the same contract can carry entities over\n * REST today and amqp or websockets later.\n */\nexport interface EntifixEnvelope<TData = unknown> {\n meta: EntifixEnvelopeMeta;\n data: TData;\n}\n\n/**\n * `data` shape when `meta.type` is `entityPage`. Generic in the entity so the\n * echoed `request` keeps its `keyof TEntity` filter/sort types — `EntityLoadRequest`\n * is invariant in its entity, so a non-generic `EntityLoadRequest<Entity>` here\n * would not accept a caller's `EntityLoadRequest<Product>`.\n */\nexport interface SerializedEntityPage<TEntity extends Entity = Entity> {\n items: SerializedEntity[];\n total: number;\n request?: EntityLoadRequest<TEntity>;\n}\n\nexport type EntityEnvelope = EntifixEnvelope<SerializedEntity>;\nexport type EntityCollectionEnvelope = EntifixEnvelope<SerializedEntity[]>;\nexport type EntityPageEnvelope<TEntity extends Entity = Entity> =\n EntifixEnvelope<SerializedEntityPage<TEntity>>;\nexport type EntityMetadataEnvelope = EntifixEnvelope<EntityMetadataDocument>;\n\n/**\n * A bus message on the wire. `meta.event` is always populated, and\n * `readEventEnvelope` is what proves it before a consumer sees the payload.\n */\nexport type EventEnvelope<TData = unknown> = EntifixEnvelope<TData> & {\n meta: EntifixEnvelopeMeta & { event: EntifixEventMeta };\n};\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/envelope/types.ts"],"sourcesContent":["import type { EntityMetadataDocument } from '../entity-definition/metadata/index.js';\nimport type { SerializedEntity } from '../entity-definition/serializer/index.js';\nimport type { Entity, EntityId } from '../types/Entity.js';\nimport type { EntityFacetCounts } from '../types/EntityFacets.js';\nimport type { EntityLoadRequest } from '../types/EntityLoadRequest.js';\n\n/**\n * Discriminates what {@link EntifixEnvelope.data} carries.\n *\n * `command` extends the contract for the transactions layer: a write is issued\n * as a `command`, whose `data` shape lives in `@entifix/transactions` —\n * core only owns the discriminant so every artifact agrees on it.\n *\n * `event` is what actually rides the bus, and it is deliberately not named\n * after one publisher's flow: once a message can be `catalog.published` as\n * easily as `transaction.completed`, `transactionEvent` would be the wrong\n * name for it.\n *\n * ⚠️ **The four transaction-shaped discriminants below are four because they\n * carry four shapes.** `transactionEvent` used to mean all of them, and a\n * discriminant that means several things asserts nothing when it is read —\n * which is what `readTransactionEventEnvelope` was doing on the `202`: checking\n * a name that the record route and the event type both also answered to.\n *\n * - `transactionAccepted` — the `202` accept body, `{ transactionId, state }`.\n * Not a record: it has no `createdAt` and names no entity id yet.\n * - `transactionRecord` — the fold a client polls, a `TransactionRecord`.\n * - `sagaResult` — how a settled multi-step walk ended: which steps ran and\n * what each returned.\n * - `sagaInstance` — the durable instance itself, which is what a resumable\n * coordinator reads and what `GET /api/saga/:id` serves. It differs from\n * `sagaResult` in carrying the inputs and the resume count, because those are\n * what an operator needs to answer *why is this one still here*.\n *\n * `entityMetadata` is the same extension made once more, for the action model:\n * its `data` is an {@link EntityMetadataDocument}, which core does own because\n * both the service that computes it and the controls that render it are already\n * below the layer that would otherwise host it.\n */\nexport type EntifixEnvelopeType =\n | 'entity'\n | 'entityCollection'\n | 'entityPage'\n | 'command'\n | 'event'\n | 'transactionAccepted'\n | 'transactionRecord'\n | 'sagaResult'\n | 'sagaInstance'\n | 'entityMetadata'\n | 'entityFacets';\n\nexport type EntifixEnvelopeMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';\n\n/**\n * A HATEOAS affordance: where the peer can go next from this message. Optional —\n * the envelope stands on its own without links, and only the artifact that owns\n * a route surface (today the REST service) is able to fill them in.\n */\nexport interface EntifixEnvelopeLink {\n rel: string;\n href: string;\n method?: EntifixEnvelopeMethod;\n}\n\n/**\n * The facts a message carries about *itself*, as opposed to about what\n * happened. Present only when the envelope is a bus message.\n *\n * The split rule: `meta` describes the message, `data` describes the\n * occurrence. So `correlationId` belongs here and an outcome's `code` does not.\n *\n * **`source` is never a consumer branch.** It is there for routing,\n * observability and audit. A handler that behaves differently depending on who\n * published re-couples the two services the bus decoupled — which is why .NET's\n * canonical `(sender, eventArgs)` types its sender as bare `object`, making the\n * dependency awkward on purpose. TypeScript cannot reproduce that friction, so\n * here it is a rule rather than a type.\n */\nexport interface EntifixEventMeta {\n /**\n * What happened, in the register's vocabulary: `catalog.published`,\n * `transaction.completed`. Declared in `tools/slices/*.slice.ts` as\n * `publishedEvents`/`subscriptions`, and used verbatim as the AMQP routing\n * key — which is what makes a subscriber's declared interest and its actual\n * queue binding the same string.\n */\n name: string;\n /**\n * Unique per message, and **the** deduplication key. Delivery is\n * at-least-once, so a consumer that must not fold twice keys on this.\n *\n * Not `correlationId`: one flow emits several messages, so correlating and\n * deduplicating are different questions. Keying dedup on the correlation id\n * makes a transaction's `completed` look like a duplicate of its `accepted`.\n */\n id: string;\n /** The emitting **slice** — the unit that owns a store — e.g. `catalog`. */\n source: string;\n /** ISO-8601 emission time. */\n at: string;\n /** Ties every message of one flow together — a transaction id, a saga id. */\n correlationId?: string;\n}\n\nexport interface EntifixEnvelopeMeta {\n type: EntifixEnvelopeType;\n /**\n * The target entity's `key` (falling back to its class name).\n *\n * Optional, because a bus message need not be about an entity at all —\n * `settlement.run.completed` is about a run. What a message *is* lives in\n * {@link EntifixEventMeta.name}; this stays the entity label the HTTP arm\n * routes on.\n */\n entity?: string;\n links?: EntifixEnvelopeLink[];\n /** Present only on a bus message. */\n event?: EntifixEventMeta;\n}\n\n/**\n * One message on the bus: its own metadata, plus the payload describing what\n * happened.\n *\n * Generic in the payload so a publisher keeps its own shape —\n * `DomainEvent<TransactionEvent>` today, `DomainEvent<PublishedOffering>` when\n * a projection's event lands — without the transport learning either.\n */\nexport interface DomainEvent<TData = unknown> extends EntifixEventMeta {\n data: TData;\n}\n\n/**\n * A change to an entity that happened on the server, delivered out-of-band —\n * the `data` of a bus/stream `event` envelope rather than a framing of its own\n *. `entity` is the `key ?? name` an adapter routes on, the same\n * string `entityQueryScope` uses, so a subscriber can invalidate exactly the\n * affected query keys.\n *\n * It lives in core rather than beside the React `ReactiveChannel` that consumes\n * it because both ends need it: the service maps a `TransactionEvent` onto it,\n * and the React packages sit above every tier that does the mapping.\n *\n * Deliberately **no** `transactionId`, timestamp or sequence: all three already\n * exist one level up as `meta.event.correlationId`, `.at` and `.id`, and\n * duplicating them onto the payload is how two sources of the same fact drift.\n */\nexport interface EntityChangeEvent {\n entity: string;\n change: 'created' | 'updated' | 'deleted';\n id: EntityId;\n}\n\n/**\n * The standard message exchanged between entifix artifacts.\n *\n * Every request and response body is an envelope: `meta` describes the payload,\n * `data` is the payload. It is deliberately transport-free — it lives in `core`\n * and knows nothing about HTTP — so the same contract can carry entities over\n * REST today and amqp or websockets later.\n */\nexport interface EntifixEnvelope<TData = unknown> {\n meta: EntifixEnvelopeMeta;\n data: TData;\n}\n\n/**\n * `data` shape when `meta.type` is `entityPage`. Generic in the entity so the\n * echoed `request` keeps its `keyof TEntity` filter/sort types — `EntityLoadRequest`\n * is invariant in its entity, so a non-generic `EntityLoadRequest<Entity>` here\n * would not accept a caller's `EntityLoadRequest<Product>`.\n */\nexport interface SerializedEntityPage<TEntity extends Entity = Entity> {\n items: SerializedEntity[];\n total: number;\n request?: EntityLoadRequest<TEntity>;\n}\n\nexport type EntityEnvelope = EntifixEnvelope<SerializedEntity>;\nexport type EntityCollectionEnvelope = EntifixEnvelope<SerializedEntity[]>;\nexport type EntityPageEnvelope<TEntity extends Entity = Entity> =\n EntifixEnvelope<SerializedEntityPage<TEntity>>;\nexport type EntityMetadataEnvelope = EntifixEnvelope<EntityMetadataDocument>;\nexport type EntityFacetsEnvelope = EntifixEnvelope<EntityFacetCounts>;\n\n/**\n * A bus message on the wire. `meta.event` is always populated, and\n * `readEventEnvelope` is what proves it before a consumer sees the payload.\n */\nexport type EventEnvelope<TData = unknown> = EntifixEnvelope<TData> & {\n meta: EntifixEnvelopeMeta & { event: EntifixEventMeta };\n};\n"],"names":[],"mappings":"AA0LA;;;CAGC,GACD,WAEE"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
import { type FailureCategory, type FailureFacts } from './failure.js';
|
|
3
|
+
export interface FailureOptions<Fields> {
|
|
4
|
+
/**
|
|
5
|
+
* The failure's `message`, from its fields — what a terminal shows when it is
|
|
6
|
+
* thrown and what a problem body carries as `detail`. Written for a reader:
|
|
7
|
+
* never a stack, never `String(error)`.
|
|
8
|
+
*/
|
|
9
|
+
readonly message?: (fields: Fields) => string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The definer for one category.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* class DuplicateSlug extends Failure.conflict<DuplicateSlug>()(
|
|
16
|
+
* 'DuplicateSlug',
|
|
17
|
+
* 'duplicateSlug',
|
|
18
|
+
* { slug: Schema.String },
|
|
19
|
+
* ) {}
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* The curried `<Self>()` is `Schema.TaggedError`'s own shape, kept because the
|
|
23
|
+
* class has to name itself for its instances to type as itself. `code`,
|
|
24
|
+
* `category` and `family` ride on the prototype — they are facts about the
|
|
25
|
+
* class, not fields an instance is constructed with — and enter the instance
|
|
26
|
+
* type through `TaggedError`'s brand parameter, as literals, so a predicate on
|
|
27
|
+
* them narrows a union.
|
|
28
|
+
*
|
|
29
|
+
* The code is registered here, at definition. A second tag claiming it throws
|
|
30
|
+
* before anything runs.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ The registry holds the class this returns, not the subclass that extends
|
|
33
|
+
* it, so a failure decoded from a wire is an instance of the base. That is why
|
|
34
|
+
* a failure class carries fields and no behaviour — a `message` written as a
|
|
35
|
+
* getter on the subclass would vanish on decode, so it is passed here and put
|
|
36
|
+
* on the base — and why nothing matches a failure with `instanceof`.
|
|
37
|
+
*/
|
|
38
|
+
export declare const defineFailure: <C extends FailureCategory>(category: C) => <Self = never>() => <const Tag extends string, const Code extends string, const Fields extends Schema.Struct.Fields = Record<never, never>>(tag: Tag, code: Code, fields?: Fields, options?: FailureOptions<Schema.Struct.Type<Fields>>) => [Self] extends [never] ? "Missing `Self` generic - use `class Self extends Schema.TaggedError<Self>(...)`" : Schema.Class<Self, Schema.TaggedStruct<Tag, Fields>, import("effect/Cause").YieldableError & FailureFacts<Code, C>>;
|
|
39
|
+
//# sourceMappingURL=define.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define.d.ts","sourceRoot":"","sources":["../../src/failure/define.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EACL,KAAK,eAAe,EAEpB,KAAK,YAAY,EAGlB,MAAM,cAAc,CAAC;AAGtB,MAAM,WAAW,cAAc,CAAC,MAAM;IACpC;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;CAC/C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,aAAa,GACvB,CAAC,SAAS,eAAe,EAAE,UAAU,CAAC,MACtC,IAAI,GAAG,KAAK,QAEX,KAAK,CAAC,GAAG,SAAS,MAAM,EACxB,KAAK,CAAC,IAAI,SAAS,MAAM,EACzB,KAAK,CAAC,MAAM,SAAS,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAEhE,KAAK,GAAG,EACR,MAAM,IAAI,EACV,SAAS,MAAM,EACf,UAAS,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAM,qOA0CzD,CAAC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
import { Schema } from 'effect';
|
|
3
|
+
import { familyOf, RESERVED_FIELDS } from './failure.js';
|
|
4
|
+
import { failureTypeUri, registerFailure } from './registry.js';
|
|
5
|
+
/**
|
|
6
|
+
* The definer for one category.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* class DuplicateSlug extends Failure.conflict<DuplicateSlug>()(
|
|
10
|
+
* 'DuplicateSlug',
|
|
11
|
+
* 'duplicateSlug',
|
|
12
|
+
* { slug: Schema.String },
|
|
13
|
+
* ) {}
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* The curried `<Self>()` is `Schema.TaggedError`'s own shape, kept because the
|
|
17
|
+
* class has to name itself for its instances to type as itself. `code`,
|
|
18
|
+
* `category` and `family` ride on the prototype — they are facts about the
|
|
19
|
+
* class, not fields an instance is constructed with — and enter the instance
|
|
20
|
+
* type through `TaggedError`'s brand parameter, as literals, so a predicate on
|
|
21
|
+
* them narrows a union.
|
|
22
|
+
*
|
|
23
|
+
* The code is registered here, at definition. A second tag claiming it throws
|
|
24
|
+
* before anything runs.
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ The registry holds the class this returns, not the subclass that extends
|
|
27
|
+
* it, so a failure decoded from a wire is an instance of the base. That is why
|
|
28
|
+
* a failure class carries fields and no behaviour — a `message` written as a
|
|
29
|
+
* getter on the subclass would vanish on decode, so it is passed here and put
|
|
30
|
+
* on the base — and why nothing matches a failure with `instanceof`.
|
|
31
|
+
*/ export const defineFailure = (category)=>()=>(tag, code, fields, options = {})=>{
|
|
32
|
+
const own = fields != null ? fields : {};
|
|
33
|
+
const shadowed = Object.keys(own).filter((key)=>RESERVED_FIELDS.has(key));
|
|
34
|
+
if (shadowed.length > 0) throw new Error(`failure '${tag}' declares ${shadowed.map((key)=>`'${key}'`).join(', ')}, ` + 'which the problem body reserves for itself. Rename the field.');
|
|
35
|
+
const Base = Schema.TaggedError()(tag, own);
|
|
36
|
+
// `Base` is still `TaggedError`'s conditional type here — `Self` resolves
|
|
37
|
+
// only at the `extends` site — so the prototype is reached through a cast.
|
|
38
|
+
Object.defineProperties(Base.prototype, _extends({
|
|
39
|
+
code: {
|
|
40
|
+
value: code,
|
|
41
|
+
enumerable: false
|
|
42
|
+
},
|
|
43
|
+
category: {
|
|
44
|
+
value: category,
|
|
45
|
+
enumerable: false
|
|
46
|
+
},
|
|
47
|
+
family: {
|
|
48
|
+
value: familyOf(category),
|
|
49
|
+
enumerable: false
|
|
50
|
+
}
|
|
51
|
+
}, options.message === undefined ? {} : {
|
|
52
|
+
message: {
|
|
53
|
+
get () {
|
|
54
|
+
return options.message == null ? void 0 : options.message.call(options, this);
|
|
55
|
+
},
|
|
56
|
+
enumerable: false
|
|
57
|
+
}
|
|
58
|
+
}));
|
|
59
|
+
registerFailure({
|
|
60
|
+
code,
|
|
61
|
+
tag,
|
|
62
|
+
category,
|
|
63
|
+
family: familyOf(category),
|
|
64
|
+
type: failureTypeUri(code),
|
|
65
|
+
failureClass: Base
|
|
66
|
+
});
|
|
67
|
+
return Base;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=define.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/failure/define.ts"],"sourcesContent":["import { Schema } from 'effect';\n\nimport {\n type FailureCategory,\n type FailureClass,\n type FailureFacts,\n familyOf,\n RESERVED_FIELDS,\n} from './failure.js';\nimport { failureTypeUri, registerFailure } from './registry.js';\n\nexport interface FailureOptions<Fields> {\n /**\n * The failure's `message`, from its fields — what a terminal shows when it is\n * thrown and what a problem body carries as `detail`. Written for a reader:\n * never a stack, never `String(error)`.\n */\n readonly message?: (fields: Fields) => string;\n}\n\n/**\n * The definer for one category.\n *\n * ```ts\n * class DuplicateSlug extends Failure.conflict<DuplicateSlug>()(\n * 'DuplicateSlug',\n * 'duplicateSlug',\n * { slug: Schema.String },\n * ) {}\n * ```\n *\n * The curried `<Self>()` is `Schema.TaggedError`'s own shape, kept because the\n * class has to name itself for its instances to type as itself. `code`,\n * `category` and `family` ride on the prototype — they are facts about the\n * class, not fields an instance is constructed with — and enter the instance\n * type through `TaggedError`'s brand parameter, as literals, so a predicate on\n * them narrows a union.\n *\n * The code is registered here, at definition. A second tag claiming it throws\n * before anything runs.\n *\n * ⚠️ The registry holds the class this returns, not the subclass that extends\n * it, so a failure decoded from a wire is an instance of the base. That is why\n * a failure class carries fields and no behaviour — a `message` written as a\n * getter on the subclass would vanish on decode, so it is passed here and put\n * on the base — and why nothing matches a failure with `instanceof`.\n */\nexport const defineFailure =\n <C extends FailureCategory>(category: C) =>\n <Self = never>() =>\n <\n const Tag extends string,\n const Code extends string,\n const Fields extends Schema.Struct.Fields = Record<never, never>,\n >(\n tag: Tag,\n code: Code,\n fields?: Fields,\n options: FailureOptions<Schema.Struct.Type<Fields>> = {},\n ) => {\n const own = fields ?? ({} as Fields);\n const shadowed = Object.keys(own).filter(key => RESERVED_FIELDS.has(key));\n if (shadowed.length > 0)\n throw new Error(\n `failure '${tag}' declares ${shadowed.map(key => `'${key}'`).join(', ')}, ` +\n 'which the problem body reserves for itself. Rename the field.',\n );\n\n const Base = Schema.TaggedError<Self, FailureFacts<Code, C>>()(tag, own);\n // `Base` is still `TaggedError`'s conditional type here — `Self` resolves\n // only at the `extends` site — so the prototype is reached through a cast.\n Object.defineProperties(\n (Base as unknown as { prototype: object }).prototype,\n {\n code: { value: code, enumerable: false },\n category: { value: category, enumerable: false },\n family: { value: familyOf(category), enumerable: false },\n ...(options.message === undefined\n ? {}\n : {\n message: {\n get(this: Schema.Struct.Type<Fields>) {\n return options.message?.(this);\n },\n enumerable: false,\n },\n }),\n },\n );\n\n registerFailure({\n code,\n tag,\n category,\n family: familyOf(category),\n type: failureTypeUri(code),\n failureClass: Base as unknown as FailureClass,\n });\n\n return Base;\n };\n"],"names":["Schema","familyOf","RESERVED_FIELDS","failureTypeUri","registerFailure","defineFailure","category","tag","code","fields","options","own","shadowed","Object","keys","filter","key","has","length","Error","map","join","Base","TaggedError","defineProperties","prototype","value","enumerable","family","message","undefined","get","type","failureClass"],"mappings":";AAAA,SAASA,MAAM,QAAQ,SAAS;AAEhC,SAIEC,QAAQ,EACRC,eAAe,QACV,eAAe;AACtB,SAASC,cAAc,EAAEC,eAAe,QAAQ,gBAAgB;AAWhE;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,MAAMC,gBACX,CAA4BC,WAC5B,IACA,CAKEC,KACAC,MACAC,QACAC,UAAsD,CAAC,CAAC;YAExD,MAAMC,MAAMF,iBAAAA,SAAW,CAAC;YACxB,MAAMG,WAAWC,OAAOC,IAAI,CAACH,KAAKI,MAAM,CAACC,CAAAA,MAAOd,gBAAgBe,GAAG,CAACD;YACpE,IAAIJ,SAASM,MAAM,GAAG,GACpB,MAAM,IAAIC,MACR,CAAC,SAAS,EAAEZ,IAAI,WAAW,EAAEK,SAASQ,GAAG,CAACJ,CAAAA,MAAO,CAAC,CAAC,EAAEA,IAAI,CAAC,CAAC,EAAEK,IAAI,CAAC,MAAM,EAAE,CAAC,GACzE;YAGN,MAAMC,OAAOtB,OAAOuB,WAAW,GAAgChB,KAAKI;YACpE,0EAA0E;YAC1E,2EAA2E;YAC3EE,OAAOW,gBAAgB,CACrB,AAACF,KAA0CG,SAAS,EACpD;gBACEjB,MAAM;oBAAEkB,OAAOlB;oBAAMmB,YAAY;gBAAM;gBACvCrB,UAAU;oBAAEoB,OAAOpB;oBAAUqB,YAAY;gBAAM;gBAC/CC,QAAQ;oBAAEF,OAAOzB,SAASK;oBAAWqB,YAAY;gBAAM;eACnDjB,QAAQmB,OAAO,KAAKC,YACpB,CAAC,IACD;gBACED,SAAS;oBACPE;wBACE,OAAOrB,QAAQmB,OAAO,oBAAfnB,QAAQmB,OAAO,MAAfnB,SAAkB,IAAI;oBAC/B;oBACAiB,YAAY;gBACd;YACF;YAIRvB,gBAAgB;gBACdI;gBACAD;gBACAD;gBACAsB,QAAQ3B,SAASK;gBACjB0B,MAAM7B,eAAeK;gBACrByB,cAAcX;YAChB;YAEA,OAAOA;QACT,EAAE"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Schema } from 'effect';
|
|
2
|
+
import type { EntifixError } from '../base-entities/entifix-error/entifix-error.js';
|
|
3
|
+
/**
|
|
4
|
+
* The two families every failure belongs to
|
|
5
|
+
* ([ADR 0013](../../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)).
|
|
6
|
+
*
|
|
7
|
+
* - `business` — an answer. The request was understood and the answer is no.
|
|
8
|
+
* Never retried, never trips a breaker.
|
|
9
|
+
* - `technical` — a non-answer. Something stopped the request being answered,
|
|
10
|
+
* and the layer directly above may retry it.
|
|
11
|
+
*/
|
|
12
|
+
export type FailureFamily = 'business' | 'technical';
|
|
13
|
+
/** The business categories, each with the status it answers by default. */
|
|
14
|
+
export declare const BUSINESS_CATEGORIES: {
|
|
15
|
+
readonly invalid: 400;
|
|
16
|
+
readonly unauthenticated: 401;
|
|
17
|
+
readonly forbidden: 403;
|
|
18
|
+
readonly notFound: 404;
|
|
19
|
+
readonly conflict: 409;
|
|
20
|
+
readonly declined: 422;
|
|
21
|
+
};
|
|
22
|
+
/** The technical categories, each with the status it answers by default. */
|
|
23
|
+
export declare const TECHNICAL_CATEGORIES: {
|
|
24
|
+
readonly contention: 409;
|
|
25
|
+
readonly exhausted: 429;
|
|
26
|
+
readonly unavailable: 503;
|
|
27
|
+
readonly timeout: 504;
|
|
28
|
+
readonly protocol: 502;
|
|
29
|
+
readonly internal: 500;
|
|
30
|
+
};
|
|
31
|
+
export type BusinessCategory = keyof typeof BUSINESS_CATEGORIES;
|
|
32
|
+
export type TechnicalCategory = keyof typeof TECHNICAL_CATEGORIES;
|
|
33
|
+
/**
|
|
34
|
+
* The closed set. Every layer branches on it — the shell's status and log
|
|
35
|
+
* tables, the bus's poison rule, the client's inline-versus-toast choice — so
|
|
36
|
+
* it stays small enough for each of them to be exhaustive over. Tags stay open.
|
|
37
|
+
*/
|
|
38
|
+
export type FailureCategory = BusinessCategory | TechnicalCategory;
|
|
39
|
+
export type FamilyOf<C extends FailureCategory> = C extends BusinessCategory ? 'business' : 'technical';
|
|
40
|
+
export declare const FAILURE_CATEGORIES: readonly FailureCategory[];
|
|
41
|
+
export declare const familyOf: <C extends FailureCategory>(category: C) => FamilyOf<C>;
|
|
42
|
+
/** The status a category answers with unless a host overrides its tag. */
|
|
43
|
+
export declare const defaultStatus: (category: FailureCategory) => number;
|
|
44
|
+
/**
|
|
45
|
+
* What every failure carries beyond its own fields. `code` is the permanent,
|
|
46
|
+
* wire-visible key a client translates; `category` and `family` are what every
|
|
47
|
+
* layer above decides by.
|
|
48
|
+
*/
|
|
49
|
+
export interface FailureFacts<Code extends string = string, C extends FailureCategory = FailureCategory> {
|
|
50
|
+
readonly code: Code;
|
|
51
|
+
readonly category: C;
|
|
52
|
+
readonly family: FamilyOf<C>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Any failure the framework or a host raises: a `Schema.TaggedError` carrying
|
|
56
|
+
* its code, category and family.
|
|
57
|
+
*/
|
|
58
|
+
export interface EntifixFailure<Tag extends string = string, Code extends string = string, C extends FailureCategory = FailureCategory> extends FailureFacts<Code, C> {
|
|
59
|
+
readonly _tag: Tag;
|
|
60
|
+
readonly message: string;
|
|
61
|
+
}
|
|
62
|
+
/** A failure class, as the registry and the problem codec hold it. */
|
|
63
|
+
export interface FailureClass {
|
|
64
|
+
new (...args: never[]): EntifixFailure;
|
|
65
|
+
readonly identifier: string;
|
|
66
|
+
readonly fields: Schema.Struct.Fields;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Members of the problem body that a failure's own fields must not shadow,
|
|
70
|
+
* because the decoder reads them back as the envelope rather than as fields.
|
|
71
|
+
*/
|
|
72
|
+
export declare const RESERVED_FIELDS: ReadonlySet<string>;
|
|
73
|
+
/**
|
|
74
|
+
* What a port or a hook fails with while the framework moves from the
|
|
75
|
+
* `EntifixError` class tree to typed failures: either one. Deleted with the
|
|
76
|
+
* class tree, in the release that removes it.
|
|
77
|
+
*/
|
|
78
|
+
export type TransitionalFailure = EntifixError | EntifixFailure;
|
|
79
|
+
//# sourceMappingURL=failure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"failure.d.ts","sourceRoot":"","sources":["../../src/failure/failure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iDAAiD,CAAC;AAEpF;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,WAAW,CAAC;AAErD,2EAA2E;AAC3E,eAAO,MAAM,mBAAmB;;;;;;;CAOtB,CAAC;AAEX,4EAA4E;AAC5E,eAAO,MAAM,oBAAoB;;;;;;;CAOvB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,MAAM,OAAO,mBAAmB,CAAC;AAChE,MAAM,MAAM,iBAAiB,GAAG,MAAM,OAAO,oBAAoB,CAAC;AAElE;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,gBAAgB,GAAG,iBAAiB,CAAC;AAEnE,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,eAAe,IAAI,CAAC,SAAS,gBAAgB,GACxE,UAAU,GACV,WAAW,CAAC;AAEhB,eAAO,MAAM,kBAAkB,EAAE,SAAS,eAAe,EAGxD,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,CAAC,SAAS,eAAe,EAAE,UAAU,CAAC,KAAG,QAAQ,CAAC,CAAC,CACC,CAAC;AAE9E,0EAA0E;AAC1E,eAAO,MAAM,aAAa,GAAI,UAAU,eAAe,KAAG,MAGD,CAAC;AAE1D;;;;GAIG;AACH,MAAM,WAAW,YAAY,CAC3B,IAAI,SAAS,MAAM,GAAG,MAAM,EAC5B,CAAC,SAAS,eAAe,GAAG,eAAe;IAE3C,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;CAC9B;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc,CAC7B,GAAG,SAAS,MAAM,GAAG,MAAM,EAC3B,IAAI,SAAS,MAAM,GAAG,MAAM,EAC5B,CAAC,SAAS,eAAe,GAAG,eAAe,CAC3C,SAAQ,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,sEAAsE;AACtE,MAAM,WAAW,YAAY;IAC3B,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,cAAc,CAAC;IACvC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;CACvC;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,CAW9C,CAAC;AAEH;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,YAAY,GAAG,cAAc,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** The business categories, each with the status it answers by default. */ export const BUSINESS_CATEGORIES = {
|
|
2
|
+
invalid: 400,
|
|
3
|
+
unauthenticated: 401,
|
|
4
|
+
forbidden: 403,
|
|
5
|
+
notFound: 404,
|
|
6
|
+
conflict: 409,
|
|
7
|
+
declined: 422
|
|
8
|
+
};
|
|
9
|
+
/** The technical categories, each with the status it answers by default. */ export const TECHNICAL_CATEGORIES = {
|
|
10
|
+
contention: 409,
|
|
11
|
+
exhausted: 429,
|
|
12
|
+
unavailable: 503,
|
|
13
|
+
timeout: 504,
|
|
14
|
+
protocol: 502,
|
|
15
|
+
internal: 500
|
|
16
|
+
};
|
|
17
|
+
export const FAILURE_CATEGORIES = [
|
|
18
|
+
...Object.keys(BUSINESS_CATEGORIES),
|
|
19
|
+
...Object.keys(TECHNICAL_CATEGORIES)
|
|
20
|
+
];
|
|
21
|
+
export const familyOf = (category)=>category in BUSINESS_CATEGORIES ? 'business' : 'technical';
|
|
22
|
+
/** The status a category answers with unless a host overrides its tag. */ export const defaultStatus = (category)=>category in BUSINESS_CATEGORIES ? BUSINESS_CATEGORIES[category] : TECHNICAL_CATEGORIES[category];
|
|
23
|
+
/**
|
|
24
|
+
* Members of the problem body that a failure's own fields must not shadow,
|
|
25
|
+
* because the decoder reads them back as the envelope rather than as fields.
|
|
26
|
+
*/ export const RESERVED_FIELDS = new Set([
|
|
27
|
+
'type',
|
|
28
|
+
'title',
|
|
29
|
+
'status',
|
|
30
|
+
'detail',
|
|
31
|
+
'instance',
|
|
32
|
+
'code',
|
|
33
|
+
'category',
|
|
34
|
+
'family',
|
|
35
|
+
'traceId',
|
|
36
|
+
'retryAfter'
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=failure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/failure/failure.ts"],"sourcesContent":["import type { Schema } from 'effect';\n\nimport type { EntifixError } from '../base-entities/entifix-error/entifix-error.js';\n\n/**\n * The two families every failure belongs to\n * ([ADR 0013](../../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)).\n *\n * - `business` — an answer. The request was understood and the answer is no.\n * Never retried, never trips a breaker.\n * - `technical` — a non-answer. Something stopped the request being answered,\n * and the layer directly above may retry it.\n */\nexport type FailureFamily = 'business' | 'technical';\n\n/** The business categories, each with the status it answers by default. */\nexport const BUSINESS_CATEGORIES = {\n invalid: 400,\n unauthenticated: 401,\n forbidden: 403,\n notFound: 404,\n conflict: 409,\n declined: 422,\n} as const;\n\n/** The technical categories, each with the status it answers by default. */\nexport const TECHNICAL_CATEGORIES = {\n contention: 409,\n exhausted: 429,\n unavailable: 503,\n timeout: 504,\n protocol: 502,\n internal: 500,\n} as const;\n\nexport type BusinessCategory = keyof typeof BUSINESS_CATEGORIES;\nexport type TechnicalCategory = keyof typeof TECHNICAL_CATEGORIES;\n\n/**\n * The closed set. Every layer branches on it — the shell's status and log\n * tables, the bus's poison rule, the client's inline-versus-toast choice — so\n * it stays small enough for each of them to be exhaustive over. Tags stay open.\n */\nexport type FailureCategory = BusinessCategory | TechnicalCategory;\n\nexport type FamilyOf<C extends FailureCategory> = C extends BusinessCategory\n ? 'business'\n : 'technical';\n\nexport const FAILURE_CATEGORIES: readonly FailureCategory[] = [\n ...(Object.keys(BUSINESS_CATEGORIES) as BusinessCategory[]),\n ...(Object.keys(TECHNICAL_CATEGORIES) as TechnicalCategory[]),\n];\n\nexport const familyOf = <C extends FailureCategory>(category: C): FamilyOf<C> =>\n (category in BUSINESS_CATEGORIES ? 'business' : 'technical') as FamilyOf<C>;\n\n/** The status a category answers with unless a host overrides its tag. */\nexport const defaultStatus = (category: FailureCategory): number =>\n category in BUSINESS_CATEGORIES\n ? BUSINESS_CATEGORIES[category as BusinessCategory]\n : TECHNICAL_CATEGORIES[category as TechnicalCategory];\n\n/**\n * What every failure carries beyond its own fields. `code` is the permanent,\n * wire-visible key a client translates; `category` and `family` are what every\n * layer above decides by.\n */\nexport interface FailureFacts<\n Code extends string = string,\n C extends FailureCategory = FailureCategory,\n> {\n readonly code: Code;\n readonly category: C;\n readonly family: FamilyOf<C>;\n}\n\n/**\n * Any failure the framework or a host raises: a `Schema.TaggedError` carrying\n * its code, category and family.\n */\nexport interface EntifixFailure<\n Tag extends string = string,\n Code extends string = string,\n C extends FailureCategory = FailureCategory,\n> extends FailureFacts<Code, C> {\n readonly _tag: Tag;\n readonly message: string;\n}\n\n/** A failure class, as the registry and the problem codec hold it. */\nexport interface FailureClass {\n new (...args: never[]): EntifixFailure;\n readonly identifier: string;\n readonly fields: Schema.Struct.Fields;\n}\n\n/**\n * Members of the problem body that a failure's own fields must not shadow,\n * because the decoder reads them back as the envelope rather than as fields.\n */\nexport const RESERVED_FIELDS: ReadonlySet<string> = new Set([\n 'type',\n 'title',\n 'status',\n 'detail',\n 'instance',\n 'code',\n 'category',\n 'family',\n 'traceId',\n 'retryAfter',\n]);\n\n/**\n * What a port or a hook fails with while the framework moves from the\n * `EntifixError` class tree to typed failures: either one. Deleted with the\n * class tree, in the release that removes it.\n */\nexport type TransitionalFailure = EntifixError | EntifixFailure;\n"],"names":["BUSINESS_CATEGORIES","invalid","unauthenticated","forbidden","notFound","conflict","declined","TECHNICAL_CATEGORIES","contention","exhausted","unavailable","timeout","protocol","internal","FAILURE_CATEGORIES","Object","keys","familyOf","category","defaultStatus","RESERVED_FIELDS","Set"],"mappings":"AAeA,yEAAyE,GACzE,OAAO,MAAMA,sBAAsB;IACjCC,SAAS;IACTC,iBAAiB;IACjBC,WAAW;IACXC,UAAU;IACVC,UAAU;IACVC,UAAU;AACZ,EAAW;AAEX,0EAA0E,GAC1E,OAAO,MAAMC,uBAAuB;IAClCC,YAAY;IACZC,WAAW;IACXC,aAAa;IACbC,SAAS;IACTC,UAAU;IACVC,UAAU;AACZ,EAAW;AAgBX,OAAO,MAAMC,qBAAiD;OACxDC,OAAOC,IAAI,CAAChB;OACZe,OAAOC,IAAI,CAACT;CACjB,CAAC;AAEF,OAAO,MAAMU,WAAW,CAA4BC,WACjDA,YAAYlB,sBAAsB,aAAa,YAA4B;AAE9E,wEAAwE,GACxE,OAAO,MAAMmB,gBAAgB,CAACD,WAC5BA,YAAYlB,sBACRA,mBAAmB,CAACkB,SAA6B,GACjDX,oBAAoB,CAACW,SAA8B,CAAC;AAoC1D;;;CAGC,GACD,OAAO,MAAME,kBAAuC,IAAIC,IAAI;IAC1D;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,EAAE"}
|