@entifix/core 0.7.1 → 0.8.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.
@@ -1 +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"}
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;AAyJD,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"}
@@ -60,37 +60,75 @@ import { EntityLink } from '../entity-definition/links/entity-link/index.js';
60
60
  const matchesLike = (actual, value)=>actual != null && new RegExp(escapeRegex(value), 'i').test(String(actual));
61
61
  const isFilterGroup = (node)=>!('property' in node) && (node.operator === 'and' || node.operator === 'or');
62
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));
63
+ /**
64
+ * The values a dotted path reaches **through an array** — every element's
65
+ * member — or `undefined` when the path crosses no array, so the caller reads
66
+ * the single value as before.
67
+ */ function valuesThroughArray(record, property) {
68
+ const dot = property.indexOf('.');
69
+ if (dot === -1) return undefined;
70
+ const elements = storedValue(record[property.slice(0, dot)]);
71
+ if (!Array.isArray(elements)) return undefined;
72
+ const member = property.slice(dot + 1);
73
+ return elements.map((element)=>typeof element === 'object' && element !== null ? valueAt(element, member) : undefined);
74
+ }
75
+ /**
76
+ * Splits a filter into its positive test and whether the operator negates it,
77
+ * so a path into an array can ask "any element" of the positive form and
78
+ * negate the answer — never "any element is not equal", which almost always
79
+ * holds.
80
+ */ function positiveTest(filter) {
65
81
  switch(filter.operator){
66
82
  case 'eq':
67
- return isEqual(actual, filter.value);
68
83
  case 'ne':
69
- return !isEqual(actual, filter.value);
84
+ return {
85
+ test: (actual)=>isEqual(actual, filter.value),
86
+ negated: filter.operator === 'ne'
87
+ };
70
88
  case 'gt':
71
- return actual != null && compareStoredValues(actual, filter.value) > 0;
89
+ return {
90
+ test: (actual)=>actual != null && compareStoredValues(actual, filter.value) > 0,
91
+ negated: false
92
+ };
72
93
  case 'gte':
73
- return actual != null && compareStoredValues(actual, filter.value) >= 0;
94
+ return {
95
+ test: (actual)=>actual != null && compareStoredValues(actual, filter.value) >= 0,
96
+ negated: false
97
+ };
74
98
  case 'lt':
75
- return actual != null && compareStoredValues(actual, filter.value) < 0;
99
+ return {
100
+ test: (actual)=>actual != null && compareStoredValues(actual, filter.value) < 0,
101
+ negated: false
102
+ };
76
103
  case 'lte':
77
- return actual != null && compareStoredValues(actual, filter.value) <= 0;
104
+ return {
105
+ test: (actual)=>actual != null && compareStoredValues(actual, filter.value) <= 0,
106
+ negated: false
107
+ };
78
108
  case 'in':
79
- return matchesAny(actual, filter.values);
80
109
  case 'nin':
81
- return !matchesAny(actual, filter.values);
110
+ return {
111
+ test: (actual)=>matchesAny(actual, filter.values),
112
+ negated: filter.operator === 'nin'
113
+ };
82
114
  case 'between':
83
- return isBetween(actual, filter.start, filter.end);
84
115
  case 'nbetween':
85
- return !isBetween(actual, filter.start, filter.end);
116
+ return {
117
+ test: (actual)=>isBetween(actual, filter.start, filter.end),
118
+ negated: filter.operator === 'nbetween'
119
+ };
86
120
  case 'like':
87
- return matchesLike(actual, filter.value);
88
121
  case 'nlike':
89
- return !matchesLike(actual, filter.value);
122
+ return {
123
+ test: (actual)=>matchesLike(actual, filter.value),
124
+ negated: filter.operator === 'nlike'
125
+ };
90
126
  case 'isNull':
91
- return actual == null;
92
127
  case 'isNotNull':
93
- return actual != null;
128
+ return {
129
+ test: (actual)=>actual != null,
130
+ negated: filter.operator === 'isNull'
131
+ };
94
132
  default:
95
133
  {
96
134
  // Exhaustiveness guard — a new operator must be handled explicitly.
@@ -99,6 +137,13 @@ function matchesFilter(record, filter) {
99
137
  }
100
138
  }
101
139
  }
140
+ function matchesFilter(record, filter) {
141
+ const { test, negated } = positiveTest(filter);
142
+ const property = String(filter.property);
143
+ const elements = valuesThroughArray(record, property);
144
+ const holds = elements === undefined ? test(valueAt(record, property)) : elements.some(test);
145
+ return negated ? !holds : holds;
146
+ }
102
147
  function matchesNode(record, node) {
103
148
  if (!isFilterGroup(node)) return matchesFilter(record, node);
104
149
  return node.operator === 'or' ? node.values.some((child)=>matchesNode(record, child)) : node.values.every((child)=>matchesNode(record, child));
@@ -1 +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
+ {"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\n/**\n * The values a dotted path reaches **through an array** — every element's\n * member — or `undefined` when the path crosses no array, so the caller reads\n * the single value as before.\n */\nfunction valuesThroughArray(\n record: object,\n property: string,\n): unknown[] | undefined {\n const dot = property.indexOf('.');\n if (dot === -1) return undefined;\n const elements = storedValue(\n (record as Record<string, unknown>)[property.slice(0, dot)],\n );\n if (!Array.isArray(elements)) return undefined;\n const member = property.slice(dot + 1);\n return elements.map(element =>\n typeof element === 'object' && element !== null\n ? valueAt(element as object, member)\n : undefined,\n );\n}\n\n/**\n * Splits a filter into its positive test and whether the operator negates it,\n * so a path into an array can ask \"any element\" of the positive form and\n * negate the answer — never \"any element is not equal\", which almost always\n * holds.\n */\nfunction positiveTest<TEntity extends Entity>(\n filter: EntityFilter<TEntity>,\n): { readonly test: (actual: unknown) => boolean; readonly negated: boolean } {\n switch (filter.operator) {\n case 'eq':\n case 'ne':\n return {\n test: actual => isEqual(actual, filter.value),\n negated: filter.operator === 'ne',\n };\n case 'gt':\n return {\n test: actual =>\n actual != null && compareStoredValues(actual, filter.value) > 0,\n negated: false,\n };\n case 'gte':\n return {\n test: actual =>\n actual != null && compareStoredValues(actual, filter.value) >= 0,\n negated: false,\n };\n case 'lt':\n return {\n test: actual =>\n actual != null && compareStoredValues(actual, filter.value) < 0,\n negated: false,\n };\n case 'lte':\n return {\n test: actual =>\n actual != null && compareStoredValues(actual, filter.value) <= 0,\n negated: false,\n };\n case 'in':\n case 'nin':\n return {\n test: actual => matchesAny(actual, filter.values),\n negated: filter.operator === 'nin',\n };\n case 'between':\n case 'nbetween':\n return {\n test: actual => isBetween(actual, filter.start, filter.end),\n negated: filter.operator === 'nbetween',\n };\n case 'like':\n case 'nlike':\n return {\n test: actual => matchesLike(actual, filter.value),\n negated: filter.operator === 'nlike',\n };\n case 'isNull':\n case 'isNotNull':\n return {\n test: actual => actual != null,\n negated: filter.operator === 'isNull',\n };\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 matchesFilter<TEntity extends Entity>(\n record: TEntity,\n filter: EntityFilter<TEntity>,\n): boolean {\n const { test, negated } = positiveTest(filter);\n const property = String(filter.property);\n const elements = valuesThroughArray(record, property);\n const holds =\n elements === undefined\n ? test(valueAt(record, property))\n : elements.some(test);\n return negated ? !holds : holds;\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","valuesThroughArray","dot","indexOf","elements","slice","map","positiveTest","filter","negated","never","matchesFilter","holds","matchesNode","child","every","matchesFiltering","filtering","entry","applyFiltering","records","applySorting","sorting","keys","flatMap","Object","Number","sort","priority","length","compared","type"],"mappings":"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"}
@@ -34,7 +34,52 @@ export type EntityFilterNull<TEntity extends Entity, TKey extends keyof TEntity>
34
34
  property: TKey;
35
35
  operator: EntityFilterNullOperator;
36
36
  };
37
- export type EntityFilter<TEntity extends Entity> = EntityFilterBinary<TEntity, keyof TEntity> | EntityFilterArray<TEntity, keyof TEntity> | EntityFilterRange<TEntity, keyof TEntity> | EntityFilterString<TEntity> | EntityFilterNull<TEntity, keyof TEntity>;
37
+ /**
38
+ * One step into a member that holds an array of records: `items.vendorId` on
39
+ * an order whose `items` are `OrderItem[]`. One level only, which is all a
40
+ * filter on an embedded line needs.
41
+ *
42
+ * ⚠️ **Server-authored filters only.** A client's request is parsed against
43
+ * the entity's own `filterable` members, and a path is not one, so a browser
44
+ * still cannot name `items.vendorId`. This type exists so the server code that
45
+ * narrows a read — a scope conjoined onto a load request — can say what it
46
+ * means without a cast.
47
+ */
48
+ export type EntityMemberPath<TEntity> = {
49
+ [K in keyof TEntity & string]: NonNullable<TEntity[K]> extends readonly (infer E)[] ? E extends object ? `${K}.${keyof E & string}` : never : never;
50
+ }[keyof TEntity & string];
51
+ /** The type a value at an {@link EntityMemberPath} holds. */
52
+ export type EntityMemberPathValue<TEntity, TPath> = TPath extends `${infer K}.${infer M}` ? K extends keyof TEntity ? NonNullable<TEntity[K]> extends readonly (infer E)[] ? M extends keyof E ? E[M] : never : never : never : never;
53
+ /**
54
+ * A filter on an {@link EntityMemberPath}. It matches a record when **any**
55
+ * element of the array satisfies it, and a negated operator (`ne`, `nin`,
56
+ * `nbetween`, `nlike`, `isNull`) when **no** element satisfies its positive
57
+ * form — the semantics Mongo applies to a dotted path into an array.
58
+ */
59
+ export type EntityPathFilter<TEntity extends Entity> = {
60
+ [P in EntityMemberPath<TEntity>]: {
61
+ property: P;
62
+ operator: EntityFilterBinaryOperator;
63
+ value: EntityMemberPathValue<TEntity, P>;
64
+ } | {
65
+ property: P;
66
+ operator: EntityFilterArrayOperator;
67
+ values: Array<EntityMemberPathValue<TEntity, P>>;
68
+ } | {
69
+ property: P;
70
+ operator: EntityFilterRangeOperator;
71
+ start: EntityMemberPathValue<TEntity, P>;
72
+ end: EntityMemberPathValue<TEntity, P>;
73
+ } | {
74
+ property: P;
75
+ operator: EntityFilterStringOperator;
76
+ value: string;
77
+ } | {
78
+ property: P;
79
+ operator: EntityFilterNullOperator;
80
+ };
81
+ }[EntityMemberPath<TEntity>];
82
+ export type EntityFilter<TEntity extends Entity> = EntityFilterBinary<TEntity, keyof TEntity> | EntityFilterArray<TEntity, keyof TEntity> | EntityFilterRange<TEntity, keyof TEntity> | EntityFilterString<TEntity> | EntityFilterNull<TEntity, keyof TEntity> | EntityPathFilter<TEntity>;
38
83
  export declare const LogicOperators: readonly ["and", "or"];
39
84
  export type LogicOperator = (typeof LogicOperators)[number];
40
85
  export type FilterGroup<TEntity extends Entity> = {
@@ -1 +1 @@
1
- {"version":3,"file":"EntityFiltering.d.ts","sourceRoot":"","sources":["../../src/types/EntityFiltering.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"EntityFiltering.d.ts","sourceRoot":"","sources":["../../src/types/EntityFiltering.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/types/EntityFiltering.ts"],"sourcesContent":["import type { Entity } from './Entity.js';\n\nexport const EntityFilterBinaryOperators = [\n 'eq',\n 'ne',\n 'gt',\n 'gte',\n 'lt',\n 'lte',\n] as const;\nexport type EntityFilterBinaryOperator =\n (typeof EntityFilterBinaryOperators)[number];\nexport type EntityFilterBinary<\n TEntity extends Entity,\n TKey extends keyof TEntity,\n> = {\n property: TKey;\n operator: EntityFilterBinaryOperator;\n value: TEntity[TKey];\n};\n\nexport const EntityFilterArrayOperators = ['in', 'nin'] as const;\nexport type EntityFilterArrayOperator =\n (typeof EntityFilterArrayOperators)[number];\nexport type EntityFilterArray<\n TEntity extends Entity,\n TKey extends keyof TEntity,\n> = {\n property: TKey;\n operator: EntityFilterArrayOperator;\n values: Array<TEntity[TKey]>;\n};\n\nexport const EntityFilterRangeOperators = ['between', 'nbetween'] as const;\nexport type EntityFilterRangeOperator =\n (typeof EntityFilterRangeOperators)[number];\nexport type EntityFilterRange<\n TEntity extends Entity,\n TKey extends keyof TEntity,\n> = {\n property: TKey;\n operator: EntityFilterRangeOperator;\n start: TEntity[TKey];\n end: TEntity[TKey];\n};\n\nexport const EntityFilterStringOperators = ['like', 'nlike'] as const;\nexport type EntityFilterStringOperator =\n (typeof EntityFilterStringOperators)[number];\nexport type EntityFilterString<TEntity extends Entity> = {\n property: keyof TEntity;\n operator: EntityFilterStringOperator;\n value: string;\n};\n\nexport const EntityFilterNullOperators = ['isNull', 'isNotNull'] as const;\nexport type EntityFilterNullOperator =\n (typeof EntityFilterNullOperators)[number];\nexport type EntityFilterNull<\n TEntity extends Entity,\n TKey extends keyof TEntity,\n> = {\n property: TKey;\n operator: EntityFilterNullOperator;\n};\n\nexport type EntityFilter<TEntity extends Entity> =\n | EntityFilterBinary<TEntity, keyof TEntity>\n | EntityFilterArray<TEntity, keyof TEntity>\n | EntityFilterRange<TEntity, keyof TEntity>\n | EntityFilterString<TEntity>\n | EntityFilterNull<TEntity, keyof TEntity>;\n\nexport const LogicOperators = ['and', 'or'] as const;\nexport type LogicOperator = (typeof LogicOperators)[number];\nexport type FilterGroup<TEntity extends Entity> = {\n operator: LogicOperator;\n values: Array<EntityFilter<TEntity> | FilterGroup<TEntity>>;\n};\n\nexport type EntityFiltering<TEntity extends Entity> =\n EntityFilter<TEntity> | Array<EntityFilter<TEntity>> | FilterGroup<TEntity>;\n"],"names":["EntityFilterBinaryOperators","EntityFilterArrayOperators","EntityFilterRangeOperators","EntityFilterStringOperators","EntityFilterNullOperators","LogicOperators"],"mappings":"AAEA,OAAO,MAAMA,8BAA8B;IACzC;IACA;IACA;IACA;IACA;IACA;CACD,CAAU;AAYX,OAAO,MAAMC,6BAA6B;IAAC;IAAM;CAAM,CAAU;AAYjE,OAAO,MAAMC,6BAA6B;IAAC;IAAW;CAAW,CAAU;AAa3E,OAAO,MAAMC,8BAA8B;IAAC;IAAQ;CAAQ,CAAU;AAStE,OAAO,MAAMC,4BAA4B;IAAC;IAAU;CAAY,CAAU;AAkB1E,OAAO,MAAMC,iBAAiB;IAAC;IAAO;CAAK,CAAU"}
1
+ {"version":3,"sources":["../../src/types/EntityFiltering.ts"],"sourcesContent":["import type { Entity } from './Entity.js';\n\nexport const EntityFilterBinaryOperators = [\n 'eq',\n 'ne',\n 'gt',\n 'gte',\n 'lt',\n 'lte',\n] as const;\nexport type EntityFilterBinaryOperator =\n (typeof EntityFilterBinaryOperators)[number];\nexport type EntityFilterBinary<\n TEntity extends Entity,\n TKey extends keyof TEntity,\n> = {\n property: TKey;\n operator: EntityFilterBinaryOperator;\n value: TEntity[TKey];\n};\n\nexport const EntityFilterArrayOperators = ['in', 'nin'] as const;\nexport type EntityFilterArrayOperator =\n (typeof EntityFilterArrayOperators)[number];\nexport type EntityFilterArray<\n TEntity extends Entity,\n TKey extends keyof TEntity,\n> = {\n property: TKey;\n operator: EntityFilterArrayOperator;\n values: Array<TEntity[TKey]>;\n};\n\nexport const EntityFilterRangeOperators = ['between', 'nbetween'] as const;\nexport type EntityFilterRangeOperator =\n (typeof EntityFilterRangeOperators)[number];\nexport type EntityFilterRange<\n TEntity extends Entity,\n TKey extends keyof TEntity,\n> = {\n property: TKey;\n operator: EntityFilterRangeOperator;\n start: TEntity[TKey];\n end: TEntity[TKey];\n};\n\nexport const EntityFilterStringOperators = ['like', 'nlike'] as const;\nexport type EntityFilterStringOperator =\n (typeof EntityFilterStringOperators)[number];\nexport type EntityFilterString<TEntity extends Entity> = {\n property: keyof TEntity;\n operator: EntityFilterStringOperator;\n value: string;\n};\n\nexport const EntityFilterNullOperators = ['isNull', 'isNotNull'] as const;\nexport type EntityFilterNullOperator =\n (typeof EntityFilterNullOperators)[number];\nexport type EntityFilterNull<\n TEntity extends Entity,\n TKey extends keyof TEntity,\n> = {\n property: TKey;\n operator: EntityFilterNullOperator;\n};\n\n/**\n * One step into a member that holds an array of records: `items.vendorId` on\n * an order whose `items` are `OrderItem[]`. One level only, which is all a\n * filter on an embedded line needs.\n *\n * ⚠️ **Server-authored filters only.** A client's request is parsed against\n * the entity's own `filterable` members, and a path is not one, so a browser\n * still cannot name `items.vendorId`. This type exists so the server code that\n * narrows a read — a scope conjoined onto a load request — can say what it\n * means without a cast.\n */\nexport type EntityMemberPath<TEntity> = {\n [K in keyof TEntity & string]: NonNullable<\n TEntity[K]\n > extends readonly (infer E)[]\n ? E extends object\n ? `${K}.${keyof E & string}`\n : never\n : never;\n}[keyof TEntity & string];\n\n/** The type a value at an {@link EntityMemberPath} holds. */\nexport type EntityMemberPathValue<TEntity, TPath> =\n TPath extends `${infer K}.${infer M}`\n ? K extends keyof TEntity\n ? NonNullable<TEntity[K]> extends readonly (infer E)[]\n ? M extends keyof E\n ? E[M]\n : never\n : never\n : never\n : never;\n\n/**\n * A filter on an {@link EntityMemberPath}. It matches a record when **any**\n * element of the array satisfies it, and a negated operator (`ne`, `nin`,\n * `nbetween`, `nlike`, `isNull`) when **no** element satisfies its positive\n * form — the semantics Mongo applies to a dotted path into an array.\n */\nexport type EntityPathFilter<TEntity extends Entity> = {\n [P in EntityMemberPath<TEntity>]:\n | {\n property: P;\n operator: EntityFilterBinaryOperator;\n value: EntityMemberPathValue<TEntity, P>;\n }\n | {\n property: P;\n operator: EntityFilterArrayOperator;\n values: Array<EntityMemberPathValue<TEntity, P>>;\n }\n | {\n property: P;\n operator: EntityFilterRangeOperator;\n start: EntityMemberPathValue<TEntity, P>;\n end: EntityMemberPathValue<TEntity, P>;\n }\n | { property: P; operator: EntityFilterStringOperator; value: string }\n | { property: P; operator: EntityFilterNullOperator };\n}[EntityMemberPath<TEntity>];\n\nexport type EntityFilter<TEntity extends Entity> =\n | EntityFilterBinary<TEntity, keyof TEntity>\n | EntityFilterArray<TEntity, keyof TEntity>\n | EntityFilterRange<TEntity, keyof TEntity>\n | EntityFilterString<TEntity>\n | EntityFilterNull<TEntity, keyof TEntity>\n | EntityPathFilter<TEntity>;\n\nexport const LogicOperators = ['and', 'or'] as const;\nexport type LogicOperator = (typeof LogicOperators)[number];\nexport type FilterGroup<TEntity extends Entity> = {\n operator: LogicOperator;\n values: Array<EntityFilter<TEntity> | FilterGroup<TEntity>>;\n};\n\nexport type EntityFiltering<TEntity extends Entity> =\n EntityFilter<TEntity> | Array<EntityFilter<TEntity>> | FilterGroup<TEntity>;\n"],"names":["EntityFilterBinaryOperators","EntityFilterArrayOperators","EntityFilterRangeOperators","EntityFilterStringOperators","EntityFilterNullOperators","LogicOperators"],"mappings":"AAEA,OAAO,MAAMA,8BAA8B;IACzC;IACA;IACA;IACA;IACA;IACA;CACD,CAAU;AAYX,OAAO,MAAMC,6BAA6B;IAAC;IAAM;CAAM,CAAU;AAYjE,OAAO,MAAMC,6BAA6B;IAAC;IAAW;CAAW,CAAU;AAa3E,OAAO,MAAMC,8BAA8B;IAAC;IAAQ;CAAQ,CAAU;AAStE,OAAO,MAAMC,4BAA4B;IAAC;IAAU;CAAY,CAAU;AAgF1E,OAAO,MAAMC,iBAAiB;IAAC;IAAO;CAAK,CAAU"}
@@ -1,5 +1,6 @@
1
1
  import type { Entity, EntityId } from './Entity.js';
2
2
  import type { FilterGroup } from './EntityFiltering.js';
3
+ import type { EntityLoadRequest } from './EntityLoadRequest.js';
3
4
  /**
4
5
  * What a collection-bound action acts on.
5
6
  *
@@ -68,6 +69,22 @@ export declare function toggleSelected<TEntity extends Entity>(selection: Entity
68
69
  * nothing, where `undefined` cannot be sent at all.
69
70
  */
70
71
  export declare function selectionIds<TEntity extends Entity>(selection: EntitySelection<TEntity>): EntityId[] | undefined;
72
+ /**
73
+ * The load request that reads what a `matching` selection names, one page at a
74
+ * time.
75
+ *
76
+ * A selection carries **one** filter group, and a load request a list of them:
77
+ * the group is wrapped, never cast. A selection with no filtering reads every
78
+ * row. The selection's exclusions are not part of the request — a server
79
+ * resolving the set removes them from what the page answers, because an
80
+ * exclusion is a row the operator took out after the filter was applied.
81
+ */
82
+ export declare function loadRequestFor<TEntity extends Entity>(selection: Extract<EntitySelection<TEntity>, {
83
+ mode: 'matching';
84
+ }>, page: {
85
+ readonly page: number;
86
+ readonly pageSize: number;
87
+ }): EntityLoadRequest<TEntity>;
71
88
  /**
72
89
  * The selection as it crosses the wire.
73
90
  *
@@ -1 +1 @@
1
- {"version":3,"file":"EntitySelection.d.ts","sourceRoot":"","sources":["../../src/types/EntitySelection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,eAAe,CAAC,OAAO,SAAS,MAAM,IAC9C;IACE,IAAI,EAAE,KAAK,CAAC;IACZ,GAAG,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;CAC5B,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,gFAAgF;IAChF,SAAS,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACjC,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,QAAQ,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;CACjC,CAAC;AAEN,yEAAyE;AACzE,eAAO,MAAM,cAAc,GACzB,OAAO,SAAS,MAAM,OACnB,eAAe,CAAC,OAAO,CAAgD,CAAC;AAE7E;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,OAAO,SAAS,MAAM,EAClD,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC,GAClC,MAAM,CAIR;AAED,gDAAgD;AAChD,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,EAC/C,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC,EACnC,EAAE,EAAE,QAAQ,GACX,OAAO,CAIT;AAED,8CAA8C;AAC9C,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,MAAM,EACrD,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC,GAClC,OAAO,CAET;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,OAAO,SAAS,MAAM,EACnD,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC,EACnC,EAAE,EAAE,QAAQ,EACZ,QAAQ,EAAE,OAAO,GAChB,eAAe,CAAC,OAAO,CAAC,CAY1B;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,OAAO,SAAS,MAAM,EACjD,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC,GAClC,QAAQ,EAAE,GAAG,SAAS,CAExB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,mBAAmB,CAAC,OAAO,SAAS,MAAM,IAClD;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,QAAQ,EAAE,CAAA;CAAE,GAChC;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,QAAQ,EAAE,CAAC;CACtB,CAAC;AAEN;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,OAAO,SAAS,MAAM,EACpD,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC,GAClC,mBAAmB,CAAC,OAAO,CAAC,CAI9B;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,SAAS,MAAM,EACtD,KAAK,EAAE,OAAO,GACb,eAAe,CAAC,OAAO,CAAC,GAAG,SAAS,CA0BtC"}
1
+ {"version":3,"file":"EntitySelection.d.ts","sourceRoot":"","sources":["../../src/types/EntitySelection.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,eAAe,CAAC,OAAO,SAAS,MAAM,IAC9C;IACE,IAAI,EAAE,KAAK,CAAC;IACZ,GAAG,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;CAC5B,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,gFAAgF;IAChF,SAAS,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACjC,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,QAAQ,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;CACjC,CAAC;AAEN,yEAAyE;AACzE,eAAO,MAAM,cAAc,GACzB,OAAO,SAAS,MAAM,OACnB,eAAe,CAAC,OAAO,CAAgD,CAAC;AAE7E;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,OAAO,SAAS,MAAM,EAClD,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC,GAClC,MAAM,CAIR;AAED,gDAAgD;AAChD,wBAAgB,UAAU,CAAC,OAAO,SAAS,MAAM,EAC/C,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC,EACnC,EAAE,EAAE,QAAQ,GACX,OAAO,CAIT;AAED,8CAA8C;AAC9C,wBAAgB,gBAAgB,CAAC,OAAO,SAAS,MAAM,EACrD,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC,GAClC,OAAO,CAET;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,OAAO,SAAS,MAAM,EACnD,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC,EACnC,EAAE,EAAE,QAAQ,EACZ,QAAQ,EAAE,OAAO,GAChB,eAAe,CAAC,OAAO,CAAC,CAY1B;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,OAAO,SAAS,MAAM,EACjD,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC,GAClC,QAAQ,EAAE,GAAG,SAAS,CAExB;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,OAAO,SAAS,MAAM,EACnD,SAAS,EAAE,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAC,EAClE,IAAI,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACzD,iBAAiB,CAAC,OAAO,CAAC,CAQ5B;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,mBAAmB,CAAC,OAAO,SAAS,MAAM,IAClD;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,QAAQ,EAAE,CAAA;CAAE,GAChC;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,CAAC,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,QAAQ,EAAE,CAAC;CACtB,CAAC;AAEN;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,OAAO,SAAS,MAAM,EACpD,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC,GAClC,mBAAmB,CAAC,OAAO,CAAC,CAI9B;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,SAAS,MAAM,EACtD,KAAK,EAAE,OAAO,GACb,eAAe,CAAC,OAAO,CAAC,GAAG,SAAS,CA0BtC"}
@@ -54,6 +54,25 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
54
54
  */ export function selectionIds(selection) {
55
55
  return selection.mode === 'ids' ? Array.from(selection.ids) : undefined;
56
56
  }
57
+ /**
58
+ * The load request that reads what a `matching` selection names, one page at a
59
+ * time.
60
+ *
61
+ * A selection carries **one** filter group, and a load request a list of them:
62
+ * the group is wrapped, never cast. A selection with no filtering reads every
63
+ * row. The selection's exclusions are not part of the request — a server
64
+ * resolving the set removes them from what the page answers, because an
65
+ * exclusion is a row the operator took out after the filter was applied.
66
+ */ export function loadRequestFor(selection, page) {
67
+ return _extends({}, selection.filtering === undefined ? {} : {
68
+ filtering: [
69
+ selection.filtering
70
+ ]
71
+ }, {
72
+ page: page.page,
73
+ pageSize: page.pageSize
74
+ });
75
+ }
57
76
  /**
58
77
  * Convert a selection to its wire form.
59
78
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/types/EntitySelection.ts"],"sourcesContent":["import type { Entity, EntityId } from './Entity.js';\nimport type { FilterGroup } from './EntityFiltering.js';\n\n/**\n * What a collection-bound action acts on.\n *\n * The two members are **not** two shapes of one idea, and collapsing them is\n * the defect this type exists to prevent. \"Select all on this page\" is a list\n * of ids the browser already holds; \"select all 3.200 matching the filter\" is a\n * *filter expression the server evaluates* — the set is by definition larger\n * than the page, so there is nothing to enumerate. Written as one optional\n * field (`ids?: …` beside `all?: boolean`) the second quietly becomes the\n * first, and a bulk action that reads as \"the 25 rows I can see\" runs over\n * every row in the store.\n *\n * `excluded` is what makes the second member usable rather than all-or-nothing:\n * an operator takes everything matching and then drops the two rows they know\n * about. It is a subtraction from a server-side set, never an addition to a\n * client-side one.\n *\n * `total` is carried rather than derived because only the server knows it, and\n * the count is the whole affordance — \"Retirar 3.200 marcas\" is a different\n * sentence from \"Retirar\", and the user has to read the number *before* they\n * confirm.\n */\nexport type EntitySelection<TEntity extends Entity> =\n | {\n mode: 'ids';\n ids: ReadonlySet<EntityId>;\n }\n | {\n mode: 'matching';\n /** The filtering in effect when the set was taken. Absent means \"every row\". */\n filtering?: FilterGroup<TEntity>;\n /** How many rows the server said match. */\n total: number;\n /** Rows the user removed from the matching set. */\n excluded: ReadonlySet<EntityId>;\n };\n\n/** An empty `ids` selection — the state a table starts and clears to. */\nexport const emptySelection = <\n TEntity extends Entity,\n>(): EntitySelection<TEntity> => ({ mode: 'ids', ids: new Set<EntityId>() });\n\n/**\n * How many rows the action will touch.\n *\n * For `matching` this is the server's count minus what the user removed, which\n * is the number the confirmation must show. It can only ever be an estimate —\n * rows may have been written since the count was taken — and that is inherent\n * to the mode rather than a defect in it.\n */\nexport function selectionSize<TEntity extends Entity>(\n selection: EntitySelection<TEntity>,\n): number {\n return selection.mode === 'ids'\n ? selection.ids.size\n : Math.max(0, selection.total - selection.excluded.size);\n}\n\n/** Whether one row is part of the selection. */\nexport function isSelected<TEntity extends Entity>(\n selection: EntitySelection<TEntity>,\n id: EntityId,\n): boolean {\n return selection.mode === 'ids'\n ? selection.ids.has(id)\n : !selection.excluded.has(id);\n}\n\n/** Whether the action would touch nothing. */\nexport function isSelectionEmpty<TEntity extends Entity>(\n selection: EntitySelection<TEntity>,\n): boolean {\n return selectionSize(selection) === 0;\n}\n\n/**\n * Add or remove one row, in whichever direction the current mode means.\n *\n * In `ids` mode a toggle writes the id set; in `matching` mode it writes the\n * *exclusion* set, because the selection is the server's answer and the only\n * thing the browser owns is what was taken back out of it. Both return a new\n * value — the selection is controlled state held above the table.\n */\nexport function toggleSelected<TEntity extends Entity>(\n selection: EntitySelection<TEntity>,\n id: EntityId,\n selected: boolean,\n): EntitySelection<TEntity> {\n if (selection.mode === 'ids') {\n const ids = new Set(selection.ids);\n if (selected) ids.add(id);\n else ids.delete(id);\n return { mode: 'ids', ids };\n }\n\n const excluded = new Set(selection.excluded);\n if (selected) excluded.delete(id);\n else excluded.add(id);\n return { ...selection, excluded };\n}\n\n/**\n * The ids a bulk request should carry, or `undefined` when it must carry the\n * filter instead.\n *\n * A caller that treats `undefined` as \"no rows\" has made exactly the mistake\n * the two modes exist to prevent, so this returns `undefined` rather than an\n * empty array: an empty array is a plausible payload and would silently do\n * nothing, where `undefined` cannot be sent at all.\n */\nexport function selectionIds<TEntity extends Entity>(\n selection: EntitySelection<TEntity>,\n): EntityId[] | undefined {\n return selection.mode === 'ids' ? Array.from(selection.ids) : undefined;\n}\n\n/**\n * The selection as it crosses the wire.\n *\n * `Set` does not survive `JSON.stringify` — it serializes to `{}`, silently,\n * and arrives as an empty object that reads as \"no rows excluded\" rather than\n * as an error. That is the same class of fault an autosaved draft meets, and it is worse here: a `matching` selection whose exclusions\n * evaporate acts on rows the operator deliberately took out.\n *\n * So the wire form is arrays and the in-memory form is sets, named separately\n * rather than one type used loosely. The set is the right in-memory shape\n * because it dedupes by construction and answers `isSelected` in constant time;\n * the array is the only JSON shape there is.\n */\nexport type EntitySelectionWire<TEntity extends Entity> =\n | { mode: 'ids'; ids: EntityId[] }\n | {\n mode: 'matching';\n filtering?: FilterGroup<TEntity>;\n total: number;\n excluded: EntityId[];\n };\n\n/**\n * Convert a selection to its wire form.\n *\n * ⚠️ **`Array.from(set)`, never `[...set]`.** Every package here compiles with\n * `\"loose\": true` in its `.swcrc`, and SWC's loose spread helper skips the\n * iterable protocol: it treats the operand as array-like, finds no `length` on\n * a `Set`, and **wraps it as a single element** — `[...someSet]` becomes\n * `[Set]`, silently, with the right `.size` still on the inner value. Measured\n * against the running service, where it turned a two-row selection into one\n * outcome whose `id` serialized as `{}`. Vitest and Turbopack transpile\n * differently, so it passes every unit test and only fails in the built\n * bundle.\n */\nexport function toWireSelection<TEntity extends Entity>(\n selection: EntitySelection<TEntity>,\n): EntitySelectionWire<TEntity> {\n return selection.mode === 'ids'\n ? { mode: 'ids', ids: Array.from(selection.ids) }\n : { ...selection, excluded: Array.from(selection.excluded) };\n}\n\n/**\n * Read a wire selection back, rejecting anything that is not one.\n *\n * Untrusted input: this arrives in a request body and decides which rows a\n * bulk write touches. A missing `mode` must not fall through to a default —\n * defaulting to `ids` with no ids would act on nothing (merely confusing),\n * while defaulting to `matching` would act on **everything**.\n */\nexport function readWireSelection<TEntity extends Entity>(\n value: unknown,\n): EntitySelection<TEntity> | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const candidate = value as Partial<EntitySelectionWire<TEntity>>;\n\n if (candidate.mode === 'ids') {\n return Array.isArray(candidate.ids)\n ? { mode: 'ids', ids: new Set(candidate.ids) }\n : undefined;\n }\n\n if (candidate.mode === 'matching') {\n const matching = candidate as Extract<\n EntitySelectionWire<TEntity>,\n { mode: 'matching' }\n >;\n if (typeof matching.total !== 'number') return undefined;\n if (!Array.isArray(matching.excluded)) return undefined;\n return {\n mode: 'matching',\n filtering: matching.filtering,\n total: matching.total,\n excluded: new Set(matching.excluded),\n };\n }\n\n return undefined;\n}\n"],"names":["emptySelection","mode","ids","Set","selectionSize","selection","size","Math","max","total","excluded","isSelected","id","has","isSelectionEmpty","toggleSelected","selected","add","delete","selectionIds","Array","from","undefined","toWireSelection","readWireSelection","value","candidate","isArray","matching","filtering"],"mappings":";AAwCA,uEAAuE,GACvE,OAAO,MAAMA,iBAAiB,IAEI,CAAA;QAAEC,MAAM;QAAOC,KAAK,IAAIC;IAAgB,CAAA,EAAG;AAE7E;;;;;;;CAOC,GACD,OAAO,SAASC,cACdC,SAAmC;IAEnC,OAAOA,UAAUJ,IAAI,KAAK,QACtBI,UAAUH,GAAG,CAACI,IAAI,GAClBC,KAAKC,GAAG,CAAC,GAAGH,UAAUI,KAAK,GAAGJ,UAAUK,QAAQ,CAACJ,IAAI;AAC3D;AAEA,8CAA8C,GAC9C,OAAO,SAASK,WACdN,SAAmC,EACnCO,EAAY;IAEZ,OAAOP,UAAUJ,IAAI,KAAK,QACtBI,UAAUH,GAAG,CAACW,GAAG,CAACD,MAClB,CAACP,UAAUK,QAAQ,CAACG,GAAG,CAACD;AAC9B;AAEA,4CAA4C,GAC5C,OAAO,SAASE,iBACdT,SAAmC;IAEnC,OAAOD,cAAcC,eAAe;AACtC;AAEA;;;;;;;CAOC,GACD,OAAO,SAASU,eACdV,SAAmC,EACnCO,EAAY,EACZI,QAAiB;IAEjB,IAAIX,UAAUJ,IAAI,KAAK,OAAO;QAC5B,MAAMC,MAAM,IAAIC,IAAIE,UAAUH,GAAG;QACjC,IAAIc,UAAUd,IAAIe,GAAG,CAACL;aACjBV,IAAIgB,MAAM,CAACN;QAChB,OAAO;YAAEX,MAAM;YAAOC;QAAI;IAC5B;IAEA,MAAMQ,WAAW,IAAIP,IAAIE,UAAUK,QAAQ;IAC3C,IAAIM,UAAUN,SAASQ,MAAM,CAACN;SACzBF,SAASO,GAAG,CAACL;IAClB,OAAO,aAAKP;QAAWK;;AACzB;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASS,aACdd,SAAmC;IAEnC,OAAOA,UAAUJ,IAAI,KAAK,QAAQmB,MAAMC,IAAI,CAAChB,UAAUH,GAAG,IAAIoB;AAChE;AAwBA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,gBACdlB,SAAmC;IAEnC,OAAOA,UAAUJ,IAAI,KAAK,QACtB;QAAEA,MAAM;QAAOC,KAAKkB,MAAMC,IAAI,CAAChB,UAAUH,GAAG;IAAE,IAC9C,aAAKG;QAAWK,UAAUU,MAAMC,IAAI,CAAChB,UAAUK,QAAQ;;AAC7D;AAEA;;;;;;;CAOC,GACD,OAAO,SAASc,kBACdC,KAAc;IAEd,IAAI,OAAOA,UAAU,YAAYA,UAAU,MAAM,OAAOH;IACxD,MAAMI,YAAYD;IAElB,IAAIC,UAAUzB,IAAI,KAAK,OAAO;QAC5B,OAAOmB,MAAMO,OAAO,CAACD,UAAUxB,GAAG,IAC9B;YAAED,MAAM;YAAOC,KAAK,IAAIC,IAAIuB,UAAUxB,GAAG;QAAE,IAC3CoB;IACN;IAEA,IAAII,UAAUzB,IAAI,KAAK,YAAY;QACjC,MAAM2B,WAAWF;QAIjB,IAAI,OAAOE,SAASnB,KAAK,KAAK,UAAU,OAAOa;QAC/C,IAAI,CAACF,MAAMO,OAAO,CAACC,SAASlB,QAAQ,GAAG,OAAOY;QAC9C,OAAO;YACLrB,MAAM;YACN4B,WAAWD,SAASC,SAAS;YAC7BpB,OAAOmB,SAASnB,KAAK;YACrBC,UAAU,IAAIP,IAAIyB,SAASlB,QAAQ;QACrC;IACF;IAEA,OAAOY;AACT"}
1
+ {"version":3,"sources":["../../src/types/EntitySelection.ts"],"sourcesContent":["import type { Entity, EntityId } from './Entity.js';\nimport type { FilterGroup } from './EntityFiltering.js';\nimport type { EntityLoadRequest } from './EntityLoadRequest.js';\n\n/**\n * What a collection-bound action acts on.\n *\n * The two members are **not** two shapes of one idea, and collapsing them is\n * the defect this type exists to prevent. \"Select all on this page\" is a list\n * of ids the browser already holds; \"select all 3.200 matching the filter\" is a\n * *filter expression the server evaluates* — the set is by definition larger\n * than the page, so there is nothing to enumerate. Written as one optional\n * field (`ids?: …` beside `all?: boolean`) the second quietly becomes the\n * first, and a bulk action that reads as \"the 25 rows I can see\" runs over\n * every row in the store.\n *\n * `excluded` is what makes the second member usable rather than all-or-nothing:\n * an operator takes everything matching and then drops the two rows they know\n * about. It is a subtraction from a server-side set, never an addition to a\n * client-side one.\n *\n * `total` is carried rather than derived because only the server knows it, and\n * the count is the whole affordance — \"Retirar 3.200 marcas\" is a different\n * sentence from \"Retirar\", and the user has to read the number *before* they\n * confirm.\n */\nexport type EntitySelection<TEntity extends Entity> =\n | {\n mode: 'ids';\n ids: ReadonlySet<EntityId>;\n }\n | {\n mode: 'matching';\n /** The filtering in effect when the set was taken. Absent means \"every row\". */\n filtering?: FilterGroup<TEntity>;\n /** How many rows the server said match. */\n total: number;\n /** Rows the user removed from the matching set. */\n excluded: ReadonlySet<EntityId>;\n };\n\n/** An empty `ids` selection — the state a table starts and clears to. */\nexport const emptySelection = <\n TEntity extends Entity,\n>(): EntitySelection<TEntity> => ({ mode: 'ids', ids: new Set<EntityId>() });\n\n/**\n * How many rows the action will touch.\n *\n * For `matching` this is the server's count minus what the user removed, which\n * is the number the confirmation must show. It can only ever be an estimate —\n * rows may have been written since the count was taken — and that is inherent\n * to the mode rather than a defect in it.\n */\nexport function selectionSize<TEntity extends Entity>(\n selection: EntitySelection<TEntity>,\n): number {\n return selection.mode === 'ids'\n ? selection.ids.size\n : Math.max(0, selection.total - selection.excluded.size);\n}\n\n/** Whether one row is part of the selection. */\nexport function isSelected<TEntity extends Entity>(\n selection: EntitySelection<TEntity>,\n id: EntityId,\n): boolean {\n return selection.mode === 'ids'\n ? selection.ids.has(id)\n : !selection.excluded.has(id);\n}\n\n/** Whether the action would touch nothing. */\nexport function isSelectionEmpty<TEntity extends Entity>(\n selection: EntitySelection<TEntity>,\n): boolean {\n return selectionSize(selection) === 0;\n}\n\n/**\n * Add or remove one row, in whichever direction the current mode means.\n *\n * In `ids` mode a toggle writes the id set; in `matching` mode it writes the\n * *exclusion* set, because the selection is the server's answer and the only\n * thing the browser owns is what was taken back out of it. Both return a new\n * value — the selection is controlled state held above the table.\n */\nexport function toggleSelected<TEntity extends Entity>(\n selection: EntitySelection<TEntity>,\n id: EntityId,\n selected: boolean,\n): EntitySelection<TEntity> {\n if (selection.mode === 'ids') {\n const ids = new Set(selection.ids);\n if (selected) ids.add(id);\n else ids.delete(id);\n return { mode: 'ids', ids };\n }\n\n const excluded = new Set(selection.excluded);\n if (selected) excluded.delete(id);\n else excluded.add(id);\n return { ...selection, excluded };\n}\n\n/**\n * The ids a bulk request should carry, or `undefined` when it must carry the\n * filter instead.\n *\n * A caller that treats `undefined` as \"no rows\" has made exactly the mistake\n * the two modes exist to prevent, so this returns `undefined` rather than an\n * empty array: an empty array is a plausible payload and would silently do\n * nothing, where `undefined` cannot be sent at all.\n */\nexport function selectionIds<TEntity extends Entity>(\n selection: EntitySelection<TEntity>,\n): EntityId[] | undefined {\n return selection.mode === 'ids' ? Array.from(selection.ids) : undefined;\n}\n\n/**\n * The load request that reads what a `matching` selection names, one page at a\n * time.\n *\n * A selection carries **one** filter group, and a load request a list of them:\n * the group is wrapped, never cast. A selection with no filtering reads every\n * row. The selection's exclusions are not part of the request — a server\n * resolving the set removes them from what the page answers, because an\n * exclusion is a row the operator took out after the filter was applied.\n */\nexport function loadRequestFor<TEntity extends Entity>(\n selection: Extract<EntitySelection<TEntity>, { mode: 'matching' }>,\n page: { readonly page: number; readonly pageSize: number },\n): EntityLoadRequest<TEntity> {\n return {\n ...(selection.filtering === undefined\n ? {}\n : { filtering: [selection.filtering] }),\n page: page.page,\n pageSize: page.pageSize,\n };\n}\n\n/**\n * The selection as it crosses the wire.\n *\n * `Set` does not survive `JSON.stringify` — it serializes to `{}`, silently,\n * and arrives as an empty object that reads as \"no rows excluded\" rather than\n * as an error. That is the same class of fault an autosaved draft meets, and it is worse here: a `matching` selection whose exclusions\n * evaporate acts on rows the operator deliberately took out.\n *\n * So the wire form is arrays and the in-memory form is sets, named separately\n * rather than one type used loosely. The set is the right in-memory shape\n * because it dedupes by construction and answers `isSelected` in constant time;\n * the array is the only JSON shape there is.\n */\nexport type EntitySelectionWire<TEntity extends Entity> =\n | { mode: 'ids'; ids: EntityId[] }\n | {\n mode: 'matching';\n filtering?: FilterGroup<TEntity>;\n total: number;\n excluded: EntityId[];\n };\n\n/**\n * Convert a selection to its wire form.\n *\n * ⚠️ **`Array.from(set)`, never `[...set]`.** Every package here compiles with\n * `\"loose\": true` in its `.swcrc`, and SWC's loose spread helper skips the\n * iterable protocol: it treats the operand as array-like, finds no `length` on\n * a `Set`, and **wraps it as a single element** — `[...someSet]` becomes\n * `[Set]`, silently, with the right `.size` still on the inner value. Measured\n * against the running service, where it turned a two-row selection into one\n * outcome whose `id` serialized as `{}`. Vitest and Turbopack transpile\n * differently, so it passes every unit test and only fails in the built\n * bundle.\n */\nexport function toWireSelection<TEntity extends Entity>(\n selection: EntitySelection<TEntity>,\n): EntitySelectionWire<TEntity> {\n return selection.mode === 'ids'\n ? { mode: 'ids', ids: Array.from(selection.ids) }\n : { ...selection, excluded: Array.from(selection.excluded) };\n}\n\n/**\n * Read a wire selection back, rejecting anything that is not one.\n *\n * Untrusted input: this arrives in a request body and decides which rows a\n * bulk write touches. A missing `mode` must not fall through to a default —\n * defaulting to `ids` with no ids would act on nothing (merely confusing),\n * while defaulting to `matching` would act on **everything**.\n */\nexport function readWireSelection<TEntity extends Entity>(\n value: unknown,\n): EntitySelection<TEntity> | undefined {\n if (typeof value !== 'object' || value === null) return undefined;\n const candidate = value as Partial<EntitySelectionWire<TEntity>>;\n\n if (candidate.mode === 'ids') {\n return Array.isArray(candidate.ids)\n ? { mode: 'ids', ids: new Set(candidate.ids) }\n : undefined;\n }\n\n if (candidate.mode === 'matching') {\n const matching = candidate as Extract<\n EntitySelectionWire<TEntity>,\n { mode: 'matching' }\n >;\n if (typeof matching.total !== 'number') return undefined;\n if (!Array.isArray(matching.excluded)) return undefined;\n return {\n mode: 'matching',\n filtering: matching.filtering,\n total: matching.total,\n excluded: new Set(matching.excluded),\n };\n }\n\n return undefined;\n}\n"],"names":["emptySelection","mode","ids","Set","selectionSize","selection","size","Math","max","total","excluded","isSelected","id","has","isSelectionEmpty","toggleSelected","selected","add","delete","selectionIds","Array","from","undefined","loadRequestFor","page","filtering","pageSize","toWireSelection","readWireSelection","value","candidate","isArray","matching"],"mappings":";AAyCA,uEAAuE,GACvE,OAAO,MAAMA,iBAAiB,IAEI,CAAA;QAAEC,MAAM;QAAOC,KAAK,IAAIC;IAAgB,CAAA,EAAG;AAE7E;;;;;;;CAOC,GACD,OAAO,SAASC,cACdC,SAAmC;IAEnC,OAAOA,UAAUJ,IAAI,KAAK,QACtBI,UAAUH,GAAG,CAACI,IAAI,GAClBC,KAAKC,GAAG,CAAC,GAAGH,UAAUI,KAAK,GAAGJ,UAAUK,QAAQ,CAACJ,IAAI;AAC3D;AAEA,8CAA8C,GAC9C,OAAO,SAASK,WACdN,SAAmC,EACnCO,EAAY;IAEZ,OAAOP,UAAUJ,IAAI,KAAK,QACtBI,UAAUH,GAAG,CAACW,GAAG,CAACD,MAClB,CAACP,UAAUK,QAAQ,CAACG,GAAG,CAACD;AAC9B;AAEA,4CAA4C,GAC5C,OAAO,SAASE,iBACdT,SAAmC;IAEnC,OAAOD,cAAcC,eAAe;AACtC;AAEA;;;;;;;CAOC,GACD,OAAO,SAASU,eACdV,SAAmC,EACnCO,EAAY,EACZI,QAAiB;IAEjB,IAAIX,UAAUJ,IAAI,KAAK,OAAO;QAC5B,MAAMC,MAAM,IAAIC,IAAIE,UAAUH,GAAG;QACjC,IAAIc,UAAUd,IAAIe,GAAG,CAACL;aACjBV,IAAIgB,MAAM,CAACN;QAChB,OAAO;YAAEX,MAAM;YAAOC;QAAI;IAC5B;IAEA,MAAMQ,WAAW,IAAIP,IAAIE,UAAUK,QAAQ;IAC3C,IAAIM,UAAUN,SAASQ,MAAM,CAACN;SACzBF,SAASO,GAAG,CAACL;IAClB,OAAO,aAAKP;QAAWK;;AACzB;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASS,aACdd,SAAmC;IAEnC,OAAOA,UAAUJ,IAAI,KAAK,QAAQmB,MAAMC,IAAI,CAAChB,UAAUH,GAAG,IAAIoB;AAChE;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASC,eACdlB,SAAkE,EAClEmB,IAA0D;IAE1D,OAAO,aACDnB,UAAUoB,SAAS,KAAKH,YACxB,CAAC,IACD;QAAEG,WAAW;YAACpB,UAAUoB,SAAS;SAAC;IAAC;QACvCD,MAAMA,KAAKA,IAAI;QACfE,UAAUF,KAAKE,QAAQ;;AAE3B;AAwBA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,gBACdtB,SAAmC;IAEnC,OAAOA,UAAUJ,IAAI,KAAK,QACtB;QAAEA,MAAM;QAAOC,KAAKkB,MAAMC,IAAI,CAAChB,UAAUH,GAAG;IAAE,IAC9C,aAAKG;QAAWK,UAAUU,MAAMC,IAAI,CAAChB,UAAUK,QAAQ;;AAC7D;AAEA;;;;;;;CAOC,GACD,OAAO,SAASkB,kBACdC,KAAc;IAEd,IAAI,OAAOA,UAAU,YAAYA,UAAU,MAAM,OAAOP;IACxD,MAAMQ,YAAYD;IAElB,IAAIC,UAAU7B,IAAI,KAAK,OAAO;QAC5B,OAAOmB,MAAMW,OAAO,CAACD,UAAU5B,GAAG,IAC9B;YAAED,MAAM;YAAOC,KAAK,IAAIC,IAAI2B,UAAU5B,GAAG;QAAE,IAC3CoB;IACN;IAEA,IAAIQ,UAAU7B,IAAI,KAAK,YAAY;QACjC,MAAM+B,WAAWF;QAIjB,IAAI,OAAOE,SAASvB,KAAK,KAAK,UAAU,OAAOa;QAC/C,IAAI,CAACF,MAAMW,OAAO,CAACC,SAAStB,QAAQ,GAAG,OAAOY;QAC9C,OAAO;YACLrB,MAAM;YACNwB,WAAWO,SAASP,SAAS;YAC7BhB,OAAOuB,SAASvB,KAAK;YACrBC,UAAU,IAAIP,IAAI6B,SAAStB,QAAQ;QACrC;IACF;IAEA,OAAOY;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@entifix/core",
3
- "version": "0.7.1",
3
+ "version": "0.8.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": {