@entifix/core 0.9.0 → 0.10.1
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/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/read-envelope.d.ts +8 -8
- package/dist/envelope/read-envelope.d.ts.map +1 -1
- package/dist/envelope/read-envelope.js +18 -27
- package/dist/envelope/read-envelope.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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Context, Effect } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* The header a request's remaining budget travels in, as whole milliseconds
|
|
4
|
+
* ([ADR 0014](../../../../../docs/adr/0014-retry-once-at-the-layer-above-under-a-budget.md)).
|
|
5
|
+
*
|
|
6
|
+
* A remainder rather than a timestamp, for the reason gRPC's `grpc-timeout`
|
|
7
|
+
* is one: two hosts' clocks need not agree, and a skewed absolute deadline is
|
|
8
|
+
* either already past or never reached.
|
|
9
|
+
*/
|
|
10
|
+
export declare const DEADLINE_HEADER = "x-request-timeout-ms";
|
|
11
|
+
/** When the caller stops waiting, as epoch milliseconds on this host's clock. */
|
|
12
|
+
export interface Deadline {
|
|
13
|
+
readonly expiresAt: number;
|
|
14
|
+
}
|
|
15
|
+
declare const DeadlineTag_base: Context.ServiceClass<DeadlineTag, "@entifix/core/Deadline", Deadline>;
|
|
16
|
+
/**
|
|
17
|
+
* The request's deadline, provided by the shell per request. Absent means no
|
|
18
|
+
* caller named one; nothing below may invent it.
|
|
19
|
+
*/
|
|
20
|
+
export declare class DeadlineTag extends DeadlineTag_base {
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The deadline a header names, read against `now`. A value that is not a
|
|
24
|
+
* non-negative integer is no deadline at all rather than an immediate one: a
|
|
25
|
+
* malformed header must not turn every request into a timeout.
|
|
26
|
+
*/
|
|
27
|
+
export declare const deadlineFromHeader: (value: string | undefined, now: number) => Deadline | undefined;
|
|
28
|
+
/** Milliseconds left before `deadline`, never negative. */
|
|
29
|
+
export declare const remainingMs: (deadline: Deadline, now: number) => number;
|
|
30
|
+
/** The current request's remaining milliseconds, or `undefined` with no deadline. */
|
|
31
|
+
export declare const currentRemainingMs: Effect.Effect<number | undefined>;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=deadline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deadline.d.ts","sourceRoot":"","sources":["../../src/deadline/deadline.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAEjD;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,yBAAyB,CAAC;AAEtD,iFAAiF;AACjF,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;;AAED;;;GAGG;AACH,qBAAa,WAAY,SAAQ,gBAEhC;CAAG;AAEJ;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC7B,OAAO,MAAM,GAAG,SAAS,EACzB,KAAK,MAAM,KACV,QAAQ,GAAG,SAGb,CAAC;AAEF,2DAA2D;AAC3D,eAAO,MAAM,WAAW,GAAI,UAAU,QAAQ,EAAE,KAAK,MAAM,KAAG,MACvB,CAAC;AAExC,qFAAqF;AACrF,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,SAAS,CAOhE,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Context, Effect, Option } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* The header a request's remaining budget travels in, as whole milliseconds
|
|
4
|
+
* ([ADR 0014](../../../../../docs/adr/0014-retry-once-at-the-layer-above-under-a-budget.md)).
|
|
5
|
+
*
|
|
6
|
+
* A remainder rather than a timestamp, for the reason gRPC's `grpc-timeout`
|
|
7
|
+
* is one: two hosts' clocks need not agree, and a skewed absolute deadline is
|
|
8
|
+
* either already past or never reached.
|
|
9
|
+
*/ export const DEADLINE_HEADER = 'x-request-timeout-ms';
|
|
10
|
+
/**
|
|
11
|
+
* The request's deadline, provided by the shell per request. Absent means no
|
|
12
|
+
* caller named one; nothing below may invent it.
|
|
13
|
+
*/ export class DeadlineTag extends Context.Service()('@entifix/core/Deadline') {
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The deadline a header names, read against `now`. A value that is not a
|
|
17
|
+
* non-negative integer is no deadline at all rather than an immediate one: a
|
|
18
|
+
* malformed header must not turn every request into a timeout.
|
|
19
|
+
*/ export const deadlineFromHeader = (value, now)=>{
|
|
20
|
+
if (value === undefined || !/^\d+$/.test(value.trim())) return undefined;
|
|
21
|
+
return {
|
|
22
|
+
expiresAt: now + Number(value.trim())
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/** Milliseconds left before `deadline`, never negative. */ export const remainingMs = (deadline, now)=>Math.max(0, deadline.expiresAt - now);
|
|
26
|
+
/** The current request's remaining milliseconds, or `undefined` with no deadline. */ export const currentRemainingMs = Effect.gen(function*() {
|
|
27
|
+
const deadline = yield* Effect.serviceOption(DeadlineTag);
|
|
28
|
+
if (Option.isNone(deadline)) return undefined;
|
|
29
|
+
const now = yield* Effect.clockWith((clock)=>clock.currentTimeMillis);
|
|
30
|
+
return remainingMs(deadline.value, now);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=deadline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/deadline/deadline.ts"],"sourcesContent":["import { Context, Effect, Option } from 'effect';\n\n/**\n * The header a request's remaining budget travels in, as whole milliseconds\n * ([ADR 0014](../../../../../docs/adr/0014-retry-once-at-the-layer-above-under-a-budget.md)).\n *\n * A remainder rather than a timestamp, for the reason gRPC's `grpc-timeout`\n * is one: two hosts' clocks need not agree, and a skewed absolute deadline is\n * either already past or never reached.\n */\nexport const DEADLINE_HEADER = 'x-request-timeout-ms';\n\n/** When the caller stops waiting, as epoch milliseconds on this host's clock. */\nexport interface Deadline {\n readonly expiresAt: number;\n}\n\n/**\n * The request's deadline, provided by the shell per request. Absent means no\n * caller named one; nothing below may invent it.\n */\nexport class DeadlineTag extends Context.Service<DeadlineTag, Deadline>()(\n '@entifix/core/Deadline',\n) {}\n\n/**\n * The deadline a header names, read against `now`. A value that is not a\n * non-negative integer is no deadline at all rather than an immediate one: a\n * malformed header must not turn every request into a timeout.\n */\nexport const deadlineFromHeader = (\n value: string | undefined,\n now: number,\n): Deadline | undefined => {\n if (value === undefined || !/^\\d+$/.test(value.trim())) return undefined;\n return { expiresAt: now + Number(value.trim()) };\n};\n\n/** Milliseconds left before `deadline`, never negative. */\nexport const remainingMs = (deadline: Deadline, now: number): number =>\n Math.max(0, deadline.expiresAt - now);\n\n/** The current request's remaining milliseconds, or `undefined` with no deadline. */\nexport const currentRemainingMs: Effect.Effect<number | undefined> = Effect.gen(\n function* () {\n const deadline = yield* Effect.serviceOption(DeadlineTag);\n if (Option.isNone(deadline)) return undefined;\n const now = yield* Effect.clockWith(clock => clock.currentTimeMillis);\n return remainingMs(deadline.value, now);\n },\n);\n"],"names":["Context","Effect","Option","DEADLINE_HEADER","DeadlineTag","Service","deadlineFromHeader","value","now","undefined","test","trim","expiresAt","Number","remainingMs","deadline","Math","max","currentRemainingMs","gen","serviceOption","isNone","clockWith","clock","currentTimeMillis"],"mappings":"AAAA,SAASA,OAAO,EAAEC,MAAM,EAAEC,MAAM,QAAQ,SAAS;AAEjD;;;;;;;CAOC,GACD,OAAO,MAAMC,kBAAkB,uBAAuB;AAOtD;;;CAGC,GACD,OAAO,MAAMC,oBAAoBJ,QAAQK,OAAO,GAC9C;AACC;AAEH;;;;CAIC,GACD,OAAO,MAAMC,qBAAqB,CAChCC,OACAC;IAEA,IAAID,UAAUE,aAAa,CAAC,QAAQC,IAAI,CAACH,MAAMI,IAAI,KAAK,OAAOF;IAC/D,OAAO;QAAEG,WAAWJ,MAAMK,OAAON,MAAMI,IAAI;IAAI;AACjD,EAAE;AAEF,yDAAyD,GACzD,OAAO,MAAMG,cAAc,CAACC,UAAoBP,MAC9CQ,KAAKC,GAAG,CAAC,GAAGF,SAASH,SAAS,GAAGJ,KAAK;AAExC,mFAAmF,GACnF,OAAO,MAAMU,qBAAwDjB,OAAOkB,GAAG,CAC7E;IACE,MAAMJ,WAAW,OAAOd,OAAOmB,aAAa,CAAChB;IAC7C,IAAIF,OAAOmB,MAAM,CAACN,WAAW,OAAON;IACpC,MAAMD,MAAM,OAAOP,OAAOqB,SAAS,CAACC,CAAAA,QAASA,MAAMC,iBAAiB;IACpE,OAAOV,YAAYC,SAASR,KAAK,EAAEC;AACrC,GACA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect } from 'effect';
|
|
2
|
-
import {
|
|
2
|
+
import type { TransitionalFailure } from '../../../failure/failure.js';
|
|
3
3
|
import { Entity, EntityConstructor, EntityId } from '../../../types/Entity.js';
|
|
4
4
|
import { EntityLinkResolver } from '../entity-link/index.js';
|
|
5
5
|
export interface EntityCollectionLinkInit<TEntity extends Entity> {
|
|
@@ -20,8 +20,8 @@ export declare class EntityCollectionLink<TEntity extends Entity> {
|
|
|
20
20
|
/**
|
|
21
21
|
* Fetches every referenced target through the resolver and caches the result.
|
|
22
22
|
*/
|
|
23
|
-
reload(resolver: EntityLinkResolver): Effect.Effect<TEntity[],
|
|
24
|
-
resolve(resolver: EntityLinkResolver): Effect.Effect<TEntity[],
|
|
23
|
+
reload(resolver: EntityLinkResolver): Effect.Effect<TEntity[], TransitionalFailure>;
|
|
24
|
+
resolve(resolver: EntityLinkResolver): Effect.Effect<TEntity[], TransitionalFailure>;
|
|
25
25
|
get ids(): EntityId[];
|
|
26
26
|
get values(): TEntity[] | undefined;
|
|
27
27
|
get isLoaded(): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-collection-link.d.ts","sourceRoot":"","sources":["../../../../src/entity-definition/links/entity-collection-link/entity-collection-link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"entity-collection-link.d.ts","sourceRoot":"","sources":["../../../../src/entity-definition/links/entity-collection-link/entity-collection-link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAC/E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,WAAW,wBAAwB,CAAC,OAAO,SAAS,MAAM;IAC9D,GAAG,CAAC,EAAE,QAAQ,EAAE,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;CACpB;AAED;;;;GAIG;AACH,qBAAa,oBAAoB,CAAC,OAAO,SAAS,MAAM;;IAEtD,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;gBAOrD,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,IAAI,CAAC,EAAE,wBAAwB,CAAC,OAAO,CAAC;IAc1C,MAAM,CAAC,GAAG,EAAE,QAAQ,EAAE,GAAG,IAAI;IAI7B,SAAS,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS,GAAG,IAAI;IAS9C;;OAEG;IACH,MAAM,CACJ,QAAQ,EAAE,kBAAkB,GAC3B,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,mBAAmB,CAAC;IAQhD,OAAO,CACL,QAAQ,EAAE,kBAAkB,GAC3B,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,mBAAmB,CAAC;IAQhD,IAAI,GAAG,IAAI,QAAQ,EAAE,CAEpB;IAED,IAAI,MAAM,IAAI,OAAO,EAAE,GAAG,SAAS,CAElC;IAED,IAAI,QAAQ,IAAI,OAAO,CAEtB;CAEF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/entity-definition/links/entity-collection-link/entity-collection-link.ts"],"sourcesContent":["import { Effect } from 'effect';\n\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../../src/entity-definition/links/entity-collection-link/entity-collection-link.ts"],"sourcesContent":["import { Effect } from 'effect';\n\nimport type { TransitionalFailure } from '../../../failure/failure.js';\nimport { Entity, EntityConstructor, EntityId } from '../../../types/Entity.js';\nimport { EntityLinkResolver } from '../entity-link/index.js';\n\nexport interface EntityCollectionLinkInit<TEntity extends Entity> {\n ids?: EntityId[];\n values?: TEntity[];\n}\n\n/**\n * A one-to-many reference. Like {@link EntityLink} it accepts either foreign\n * keys (lazy, resolved through an {@link EntityLinkResolver}) or embedded\n * instances, but for a collection of targets.\n */\nexport class EntityCollectionLink<TEntity extends Entity> {\n // #region properties\n readonly entityConstructor: EntityConstructor<TEntity>;\n #ids: EntityId[];\n #values?: TEntity[];\n // #endregion\n\n // #region constructors\n constructor(\n entityConstructor: EntityConstructor<TEntity>,\n init?: EntityCollectionLinkInit<TEntity>,\n ) {\n this.entityConstructor = entityConstructor;\n this.#values = init?.values;\n this.#ids =\n init?.ids ??\n init?.values\n ?.map(value => value.id)\n .filter((id): id is EntityId => id != null) ??\n [];\n }\n // #endregion\n\n // #region methods\n setIds(ids: EntityId[]): void {\n this.#ids = ids;\n }\n\n setValues(values: TEntity[] | undefined): void {\n this.#values = values;\n if (values !== undefined) {\n this.#ids = values\n .map(value => value.id)\n .filter((id): id is EntityId => id != null);\n }\n }\n\n /**\n * Fetches every referenced target through the resolver and caches the result.\n */\n reload(\n resolver: EntityLinkResolver,\n ): Effect.Effect<TEntity[], TransitionalFailure> {\n return Effect.forEach(\n this.#ids,\n id => resolver.resolve(this.entityConstructor, id),\n { concurrency: 'unbounded' },\n ).pipe(Effect.tap(values => Effect.sync(() => this.setValues(values))));\n }\n\n resolve(\n resolver: EntityLinkResolver,\n ): Effect.Effect<TEntity[], TransitionalFailure> {\n return this.#values !== undefined\n ? Effect.succeed(this.#values)\n : this.reload(resolver);\n }\n // #endregion\n\n // #region accessors\n get ids(): EntityId[] {\n return this.#ids;\n }\n\n get values(): TEntity[] | undefined {\n return this.#values;\n }\n\n get isLoaded(): boolean {\n return this.#values !== undefined;\n }\n // #endregion\n}\n"],"names":["Effect","EntityCollectionLink","setIds","ids","setValues","values","undefined","map","value","id","filter","reload","resolver","forEach","resolve","entityConstructor","concurrency","pipe","tap","sync","succeed","isLoaded","init"],"mappings":";;AAAA,SAASA,MAAM,QAAQ,SAAS;IAmB9B,6DACA;AATF;;;;CAIC,GACD,OAAO,MAAMC;IAqBX,aAAa;IAEb,kBAAkB;IAClBC,OAAOC,GAAe,EAAQ;QAC5B,gCAAA,IAAI,EAAC,MAAA,QAAOA;IACd;IAEAC,UAAUC,MAA6B,EAAQ;QAC7C,gCAAA,IAAI,EAAC,SAAA,WAAUA;QACf,IAAIA,WAAWC,WAAW;YACxB,gCAAA,IAAI,EAAC,MAAA,QAAOD,OACTE,GAAG,CAACC,CAAAA,QAASA,MAAMC,EAAE,EACrBC,MAAM,CAAC,CAACD,KAAuBA,MAAM;QAC1C;IACF;IAEA;;GAEC,GACDE,OACEC,QAA4B,EACmB;QAC/C,OAAOZ,OAAOa,OAAO,CACnB,gCAAA,IAAI,EAAC,MAAA,OACLJ,CAAAA,KAAMG,SAASE,OAAO,CAAC,IAAI,CAACC,iBAAiB,EAAEN,KAC/C;YAAEO,aAAa;QAAY,GAC3BC,IAAI,CAACjB,OAAOkB,GAAG,CAACb,CAAAA,SAAUL,OAAOmB,IAAI,CAAC,IAAM,IAAI,CAACf,SAAS,CAACC;IAC/D;IAEAS,QACEF,QAA4B,EACmB;QAC/C,OAAO,gCAAA,IAAI,EAAC,SAAA,aAAYN,YACpBN,OAAOoB,OAAO,CAAC,gCAAA,IAAI,EAAC,SAAA,YACpB,IAAI,CAACT,MAAM,CAACC;IAClB;IACA,aAAa;IAEb,oBAAoB;IACpB,IAAIT,MAAkB;QACpB,OAAO,gCAAA,IAAI,EAAC,MAAA;IACd;IAEA,IAAIE,SAAgC;QAClC,OAAO,gCAAA,IAAI,EAAC,SAAA;IACd;IAEA,IAAIgB,WAAoB;QACtB,OAAO,gCAAA,IAAI,EAAC,SAAA,aAAYf;IAC1B;IAjEA,aAAa;IAEb,uBAAuB;IACvB,YACES,iBAA6C,EAC7CO,IAAwC,CACxC;YAIEA;YACAA;QAbJ,4BAAA;;mBAAA,KAAA;;QACA,4BAAA;;mBAAA,KAAA;;QAQE,IAAI,CAACP,iBAAiB,GAAGA;QACzB,gCAAA,IAAI,EAAC,SAAA,WAAUO,wBAAAA,KAAMjB,MAAM;QAC3B,gCAAA,IAAI,EAAC,MAAA,SACHiB,gBAAAA,wBAAAA,KAAMnB,GAAG,oBACTmB,yBAAAA,eAAAA,KAAMjB,MAAM,qBAAZiB,aACIf,GAAG,CAACC,CAAAA,QAASA,MAAMC,EAAE,EACtBC,MAAM,CAAC,CAACD,KAAuBA,MAAM,iBAHxCa,OAIA,EAAE;IACN;AAoDF"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Effect } from 'effect';
|
|
2
|
-
import {
|
|
2
|
+
import type { TransitionalFailure } from '../../../failure/failure.js';
|
|
3
3
|
import { Entity, EntityConstructor, EntityId } from '../../../types/Entity.js';
|
|
4
4
|
/**
|
|
5
5
|
* Framework-free seam a link uses to materialize its target entity. The concrete
|
|
@@ -9,7 +9,7 @@ import { Entity, EntityConstructor, EntityId } from '../../../types/Entity.js';
|
|
|
9
9
|
* repository contracts defined in `@entifix/business`.
|
|
10
10
|
*/
|
|
11
11
|
export interface EntityLinkResolver {
|
|
12
|
-
resolve<TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, id: EntityId): Effect.Effect<TEntity,
|
|
12
|
+
resolve<TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, id: EntityId): Effect.Effect<TEntity, TransitionalFailure>;
|
|
13
13
|
}
|
|
14
14
|
export interface EntityLinkInit<TEntity extends Entity> {
|
|
15
15
|
id?: EntityId;
|
|
@@ -46,11 +46,11 @@ export declare class EntityLink<TEntity extends Entity> {
|
|
|
46
46
|
* hits the resolver — use {@link resolve} to fetch only when not already
|
|
47
47
|
* loaded.
|
|
48
48
|
*/
|
|
49
|
-
reload(resolver: EntityLinkResolver): Effect.Effect<TEntity,
|
|
49
|
+
reload(resolver: EntityLinkResolver): Effect.Effect<TEntity, TransitionalFailure>;
|
|
50
50
|
/**
|
|
51
51
|
* Returns the cached target if present, otherwise reloads it once.
|
|
52
52
|
*/
|
|
53
|
-
resolve(resolver: EntityLinkResolver): Effect.Effect<TEntity,
|
|
53
|
+
resolve(resolver: EntityLinkResolver): Effect.Effect<TEntity, TransitionalFailure>;
|
|
54
54
|
get id(): EntityId;
|
|
55
55
|
get value(): TEntity | undefined;
|
|
56
56
|
get isLoaded(): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-link.d.ts","sourceRoot":"","sources":["../../../../src/entity-definition/links/entity-link/entity-link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"entity-link.d.ts","sourceRoot":"","sources":["../../../../src/entity-definition/links/entity-link/entity-link.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAE/E;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,OAAO,SAAS,MAAM,EAC5B,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,EAAE,EAAE,QAAQ,GACX,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,cAAc,CAAC,OAAO,SAAS,MAAM;IACpD,EAAE,CAAC,EAAE,QAAQ,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,UAAU,CAAC,OAAO,SAAS,MAAM;;IAE5C,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;gBAOrD,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,IAAI,CAAC,EAAE,cAAc,CAAC,OAAO,CAAC;IAYhC;;OAEG;IACH,KAAK,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI;IAIzB;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI;IAO1C;;;;OAIG;IACH,MAAM,CACJ,QAAQ,EAAE,kBAAkB,GAC3B,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,mBAAmB,CAAC;IAM9C;;OAEG;IACH,OAAO,CACL,QAAQ,EAAE,kBAAkB,GAC3B,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,mBAAmB,CAAC;IAQ9C,IAAI,EAAE,IAAI,QAAQ,CAEjB;IAED,IAAI,KAAK,IAAI,OAAO,GAAG,SAAS,CAE/B;IAED,IAAI,QAAQ,IAAI,OAAO,CAEtB;CAEF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/entity-definition/links/entity-link/entity-link.ts"],"sourcesContent":["import { Effect } from 'effect';\n\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../../src/entity-definition/links/entity-link/entity-link.ts"],"sourcesContent":["import { Effect } from 'effect';\n\nimport type { TransitionalFailure } from '../../../failure/failure.js';\nimport { Entity, EntityConstructor, EntityId } from '../../../types/Entity.js';\n\n/**\n * Framework-free seam a link uses to materialize its target entity. The concrete\n * implementation lives at the composition root (a REST-backed resolver on the\n * web, a Mongo-backed one on the backend) and is injected through Effect's\n * context. Keeping the interface in `core` avoids an upward dependency on the\n * repository contracts defined in `@entifix/business`.\n */\nexport interface EntityLinkResolver {\n resolve<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n id: EntityId,\n ): Effect.Effect<TEntity, TransitionalFailure>;\n}\n\nexport interface EntityLinkInit<TEntity extends Entity> {\n id?: EntityId;\n value?: TEntity;\n}\n\n/**\n * A reference from one entity to another. It models both shapes a raw payload\n * can carry for a relation:\n *\n * - a **foreign key** — only the target id is known, so the entity is fetched\n * lazily through {@link reload};\n * - **embedded data** — the target already arrived inline and was deserialized\n * into an instance, so {@link value} is populated and no fetch is needed.\n *\n * Resolution is expressed as an `Effect`, so the same link works in any\n * environment: the caller provides an {@link EntityLinkResolver} backed by\n * whatever adapter (REST, Mongo, …) fits the runtime.\n */\nexport class EntityLink<TEntity extends Entity> {\n // #region properties\n readonly entityConstructor: EntityConstructor<TEntity>;\n #id: EntityId;\n #value?: TEntity;\n // #endregion\n\n // #region constructors\n constructor(\n entityConstructor: EntityConstructor<TEntity>,\n init?: EntityLinkInit<TEntity>,\n ) {\n this.entityConstructor = entityConstructor;\n this.#id = init?.id;\n this.#value = init?.value;\n if (this.#value !== undefined && this.#id == null) {\n this.#id = this.#value.id;\n }\n }\n // #endregion\n\n // #region methods\n /**\n * Records the foreign key when the payload only referenced the target by id.\n */\n setId(id: EntityId): void {\n this.#id = id;\n }\n\n /**\n * Stores an already-materialized target (embedded payload or a completed\n * reload) and keeps the id in sync with it.\n */\n setValue(value: TEntity | undefined): void {\n this.#value = value;\n if (value !== undefined && value.id != null) {\n this.#id = value.id;\n }\n }\n\n /**\n * Fetches the target through the resolver and caches it on the link. Always\n * hits the resolver — use {@link resolve} to fetch only when not already\n * loaded.\n */\n reload(\n resolver: EntityLinkResolver,\n ): Effect.Effect<TEntity, TransitionalFailure> {\n return resolver\n .resolve(this.entityConstructor, this.#id)\n .pipe(Effect.tap(value => Effect.sync(() => this.setValue(value))));\n }\n\n /**\n * Returns the cached target if present, otherwise reloads it once.\n */\n resolve(\n resolver: EntityLinkResolver,\n ): Effect.Effect<TEntity, TransitionalFailure> {\n return this.#value !== undefined\n ? Effect.succeed(this.#value)\n : this.reload(resolver);\n }\n // #endregion\n\n // #region accessors\n get id(): EntityId {\n return this.#id ?? this.#value?.id;\n }\n\n get value(): TEntity | undefined {\n return this.#value;\n }\n\n get isLoaded(): boolean {\n return this.#value !== undefined;\n }\n // #endregion\n}\n"],"names":["Effect","EntityLink","setId","id","setValue","value","undefined","reload","resolver","resolve","entityConstructor","pipe","tap","sync","succeed","isLoaded","init"],"mappings":";;AAAA,SAASA,MAAM,QAAQ,SAAS;IAwC9B,2DACA;AAjBF;;;;;;;;;;;;CAYC,GACD,OAAO,MAAMC;IAmBX,aAAa;IAEb,kBAAkB;IAClB;;GAEC,GACDC,MAAMC,EAAY,EAAQ;QACxB,gCAAA,IAAI,EAAC,KAAA,OAAMA;IACb;IAEA;;;GAGC,GACDC,SAASC,KAA0B,EAAQ;QACzC,gCAAA,IAAI,EAAC,QAAA,UAASA;QACd,IAAIA,UAAUC,aAAaD,MAAMF,EAAE,IAAI,MAAM;YAC3C,gCAAA,IAAI,EAAC,KAAA,OAAME,MAAMF,EAAE;QACrB;IACF;IAEA;;;;GAIC,GACDI,OACEC,QAA4B,EACiB;QAC7C,OAAOA,SACJC,OAAO,CAAC,IAAI,CAACC,iBAAiB,EAAE,gCAAA,IAAI,EAAC,KAAA,MACrCC,IAAI,CAACX,OAAOY,GAAG,CAACP,CAAAA,QAASL,OAAOa,IAAI,CAAC,IAAM,IAAI,CAACT,QAAQ,CAACC;IAC9D;IAEA;;GAEC,GACDI,QACED,QAA4B,EACiB;QAC7C,OAAO,gCAAA,IAAI,EAAC,QAAA,YAAWF,YACnBN,OAAOc,OAAO,CAAC,gCAAA,IAAI,EAAC,QAAA,WACpB,IAAI,CAACP,MAAM,CAACC;IAClB;IACA,aAAa;IAEb,oBAAoB;IACpB,IAAIL,KAAe;;;QACjB,8CAAO,gCAAA,IAAI,EAAC,KAAA,gGAAO,gCAAA,IAAI,EAAC,QAAA,4BAAL,uCAAaA,EAAE;IACpC;IAEA,IAAIE,QAA6B;QAC/B,OAAO,gCAAA,IAAI,EAAC,QAAA;IACd;IAEA,IAAIU,WAAoB;QACtB,OAAO,gCAAA,IAAI,EAAC,QAAA,YAAWT;IACzB;IAvEA,aAAa;IAEb,uBAAuB;IACvB,YACEI,iBAA6C,EAC7CM,IAA8B,CAC9B;QARF,4BAAA;;mBAAA,KAAA;;QACA,4BAAA;;mBAAA,KAAA;;QAQE,IAAI,CAACN,iBAAiB,GAAGA;QACzB,gCAAA,IAAI,EAAC,KAAA,OAAMM,wBAAAA,KAAMb,EAAE;QACnB,gCAAA,IAAI,EAAC,QAAA,UAASa,wBAAAA,KAAMX,KAAK;QACzB,IAAI,gCAAA,IAAI,EAAC,QAAA,YAAWC,aAAa,gCAAA,IAAI,EAAC,KAAA,QAAO,MAAM;YACjD,gCAAA,IAAI,EAAC,KAAA,OAAM,gCAAA,IAAI,EAAC,QAAA,QAAOH,EAAE;QAC3B;IACF;AA4DF"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { TransitionalFailure } from '../../../failure/failure.js';
|
|
2
2
|
import { Entity, EntityConstructor } from '../../../types/Entity.js';
|
|
3
3
|
import { FilterGroup } from '../../../types/EntityFiltering.js';
|
|
4
4
|
import { EntitySorting } from '../../../types/EntitySorting.js';
|
|
@@ -18,7 +18,7 @@ export interface EntityLinkListView<TEntity extends Entity> {
|
|
|
18
18
|
currentPage: number;
|
|
19
19
|
pageSize: number;
|
|
20
20
|
isLoading: boolean;
|
|
21
|
-
error?:
|
|
21
|
+
error?: TransitionalFailure;
|
|
22
22
|
filtering?: FilterGroup<TEntity>;
|
|
23
23
|
sorting?: EntitySorting<TEntity>;
|
|
24
24
|
onPageChange: (page: number) => void;
|
|
@@ -32,7 +32,7 @@ export interface EntityLinkQuickSearch<TEntity extends Entity> {
|
|
|
32
32
|
setTerm: (term: string) => void;
|
|
33
33
|
options: ReadonlyArray<TEntity>;
|
|
34
34
|
isLoading: boolean;
|
|
35
|
-
error?:
|
|
35
|
+
error?: TransitionalFailure;
|
|
36
36
|
}
|
|
37
37
|
/** The browse half: the full list view, plus the dialog holding it. */
|
|
38
38
|
export interface EntityLinkBrowse<TEntity extends Entity> extends EntityLinkListView<TEntity> {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entity-link-source.d.ts","sourceRoot":"","sources":["../../../../src/entity-definition/links/entity-link-source/entity-link-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"entity-link-source.d.ts","sourceRoot":"","sources":["../../../../src/entity-definition/links/entity-link-source/entity-link-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AACvE,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,mCAAmC,CAAC;AAChE,OAAO,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAEhE;;;;;;;;;GASG;AACH,MAAM,WAAW,kBAAkB,CAAC,OAAO,SAAS,MAAM;IACxD,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,mBAAmB,CAAC;IAC5B,SAAS,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IACjC,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,iBAAiB,EAAE,CAAC,SAAS,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;IAC7D,eAAe,EAAE,CAAC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;CAC5D;AAED,yEAAyE;AACzE,MAAM,WAAW,qBAAqB,CAAC,OAAO,SAAS,MAAM;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IAChC,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B;AAED,uEAAuE;AACvE,MAAM,WAAW,gBAAgB,CAC/B,OAAO,SAAS,MAAM,CACtB,SAAQ,kBAAkB,CAAC,OAAO,CAAC;IACnC,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB,CAAC,OAAO,SAAS,MAAM;IACtD,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC9C,2EAA2E;IAC3E,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,MAAM,CAAC;IACrC;;;;;;;;;OASG;IACH,QAAQ,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;IACnE,KAAK,EAAE,qBAAqB,CAAC,OAAO,CAAC,CAAC;IACtC,MAAM,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;CACnC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/entity-definition/links/entity-link-source/entity-link-source.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"sources":["../../../../src/entity-definition/links/entity-link-source/entity-link-source.ts"],"sourcesContent":["import type { TransitionalFailure } from '../../../failure/failure.js';\nimport { Entity, EntityConstructor } from '../../../types/Entity.js';\nimport { FilterGroup } from '../../../types/EntityFiltering.js';\nimport { EntitySorting } from '../../../types/EntitySorting.js';\n\n/**\n * A paged, filterable list of link targets, expressed as state plus the commands\n * that change it.\n *\n * The member names match what a table already renders from, so a browse dialog\n * forwards this object through untouched. It carries no Effect and no React: the\n * hook that produces it lives in the integration layer, the components that\n * consume it in the controls layer, and those two may not import each other —\n * this contract in `core` is what lets them meet.\n */\nexport interface EntityLinkListView<TEntity extends Entity> {\n items: ReadonlyArray<TEntity>;\n totalItems: number;\n currentPage: number;\n pageSize: number;\n isLoading: boolean;\n error?: TransitionalFailure;\n filtering?: FilterGroup<TEntity>;\n sorting?: EntitySorting<TEntity>;\n onPageChange: (page: number) => void;\n onPageSizeChange: (pageSize: number) => void;\n onFilteringChange: (filtering: FilterGroup<TEntity>) => void;\n onSortingChange: (sorting: EntitySorting<TEntity>) => void;\n}\n\n/** The quick half: a term, and the short list of matches it produces. */\nexport interface EntityLinkQuickSearch<TEntity extends Entity> {\n term: string;\n setTerm: (term: string) => void;\n options: ReadonlyArray<TEntity>;\n isLoading: boolean;\n error?: TransitionalFailure;\n}\n\n/** The browse half: the full list view, plus the dialog holding it. */\nexport interface EntityLinkBrowse<\n TEntity extends Entity,\n> extends EntityLinkListView<TEntity> {\n isOpen: boolean;\n open: () => void;\n close: () => void;\n}\n\n/**\n * Everything an editor needs in order to set one relation, and nothing about how\n * the targets are fetched.\n *\n * Two ways in, because they answer different questions: {@link quick} is\n * \"I know roughly what it is called\", {@link browse} is \"I need to look at the\n * catalog and filter it\". {@link selected} is the third, quieter one — a draft\n * restored from storage holds an id and no instance, and an id is not a name.\n */\nexport interface EntityLinkSource<TEntity extends Entity> {\n entityConstructor: EntityConstructor<TEntity>;\n /** Reads a target's display label, per the owner's `linkLabelProperty`. */\n labelOf: (entity: TEntity) => string;\n /**\n * The currently held id, resolved into something a form can use.\n *\n * `entity` is the whole target, not just its name, and that matters: a draft\n * restored from storage holds ids only, so the instance a `linkSerialization:\n * 'embedded'` member needs at submit is gone. Resolving the id was already\n * happening for the label — carrying the instance out is what lets the form\n * rehydrate its selection instead of quietly writing the `id` shape onto an\n * `embedded` member.\n */\n selected: { label?: string; entity?: TEntity; isLoading: boolean };\n quick: EntityLinkQuickSearch<TEntity>;\n browse: EntityLinkBrowse<TEntity>;\n}\n"],"names":[],"mappings":"AAgDA;;;;;;;;CAQC,GACD,WAiBC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
import { FieldProblem } from '../../failure/generic.js';
|
|
2
3
|
import type { Entity, EntityConstructor, EntityId } from '../../types/Entity.js';
|
|
3
4
|
/**
|
|
4
5
|
* Records checked against their entity's metadata before anything serves them.
|
|
@@ -24,11 +25,26 @@ export interface RecordProblem {
|
|
|
24
25
|
readonly path: string;
|
|
25
26
|
readonly message: string;
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
declare const InvalidRecords_base: Schema.Class<InvalidRecords, Schema.TaggedStruct<"InvalidRecords", {
|
|
29
|
+
readonly errors: Schema.$Array<Schema.Struct<{
|
|
30
|
+
readonly pointer: Schema.String;
|
|
31
|
+
readonly detail: Schema.String;
|
|
32
|
+
readonly code: Schema.String;
|
|
33
|
+
}>>;
|
|
34
|
+
}>, import("effect/Cause").YieldableError & import("../../index.js").FailureFacts<"invalidRecords", "invalid">>;
|
|
35
|
+
/**
|
|
36
|
+
* Thrown by {@link validRecordsOrThrow}: an `invalid` failure carrying every
|
|
37
|
+
* problem found, each as a field problem whose `pointer` is the problem's path
|
|
38
|
+
* ([ADR 0013](../../../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)).
|
|
39
|
+
*
|
|
40
|
+
* Its `message` lists them, because the usual place it surfaces is a build that
|
|
41
|
+
* read a bundled file — where the person reading is looking at a terminal, not
|
|
42
|
+
* a problem body.
|
|
43
|
+
*/
|
|
44
|
+
export declare class InvalidRecords extends InvalidRecords_base {
|
|
31
45
|
}
|
|
46
|
+
/** The field problems {@link InvalidRecords} carries for a set of record problems. */
|
|
47
|
+
export declare const recordFieldProblems: (problems: readonly RecordProblem[]) => FieldProblem[];
|
|
32
48
|
/**
|
|
33
49
|
* A check one entity needs that metadata cannot express — at most one contact
|
|
34
50
|
* channel per type, say. Handed every record (dates already parsed) and a way
|
|
@@ -70,6 +86,7 @@ export interface ValidateRecordsResult {
|
|
|
70
86
|
* cardinality — then runs the caller's rules.
|
|
71
87
|
*/
|
|
72
88
|
export declare function validateRecords<TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, records: readonly unknown[], { source, linkTargets, rules, locales: required, view, }?: ValidateRecordsOptions): ValidateRecordsResult;
|
|
73
|
-
/** The records with their dates parsed, or
|
|
89
|
+
/** The records with their dates parsed, or an {@link InvalidRecords}. */
|
|
74
90
|
export declare function validRecordsOrThrow<TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, records: readonly unknown[], options?: ValidateRecordsOptions): ReadonlyArray<Record<string, unknown>>;
|
|
91
|
+
export {};
|
|
75
92
|
//# sourceMappingURL=validate-records.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-records.d.ts","sourceRoot":"","sources":["../../../src/entity-definition/validate/validate-records.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"validate-records.d.ts","sourceRoot":"","sources":["../../../src/entity-definition/validate/validate-records.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhC,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAExD,OAAO,KAAK,EACV,MAAM,EACN,iBAAiB,EACjB,QAAQ,EACT,MAAM,uBAAuB,CAAC;AAS/B;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;;;;;;;;AAED;;;;;;;;GAQG;AACH,qBAAa,cAAe,SAAQ,mBAUnC;CAAG;AAEJ,sFAAsF;AACtF,eAAO,MAAM,mBAAmB,GAC9B,UAAU,SAAS,aAAa,EAAE,KACjC,YAAY,EAKV,CAAC;AAEN;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAC7B,OAAO,EAAE,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,EACzD,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,KACzE,IAAI,CAAC;AAEV,MAAM,WAAW,sBAAsB;IACrC,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;IACvE,4DAA4D;IAC5D,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,gBAAgB,EAAE,CAAC;IAC7C;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;IAC5C;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC1D;AA0JD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,SAAS,MAAM,EACpD,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,EACE,MAAM,EACN,WAAgB,EAChB,KAAU,EACV,OAAO,EAAE,QAAoB,EAC7B,IAAI,GACL,GAAE,sBAA2B,GAC7B,qBAAqB,CAkGvB;AAED,yEAAyE;AACzE,wBAAgB,mBAAmB,CAAC,OAAO,SAAS,MAAM,EACxD,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,OAAO,CAAC,EAAE,sBAAsB,GAC/B,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAMxC"}
|
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
-
import {
|
|
2
|
+
import { Schema } from 'effect';
|
|
3
|
+
import { defineFailure } from '../../failure/define.js';
|
|
4
|
+
import { FieldProblem } from '../../failure/generic.js';
|
|
3
5
|
import { locales } from '../../i18n/locales.js';
|
|
4
6
|
import { COLLECTION_TYPES, describeEntityColumns, extractEntityView } from '../describe/index.js';
|
|
5
7
|
import { extractMetaEntity } from '../helpers/index.js';
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Thrown by {@link validRecordsOrThrow}: an `invalid` failure carrying every
|
|
10
|
+
* problem found, each as a field problem whose `pointer` is the problem's path
|
|
11
|
+
* ([ADR 0013](../../../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)).
|
|
12
|
+
*
|
|
13
|
+
* Its `message` lists them, because the usual place it surfaces is a build that
|
|
14
|
+
* read a bundled file — where the person reading is looking at a terminal, not
|
|
15
|
+
* a problem body.
|
|
16
|
+
*/ export class InvalidRecords extends defineFailure('invalid')()('InvalidRecords', 'invalidRecords', {
|
|
17
|
+
errors: Schema.Array(FieldProblem)
|
|
18
|
+
}, {
|
|
19
|
+
message: ({ errors })=>`Records are invalid:\n${errors.map((problem)=>` ${problem.pointer} ${problem.detail}`).join('\n')}`
|
|
20
|
+
}) {
|
|
13
21
|
}
|
|
22
|
+
/** The field problems {@link InvalidRecords} carries for a set of record problems. */ export const recordFieldProblems = (problems)=>problems.map((problem)=>({
|
|
23
|
+
pointer: problem.path,
|
|
24
|
+
detail: problem.message,
|
|
25
|
+
code: 'invalidRecord'
|
|
26
|
+
}));
|
|
14
27
|
const isPlainObject = (value)=>value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
15
28
|
const isAbsent = (value)=>value === undefined || value === null || value === '';
|
|
16
29
|
const counted = (n, noun)=>`${n} ${noun}${n === 1 ? '' : 's'}`;
|
|
@@ -194,10 +207,12 @@ const counted = (n, noun)=>`${n} ${noun}${n === 1 ? '' : 's'}`;
|
|
|
194
207
|
records: parsed
|
|
195
208
|
};
|
|
196
209
|
}
|
|
197
|
-
/** The records with their dates parsed, or
|
|
210
|
+
/** The records with their dates parsed, or an {@link InvalidRecords}. */ export function validRecordsOrThrow(entityConstructor, records, options) {
|
|
198
211
|
const result = validateRecords(entityConstructor, records, options);
|
|
199
212
|
if (result.problems.length > 0) {
|
|
200
|
-
throw new
|
|
213
|
+
throw new InvalidRecords({
|
|
214
|
+
errors: recordFieldProblems(result.problems)
|
|
215
|
+
});
|
|
201
216
|
}
|
|
202
217
|
return result.records;
|
|
203
218
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/entity-definition/validate/validate-records.ts"],"sourcesContent":["import { EntifixBuildError } from '../../base-entities/entifix-error/index.js';\nimport { locales } from '../../i18n/locales.js';\nimport type {\n Entity,\n EntityConstructor,\n EntityId,\n} from '../../types/Entity.js';\nimport {\n COLLECTION_TYPES,\n describeEntityColumns,\n type EntityFieldDescriptor,\n extractEntityView,\n} from '../describe/index.js';\nimport { extractMetaEntity } from '../helpers/index.js';\n\n/**\n * Records checked against their entity's metadata before anything serves them.\n *\n * The mapping does not do this, on purpose: `deserializeSingleEntity` assigns\n * every non-relation value raw — it converts no date, enforces no `required`,\n * validates no enum value and resolves no link id — because a record off a\n * database was written through that same mapping and needs none of it. Records\n * that arrive as **untyped JSON** — files bundled with a static build, an\n * import, a seed — have had no such guarantee, and a wrong one reaches a reader\n * as `undefined` in the middle of a page, or as a string where a `Date` was\n * expected. This is the check those records go through.\n *\n * It sits in core, beside the deserializer it makes up for, so every adapter\n * that reads untyped records shares it — and the generated form judges a\n * draft by the same metadata, so a record a form refuses is one this refuses.\n *\n * Every problem is collected rather than the first thrown, each with the path\n * to its value: `employment.json › acme › role is required, and is missing`.\n */\n\nexport interface RecordProblem {\n /** `<source> › <id> › <member>`, as far as each is known. */\n readonly path: string;\n readonly message: string;\n}\n\n/** Thrown by {@link validRecordsOrThrow}. Carries every problem found. */\nexport class RecordValidationError extends EntifixBuildError {\n readonly problems: readonly RecordProblem[];\n\n constructor(problems: readonly RecordProblem[]) {\n super(\n `Records are invalid:\\n${problems\n .map(problem => ` ${problem.path} ${problem.message}`)\n .join('\\n')}`,\n undefined,\n { count: problems.length },\n );\n this.problems = problems;\n }\n}\n\n/**\n * A check one entity needs that metadata cannot express — at most one contact\n * channel per type, say. Handed every record (dates already parsed) and a way\n * to report against one of them by index.\n */\nexport type EntityRecordRule = (\n records: ReadonlyArray<Readonly<Record<string, unknown>>>,\n report: (index: number, member: string | undefined, message: string) => void,\n) => void;\n\nexport interface ValidateRecordsOptions {\n /** Where the records came from, for the path in a message. */\n readonly source?: string;\n /**\n * The ids that exist for each `link` or `linkCollection` member, by member\n * name. A member left out is checked for shape only.\n */\n readonly linkTargets?: Readonly<Record<string, ReadonlySet<EntityId>>>;\n /** Checks of the caller's own, run after the metadata's. */\n readonly rules?: readonly EntityRecordRule[];\n /**\n * Every locale a `localized` member must carry. Defaults to the locales the\n * host declared through `defineLocales`.\n */\n readonly locales?: readonly string[];\n /**\n * The view the records were published in. A member it omits is not\n * required of them, since the view left it out on purpose.\n */\n readonly view?: string;\n}\n\nexport interface ValidateRecordsResult {\n readonly problems: readonly RecordProblem[];\n /**\n * The records with every `date` member parsed into a `Date`, ready for the\n * mapping. Meaningless when `problems` is not empty.\n */\n readonly records: ReadonlyArray<Record<string, unknown>>;\n}\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n value !== null && typeof value === 'object' && !Array.isArray(value);\n\nconst isAbsent = (value: unknown): boolean =>\n value === undefined || value === null || value === '';\n\ntype Report = (member: string | undefined, message: string) => void;\n\nconst counted = (n: number, noun: string) =>\n `${n} ${noun}${n === 1 ? '' : 's'}`;\n\n/** A date member, parsed: the mapping would leave it a string. */\nfunction parseDate(\n value: unknown,\n report: Report,\n member: string,\n): Date | undefined {\n if (value instanceof Date) {\n if (Number.isNaN(value.getTime())) report(member, 'is an invalid date');\n return Number.isNaN(value.getTime()) ? undefined : value;\n }\n if (typeof value !== 'string' && typeof value !== 'number') {\n report(member, `holds a ${typeof value}, not a date`);\n return undefined;\n }\n const parsed = new Date(value);\n if (Number.isNaN(parsed.getTime())) {\n report(member, `is not a date: ${JSON.stringify(value)}`);\n return undefined;\n }\n return parsed;\n}\n\n/** A relation: ids, or embedded records, and ids that exist. */\nfunction checkLink(\n value: unknown,\n descriptor: EntityFieldDescriptor,\n known: ReadonlySet<EntityId> | undefined,\n report: Report,\n): void {\n const isCollection = descriptor.type === 'linkCollection';\n if (isCollection && !Array.isArray(value)) {\n report(descriptor.name, 'is not a list');\n return;\n }\n for (const id of isCollection ? (value as unknown[]) : [value]) {\n if (isPlainObject(id)) continue; // An embedded record carries its own.\n if (typeof id !== 'string' && typeof id !== 'number') {\n report(descriptor.name, `holds a ${typeof id} where an id was expected`);\n } else if (known !== undefined && !known.has(id)) {\n report(\n descriptor.name,\n `points at ${JSON.stringify(id)}, which does not exist`,\n );\n }\n }\n}\n\n/** A scalar: the type the metadata declares. */\nfunction checkScalar(\n value: unknown,\n descriptor: EntityFieldDescriptor,\n report: Report,\n): void {\n switch (descriptor.type) {\n case 'number':\n if (typeof value !== 'number' || Number.isNaN(value)) {\n report(descriptor.name, `holds ${JSON.stringify(value)}, not a number`);\n }\n return;\n case 'boolean':\n if (typeof value !== 'boolean') {\n report(\n descriptor.name,\n `holds ${JSON.stringify(value)}, not a boolean`,\n );\n }\n return;\n case 'enum':\n if (!descriptor.enumValues?.includes(value as string)) {\n report(\n descriptor.name,\n `holds ${JSON.stringify(value)}, which is not one of ${JSON.stringify(\n descriptor.enumValues ?? [],\n )}`,\n );\n }\n return;\n case 'scalarCollection':\n case 'composition':\n if (!Array.isArray(value)) report(descriptor.name, 'is not a list');\n return;\n default:\n // `string` and `id`, the two that carry text.\n if (typeof value !== 'string' && typeof value !== 'number') {\n report(descriptor.name, `holds a ${typeof value}, not text`);\n }\n }\n}\n\n/**\n * A localized member: an object with non-empty text under every locale. A\n * missing locale fails rather than falls back, so a page never shows one\n * language in the middle of another.\n */\nfunction checkLocalized(\n value: unknown,\n descriptor: EntityFieldDescriptor,\n locales: readonly string[],\n report: Report,\n): void {\n if (!isPlainObject(value)) {\n report(descriptor.name, 'is not a text per locale');\n return;\n }\n for (const locale of locales) {\n const text = value[locale];\n if (text === undefined) {\n report(descriptor.name, `is missing \"${locale}\"`);\n } else if (typeof text !== 'string') {\n report(\n descriptor.name,\n `holds a ${typeof text} for \"${locale}\", not text`,\n );\n } else if (text.trim() === '') {\n report(descriptor.name, `is empty for \"${locale}\"`);\n }\n }\n}\n\n/** `minItems` / `maxItems` on a collection that is present and is a list. */\nfunction checkItemCount(\n value: unknown,\n descriptor: EntityFieldDescriptor,\n report: Report,\n): void {\n if (!Array.isArray(value)) return;\n const { minItems, maxItems } = descriptor;\n if (minItems !== undefined && value.length < minItems) {\n report(\n descriptor.name,\n `holds ${counted(value.length, 'item')}, and needs at least ${minItems}`,\n );\n }\n if (maxItems !== undefined && value.length > maxItems) {\n report(\n descriptor.name,\n `holds ${counted(value.length, 'item')}, and takes at most ${maxItems}`,\n );\n }\n}\n\n/**\n * Checks every record of one entity against its metadata — identity, `required`,\n * each member's type, link ids, item counts, `notBefore`, and the entity's\n * cardinality — then runs the caller's rules.\n */\nexport function validateRecords<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n records: readonly unknown[],\n {\n source,\n linkTargets = {},\n rules = [],\n locales: required = locales(),\n view,\n }: ValidateRecordsOptions = {},\n): ValidateRecordsResult {\n const problems: RecordProblem[] = [];\n const descriptors = describeEntityColumns(entityConstructor);\n const omitted = new Set(\n view === undefined\n ? []\n : extractEntityView(entityConstructor, view).omitted.map(\n descriptor => descriptor.name,\n ),\n );\n const parsed: Record<string, unknown>[] = [];\n const seen = new Set<unknown>();\n\n const at = (where: string, member: string | undefined, message: string) =>\n problems.push({\n path: [source, where, member].filter(Boolean).join(' › '),\n message,\n });\n\n records.forEach((raw, index) => {\n if (!isPlainObject(raw)) {\n at(`#${index}`, undefined, 'is not a record');\n return;\n }\n\n const id = raw['id'];\n const report: Report = (member, message) =>\n at(isAbsent(id) ? `#${index}` : String(id), member, message);\n\n if (isAbsent(id)) {\n report('id', 'is missing');\n } else if (seen.has(id)) {\n report('id', 'is used more than once');\n } else {\n seen.add(id);\n }\n\n const record: Record<string, unknown> = { ...raw };\n\n for (const descriptor of descriptors) {\n if (descriptor.name === 'id') continue;\n const value = raw[descriptor.key];\n\n if (isAbsent(value)) {\n if (descriptor.required && !omitted.has(descriptor.name)) {\n report(descriptor.name, 'is required, and is missing');\n }\n continue;\n }\n\n if (descriptor.type === 'link' || descriptor.type === 'linkCollection') {\n checkLink(value, descriptor, linkTargets[descriptor.name], report);\n } else if (descriptor.type === 'localized') {\n checkLocalized(value, descriptor, required, report);\n } else if (descriptor.type === 'date') {\n const date = parseDate(value, report, descriptor.name);\n if (date !== undefined) record[descriptor.key] = date;\n } else {\n checkScalar(value, descriptor, report);\n }\n\n if (COLLECTION_TYPES.includes(descriptor.type)) {\n checkItemCount(value, descriptor, report);\n }\n }\n\n for (const descriptor of descriptors) {\n if (descriptor.notBefore === undefined) continue;\n const earlier = descriptors.find(\n other => other.name === descriptor.notBefore,\n ) as EntityFieldDescriptor;\n const end = record[descriptor.key];\n const start = record[earlier.key];\n if (start instanceof Date && end instanceof Date && end < start) {\n report(descriptor.name, `is before ${earlier.name}`);\n }\n }\n\n parsed.push(record);\n });\n\n const { cardinality, name } = extractMetaEntity(entityConstructor);\n const where = `${counted(records.length, 'record')}`;\n if (cardinality?.min !== undefined && records.length < cardinality.min) {\n at(name, undefined, `has ${where}, and needs at least ${cardinality.min}`);\n }\n if (cardinality?.max !== undefined && records.length > cardinality.max) {\n at(name, undefined, `has ${where}, and takes at most ${cardinality.max}`);\n }\n\n for (const rule of rules) {\n rule(parsed, (index, member, message) => {\n const id = parsed[index]?.['id'];\n at(isAbsent(id) ? `#${index}` : String(id), member, message);\n });\n }\n\n return { problems, records: parsed };\n}\n\n/** The records with their dates parsed, or a {@link RecordValidationError}. */\nexport function validRecordsOrThrow<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n records: readonly unknown[],\n options?: ValidateRecordsOptions,\n): ReadonlyArray<Record<string, unknown>> {\n const result = validateRecords(entityConstructor, records, options);\n if (result.problems.length > 0) {\n throw new RecordValidationError(result.problems);\n }\n return result.records;\n}\n"],"names":["EntifixBuildError","locales","COLLECTION_TYPES","describeEntityColumns","extractEntityView","extractMetaEntity","RecordValidationError","problems","map","problem","path","message","join","undefined","count","length","isPlainObject","value","Array","isArray","isAbsent","counted","n","noun","parseDate","report","member","Date","Number","isNaN","getTime","parsed","JSON","stringify","checkLink","descriptor","known","isCollection","type","name","id","has","checkScalar","enumValues","includes","checkLocalized","locale","text","trim","checkItemCount","minItems","maxItems","validateRecords","entityConstructor","records","source","linkTargets","rules","required","view","descriptors","omitted","Set","seen","at","where","push","filter","Boolean","forEach","raw","index","String","add","record","key","date","notBefore","earlier","find","other","end","start","cardinality","min","max","rule","validRecordsOrThrow","options","result"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/entity-definition/validate/validate-records.ts"],"sourcesContent":["import { Schema } from 'effect';\n\nimport { defineFailure } from '../../failure/define.js';\nimport { FieldProblem } from '../../failure/generic.js';\nimport { locales } from '../../i18n/locales.js';\nimport type {\n Entity,\n EntityConstructor,\n EntityId,\n} from '../../types/Entity.js';\nimport {\n COLLECTION_TYPES,\n describeEntityColumns,\n type EntityFieldDescriptor,\n extractEntityView,\n} from '../describe/index.js';\nimport { extractMetaEntity } from '../helpers/index.js';\n\n/**\n * Records checked against their entity's metadata before anything serves them.\n *\n * The mapping does not do this, on purpose: `deserializeSingleEntity` assigns\n * every non-relation value raw — it converts no date, enforces no `required`,\n * validates no enum value and resolves no link id — because a record off a\n * database was written through that same mapping and needs none of it. Records\n * that arrive as **untyped JSON** — files bundled with a static build, an\n * import, a seed — have had no such guarantee, and a wrong one reaches a reader\n * as `undefined` in the middle of a page, or as a string where a `Date` was\n * expected. This is the check those records go through.\n *\n * It sits in core, beside the deserializer it makes up for, so every adapter\n * that reads untyped records shares it — and the generated form judges a\n * draft by the same metadata, so a record a form refuses is one this refuses.\n *\n * Every problem is collected rather than the first thrown, each with the path\n * to its value: `employment.json › acme › role is required, and is missing`.\n */\n\nexport interface RecordProblem {\n /** `<source> › <id> › <member>`, as far as each is known. */\n readonly path: string;\n readonly message: string;\n}\n\n/**\n * Thrown by {@link validRecordsOrThrow}: an `invalid` failure carrying every\n * problem found, each as a field problem whose `pointer` is the problem's path\n * ([ADR 0013](../../../../../../docs/adr/0013-failures-are-typed-values-decided-at-the-shell.md)).\n *\n * Its `message` lists them, because the usual place it surfaces is a build that\n * read a bundled file — where the person reading is looking at a terminal, not\n * a problem body.\n */\nexport class InvalidRecords extends defineFailure('invalid')<InvalidRecords>()(\n 'InvalidRecords',\n 'invalidRecords',\n { errors: Schema.Array(FieldProblem) },\n {\n message: ({ errors }) =>\n `Records are invalid:\\n${errors\n .map(problem => ` ${problem.pointer} ${problem.detail}`)\n .join('\\n')}`,\n },\n) {}\n\n/** The field problems {@link InvalidRecords} carries for a set of record problems. */\nexport const recordFieldProblems = (\n problems: readonly RecordProblem[],\n): FieldProblem[] =>\n problems.map(problem => ({\n pointer: problem.path,\n detail: problem.message,\n code: 'invalidRecord',\n }));\n\n/**\n * A check one entity needs that metadata cannot express — at most one contact\n * channel per type, say. Handed every record (dates already parsed) and a way\n * to report against one of them by index.\n */\nexport type EntityRecordRule = (\n records: ReadonlyArray<Readonly<Record<string, unknown>>>,\n report: (index: number, member: string | undefined, message: string) => void,\n) => void;\n\nexport interface ValidateRecordsOptions {\n /** Where the records came from, for the path in a message. */\n readonly source?: string;\n /**\n * The ids that exist for each `link` or `linkCollection` member, by member\n * name. A member left out is checked for shape only.\n */\n readonly linkTargets?: Readonly<Record<string, ReadonlySet<EntityId>>>;\n /** Checks of the caller's own, run after the metadata's. */\n readonly rules?: readonly EntityRecordRule[];\n /**\n * Every locale a `localized` member must carry. Defaults to the locales the\n * host declared through `defineLocales`.\n */\n readonly locales?: readonly string[];\n /**\n * The view the records were published in. A member it omits is not\n * required of them, since the view left it out on purpose.\n */\n readonly view?: string;\n}\n\nexport interface ValidateRecordsResult {\n readonly problems: readonly RecordProblem[];\n /**\n * The records with every `date` member parsed into a `Date`, ready for the\n * mapping. Meaningless when `problems` is not empty.\n */\n readonly records: ReadonlyArray<Record<string, unknown>>;\n}\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n value !== null && typeof value === 'object' && !Array.isArray(value);\n\nconst isAbsent = (value: unknown): boolean =>\n value === undefined || value === null || value === '';\n\ntype Report = (member: string | undefined, message: string) => void;\n\nconst counted = (n: number, noun: string) =>\n `${n} ${noun}${n === 1 ? '' : 's'}`;\n\n/** A date member, parsed: the mapping would leave it a string. */\nfunction parseDate(\n value: unknown,\n report: Report,\n member: string,\n): Date | undefined {\n if (value instanceof Date) {\n if (Number.isNaN(value.getTime())) report(member, 'is an invalid date');\n return Number.isNaN(value.getTime()) ? undefined : value;\n }\n if (typeof value !== 'string' && typeof value !== 'number') {\n report(member, `holds a ${typeof value}, not a date`);\n return undefined;\n }\n const parsed = new Date(value);\n if (Number.isNaN(parsed.getTime())) {\n report(member, `is not a date: ${JSON.stringify(value)}`);\n return undefined;\n }\n return parsed;\n}\n\n/** A relation: ids, or embedded records, and ids that exist. */\nfunction checkLink(\n value: unknown,\n descriptor: EntityFieldDescriptor,\n known: ReadonlySet<EntityId> | undefined,\n report: Report,\n): void {\n const isCollection = descriptor.type === 'linkCollection';\n if (isCollection && !Array.isArray(value)) {\n report(descriptor.name, 'is not a list');\n return;\n }\n for (const id of isCollection ? (value as unknown[]) : [value]) {\n if (isPlainObject(id)) continue; // An embedded record carries its own.\n if (typeof id !== 'string' && typeof id !== 'number') {\n report(descriptor.name, `holds a ${typeof id} where an id was expected`);\n } else if (known !== undefined && !known.has(id)) {\n report(\n descriptor.name,\n `points at ${JSON.stringify(id)}, which does not exist`,\n );\n }\n }\n}\n\n/** A scalar: the type the metadata declares. */\nfunction checkScalar(\n value: unknown,\n descriptor: EntityFieldDescriptor,\n report: Report,\n): void {\n switch (descriptor.type) {\n case 'number':\n if (typeof value !== 'number' || Number.isNaN(value)) {\n report(descriptor.name, `holds ${JSON.stringify(value)}, not a number`);\n }\n return;\n case 'boolean':\n if (typeof value !== 'boolean') {\n report(\n descriptor.name,\n `holds ${JSON.stringify(value)}, not a boolean`,\n );\n }\n return;\n case 'enum':\n if (!descriptor.enumValues?.includes(value as string)) {\n report(\n descriptor.name,\n `holds ${JSON.stringify(value)}, which is not one of ${JSON.stringify(\n descriptor.enumValues ?? [],\n )}`,\n );\n }\n return;\n case 'scalarCollection':\n case 'composition':\n if (!Array.isArray(value)) report(descriptor.name, 'is not a list');\n return;\n default:\n // `string` and `id`, the two that carry text.\n if (typeof value !== 'string' && typeof value !== 'number') {\n report(descriptor.name, `holds a ${typeof value}, not text`);\n }\n }\n}\n\n/**\n * A localized member: an object with non-empty text under every locale. A\n * missing locale fails rather than falls back, so a page never shows one\n * language in the middle of another.\n */\nfunction checkLocalized(\n value: unknown,\n descriptor: EntityFieldDescriptor,\n locales: readonly string[],\n report: Report,\n): void {\n if (!isPlainObject(value)) {\n report(descriptor.name, 'is not a text per locale');\n return;\n }\n for (const locale of locales) {\n const text = value[locale];\n if (text === undefined) {\n report(descriptor.name, `is missing \"${locale}\"`);\n } else if (typeof text !== 'string') {\n report(\n descriptor.name,\n `holds a ${typeof text} for \"${locale}\", not text`,\n );\n } else if (text.trim() === '') {\n report(descriptor.name, `is empty for \"${locale}\"`);\n }\n }\n}\n\n/** `minItems` / `maxItems` on a collection that is present and is a list. */\nfunction checkItemCount(\n value: unknown,\n descriptor: EntityFieldDescriptor,\n report: Report,\n): void {\n if (!Array.isArray(value)) return;\n const { minItems, maxItems } = descriptor;\n if (minItems !== undefined && value.length < minItems) {\n report(\n descriptor.name,\n `holds ${counted(value.length, 'item')}, and needs at least ${minItems}`,\n );\n }\n if (maxItems !== undefined && value.length > maxItems) {\n report(\n descriptor.name,\n `holds ${counted(value.length, 'item')}, and takes at most ${maxItems}`,\n );\n }\n}\n\n/**\n * Checks every record of one entity against its metadata — identity, `required`,\n * each member's type, link ids, item counts, `notBefore`, and the entity's\n * cardinality — then runs the caller's rules.\n */\nexport function validateRecords<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n records: readonly unknown[],\n {\n source,\n linkTargets = {},\n rules = [],\n locales: required = locales(),\n view,\n }: ValidateRecordsOptions = {},\n): ValidateRecordsResult {\n const problems: RecordProblem[] = [];\n const descriptors = describeEntityColumns(entityConstructor);\n const omitted = new Set(\n view === undefined\n ? []\n : extractEntityView(entityConstructor, view).omitted.map(\n descriptor => descriptor.name,\n ),\n );\n const parsed: Record<string, unknown>[] = [];\n const seen = new Set<unknown>();\n\n const at = (where: string, member: string | undefined, message: string) =>\n problems.push({\n path: [source, where, member].filter(Boolean).join(' › '),\n message,\n });\n\n records.forEach((raw, index) => {\n if (!isPlainObject(raw)) {\n at(`#${index}`, undefined, 'is not a record');\n return;\n }\n\n const id = raw['id'];\n const report: Report = (member, message) =>\n at(isAbsent(id) ? `#${index}` : String(id), member, message);\n\n if (isAbsent(id)) {\n report('id', 'is missing');\n } else if (seen.has(id)) {\n report('id', 'is used more than once');\n } else {\n seen.add(id);\n }\n\n const record: Record<string, unknown> = { ...raw };\n\n for (const descriptor of descriptors) {\n if (descriptor.name === 'id') continue;\n const value = raw[descriptor.key];\n\n if (isAbsent(value)) {\n if (descriptor.required && !omitted.has(descriptor.name)) {\n report(descriptor.name, 'is required, and is missing');\n }\n continue;\n }\n\n if (descriptor.type === 'link' || descriptor.type === 'linkCollection') {\n checkLink(value, descriptor, linkTargets[descriptor.name], report);\n } else if (descriptor.type === 'localized') {\n checkLocalized(value, descriptor, required, report);\n } else if (descriptor.type === 'date') {\n const date = parseDate(value, report, descriptor.name);\n if (date !== undefined) record[descriptor.key] = date;\n } else {\n checkScalar(value, descriptor, report);\n }\n\n if (COLLECTION_TYPES.includes(descriptor.type)) {\n checkItemCount(value, descriptor, report);\n }\n }\n\n for (const descriptor of descriptors) {\n if (descriptor.notBefore === undefined) continue;\n const earlier = descriptors.find(\n other => other.name === descriptor.notBefore,\n ) as EntityFieldDescriptor;\n const end = record[descriptor.key];\n const start = record[earlier.key];\n if (start instanceof Date && end instanceof Date && end < start) {\n report(descriptor.name, `is before ${earlier.name}`);\n }\n }\n\n parsed.push(record);\n });\n\n const { cardinality, name } = extractMetaEntity(entityConstructor);\n const where = `${counted(records.length, 'record')}`;\n if (cardinality?.min !== undefined && records.length < cardinality.min) {\n at(name, undefined, `has ${where}, and needs at least ${cardinality.min}`);\n }\n if (cardinality?.max !== undefined && records.length > cardinality.max) {\n at(name, undefined, `has ${where}, and takes at most ${cardinality.max}`);\n }\n\n for (const rule of rules) {\n rule(parsed, (index, member, message) => {\n const id = parsed[index]?.['id'];\n at(isAbsent(id) ? `#${index}` : String(id), member, message);\n });\n }\n\n return { problems, records: parsed };\n}\n\n/** The records with their dates parsed, or an {@link InvalidRecords}. */\nexport function validRecordsOrThrow<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n records: readonly unknown[],\n options?: ValidateRecordsOptions,\n): ReadonlyArray<Record<string, unknown>> {\n const result = validateRecords(entityConstructor, records, options);\n if (result.problems.length > 0) {\n throw new InvalidRecords({ errors: recordFieldProblems(result.problems) });\n }\n return result.records;\n}\n"],"names":["Schema","defineFailure","FieldProblem","locales","COLLECTION_TYPES","describeEntityColumns","extractEntityView","extractMetaEntity","InvalidRecords","errors","Array","message","map","problem","pointer","detail","join","recordFieldProblems","problems","path","code","isPlainObject","value","isArray","isAbsent","undefined","counted","n","noun","parseDate","report","member","Date","Number","isNaN","getTime","parsed","JSON","stringify","checkLink","descriptor","known","isCollection","type","name","id","has","checkScalar","enumValues","includes","checkLocalized","locale","text","trim","checkItemCount","minItems","maxItems","length","validateRecords","entityConstructor","records","source","linkTargets","rules","required","view","descriptors","omitted","Set","seen","at","where","push","filter","Boolean","forEach","raw","index","String","add","record","key","date","notBefore","earlier","find","other","end","start","cardinality","min","max","rule","validRecordsOrThrow","options","result"],"mappings":";AAAA,SAASA,MAAM,QAAQ,SAAS;AAEhC,SAASC,aAAa,QAAQ,0BAA0B;AACxD,SAASC,YAAY,QAAQ,2BAA2B;AACxD,SAASC,OAAO,QAAQ,wBAAwB;AAMhD,SACEC,gBAAgB,EAChBC,qBAAqB,EAErBC,iBAAiB,QACZ,uBAAuB;AAC9B,SAASC,iBAAiB,QAAQ,sBAAsB;AA4BxD;;;;;;;;CAQC,GACD,OAAO,MAAMC,uBAAuBP,cAAc,aAChD,kBACA,kBACA;IAAEQ,QAAQT,OAAOU,KAAK,CAACR;AAAc,GACrC;IACES,SAAS,CAAC,EAAEF,MAAM,EAAE,GAClB,CAAC,sBAAsB,EAAEA,OACtBG,GAAG,CAACC,CAAAA,UAAW,CAAC,EAAE,EAAEA,QAAQC,OAAO,CAAC,CAAC,EAAED,QAAQE,MAAM,EAAE,EACvDC,IAAI,CAAC,OAAO;AACnB;AACC;AAEH,oFAAoF,GACpF,OAAO,MAAMC,sBAAsB,CACjCC,WAEAA,SAASN,GAAG,CAACC,CAAAA,UAAY,CAAA;YACvBC,SAASD,QAAQM,IAAI;YACrBJ,QAAQF,QAAQF,OAAO;YACvBS,MAAM;QACR,CAAA,GAAI;AA2CN,MAAMC,gBAAgB,CAACC,QACrBA,UAAU,QAAQ,OAAOA,UAAU,YAAY,CAACZ,MAAMa,OAAO,CAACD;AAEhE,MAAME,WAAW,CAACF,QAChBA,UAAUG,aAAaH,UAAU,QAAQA,UAAU;AAIrD,MAAMI,UAAU,CAACC,GAAWC,OAC1B,GAAGD,EAAE,CAAC,EAAEC,OAAOD,MAAM,IAAI,KAAK,KAAK;AAErC,gEAAgE,GAChE,SAASE,UACPP,KAAc,EACdQ,MAAc,EACdC,MAAc;IAEd,IAAIT,iBAAiBU,MAAM;QACzB,IAAIC,OAAOC,KAAK,CAACZ,MAAMa,OAAO,KAAKL,OAAOC,QAAQ;QAClD,OAAOE,OAAOC,KAAK,CAACZ,MAAMa,OAAO,MAAMV,YAAYH;IACrD;IACA,IAAI,OAAOA,UAAU,YAAY,OAAOA,UAAU,UAAU;QAC1DQ,OAAOC,QAAQ,CAAC,QAAQ,EAAE,OAAOT,MAAM,YAAY,CAAC;QACpD,OAAOG;IACT;IACA,MAAMW,SAAS,IAAIJ,KAAKV;IACxB,IAAIW,OAAOC,KAAK,CAACE,OAAOD,OAAO,KAAK;QAClCL,OAAOC,QAAQ,CAAC,eAAe,EAAEM,KAAKC,SAAS,CAAChB,QAAQ;QACxD,OAAOG;IACT;IACA,OAAOW;AACT;AAEA,8DAA8D,GAC9D,SAASG,UACPjB,KAAc,EACdkB,UAAiC,EACjCC,KAAwC,EACxCX,MAAc;IAEd,MAAMY,eAAeF,WAAWG,IAAI,KAAK;IACzC,IAAID,gBAAgB,CAAChC,MAAMa,OAAO,CAACD,QAAQ;QACzCQ,OAAOU,WAAWI,IAAI,EAAE;QACxB;IACF;IACA,KAAK,MAAMC,MAAMH,eAAgBpB,QAAsB;QAACA;KAAM,CAAE;QAC9D,IAAID,cAAcwB,KAAK,UAAU,sCAAsC;QACvE,IAAI,OAAOA,OAAO,YAAY,OAAOA,OAAO,UAAU;YACpDf,OAAOU,WAAWI,IAAI,EAAE,CAAC,QAAQ,EAAE,OAAOC,GAAG,yBAAyB,CAAC;QACzE,OAAO,IAAIJ,UAAUhB,aAAa,CAACgB,MAAMK,GAAG,CAACD,KAAK;YAChDf,OACEU,WAAWI,IAAI,EACf,CAAC,UAAU,EAAEP,KAAKC,SAAS,CAACO,IAAI,sBAAsB,CAAC;QAE3D;IACF;AACF;AAEA,8CAA8C,GAC9C,SAASE,YACPzB,KAAc,EACdkB,UAAiC,EACjCV,MAAc;IAEd,OAAQU,WAAWG,IAAI;QACrB,KAAK;YACH,IAAI,OAAOrB,UAAU,YAAYW,OAAOC,KAAK,CAACZ,QAAQ;gBACpDQ,OAAOU,WAAWI,IAAI,EAAE,CAAC,MAAM,EAAEP,KAAKC,SAAS,CAAChB,OAAO,cAAc,CAAC;YACxE;YACA;QACF,KAAK;YACH,IAAI,OAAOA,UAAU,WAAW;gBAC9BQ,OACEU,WAAWI,IAAI,EACf,CAAC,MAAM,EAAEP,KAAKC,SAAS,CAAChB,OAAO,eAAe,CAAC;YAEnD;YACA;QACF,KAAK;gBACEkB;YAAL,IAAI,GAACA,yBAAAA,WAAWQ,UAAU,qBAArBR,uBAAuBS,QAAQ,CAAC3B,SAAkB;oBAIjDkB;gBAHJV,OACEU,WAAWI,IAAI,EACf,CAAC,MAAM,EAAEP,KAAKC,SAAS,CAAChB,OAAO,sBAAsB,EAAEe,KAAKC,SAAS,EACnEE,0BAAAA,WAAWQ,UAAU,YAArBR,0BAAyB,EAAE,GAC1B;YAEP;YACA;QACF,KAAK;QACL,KAAK;YACH,IAAI,CAAC9B,MAAMa,OAAO,CAACD,QAAQQ,OAAOU,WAAWI,IAAI,EAAE;YACnD;QACF;YACE,8CAA8C;YAC9C,IAAI,OAAOtB,UAAU,YAAY,OAAOA,UAAU,UAAU;gBAC1DQ,OAAOU,WAAWI,IAAI,EAAE,CAAC,QAAQ,EAAE,OAAOtB,MAAM,UAAU,CAAC;YAC7D;IACJ;AACF;AAEA;;;;CAIC,GACD,SAAS4B,eACP5B,KAAc,EACdkB,UAAiC,EACjCrC,OAA0B,EAC1B2B,MAAc;IAEd,IAAI,CAACT,cAAcC,QAAQ;QACzBQ,OAAOU,WAAWI,IAAI,EAAE;QACxB;IACF;IACA,KAAK,MAAMO,UAAUhD,QAAS;QAC5B,MAAMiD,OAAO9B,KAAK,CAAC6B,OAAO;QAC1B,IAAIC,SAAS3B,WAAW;YACtBK,OAAOU,WAAWI,IAAI,EAAE,CAAC,YAAY,EAAEO,OAAO,CAAC,CAAC;QAClD,OAAO,IAAI,OAAOC,SAAS,UAAU;YACnCtB,OACEU,WAAWI,IAAI,EACf,CAAC,QAAQ,EAAE,OAAOQ,KAAK,MAAM,EAAED,OAAO,WAAW,CAAC;QAEtD,OAAO,IAAIC,KAAKC,IAAI,OAAO,IAAI;YAC7BvB,OAAOU,WAAWI,IAAI,EAAE,CAAC,cAAc,EAAEO,OAAO,CAAC,CAAC;QACpD;IACF;AACF;AAEA,2EAA2E,GAC3E,SAASG,eACPhC,KAAc,EACdkB,UAAiC,EACjCV,MAAc;IAEd,IAAI,CAACpB,MAAMa,OAAO,CAACD,QAAQ;IAC3B,MAAM,EAAEiC,QAAQ,EAAEC,QAAQ,EAAE,GAAGhB;IAC/B,IAAIe,aAAa9B,aAAaH,MAAMmC,MAAM,GAAGF,UAAU;QACrDzB,OACEU,WAAWI,IAAI,EACf,CAAC,MAAM,EAAElB,QAAQJ,MAAMmC,MAAM,EAAE,QAAQ,qBAAqB,EAAEF,UAAU;IAE5E;IACA,IAAIC,aAAa/B,aAAaH,MAAMmC,MAAM,GAAGD,UAAU;QACrD1B,OACEU,WAAWI,IAAI,EACf,CAAC,MAAM,EAAElB,QAAQJ,MAAMmC,MAAM,EAAE,QAAQ,oBAAoB,EAAED,UAAU;IAE3E;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASE,gBACdC,iBAA6C,EAC7CC,OAA2B,EAC3B,EACEC,MAAM,EACNC,cAAc,CAAC,CAAC,EAChBC,QAAQ,EAAE,EACV5D,SAAS6D,WAAW7D,SAAS,EAC7B8D,IAAI,EACmB,GAAG,CAAC,CAAC;IAE9B,MAAM/C,WAA4B,EAAE;IACpC,MAAMgD,cAAc7D,sBAAsBsD;IAC1C,MAAMQ,UAAU,IAAIC,IAClBH,SAASxC,YACL,EAAE,GACFnB,kBAAkBqD,mBAAmBM,MAAME,OAAO,CAACvD,GAAG,CACpD4B,CAAAA,aAAcA,WAAWI,IAAI;IAGrC,MAAMR,SAAoC,EAAE;IAC5C,MAAMiC,OAAO,IAAID;IAEjB,MAAME,KAAK,CAACC,OAAexC,QAA4BpB,UACrDO,SAASsD,IAAI,CAAC;YACZrD,MAAM;gBAAC0C;gBAAQU;gBAAOxC;aAAO,CAAC0C,MAAM,CAACC,SAAS1D,IAAI,CAAC;YACnDL;QACF;IAEFiD,QAAQe,OAAO,CAAC,CAACC,KAAKC;QACpB,IAAI,CAACxD,cAAcuD,MAAM;YACvBN,GAAG,CAAC,CAAC,EAAEO,OAAO,EAAEpD,WAAW;YAC3B;QACF;QAEA,MAAMoB,KAAK+B,GAAG,CAAC,KAAK;QACpB,MAAM9C,SAAiB,CAACC,QAAQpB,UAC9B2D,GAAG9C,SAASqB,MAAM,CAAC,CAAC,EAAEgC,OAAO,GAAGC,OAAOjC,KAAKd,QAAQpB;QAEtD,IAAIa,SAASqB,KAAK;YAChBf,OAAO,MAAM;QACf,OAAO,IAAIuC,KAAKvB,GAAG,CAACD,KAAK;YACvBf,OAAO,MAAM;QACf,OAAO;YACLuC,KAAKU,GAAG,CAAClC;QACX;QAEA,MAAMmC,SAAkC,aAAKJ;QAE7C,KAAK,MAAMpC,cAAc0B,YAAa;YACpC,IAAI1B,WAAWI,IAAI,KAAK,MAAM;YAC9B,MAAMtB,QAAQsD,GAAG,CAACpC,WAAWyC,GAAG,CAAC;YAEjC,IAAIzD,SAASF,QAAQ;gBACnB,IAAIkB,WAAWwB,QAAQ,IAAI,CAACG,QAAQrB,GAAG,CAACN,WAAWI,IAAI,GAAG;oBACxDd,OAAOU,WAAWI,IAAI,EAAE;gBAC1B;gBACA;YACF;YAEA,IAAIJ,WAAWG,IAAI,KAAK,UAAUH,WAAWG,IAAI,KAAK,kBAAkB;gBACtEJ,UAAUjB,OAAOkB,YAAYsB,WAAW,CAACtB,WAAWI,IAAI,CAAC,EAAEd;YAC7D,OAAO,IAAIU,WAAWG,IAAI,KAAK,aAAa;gBAC1CO,eAAe5B,OAAOkB,YAAYwB,UAAUlC;YAC9C,OAAO,IAAIU,WAAWG,IAAI,KAAK,QAAQ;gBACrC,MAAMuC,OAAOrD,UAAUP,OAAOQ,QAAQU,WAAWI,IAAI;gBACrD,IAAIsC,SAASzD,WAAWuD,MAAM,CAACxC,WAAWyC,GAAG,CAAC,GAAGC;YACnD,OAAO;gBACLnC,YAAYzB,OAAOkB,YAAYV;YACjC;YAEA,IAAI1B,iBAAiB6C,QAAQ,CAACT,WAAWG,IAAI,GAAG;gBAC9CW,eAAehC,OAAOkB,YAAYV;YACpC;QACF;QAEA,KAAK,MAAMU,cAAc0B,YAAa;YACpC,IAAI1B,WAAW2C,SAAS,KAAK1D,WAAW;YACxC,MAAM2D,UAAUlB,YAAYmB,IAAI,CAC9BC,CAAAA,QAASA,MAAM1C,IAAI,KAAKJ,WAAW2C,SAAS;YAE9C,MAAMI,MAAMP,MAAM,CAACxC,WAAWyC,GAAG,CAAC;YAClC,MAAMO,QAAQR,MAAM,CAACI,QAAQH,GAAG,CAAC;YACjC,IAAIO,iBAAiBxD,QAAQuD,eAAevD,QAAQuD,MAAMC,OAAO;gBAC/D1D,OAAOU,WAAWI,IAAI,EAAE,CAAC,UAAU,EAAEwC,QAAQxC,IAAI,EAAE;YACrD;QACF;QAEAR,OAAOoC,IAAI,CAACQ;IACd;IAEA,MAAM,EAAES,WAAW,EAAE7C,IAAI,EAAE,GAAGrC,kBAAkBoD;IAChD,MAAMY,QAAQ,GAAG7C,QAAQkC,QAAQH,MAAM,EAAE,WAAW;IACpD,IAAIgC,CAAAA,+BAAAA,YAAaC,GAAG,MAAKjE,aAAamC,QAAQH,MAAM,GAAGgC,YAAYC,GAAG,EAAE;QACtEpB,GAAG1B,MAAMnB,WAAW,CAAC,IAAI,EAAE8C,MAAM,qBAAqB,EAAEkB,YAAYC,GAAG,EAAE;IAC3E;IACA,IAAID,CAAAA,+BAAAA,YAAaE,GAAG,MAAKlE,aAAamC,QAAQH,MAAM,GAAGgC,YAAYE,GAAG,EAAE;QACtErB,GAAG1B,MAAMnB,WAAW,CAAC,IAAI,EAAE8C,MAAM,oBAAoB,EAAEkB,YAAYE,GAAG,EAAE;IAC1E;IAEA,KAAK,MAAMC,QAAQ7B,MAAO;QACxB6B,KAAKxD,QAAQ,CAACyC,OAAO9C,QAAQpB;gBAChByB;YAAX,MAAMS,MAAKT,gBAAAA,MAAM,CAACyC,MAAM,qBAAbzC,aAAe,CAAC,KAAK;YAChCkC,GAAG9C,SAASqB,MAAM,CAAC,CAAC,EAAEgC,OAAO,GAAGC,OAAOjC,KAAKd,QAAQpB;QACtD;IACF;IAEA,OAAO;QAAEO;QAAU0C,SAASxB;IAAO;AACrC;AAEA,uEAAuE,GACvE,OAAO,SAASyD,oBACdlC,iBAA6C,EAC7CC,OAA2B,EAC3BkC,OAAgC;IAEhC,MAAMC,SAASrC,gBAAgBC,mBAAmBC,SAASkC;IAC3D,IAAIC,OAAO7E,QAAQ,CAACuC,MAAM,GAAG,GAAG;QAC9B,MAAM,IAAIjD,eAAe;YAAEC,QAAQQ,oBAAoB8E,OAAO7E,QAAQ;QAAE;IAC1E;IACA,OAAO6E,OAAOnC,OAAO;AACvB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
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
5
|
import type { EntityFacetValue } from '../types/EntityFacets.js';
|
|
6
6
|
import type { DomainEvent, EntifixEnvelope, EntifixEnvelopeType } from './types.js';
|
|
@@ -14,14 +14,14 @@ export declare function isEntifixEnvelope(body: unknown): body is EntifixEnvelop
|
|
|
14
14
|
* requiring an entity constructor — for `command`/`transactionRecord` messages
|
|
15
15
|
* whose `data` is not a serialized entity. `label` only sharpens error text.
|
|
16
16
|
*/
|
|
17
|
-
export declare function readEnvelope<TData>(body: unknown, expected: EntifixEnvelopeType, label?: string): Effect.Effect<EntifixEnvelope<TData>,
|
|
18
|
-
export declare const readEntityEnvelope: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, body: unknown) => Effect.Effect<TEntity, EntifixBuildError, never>;
|
|
19
|
-
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>;
|
|
20
20
|
export declare const readEntityPageEnvelope: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, body: unknown) => Effect.Effect<{
|
|
21
21
|
items: TEntity[];
|
|
22
22
|
total: number;
|
|
23
23
|
request: import("../index.js").EntityLoadRequest<TEntity>;
|
|
24
|
-
}, EntifixBuildError, never>;
|
|
24
|
+
}, import("../index.js").EntifixBuildError | InvalidEnvelope, never>;
|
|
25
25
|
/**
|
|
26
26
|
* Reads a `$metadata` response back into its document.
|
|
27
27
|
*
|
|
@@ -30,13 +30,13 @@ export declare const readEntityPageEnvelope: <TEntity extends Entity>(entityCons
|
|
|
30
30
|
* actions" meant "denied" or "malformed", so a broken payload fails loudly here
|
|
31
31
|
* the way a half-populated entity does.
|
|
32
32
|
*/
|
|
33
|
-
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
34
|
/**
|
|
35
35
|
* Reads a `$facets` response back into its counts, refusing a body whose lists
|
|
36
36
|
* are not `{ value, count }` rows — a panel that rendered a malformed count
|
|
37
37
|
* would show a number nobody computed.
|
|
38
38
|
*/
|
|
39
|
-
export declare const readEntityFacetsEnvelope: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, body: unknown) => Effect.Effect<Readonly<Record<string, readonly EntityFacetValue[]>>,
|
|
39
|
+
export declare const readEntityFacetsEnvelope: <TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, body: unknown) => Effect.Effect<Readonly<Record<string, readonly EntityFacetValue[]>>, InvalidEnvelope, never>;
|
|
40
40
|
/**
|
|
41
41
|
* Reads a bus message back into a {@link DomainEvent}.
|
|
42
42
|
*
|
|
@@ -51,5 +51,5 @@ export declare const readEntityFacetsEnvelope: <TEntity extends Entity>(entityCo
|
|
|
51
51
|
* `catalog.published` payload should look like, and inventing an opinion here
|
|
52
52
|
* is how the bus would start knowing about domains.
|
|
53
53
|
*/
|
|
54
|
-
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>;
|
|
55
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"}
|