@entifix/core 0.7.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/envelope/make-envelope.d.ts +7 -1
- package/dist/envelope/make-envelope.d.ts.map +1 -1
- package/dist/envelope/make-envelope.js +12 -0
- package/dist/envelope/make-envelope.js.map +1 -1
- package/dist/envelope/read-envelope.d.ts +7 -0
- package/dist/envelope/read-envelope.d.ts.map +1 -1
- package/dist/envelope/read-envelope.js +20 -0
- package/dist/envelope/read-envelope.js.map +1 -1
- package/dist/envelope/types.d.ts +3 -1
- package/dist/envelope/types.d.ts.map +1 -1
- package/dist/envelope/types.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/query/fold.d.ts +40 -0
- package/dist/query/fold.d.ts.map +1 -0
- package/dist/query/fold.js +49 -0
- package/dist/query/fold.js.map +1 -0
- package/dist/query/in-memory-query.d.ts +12 -1
- package/dist/query/in-memory-query.d.ts.map +1 -1
- package/dist/query/in-memory-query.js +94 -19
- package/dist/query/in-memory-query.js.map +1 -1
- package/dist/rsql/facet-query-url.d.ts +25 -0
- package/dist/rsql/facet-query-url.d.ts.map +1 -0
- package/dist/rsql/facet-query-url.js +63 -0
- package/dist/rsql/facet-query-url.js.map +1 -0
- package/dist/rsql/index.d.ts +1 -0
- package/dist/rsql/index.d.ts.map +1 -1
- package/dist/rsql/index.js +1 -0
- package/dist/rsql/index.js.map +1 -1
- package/dist/types/EntityFacets.d.ts +30 -0
- package/dist/types/EntityFacets.d.ts.map +1 -0
- package/dist/types/EntityFacets.js +10 -0
- package/dist/types/EntityFacets.js.map +1 -0
- package/dist/types/EntityFiltering.d.ts +46 -1
- package/dist/types/EntityFiltering.d.ts.map +1 -1
- package/dist/types/EntityFiltering.js.map +1 -1
- package/dist/types/EntitySelection.d.ts +17 -0
- package/dist/types/EntitySelection.d.ts.map +1 -1
- package/dist/types/EntitySelection.js +19 -0
- package/dist/types/EntitySelection.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EntifixBuildError } from '../base-entities/entifix-error/index.js';
|
|
2
2
|
import { EntityCollectionLink } from '../entity-definition/links/entity-collection-link/index.js';
|
|
3
3
|
import { EntityLink } from '../entity-definition/links/entity-link/index.js';
|
|
4
|
+
import { containsFolded } from './fold.js';
|
|
4
5
|
/**
|
|
5
6
|
* An {@link EntityLoadRequest}'s filtering and sorting, answered over records
|
|
6
7
|
* held in memory, with the semantics of the Mongo adapter.
|
|
@@ -16,7 +17,8 @@ import { EntityLink } from '../entity-definition/links/entity-link/index.js';
|
|
|
16
17
|
* - `in` and `nin` against an array match when **any element** does, as `$in`;
|
|
17
18
|
* - a property may be a dotted path into a member holding an object, which is
|
|
18
19
|
* how a filter reaches one locale of a `localized` member (`name.en`);
|
|
19
|
-
* - `like` is a
|
|
20
|
+
* - `like` is a substring match that ignores accents and case — `cafe` finds
|
|
21
|
+
* `Café` — by the one rule in `fold.ts` every adapter derives its form from;
|
|
20
22
|
* - a missing value sorts below a present one, numbers and dates compare as
|
|
21
23
|
* themselves and anything else as text;
|
|
22
24
|
* - every top-level filtering entry is combined with `and`.
|
|
@@ -56,41 +58,78 @@ import { EntityLink } from '../entity-definition/links/entity-link/index.js';
|
|
|
56
58
|
const matches = (element)=>values.some((value)=>isEqual(element, value));
|
|
57
59
|
return Array.isArray(actual) ? actual.some(matches) : matches(actual);
|
|
58
60
|
};
|
|
59
|
-
/**
|
|
60
|
-
const matchesLike = (actual, value)=>actual != null && new RegExp(escapeRegex(value), 'i').test(String(actual));
|
|
61
|
+
/** `like`: a substring match with accents and case folded off (`fold.ts`). */ const matchesLike = (actual, value)=>actual != null && containsFolded(String(actual), value);
|
|
61
62
|
const isFilterGroup = (node)=>!('property' in node) && (node.operator === 'and' || node.operator === 'or');
|
|
62
63
|
const isBetween = (actual, start, end)=>actual != null && compareStoredValues(actual, start) >= 0 && compareStoredValues(actual, end) <= 0;
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
/**
|
|
65
|
+
* The values a dotted path reaches **through an array** — every element's
|
|
66
|
+
* member — or `undefined` when the path crosses no array, so the caller reads
|
|
67
|
+
* the single value as before.
|
|
68
|
+
*/ function valuesThroughArray(record, property) {
|
|
69
|
+
const dot = property.indexOf('.');
|
|
70
|
+
if (dot === -1) return undefined;
|
|
71
|
+
const elements = storedValue(record[property.slice(0, dot)]);
|
|
72
|
+
if (!Array.isArray(elements)) return undefined;
|
|
73
|
+
const member = property.slice(dot + 1);
|
|
74
|
+
return elements.map((element)=>typeof element === 'object' && element !== null ? valueAt(element, member) : undefined);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Splits a filter into its positive test and whether the operator negates it,
|
|
78
|
+
* so a path into an array can ask "any element" of the positive form and
|
|
79
|
+
* negate the answer — never "any element is not equal", which almost always
|
|
80
|
+
* holds.
|
|
81
|
+
*/ function positiveTest(filter) {
|
|
65
82
|
switch(filter.operator){
|
|
66
83
|
case 'eq':
|
|
67
|
-
return isEqual(actual, filter.value);
|
|
68
84
|
case 'ne':
|
|
69
|
-
return
|
|
85
|
+
return {
|
|
86
|
+
test: (actual)=>isEqual(actual, filter.value),
|
|
87
|
+
negated: filter.operator === 'ne'
|
|
88
|
+
};
|
|
70
89
|
case 'gt':
|
|
71
|
-
return
|
|
90
|
+
return {
|
|
91
|
+
test: (actual)=>actual != null && compareStoredValues(actual, filter.value) > 0,
|
|
92
|
+
negated: false
|
|
93
|
+
};
|
|
72
94
|
case 'gte':
|
|
73
|
-
return
|
|
95
|
+
return {
|
|
96
|
+
test: (actual)=>actual != null && compareStoredValues(actual, filter.value) >= 0,
|
|
97
|
+
negated: false
|
|
98
|
+
};
|
|
74
99
|
case 'lt':
|
|
75
|
-
return
|
|
100
|
+
return {
|
|
101
|
+
test: (actual)=>actual != null && compareStoredValues(actual, filter.value) < 0,
|
|
102
|
+
negated: false
|
|
103
|
+
};
|
|
76
104
|
case 'lte':
|
|
77
|
-
return
|
|
105
|
+
return {
|
|
106
|
+
test: (actual)=>actual != null && compareStoredValues(actual, filter.value) <= 0,
|
|
107
|
+
negated: false
|
|
108
|
+
};
|
|
78
109
|
case 'in':
|
|
79
|
-
return matchesAny(actual, filter.values);
|
|
80
110
|
case 'nin':
|
|
81
|
-
return
|
|
111
|
+
return {
|
|
112
|
+
test: (actual)=>matchesAny(actual, filter.values),
|
|
113
|
+
negated: filter.operator === 'nin'
|
|
114
|
+
};
|
|
82
115
|
case 'between':
|
|
83
|
-
return isBetween(actual, filter.start, filter.end);
|
|
84
116
|
case 'nbetween':
|
|
85
|
-
return
|
|
117
|
+
return {
|
|
118
|
+
test: (actual)=>isBetween(actual, filter.start, filter.end),
|
|
119
|
+
negated: filter.operator === 'nbetween'
|
|
120
|
+
};
|
|
86
121
|
case 'like':
|
|
87
|
-
return matchesLike(actual, filter.value);
|
|
88
122
|
case 'nlike':
|
|
89
|
-
return
|
|
123
|
+
return {
|
|
124
|
+
test: (actual)=>matchesLike(actual, filter.value),
|
|
125
|
+
negated: filter.operator === 'nlike'
|
|
126
|
+
};
|
|
90
127
|
case 'isNull':
|
|
91
|
-
return actual == null;
|
|
92
128
|
case 'isNotNull':
|
|
93
|
-
return
|
|
129
|
+
return {
|
|
130
|
+
test: (actual)=>actual != null,
|
|
131
|
+
negated: filter.operator === 'isNull'
|
|
132
|
+
};
|
|
94
133
|
default:
|
|
95
134
|
{
|
|
96
135
|
// Exhaustiveness guard — a new operator must be handled explicitly.
|
|
@@ -99,6 +138,13 @@ function matchesFilter(record, filter) {
|
|
|
99
138
|
}
|
|
100
139
|
}
|
|
101
140
|
}
|
|
141
|
+
function matchesFilter(record, filter) {
|
|
142
|
+
const { test, negated } = positiveTest(filter);
|
|
143
|
+
const property = String(filter.property);
|
|
144
|
+
const elements = valuesThroughArray(record, property);
|
|
145
|
+
const holds = elements === undefined ? test(valueAt(record, property)) : elements.some(test);
|
|
146
|
+
return negated ? !holds : holds;
|
|
147
|
+
}
|
|
102
148
|
function matchesNode(record, node) {
|
|
103
149
|
if (!isFilterGroup(node)) return matchesFilter(record, node);
|
|
104
150
|
return node.operator === 'or' ? node.values.some((child)=>matchesNode(record, child)) : node.values.every((child)=>matchesNode(record, child));
|
|
@@ -129,5 +175,34 @@ function matchesNode(record, node) {
|
|
|
129
175
|
return 0;
|
|
130
176
|
});
|
|
131
177
|
}
|
|
178
|
+
const isFacetValue = (value)=>typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
|
|
179
|
+
/** Count first, highest first; a tie by value, so the order is stable. */ const byCountThenValue = (left, right)=>right.count - left.count || compareStoredValues(left.value, right.value);
|
|
180
|
+
/**
|
|
181
|
+
* How many records hold each value of each queried member, each member counted
|
|
182
|
+
* over the records **its own** filtering keeps — the in-memory form of a Mongo
|
|
183
|
+
* `$facet` of `$match`, `$unwind` and `$group`, with its rules:
|
|
184
|
+
* - an array member counts each element, as `$unwind` does;
|
|
185
|
+
* - a missing or `null` value, or an empty array, is not counted;
|
|
186
|
+
* - only a string, number or boolean is a value; anything else is skipped.
|
|
187
|
+
*/ export function countFacets(records, queries) {
|
|
188
|
+
const counts = {};
|
|
189
|
+
for (const { member, filtering } of queries){
|
|
190
|
+
const tally = new Map();
|
|
191
|
+
for (const record of applyFiltering(records, filtering)){
|
|
192
|
+
const stored = valueAt(record, member);
|
|
193
|
+
for (const value of Array.isArray(stored) ? stored : [
|
|
194
|
+
stored
|
|
195
|
+
]){
|
|
196
|
+
var _tally_get;
|
|
197
|
+
if (isFacetValue(value)) tally.set(value, ((_tally_get = tally.get(value)) != null ? _tally_get : 0) + 1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
counts[member] = Array.from(tally, ([value, count])=>({
|
|
201
|
+
value,
|
|
202
|
+
count
|
|
203
|
+
})).sort(byCountThenValue);
|
|
204
|
+
}
|
|
205
|
+
return counts;
|
|
206
|
+
}
|
|
132
207
|
|
|
133
208
|
//# sourceMappingURL=in-memory-query.js.map
|
|
@@ -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 EntityFacetCounts,\n EntityFacetQuery,\n EntityFacetValue,\n} from '../types/EntityFacets.js';\nimport type {\n EntityFilter,\n EntityFiltering,\n FilterGroup,\n} from '../types/EntityFiltering.js';\nimport type { EntitySorting } from '../types/EntitySorting.js';\nimport { containsFolded } from './fold.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 substring match that ignores accents and case — `cafe` finds\n * `Café` — by the one rule in `fold.ts` every adapter derives its form from;\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/** `like`: a substring match with accents and case folded off (`fold.ts`). */\nconst matchesLike = (actual: unknown, value: string): boolean =>\n actual != null && containsFolded(String(actual), value);\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\nconst isFacetValue = (value: unknown): value is EntityFacetValue['value'] =>\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean';\n\n/** Count first, highest first; a tie by value, so the order is stable. */\nconst byCountThenValue = (left: EntityFacetValue, right: EntityFacetValue) =>\n right.count - left.count || compareStoredValues(left.value, right.value);\n\n/**\n * How many records hold each value of each queried member, each member counted\n * over the records **its own** filtering keeps — the in-memory form of a Mongo\n * `$facet` of `$match`, `$unwind` and `$group`, with its rules:\n * - an array member counts each element, as `$unwind` does;\n * - a missing or `null` value, or an empty array, is not counted;\n * - only a string, number or boolean is a value; anything else is skipped.\n */\nexport function countFacets<TEntity extends Entity>(\n records: readonly TEntity[],\n queries: readonly EntityFacetQuery<TEntity>[],\n): EntityFacetCounts {\n const counts: Record<string, readonly EntityFacetValue[]> = {};\n for (const { member, filtering } of queries) {\n const tally = new Map<EntityFacetValue['value'], number>();\n for (const record of applyFiltering(records, filtering)) {\n const stored = valueAt(record, member);\n for (const value of Array.isArray(stored) ? stored : [stored]) {\n if (isFacetValue(value)) tally.set(value, (tally.get(value) ?? 0) + 1);\n }\n }\n counts[member] = Array.from(tally, ([value, count]) => ({\n value,\n count,\n })).sort(byCountThenValue);\n }\n return counts;\n}\n"],"names":["EntifixBuildError","EntityCollectionLink","EntityLink","containsFolded","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","matchesLike","isFilterGroup","node","operator","isBetween","start","end","valuesThroughArray","dot","indexOf","elements","slice","map","positiveTest","filter","test","negated","never","matchesFilter","holds","matchesNode","child","every","matchesFiltering","filtering","entry","applyFiltering","records","applySorting","sorting","keys","flatMap","Object","Number","sort","priority","length","compared","type","isFacetValue","byCountThenValue","count","countFacets","queries","counts","tally","Map","stored","set","get","from"],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Entity, EntityConstructor } from '../types/Entity.js';
|
|
2
|
+
import type { EntityFacetQuery } from '../types/EntityFacets.js';
|
|
3
|
+
/** The parameter naming one member to count, repeated once per facet. */
|
|
4
|
+
export declare const FACET_PARAM = "facet";
|
|
5
|
+
/** The parameter holding one facet's own filtering: `rsql.<member>`. */
|
|
6
|
+
export declare const facetFilterParam: (member: string) => string;
|
|
7
|
+
/**
|
|
8
|
+
* Serializes facet queries into query parameters: each member as a `facet`,
|
|
9
|
+
* and each one's filtering as RSQL under `rsql.<member>` — omitted when it
|
|
10
|
+
* filters nothing, so an unfiltered count keeps a clean URL.
|
|
11
|
+
*/
|
|
12
|
+
export declare function serializeFacetQueryParams<TEntity extends Entity>(queries: readonly EntityFacetQuery<TEntity>[]): URLSearchParams;
|
|
13
|
+
/**
|
|
14
|
+
* Parses query parameters into facet queries, validated against the entity's
|
|
15
|
+
* own metadata — the server-side half of the facet protocol, and the same
|
|
16
|
+
* allowlist a load uses.
|
|
17
|
+
*
|
|
18
|
+
* A counted member must be `filterable`: a count is a question about the
|
|
19
|
+
* values a filter would choose between, and counting a member nobody may
|
|
20
|
+
* filter on would read out values the entity chose not to expose. A
|
|
21
|
+
* `localized` member is refused, since its value is a record of texts, not one
|
|
22
|
+
* to count. Every failure is an {@link EntifixBuildError}, so a `400`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseFacetQueryParams<TEntity extends Entity>(entityConstructor: EntityConstructor<TEntity>, params: URLSearchParams): EntityFacetQuery<TEntity>[];
|
|
25
|
+
//# sourceMappingURL=facet-query-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"facet-query-url.d.ts","sourceRoot":"","sources":["../../src/rsql/facet-query-url.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAUjE,yEAAyE;AACzE,eAAO,MAAM,WAAW,UAAU,CAAC;AAEnC,wEAAwE;AACxE,eAAO,MAAM,gBAAgB,GAAI,QAAQ,MAAM,KAAG,MACvB,CAAC;AAE5B;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,SAAS,MAAM,EAC9D,OAAO,EAAE,SAAS,gBAAgB,CAAC,OAAO,CAAC,EAAE,GAC5C,eAAe,CAQjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,SAAS,MAAM,EAC1D,iBAAiB,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAC7C,MAAM,EAAE,eAAe,GACtB,gBAAgB,CAAC,OAAO,CAAC,EAAE,CA6C7B"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
import { EntifixBuildError } from '../base-entities/entifix-error/index.js';
|
|
3
|
+
import { allowedDescriptors, coerceFiltering, resolveAllowedProperty } from './coerce-rsql.js';
|
|
4
|
+
import { RSQL_PARAM } from './load-request-url.js';
|
|
5
|
+
import { parseRsql } from './parse-rsql.js';
|
|
6
|
+
import { serializeRsql } from './serialize-rsql.js';
|
|
7
|
+
/** The parameter naming one member to count, repeated once per facet. */ export const FACET_PARAM = 'facet';
|
|
8
|
+
/** The parameter holding one facet's own filtering: `rsql.<member>`. */ export const facetFilterParam = (member)=>`${RSQL_PARAM}.${member}`;
|
|
9
|
+
/**
|
|
10
|
+
* Serializes facet queries into query parameters: each member as a `facet`,
|
|
11
|
+
* and each one's filtering as RSQL under `rsql.<member>` — omitted when it
|
|
12
|
+
* filters nothing, so an unfiltered count keeps a clean URL.
|
|
13
|
+
*/ export function serializeFacetQueryParams(queries) {
|
|
14
|
+
const params = new URLSearchParams();
|
|
15
|
+
for (const { member, filtering } of queries){
|
|
16
|
+
params.append(FACET_PARAM, member);
|
|
17
|
+
const rsql = serializeRsql(filtering);
|
|
18
|
+
if (rsql !== '') params.set(facetFilterParam(member), rsql);
|
|
19
|
+
}
|
|
20
|
+
return params;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parses query parameters into facet queries, validated against the entity's
|
|
24
|
+
* own metadata — the server-side half of the facet protocol, and the same
|
|
25
|
+
* allowlist a load uses.
|
|
26
|
+
*
|
|
27
|
+
* A counted member must be `filterable`: a count is a question about the
|
|
28
|
+
* values a filter would choose between, and counting a member nobody may
|
|
29
|
+
* filter on would read out values the entity chose not to expose. A
|
|
30
|
+
* `localized` member is refused, since its value is a record of texts, not one
|
|
31
|
+
* to count. Every failure is an {@link EntifixBuildError}, so a `400`.
|
|
32
|
+
*/ export function parseFacetQueryParams(entityConstructor, params) {
|
|
33
|
+
const members = params.getAll(FACET_PARAM);
|
|
34
|
+
if (members.length === 0) {
|
|
35
|
+
throw new EntifixBuildError(`name at least one "${FACET_PARAM}"`);
|
|
36
|
+
}
|
|
37
|
+
if (new Set(members).size !== members.length) {
|
|
38
|
+
throw new EntifixBuildError(`a "${FACET_PARAM}" is named twice`, undefined, {
|
|
39
|
+
members
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const allowed = allowedDescriptors(entityConstructor, (descriptor)=>descriptor.filterable);
|
|
43
|
+
return members.map((member)=>{
|
|
44
|
+
const { key, locale } = resolveAllowedProperty(allowed, member, 'filterable');
|
|
45
|
+
if (locale !== undefined) {
|
|
46
|
+
// One locale of a localized member is prose, one value per record —
|
|
47
|
+
// there is nothing to choose between, so nothing to count.
|
|
48
|
+
throw new EntifixBuildError(`"${member}" cannot be counted`, undefined, {
|
|
49
|
+
member
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const expression = params.get(facetFilterParam(member));
|
|
53
|
+
return _extends({
|
|
54
|
+
member: key
|
|
55
|
+
}, (expression == null ? void 0 : expression.trim()) ? {
|
|
56
|
+
filtering: [
|
|
57
|
+
coerceFiltering(entityConstructor, parseRsql(expression))
|
|
58
|
+
]
|
|
59
|
+
} : {});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//# sourceMappingURL=facet-query-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/rsql/facet-query-url.ts"],"sourcesContent":["import { EntifixBuildError } from '../base-entities/entifix-error/index.js';\nimport type { Entity, EntityConstructor } from '../types/Entity.js';\nimport type { EntityFacetQuery } from '../types/EntityFacets.js';\nimport {\n allowedDescriptors,\n coerceFiltering,\n resolveAllowedProperty,\n} from './coerce-rsql.js';\nimport { RSQL_PARAM } from './load-request-url.js';\nimport { parseRsql } from './parse-rsql.js';\nimport { serializeRsql } from './serialize-rsql.js';\n\n/** The parameter naming one member to count, repeated once per facet. */\nexport const FACET_PARAM = 'facet';\n\n/** The parameter holding one facet's own filtering: `rsql.<member>`. */\nexport const facetFilterParam = (member: string): string =>\n `${RSQL_PARAM}.${member}`;\n\n/**\n * Serializes facet queries into query parameters: each member as a `facet`,\n * and each one's filtering as RSQL under `rsql.<member>` — omitted when it\n * filters nothing, so an unfiltered count keeps a clean URL.\n */\nexport function serializeFacetQueryParams<TEntity extends Entity>(\n queries: readonly EntityFacetQuery<TEntity>[],\n): URLSearchParams {\n const params = new URLSearchParams();\n for (const { member, filtering } of queries) {\n params.append(FACET_PARAM, member);\n const rsql = serializeRsql(filtering);\n if (rsql !== '') params.set(facetFilterParam(member), rsql);\n }\n return params;\n}\n\n/**\n * Parses query parameters into facet queries, validated against the entity's\n * own metadata — the server-side half of the facet protocol, and the same\n * allowlist a load uses.\n *\n * A counted member must be `filterable`: a count is a question about the\n * values a filter would choose between, and counting a member nobody may\n * filter on would read out values the entity chose not to expose. A\n * `localized` member is refused, since its value is a record of texts, not one\n * to count. Every failure is an {@link EntifixBuildError}, so a `400`.\n */\nexport function parseFacetQueryParams<TEntity extends Entity>(\n entityConstructor: EntityConstructor<TEntity>,\n params: URLSearchParams,\n): EntityFacetQuery<TEntity>[] {\n const members = params.getAll(FACET_PARAM);\n if (members.length === 0) {\n throw new EntifixBuildError(`name at least one \"${FACET_PARAM}\"`);\n }\n if (new Set(members).size !== members.length) {\n throw new EntifixBuildError(\n `a \"${FACET_PARAM}\" is named twice`,\n undefined,\n {\n members,\n },\n );\n }\n\n const allowed = allowedDescriptors(\n entityConstructor,\n descriptor => descriptor.filterable,\n );\n\n return members.map(member => {\n const { key, locale } = resolveAllowedProperty(\n allowed,\n member,\n 'filterable',\n );\n if (locale !== undefined) {\n // One locale of a localized member is prose, one value per record —\n // there is nothing to choose between, so nothing to count.\n throw new EntifixBuildError(`\"${member}\" cannot be counted`, undefined, {\n member,\n });\n }\n const expression = params.get(facetFilterParam(member));\n return {\n member: key as keyof TEntity & string,\n ...(expression?.trim()\n ? {\n filtering: [\n coerceFiltering(entityConstructor, parseRsql(expression)),\n ],\n }\n : {}),\n };\n });\n}\n"],"names":["EntifixBuildError","allowedDescriptors","coerceFiltering","resolveAllowedProperty","RSQL_PARAM","parseRsql","serializeRsql","FACET_PARAM","facetFilterParam","member","serializeFacetQueryParams","queries","params","URLSearchParams","filtering","append","rsql","set","parseFacetQueryParams","entityConstructor","members","getAll","length","Set","size","undefined","allowed","descriptor","filterable","map","key","locale","expression","get","trim"],"mappings":";AAAA,SAASA,iBAAiB,QAAQ,0CAA0C;AAG5E,SACEC,kBAAkB,EAClBC,eAAe,EACfC,sBAAsB,QACjB,mBAAmB;AAC1B,SAASC,UAAU,QAAQ,wBAAwB;AACnD,SAASC,SAAS,QAAQ,kBAAkB;AAC5C,SAASC,aAAa,QAAQ,sBAAsB;AAEpD,uEAAuE,GACvE,OAAO,MAAMC,cAAc,QAAQ;AAEnC,sEAAsE,GACtE,OAAO,MAAMC,mBAAmB,CAACC,SAC/B,GAAGL,WAAW,CAAC,EAAEK,QAAQ,CAAC;AAE5B;;;;CAIC,GACD,OAAO,SAASC,0BACdC,OAA6C;IAE7C,MAAMC,SAAS,IAAIC;IACnB,KAAK,MAAM,EAAEJ,MAAM,EAAEK,SAAS,EAAE,IAAIH,QAAS;QAC3CC,OAAOG,MAAM,CAACR,aAAaE;QAC3B,MAAMO,OAAOV,cAAcQ;QAC3B,IAAIE,SAAS,IAAIJ,OAAOK,GAAG,CAACT,iBAAiBC,SAASO;IACxD;IACA,OAAOJ;AACT;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASM,sBACdC,iBAA6C,EAC7CP,MAAuB;IAEvB,MAAMQ,UAAUR,OAAOS,MAAM,CAACd;IAC9B,IAAIa,QAAQE,MAAM,KAAK,GAAG;QACxB,MAAM,IAAItB,kBAAkB,CAAC,mBAAmB,EAAEO,YAAY,CAAC,CAAC;IAClE;IACA,IAAI,IAAIgB,IAAIH,SAASI,IAAI,KAAKJ,QAAQE,MAAM,EAAE;QAC5C,MAAM,IAAItB,kBACR,CAAC,GAAG,EAAEO,YAAY,gBAAgB,CAAC,EACnCkB,WACA;YACEL;QACF;IAEJ;IAEA,MAAMM,UAAUzB,mBACdkB,mBACAQ,CAAAA,aAAcA,WAAWC,UAAU;IAGrC,OAAOR,QAAQS,GAAG,CAACpB,CAAAA;QACjB,MAAM,EAAEqB,GAAG,EAAEC,MAAM,EAAE,GAAG5B,uBACtBuB,SACAjB,QACA;QAEF,IAAIsB,WAAWN,WAAW;YACxB,oEAAoE;YACpE,2DAA2D;YAC3D,MAAM,IAAIzB,kBAAkB,CAAC,CAAC,EAAES,OAAO,mBAAmB,CAAC,EAAEgB,WAAW;gBACtEhB;YACF;QACF;QACA,MAAMuB,aAAapB,OAAOqB,GAAG,CAACzB,iBAAiBC;QAC/C,OAAO;YACLA,QAAQqB;WACJE,CAAAA,8BAAAA,WAAYE,IAAI,MAChB;YACEpB,WAAW;gBACTZ,gBAAgBiB,mBAAmBd,UAAU2B;aAC9C;QACH,IACA,CAAC;IAET;AACF"}
|
package/dist/rsql/index.d.ts
CHANGED
package/dist/rsql/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rsql/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rsql/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC"}
|
package/dist/rsql/index.js
CHANGED
package/dist/rsql/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/rsql/index.ts"],"sourcesContent":["export * from './coerce-rsql.js';\nexport * from './load-request-url.js';\nexport * from './parse-rsql.js';\nexport * from './rsql-operators.js';\nexport * from './rsql-values.js';\nexport * from './serialize-rsql.js';\nexport * from './sort-param.js';\n"],"names":[],"mappings":"AAAA,cAAc,mBAAmB;AACjC,cAAc,wBAAwB;AACtC,cAAc,kBAAkB;AAChC,cAAc,sBAAsB;AACpC,cAAc,mBAAmB;AACjC,cAAc,sBAAsB;AACpC,cAAc,kBAAkB"}
|
|
1
|
+
{"version":3,"sources":["../../src/rsql/index.ts"],"sourcesContent":["export * from './coerce-rsql.js';\nexport * from './facet-query-url.js';\nexport * from './load-request-url.js';\nexport * from './parse-rsql.js';\nexport * from './rsql-operators.js';\nexport * from './rsql-values.js';\nexport * from './serialize-rsql.js';\nexport * from './sort-param.js';\n"],"names":[],"mappings":"AAAA,cAAc,mBAAmB;AACjC,cAAc,uBAAuB;AACrC,cAAc,wBAAwB;AACtC,cAAc,kBAAkB;AAChC,cAAc,sBAAsB;AACpC,cAAc,mBAAmB;AACjC,cAAc,sBAAsB;AACpC,cAAc,kBAAkB"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Entity } from './Entity.js';
|
|
2
|
+
import type { EntityFiltering } from './EntityFiltering.js';
|
|
3
|
+
/**
|
|
4
|
+
* One member to count the values of, under its own filtering.
|
|
5
|
+
*
|
|
6
|
+
* Each facet carries its own filtering rather than sharing one, because the
|
|
7
|
+
* counts a shop shows are usually **disjunctive**: a brand facet is counted
|
|
8
|
+
* under every filter except the brand choice itself, so choosing one brand
|
|
9
|
+
* does not drop every other brand to zero. Which filters a facet leaves out is
|
|
10
|
+
* the caller's to say — the counter counts what it is asked to.
|
|
11
|
+
*/
|
|
12
|
+
export interface EntityFacetQuery<TEntity extends Entity = Entity> {
|
|
13
|
+
readonly member: keyof TEntity & string;
|
|
14
|
+
readonly filtering?: EntityFiltering<TEntity>[];
|
|
15
|
+
}
|
|
16
|
+
/** One value a member holds, and how many records hold it. */
|
|
17
|
+
export interface EntityFacetValue {
|
|
18
|
+
readonly value: string | number | boolean;
|
|
19
|
+
readonly count: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The values each queried member holds, keyed by the member as it was asked
|
|
23
|
+
* for. Every queried member is present; one no record holds is an empty list.
|
|
24
|
+
* A value held by no record is absent rather than counted `0`, since the
|
|
25
|
+
* counter cannot know which values the caller offers.
|
|
26
|
+
*
|
|
27
|
+
* Ordered by count, highest first, then by value.
|
|
28
|
+
*/
|
|
29
|
+
export type EntityFacetCounts = Readonly<Record<string, readonly EntityFacetValue[]>>;
|
|
30
|
+
//# sourceMappingURL=EntityFacets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EntityFacets.d.ts","sourceRoot":"","sources":["../../src/types/EntityFacets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5D;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB,CAAC,OAAO,SAAS,MAAM,GAAG,MAAM;IAC/D,QAAQ,CAAC,MAAM,EAAE,MAAM,OAAO,GAAG,MAAM,CAAC;IACxC,QAAQ,CAAC,SAAS,CAAC,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC;CACjD;AAED,8DAA8D;AAC9D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CACtC,MAAM,CAAC,MAAM,EAAE,SAAS,gBAAgB,EAAE,CAAC,CAC5C,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The values each queried member holds, keyed by the member as it was asked
|
|
3
|
+
* for. Every queried member is present; one no record holds is an empty list.
|
|
4
|
+
* A value held by no record is absent rather than counted `0`, since the
|
|
5
|
+
* counter cannot know which values the caller offers.
|
|
6
|
+
*
|
|
7
|
+
* Ordered by count, highest first, then by value.
|
|
8
|
+
*/ export { };
|
|
9
|
+
|
|
10
|
+
//# sourceMappingURL=EntityFacets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/types/EntityFacets.ts"],"sourcesContent":["import type { Entity } from './Entity.js';\nimport type { EntityFiltering } from './EntityFiltering.js';\n\n/**\n * One member to count the values of, under its own filtering.\n *\n * Each facet carries its own filtering rather than sharing one, because the\n * counts a shop shows are usually **disjunctive**: a brand facet is counted\n * under every filter except the brand choice itself, so choosing one brand\n * does not drop every other brand to zero. Which filters a facet leaves out is\n * the caller's to say — the counter counts what it is asked to.\n */\nexport interface EntityFacetQuery<TEntity extends Entity = Entity> {\n readonly member: keyof TEntity & string;\n readonly filtering?: EntityFiltering<TEntity>[];\n}\n\n/** One value a member holds, and how many records hold it. */\nexport interface EntityFacetValue {\n readonly value: string | number | boolean;\n readonly count: number;\n}\n\n/**\n * The values each queried member holds, keyed by the member as it was asked\n * for. Every queried member is present; one no record holds is an empty list.\n * A value held by no record is absent rather than counted `0`, since the\n * counter cannot know which values the caller offers.\n *\n * Ordered by count, highest first, then by value.\n */\nexport type EntityFacetCounts = Readonly<\n Record<string, readonly EntityFacetValue[]>\n>;\n"],"names":[],"mappings":"AAuBA;;;;;;;CAOC,GACD,WAEE"}
|
|
@@ -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
|
-
|
|
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":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,eAAO,MAAM,2BAA2B,iDAO9B,CAAC;AACX,MAAM,MAAM,0BAA0B,GACpC,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,CAAC,CAAC;AAC/C,MAAM,MAAM,kBAAkB,CAC5B,OAAO,SAAS,MAAM,EACtB,IAAI,SAAS,MAAM,OAAO,IACxB;IACF,QAAQ,EAAE,IAAI,CAAC;IACf,QAAQ,EAAE,0BAA0B,CAAC;IACrC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,0BAA0B,wBAAyB,CAAC;AACjE,MAAM,MAAM,yBAAyB,GACnC,CAAC,OAAO,0BAA0B,CAAC,CAAC,MAAM,CAAC,CAAC;AAC9C,MAAM,MAAM,iBAAiB,CAC3B,OAAO,SAAS,MAAM,EACtB,IAAI,SAAS,MAAM,OAAO,IACxB;IACF,QAAQ,EAAE,IAAI,CAAC;IACf,QAAQ,EAAE,yBAAyB,CAAC;IACpC,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CAC9B,CAAC;AAEF,eAAO,MAAM,0BAA0B,kCAAmC,CAAC;AAC3E,MAAM,MAAM,yBAAyB,GACnC,CAAC,OAAO,0BAA0B,CAAC,CAAC,MAAM,CAAC,CAAC;AAC9C,MAAM,MAAM,iBAAiB,CAC3B,OAAO,SAAS,MAAM,EACtB,IAAI,SAAS,MAAM,OAAO,IACxB;IACF,QAAQ,EAAE,IAAI,CAAC;IACf,QAAQ,EAAE,yBAAyB,CAAC;IACpC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACrB,GAAG,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB,CAAC;AAEF,eAAO,MAAM,2BAA2B,4BAA6B,CAAC;AACtE,MAAM,MAAM,0BAA0B,GACpC,CAAC,OAAO,2BAA2B,CAAC,CAAC,MAAM,CAAC,CAAC;AAC/C,MAAM,MAAM,kBAAkB,CAAC,OAAO,SAAS,MAAM,IAAI;IACvD,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,QAAQ,EAAE,0BAA0B,CAAC;IACrC,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,eAAO,MAAM,yBAAyB,kCAAmC,CAAC;AAC1E,MAAM,MAAM,wBAAwB,GAClC,CAAC,OAAO,yBAAyB,CAAC,CAAC,MAAM,CAAC,CAAC;AAC7C,MAAM,MAAM,gBAAgB,CAC1B,OAAO,SAAS,MAAM,EACtB,IAAI,SAAS,MAAM,OAAO,IACxB;IACF,QAAQ,EAAE,IAAI,CAAC;IACf,QAAQ,EAAE,wBAAwB,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,YAAY,CAAC,OAAO,SAAS,MAAM,IAC3C,kBAAkB,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,GAC1C,iBAAiB,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,GACzC,iBAAiB,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,GACzC,kBAAkB,CAAC,OAAO,CAAC,GAC3B,gBAAgB,CAAC,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC;
|
|
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;
|
|
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;
|
|
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
|
*
|