@entifix/core 0.2.0 → 0.3.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/dist/entity-definition/describe/describe-child-columns.d.ts +3 -0
- package/dist/entity-definition/describe/describe-child-columns.d.ts.map +1 -1
- package/dist/entity-definition/describe/describe-child-columns.js +11 -0
- package/dist/entity-definition/describe/describe-child-columns.js.map +1 -1
- package/dist/entity-definition/describe/describe-entity-columns.d.ts +7 -1
- package/dist/entity-definition/describe/describe-entity-columns.d.ts.map +1 -1
- package/dist/entity-definition/describe/describe-entity-columns.js +41 -4
- package/dist/entity-definition/describe/describe-entity-columns.js.map +1 -1
- package/dist/entity-definition/describe/entity-view.d.ts +19 -0
- package/dist/entity-definition/describe/entity-view.d.ts.map +1 -0
- package/dist/entity-definition/describe/entity-view.js +35 -0
- package/dist/entity-definition/describe/entity-view.js.map +1 -0
- package/dist/entity-definition/describe/index.d.ts +2 -0
- package/dist/entity-definition/describe/index.d.ts.map +1 -1
- package/dist/entity-definition/describe/index.js +2 -0
- package/dist/entity-definition/describe/index.js.map +1 -1
- package/dist/entity-definition/describe/query-property.d.ts +17 -0
- package/dist/entity-definition/describe/query-property.d.ts.map +1 -0
- package/dist/entity-definition/describe/query-property.js +18 -0
- package/dist/entity-definition/describe/query-property.js.map +1 -0
- package/dist/entity-definition/meta-entities/bounds.d.ts +10 -0
- package/dist/entity-definition/meta-entities/bounds.d.ts.map +1 -0
- package/dist/entity-definition/meta-entities/bounds.js +17 -0
- package/dist/entity-definition/meta-entities/bounds.js.map +1 -0
- package/dist/entity-definition/meta-entities/meta-accessor/meta-accessor.d.ts +24 -1
- package/dist/entity-definition/meta-entities/meta-accessor/meta-accessor.d.ts.map +1 -1
- package/dist/entity-definition/meta-entities/meta-accessor/meta-accessor.js +8 -1
- package/dist/entity-definition/meta-entities/meta-accessor/meta-accessor.js.map +1 -1
- package/dist/entity-definition/meta-entities/meta-entity/meta-entity.d.ts +45 -0
- package/dist/entity-definition/meta-entities/meta-entity/meta-entity.d.ts.map +1 -1
- package/dist/entity-definition/meta-entities/meta-entity/meta-entity.js +4 -0
- package/dist/entity-definition/meta-entities/meta-entity/meta-entity.js.map +1 -1
- package/dist/entity-definition/reconstruct/coerce-field-value.d.ts +7 -0
- package/dist/entity-definition/reconstruct/coerce-field-value.d.ts.map +1 -1
- package/dist/entity-definition/reconstruct/coerce-field-value.js +9 -0
- package/dist/entity-definition/reconstruct/coerce-field-value.js.map +1 -1
- package/dist/entity-definition/reconstruct/reconstruct-entity.d.ts.map +1 -1
- package/dist/entity-definition/reconstruct/reconstruct-entity.js +8 -2
- package/dist/entity-definition/reconstruct/reconstruct-entity.js.map +1 -1
- package/dist/entity-definition/serializer/serialize.d.ts +13 -1
- package/dist/entity-definition/serializer/serialize.d.ts.map +1 -1
- package/dist/entity-definition/serializer/serialize.js +14 -2
- package/dist/entity-definition/serializer/serialize.js.map +1 -1
- package/dist/entity-definition/validate/index.d.ts +2 -0
- package/dist/entity-definition/validate/index.d.ts.map +1 -0
- package/dist/entity-definition/validate/index.js +3 -0
- package/dist/entity-definition/validate/index.js.map +1 -0
- package/dist/entity-definition/validate/validate-records.d.ts +75 -0
- package/dist/entity-definition/validate/validate-records.d.ts.map +1 -0
- package/dist/entity-definition/validate/validate-records.js +205 -0
- package/dist/entity-definition/validate/validate-records.js.map +1 -0
- package/dist/i18n/locales.d.ts +46 -7
- package/dist/i18n/locales.d.ts.map +1 -1
- package/dist/i18n/locales.js +49 -10
- package/dist/i18n/locales.js.map +1 -1
- package/dist/i18n/localized-text.d.ts +27 -0
- package/dist/i18n/localized-text.d.ts.map +1 -0
- package/dist/i18n/localized-text.js +18 -0
- package/dist/i18n/localized-text.js.map +1 -0
- package/dist/i18n/negotiate.d.ts +1 -1
- package/dist/i18n/negotiate.d.ts.map +1 -1
- package/dist/i18n/negotiate.js +3 -3
- package/dist/i18n/negotiate.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/query/in-memory-query.d.ts +49 -0
- package/dist/query/in-memory-query.d.ts.map +1 -0
- package/dist/query/in-memory-query.js +133 -0
- package/dist/query/in-memory-query.js.map +1 -0
- package/dist/rsql/coerce-rsql.d.ts +21 -0
- package/dist/rsql/coerce-rsql.d.ts.map +1 -1
- package/dist/rsql/coerce-rsql.js +44 -8
- package/dist/rsql/coerce-rsql.js.map +1 -1
- package/dist/rsql/sort-param.d.ts.map +1 -1
- package/dist/rsql/sort-param.js +3 -9
- package/dist/rsql/sort-param.js.map +1 -1
- package/dist/types/EntityDraft.d.ts +18 -2
- package/dist/types/EntityDraft.d.ts.map +1 -1
- package/dist/types/EntityDraft.js +11 -0
- package/dist/types/EntityDraft.js.map +1 -1
- package/package.json +1 -2
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
import type { Entity } from '../../../types/Entity.js';
|
|
2
|
+
import type { EntityFiltering } from '../../../types/EntityFiltering.js';
|
|
3
|
+
import type { EntitySorting } from '../../../types/EntitySorting.js';
|
|
4
|
+
/**
|
|
5
|
+
* An entity a view's filtering names members of. `@entity()` is applied before
|
|
6
|
+
* TypeScript knows which class it decorates, so a view's members cannot be
|
|
7
|
+
* checked against it there; `extractEntityView` checks them when the view is
|
|
8
|
+
* first read instead.
|
|
9
|
+
*/
|
|
10
|
+
type AnyEntity = Entity & Record<string, unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* A narrower shape an entity is published in: some records left out, some
|
|
13
|
+
* members omitted — posts without their bodies, drafts left out.
|
|
14
|
+
*
|
|
15
|
+
* Read by `serializeEntityCollection(entity, records, { view })` and by any
|
|
16
|
+
* adapter that publishes records, so the shape is declared once on the entity
|
|
17
|
+
* rather than per consumer. A member a view omits is optional **in that view
|
|
18
|
+
* only**: `validateRecords(…, { view })` does not require it, and `required`
|
|
19
|
+
* keeps its meaning for the full record.
|
|
20
|
+
*/
|
|
21
|
+
export interface EntityViewOptions {
|
|
22
|
+
/** The records the view keeps, as a load request filters them. */
|
|
23
|
+
readonly filtering?: readonly EntityFiltering<AnyEntity>[];
|
|
24
|
+
/** The order the view lists them in. */
|
|
25
|
+
readonly sorting?: readonly EntitySorting<AnyEntity>[];
|
|
26
|
+
/** Members, by accessor name, the view leaves out. */
|
|
27
|
+
readonly omit?: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* How many records an entity may have: `{ min: 1, max: 1 }` for a site's one
|
|
31
|
+
* profile, `{ min: 1 }` for "at least one". Checked by `validateRecords` over a
|
|
32
|
+
* whole set of records — a static build's content, an import — and never by a
|
|
33
|
+
* single save, which cannot see the others.
|
|
34
|
+
*/
|
|
35
|
+
export interface EntityCardinality {
|
|
36
|
+
readonly min?: number;
|
|
37
|
+
readonly max?: number;
|
|
38
|
+
}
|
|
1
39
|
export interface MetaEntityOptions {
|
|
2
40
|
key?: string;
|
|
3
41
|
domain?: string;
|
|
@@ -5,6 +43,10 @@ export interface MetaEntityOptions {
|
|
|
5
43
|
labelKey?: string;
|
|
6
44
|
/** Translation key for its plural, e.g. `entity:product-specification.plural`. */
|
|
7
45
|
pluralKey?: string;
|
|
46
|
+
/** How many records the entity may have. */
|
|
47
|
+
cardinality?: EntityCardinality;
|
|
48
|
+
/** Named shapes the entity is published in, by view name. */
|
|
49
|
+
views?: Readonly<Record<string, EntityViewOptions>>;
|
|
8
50
|
}
|
|
9
51
|
export declare class MetaEntity {
|
|
10
52
|
readonly name: string;
|
|
@@ -12,6 +54,9 @@ export declare class MetaEntity {
|
|
|
12
54
|
readonly domain?: string;
|
|
13
55
|
readonly labelKey?: string;
|
|
14
56
|
readonly pluralKey?: string;
|
|
57
|
+
readonly cardinality?: EntityCardinality;
|
|
58
|
+
readonly views?: Readonly<Record<string, EntityViewOptions>>;
|
|
15
59
|
constructor(name: string, options?: MetaEntityOptions);
|
|
16
60
|
}
|
|
61
|
+
export {};
|
|
17
62
|
//# sourceMappingURL=meta-entity.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"meta-entity.d.ts","sourceRoot":"","sources":["../../../../src/entity-definition/meta-entities/meta-entity/meta-entity.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iGAAiG;IACjG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"meta-entity.d.ts","sourceRoot":"","sources":["../../../../src/entity-definition/meta-entities/meta-entity/meta-entity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAGrE;;;;;GAKG;AACH,KAAK,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAElD;;;;;;;;;GASG;AACH,MAAM,WAAW,iBAAiB;IAChC,kEAAkE;IAClE,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,eAAe,CAAC,SAAS,CAAC,EAAE,CAAC;IAC3D,wCAAwC;IACxC,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,aAAa,CAAC,SAAS,CAAC,EAAE,CAAC;IACvD,sDAAsD;IACtD,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iGAAiG;IACjG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,iBAAiB,CAAC;IAChC,6DAA6D;IAC7D,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC;CACrD;AAED,qBAAa,UAAU;IAErB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,CAAC,EAAE,iBAAiB,CAAC;IACzC,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC;gBAIjD,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB;CAiBtD"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assertBounds } from '../bounds.js';
|
|
1
2
|
export class MetaEntity {
|
|
2
3
|
//#endregion
|
|
3
4
|
//#region Constructors
|
|
@@ -7,6 +8,9 @@ export class MetaEntity {
|
|
|
7
8
|
this.domain = options == null ? void 0 : options.domain;
|
|
8
9
|
this.labelKey = options == null ? void 0 : options.labelKey;
|
|
9
10
|
this.pluralKey = options == null ? void 0 : options.pluralKey;
|
|
11
|
+
assertBounds(`${name}'s cardinality`, options == null ? void 0 : options.cardinality);
|
|
12
|
+
this.cardinality = options == null ? void 0 : options.cardinality;
|
|
13
|
+
this.views = options == null ? void 0 : options.views;
|
|
10
14
|
}
|
|
11
15
|
}
|
|
12
16
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/entity-definition/meta-entities/meta-entity/meta-entity.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../../../../src/entity-definition/meta-entities/meta-entity/meta-entity.ts"],"sourcesContent":["import type { Entity } from '../../../types/Entity.js';\nimport type { EntityFiltering } from '../../../types/EntityFiltering.js';\nimport type { EntitySorting } from '../../../types/EntitySorting.js';\nimport { assertBounds } from '../bounds.js';\n\n/**\n * An entity a view's filtering names members of. `@entity()` is applied before\n * TypeScript knows which class it decorates, so a view's members cannot be\n * checked against it there; `extractEntityView` checks them when the view is\n * first read instead.\n */\ntype AnyEntity = Entity & Record<string, unknown>;\n\n/**\n * A narrower shape an entity is published in: some records left out, some\n * members omitted — posts without their bodies, drafts left out.\n *\n * Read by `serializeEntityCollection(entity, records, { view })` and by any\n * adapter that publishes records, so the shape is declared once on the entity\n * rather than per consumer. A member a view omits is optional **in that view\n * only**: `validateRecords(…, { view })` does not require it, and `required`\n * keeps its meaning for the full record.\n */\nexport interface EntityViewOptions {\n /** The records the view keeps, as a load request filters them. */\n readonly filtering?: readonly EntityFiltering<AnyEntity>[];\n /** The order the view lists them in. */\n readonly sorting?: readonly EntitySorting<AnyEntity>[];\n /** Members, by accessor name, the view leaves out. */\n readonly omit?: readonly string[];\n}\n\n/**\n * How many records an entity may have: `{ min: 1, max: 1 }` for a site's one\n * profile, `{ min: 1 }` for \"at least one\". Checked by `validateRecords` over a\n * whole set of records — a static build's content, an import — and never by a\n * single save, which cannot see the others.\n */\nexport interface EntityCardinality {\n readonly min?: number;\n readonly max?: number;\n}\n\nexport interface MetaEntityOptions {\n key?: string;\n domain?: string;\n /** Translation key for the entity's singular name, e.g. `entity:product-specification.label`. */\n labelKey?: string;\n /** Translation key for its plural, e.g. `entity:product-specification.plural`. */\n pluralKey?: string;\n /** How many records the entity may have. */\n cardinality?: EntityCardinality;\n /** Named shapes the entity is published in, by view name. */\n views?: Readonly<Record<string, EntityViewOptions>>;\n}\n\nexport class MetaEntity {\n //#region Properties\n readonly name: string;\n readonly key?: string;\n readonly domain?: string;\n readonly labelKey?: string;\n readonly pluralKey?: string;\n readonly cardinality?: EntityCardinality;\n readonly views?: Readonly<Record<string, EntityViewOptions>>;\n //#endregion\n\n //#region Constructors\n constructor(name: string, options?: MetaEntityOptions) {\n this.name = name;\n this.key = options?.key;\n this.domain = options?.domain;\n this.labelKey = options?.labelKey;\n this.pluralKey = options?.pluralKey;\n assertBounds(`${name}'s cardinality`, options?.cardinality);\n this.cardinality = options?.cardinality;\n this.views = options?.views;\n }\n //#endregion\n\n //#region Methods\n //#endregion\n\n //#region Accessors\n //#endregion\n}\n"],"names":["assertBounds","MetaEntity","name","options","key","domain","labelKey","pluralKey","cardinality","views"],"mappings":"AAGA,SAASA,YAAY,QAAQ,eAAe;AAqD5C,OAAO,MAAMC;IASX,YAAY;IAEZ,sBAAsB;IACtB,YAAYC,IAAY,EAAEC,OAA2B,CAAE;QACrD,IAAI,CAACD,IAAI,GAAGA;QACZ,IAAI,CAACE,GAAG,GAAGD,2BAAAA,QAASC,GAAG;QACvB,IAAI,CAACC,MAAM,GAAGF,2BAAAA,QAASE,MAAM;QAC7B,IAAI,CAACC,QAAQ,GAAGH,2BAAAA,QAASG,QAAQ;QACjC,IAAI,CAACC,SAAS,GAAGJ,2BAAAA,QAASI,SAAS;QACnCP,aAAa,GAAGE,KAAK,cAAc,CAAC,EAAEC,2BAAAA,QAASK,WAAW;QAC1D,IAAI,CAACA,WAAW,GAAGL,2BAAAA,QAASK,WAAW;QACvC,IAAI,CAACC,KAAK,GAAGN,2BAAAA,QAASM,KAAK;IAC7B;AAQF"}
|
|
@@ -31,4 +31,11 @@ import type { EntityFieldDescriptor } from '../describe/index.js';
|
|
|
31
31
|
* how a child's `number` member would quietly start meaning something else.
|
|
32
32
|
*/
|
|
33
33
|
export declare function coerceFieldValue(descriptor: EntityFieldDescriptor, raw: string): unknown;
|
|
34
|
+
/**
|
|
35
|
+
* The value a `localized` member's draft stands for: the locales that hold
|
|
36
|
+
* text, or `undefined` when none does — an untouched member is not an object
|
|
37
|
+
* of empty strings. Whether every declared locale is filled is the form's
|
|
38
|
+
* question to ask before submit, not this one's.
|
|
39
|
+
*/
|
|
40
|
+
export declare function coerceLocalizedDraft(draft: Readonly<Record<string, string>>): Record<string, string> | undefined;
|
|
34
41
|
//# sourceMappingURL=coerce-field-value.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coerce-field-value.d.ts","sourceRoot":"","sources":["../../../src/entity-definition/reconstruct/coerce-field-value.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,qBAAqB,EACjC,GAAG,EAAE,MAAM,GACV,OAAO,CAST"}
|
|
1
|
+
{"version":3,"file":"coerce-field-value.d.ts","sourceRoot":"","sources":["../../../src/entity-definition/reconstruct/coerce-field-value.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,qBAAqB,EACjC,GAAG,EAAE,MAAM,GACV,OAAO,CAST;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GACtC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAGpC"}
|
|
@@ -38,5 +38,14 @@
|
|
|
38
38
|
if (descriptor.type === 'date') return new Date(raw);
|
|
39
39
|
return raw;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The value a `localized` member's draft stands for: the locales that hold
|
|
43
|
+
* text, or `undefined` when none does — an untouched member is not an object
|
|
44
|
+
* of empty strings. Whether every declared locale is filled is the form's
|
|
45
|
+
* question to ask before submit, not this one's.
|
|
46
|
+
*/ export function coerceLocalizedDraft(draft) {
|
|
47
|
+
const filled = Object.entries(draft).filter(([, text])=>text.trim() !== '');
|
|
48
|
+
return filled.length === 0 ? undefined : Object.fromEntries(filled);
|
|
49
|
+
}
|
|
41
50
|
|
|
42
51
|
//# sourceMappingURL=coerce-field-value.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/entity-definition/reconstruct/coerce-field-value.ts"],"sourcesContent":["import type { EntityFieldDescriptor } from '../describe/index.js';\n\n/**\n * The typed value a draft string stands for.\n *\n * This is the exact inverse of the form layer's `seedFieldValue`, and the two\n * have to agree or the round trip is lossy — a member seeded from a record and\n * submitted untouched must come back equal. Two rules are not obvious:\n *\n * - **`boolean` is decided before the empty check, and never yields\n * `undefined`.** A checkbox renders `''` as unchecked, so `''` means `false`;\n * letting the blanket empty rule run first would write `undefined` where the\n * user saw a box they had deliberately left clear.\n * - **`number` checks empty first.** `Number('')` is `0`, so the other order\n * turns every blank numeric field into a real zero.\n *\n * A malformed `number` or `date` is passed through as `NaN` / an invalid `Date`\n * rather than dropped: the form's metadata validation rejects both before\n * submit, and silently discarding a value the user typed would be worse than\n * surfacing it.\n *\n * `scalarCollection` is the one collection with a lossless string form, so it\n * round-trips here rather than waiting for an editor: a comma list in, a\n * `string[]` out. It reads **empty as `[]`, never `undefined`** — a member the\n * user cleared holds no values, which is a different fact from a member that\n * was never set, and only the empty array survives a `required` check honestly.\n * `composition` has no string form at all and never reaches this function; its\n * rows are rebuilt by `reconstructChild`.\n *\n * Extracted from `reconstructEntity` so that a composition's rows coerce through\n * the **same** function their master's scalars do. Two copies of these rules is\n * how a child's `number` member would quietly start meaning something else.\n */\nexport function coerceFieldValue(\n descriptor: EntityFieldDescriptor,\n raw: string,\n): unknown {\n if (descriptor.type === 'boolean') return raw === 'true';\n if (descriptor.type === 'scalarCollection') {\n return raw === '' ? [] : raw.split(',');\n }\n if (raw === '') return undefined;\n if (descriptor.type === 'number') return Number(raw);\n if (descriptor.type === 'date') return new Date(raw);\n return raw;\n}\n"],"names":["coerceFieldValue","descriptor","raw","type","split","undefined","Number","Date"],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,SAASA,iBACdC,UAAiC,EACjCC,GAAW;IAEX,IAAID,WAAWE,IAAI,KAAK,WAAW,OAAOD,QAAQ;IAClD,IAAID,WAAWE,IAAI,KAAK,oBAAoB;QAC1C,OAAOD,QAAQ,KAAK,EAAE,GAAGA,IAAIE,KAAK,CAAC;IACrC;IACA,IAAIF,QAAQ,IAAI,OAAOG;IACvB,IAAIJ,WAAWE,IAAI,KAAK,UAAU,OAAOG,OAAOJ;IAChD,IAAID,WAAWE,IAAI,KAAK,QAAQ,OAAO,IAAII,KAAKL;IAChD,OAAOA;AACT"}
|
|
1
|
+
{"version":3,"sources":["../../../src/entity-definition/reconstruct/coerce-field-value.ts"],"sourcesContent":["import type { EntityFieldDescriptor } from '../describe/index.js';\n\n/**\n * The typed value a draft string stands for.\n *\n * This is the exact inverse of the form layer's `seedFieldValue`, and the two\n * have to agree or the round trip is lossy — a member seeded from a record and\n * submitted untouched must come back equal. Two rules are not obvious:\n *\n * - **`boolean` is decided before the empty check, and never yields\n * `undefined`.** A checkbox renders `''` as unchecked, so `''` means `false`;\n * letting the blanket empty rule run first would write `undefined` where the\n * user saw a box they had deliberately left clear.\n * - **`number` checks empty first.** `Number('')` is `0`, so the other order\n * turns every blank numeric field into a real zero.\n *\n * A malformed `number` or `date` is passed through as `NaN` / an invalid `Date`\n * rather than dropped: the form's metadata validation rejects both before\n * submit, and silently discarding a value the user typed would be worse than\n * surfacing it.\n *\n * `scalarCollection` is the one collection with a lossless string form, so it\n * round-trips here rather than waiting for an editor: a comma list in, a\n * `string[]` out. It reads **empty as `[]`, never `undefined`** — a member the\n * user cleared holds no values, which is a different fact from a member that\n * was never set, and only the empty array survives a `required` check honestly.\n * `composition` has no string form at all and never reaches this function; its\n * rows are rebuilt by `reconstructChild`.\n *\n * Extracted from `reconstructEntity` so that a composition's rows coerce through\n * the **same** function their master's scalars do. Two copies of these rules is\n * how a child's `number` member would quietly start meaning something else.\n */\nexport function coerceFieldValue(\n descriptor: EntityFieldDescriptor,\n raw: string,\n): unknown {\n if (descriptor.type === 'boolean') return raw === 'true';\n if (descriptor.type === 'scalarCollection') {\n return raw === '' ? [] : raw.split(',');\n }\n if (raw === '') return undefined;\n if (descriptor.type === 'number') return Number(raw);\n if (descriptor.type === 'date') return new Date(raw);\n return raw;\n}\n\n/**\n * The value a `localized` member's draft stands for: the locales that hold\n * text, or `undefined` when none does — an untouched member is not an object\n * of empty strings. Whether every declared locale is filled is the form's\n * question to ask before submit, not this one's.\n */\nexport function coerceLocalizedDraft(\n draft: Readonly<Record<string, string>>,\n): Record<string, string> | undefined {\n const filled = Object.entries(draft).filter(([, text]) => text.trim() !== '');\n return filled.length === 0 ? undefined : Object.fromEntries(filled);\n}\n"],"names":["coerceFieldValue","descriptor","raw","type","split","undefined","Number","Date","coerceLocalizedDraft","draft","filled","Object","entries","filter","text","trim","length","fromEntries"],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,SAASA,iBACdC,UAAiC,EACjCC,GAAW;IAEX,IAAID,WAAWE,IAAI,KAAK,WAAW,OAAOD,QAAQ;IAClD,IAAID,WAAWE,IAAI,KAAK,oBAAoB;QAC1C,OAAOD,QAAQ,KAAK,EAAE,GAAGA,IAAIE,KAAK,CAAC;IACrC;IACA,IAAIF,QAAQ,IAAI,OAAOG;IACvB,IAAIJ,WAAWE,IAAI,KAAK,UAAU,OAAOG,OAAOJ;IAChD,IAAID,WAAWE,IAAI,KAAK,QAAQ,OAAO,IAAII,KAAKL;IAChD,OAAOA;AACT;AAEA;;;;;CAKC,GACD,OAAO,SAASM,qBACdC,KAAuC;IAEvC,MAAMC,SAASC,OAAOC,OAAO,CAACH,OAAOI,MAAM,CAAC,CAAC,GAAGC,KAAK,GAAKA,KAAKC,IAAI,OAAO;IAC1E,OAAOL,OAAOM,MAAM,KAAK,IAAIX,YAAYM,OAAOM,WAAW,CAACP;AAC9D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconstruct-entity.d.ts","sourceRoot":"","sources":["../../../src/entity-definition/reconstruct/reconstruct-entity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,
|
|
1
|
+
{"version":3,"file":"reconstruct-entity.d.ts","sourceRoot":"","sources":["../../../src/entity-definition/reconstruct/reconstruct-entity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,EACL,WAAW,EAGZ,MAAM,4BAA4B,CAAC;AAMpC,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,sCAAsC,CAAC;AAS9C,0EAA0E;AAC1E,MAAM,WAAW,wBAAwB,CAAC,OAAO,SAAS,MAAM;IAC9D;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,mBAAmB,CAAC;CACjC;AAmFD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,SAAS,MAAM,EACtD,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,EAAE,WAAW,EACnB,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAE,wBAAwB,CAAC,OAAO,CAAM,GAC9D,OAAO,CAgCT"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { readDraftString } from '../../types/EntityDraft.js';
|
|
1
|
+
import { readDraftString, readLocalizedDraft } from '../../types/EntityDraft.js';
|
|
2
2
|
import { isRowDraftArray } from '../../types/EntityRowDraft.js';
|
|
3
3
|
import { describeEntityColumns } from '../describe/index.js';
|
|
4
4
|
import { applyEntityLinks } from '../links/apply-entity-links/index.js';
|
|
5
5
|
import { EntityCollectionLink } from '../links/entity-collection-link/index.js';
|
|
6
6
|
import { EntityLink } from '../links/entity-link/index.js';
|
|
7
|
-
import { coerceFieldValue } from './coerce-field-value.js';
|
|
7
|
+
import { coerceFieldValue, coerceLocalizedDraft } from './coerce-field-value.js';
|
|
8
8
|
import { reconstructChild } from './reconstruct-child.js';
|
|
9
9
|
/**
|
|
10
10
|
* Writes an entity's owned collections back from the rows its draft holds.
|
|
@@ -103,6 +103,12 @@ import { reconstructChild } from './reconstruct-child.js';
|
|
|
103
103
|
// `alias ?? name`, which is the wire/SQL column, not the JS property.
|
|
104
104
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
105
105
|
const current = instance[descriptor.name];
|
|
106
|
+
if (descriptor.type === 'localized') {
|
|
107
|
+
if (descriptor.readonly) continue;
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
|
+
instance[descriptor.name] = coerceLocalizedDraft(readLocalizedDraft(values, descriptor.name));
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
106
112
|
if (!isWritableScalar(descriptor, current)) continue;
|
|
107
113
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
114
|
instance[descriptor.name] = coerceFieldValue(descriptor, readDraftString(values, descriptor.name));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/entity-definition/reconstruct/reconstruct-entity.ts"],"sourcesContent":["import { Entity, EntityConstructor } from '../../types/Entity.js';\nimport { EntityDraft, readDraftString } from '../../types/EntityDraft.js';\nimport { isRowDraftArray } from '../../types/EntityRowDraft.js';\nimport {\n describeEntityColumns,\n EntityFieldDescriptor,\n} from '../describe/index.js';\nimport {\n applyEntityLinks,\n type EntityLinkSelection,\n} from '../links/apply-entity-links/index.js';\nimport { EntityCollectionLink } from '../links/entity-collection-link/index.js';\nimport { EntityLink } from '../links/entity-link/index.js';\nimport { coerceFieldValue } from './coerce-field-value.js';\nimport { reconstructChild } from './reconstruct-child.js';\n\n/** Everything {@link reconstructEntity} needs beyond the draft itself. */\nexport interface ReconstructEntityOptions<TEntity extends Entity> {\n /**\n * The record being edited. Supplies the `id` and nothing else — see\n * {@link reconstructEntity} on why it is only the id.\n */\n existing?: TEntity;\n /**\n * The instances a picker handed over, for members whose declared\n * `linkSerialization` is `embedded`. Passed straight to `applyEntityLinks`.\n */\n selection?: EntityLinkSelection;\n}\n\n/**\n * Writes an entity's owned collections back from the rows its draft holds.\n *\n * A **second pass** rather than a branch inside the scalar walk, and the reason\n * is the invariant it protects: `isWritableScalar` must go on refusing\n * `composition`, because assigning a coerced string to a member that holds rows\n * is how the master's own lines get blanked on a save that touches nothing else.\n * Keeping the two walks separate means a `composition` is written by exactly one\n * piece of code, and that code cannot be reached with a string.\n *\n * A member whose draft entry is missing or unreadable is **skipped, not\n * cleared** — the same rule the scalar walk applies to a member the form hides.\n * A draft written before this member existed, or one restored from a build that\n * did not know it, must leave the record's rows exactly as they were rather than\n * emptying them, which is a data loss the user never asked for and cannot see.\n * An explicitly empty list *is* a value and is written: that is a user who\n * removed every row.\n *\n * Read-only is honoured for the same reason as everywhere else, and the\n * `childType` guard is not defensive — a `composition` that declared no child\n * has no columns to coerce against, so there is nothing to rebuild.\n */\nfunction applyEntityComposition(\n instance: object,\n descriptors: readonly EntityFieldDescriptor[],\n values: EntityDraft,\n): void {\n for (const descriptor of descriptors) {\n if (descriptor.type !== 'composition') continue;\n if (descriptor.readonly || descriptor.childType === undefined) continue;\n\n const drafted = values[descriptor.name];\n if (!isRowDraftArray(drafted)) continue;\n\n const rows = drafted;\n const childType = descriptor.childType;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (instance as any)[descriptor.name] = rows.map(row =>\n reconstructChild(childType, row),\n );\n }\n}\n\n/**\n * A member this function must not write.\n *\n * `readonly` is the declared half — `describeEntityColumns` keeps read-only\n * members because a form still *shows* them, so unlike the column list this\n * walk has to filter them itself, exactly as `buildEntityInstance` does.\n *\n * The relation half is checked twice on purpose. The declared `link` /\n * `linkCollection` types are `applyEntityLinks`' business, and the runtime\n * `instanceof` catches a bare `@accessor()` on a relation that declared no\n * type: those accessors are getter-only, so assigning a coerced string to one\n * would throw rather than misbehave quietly.\n *\n * `composition` is excluded for the reason `linkCollection` is: it has no\n * editor yet, so a draft never holds its rows and writing one would mean\n * writing `undefined` over the master's own lines on every save. The rows'\n * write path lands with the detail control (#122). `scalarCollection` is *not*\n * excluded — it is a scalar as far as this walk is concerned, because its\n * comma-list draft coerces losslessly back to a `string[]`.\n */\nfunction isWritableScalar(\n descriptor: EntityFieldDescriptor,\n current: unknown,\n): boolean {\n if (descriptor.readonly) return false;\n if (\n descriptor.type === 'link' ||\n descriptor.type === 'linkCollection' ||\n descriptor.type === 'composition'\n ) {\n return false;\n }\n return !(\n current instanceof EntityLink || current instanceof EntityCollectionLink\n );\n}\n\n/**\n * Rebuilds a domain instance from a form's string draft, deriving every\n * assignment from the entity's own accessor metadata.\n *\n * This is the one genuinely hand-written part of an entity form, and writing it\n * per entity is a bug farm: each one repeats the same empty-string-to-undefined\n * rule, and each one has to remember to carry back a member the form hides —\n * `ProductBrand.code` is assigned by the create transaction, and dropping it\n * blanks the record's identifier on every update. Deriving the walk from the\n * metadata makes that structural instead of remembered.\n *\n * **Construction is zero-argument.** `new entityConstructor()` then assignment\n * through setters, the same shape `buildEntityInstance` uses to rebuild a record\n * off the wire. Entities default their constructor parameters, and a required\n * member is filled by its setter a moment later — so required-ness stays one\n * fact (the `@accessor({ required })` flag the form already validates against)\n * rather than a second, unverifiable list of constructor argument names.\n *\n * Constructing first also gives `describeEntityColumns` a sample, which is what\n * lets it infer `link`-ness for an accessor that declared no `type`: relations\n * are initialized by the constructor, so the fresh instance already carries them.\n *\n * **`existing` supplies the id and nothing else.** Every other member comes from\n * the draft, which was seeded from the record — so a create/update distinction\n * lives here once instead of a `target.id = entity?.id` line per form. A draft\n * may be client-supplied (a workspace autosaves it), which is not a new surface:\n * a save route already overwrites the id from its own route parameter.\n *\n * Pure and framework-free, so the same reconstruction runs in a browser form and\n * in a backend use-case assembling an entity from a raw draft.\n */\nexport function reconstructEntity<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n values: EntityDraft,\n { existing, selection }: ReconstructEntityOptions<TEntity> = {},\n): TEntity {\n const instance = new entityConstructor();\n const descriptors = describeEntityColumns(entityConstructor, instance);\n\n for (const descriptor of descriptors) {\n // The draft is keyed by accessor name, and so is the assignment: `key` is\n // `alias ?? name`, which is the wire/SQL column, not the JS property.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const current = (instance as any)[descriptor.name];\n if (!isWritableScalar(descriptor, current)) continue;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (instance as any)[descriptor.name] = coerceFieldValue(\n descriptor,\n readDraftString(values, descriptor.name),\n );\n }\n\n applyEntityComposition(instance, descriptors, values);\n applyEntityLinks(instance, descriptors, values, selection);\n\n if (existing !== undefined) instance.id = existing.id;\n\n return instance;\n}\n"],"names":["readDraftString","isRowDraftArray","describeEntityColumns","applyEntityLinks","EntityCollectionLink","EntityLink","coerceFieldValue","reconstructChild","applyEntityComposition","instance","descriptors","values","descriptor","type","readonly","childType","undefined","drafted","name","rows","map","row","isWritableScalar","current","reconstructEntity","entityConstructor","existing","selection","id"],"mappings":"AACA,SAAsBA,eAAe,QAAQ,6BAA6B;AAC1E,SAASC,eAAe,QAAQ,gCAAgC;AAChE,SACEC,qBAAqB,QAEhB,uBAAuB;AAC9B,SACEC,gBAAgB,QAEX,uCAAuC;AAC9C,SAASC,oBAAoB,QAAQ,2CAA2C;AAChF,SAASC,UAAU,QAAQ,gCAAgC;AAC3D,SAASC,gBAAgB,QAAQ,0BAA0B;AAC3D,SAASC,gBAAgB,QAAQ,yBAAyB;AAgB1D;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,SAASC,uBACPC,QAAgB,EAChBC,WAA6C,EAC7CC,MAAmB;IAEnB,KAAK,MAAMC,cAAcF,YAAa;QACpC,IAAIE,WAAWC,IAAI,KAAK,eAAe;QACvC,IAAID,WAAWE,QAAQ,IAAIF,WAAWG,SAAS,KAAKC,WAAW;QAE/D,MAAMC,UAAUN,MAAM,CAACC,WAAWM,IAAI,CAAC;QACvC,IAAI,CAACjB,gBAAgBgB,UAAU;QAE/B,MAAME,OAAOF;QACb,MAAMF,YAAYH,WAAWG,SAAS;QAEtC,8DAA8D;QAC7DN,QAAgB,CAACG,WAAWM,IAAI,CAAC,GAAGC,KAAKC,GAAG,CAACC,CAAAA,MAC5Cd,iBAAiBQ,WAAWM;IAEhC;AACF;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,SAASC,iBACPV,UAAiC,EACjCW,OAAgB;IAEhB,IAAIX,WAAWE,QAAQ,EAAE,OAAO;IAChC,IACEF,WAAWC,IAAI,KAAK,UACpBD,WAAWC,IAAI,KAAK,oBACpBD,WAAWC,IAAI,KAAK,eACpB;QACA,OAAO;IACT;IACA,OAAO,CACLU,CAAAA,mBAAmBlB,cAAckB,mBAAmBnB,oBAAmB;AAE3E;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,SAASoB,kBACdC,iBAA6C,EAC7Cd,MAAmB,EACnB,EAAEe,QAAQ,EAAEC,SAAS,EAAqC,GAAG,CAAC,CAAC;IAE/D,MAAMlB,WAAW,IAAIgB;IACrB,MAAMf,cAAcR,sBAAsBuB,mBAAmBhB;IAE7D,KAAK,MAAMG,cAAcF,YAAa;QACpC,0EAA0E;QAC1E,sEAAsE;QACtE,8DAA8D;QAC9D,MAAMa,UAAU,AAACd,QAAgB,CAACG,WAAWM,IAAI,CAAC;QAClD,IAAI,CAACI,iBAAiBV,YAAYW,UAAU;QAE5C,8DAA8D;QAC7Dd,QAAgB,CAACG,WAAWM,IAAI,CAAC,GAAGZ,iBACnCM,YACAZ,gBAAgBW,QAAQC,WAAWM,IAAI;IAE3C;IAEAV,uBAAuBC,UAAUC,aAAaC;IAC9CR,iBAAiBM,UAAUC,aAAaC,QAAQgB;IAEhD,IAAID,aAAaV,WAAWP,SAASmB,EAAE,GAAGF,SAASE,EAAE;IAErD,OAAOnB;AACT"}
|
|
1
|
+
{"version":3,"sources":["../../../src/entity-definition/reconstruct/reconstruct-entity.ts"],"sourcesContent":["import { Entity, EntityConstructor } from '../../types/Entity.js';\nimport {\n EntityDraft,\n readDraftString,\n readLocalizedDraft,\n} from '../../types/EntityDraft.js';\nimport { isRowDraftArray } from '../../types/EntityRowDraft.js';\nimport {\n describeEntityColumns,\n EntityFieldDescriptor,\n} from '../describe/index.js';\nimport {\n applyEntityLinks,\n type EntityLinkSelection,\n} from '../links/apply-entity-links/index.js';\nimport { EntityCollectionLink } from '../links/entity-collection-link/index.js';\nimport { EntityLink } from '../links/entity-link/index.js';\nimport {\n coerceFieldValue,\n coerceLocalizedDraft,\n} from './coerce-field-value.js';\nimport { reconstructChild } from './reconstruct-child.js';\n\n/** Everything {@link reconstructEntity} needs beyond the draft itself. */\nexport interface ReconstructEntityOptions<TEntity extends Entity> {\n /**\n * The record being edited. Supplies the `id` and nothing else — see\n * {@link reconstructEntity} on why it is only the id.\n */\n existing?: TEntity;\n /**\n * The instances a picker handed over, for members whose declared\n * `linkSerialization` is `embedded`. Passed straight to `applyEntityLinks`.\n */\n selection?: EntityLinkSelection;\n}\n\n/**\n * Writes an entity's owned collections back from the rows its draft holds.\n *\n * A **second pass** rather than a branch inside the scalar walk, and the reason\n * is the invariant it protects: `isWritableScalar` must go on refusing\n * `composition`, because assigning a coerced string to a member that holds rows\n * is how the master's own lines get blanked on a save that touches nothing else.\n * Keeping the two walks separate means a `composition` is written by exactly one\n * piece of code, and that code cannot be reached with a string.\n *\n * A member whose draft entry is missing or unreadable is **skipped, not\n * cleared** — the same rule the scalar walk applies to a member the form hides.\n * A draft written before this member existed, or one restored from a build that\n * did not know it, must leave the record's rows exactly as they were rather than\n * emptying them, which is a data loss the user never asked for and cannot see.\n * An explicitly empty list *is* a value and is written: that is a user who\n * removed every row.\n *\n * Read-only is honoured for the same reason as everywhere else, and the\n * `childType` guard is not defensive — a `composition` that declared no child\n * has no columns to coerce against, so there is nothing to rebuild.\n */\nfunction applyEntityComposition(\n instance: object,\n descriptors: readonly EntityFieldDescriptor[],\n values: EntityDraft,\n): void {\n for (const descriptor of descriptors) {\n if (descriptor.type !== 'composition') continue;\n if (descriptor.readonly || descriptor.childType === undefined) continue;\n\n const drafted = values[descriptor.name];\n if (!isRowDraftArray(drafted)) continue;\n\n const rows = drafted;\n const childType = descriptor.childType;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (instance as any)[descriptor.name] = rows.map(row =>\n reconstructChild(childType, row),\n );\n }\n}\n\n/**\n * A member this function must not write.\n *\n * `readonly` is the declared half — `describeEntityColumns` keeps read-only\n * members because a form still *shows* them, so unlike the column list this\n * walk has to filter them itself, exactly as `buildEntityInstance` does.\n *\n * The relation half is checked twice on purpose. The declared `link` /\n * `linkCollection` types are `applyEntityLinks`' business, and the runtime\n * `instanceof` catches a bare `@accessor()` on a relation that declared no\n * type: those accessors are getter-only, so assigning a coerced string to one\n * would throw rather than misbehave quietly.\n *\n * `composition` is excluded for the reason `linkCollection` is: it has no\n * editor yet, so a draft never holds its rows and writing one would mean\n * writing `undefined` over the master's own lines on every save. The rows'\n * write path lands with the detail control (#122). `scalarCollection` is *not*\n * excluded — it is a scalar as far as this walk is concerned, because its\n * comma-list draft coerces losslessly back to a `string[]`.\n */\nfunction isWritableScalar(\n descriptor: EntityFieldDescriptor,\n current: unknown,\n): boolean {\n if (descriptor.readonly) return false;\n if (\n descriptor.type === 'link' ||\n descriptor.type === 'linkCollection' ||\n descriptor.type === 'composition'\n ) {\n return false;\n }\n return !(\n current instanceof EntityLink || current instanceof EntityCollectionLink\n );\n}\n\n/**\n * Rebuilds a domain instance from a form's string draft, deriving every\n * assignment from the entity's own accessor metadata.\n *\n * This is the one genuinely hand-written part of an entity form, and writing it\n * per entity is a bug farm: each one repeats the same empty-string-to-undefined\n * rule, and each one has to remember to carry back a member the form hides —\n * `ProductBrand.code` is assigned by the create transaction, and dropping it\n * blanks the record's identifier on every update. Deriving the walk from the\n * metadata makes that structural instead of remembered.\n *\n * **Construction is zero-argument.** `new entityConstructor()` then assignment\n * through setters, the same shape `buildEntityInstance` uses to rebuild a record\n * off the wire. Entities default their constructor parameters, and a required\n * member is filled by its setter a moment later — so required-ness stays one\n * fact (the `@accessor({ required })` flag the form already validates against)\n * rather than a second, unverifiable list of constructor argument names.\n *\n * Constructing first also gives `describeEntityColumns` a sample, which is what\n * lets it infer `link`-ness for an accessor that declared no `type`: relations\n * are initialized by the constructor, so the fresh instance already carries them.\n *\n * **`existing` supplies the id and nothing else.** Every other member comes from\n * the draft, which was seeded from the record — so a create/update distinction\n * lives here once instead of a `target.id = entity?.id` line per form. A draft\n * may be client-supplied (a workspace autosaves it), which is not a new surface:\n * a save route already overwrites the id from its own route parameter.\n *\n * Pure and framework-free, so the same reconstruction runs in a browser form and\n * in a backend use-case assembling an entity from a raw draft.\n */\nexport function reconstructEntity<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n values: EntityDraft,\n { existing, selection }: ReconstructEntityOptions<TEntity> = {},\n): TEntity {\n const instance = new entityConstructor();\n const descriptors = describeEntityColumns(entityConstructor, instance);\n\n for (const descriptor of descriptors) {\n // The draft is keyed by accessor name, and so is the assignment: `key` is\n // `alias ?? name`, which is the wire/SQL column, not the JS property.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const current = (instance as any)[descriptor.name];\n if (descriptor.type === 'localized') {\n if (descriptor.readonly) continue;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (instance as any)[descriptor.name] = coerceLocalizedDraft(\n readLocalizedDraft(values, descriptor.name),\n );\n continue;\n }\n if (!isWritableScalar(descriptor, current)) continue;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (instance as any)[descriptor.name] = coerceFieldValue(\n descriptor,\n readDraftString(values, descriptor.name),\n );\n }\n\n applyEntityComposition(instance, descriptors, values);\n applyEntityLinks(instance, descriptors, values, selection);\n\n if (existing !== undefined) instance.id = existing.id;\n\n return instance;\n}\n"],"names":["readDraftString","readLocalizedDraft","isRowDraftArray","describeEntityColumns","applyEntityLinks","EntityCollectionLink","EntityLink","coerceFieldValue","coerceLocalizedDraft","reconstructChild","applyEntityComposition","instance","descriptors","values","descriptor","type","readonly","childType","undefined","drafted","name","rows","map","row","isWritableScalar","current","reconstructEntity","entityConstructor","existing","selection","id"],"mappings":"AACA,SAEEA,eAAe,EACfC,kBAAkB,QACb,6BAA6B;AACpC,SAASC,eAAe,QAAQ,gCAAgC;AAChE,SACEC,qBAAqB,QAEhB,uBAAuB;AAC9B,SACEC,gBAAgB,QAEX,uCAAuC;AAC9C,SAASC,oBAAoB,QAAQ,2CAA2C;AAChF,SAASC,UAAU,QAAQ,gCAAgC;AAC3D,SACEC,gBAAgB,EAChBC,oBAAoB,QACf,0BAA0B;AACjC,SAASC,gBAAgB,QAAQ,yBAAyB;AAgB1D;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,SAASC,uBACPC,QAAgB,EAChBC,WAA6C,EAC7CC,MAAmB;IAEnB,KAAK,MAAMC,cAAcF,YAAa;QACpC,IAAIE,WAAWC,IAAI,KAAK,eAAe;QACvC,IAAID,WAAWE,QAAQ,IAAIF,WAAWG,SAAS,KAAKC,WAAW;QAE/D,MAAMC,UAAUN,MAAM,CAACC,WAAWM,IAAI,CAAC;QACvC,IAAI,CAAClB,gBAAgBiB,UAAU;QAE/B,MAAME,OAAOF;QACb,MAAMF,YAAYH,WAAWG,SAAS;QAEtC,8DAA8D;QAC7DN,QAAgB,CAACG,WAAWM,IAAI,CAAC,GAAGC,KAAKC,GAAG,CAACC,CAAAA,MAC5Cd,iBAAiBQ,WAAWM;IAEhC;AACF;AAEA;;;;;;;;;;;;;;;;;;;CAmBC,GACD,SAASC,iBACPV,UAAiC,EACjCW,OAAgB;IAEhB,IAAIX,WAAWE,QAAQ,EAAE,OAAO;IAChC,IACEF,WAAWC,IAAI,KAAK,UACpBD,WAAWC,IAAI,KAAK,oBACpBD,WAAWC,IAAI,KAAK,eACpB;QACA,OAAO;IACT;IACA,OAAO,CACLU,CAAAA,mBAAmBnB,cAAcmB,mBAAmBpB,oBAAmB;AAE3E;AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BC,GACD,OAAO,SAASqB,kBACdC,iBAA6C,EAC7Cd,MAAmB,EACnB,EAAEe,QAAQ,EAAEC,SAAS,EAAqC,GAAG,CAAC,CAAC;IAE/D,MAAMlB,WAAW,IAAIgB;IACrB,MAAMf,cAAcT,sBAAsBwB,mBAAmBhB;IAE7D,KAAK,MAAMG,cAAcF,YAAa;QACpC,0EAA0E;QAC1E,sEAAsE;QACtE,8DAA8D;QAC9D,MAAMa,UAAU,AAACd,QAAgB,CAACG,WAAWM,IAAI,CAAC;QAClD,IAAIN,WAAWC,IAAI,KAAK,aAAa;YACnC,IAAID,WAAWE,QAAQ,EAAE;YACzB,8DAA8D;YAC7DL,QAAgB,CAACG,WAAWM,IAAI,CAAC,GAAGZ,qBACnCP,mBAAmBY,QAAQC,WAAWM,IAAI;YAE5C;QACF;QACA,IAAI,CAACI,iBAAiBV,YAAYW,UAAU;QAE5C,8DAA8D;QAC7Dd,QAAgB,CAACG,WAAWM,IAAI,CAAC,GAAGb,iBACnCO,YACAd,gBAAgBa,QAAQC,WAAWM,IAAI;IAE3C;IAEAV,uBAAuBC,UAAUC,aAAaC;IAC9CT,iBAAiBO,UAAUC,aAAaC,QAAQgB;IAEhD,IAAID,aAAaV,WAAWP,SAASmB,EAAE,GAAGF,SAASE,EAAE;IAErD,OAAOnB;AACT"}
|
|
@@ -22,5 +22,17 @@ export type SerializedEntity = Record<string, unknown>;
|
|
|
22
22
|
* unresolved relations don't pollute the output.
|
|
23
23
|
*/
|
|
24
24
|
export declare function serializeEntity<TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, instance: TEntity): SerializedEntity;
|
|
25
|
-
export
|
|
25
|
+
export interface SerializeCollectionOptions {
|
|
26
|
+
/**
|
|
27
|
+
* One of the entity's named views: only the records it keeps, in its order,
|
|
28
|
+
* without the members it omits.
|
|
29
|
+
*/
|
|
30
|
+
readonly view?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Serializes many instances; with a `view`, publishes them in that view's
|
|
34
|
+
* shape — the one place a published shape is applied, so every adapter that
|
|
35
|
+
* publishes records writes the same thing.
|
|
36
|
+
*/
|
|
37
|
+
export declare function serializeEntityCollection<TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, instances: readonly TEntity[], { view: viewName }?: SerializeCollectionOptions): SerializedEntity[];
|
|
26
38
|
//# sourceMappingURL=serialize.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../../src/entity-definition/serializer/serialize.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../../src/entity-definition/serializer/serialize.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AASlE;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AA4CvD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,SAAS,MAAM,EACpD,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,QAAQ,EAAE,OAAO,GAChB,gBAAgB,CAoClB;AAED,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,SAAS,MAAM,EAC9D,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,SAAS,EAAE,SAAS,OAAO,EAAE,EAC7B,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAE,0BAA+B,GAClD,gBAAgB,EAAE,CAsBpB"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { applyFiltering, applySorting } from '../../query/in-memory-query.js';
|
|
2
|
+
import { extractEntityView } from '../describe/entity-view.js';
|
|
1
3
|
import { extractMetaAccessors } from '../helpers/index.js';
|
|
2
4
|
import { EntityCollectionLink } from '../links/entity-collection-link/index.js';
|
|
3
5
|
import { EntityLink } from '../links/entity-link/index.js';
|
|
@@ -57,8 +59,18 @@ function serializeCollectionLink(link) {
|
|
|
57
59
|
});
|
|
58
60
|
return result;
|
|
59
61
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Serializes many instances; with a `view`, publishes them in that view's
|
|
64
|
+
* shape — the one place a published shape is applied, so every adapter that
|
|
65
|
+
* publishes records writes the same thing.
|
|
66
|
+
*/ export function serializeEntityCollection(entityConstructor, instances, { view: viewName } = {}) {
|
|
67
|
+
if (viewName === undefined) {
|
|
68
|
+
return instances.map((instance)=>serializeEntity(entityConstructor, instance));
|
|
69
|
+
}
|
|
70
|
+
const view = extractEntityView(entityConstructor, viewName);
|
|
71
|
+
const omitted = new Set(view.omitted.map((descriptor)=>descriptor.key));
|
|
72
|
+
const kept = applySorting(applyFiltering(instances, view.filtering), view.sorting);
|
|
73
|
+
return kept.map((instance)=>Object.fromEntries(Object.entries(serializeEntity(entityConstructor, instance)).filter(([key])=>!omitted.has(key))));
|
|
62
74
|
}
|
|
63
75
|
|
|
64
76
|
//# sourceMappingURL=serialize.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/entity-definition/serializer/serialize.ts"],"sourcesContent":["import { Entity, EntityConstructor } from '../../types/Entity.js';\nimport { extractMetaAccessors } from '../helpers/index.js';\nimport { EntityCollectionLink } from '../links/entity-collection-link/index.js';\nimport { EntityLink } from '../links/entity-link/index.js';\nimport type { ChildConstructor } from '../meta-entities/meta-accessor/index.js';\n\n/**\n * A plain, JSON-safe representation of an entity — the inverse of\n * {@link buildEntityInstance}. Relations collapse to the shape the deserializer\n * expects to round-trip:\n * - an {@link EntityLink} → the embedded plain object when loaded, otherwise the\n * scalar foreign-key id.\n * - an {@link EntityCollectionLink} → an array mixing embedded objects (loaded)\n * or scalar ids.\n * - a `composition` member → an array of plain child objects, each walked with\n * the child's own accessor metadata.\n *\n * This one serialization serves both persistence (entity → Mongo document) and\n * transport (entity → HTTP wire shape), so a value written by an adapter reads\n * back identically through {@link deserializeSingleEntity}.\n */\nexport type SerializedEntity = Record<string, unknown>;\n\nfunction serializeLink<TEntity extends Entity>(\n link: EntityLink<TEntity>,\n): unknown {\n if (link.isLoaded && link.value !== undefined) {\n return serializeEntity(link.entityConstructor, link.value);\n }\n return link.id ?? undefined;\n}\n\n/**\n * Owned rows, each flattened through the child's own accessors.\n *\n * A child is a class whose private fields carry its state, so passing the array\n * through untouched would put `[{}, {}]` on the wire and in the database — the\n * rows would silently never persist. Walking each one is also what keeps a\n * child's `alias` meaning what it means everywhere else: its column name.\n *\n * It reads a plain object just as happily as an instance, which is deliberate:\n * a row that never went through the deserializer (a fixture, a hand-built\n * command payload) serializes to the same document.\n */\nfunction serializeComposition(\n rows: readonly unknown[],\n childType: ChildConstructor,\n): unknown {\n return rows.map(row =>\n serializeEntity(childType as EntityConstructor<Entity>, row as Entity),\n );\n}\n\nfunction serializeCollectionLink<TEntity extends Entity>(\n link: EntityCollectionLink<TEntity>,\n): unknown {\n if (link.isLoaded && link.values !== undefined) {\n return link.values.map(value =>\n serializeEntity(link.entityConstructor, value),\n );\n }\n const ids = link.ids;\n return ids.length > 0 ? ids : undefined;\n}\n\n/**\n * Serializes an entity instance into a plain object, walking the same accessor\n * metadata the deserializer reads (getter-kind, not hidden/readonly), keyed by\n * `alias ?? name`. `undefined` values are omitted so absent optionals and\n * unresolved relations don't pollute the output.\n */\nexport function serializeEntity<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n instance: TEntity,\n): SerializedEntity {\n const result: SerializedEntity = {};\n\n extractMetaAccessors(entityConstructor)\n .filter(\n metaAccessor =>\n !metaAccessor.hidden &&\n !metaAccessor.readonly &&\n metaAccessor.kind === 'getter',\n )\n .forEach(metaAccessor => {\n const propertyName = metaAccessor.alias ?? metaAccessor.name;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const value = (instance as any)[metaAccessor.name];\n\n let serialized: unknown;\n if (value instanceof EntityLink) {\n serialized = serializeLink(value);\n } else if (value instanceof EntityCollectionLink) {\n serialized = serializeCollectionLink(value);\n } else if (\n metaAccessor.type === 'composition' &&\n metaAccessor.childType !== undefined &&\n Array.isArray(value)\n ) {\n serialized = serializeComposition(value, metaAccessor.childType());\n } else {\n serialized = value;\n }\n\n if (serialized !== undefined) {\n result[propertyName as string] = serialized;\n }\n });\n\n return result;\n}\n\nexport function serializeEntityCollection<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n instances: readonly TEntity[],\n): SerializedEntity[] {\n return instances.map(instance =>\n serializeEntity(entityConstructor, instance),\n );\n}\n"],"names":["extractMetaAccessors","EntityCollectionLink","EntityLink","serializeLink","link","isLoaded","value","undefined","serializeEntity","entityConstructor","id","serializeComposition","rows","childType","map","row","serializeCollectionLink","values","ids","length","instance","result","filter","metaAccessor","hidden","readonly","kind","forEach","propertyName","alias","name","serialized","type","Array","isArray","serializeEntityCollection","instances"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../src/entity-definition/serializer/serialize.ts"],"sourcesContent":["import { applyFiltering, applySorting } from '../../query/in-memory-query.js';\nimport { Entity, EntityConstructor } from '../../types/Entity.js';\nimport type { EntityFiltering } from '../../types/EntityFiltering.js';\nimport type { EntitySorting } from '../../types/EntitySorting.js';\nimport { extractEntityView } from '../describe/entity-view.js';\nimport { extractMetaAccessors } from '../helpers/index.js';\nimport { EntityCollectionLink } from '../links/entity-collection-link/index.js';\nimport { EntityLink } from '../links/entity-link/index.js';\nimport type { ChildConstructor } from '../meta-entities/meta-accessor/index.js';\n\n/**\n * A plain, JSON-safe representation of an entity — the inverse of\n * {@link buildEntityInstance}. Relations collapse to the shape the deserializer\n * expects to round-trip:\n * - an {@link EntityLink} → the embedded plain object when loaded, otherwise the\n * scalar foreign-key id.\n * - an {@link EntityCollectionLink} → an array mixing embedded objects (loaded)\n * or scalar ids.\n * - a `composition` member → an array of plain child objects, each walked with\n * the child's own accessor metadata.\n *\n * This one serialization serves both persistence (entity → Mongo document) and\n * transport (entity → HTTP wire shape), so a value written by an adapter reads\n * back identically through {@link deserializeSingleEntity}.\n */\nexport type SerializedEntity = Record<string, unknown>;\n\nfunction serializeLink<TEntity extends Entity>(\n link: EntityLink<TEntity>,\n): unknown {\n if (link.isLoaded && link.value !== undefined) {\n return serializeEntity(link.entityConstructor, link.value);\n }\n return link.id ?? undefined;\n}\n\n/**\n * Owned rows, each flattened through the child's own accessors.\n *\n * A child is a class whose private fields carry its state, so passing the array\n * through untouched would put `[{}, {}]` on the wire and in the database — the\n * rows would silently never persist. Walking each one is also what keeps a\n * child's `alias` meaning what it means everywhere else: its column name.\n *\n * It reads a plain object just as happily as an instance, which is deliberate:\n * a row that never went through the deserializer (a fixture, a hand-built\n * command payload) serializes to the same document.\n */\nfunction serializeComposition(\n rows: readonly unknown[],\n childType: ChildConstructor,\n): unknown {\n return rows.map(row =>\n serializeEntity(childType as EntityConstructor<Entity>, row as Entity),\n );\n}\n\nfunction serializeCollectionLink<TEntity extends Entity>(\n link: EntityCollectionLink<TEntity>,\n): unknown {\n if (link.isLoaded && link.values !== undefined) {\n return link.values.map(value =>\n serializeEntity(link.entityConstructor, value),\n );\n }\n const ids = link.ids;\n return ids.length > 0 ? ids : undefined;\n}\n\n/**\n * Serializes an entity instance into a plain object, walking the same accessor\n * metadata the deserializer reads (getter-kind, not hidden/readonly), keyed by\n * `alias ?? name`. `undefined` values are omitted so absent optionals and\n * unresolved relations don't pollute the output.\n */\nexport function serializeEntity<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n instance: TEntity,\n): SerializedEntity {\n const result: SerializedEntity = {};\n\n extractMetaAccessors(entityConstructor)\n .filter(\n metaAccessor =>\n !metaAccessor.hidden &&\n !metaAccessor.readonly &&\n metaAccessor.kind === 'getter',\n )\n .forEach(metaAccessor => {\n const propertyName = metaAccessor.alias ?? metaAccessor.name;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const value = (instance as any)[metaAccessor.name];\n\n let serialized: unknown;\n if (value instanceof EntityLink) {\n serialized = serializeLink(value);\n } else if (value instanceof EntityCollectionLink) {\n serialized = serializeCollectionLink(value);\n } else if (\n metaAccessor.type === 'composition' &&\n metaAccessor.childType !== undefined &&\n Array.isArray(value)\n ) {\n serialized = serializeComposition(value, metaAccessor.childType());\n } else {\n serialized = value;\n }\n\n if (serialized !== undefined) {\n result[propertyName as string] = serialized;\n }\n });\n\n return result;\n}\n\nexport interface SerializeCollectionOptions {\n /**\n * One of the entity's named views: only the records it keeps, in its order,\n * without the members it omits.\n */\n readonly view?: string;\n}\n\n/**\n * Serializes many instances; with a `view`, publishes them in that view's\n * shape — the one place a published shape is applied, so every adapter that\n * publishes records writes the same thing.\n */\nexport function serializeEntityCollection<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n instances: readonly TEntity[],\n { view: viewName }: SerializeCollectionOptions = {},\n): SerializedEntity[] {\n if (viewName === undefined) {\n return instances.map(instance =>\n serializeEntity(entityConstructor, instance),\n );\n }\n const view = extractEntityView(entityConstructor, viewName);\n const omitted = new Set(view.omitted.map(descriptor => descriptor.key));\n const kept = applySorting(\n applyFiltering(\n instances,\n view.filtering as readonly EntityFiltering<TEntity>[] | undefined,\n ),\n view.sorting as readonly EntitySorting<TEntity>[] | undefined,\n );\n return kept.map(instance =>\n Object.fromEntries(\n Object.entries(serializeEntity(entityConstructor, instance)).filter(\n ([key]) => !omitted.has(key),\n ),\n ),\n );\n}\n"],"names":["applyFiltering","applySorting","extractEntityView","extractMetaAccessors","EntityCollectionLink","EntityLink","serializeLink","link","isLoaded","value","undefined","serializeEntity","entityConstructor","id","serializeComposition","rows","childType","map","row","serializeCollectionLink","values","ids","length","instance","result","filter","metaAccessor","hidden","readonly","kind","forEach","propertyName","alias","name","serialized","type","Array","isArray","serializeEntityCollection","instances","view","viewName","omitted","Set","descriptor","key","kept","filtering","sorting","Object","fromEntries","entries","has"],"mappings":"AAAA,SAASA,cAAc,EAAEC,YAAY,QAAQ,iCAAiC;AAI9E,SAASC,iBAAiB,QAAQ,6BAA6B;AAC/D,SAASC,oBAAoB,QAAQ,sBAAsB;AAC3D,SAASC,oBAAoB,QAAQ,2CAA2C;AAChF,SAASC,UAAU,QAAQ,gCAAgC;AAoB3D,SAASC,cACPC,IAAyB;QAKlBA;IAHP,IAAIA,KAAKC,QAAQ,IAAID,KAAKE,KAAK,KAAKC,WAAW;QAC7C,OAAOC,gBAAgBJ,KAAKK,iBAAiB,EAAEL,KAAKE,KAAK;IAC3D;IACA,QAAOF,WAAAA,KAAKM,EAAE,YAAPN,WAAWG;AACpB;AAEA;;;;;;;;;;;CAWC,GACD,SAASI,qBACPC,IAAwB,EACxBC,SAA2B;IAE3B,OAAOD,KAAKE,GAAG,CAACC,CAAAA,MACdP,gBAAgBK,WAAwCE;AAE5D;AAEA,SAASC,wBACPZ,IAAmC;IAEnC,IAAIA,KAAKC,QAAQ,IAAID,KAAKa,MAAM,KAAKV,WAAW;QAC9C,OAAOH,KAAKa,MAAM,CAACH,GAAG,CAACR,CAAAA,QACrBE,gBAAgBJ,KAAKK,iBAAiB,EAAEH;IAE5C;IACA,MAAMY,MAAMd,KAAKc,GAAG;IACpB,OAAOA,IAAIC,MAAM,GAAG,IAAID,MAAMX;AAChC;AAEA;;;;;CAKC,GACD,OAAO,SAASC,gBACdC,iBAA6C,EAC7CW,QAAiB;IAEjB,MAAMC,SAA2B,CAAC;IAElCrB,qBAAqBS,mBAClBa,MAAM,CACLC,CAAAA,eACE,CAACA,aAAaC,MAAM,IACpB,CAACD,aAAaE,QAAQ,IACtBF,aAAaG,IAAI,KAAK,UAEzBC,OAAO,CAACJ,CAAAA;YACcA;QAArB,MAAMK,gBAAeL,sBAAAA,aAAaM,KAAK,YAAlBN,sBAAsBA,aAAaO,IAAI;QAC5D,8DAA8D;QAC9D,MAAMxB,QAAQ,AAACc,QAAgB,CAACG,aAAaO,IAAI,CAAC;QAElD,IAAIC;QACJ,IAAIzB,iBAAiBJ,YAAY;YAC/B6B,aAAa5B,cAAcG;QAC7B,OAAO,IAAIA,iBAAiBL,sBAAsB;YAChD8B,aAAaf,wBAAwBV;QACvC,OAAO,IACLiB,aAAaS,IAAI,KAAK,iBACtBT,aAAaV,SAAS,KAAKN,aAC3B0B,MAAMC,OAAO,CAAC5B,QACd;YACAyB,aAAapB,qBAAqBL,OAAOiB,aAAaV,SAAS;QACjE,OAAO;YACLkB,aAAazB;QACf;QAEA,IAAIyB,eAAexB,WAAW;YAC5Bc,MAAM,CAACO,aAAuB,GAAGG;QACnC;IACF;IAEF,OAAOV;AACT;AAUA;;;;CAIC,GACD,OAAO,SAASc,0BACd1B,iBAA6C,EAC7C2B,SAA6B,EAC7B,EAAEC,MAAMC,QAAQ,EAA8B,GAAG,CAAC,CAAC;IAEnD,IAAIA,aAAa/B,WAAW;QAC1B,OAAO6B,UAAUtB,GAAG,CAACM,CAAAA,WACnBZ,gBAAgBC,mBAAmBW;IAEvC;IACA,MAAMiB,OAAOtC,kBAAkBU,mBAAmB6B;IAClD,MAAMC,UAAU,IAAIC,IAAIH,KAAKE,OAAO,CAACzB,GAAG,CAAC2B,CAAAA,aAAcA,WAAWC,GAAG;IACrE,MAAMC,OAAO7C,aACXD,eACEuC,WACAC,KAAKO,SAAS,GAEhBP,KAAKQ,OAAO;IAEd,OAAOF,KAAK7B,GAAG,CAACM,CAAAA,WACd0B,OAAOC,WAAW,CAChBD,OAAOE,OAAO,CAACxC,gBAAgBC,mBAAmBW,WAAWE,MAAM,CACjE,CAAC,CAACoB,IAAI,GAAK,CAACH,QAAQU,GAAG,CAACP;AAIhC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/entity-definition/validate/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/entity-definition/validate/index.ts"],"sourcesContent":["export * from './validate-records.js';\n"],"names":[],"mappings":"AAAA,cAAc,wBAAwB"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { EntifixBuildError } from '../../base-entities/entifix-error/index.js';
|
|
2
|
+
import type { Entity, EntityConstructor, EntityId } from '../../types/Entity.js';
|
|
3
|
+
/**
|
|
4
|
+
* Records checked against their entity's metadata before anything serves them.
|
|
5
|
+
*
|
|
6
|
+
* The mapping does not do this, on purpose: `deserializeSingleEntity` assigns
|
|
7
|
+
* every non-relation value raw — it converts no date, enforces no `required`,
|
|
8
|
+
* validates no enum value and resolves no link id — because a record off a
|
|
9
|
+
* database was written through that same mapping and needs none of it. Records
|
|
10
|
+
* that arrive as **untyped JSON** — files bundled with a static build, an
|
|
11
|
+
* import, a seed — have had no such guarantee, and a wrong one reaches a reader
|
|
12
|
+
* as `undefined` in the middle of a page, or as a string where a `Date` was
|
|
13
|
+
* expected. This is the check those records go through.
|
|
14
|
+
*
|
|
15
|
+
* It sits in core, beside the deserializer it makes up for, so every adapter
|
|
16
|
+
* that reads untyped records shares it — and the generated form judges a
|
|
17
|
+
* draft by the same metadata, so a record a form refuses is one this refuses.
|
|
18
|
+
*
|
|
19
|
+
* Every problem is collected rather than the first thrown, each with the path
|
|
20
|
+
* to its value: `employment.json › acme › role is required, and is missing`.
|
|
21
|
+
*/
|
|
22
|
+
export interface RecordProblem {
|
|
23
|
+
/** `<source> › <id> › <member>`, as far as each is known. */
|
|
24
|
+
readonly path: string;
|
|
25
|
+
readonly message: string;
|
|
26
|
+
}
|
|
27
|
+
/** Thrown by {@link validRecordsOrThrow}. Carries every problem found. */
|
|
28
|
+
export declare class RecordValidationError extends EntifixBuildError {
|
|
29
|
+
readonly problems: readonly RecordProblem[];
|
|
30
|
+
constructor(problems: readonly RecordProblem[]);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A check one entity needs that metadata cannot express — at most one contact
|
|
34
|
+
* channel per type, say. Handed every record (dates already parsed) and a way
|
|
35
|
+
* to report against one of them by index.
|
|
36
|
+
*/
|
|
37
|
+
export type EntityRecordRule = (records: ReadonlyArray<Readonly<Record<string, unknown>>>, report: (index: number, member: string | undefined, message: string) => void) => void;
|
|
38
|
+
export interface ValidateRecordsOptions {
|
|
39
|
+
/** Where the records came from, for the path in a message. */
|
|
40
|
+
readonly source?: string;
|
|
41
|
+
/**
|
|
42
|
+
* The ids that exist for each `link` or `linkCollection` member, by member
|
|
43
|
+
* name. A member left out is checked for shape only.
|
|
44
|
+
*/
|
|
45
|
+
readonly linkTargets?: Readonly<Record<string, ReadonlySet<EntityId>>>;
|
|
46
|
+
/** Checks of the caller's own, run after the metadata's. */
|
|
47
|
+
readonly rules?: readonly EntityRecordRule[];
|
|
48
|
+
/**
|
|
49
|
+
* Every locale a `localized` member must carry. Defaults to the locales the
|
|
50
|
+
* host declared through `defineLocales`.
|
|
51
|
+
*/
|
|
52
|
+
readonly locales?: readonly string[];
|
|
53
|
+
/**
|
|
54
|
+
* The view the records were published in. A member it omits is not
|
|
55
|
+
* required of them, since the view left it out on purpose.
|
|
56
|
+
*/
|
|
57
|
+
readonly view?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface ValidateRecordsResult {
|
|
60
|
+
readonly problems: readonly RecordProblem[];
|
|
61
|
+
/**
|
|
62
|
+
* The records with every `date` member parsed into a `Date`, ready for the
|
|
63
|
+
* mapping. Meaningless when `problems` is not empty.
|
|
64
|
+
*/
|
|
65
|
+
readonly records: ReadonlyArray<Record<string, unknown>>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Checks every record of one entity against its metadata — identity, `required`,
|
|
69
|
+
* each member's type, link ids, item counts, `notBefore`, and the entity's
|
|
70
|
+
* cardinality — then runs the caller's rules.
|
|
71
|
+
*/
|
|
72
|
+
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 a {@link RecordValidationError}. */
|
|
74
|
+
export declare function validRecordsOrThrow<TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, records: readonly unknown[], options?: ValidateRecordsOptions): ReadonlyArray<Record<string, unknown>>;
|
|
75
|
+
//# sourceMappingURL=validate-records.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-records.d.ts","sourceRoot":"","sources":["../../../src/entity-definition/validate/validate-records.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAC;AAE/E,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,0EAA0E;AAC1E,qBAAa,qBAAsB,SAAQ,iBAAiB;IAC1D,QAAQ,CAAC,QAAQ,EAAE,SAAS,aAAa,EAAE,CAAC;gBAEhC,QAAQ,EAAE,SAAS,aAAa,EAAE;CAU/C;AAED;;;;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,+EAA+E;AAC/E,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"}
|