@entifix/core 0.2.0 → 0.4.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.
Files changed (83) hide show
  1. package/dist/entity-definition/describe/describe-child-columns.d.ts +3 -0
  2. package/dist/entity-definition/describe/describe-child-columns.d.ts.map +1 -1
  3. package/dist/entity-definition/describe/describe-child-columns.js +11 -0
  4. package/dist/entity-definition/describe/describe-child-columns.js.map +1 -1
  5. package/dist/entity-definition/describe/describe-entity-columns.d.ts +7 -1
  6. package/dist/entity-definition/describe/describe-entity-columns.d.ts.map +1 -1
  7. package/dist/entity-definition/describe/describe-entity-columns.js +41 -4
  8. package/dist/entity-definition/describe/describe-entity-columns.js.map +1 -1
  9. package/dist/entity-definition/describe/entity-view.d.ts +19 -0
  10. package/dist/entity-definition/describe/entity-view.d.ts.map +1 -0
  11. package/dist/entity-definition/describe/entity-view.js +35 -0
  12. package/dist/entity-definition/describe/entity-view.js.map +1 -0
  13. package/dist/entity-definition/describe/index.d.ts +2 -0
  14. package/dist/entity-definition/describe/index.d.ts.map +1 -1
  15. package/dist/entity-definition/describe/index.js +2 -0
  16. package/dist/entity-definition/describe/index.js.map +1 -1
  17. package/dist/entity-definition/describe/query-property.d.ts +17 -0
  18. package/dist/entity-definition/describe/query-property.d.ts.map +1 -0
  19. package/dist/entity-definition/describe/query-property.js +18 -0
  20. package/dist/entity-definition/describe/query-property.js.map +1 -0
  21. package/dist/entity-definition/meta-entities/bounds.d.ts +10 -0
  22. package/dist/entity-definition/meta-entities/bounds.d.ts.map +1 -0
  23. package/dist/entity-definition/meta-entities/bounds.js +17 -0
  24. package/dist/entity-definition/meta-entities/bounds.js.map +1 -0
  25. package/dist/entity-definition/meta-entities/meta-accessor/meta-accessor.d.ts +24 -1
  26. package/dist/entity-definition/meta-entities/meta-accessor/meta-accessor.d.ts.map +1 -1
  27. package/dist/entity-definition/meta-entities/meta-accessor/meta-accessor.js +8 -1
  28. package/dist/entity-definition/meta-entities/meta-accessor/meta-accessor.js.map +1 -1
  29. package/dist/entity-definition/meta-entities/meta-entity/meta-entity.d.ts +45 -0
  30. package/dist/entity-definition/meta-entities/meta-entity/meta-entity.d.ts.map +1 -1
  31. package/dist/entity-definition/meta-entities/meta-entity/meta-entity.js +4 -0
  32. package/dist/entity-definition/meta-entities/meta-entity/meta-entity.js.map +1 -1
  33. package/dist/entity-definition/reconstruct/coerce-field-value.d.ts +7 -0
  34. package/dist/entity-definition/reconstruct/coerce-field-value.d.ts.map +1 -1
  35. package/dist/entity-definition/reconstruct/coerce-field-value.js +9 -0
  36. package/dist/entity-definition/reconstruct/coerce-field-value.js.map +1 -1
  37. package/dist/entity-definition/reconstruct/reconstruct-entity.d.ts.map +1 -1
  38. package/dist/entity-definition/reconstruct/reconstruct-entity.js +8 -2
  39. package/dist/entity-definition/reconstruct/reconstruct-entity.js.map +1 -1
  40. package/dist/entity-definition/serializer/serialize.d.ts +13 -1
  41. package/dist/entity-definition/serializer/serialize.d.ts.map +1 -1
  42. package/dist/entity-definition/serializer/serialize.js +14 -2
  43. package/dist/entity-definition/serializer/serialize.js.map +1 -1
  44. package/dist/entity-definition/validate/index.d.ts +2 -0
  45. package/dist/entity-definition/validate/index.d.ts.map +1 -0
  46. package/dist/entity-definition/validate/index.js +3 -0
  47. package/dist/entity-definition/validate/index.js.map +1 -0
  48. package/dist/entity-definition/validate/validate-records.d.ts +75 -0
  49. package/dist/entity-definition/validate/validate-records.d.ts.map +1 -0
  50. package/dist/entity-definition/validate/validate-records.js +205 -0
  51. package/dist/entity-definition/validate/validate-records.js.map +1 -0
  52. package/dist/i18n/locales.d.ts +46 -7
  53. package/dist/i18n/locales.d.ts.map +1 -1
  54. package/dist/i18n/locales.js +49 -10
  55. package/dist/i18n/locales.js.map +1 -1
  56. package/dist/i18n/localized-text.d.ts +27 -0
  57. package/dist/i18n/localized-text.d.ts.map +1 -0
  58. package/dist/i18n/localized-text.js +18 -0
  59. package/dist/i18n/localized-text.js.map +1 -0
  60. package/dist/i18n/negotiate.d.ts +1 -1
  61. package/dist/i18n/negotiate.d.ts.map +1 -1
  62. package/dist/i18n/negotiate.js +3 -3
  63. package/dist/i18n/negotiate.js.map +1 -1
  64. package/dist/index.d.ts +3 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +3 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/query/in-memory-query.d.ts +49 -0
  69. package/dist/query/in-memory-query.d.ts.map +1 -0
  70. package/dist/query/in-memory-query.js +133 -0
  71. package/dist/query/in-memory-query.js.map +1 -0
  72. package/dist/rsql/coerce-rsql.d.ts +21 -0
  73. package/dist/rsql/coerce-rsql.d.ts.map +1 -1
  74. package/dist/rsql/coerce-rsql.js +44 -8
  75. package/dist/rsql/coerce-rsql.js.map +1 -1
  76. package/dist/rsql/sort-param.d.ts.map +1 -1
  77. package/dist/rsql/sort-param.js +3 -9
  78. package/dist/rsql/sort-param.js.map +1 -1
  79. package/dist/types/EntityDraft.d.ts +18 -2
  80. package/dist/types/EntityDraft.d.ts.map +1 -1
  81. package/dist/types/EntityDraft.js +11 -0
  82. package/dist/types/EntityDraft.js.map +1 -1
  83. package/package.json +1 -2
@@ -0,0 +1,49 @@
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 {@link EntityLoadRequest}'s filtering and sorting, answered over records
6
+ * held in memory, with the semantics of the Mongo adapter.
7
+ *
8
+ * One implementation for everything that queries records it holds itself — the
9
+ * in-memory test double, the static repository, a published view — so none of
10
+ * them can drift into a more forgiving version of the others. They used to be
11
+ * separate copies, and one of them compared an array member with `===` and so
12
+ * never matched `in` (#34).
13
+ *
14
+ * The rules, each Mongo's:
15
+ * - a link is its target's id, a collection link the list of their ids;
16
+ * - `in` and `nin` against an array match when **any element** does, as `$in`;
17
+ * - a property may be a dotted path into a member holding an object, which is
18
+ * how a filter reaches one locale of a `localized` member (`name.en`);
19
+ * - `like` is a case-insensitive substring match, as `$regex` with `i`;
20
+ * - a missing value sorts below a present one, numbers and dates compare as
21
+ * themselves and anything else as text;
22
+ * - every top-level filtering entry is combined with `and`.
23
+ */
24
+ /**
25
+ * A member as the Mongo adapter stores it, and so as its queries see it: a
26
+ * link is its target's id, a collection link the list of their ids. Without
27
+ * this no operator can compare a link, since `eq` against an id is never
28
+ * `true` for an object.
29
+ */
30
+ export declare function storedValue(value: unknown): unknown;
31
+ /**
32
+ * The value a property names: a member, or a dotted path into one. A path
33
+ * through something that is not an object ends in `undefined`, as a missing
34
+ * field does in Mongo.
35
+ */
36
+ export declare function valueAt(record: object, property: string): unknown;
37
+ /** Ordering with Mongo's rules; `0` for two values it considers equal. */
38
+ export declare function compareStoredValues(left: unknown, right: unknown): number;
39
+ /** Whether one record matches a request's filtering. */
40
+ export declare function matchesFiltering<TEntity extends Entity>(record: TEntity, filtering: readonly EntityFiltering<TEntity>[] | undefined): boolean;
41
+ /** The records a request's filtering keeps, in the order they were given. */
42
+ export declare function applyFiltering<TEntity extends Entity>(records: readonly TEntity[], filtering: readonly EntityFiltering<TEntity>[] | undefined): TEntity[];
43
+ /**
44
+ * The records in the order a request asks for, as a new array. `EntitySorting`
45
+ * is a record keyed by priority rather than a list, so two keys cannot tie by
46
+ * accident. No key leaves the order the records were given in.
47
+ */
48
+ export declare function applySorting<TEntity extends Entity>(records: readonly TEntity[], sorting: readonly EntitySorting<TEntity>[] | undefined): TEntity[];
49
+ //# sourceMappingURL=in-memory-query.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"in-memory-query.d.ts","sourceRoot":"","sources":["../../src/query/in-memory-query.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,KAAK,EAEV,eAAe,EAEhB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAInD;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CASjE;AAED,0EAA0E;AAC1E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,CAYzE;AAuFD,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,MAAM,EACrD,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,SAAS,eAAe,CAAC,OAAO,CAAC,EAAE,GAAG,SAAS,GACzD,OAAO,CAMT;AAED,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,OAAO,SAAS,MAAM,EACnD,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,SAAS,EAAE,SAAS,eAAe,CAAC,OAAO,CAAC,EAAE,GAAG,SAAS,GACzD,OAAO,EAAE,CAEX;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,SAAS,MAAM,EACjD,OAAO,EAAE,SAAS,OAAO,EAAE,EAC3B,OAAO,EAAE,SAAS,aAAa,CAAC,OAAO,CAAC,EAAE,GAAG,SAAS,GACrD,OAAO,EAAE,CAqBX"}
@@ -0,0 +1,133 @@
1
+ import { EntifixBuildError } from '../base-entities/entifix-error/index.js';
2
+ import { EntityCollectionLink } from '../entity-definition/links/entity-collection-link/index.js';
3
+ import { EntityLink } from '../entity-definition/links/entity-link/index.js';
4
+ /**
5
+ * An {@link EntityLoadRequest}'s filtering and sorting, answered over records
6
+ * held in memory, with the semantics of the Mongo adapter.
7
+ *
8
+ * One implementation for everything that queries records it holds itself — the
9
+ * in-memory test double, the static repository, a published view — so none of
10
+ * them can drift into a more forgiving version of the others. They used to be
11
+ * separate copies, and one of them compared an array member with `===` and so
12
+ * never matched `in` (#34).
13
+ *
14
+ * The rules, each Mongo's:
15
+ * - a link is its target's id, a collection link the list of their ids;
16
+ * - `in` and `nin` against an array match when **any element** does, as `$in`;
17
+ * - a property may be a dotted path into a member holding an object, which is
18
+ * how a filter reaches one locale of a `localized` member (`name.en`);
19
+ * - `like` is a case-insensitive substring match, as `$regex` with `i`;
20
+ * - a missing value sorts below a present one, numbers and dates compare as
21
+ * themselves and anything else as text;
22
+ * - every top-level filtering entry is combined with `and`.
23
+ */ /**
24
+ * A member as the Mongo adapter stores it, and so as its queries see it: a
25
+ * link is its target's id, a collection link the list of their ids. Without
26
+ * this no operator can compare a link, since `eq` against an id is never
27
+ * `true` for an object.
28
+ */ export function storedValue(value) {
29
+ if (value instanceof EntityLink) return value.id;
30
+ if (value instanceof EntityCollectionLink) return value.ids;
31
+ return value;
32
+ }
33
+ /**
34
+ * The value a property names: a member, or a dotted path into one. A path
35
+ * through something that is not an object ends in `undefined`, as a missing
36
+ * field does in Mongo.
37
+ */ export function valueAt(record, property) {
38
+ const [member, ...path] = property.split('.');
39
+ return path.reduce((value, key)=>typeof value === 'object' && value !== null ? value[key] : undefined, storedValue(record[member]));
40
+ }
41
+ /** Ordering with Mongo's rules; `0` for two values it considers equal. */ export function compareStoredValues(left, right) {
42
+ if (left === right) return 0;
43
+ // Mongo sorts missing values below present ones.
44
+ if (left == null) return right == null ? 0 : -1;
45
+ if (right == null) return 1;
46
+ if (typeof left === 'number' && typeof right === 'number') {
47
+ return left - right;
48
+ }
49
+ if (left instanceof Date && right instanceof Date) {
50
+ return left.getTime() - right.getTime();
51
+ }
52
+ return String(left).localeCompare(String(right));
53
+ }
54
+ /** Equality by value, so two `Date`s naming one instant are equal. */ const isEqual = (actual, value)=>actual instanceof Date && value instanceof Date ? actual.getTime() === value.getTime() : actual === value;
55
+ /** `$in` semantics: a scalar matches a value, an array when any element does. */ const matchesAny = (actual, values)=>{
56
+ const matches = (element)=>values.some((value)=>isEqual(element, value));
57
+ return Array.isArray(actual) ? actual.some(matches) : matches(actual);
58
+ };
59
+ /** Escapes a string so `like`/`nlike` match literally, as the Mongo adapter does. */ const escapeRegex = (value)=>value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
60
+ const matchesLike = (actual, value)=>actual != null && new RegExp(escapeRegex(value), 'i').test(String(actual));
61
+ const isFilterGroup = (node)=>!('property' in node) && (node.operator === 'and' || node.operator === 'or');
62
+ const isBetween = (actual, start, end)=>actual != null && compareStoredValues(actual, start) >= 0 && compareStoredValues(actual, end) <= 0;
63
+ function matchesFilter(record, filter) {
64
+ const actual = valueAt(record, String(filter.property));
65
+ switch(filter.operator){
66
+ case 'eq':
67
+ return isEqual(actual, filter.value);
68
+ case 'ne':
69
+ return !isEqual(actual, filter.value);
70
+ case 'gt':
71
+ return actual != null && compareStoredValues(actual, filter.value) > 0;
72
+ case 'gte':
73
+ return actual != null && compareStoredValues(actual, filter.value) >= 0;
74
+ case 'lt':
75
+ return actual != null && compareStoredValues(actual, filter.value) < 0;
76
+ case 'lte':
77
+ return actual != null && compareStoredValues(actual, filter.value) <= 0;
78
+ case 'in':
79
+ return matchesAny(actual, filter.values);
80
+ case 'nin':
81
+ return !matchesAny(actual, filter.values);
82
+ case 'between':
83
+ return isBetween(actual, filter.start, filter.end);
84
+ case 'nbetween':
85
+ return !isBetween(actual, filter.start, filter.end);
86
+ case 'like':
87
+ return matchesLike(actual, filter.value);
88
+ case 'nlike':
89
+ return !matchesLike(actual, filter.value);
90
+ case 'isNull':
91
+ return actual == null;
92
+ case 'isNotNull':
93
+ return actual != null;
94
+ default:
95
+ {
96
+ // Exhaustiveness guard — a new operator must be handled explicitly.
97
+ const never = filter;
98
+ throw new EntifixBuildError(`Unknown filter operator "${String(never.operator)}"`);
99
+ }
100
+ }
101
+ }
102
+ function matchesNode(record, node) {
103
+ if (!isFilterGroup(node)) return matchesFilter(record, node);
104
+ return node.operator === 'or' ? node.values.some((child)=>matchesNode(record, child)) : node.values.every((child)=>matchesNode(record, child));
105
+ }
106
+ /** Whether one record matches a request's filtering. */ export function matchesFiltering(record, filtering) {
107
+ return (filtering != null ? filtering : []).every((entry)=>Array.isArray(entry) ? entry.every((child)=>matchesNode(record, child)) : matchesNode(record, entry));
108
+ }
109
+ /** The records a request's filtering keeps, in the order they were given. */ export function applyFiltering(records, filtering) {
110
+ return records.filter((record)=>matchesFiltering(record, filtering));
111
+ }
112
+ /**
113
+ * The records in the order a request asks for, as a new array. `EntitySorting`
114
+ * is a record keyed by priority rather than a list, so two keys cannot tie by
115
+ * accident. No key leaves the order the records were given in.
116
+ */ export function applySorting(records, sorting) {
117
+ const keys = (sorting != null ? sorting : []).flatMap((entry)=>Object.keys(entry).map(Number).sort((left, right)=>left - right).map((priority)=>entry[priority]).filter((key)=>key != null));
118
+ if (keys.length === 0) return [
119
+ ...records
120
+ ];
121
+ return [
122
+ ...records
123
+ ].sort((left, right)=>{
124
+ for (const key of keys){
125
+ const property = String(key.property);
126
+ const compared = compareStoredValues(valueAt(left, property), valueAt(right, property));
127
+ if (compared !== 0) return key.type === 'desc' ? -compared : compared;
128
+ }
129
+ return 0;
130
+ });
131
+ }
132
+
133
+ //# sourceMappingURL=in-memory-query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/query/in-memory-query.ts"],"sourcesContent":["import { EntifixBuildError } from '../base-entities/entifix-error/index.js';\nimport { EntityCollectionLink } from '../entity-definition/links/entity-collection-link/index.js';\nimport { EntityLink } from '../entity-definition/links/entity-link/index.js';\nimport type { Entity } from '../types/Entity.js';\nimport type {\n EntityFilter,\n EntityFiltering,\n FilterGroup,\n} from '../types/EntityFiltering.js';\nimport type { EntitySorting } from '../types/EntitySorting.js';\n\n/**\n * An {@link EntityLoadRequest}'s filtering and sorting, answered over records\n * held in memory, with the semantics of the Mongo adapter.\n *\n * One implementation for everything that queries records it holds itself — the\n * in-memory test double, the static repository, a published view — so none of\n * them can drift into a more forgiving version of the others. They used to be\n * separate copies, and one of them compared an array member with `===` and so\n * never matched `in` (#34).\n *\n * The rules, each Mongo's:\n * - a link is its target's id, a collection link the list of their ids;\n * - `in` and `nin` against an array match when **any element** does, as `$in`;\n * - a property may be a dotted path into a member holding an object, which is\n * how a filter reaches one locale of a `localized` member (`name.en`);\n * - `like` is a case-insensitive substring match, as `$regex` with `i`;\n * - a missing value sorts below a present one, numbers and dates compare as\n * themselves and anything else as text;\n * - every top-level filtering entry is combined with `and`.\n */\n\n/**\n * A member as the Mongo adapter stores it, and so as its queries see it: a\n * link is its target's id, a collection link the list of their ids. Without\n * this no operator can compare a link, since `eq` against an id is never\n * `true` for an object.\n */\nexport function storedValue(value: unknown): unknown {\n if (value instanceof EntityLink) return value.id;\n if (value instanceof EntityCollectionLink) return value.ids;\n return value;\n}\n\n/**\n * The value a property names: a member, or a dotted path into one. A path\n * through something that is not an object ends in `undefined`, as a missing\n * field does in Mongo.\n */\nexport function valueAt(record: object, property: string): unknown {\n const [member, ...path] = property.split('.');\n return path.reduce<unknown>(\n (value, key) =>\n typeof value === 'object' && value !== null\n ? (value as Record<string, unknown>)[key]\n : undefined,\n storedValue((record as Record<string, unknown>)[member as string]),\n );\n}\n\n/** Ordering with Mongo's rules; `0` for two values it considers equal. */\nexport function compareStoredValues(left: unknown, right: unknown): number {\n if (left === right) return 0;\n // Mongo sorts missing values below present ones.\n if (left == null) return right == null ? 0 : -1;\n if (right == null) return 1;\n if (typeof left === 'number' && typeof right === 'number') {\n return left - right;\n }\n if (left instanceof Date && right instanceof Date) {\n return left.getTime() - right.getTime();\n }\n return String(left).localeCompare(String(right));\n}\n\n/** Equality by value, so two `Date`s naming one instant are equal. */\nconst isEqual = (actual: unknown, value: unknown): boolean =>\n actual instanceof Date && value instanceof Date\n ? actual.getTime() === value.getTime()\n : actual === value;\n\n/** `$in` semantics: a scalar matches a value, an array when any element does. */\nconst matchesAny = (actual: unknown, values: readonly unknown[]): boolean => {\n const matches = (element: unknown) =>\n values.some(value => isEqual(element, value));\n return Array.isArray(actual) ? actual.some(matches) : matches(actual);\n};\n\n/** Escapes a string so `like`/`nlike` match literally, as the Mongo adapter does. */\nconst escapeRegex = (value: string): string =>\n value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\nconst matchesLike = (actual: unknown, value: string): boolean =>\n actual != null && new RegExp(escapeRegex(value), 'i').test(String(actual));\n\nconst isFilterGroup = <TEntity extends Entity>(\n node: EntityFilter<TEntity> | FilterGroup<TEntity>,\n): node is FilterGroup<TEntity> =>\n !('property' in node) && (node.operator === 'and' || node.operator === 'or');\n\nconst isBetween = (actual: unknown, start: unknown, end: unknown) =>\n actual != null &&\n compareStoredValues(actual, start) >= 0 &&\n compareStoredValues(actual, end) <= 0;\n\nfunction matchesFilter<TEntity extends Entity>(\n record: TEntity,\n filter: EntityFilter<TEntity>,\n): boolean {\n const actual = valueAt(record, String(filter.property));\n\n switch (filter.operator) {\n case 'eq':\n return isEqual(actual, filter.value);\n case 'ne':\n return !isEqual(actual, filter.value);\n case 'gt':\n return actual != null && compareStoredValues(actual, filter.value) > 0;\n case 'gte':\n return actual != null && compareStoredValues(actual, filter.value) >= 0;\n case 'lt':\n return actual != null && compareStoredValues(actual, filter.value) < 0;\n case 'lte':\n return actual != null && compareStoredValues(actual, filter.value) <= 0;\n case 'in':\n return matchesAny(actual, filter.values);\n case 'nin':\n return !matchesAny(actual, filter.values);\n case 'between':\n return isBetween(actual, filter.start, filter.end);\n case 'nbetween':\n return !isBetween(actual, filter.start, filter.end);\n case 'like':\n return matchesLike(actual, filter.value);\n case 'nlike':\n return !matchesLike(actual, filter.value);\n case 'isNull':\n return actual == null;\n case 'isNotNull':\n return actual != null;\n default: {\n // Exhaustiveness guard — a new operator must be handled explicitly.\n const never: never = filter;\n throw new EntifixBuildError(\n `Unknown filter operator \"${String((never as { operator: unknown }).operator)}\"`,\n );\n }\n }\n}\n\nfunction matchesNode<TEntity extends Entity>(\n record: TEntity,\n node: EntityFilter<TEntity> | FilterGroup<TEntity>,\n): boolean {\n if (!isFilterGroup(node)) return matchesFilter(record, node);\n return node.operator === 'or'\n ? node.values.some(child => matchesNode(record, child))\n : node.values.every(child => matchesNode(record, child));\n}\n\n/** Whether one record matches a request's filtering. */\nexport function matchesFiltering<TEntity extends Entity>(\n record: TEntity,\n filtering: readonly EntityFiltering<TEntity>[] | undefined,\n): boolean {\n return (filtering ?? []).every(entry =>\n Array.isArray(entry)\n ? entry.every(child => matchesNode(record, child))\n : matchesNode(record, entry),\n );\n}\n\n/** The records a request's filtering keeps, in the order they were given. */\nexport function applyFiltering<TEntity extends Entity>(\n records: readonly TEntity[],\n filtering: readonly EntityFiltering<TEntity>[] | undefined,\n): TEntity[] {\n return records.filter(record => matchesFiltering(record, filtering));\n}\n\n/**\n * The records in the order a request asks for, as a new array. `EntitySorting`\n * is a record keyed by priority rather than a list, so two keys cannot tie by\n * accident. No key leaves the order the records were given in.\n */\nexport function applySorting<TEntity extends Entity>(\n records: readonly TEntity[],\n sorting: readonly EntitySorting<TEntity>[] | undefined,\n): TEntity[] {\n const keys = (sorting ?? []).flatMap(entry =>\n Object.keys(entry)\n .map(Number)\n .sort((left, right) => left - right)\n .map(priority => entry[priority])\n .filter(key => key != null),\n );\n if (keys.length === 0) return [...records];\n\n return [...records].sort((left, right) => {\n for (const key of keys) {\n const property = String(key.property);\n const compared = compareStoredValues(\n valueAt(left, property),\n valueAt(right, property),\n );\n if (compared !== 0) return key.type === 'desc' ? -compared : compared;\n }\n return 0;\n });\n}\n"],"names":["EntifixBuildError","EntityCollectionLink","EntityLink","storedValue","value","id","ids","valueAt","record","property","member","path","split","reduce","key","undefined","compareStoredValues","left","right","Date","getTime","String","localeCompare","isEqual","actual","matchesAny","values","matches","element","some","Array","isArray","escapeRegex","replace","matchesLike","RegExp","test","isFilterGroup","node","operator","isBetween","start","end","matchesFilter","filter","never","matchesNode","child","every","matchesFiltering","filtering","entry","applyFiltering","records","applySorting","sorting","keys","flatMap","Object","map","Number","sort","priority","length","compared","type"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { type EntityFieldDescriptor } from '../entity-definition/describe/index.js';
2
+ import { type Locale } from '../i18n/locales.js';
2
3
  import type { Entity, EntityConstructor } from '../types/Entity.js';
3
4
  import type { FilterGroup } from '../types/EntityFiltering.js';
4
5
  import { type RawFilterGroup } from './parse-rsql.js';
@@ -12,6 +13,26 @@ import { type RawFilterGroup } from './parse-rsql.js';
12
13
  * on a `hidden` member or an unindexed one.
13
14
  */
14
15
  export declare function allowedDescriptors<TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, select: (descriptor: EntityFieldDescriptor) => boolean): Map<string, EntityFieldDescriptor>;
16
+ /** A query property resolved against the allowlist. */
17
+ export interface AllowedProperty {
18
+ readonly descriptor: EntityFieldDescriptor;
19
+ /**
20
+ * What an adapter names: the member's wire key, or for a `localized` member
21
+ * the path to one locale's text (`title.en`).
22
+ */
23
+ readonly key: string;
24
+ /** The locale a `localized` member is queried in. */
25
+ readonly locale?: Locale;
26
+ }
27
+ /**
28
+ * Resolves the property a filter or a sort names, or rejects it.
29
+ *
30
+ * A `localized` member is the one that takes a path: it holds an object, and
31
+ * comparing an object as text matches nothing, so it is only ever queried
32
+ * through one locale — `title.en` — and that locale must be one the host
33
+ * declared. The bare member is rejected with a message that says so.
34
+ */
35
+ export declare function resolveAllowedProperty(allowed: Map<string, EntityFieldDescriptor>, property: string, requirement: 'filterable' | 'sortable'): AllowedProperty;
15
36
  /**
16
37
  * Turns a parsed, untyped RSQL tree into a typed {@link FilterGroup} the
17
38
  * repositories can execute, validating it against the entity's metadata on the
@@ -1 +1 @@
1
- {"version":3,"file":"coerce-rsql.d.ts","sourceRoot":"","sources":["../../src/rsql/coerce-rsql.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,wCAAwC,CAAC;AAEhD,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EAAgB,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,SAAS,MAAM,EACvD,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,EAAE,CAAC,UAAU,EAAE,qBAAqB,KAAK,OAAO,GACrD,GAAG,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAQpC;AA8FD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,OAAO,SAAS,MAAM,EACpD,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,EAAE,cAAc,GACrB,WAAW,CAAC,OAAO,CAAC,CAUtB"}
1
+ {"version":3,"file":"coerce-rsql.d.ts","sourceRoot":"","sources":["../../src/rsql/coerce-rsql.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,wCAAwC,CAAC;AAEhD,OAAO,EAAY,KAAK,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EAAgB,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,SAAS,MAAM,EACvD,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,EAAE,CAAC,UAAU,EAAE,qBAAqB,KAAK,OAAO,GACrD,GAAG,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAQpC;AAKD,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,UAAU,EAAE,qBAAqB,CAAC;IAC3C;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,qDAAqD;IACrD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,qBAAqB,CAAC,EAC3C,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,YAAY,GAAG,UAAU,GACrC,eAAe,CAuBjB;AA2FD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,OAAO,SAAS,MAAM,EACpD,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,EAAE,cAAc,GACrB,WAAW,CAAC,OAAO,CAAC,CAUtB"}
@@ -1,5 +1,6 @@
1
1
  import { EntifixBuildError } from '../base-entities/entifix-error/index.js';
2
2
  import { describeEntityColumns } from '../entity-definition/describe/index.js';
3
+ import { isLocale } from '../i18n/locales.js';
3
4
  import { isRawFilter } from './parse-rsql.js';
4
5
  /**
5
6
  * Resolves the members a client is allowed to name, keyed by both the accessor
@@ -19,6 +20,45 @@ import { isRawFilter } from './parse-rsql.js';
19
20
  return allowed;
20
21
  }
21
22
  const reject = (message, details)=>new EntifixBuildError(message, undefined, details);
23
+ /**
24
+ * Resolves the property a filter or a sort names, or rejects it.
25
+ *
26
+ * A `localized` member is the one that takes a path: it holds an object, and
27
+ * comparing an object as text matches nothing, so it is only ever queried
28
+ * through one locale — `title.en` — and that locale must be one the host
29
+ * declared. The bare member is rejected with a message that says so.
30
+ */ export function resolveAllowedProperty(allowed, property, requirement) {
31
+ const direct = allowed.get(property);
32
+ if (direct !== undefined) {
33
+ if (direct.type === 'localized') {
34
+ throw reject(`"${property}" is localized; name one locale, as "${property}.<locale>"`, {
35
+ property
36
+ });
37
+ }
38
+ return {
39
+ descriptor: direct,
40
+ key: direct.key
41
+ };
42
+ }
43
+ const dot = property.indexOf('.');
44
+ const descriptor = dot === -1 ? undefined : allowed.get(property.slice(0, dot));
45
+ if ((descriptor == null ? void 0 : descriptor.type) !== 'localized') {
46
+ throw reject(`"${property}" is not a ${requirement} member`, {
47
+ property
48
+ });
49
+ }
50
+ const locale = property.slice(dot + 1);
51
+ if (!isLocale(locale)) {
52
+ throw reject(`"${locale}" is not a declared locale`, {
53
+ property
54
+ });
55
+ }
56
+ return {
57
+ descriptor,
58
+ key: `${descriptor.key}.${locale}`,
59
+ locale
60
+ };
61
+ }
22
62
  /** Re-types one raw argument from the member's declared type. */ function coerceValue(raw, descriptor) {
23
63
  switch(descriptor.type){
24
64
  case 'number':
@@ -56,15 +96,11 @@ const reject = (message, details)=>new EntifixBuildError(message, undefined, det
56
96
  }
57
97
  }
58
98
  function coerceFilter(raw, allowed) {
59
- const descriptor = allowed.get(raw.property);
60
- if (!descriptor) {
61
- throw reject(`"${raw.property}" is not a filterable member`, {
62
- property: raw.property
63
- });
64
- }
99
+ const { descriptor, key } = resolveAllowedProperty(allowed, raw.property, 'filterable');
65
100
  // The wire key is also the stored field name, so this is what the Mongo
66
- // translator downstream will use.
67
- const property = descriptor.key;
101
+ // translator downstream will use — dotted into one locale for a localized
102
+ // member, which Mongo reads as a path natively.
103
+ const property = key;
68
104
  const coerce = (value)=>coerceValue(value, descriptor);
69
105
  if (raw.values) {
70
106
  return {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/rsql/coerce-rsql.ts"],"sourcesContent":["import { EntifixBuildError } from '../base-entities/entifix-error/index.js';\nimport {\n describeEntityColumns,\n type EntityFieldDescriptor,\n} from '../entity-definition/describe/index.js';\nimport type { MetaAccessorType } from '../entity-definition/meta-entities/meta-accessor/index.js';\nimport type { Entity, EntityConstructor } from '../types/Entity.js';\nimport type { EntityFilter, FilterGroup } from '../types/EntityFiltering.js';\nimport {\n isRawFilter,\n type RawFilter,\n type RawFilterGroup,\n} from './parse-rsql.js';\n\n/**\n * Resolves the members a client is allowed to name, keyed by both the accessor\n * name and its wire key so a URL may use either.\n *\n * `select` is what makes this an allowlist rather than a lookup: a member the\n * entity did not mark `filterable`/`sortable` is simply not in the map, and a\n * request naming it is rejected. That is the guard against a client filtering\n * on a `hidden` member or an unindexed one.\n */\nexport function allowedDescriptors<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n select: (descriptor: EntityFieldDescriptor) => boolean,\n): Map<string, EntityFieldDescriptor> {\n const allowed = new Map<string, EntityFieldDescriptor>();\n for (const descriptor of describeEntityColumns(entityConstructor)) {\n if (!select(descriptor)) continue;\n allowed.set(descriptor.name, descriptor);\n allowed.set(descriptor.key, descriptor);\n }\n return allowed;\n}\n\nconst reject = (message: string, details?: Record<string, unknown>) =>\n new EntifixBuildError(message, undefined, details);\n\n/** Re-types one raw argument from the member's declared type. */\nfunction coerceValue(raw: string, descriptor: EntityFieldDescriptor): unknown {\n switch (descriptor.type as MetaAccessorType) {\n case 'number': {\n const value = Number(raw);\n if (raw.trim() === '' || Number.isNaN(value)) {\n throw reject(`\"${raw}\" is not a number for \"${descriptor.name}\"`);\n }\n return value;\n }\n case 'boolean': {\n if (raw !== 'true' && raw !== 'false') {\n throw reject(`\"${raw}\" is not a boolean for \"${descriptor.name}\"`);\n }\n return raw === 'true';\n }\n case 'date': {\n const value = new Date(raw);\n if (Number.isNaN(value.getTime())) {\n throw reject(`\"${raw}\" is not a date for \"${descriptor.name}\"`);\n }\n return value;\n }\n case 'enum': {\n if (descriptor.enumValues && !descriptor.enumValues.includes(raw)) {\n throw reject(`\"${raw}\" is not a value of \"${descriptor.name}\"`);\n }\n return raw;\n }\n default:\n return raw;\n }\n}\n\nfunction coerceFilter<TEntity extends Entity>(\n raw: RawFilter,\n allowed: Map<string, EntityFieldDescriptor>,\n): EntityFilter<TEntity> {\n const descriptor = allowed.get(raw.property);\n if (!descriptor) {\n throw reject(`\"${raw.property}\" is not a filterable member`, {\n property: raw.property,\n });\n }\n\n // The wire key is also the stored field name, so this is what the Mongo\n // translator downstream will use.\n const property = descriptor.key as keyof TEntity;\n const coerce = (value: string) => coerceValue(value, descriptor);\n\n if (raw.values) {\n return {\n property,\n operator: raw.operator,\n values: raw.values.map(coerce),\n } as EntityFilter<TEntity>;\n }\n if (raw.start !== undefined && raw.end !== undefined) {\n return {\n property,\n operator: raw.operator,\n start: coerce(raw.start),\n end: coerce(raw.end),\n } as EntityFilter<TEntity>;\n }\n if (raw.value !== undefined) {\n return {\n property,\n operator: raw.operator,\n value: coerce(raw.value),\n } as EntityFilter<TEntity>;\n }\n\n // `isNull`/`isNotNull` — no argument to coerce.\n return { property, operator: raw.operator } as EntityFilter<TEntity>;\n}\n\nfunction coerceNode<TEntity extends Entity>(\n node: RawFilter | RawFilterGroup,\n allowed: Map<string, EntityFieldDescriptor>,\n): EntityFilter<TEntity> | FilterGroup<TEntity> {\n return isRawFilter(node)\n ? coerceFilter<TEntity>(node, allowed)\n : {\n operator: node.operator,\n values: node.values.map(child => coerceNode<TEntity>(child, allowed)),\n };\n}\n\n/**\n * Turns a parsed, untyped RSQL tree into a typed {@link FilterGroup} the\n * repositories can execute, validating it against the entity's metadata on the\n * way through.\n *\n * This is the trust boundary of the query protocol. Everything before it is a\n * string a client chose; everything after it names only members the entity\n * declared `filterable` and carries only values of those members' declared\n * types. A violation throws {@link EntifixBuildError}, which services already\n * map to a `400`.\n */\nexport function coerceFiltering<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n parsed: RawFilterGroup,\n): FilterGroup<TEntity> {\n const allowed = allowedDescriptors(\n entityConstructor,\n descriptor => descriptor.filterable,\n );\n\n return {\n operator: parsed.operator,\n values: parsed.values.map(node => coerceNode<TEntity>(node, allowed)),\n };\n}\n"],"names":["EntifixBuildError","describeEntityColumns","isRawFilter","allowedDescriptors","entityConstructor","select","allowed","Map","descriptor","set","name","key","reject","message","details","undefined","coerceValue","raw","type","value","Number","trim","isNaN","Date","getTime","enumValues","includes","coerceFilter","get","property","coerce","values","operator","map","start","end","coerceNode","node","child","coerceFiltering","parsed","filterable"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/rsql/coerce-rsql.ts"],"sourcesContent":["import { EntifixBuildError } from '../base-entities/entifix-error/index.js';\nimport {\n describeEntityColumns,\n type EntityFieldDescriptor,\n} from '../entity-definition/describe/index.js';\nimport type { MetaAccessorType } from '../entity-definition/meta-entities/meta-accessor/index.js';\nimport { isLocale, type Locale } from '../i18n/locales.js';\nimport type { Entity, EntityConstructor } from '../types/Entity.js';\nimport type { EntityFilter, FilterGroup } from '../types/EntityFiltering.js';\nimport {\n isRawFilter,\n type RawFilter,\n type RawFilterGroup,\n} from './parse-rsql.js';\n\n/**\n * Resolves the members a client is allowed to name, keyed by both the accessor\n * name and its wire key so a URL may use either.\n *\n * `select` is what makes this an allowlist rather than a lookup: a member the\n * entity did not mark `filterable`/`sortable` is simply not in the map, and a\n * request naming it is rejected. That is the guard against a client filtering\n * on a `hidden` member or an unindexed one.\n */\nexport function allowedDescriptors<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n select: (descriptor: EntityFieldDescriptor) => boolean,\n): Map<string, EntityFieldDescriptor> {\n const allowed = new Map<string, EntityFieldDescriptor>();\n for (const descriptor of describeEntityColumns(entityConstructor)) {\n if (!select(descriptor)) continue;\n allowed.set(descriptor.name, descriptor);\n allowed.set(descriptor.key, descriptor);\n }\n return allowed;\n}\n\nconst reject = (message: string, details?: Record<string, unknown>) =>\n new EntifixBuildError(message, undefined, details);\n\n/** A query property resolved against the allowlist. */\nexport interface AllowedProperty {\n readonly descriptor: EntityFieldDescriptor;\n /**\n * What an adapter names: the member's wire key, or for a `localized` member\n * the path to one locale's text (`title.en`).\n */\n readonly key: string;\n /** The locale a `localized` member is queried in. */\n readonly locale?: Locale;\n}\n\n/**\n * Resolves the property a filter or a sort names, or rejects it.\n *\n * A `localized` member is the one that takes a path: it holds an object, and\n * comparing an object as text matches nothing, so it is only ever queried\n * through one locale — `title.en` — and that locale must be one the host\n * declared. The bare member is rejected with a message that says so.\n */\nexport function resolveAllowedProperty(\n allowed: Map<string, EntityFieldDescriptor>,\n property: string,\n requirement: 'filterable' | 'sortable',\n): AllowedProperty {\n const direct = allowed.get(property);\n if (direct !== undefined) {\n if (direct.type === 'localized') {\n throw reject(\n `\"${property}\" is localized; name one locale, as \"${property}.<locale>\"`,\n { property },\n );\n }\n return { descriptor: direct, key: direct.key };\n }\n\n const dot = property.indexOf('.');\n const descriptor =\n dot === -1 ? undefined : allowed.get(property.slice(0, dot));\n if (descriptor?.type !== 'localized') {\n throw reject(`\"${property}\" is not a ${requirement} member`, { property });\n }\n const locale = property.slice(dot + 1);\n if (!isLocale(locale)) {\n throw reject(`\"${locale}\" is not a declared locale`, { property });\n }\n return { descriptor, key: `${descriptor.key}.${locale}`, locale };\n}\n\n/** Re-types one raw argument from the member's declared type. */\nfunction coerceValue(raw: string, descriptor: EntityFieldDescriptor): unknown {\n switch (descriptor.type as MetaAccessorType) {\n case 'number': {\n const value = Number(raw);\n if (raw.trim() === '' || Number.isNaN(value)) {\n throw reject(`\"${raw}\" is not a number for \"${descriptor.name}\"`);\n }\n return value;\n }\n case 'boolean': {\n if (raw !== 'true' && raw !== 'false') {\n throw reject(`\"${raw}\" is not a boolean for \"${descriptor.name}\"`);\n }\n return raw === 'true';\n }\n case 'date': {\n const value = new Date(raw);\n if (Number.isNaN(value.getTime())) {\n throw reject(`\"${raw}\" is not a date for \"${descriptor.name}\"`);\n }\n return value;\n }\n case 'enum': {\n if (descriptor.enumValues && !descriptor.enumValues.includes(raw)) {\n throw reject(`\"${raw}\" is not a value of \"${descriptor.name}\"`);\n }\n return raw;\n }\n default:\n return raw;\n }\n}\n\nfunction coerceFilter<TEntity extends Entity>(\n raw: RawFilter,\n allowed: Map<string, EntityFieldDescriptor>,\n): EntityFilter<TEntity> {\n const { descriptor, key } = resolveAllowedProperty(\n allowed,\n raw.property,\n 'filterable',\n );\n\n // The wire key is also the stored field name, so this is what the Mongo\n // translator downstream will use — dotted into one locale for a localized\n // member, which Mongo reads as a path natively.\n const property = key as keyof TEntity;\n const coerce = (value: string) => coerceValue(value, descriptor);\n\n if (raw.values) {\n return {\n property,\n operator: raw.operator,\n values: raw.values.map(coerce),\n } as EntityFilter<TEntity>;\n }\n if (raw.start !== undefined && raw.end !== undefined) {\n return {\n property,\n operator: raw.operator,\n start: coerce(raw.start),\n end: coerce(raw.end),\n } as EntityFilter<TEntity>;\n }\n if (raw.value !== undefined) {\n return {\n property,\n operator: raw.operator,\n value: coerce(raw.value),\n } as EntityFilter<TEntity>;\n }\n\n // `isNull`/`isNotNull` — no argument to coerce.\n return { property, operator: raw.operator } as EntityFilter<TEntity>;\n}\n\nfunction coerceNode<TEntity extends Entity>(\n node: RawFilter | RawFilterGroup,\n allowed: Map<string, EntityFieldDescriptor>,\n): EntityFilter<TEntity> | FilterGroup<TEntity> {\n return isRawFilter(node)\n ? coerceFilter<TEntity>(node, allowed)\n : {\n operator: node.operator,\n values: node.values.map(child => coerceNode<TEntity>(child, allowed)),\n };\n}\n\n/**\n * Turns a parsed, untyped RSQL tree into a typed {@link FilterGroup} the\n * repositories can execute, validating it against the entity's metadata on the\n * way through.\n *\n * This is the trust boundary of the query protocol. Everything before it is a\n * string a client chose; everything after it names only members the entity\n * declared `filterable` and carries only values of those members' declared\n * types. A violation throws {@link EntifixBuildError}, which services already\n * map to a `400`.\n */\nexport function coerceFiltering<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n parsed: RawFilterGroup,\n): FilterGroup<TEntity> {\n const allowed = allowedDescriptors(\n entityConstructor,\n descriptor => descriptor.filterable,\n );\n\n return {\n operator: parsed.operator,\n values: parsed.values.map(node => coerceNode<TEntity>(node, allowed)),\n };\n}\n"],"names":["EntifixBuildError","describeEntityColumns","isLocale","isRawFilter","allowedDescriptors","entityConstructor","select","allowed","Map","descriptor","set","name","key","reject","message","details","undefined","resolveAllowedProperty","property","requirement","direct","get","type","dot","indexOf","slice","locale","coerceValue","raw","value","Number","trim","isNaN","Date","getTime","enumValues","includes","coerceFilter","coerce","values","operator","map","start","end","coerceNode","node","child","coerceFiltering","parsed","filterable"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,0CAA0C;AAC5E,SACEC,qBAAqB,QAEhB,yCAAyC;AAEhD,SAASC,QAAQ,QAAqB,qBAAqB;AAG3D,SACEC,WAAW,QAGN,kBAAkB;AAEzB;;;;;;;;CAQC,GACD,OAAO,SAASC,mBACdC,iBAA6C,EAC7CC,MAAsD;IAEtD,MAAMC,UAAU,IAAIC;IACpB,KAAK,MAAMC,cAAcR,sBAAsBI,mBAAoB;QACjE,IAAI,CAACC,OAAOG,aAAa;QACzBF,QAAQG,GAAG,CAACD,WAAWE,IAAI,EAAEF;QAC7BF,QAAQG,GAAG,CAACD,WAAWG,GAAG,EAAEH;IAC9B;IACA,OAAOF;AACT;AAEA,MAAMM,SAAS,CAACC,SAAiBC,UAC/B,IAAIf,kBAAkBc,SAASE,WAAWD;AAc5C;;;;;;;CAOC,GACD,OAAO,SAASE,uBACdV,OAA2C,EAC3CW,QAAgB,EAChBC,WAAsC;IAEtC,MAAMC,SAASb,QAAQc,GAAG,CAACH;IAC3B,IAAIE,WAAWJ,WAAW;QACxB,IAAII,OAAOE,IAAI,KAAK,aAAa;YAC/B,MAAMT,OACJ,CAAC,CAAC,EAAEK,SAAS,qCAAqC,EAAEA,SAAS,UAAU,CAAC,EACxE;gBAAEA;YAAS;QAEf;QACA,OAAO;YAAET,YAAYW;YAAQR,KAAKQ,OAAOR,GAAG;QAAC;IAC/C;IAEA,MAAMW,MAAML,SAASM,OAAO,CAAC;IAC7B,MAAMf,aACJc,QAAQ,CAAC,IAAIP,YAAYT,QAAQc,GAAG,CAACH,SAASO,KAAK,CAAC,GAAGF;IACzD,IAAId,CAAAA,8BAAAA,WAAYa,IAAI,MAAK,aAAa;QACpC,MAAMT,OAAO,CAAC,CAAC,EAAEK,SAAS,WAAW,EAAEC,YAAY,OAAO,CAAC,EAAE;YAAED;QAAS;IAC1E;IACA,MAAMQ,SAASR,SAASO,KAAK,CAACF,MAAM;IACpC,IAAI,CAACrB,SAASwB,SAAS;QACrB,MAAMb,OAAO,CAAC,CAAC,EAAEa,OAAO,0BAA0B,CAAC,EAAE;YAAER;QAAS;IAClE;IACA,OAAO;QAAET;QAAYG,KAAK,GAAGH,WAAWG,GAAG,CAAC,CAAC,EAAEc,QAAQ;QAAEA;IAAO;AAClE;AAEA,+DAA+D,GAC/D,SAASC,YAAYC,GAAW,EAAEnB,UAAiC;IACjE,OAAQA,WAAWa,IAAI;QACrB,KAAK;YAAU;gBACb,MAAMO,QAAQC,OAAOF;gBACrB,IAAIA,IAAIG,IAAI,OAAO,MAAMD,OAAOE,KAAK,CAACH,QAAQ;oBAC5C,MAAMhB,OAAO,CAAC,CAAC,EAAEe,IAAI,uBAAuB,EAAEnB,WAAWE,IAAI,CAAC,CAAC,CAAC;gBAClE;gBACA,OAAOkB;YACT;QACA,KAAK;YAAW;gBACd,IAAID,QAAQ,UAAUA,QAAQ,SAAS;oBACrC,MAAMf,OAAO,CAAC,CAAC,EAAEe,IAAI,wBAAwB,EAAEnB,WAAWE,IAAI,CAAC,CAAC,CAAC;gBACnE;gBACA,OAAOiB,QAAQ;YACjB;QACA,KAAK;YAAQ;gBACX,MAAMC,QAAQ,IAAII,KAAKL;gBACvB,IAAIE,OAAOE,KAAK,CAACH,MAAMK,OAAO,KAAK;oBACjC,MAAMrB,OAAO,CAAC,CAAC,EAAEe,IAAI,qBAAqB,EAAEnB,WAAWE,IAAI,CAAC,CAAC,CAAC;gBAChE;gBACA,OAAOkB;YACT;QACA,KAAK;YAAQ;gBACX,IAAIpB,WAAW0B,UAAU,IAAI,CAAC1B,WAAW0B,UAAU,CAACC,QAAQ,CAACR,MAAM;oBACjE,MAAMf,OAAO,CAAC,CAAC,EAAEe,IAAI,qBAAqB,EAAEnB,WAAWE,IAAI,CAAC,CAAC,CAAC;gBAChE;gBACA,OAAOiB;YACT;QACA;YACE,OAAOA;IACX;AACF;AAEA,SAASS,aACPT,GAAc,EACdrB,OAA2C;IAE3C,MAAM,EAAEE,UAAU,EAAEG,GAAG,EAAE,GAAGK,uBAC1BV,SACAqB,IAAIV,QAAQ,EACZ;IAGF,wEAAwE;IACxE,0EAA0E;IAC1E,gDAAgD;IAChD,MAAMA,WAAWN;IACjB,MAAM0B,SAAS,CAACT,QAAkBF,YAAYE,OAAOpB;IAErD,IAAImB,IAAIW,MAAM,EAAE;QACd,OAAO;YACLrB;YACAsB,UAAUZ,IAAIY,QAAQ;YACtBD,QAAQX,IAAIW,MAAM,CAACE,GAAG,CAACH;QACzB;IACF;IACA,IAAIV,IAAIc,KAAK,KAAK1B,aAAaY,IAAIe,GAAG,KAAK3B,WAAW;QACpD,OAAO;YACLE;YACAsB,UAAUZ,IAAIY,QAAQ;YACtBE,OAAOJ,OAAOV,IAAIc,KAAK;YACvBC,KAAKL,OAAOV,IAAIe,GAAG;QACrB;IACF;IACA,IAAIf,IAAIC,KAAK,KAAKb,WAAW;QAC3B,OAAO;YACLE;YACAsB,UAAUZ,IAAIY,QAAQ;YACtBX,OAAOS,OAAOV,IAAIC,KAAK;QACzB;IACF;IAEA,gDAAgD;IAChD,OAAO;QAAEX;QAAUsB,UAAUZ,IAAIY,QAAQ;IAAC;AAC5C;AAEA,SAASI,WACPC,IAAgC,EAChCtC,OAA2C;IAE3C,OAAOJ,YAAY0C,QACfR,aAAsBQ,MAAMtC,WAC5B;QACEiC,UAAUK,KAAKL,QAAQ;QACvBD,QAAQM,KAAKN,MAAM,CAACE,GAAG,CAACK,CAAAA,QAASF,WAAoBE,OAAOvC;IAC9D;AACN;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASwC,gBACd1C,iBAA6C,EAC7C2C,MAAsB;IAEtB,MAAMzC,UAAUH,mBACdC,mBACAI,CAAAA,aAAcA,WAAWwC,UAAU;IAGrC,OAAO;QACLT,UAAUQ,OAAOR,QAAQ;QACzBD,QAAQS,OAAOT,MAAM,CAACE,GAAG,CAACI,CAAAA,OAAQD,WAAoBC,MAAMtC;IAC9D;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"sort-param.d.ts","sourceRoot":"","sources":["../../src/rsql/sort-param.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,2BAA2B,CAAC;AA6B/E;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,SAAS,MAAM,EAClD,OAAO,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,EAAE,GACjC,MAAM,CAMR;AAED;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,OAAO,SAAS,MAAM,EAC9C,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACnC,aAAa,CAAC,OAAO,CAAC,EAAE,GAAG,SAAS,CAgCtC"}
1
+ {"version":3,"file":"sort-param.d.ts","sourceRoot":"","sources":["../../src/rsql/sort-param.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,2BAA2B,CAAC;AA6B/E;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,SAAS,MAAM,EAClD,OAAO,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,EAAE,GACjC,MAAM,CAMR;AAED;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,OAAO,SAAS,MAAM,EAC9C,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACnC,aAAa,CAAC,OAAO,CAAC,EAAE,GAAG,SAAS,CAwBtC"}
@@ -1,5 +1,4 @@
1
- import { EntifixBuildError } from '../base-entities/entifix-error/index.js';
2
- import { allowedDescriptors } from './coerce-rsql.js';
1
+ import { allowedDescriptors, resolveAllowedProperty } from './coerce-rsql.js';
3
2
  /**
4
3
  * RSQL standardizes filtering only, so sorting travels in its own `sort`
5
4
  * parameter: `+name,-code`. The sign is the direction (`-` descending, `+` or
@@ -47,14 +46,9 @@ import { allowedDescriptors } from './coerce-rsql.js';
47
46
  if (term === '') continue;
48
47
  const type = term.startsWith('-') ? 'desc' : 'asc';
49
48
  const name = term.replace(/^[+-]/, '');
50
- const descriptor = allowed.get(name);
51
- if (!descriptor) {
52
- throw new EntifixBuildError(`"${name}" is not a sortable member`, undefined, {
53
- property: name
54
- });
55
- }
49
+ const { key } = resolveAllowedProperty(allowed, name, 'sortable');
56
50
  record[priority++] = {
57
- property: descriptor.key,
51
+ property: key,
58
52
  type
59
53
  };
60
54
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/rsql/sort-param.ts"],"sourcesContent":["import { EntifixBuildError } from '../base-entities/entifix-error/index.js';\nimport type { Entity, EntityConstructor } from '../types/Entity.js';\nimport type { EntitySorting, EntitySortType } from '../types/EntitySorting.js';\nimport { allowedDescriptors } from './coerce-rsql.js';\n\n/**\n * RSQL standardizes filtering only, so sorting travels in its own `sort`\n * parameter: `+name,-code`. The sign is the direction (`-` descending, `+` or\n * none ascending) and the list position is the precedence — which is exactly\n * what `EntitySorting`'s numeric keys mean, so the two forms map one to one.\n */\nconst SEPARATOR = ',';\n\n/** Flattens the sorting records into `[property, direction]` in priority order. */\nfunction orderedEntries<TEntity extends Entity>(\n sorting: EntitySorting<TEntity>[],\n): Array<[string, EntitySortType]> {\n const entries: Array<[string, EntitySortType]> = [];\n\n for (const record of sorting) {\n for (const priority of Object.keys(record)\n .map(Number)\n .sort((left, right) => left - right)) {\n const entry = record[priority];\n if (entry) entries.push([String(entry.property), entry.type]);\n }\n }\n\n return entries;\n}\n\n/**\n * Serializes an {@link EntityLoadRequest}'s `sorting` into the `sort`\n * parameter's value. Absent or empty sorting yields `''`, which callers omit\n * from the URL rather than sending empty.\n */\nexport function serializeSort<TEntity extends Entity>(\n sorting?: EntitySorting<TEntity>[],\n): string {\n if (!sorting || sorting.length === 0) return '';\n\n return orderedEntries(sorting)\n .map(([property, type]) => `${type === 'desc' ? '-' : '+'}${property}`)\n .join(SEPARATOR);\n}\n\n/**\n * Parses the `sort` parameter into the request's `sorting`, allowlisting every\n * member against the entity's `sortable` metadata for the same reason\n * {@link coerceFiltering} does: an unchecked property name would let a client\n * sort by a member the entity never meant to expose, and an unindexed one is a\n * collection scan.\n *\n * Returns a single-record array — one record whose numeric keys carry the\n * precedence — or `undefined` when there is nothing to sort by.\n */\nexport function parseSort<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n parameter: string | null | undefined,\n): EntitySorting<TEntity>[] | undefined {\n const text = parameter?.trim();\n if (!text) return undefined;\n\n const allowed = allowedDescriptors(\n entityConstructor,\n descriptor => descriptor.sortable,\n );\n\n const record = {} as EntitySorting<TEntity>;\n let priority = 0;\n\n for (const rawTerm of text.split(SEPARATOR)) {\n const term = rawTerm.trim();\n if (term === '') continue;\n\n const type: EntitySortType = term.startsWith('-') ? 'desc' : 'asc';\n const name = term.replace(/^[+-]/, '');\n const descriptor = allowed.get(name);\n\n if (!descriptor) {\n throw new EntifixBuildError(\n `\"${name}\" is not a sortable member`,\n undefined,\n { property: name },\n );\n }\n\n record[priority++] = { property: descriptor.key as keyof TEntity, type };\n }\n\n return priority === 0 ? undefined : [record];\n}\n"],"names":["EntifixBuildError","allowedDescriptors","SEPARATOR","orderedEntries","sorting","entries","record","priority","Object","keys","map","Number","sort","left","right","entry","push","String","property","type","serializeSort","length","join","parseSort","entityConstructor","parameter","text","trim","undefined","allowed","descriptor","sortable","rawTerm","split","term","startsWith","name","replace","get","key"],"mappings":"AAAA,SAASA,iBAAiB,QAAQ,0CAA0C;AAG5E,SAASC,kBAAkB,QAAQ,mBAAmB;AAEtD;;;;;CAKC,GACD,MAAMC,YAAY;AAElB,iFAAiF,GACjF,SAASC,eACPC,OAAiC;IAEjC,MAAMC,UAA2C,EAAE;IAEnD,KAAK,MAAMC,UAAUF,QAAS;QAC5B,KAAK,MAAMG,YAAYC,OAAOC,IAAI,CAACH,QAChCI,GAAG,CAACC,QACJC,IAAI,CAAC,CAACC,MAAMC,QAAUD,OAAOC,OAAQ;YACtC,MAAMC,QAAQT,MAAM,CAACC,SAAS;YAC9B,IAAIQ,OAAOV,QAAQW,IAAI,CAAC;gBAACC,OAAOF,MAAMG,QAAQ;gBAAGH,MAAMI,IAAI;aAAC;QAC9D;IACF;IAEA,OAAOd;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASe,cACdhB,OAAkC;IAElC,IAAI,CAACA,WAAWA,QAAQiB,MAAM,KAAK,GAAG,OAAO;IAE7C,OAAOlB,eAAeC,SACnBM,GAAG,CAAC,CAAC,CAACQ,UAAUC,KAAK,GAAK,GAAGA,SAAS,SAAS,MAAM,MAAMD,UAAU,EACrEI,IAAI,CAACpB;AACV;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASqB,UACdC,iBAA6C,EAC7CC,SAAoC;IAEpC,MAAMC,OAAOD,6BAAAA,UAAWE,IAAI;IAC5B,IAAI,CAACD,MAAM,OAAOE;IAElB,MAAMC,UAAU5B,mBACduB,mBACAM,CAAAA,aAAcA,WAAWC,QAAQ;IAGnC,MAAMzB,SAAS,CAAC;IAChB,IAAIC,WAAW;IAEf,KAAK,MAAMyB,WAAWN,KAAKO,KAAK,CAAC/B,WAAY;QAC3C,MAAMgC,OAAOF,QAAQL,IAAI;QACzB,IAAIO,SAAS,IAAI;QAEjB,MAAMf,OAAuBe,KAAKC,UAAU,CAAC,OAAO,SAAS;QAC7D,MAAMC,OAAOF,KAAKG,OAAO,CAAC,SAAS;QACnC,MAAMP,aAAaD,QAAQS,GAAG,CAACF;QAE/B,IAAI,CAACN,YAAY;YACf,MAAM,IAAI9B,kBACR,CAAC,CAAC,EAAEoC,KAAK,0BAA0B,CAAC,EACpCR,WACA;gBAAEV,UAAUkB;YAAK;QAErB;QAEA9B,MAAM,CAACC,WAAW,GAAG;YAAEW,UAAUY,WAAWS,GAAG;YAAmBpB;QAAK;IACzE;IAEA,OAAOZ,aAAa,IAAIqB,YAAY;QAACtB;KAAO;AAC9C"}
1
+ {"version":3,"sources":["../../src/rsql/sort-param.ts"],"sourcesContent":["import type { Entity, EntityConstructor } from '../types/Entity.js';\nimport type { EntitySorting, EntitySortType } from '../types/EntitySorting.js';\nimport { allowedDescriptors, resolveAllowedProperty } from './coerce-rsql.js';\n\n/**\n * RSQL standardizes filtering only, so sorting travels in its own `sort`\n * parameter: `+name,-code`. The sign is the direction (`-` descending, `+` or\n * none ascending) and the list position is the precedence — which is exactly\n * what `EntitySorting`'s numeric keys mean, so the two forms map one to one.\n */\nconst SEPARATOR = ',';\n\n/** Flattens the sorting records into `[property, direction]` in priority order. */\nfunction orderedEntries<TEntity extends Entity>(\n sorting: EntitySorting<TEntity>[],\n): Array<[string, EntitySortType]> {\n const entries: Array<[string, EntitySortType]> = [];\n\n for (const record of sorting) {\n for (const priority of Object.keys(record)\n .map(Number)\n .sort((left, right) => left - right)) {\n const entry = record[priority];\n if (entry) entries.push([String(entry.property), entry.type]);\n }\n }\n\n return entries;\n}\n\n/**\n * Serializes an {@link EntityLoadRequest}'s `sorting` into the `sort`\n * parameter's value. Absent or empty sorting yields `''`, which callers omit\n * from the URL rather than sending empty.\n */\nexport function serializeSort<TEntity extends Entity>(\n sorting?: EntitySorting<TEntity>[],\n): string {\n if (!sorting || sorting.length === 0) return '';\n\n return orderedEntries(sorting)\n .map(([property, type]) => `${type === 'desc' ? '-' : '+'}${property}`)\n .join(SEPARATOR);\n}\n\n/**\n * Parses the `sort` parameter into the request's `sorting`, allowlisting every\n * member against the entity's `sortable` metadata for the same reason\n * {@link coerceFiltering} does: an unchecked property name would let a client\n * sort by a member the entity never meant to expose, and an unindexed one is a\n * collection scan.\n *\n * Returns a single-record array — one record whose numeric keys carry the\n * precedence — or `undefined` when there is nothing to sort by.\n */\nexport function parseSort<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n parameter: string | null | undefined,\n): EntitySorting<TEntity>[] | undefined {\n const text = parameter?.trim();\n if (!text) return undefined;\n\n const allowed = allowedDescriptors(\n entityConstructor,\n descriptor => descriptor.sortable,\n );\n\n const record = {} as EntitySorting<TEntity>;\n let priority = 0;\n\n for (const rawTerm of text.split(SEPARATOR)) {\n const term = rawTerm.trim();\n if (term === '') continue;\n\n const type: EntitySortType = term.startsWith('-') ? 'desc' : 'asc';\n const name = term.replace(/^[+-]/, '');\n const { key } = resolveAllowedProperty(allowed, name, 'sortable');\n\n record[priority++] = { property: key as keyof TEntity, type };\n }\n\n return priority === 0 ? undefined : [record];\n}\n"],"names":["allowedDescriptors","resolveAllowedProperty","SEPARATOR","orderedEntries","sorting","entries","record","priority","Object","keys","map","Number","sort","left","right","entry","push","String","property","type","serializeSort","length","join","parseSort","entityConstructor","parameter","text","trim","undefined","allowed","descriptor","sortable","rawTerm","split","term","startsWith","name","replace","key"],"mappings":"AAEA,SAASA,kBAAkB,EAAEC,sBAAsB,QAAQ,mBAAmB;AAE9E;;;;;CAKC,GACD,MAAMC,YAAY;AAElB,iFAAiF,GACjF,SAASC,eACPC,OAAiC;IAEjC,MAAMC,UAA2C,EAAE;IAEnD,KAAK,MAAMC,UAAUF,QAAS;QAC5B,KAAK,MAAMG,YAAYC,OAAOC,IAAI,CAACH,QAChCI,GAAG,CAACC,QACJC,IAAI,CAAC,CAACC,MAAMC,QAAUD,OAAOC,OAAQ;YACtC,MAAMC,QAAQT,MAAM,CAACC,SAAS;YAC9B,IAAIQ,OAAOV,QAAQW,IAAI,CAAC;gBAACC,OAAOF,MAAMG,QAAQ;gBAAGH,MAAMI,IAAI;aAAC;QAC9D;IACF;IAEA,OAAOd;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASe,cACdhB,OAAkC;IAElC,IAAI,CAACA,WAAWA,QAAQiB,MAAM,KAAK,GAAG,OAAO;IAE7C,OAAOlB,eAAeC,SACnBM,GAAG,CAAC,CAAC,CAACQ,UAAUC,KAAK,GAAK,GAAGA,SAAS,SAAS,MAAM,MAAMD,UAAU,EACrEI,IAAI,CAACpB;AACV;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASqB,UACdC,iBAA6C,EAC7CC,SAAoC;IAEpC,MAAMC,OAAOD,6BAAAA,UAAWE,IAAI;IAC5B,IAAI,CAACD,MAAM,OAAOE;IAElB,MAAMC,UAAU7B,mBACdwB,mBACAM,CAAAA,aAAcA,WAAWC,QAAQ;IAGnC,MAAMzB,SAAS,CAAC;IAChB,IAAIC,WAAW;IAEf,KAAK,MAAMyB,WAAWN,KAAKO,KAAK,CAAC/B,WAAY;QAC3C,MAAMgC,OAAOF,QAAQL,IAAI;QACzB,IAAIO,SAAS,IAAI;QAEjB,MAAMf,OAAuBe,KAAKC,UAAU,CAAC,OAAO,SAAS;QAC7D,MAAMC,OAAOF,KAAKG,OAAO,CAAC,SAAS;QACnC,MAAM,EAAEC,GAAG,EAAE,GAAGrC,uBAAuB4B,SAASO,MAAM;QAEtD9B,MAAM,CAACC,WAAW,GAAG;YAAEW,UAAUoB;YAAsBnB;QAAK;IAC9D;IAEA,OAAOZ,aAAa,IAAIqB,YAAY;QAACtB;KAAO;AAC9C"}
@@ -12,7 +12,10 @@ import type { EntityRowDraft } from './EntityRowDraft.js';
12
12
  * this is a union and not `string`: rows have no lossless string form, and the
13
13
  * two shapes a draft can hold are exactly the two ADR 0034 named.
14
14
  *
15
- * **Exactly two shapes, and the union says so rather than `JsonValue`.** Both
15
+ * A **`localized`** member's value is an {@link EntityLocalizedDraft}, a text per
16
+ * locale.
17
+ *
18
+ * **Exactly three shapes, and the union says so rather than `JsonValue`.** Both
16
19
  * are JSON — a workspace autosaves a draft through `createJSONStorage`
17
20
  * ([ADR 0032](../../../../../../docs/adr/0032-what-may-live-in-an-autosaved-draft.md)),
18
21
  * so a class instance, an `EntityLink` or a `Date` does not degrade, it returns
@@ -32,7 +35,14 @@ import type { EntityRowDraft } from './EntityRowDraft.js';
32
35
  * and `EntityCrudDraft` — and collapsing them is what made this widening a
33
36
  * single edit rather than four found by search.
34
37
  */
35
- export type EntityDraftValue = string | readonly EntityRowDraft[];
38
+ export type EntityDraftValue = string | readonly EntityRowDraft[] | EntityLocalizedDraft;
39
+ /**
40
+ * A `localized` member's draft: one string per locale, the text each input
41
+ * holds. Flat like a row, so the union still has a bottom for TanStack Form to
42
+ * derive paths from, and JSON-safe for the same autosave a row has to survive.
43
+ * Told apart from a row list by not being an array.
44
+ */
45
+ export type EntityLocalizedDraft = Readonly<Record<string, string>>;
36
46
  export type EntityDraft = Record<string, EntityDraftValue>;
37
47
  /**
38
48
  * The string a draft holds for a scalar member.
@@ -44,4 +54,10 @@ export type EntityDraft = Record<string, EntityDraftValue>;
44
54
  * mid-render.
45
55
  */
46
56
  export declare function readDraftString(values: EntityDraft | undefined, name: string): string;
57
+ /**
58
+ * The per-locale texts a draft holds for a `localized` member, or an empty map
59
+ * for anything else — a member never seeded, or one that came back from
60
+ * storage in another shape.
61
+ */
62
+ export declare function readLocalizedDraft(values: EntityDraft | undefined, name: string): EntityLocalizedDraft;
47
63
  //# sourceMappingURL=EntityDraft.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"EntityDraft.d.ts","sourceRoot":"","sources":["../../src/types/EntityDraft.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,cAAc,EAAE,CAAC;AAElE,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;AAE3D;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,IAAI,EAAE,MAAM,GACX,MAAM,CAGR"}
1
+ {"version":3,"file":"EntityDraft.d.ts","sourceRoot":"","sources":["../../src/types/EntityDraft.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,MAAM,gBAAgB,GAC1B,MAAM,GAAG,SAAS,cAAc,EAAE,GAAG,oBAAoB,CAAC;AAE5D;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAEpE,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;AAE3D;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,IAAI,EAAE,MAAM,GACX,MAAM,CAGR;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,IAAI,EAAE,MAAM,GACX,oBAAoB,CActB"}
@@ -10,5 +10,16 @@
10
10
  const value = values == null ? void 0 : values[name];
11
11
  return typeof value === 'string' ? value : '';
12
12
  }
13
+ /**
14
+ * The per-locale texts a draft holds for a `localized` member, or an empty map
15
+ * for anything else — a member never seeded, or one that came back from
16
+ * storage in another shape.
17
+ */ export function readLocalizedDraft(values, name) {
18
+ const value = values == null ? void 0 : values[name];
19
+ if (value === undefined || typeof value === 'string' || Array.isArray(value)) {
20
+ return {};
21
+ }
22
+ return Object.fromEntries(Object.entries(value).filter(([, text])=>typeof text === 'string'));
23
+ }
13
24
 
14
25
  //# sourceMappingURL=EntityDraft.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/types/EntityDraft.ts"],"sourcesContent":["import type { EntityRowDraft } from './EntityRowDraft.js';\n\n/**\n * A form's in-progress values, keyed by accessor name.\n *\n * A scalar member's value is a **string**, because that is what a native input\n * round-trips: a `scalarCollection` drafts as a comma list, a `date` as\n * `yyyy-mm-dd`, a relation as its foreign key. Reading one is therefore still\n * `values[name] ?? ''` in every place that only edits scalars.\n *\n * A **`composition`** member's value is an `EntityRowDraft[]` — rows the master\n * owns, each a string map carrying its own `ROW_KEY`. That is the whole reason\n * this is a union and not `string`: rows have no lossless string form, and the\n * two shapes a draft can hold are exactly the two ADR 0034 named.\n *\n * **Exactly two shapes, and the union says so rather than `JsonValue`.** Both\n * are JSON — a workspace autosaves a draft through `createJSONStorage`\n * ([ADR 0032](../../../../../../docs/adr/0032-what-may-live-in-an-autosaved-draft.md)),\n * so a class instance, an `EntityLink` or a `Date` does not degrade, it returns\n * as something else, silently — but `JsonValue` would say more than is true. A\n * member whose value is neither a string nor a row list has no editor, no\n * coercion and no restore rule: `reconstructEntity` would carry it through\n * untouched and the serializer would write it.\n *\n * The narrower union is also what keeps the form engine compiling. `JsonValue`\n * is **recursive**, and TanStack Form derives a field-path type from the value\n * it is given, so a draft typed that way makes that derivation unbounded —\n * `Type instantiation is excessively deep and possibly infinite`. A row is a\n * flat string map, so this union has a bottom and the derivation terminates.\n *\n * The alias exists in **one** place on purpose. The same structural type was\n * declared four times — `EntityFormValues`, `EntityFormDraft`, `EntityLinkDraft`\n * and `EntityCrudDraft` — and collapsing them is what made this widening a\n * single edit rather than four found by search.\n */\nexport type EntityDraftValue = string | readonly EntityRowDraft[];\n\nexport type EntityDraft = Record<string, EntityDraftValue>;\n\n/**\n * The string a draft holds for a scalar member.\n *\n * A draft value is `JsonValue` now, but every scalar editor still writes and\n * reads a string, so this is the read every one of them performs — in one\n * place, so a member that came back from storage as a number or `null` renders\n * as an empty input instead of flipping React from controlled to uncontrolled\n * mid-render.\n */\nexport function readDraftString(\n values: EntityDraft | undefined,\n name: string,\n): string {\n const value = values?.[name];\n return typeof value === 'string' ? value : '';\n}\n"],"names":["readDraftString","values","name","value"],"mappings":"AAuCA;;;;;;;;CAQC,GACD,OAAO,SAASA,gBACdC,MAA+B,EAC/BC,IAAY;IAEZ,MAAMC,QAAQF,0BAAAA,MAAQ,CAACC,KAAK;IAC5B,OAAO,OAAOC,UAAU,WAAWA,QAAQ;AAC7C"}
1
+ {"version":3,"sources":["../../src/types/EntityDraft.ts"],"sourcesContent":["import type { EntityRowDraft } from './EntityRowDraft.js';\n\n/**\n * A form's in-progress values, keyed by accessor name.\n *\n * A scalar member's value is a **string**, because that is what a native input\n * round-trips: a `scalarCollection` drafts as a comma list, a `date` as\n * `yyyy-mm-dd`, a relation as its foreign key. Reading one is therefore still\n * `values[name] ?? ''` in every place that only edits scalars.\n *\n * A **`composition`** member's value is an `EntityRowDraft[]` — rows the master\n * owns, each a string map carrying its own `ROW_KEY`. That is the whole reason\n * this is a union and not `string`: rows have no lossless string form, and the\n * two shapes a draft can hold are exactly the two ADR 0034 named.\n *\n * A **`localized`** member's value is an {@link EntityLocalizedDraft}, a text per\n * locale.\n *\n * **Exactly three shapes, and the union says so rather than `JsonValue`.** Both\n * are JSON — a workspace autosaves a draft through `createJSONStorage`\n * ([ADR 0032](../../../../../../docs/adr/0032-what-may-live-in-an-autosaved-draft.md)),\n * so a class instance, an `EntityLink` or a `Date` does not degrade, it returns\n * as something else, silently — but `JsonValue` would say more than is true. A\n * member whose value is neither a string nor a row list has no editor, no\n * coercion and no restore rule: `reconstructEntity` would carry it through\n * untouched and the serializer would write it.\n *\n * The narrower union is also what keeps the form engine compiling. `JsonValue`\n * is **recursive**, and TanStack Form derives a field-path type from the value\n * it is given, so a draft typed that way makes that derivation unbounded —\n * `Type instantiation is excessively deep and possibly infinite`. A row is a\n * flat string map, so this union has a bottom and the derivation terminates.\n *\n * The alias exists in **one** place on purpose. The same structural type was\n * declared four times — `EntityFormValues`, `EntityFormDraft`, `EntityLinkDraft`\n * and `EntityCrudDraft` — and collapsing them is what made this widening a\n * single edit rather than four found by search.\n */\nexport type EntityDraftValue =\n string | readonly EntityRowDraft[] | EntityLocalizedDraft;\n\n/**\n * A `localized` member's draft: one string per locale, the text each input\n * holds. Flat like a row, so the union still has a bottom for TanStack Form to\n * derive paths from, and JSON-safe for the same autosave a row has to survive.\n * Told apart from a row list by not being an array.\n */\nexport type EntityLocalizedDraft = Readonly<Record<string, string>>;\n\nexport type EntityDraft = Record<string, EntityDraftValue>;\n\n/**\n * The string a draft holds for a scalar member.\n *\n * A draft value is `JsonValue` now, but every scalar editor still writes and\n * reads a string, so this is the read every one of them performs — in one\n * place, so a member that came back from storage as a number or `null` renders\n * as an empty input instead of flipping React from controlled to uncontrolled\n * mid-render.\n */\nexport function readDraftString(\n values: EntityDraft | undefined,\n name: string,\n): string {\n const value = values?.[name];\n return typeof value === 'string' ? value : '';\n}\n\n/**\n * The per-locale texts a draft holds for a `localized` member, or an empty map\n * for anything else — a member never seeded, or one that came back from\n * storage in another shape.\n */\nexport function readLocalizedDraft(\n values: EntityDraft | undefined,\n name: string,\n): EntityLocalizedDraft {\n const value = values?.[name];\n if (\n value === undefined ||\n typeof value === 'string' ||\n Array.isArray(value)\n ) {\n return {};\n }\n return Object.fromEntries(\n Object.entries(value as EntityLocalizedDraft).filter(\n ([, text]) => typeof text === 'string',\n ),\n );\n}\n"],"names":["readDraftString","values","name","value","readLocalizedDraft","undefined","Array","isArray","Object","fromEntries","entries","filter","text"],"mappings":"AAmDA;;;;;;;;CAQC,GACD,OAAO,SAASA,gBACdC,MAA+B,EAC/BC,IAAY;IAEZ,MAAMC,QAAQF,0BAAAA,MAAQ,CAACC,KAAK;IAC5B,OAAO,OAAOC,UAAU,WAAWA,QAAQ;AAC7C;AAEA;;;;CAIC,GACD,OAAO,SAASC,mBACdH,MAA+B,EAC/BC,IAAY;IAEZ,MAAMC,QAAQF,0BAAAA,MAAQ,CAACC,KAAK;IAC5B,IACEC,UAAUE,aACV,OAAOF,UAAU,YACjBG,MAAMC,OAAO,CAACJ,QACd;QACA,OAAO,CAAC;IACV;IACA,OAAOK,OAAOC,WAAW,CACvBD,OAAOE,OAAO,CAACP,OAA+BQ,MAAM,CAClD,CAAC,GAAGC,KAAK,GAAK,OAAOA,SAAS;AAGpC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entifix/core",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "The entity system: decorators that make a class describe itself, entity serialization, the RSQL query protocol and the wire envelopes.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -18,7 +18,6 @@
18
18
  "exports": {
19
19
  "./package.json": "./package.json",
20
20
  ".": {
21
- "@entifix/source": "./src/index.ts",
22
21
  "types": "./dist/index.d.ts",
23
22
  "import": "./dist/index.js",
24
23
  "default": "./dist/index.js"