@adonis-agora/filter 0.0.0-beta-20261110131309

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/CHANGELOG.md +417 -0
  2. package/LICENSE +21 -0
  3. package/README.md +75 -0
  4. package/dist/commands/main.d.ts +19 -0
  5. package/dist/commands/main.d.ts.map +1 -0
  6. package/dist/commands/main.js +19 -0
  7. package/dist/commands/main.js.map +1 -0
  8. package/dist/commands/make_filter.d.ts +18 -0
  9. package/dist/commands/make_filter.d.ts.map +1 -0
  10. package/dist/commands/make_filter.js +32 -0
  11. package/dist/commands/make_filter.js.map +1 -0
  12. package/dist/commands/make_filter_client.d.ts +35 -0
  13. package/dist/commands/make_filter_client.d.ts.map +1 -0
  14. package/dist/commands/make_filter_client.js +85 -0
  15. package/dist/commands/make_filter_client.js.map +1 -0
  16. package/dist/configure.d.ts +15 -0
  17. package/dist/configure.d.ts.map +1 -0
  18. package/dist/configure.js +20 -0
  19. package/dist/configure.js.map +1 -0
  20. package/dist/providers/filter_provider.d.ts +28 -0
  21. package/dist/providers/filter_provider.d.ts.map +1 -0
  22. package/dist/providers/filter_provider.js +33 -0
  23. package/dist/providers/filter_provider.js.map +1 -0
  24. package/dist/src/aggregate.d.ts +70 -0
  25. package/dist/src/aggregate.d.ts.map +1 -0
  26. package/dist/src/aggregate.js +195 -0
  27. package/dist/src/aggregate.js.map +1 -0
  28. package/dist/src/aggregate_path.d.ts +25 -0
  29. package/dist/src/aggregate_path.d.ts.map +1 -0
  30. package/dist/src/aggregate_path.js +35 -0
  31. package/dist/src/aggregate_path.js.map +1 -0
  32. package/dist/src/apply_from_request.d.ts +85 -0
  33. package/dist/src/apply_from_request.d.ts.map +1 -0
  34. package/dist/src/apply_from_request.js +164 -0
  35. package/dist/src/apply_from_request.js.map +1 -0
  36. package/dist/src/base_filter.d.ts +49 -0
  37. package/dist/src/base_filter.d.ts.map +1 -0
  38. package/dist/src/base_filter.js +33 -0
  39. package/dist/src/base_filter.js.map +1 -0
  40. package/dist/src/base_model_filter.d.ts +90 -0
  41. package/dist/src/base_model_filter.d.ts.map +1 -0
  42. package/dist/src/base_model_filter.js +85 -0
  43. package/dist/src/base_model_filter.js.map +1 -0
  44. package/dist/src/coerce_value.d.ts +26 -0
  45. package/dist/src/coerce_value.d.ts.map +1 -0
  46. package/dist/src/coerce_value.js +82 -0
  47. package/dist/src/coerce_value.js.map +1 -0
  48. package/dist/src/cursor.d.ts +92 -0
  49. package/dist/src/cursor.d.ts.map +1 -0
  50. package/dist/src/cursor.js +107 -0
  51. package/dist/src/cursor.js.map +1 -0
  52. package/dist/src/custom_filter.d.ts +36 -0
  53. package/dist/src/custom_filter.d.ts.map +1 -0
  54. package/dist/src/custom_filter.js +127 -0
  55. package/dist/src/custom_filter.js.map +1 -0
  56. package/dist/src/decorator_metadata.d.ts +41 -0
  57. package/dist/src/decorator_metadata.d.ts.map +1 -0
  58. package/dist/src/decorator_metadata.js +83 -0
  59. package/dist/src/decorator_metadata.js.map +1 -0
  60. package/dist/src/decorators.d.ts +73 -0
  61. package/dist/src/decorators.d.ts.map +1 -0
  62. package/dist/src/decorators.js +118 -0
  63. package/dist/src/decorators.js.map +1 -0
  64. package/dist/src/escape-like.d.ts +10 -0
  65. package/dist/src/escape-like.d.ts.map +1 -0
  66. package/dist/src/escape-like.js +15 -0
  67. package/dist/src/escape-like.js.map +1 -0
  68. package/dist/src/field_aliases.d.ts +30 -0
  69. package/dist/src/field_aliases.d.ts.map +1 -0
  70. package/dist/src/field_aliases.js +65 -0
  71. package/dist/src/field_aliases.js.map +1 -0
  72. package/dist/src/filter_class.d.ts +46 -0
  73. package/dist/src/filter_class.d.ts.map +1 -0
  74. package/dist/src/filter_class.js +244 -0
  75. package/dist/src/filter_class.js.map +1 -0
  76. package/dist/src/filter_spec.d.ts +231 -0
  77. package/dist/src/filter_spec.d.ts.map +1 -0
  78. package/dist/src/filter_spec.js +182 -0
  79. package/dist/src/filter_spec.js.map +1 -0
  80. package/dist/src/filterable_mixin.d.ts +49 -0
  81. package/dist/src/filterable_mixin.d.ts.map +1 -0
  82. package/dist/src/filterable_mixin.js +47 -0
  83. package/dist/src/filterable_mixin.js.map +1 -0
  84. package/dist/src/generate_client.d.ts +107 -0
  85. package/dist/src/generate_client.d.ts.map +1 -0
  86. package/dist/src/generate_client.js +164 -0
  87. package/dist/src/generate_client.js.map +1 -0
  88. package/dist/src/group_by_count.d.ts +67 -0
  89. package/dist/src/group_by_count.d.ts.map +1 -0
  90. package/dist/src/group_by_count.js +71 -0
  91. package/dist/src/group_by_count.js.map +1 -0
  92. package/dist/src/in_memory.d.ts +279 -0
  93. package/dist/src/in_memory.d.ts.map +1 -0
  94. package/dist/src/in_memory.js +810 -0
  95. package/dist/src/in_memory.js.map +1 -0
  96. package/dist/src/index.d.ts +51 -0
  97. package/dist/src/index.d.ts.map +1 -0
  98. package/dist/src/index.js +33 -0
  99. package/dist/src/index.js.map +1 -0
  100. package/dist/src/lucid_adapter.d.ts +311 -0
  101. package/dist/src/lucid_adapter.d.ts.map +1 -0
  102. package/dist/src/lucid_adapter.js +469 -0
  103. package/dist/src/lucid_adapter.js.map +1 -0
  104. package/dist/src/lucid_macros.d.ts +87 -0
  105. package/dist/src/lucid_macros.d.ts.map +1 -0
  106. package/dist/src/lucid_macros.js +67 -0
  107. package/dist/src/lucid_macros.js.map +1 -0
  108. package/dist/src/normalizer.d.ts +21 -0
  109. package/dist/src/normalizer.d.ts.map +1 -0
  110. package/dist/src/normalizer.js +61 -0
  111. package/dist/src/normalizer.js.map +1 -0
  112. package/dist/src/operators.d.ts +37 -0
  113. package/dist/src/operators.d.ts.map +1 -0
  114. package/dist/src/operators.js +43 -0
  115. package/dist/src/operators.js.map +1 -0
  116. package/dist/src/paginate.d.ts +12 -0
  117. package/dist/src/paginate.d.ts.map +1 -0
  118. package/dist/src/paginate.js +88 -0
  119. package/dist/src/paginate.js.map +1 -0
  120. package/dist/src/pagination_stash.d.ts +4 -0
  121. package/dist/src/pagination_stash.d.ts.map +1 -0
  122. package/dist/src/pagination_stash.js +19 -0
  123. package/dist/src/pagination_stash.js.map +1 -0
  124. package/dist/src/parse_request.d.ts +50 -0
  125. package/dist/src/parse_request.d.ts.map +1 -0
  126. package/dist/src/parse_request.js +220 -0
  127. package/dist/src/parse_request.js.map +1 -0
  128. package/dist/src/runner.d.ts +65 -0
  129. package/dist/src/runner.d.ts.map +1 -0
  130. package/dist/src/runner.js +371 -0
  131. package/dist/src/runner.js.map +1 -0
  132. package/dist/src/source_resolver.d.ts +18 -0
  133. package/dist/src/source_resolver.d.ts.map +1 -0
  134. package/dist/src/source_resolver.js +64 -0
  135. package/dist/src/source_resolver.js.map +1 -0
  136. package/dist/src/spatie_parser.d.ts +39 -0
  137. package/dist/src/spatie_parser.d.ts.map +1 -0
  138. package/dist/src/spatie_parser.js +134 -0
  139. package/dist/src/spatie_parser.js.map +1 -0
  140. package/dist/src/testing.d.ts +73 -0
  141. package/dist/src/testing.d.ts.map +1 -0
  142. package/dist/src/testing.js +136 -0
  143. package/dist/src/testing.js.map +1 -0
  144. package/dist/src/types.d.ts +226 -0
  145. package/dist/src/types.d.ts.map +1 -0
  146. package/dist/src/types.js +2 -0
  147. package/dist/src/types.js.map +1 -0
  148. package/dist/src/validate-column-filter.d.ts +30 -0
  149. package/dist/src/validate-column-filter.d.ts.map +1 -0
  150. package/dist/src/validate-column-filter.js +148 -0
  151. package/dist/src/validate-column-filter.js.map +1 -0
  152. package/dist/src/value-shape.d.ts +18 -0
  153. package/dist/src/value-shape.d.ts.map +1 -0
  154. package/dist/src/value-shape.js +36 -0
  155. package/dist/src/value-shape.js.map +1 -0
  156. package/dist/stubs/main.d.ts +3 -0
  157. package/dist/stubs/main.d.ts.map +1 -0
  158. package/dist/stubs/main.js +4 -0
  159. package/dist/stubs/main.js.map +1 -0
  160. package/dist/stubs/make/filter/main.stub +45 -0
  161. package/package.json +102 -0
  162. package/skills/filter-basics/SKILL.md +285 -0
  163. package/skills/filter-codegen/SKILL.md +246 -0
  164. package/skills/filter-definitions/SKILL.md +340 -0
  165. package/skills/filter-querying/SKILL.md +319 -0
  166. package/skills/filter-safety/SKILL.md +304 -0
  167. package/stubs/main.ts +4 -0
  168. package/stubs/make/filter/main.stub +45 -0
@@ -0,0 +1,810 @@
1
+ import { buildCursorPage, } from './cursor.js';
2
+ import { applyCursor, applyFilter } from './runner.js';
3
+ /**
4
+ * Thrown when the runner asks the in-memory builder for something only SQL can do: a raw
5
+ * predicate or ordering (policy `computed` fields, `fullText`, `vectorSimilarity`) — declare a
6
+ * virtual field instead — or a relation/field path the collection does not declare.
7
+ */
8
+ export class InMemoryQueryError extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = 'InMemoryQueryError';
12
+ }
13
+ }
14
+ const ARRAY_KINDS = {
15
+ 'string[]': 'string',
16
+ 'number[]': 'number',
17
+ 'boolean[]': 'boolean',
18
+ 'date[]': 'date',
19
+ };
20
+ function resolveType(type) {
21
+ const element = ARRAY_KINDS[type];
22
+ return element ? { kind: element, array: true } : { kind: type, array: false };
23
+ }
24
+ /**
25
+ * Declare a collection: its fields (and their types), virtual fields, primary key and relations.
26
+ *
27
+ * ```ts
28
+ * const posts = defineCollection<Post>({ fields: { id: 'number', title: 'string' } })
29
+ * const users = defineCollection<User>({
30
+ * fields: {
31
+ * id: 'number',
32
+ * name: 'string',
33
+ * age: 'number',
34
+ * tags: 'string[]',
35
+ * fullName: { type: 'string', get: (u) => `${u.first} ${u.last}` },
36
+ * },
37
+ * relations: { posts: { kind: 'one-to-many', target: () => posts, get: (u) => u.posts } },
38
+ * })
39
+ *
40
+ * const qb = users.query(rows)
41
+ * const { page, size } = applyFilter(qb, input, { allowed: ['name', 'posts.title'] })
42
+ * return qb.paginate(page, size)
43
+ * ```
44
+ */
45
+ export function defineCollection(options) {
46
+ const fields = Object.create(null);
47
+ const filterable = {};
48
+ const fieldTypes = {};
49
+ for (const [name, declaration] of Object.entries(options.fields)) {
50
+ const virtual = typeof declaration === 'object' ? declaration : undefined;
51
+ const { kind, array } = resolveType(virtual ? virtual.type : declaration);
52
+ fields[name] = { kind, array, get: virtual?.get };
53
+ filterable[name] = kind;
54
+ fieldTypes[name] = { kind };
55
+ }
56
+ const relations = Object.create(null);
57
+ for (const [name, relation] of Object.entries(options.relations ?? {})) {
58
+ relations[name] = relation;
59
+ }
60
+ const collection = {
61
+ name: options.name,
62
+ primaryKey: options.primaryKey ?? 'id',
63
+ filterable,
64
+ fieldTypes,
65
+ $fields: fields,
66
+ $relations: relations,
67
+ query: (rows) => new InMemoryQueryBuilder(collection, [...rows]),
68
+ };
69
+ return collection;
70
+ }
71
+ /** Sentinel for a not-yet-read sort value (`undefined` is a legitimate read result). */
72
+ const UNREAD = Symbol('unread');
73
+ function isNil(value) {
74
+ return value === null || value === undefined;
75
+ }
76
+ function hasToMillis(value) {
77
+ return (typeof value === 'object' &&
78
+ value !== null &&
79
+ typeof value.toMillis === 'function');
80
+ }
81
+ /** The kind a runtime value has — used for undeclared (`unknown`/`json`) fields. */
82
+ function inferKind(value) {
83
+ if (typeof value === 'number' || typeof value === 'bigint')
84
+ return 'number';
85
+ if (typeof value === 'boolean')
86
+ return 'boolean';
87
+ if (typeof value === 'string')
88
+ return 'string';
89
+ if (value instanceof Date || hasToMillis(value))
90
+ return 'date';
91
+ return 'json';
92
+ }
93
+ /**
94
+ * Normalize a value to a comparable primitive of `kind`, the way Postgres casts a bound literal to
95
+ * the column type. `undefined` means "cannot be cast" — the comparison is then false, where
96
+ * Postgres would have rejected the query outright.
97
+ */
98
+ function normalize(value, kind) {
99
+ if (isNil(value))
100
+ return undefined;
101
+ switch (kind) {
102
+ case 'number': {
103
+ if (typeof value === 'number')
104
+ return Number.isFinite(value) ? value : undefined;
105
+ if (typeof value === 'bigint')
106
+ return Number(value);
107
+ if (typeof value === 'string' && value.trim() !== '') {
108
+ const n = Number(value.trim());
109
+ return Number.isFinite(n) ? n : undefined;
110
+ }
111
+ return undefined;
112
+ }
113
+ case 'boolean': {
114
+ if (typeof value === 'boolean')
115
+ return value;
116
+ if (value === 1 || value === 0)
117
+ return value === 1;
118
+ if (typeof value === 'string') {
119
+ const v = value.trim().toLowerCase();
120
+ if (v === 'true' || v === '1')
121
+ return true;
122
+ if (v === 'false' || v === '0')
123
+ return false;
124
+ }
125
+ return undefined;
126
+ }
127
+ case 'date': {
128
+ if (value instanceof Date) {
129
+ const t = value.getTime();
130
+ return Number.isNaN(t) ? undefined : t;
131
+ }
132
+ if (hasToMillis(value))
133
+ return value.toMillis();
134
+ if (typeof value === 'number')
135
+ return Number.isFinite(value) ? value : undefined;
136
+ if (typeof value === 'string') {
137
+ const t = Date.parse(value);
138
+ return Number.isNaN(t) ? undefined : t;
139
+ }
140
+ return undefined;
141
+ }
142
+ case 'string':
143
+ return typeof value === 'string' ? value : stringify(value);
144
+ default:
145
+ // json / unknown: structural equality only.
146
+ return typeof value === 'object' ? JSON.stringify(value) : value;
147
+ }
148
+ }
149
+ /** Text form of a value — what a LIKE on a non-text column matches against. */
150
+ function stringify(value) {
151
+ if (typeof value === 'string')
152
+ return value;
153
+ if (value instanceof Date)
154
+ return value.toISOString();
155
+ if (hasToMillis(value))
156
+ return new Date(value.toMillis()).toISOString();
157
+ if (typeof value === 'object' && value !== null)
158
+ return JSON.stringify(value);
159
+ return String(value);
160
+ }
161
+ /**
162
+ * Compare a row value to a comparand under `kind` (`json`/`unknown` → the row value's own kind).
163
+ * `undefined` when either side cannot be cast or the kind has no ordering.
164
+ */
165
+ function compare(rowValue, comparand, declared) {
166
+ const kind = declared === 'json' || declared === 'unknown' ? inferKind(rowValue) : declared;
167
+ const a = normalize(rowValue, kind);
168
+ const b = normalize(comparand, kind);
169
+ if (a === undefined || b === undefined)
170
+ return undefined;
171
+ if (typeof a === 'number' && typeof b === 'number')
172
+ return a - b;
173
+ if (typeof a === 'boolean' && typeof b === 'boolean')
174
+ return Number(a) - Number(b);
175
+ if (typeof a === 'string' && typeof b === 'string') {
176
+ // Code-unit order — Postgres' "C" collation. Deterministic and locale-free, which a keyset
177
+ // cursor needs: the seek predicate and the ORDER BY must agree on every pair of values.
178
+ return a < b ? -1 : a > b ? 1 : 0;
179
+ }
180
+ return a === b ? 0 : undefined;
181
+ }
182
+ /** Compile a LIKE pattern (backslash escapes, `%`, `_`) into an anchored RegExp. */
183
+ function likeToRegExp(pattern, caseInsensitive) {
184
+ let source = '';
185
+ for (let i = 0; i < pattern.length; i++) {
186
+ const ch = pattern[i];
187
+ if (ch === '\\' && i + 1 < pattern.length) {
188
+ source += escapeRegExp(pattern[++i]);
189
+ }
190
+ else if (ch === '%') {
191
+ source += '.*';
192
+ }
193
+ else if (ch === '_') {
194
+ source += '.';
195
+ }
196
+ else {
197
+ source += escapeRegExp(ch);
198
+ }
199
+ }
200
+ return new RegExp(`^${source}$`, caseInsensitive ? 'is' : 's');
201
+ }
202
+ function escapeRegExp(ch) {
203
+ return ch.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
204
+ }
205
+ /** Relation targets normalized to a list (a to-one relation yields zero or one row). */
206
+ function relatedRows(value) {
207
+ if (isNil(value))
208
+ return [];
209
+ if (Array.isArray(value))
210
+ return value.filter((v) => !isNil(v));
211
+ return [value];
212
+ }
213
+ /** Parse a `expr AS alias` select/count argument. */
214
+ function parseAlias(expression) {
215
+ const match = /^\s*(.+?)\s+as\s+([A-Za-z_][A-Za-z0-9_]*)\s*$/i.exec(expression);
216
+ return match
217
+ ? { expr: match[1].trim(), alias: match[2] }
218
+ : { expr: expression.trim(), alias: expression.trim() };
219
+ }
220
+ /** The raw predicate the group-by-count search emits — the only raw SQL this adapter understands. */
221
+ const GROUP_SEARCH_SQL = /^\s*LOWER\(\?\?\)\s+LIKE\s+\?\s*$/i;
222
+ /**
223
+ * A {@link QueryBuilderLike} over an array. Created by {@link InMemoryCollection.query}; hand it to
224
+ * any runner entry point exactly as you would a Lucid query, then read the result with
225
+ * {@link all}, {@link paginate}, or by awaiting it (it is thenable, like a Lucid query — which is
226
+ * what lets `groupByCountFromRequest` run over it unchanged).
227
+ */
228
+ export class InMemoryQueryBuilder {
229
+ collection;
230
+ rows;
231
+ clauses = [];
232
+ sorts = [];
233
+ selects = [];
234
+ groups = [];
235
+ groupSearches = [];
236
+ distinctFields = [];
237
+ countAlias;
238
+ limitCount;
239
+ offsetCount;
240
+ /** Lazily-built readers for to-one relation hops, so a dotted read does not allocate per row. */
241
+ relationReaders;
242
+ constructor(collection, rows = []) {
243
+ this.collection = collection;
244
+ this.rows = rows;
245
+ }
246
+ // -- field access -----------------------------------------------------------------------------
247
+ field(name) {
248
+ return this.collection.$fields[name];
249
+ }
250
+ /** Strip a leading `<collection name>.` qualifier. */
251
+ unqualify(path) {
252
+ const name = this.collection.name;
253
+ return name !== undefined && path.startsWith(`${name}.`) ? path.slice(name.length + 1) : path;
254
+ }
255
+ /**
256
+ * Read a (possibly dotted) path off a row. A dotted path crosses a to-one relation; crossing a
257
+ * to-many relation is ambiguous outside a `whereHas` (which the runner uses for filters) and is
258
+ * rejected.
259
+ */
260
+ read(row, rawPath) {
261
+ const path = this.unqualify(rawPath);
262
+ const dot = path.indexOf('.');
263
+ if (dot === -1) {
264
+ const declared = this.field(path);
265
+ if (declared?.get)
266
+ return declared.get(row);
267
+ return Object.hasOwn(row, path) ? row[path] : undefined;
268
+ }
269
+ const head = path.slice(0, dot);
270
+ const relation = this.collection.$relations[head];
271
+ if (!relation) {
272
+ throw new InMemoryQueryError(`"${rawPath}" crosses "${head}", which is not a relation declared on this collection.`);
273
+ }
274
+ if (relation.kind === 'one-to-many' || relation.kind === 'many-to-many') {
275
+ throw new InMemoryQueryError(`"${rawPath}" reads through the to-many relation "${head}" — only a filter (EXISTS) can cross it.`);
276
+ }
277
+ const related = relatedRows(relation.get(row))[0];
278
+ if (related === undefined)
279
+ return undefined;
280
+ return this.relationReader(head, relation).read(related, path.slice(dot + 1));
281
+ }
282
+ /** The (memoized) empty builder over a relation's target collection, used to read through it. */
283
+ relationReader(head, relation) {
284
+ this.relationReaders ??= new Map();
285
+ let reader = this.relationReaders.get(head);
286
+ if (reader === undefined) {
287
+ reader = relation.target().query([]);
288
+ this.relationReaders.set(head, reader);
289
+ }
290
+ return reader;
291
+ }
292
+ /** The declared kind of a path (following to-one relations), or `unknown`. */
293
+ kindOf(rawPath) {
294
+ const path = this.unqualify(rawPath);
295
+ const dot = path.indexOf('.');
296
+ if (dot === -1)
297
+ return this.field(path) ?? { kind: 'unknown', array: false };
298
+ const head = path.slice(0, dot);
299
+ const relation = this.collection.$relations[head];
300
+ if (!relation)
301
+ return { kind: 'unknown', array: false };
302
+ return this.relationReader(head, relation).kindOf(path.slice(dot + 1));
303
+ }
304
+ /**
305
+ * Build a leaf predicate. `match` tests one non-null element; `negated` flips the element-level
306
+ * result into "no element matches" — with SQL's rule that a NULL value (or an uncastable
307
+ * comparison) satisfies neither a condition nor its negation.
308
+ */
309
+ leaf(path, match, negated = false) {
310
+ const { kind, array } = this.kindOf(path);
311
+ return (row) => {
312
+ const value = this.read(row, path);
313
+ if (isNil(value))
314
+ return false;
315
+ if (array || (Array.isArray(value) && (kind === 'unknown' || kind === 'json'))) {
316
+ const elements = (Array.isArray(value) ? value : [value]).filter((e) => !isNil(e));
317
+ const results = elements.map((e) => match(e, kind));
318
+ if (negated)
319
+ return results.every((r) => r === false);
320
+ return results.some((r) => r === true);
321
+ }
322
+ const result = match(value, kind);
323
+ if (result === undefined)
324
+ return false;
325
+ return negated ? !result : result;
326
+ };
327
+ }
328
+ equalsLeaf(path, value, negated) {
329
+ return this.leaf(path, (e, kind) => {
330
+ const c = compare(e, value, kind);
331
+ return c === undefined ? undefined : c === 0;
332
+ }, negated);
333
+ }
334
+ compareLeaf(path, operator, value) {
335
+ switch (operator) {
336
+ case '=':
337
+ return isNil(value) ? () => false : this.equalsLeaf(path, value, false);
338
+ case '!=':
339
+ case '<>':
340
+ return isNil(value) ? () => false : this.equalsLeaf(path, value, true);
341
+ case '>':
342
+ case '>=':
343
+ case '<':
344
+ case '<=':
345
+ if (isNil(value))
346
+ return () => false;
347
+ return this.leaf(path, (e, kind) => {
348
+ const c = compare(e, value, kind);
349
+ if (c === undefined)
350
+ return undefined;
351
+ if (operator === '>')
352
+ return c > 0;
353
+ if (operator === '>=')
354
+ return c >= 0;
355
+ if (operator === '<')
356
+ return c < 0;
357
+ return c <= 0;
358
+ });
359
+ default:
360
+ throw new InMemoryQueryError(`Unsupported comparison operator "${operator}".`);
361
+ }
362
+ }
363
+ inLeaf(path, values, negated) {
364
+ // SQL: `x IN ()` is false and `x NOT IN ()` is true for every row, NULLs included (knex emits
365
+ // 1 = 0 / 1 = 1); `x NOT IN (…, NULL)` is never true.
366
+ if (values.length === 0)
367
+ return () => negated;
368
+ if (negated && values.some(isNil))
369
+ return () => false;
370
+ const candidates = values.filter((v) => !isNil(v));
371
+ return this.leaf(path, (e, kind) => {
372
+ let unknown = false;
373
+ for (const candidate of candidates) {
374
+ const c = compare(e, candidate, kind);
375
+ if (c === 0)
376
+ return true;
377
+ if (c === undefined)
378
+ unknown = true;
379
+ }
380
+ return unknown ? undefined : false;
381
+ }, negated);
382
+ }
383
+ betweenLeaf(path, range, negated) {
384
+ const [low, high] = Array.isArray(range) ? range : [undefined, undefined];
385
+ if (isNil(low) || isNil(high))
386
+ return () => false;
387
+ return this.leaf(path, (e, kind) => {
388
+ const lo = compare(e, low, kind);
389
+ const hi = compare(e, high, kind);
390
+ if (lo === undefined || hi === undefined)
391
+ return undefined;
392
+ return lo >= 0 && hi <= 0;
393
+ }, negated);
394
+ }
395
+ likeLeaf(path, pattern) {
396
+ const regex = likeToRegExp(pattern, true);
397
+ return this.leaf(path, (e) => regex.test(stringify(e)));
398
+ }
399
+ add(test, or = false) {
400
+ this.clauses.push({ or, test });
401
+ return this;
402
+ }
403
+ group(callback, or) {
404
+ const child = new InMemoryQueryBuilder(this.collection);
405
+ callback(child);
406
+ // An empty group compiles to nothing in knex — not to an always-true/false clause.
407
+ if (child.clauses.length === 0)
408
+ return this;
409
+ return this.add((row) => child.matches(row), or);
410
+ }
411
+ /**
412
+ * Does `row` satisfy every recorded condition? Clauses chain left to right the way knex joins
413
+ * them — `a AND b OR c` — so AND binds tighter than OR, exactly as the emitted SQL would.
414
+ */
415
+ matches(row) {
416
+ let result = false;
417
+ let segment = true;
418
+ this.clauses.forEach((clause, index) => {
419
+ if (clause.or && index > 0) {
420
+ result ||= segment;
421
+ segment = true;
422
+ }
423
+ segment &&= clause.test(row);
424
+ });
425
+ return this.clauses.length === 0 ? true : result || segment;
426
+ }
427
+ where(...args) {
428
+ if (typeof args[0] === 'function') {
429
+ return this.group(args[0], false);
430
+ }
431
+ const column = args[0];
432
+ if (args.length >= 3)
433
+ return this.add(this.compareLeaf(column, String(args[1]), args[2]));
434
+ // knex compiles `where(col, null)` to `col IS NULL`.
435
+ if (isNil(args[1]))
436
+ return this.whereNull(column);
437
+ return this.add(this.equalsLeaf(column, args[1], false));
438
+ }
439
+ orWhere(callback) {
440
+ return this.group(callback, true);
441
+ }
442
+ // biome-ignore lint/suspicious/noExplicitAny: matches QueryBuilderLike.whereHas (see its docblock).
443
+ whereHas(relation, callback) {
444
+ const name = String(relation);
445
+ const declared = this.collection.$relations[name];
446
+ if (!declared) {
447
+ throw new InMemoryQueryError(`"${name}" is not a relation declared on this collection.`);
448
+ }
449
+ const child = new InMemoryQueryBuilder(declared.target());
450
+ callback(child);
451
+ return this.add((row) => relatedRows(declared.get(row)).some((r) => child.matches(r)));
452
+ }
453
+ whereNot(...args) {
454
+ if (typeof args[0] === 'function') {
455
+ const child = new InMemoryQueryBuilder(this.collection);
456
+ args[0](child);
457
+ if (child.clauses.length === 0)
458
+ return this;
459
+ return this.add((row) => !child.matches(row));
460
+ }
461
+ const [column, value] = args;
462
+ if (isNil(value))
463
+ return this.whereNotNull(column);
464
+ return this.add(this.equalsLeaf(column, value, true));
465
+ }
466
+ whereIn(column, values) {
467
+ return this.add(this.inLeaf(column, Array.isArray(values) ? values : [values], false));
468
+ }
469
+ whereNotIn(column, values) {
470
+ return this.add(this.inLeaf(column, Array.isArray(values) ? values : [values], true));
471
+ }
472
+ whereNull(column) {
473
+ return this.add((row) => isNil(this.read(row, column)));
474
+ }
475
+ whereNotNull(column) {
476
+ return this.add((row) => !isNil(this.read(row, column)));
477
+ }
478
+ whereBetween(column, range) {
479
+ return this.add(this.betweenLeaf(column, range, false));
480
+ }
481
+ whereNotBetween(column, range) {
482
+ return this.add(this.betweenLeaf(column, range, true));
483
+ }
484
+ whereILike(column, value) {
485
+ return this.add(this.likeLeaf(column, value));
486
+ }
487
+ orWhereILike(column, value) {
488
+ return this.add(this.likeLeaf(column, value), true);
489
+ }
490
+ orderBy(column, direction) {
491
+ this.sorts.push({ field: column, direction: direction === 'desc' ? 'desc' : 'asc' });
492
+ return this;
493
+ }
494
+ /**
495
+ * Only the group-by-count search predicate (`LOWER(??) LIKE ?`) is understood; any other raw
496
+ * SQL — a policy's `computed` fields, `fullText`, `vectorSimilarity` — throws
497
+ * {@link InMemoryQueryError}. Declare a virtual field on the collection instead of `computed`.
498
+ */
499
+ whereRaw(sql, bindings = []) {
500
+ if (GROUP_SEARCH_SQL.test(sql) && typeof bindings[0] === 'string') {
501
+ const field = bindings[0];
502
+ const pattern = likeToRegExp(String(bindings[1] ?? ''), false);
503
+ this.groupSearches.push({ field, pattern });
504
+ return this.add(this.leaf(field, (e) => pattern.test(stringify(e).toLowerCase())), false);
505
+ }
506
+ throw new InMemoryQueryError(`The in-memory adapter cannot evaluate raw SQL (${JSON.stringify(sql)}). Policy \`computed\` fields, \`fullText\` and \`vectorSimilarity\` are SQL-only — declare a virtual field on the collection instead.`);
507
+ }
508
+ orderByRaw(sql, _bindings = []) {
509
+ throw new InMemoryQueryError(`The in-memory adapter cannot order by raw SQL (${JSON.stringify(sql)}). Declare a virtual field on the collection and sort on it instead.`);
510
+ }
511
+ distinct(...columns) {
512
+ this.distinctFields = columns;
513
+ return this;
514
+ }
515
+ limit(count) {
516
+ this.limitCount = Math.max(0, count);
517
+ return this;
518
+ }
519
+ offset(n) {
520
+ this.offsetCount = Math.max(0, n);
521
+ return this;
522
+ }
523
+ select(...columns) {
524
+ for (const column of columns)
525
+ this.selects.push(parseAlias(column));
526
+ return this;
527
+ }
528
+ count(column) {
529
+ const { expr, alias } = parseAlias(column);
530
+ if (expr !== '*') {
531
+ throw new InMemoryQueryError(`Only count('*') is supported in memory, got "${column}".`);
532
+ }
533
+ this.countAlias = alias;
534
+ return this;
535
+ }
536
+ groupBy(...columns) {
537
+ this.groups.push(...columns);
538
+ return this;
539
+ }
540
+ // -- execution --------------------------------------------------------------------------------
541
+ /**
542
+ * Sort rows by the recorded `orderBy`s. Each row's sort values are read at most once per field
543
+ * (memoized lazily, so a field is only read for rows whose earlier fields tied — exactly when a
544
+ * pairwise comparator would read it) instead of twice per comparison, turning O(n log n) reads
545
+ * (virtual getters, relation hops) into O(n).
546
+ */
547
+ sortRows(rows) {
548
+ const sorts = this.sorts;
549
+ const kinds = new Array(sorts.length);
550
+ const entries = rows.map((row) => ({
551
+ row,
552
+ values: new Array(sorts.length).fill(UNREAD),
553
+ }));
554
+ const valueAt = (entry, i) => {
555
+ let value = entry.values[i];
556
+ if (value === UNREAD) {
557
+ value = this.read(entry.row, sorts[i].field);
558
+ entry.values[i] = value;
559
+ }
560
+ return value;
561
+ };
562
+ entries.sort((a, b) => {
563
+ for (let i = 0; i < sorts.length; i++) {
564
+ const va = valueAt(a, i);
565
+ const vb = valueAt(b, i);
566
+ let kind = kinds[i];
567
+ if (kind === undefined) {
568
+ kind = this.kindOf(sorts[i].field).kind;
569
+ kinds[i] = kind;
570
+ }
571
+ const c = this.compareValues(va, vb, kind);
572
+ if (c !== 0)
573
+ return sorts[i].direction === 'desc' ? -c : c;
574
+ }
575
+ return 0;
576
+ });
577
+ return entries.map((entry) => entry.row);
578
+ }
579
+ /** Ascending comparison with NULLs sorting last (so NULLS FIRST once a desc sort negates it). */
580
+ compareValues(a, b, kind) {
581
+ if (isNil(a))
582
+ return isNil(b) ? 0 : 1;
583
+ if (isNil(b))
584
+ return -1;
585
+ return compare(a, b, kind) ?? 0;
586
+ }
587
+ /** Filter + sort (+ distinct / projection / aggregation), before limit/offset. */
588
+ evaluate() {
589
+ const matched = this.rows.filter((row) => this.matches(row));
590
+ if (this.groups.length > 0)
591
+ return this.aggregate(matched);
592
+ const sorted = this.sorts.length > 0 && matched.length > 1 ? this.sortRows(matched) : matched;
593
+ if (this.distinctFields.length > 0) {
594
+ const seen = new Set();
595
+ const out = [];
596
+ const fields = this.distinctFields;
597
+ const names = fields.map((field) => this.unqualify(field));
598
+ const kinds = fields.map((field) => this.kindOf(field).kind);
599
+ for (const row of sorted) {
600
+ const projected = {};
601
+ const key = [];
602
+ for (let i = 0; i < fields.length; i++) {
603
+ const value = this.read(row, fields[i]);
604
+ projected[names[i]] = value;
605
+ key.push(normalize(value, kinds[i]) ?? null);
606
+ }
607
+ const k = JSON.stringify(key);
608
+ if (!seen.has(k)) {
609
+ seen.add(k);
610
+ out.push(projected);
611
+ }
612
+ }
613
+ return out;
614
+ }
615
+ if (this.selects.length > 0) {
616
+ return sorted.map((row) => {
617
+ const projected = {};
618
+ for (const { expr, alias } of this.selects)
619
+ projected[alias] = this.read(row, expr);
620
+ return projected;
621
+ });
622
+ }
623
+ return sorted;
624
+ }
625
+ /**
626
+ * `GROUP BY` over the matched rows. An array-typed grouping field is unnested — each element is
627
+ * its own group — which is what a tag picker wants (Postgres would group whole arrays).
628
+ */
629
+ aggregate(rows) {
630
+ const columns = this.groups;
631
+ const groups = new Map();
632
+ const single = columns.length === 1 ? columns[0] : undefined;
633
+ const unnest = single !== undefined && this.kindOf(single).array;
634
+ const searches = this.groupSearches.filter((s) => s.field === single);
635
+ const kinds = columns.map((c) => this.kindOf(c).kind);
636
+ for (const row of rows) {
637
+ const tuples = [];
638
+ if (unnest) {
639
+ const value = this.read(row, single);
640
+ const elements = Array.isArray(value) ? value : isNil(value) ? [null] : [value];
641
+ for (const element of elements) {
642
+ if (searches.every((s) => !isNil(element) && s.pattern.test(stringify(element).toLowerCase()))) {
643
+ tuples.push([element]);
644
+ }
645
+ }
646
+ }
647
+ else {
648
+ tuples.push(columns.map((c) => this.read(row, c)));
649
+ }
650
+ for (const values of tuples) {
651
+ const key = JSON.stringify(values.map((v, i) => normalize(v, kinds[i]) ?? null));
652
+ const group = groups.get(key);
653
+ if (group)
654
+ group.count++;
655
+ else
656
+ groups.set(key, { values, count: 1 });
657
+ }
658
+ }
659
+ const outputs = [...groups.values()].map(({ values, count }) => {
660
+ const row = {};
661
+ const selects = this.selects.length > 0 ? this.selects : columns.map((c) => ({ expr: c, alias: c }));
662
+ for (const { expr, alias } of selects) {
663
+ const index = columns.indexOf(expr);
664
+ if (index === -1) {
665
+ throw new InMemoryQueryError(`"${expr}" is selected but not grouped by.`);
666
+ }
667
+ row[alias] = values[index];
668
+ }
669
+ if (this.countAlias !== undefined)
670
+ row[this.countAlias] = count;
671
+ return { row, values, count };
672
+ });
673
+ outputs.sort((a, b) => {
674
+ for (const { field, direction } of this.sorts) {
675
+ let c;
676
+ if (field === this.countAlias) {
677
+ c = a.count - b.count;
678
+ }
679
+ else {
680
+ const index = columns.indexOf(field);
681
+ const aliasIndex = this.selects.findIndex((s) => s.alias === field);
682
+ const i = index !== -1
683
+ ? index
684
+ : aliasIndex !== -1
685
+ ? columns.indexOf(this.selects[aliasIndex].expr)
686
+ : -1;
687
+ if (i === -1)
688
+ throw new InMemoryQueryError(`Cannot order grouped rows by "${field}".`);
689
+ c = this.compareValues(a.values[i], b.values[i], kinds[i]);
690
+ }
691
+ if (c !== 0)
692
+ return direction === 'desc' ? -c : c;
693
+ }
694
+ return 0;
695
+ });
696
+ return outputs.map((o) => o.row);
697
+ }
698
+ window(rows) {
699
+ const start = this.offsetCount ?? 0;
700
+ const end = this.limitCount === undefined ? undefined : start + this.limitCount;
701
+ return rows.slice(start, end);
702
+ }
703
+ /**
704
+ * Execute: the matching rows in order, honouring `limit`/`offset` (which is what a cursor page
705
+ * sets). Under a `distinct`, a `select` or a `groupBy`, the rows are projection objects — just
706
+ * as a Lucid query returns them.
707
+ */
708
+ all() {
709
+ return this.window(this.evaluate());
710
+ }
711
+ /** How many rows (or distinct tuples / groups) match, ignoring `limit`/`offset`. */
712
+ total() {
713
+ return this.evaluate().length;
714
+ }
715
+ /**
716
+ * One offset page — the in-memory `query.paginate(page, size)`. Feed it the `{ page, size }`
717
+ * the runner resolved (clamped and defaulted), never raw request values.
718
+ */
719
+ paginate(page, perPage) {
720
+ const size = Math.max(1, Math.floor(perPage));
721
+ const current = Math.max(1, Math.floor(page));
722
+ const rows = this.evaluate();
723
+ const total = rows.length;
724
+ return {
725
+ data: rows.slice((current - 1) * size, current * size),
726
+ meta: {
727
+ total,
728
+ perPage: size,
729
+ currentPage: current,
730
+ lastPage: Math.max(1, Math.ceil(total / size)),
731
+ firstPage: 1,
732
+ },
733
+ };
734
+ }
735
+ /**
736
+ * Thenable, like a Lucid query: `await qb` resolves to {@link all}. This is what lets helpers
737
+ * that execute a builder by awaiting it (`groupByCountFromRequest`) run over an array.
738
+ */
739
+ // biome-ignore lint/suspicious/noThenProperty: deliberately thenable, mirroring Lucid's query builders.
740
+ then(onfulfilled, onrejected) {
741
+ return new Promise((resolve) => resolve(this.all())).then(onfulfilled, onrejected);
742
+ }
743
+ }
744
+ // ---------------------------------------------------------------------------------------------
745
+ // One-call helpers
746
+ // ---------------------------------------------------------------------------------------------
747
+ /**
748
+ * Filter, search, sort and offset-paginate `rows` in one call: {@link applyFilter} over a fresh
749
+ * builder, then {@link InMemoryQueryBuilder.paginate} with the resolved page. A `distinct` request
750
+ * pages the distinct tuples instead of rows.
751
+ *
752
+ * ```ts
753
+ * const page = filterInMemory(users, rows, parseFilterRequest(ctx.request.qs()), {
754
+ * allowed: ['name', 'age', 'posts.title'],
755
+ * searchable: ['name'],
756
+ * fieldTypes: users.fieldTypes,
757
+ * })
758
+ * // → { data, meta: { total, perPage, currentPage, lastPage, firstPage } }
759
+ * ```
760
+ */
761
+ export function filterInMemory(collection, rows, input, config) {
762
+ const qb = collection.query(rows);
763
+ const { page, size } = applyFilter(qb, input, config);
764
+ return qb.paginate(page, size);
765
+ }
766
+ /**
767
+ * The keyset (cursor) counterpart of {@link filterInMemory}: {@link applyCursor} over a fresh
768
+ * builder (the primary key defaults to the collection's), then `buildCursorPage`. Keyset values
769
+ * are read through the collection, so a virtual field or a to-one relation path works as a cursor
770
+ * sort field.
771
+ */
772
+ export function cursorInMemory(collection, rows, input, config) {
773
+ const qb = collection.query(rows);
774
+ const cursorConfig = {
775
+ ...config,
776
+ primaryKey: config.primaryKey ?? collection.primaryKey,
777
+ };
778
+ const resolved = applyCursor(qb, input, cursorConfig);
779
+ const fetched = qb.all();
780
+ // buildCursorPage reads keyset values as plain properties; stand each row in with an object
781
+ // carrying its keyset values (virtual fields and relation paths resolved), then map back.
782
+ const originals = new Map();
783
+ const keyed = fetched.map((row) => {
784
+ const values = resolved.keyset.map((k) => qb.read(row, k.field));
785
+ const stand = {};
786
+ resolved.keyset.forEach((k, i) => {
787
+ setPath(stand, k.field, values[i]);
788
+ });
789
+ originals.set(stand, row);
790
+ return stand;
791
+ });
792
+ const page = buildCursorPage(keyed, resolved);
793
+ return { ...page, items: page.items.map((stand) => originals.get(stand)) };
794
+ }
795
+ function setPath(target, path, value) {
796
+ const segments = path.split('.');
797
+ let node = target;
798
+ for (const segment of segments.slice(0, -1)) {
799
+ const next = node[segment];
800
+ if (typeof next === 'object' && next !== null)
801
+ node = next;
802
+ else {
803
+ const created = {};
804
+ node[segment] = created;
805
+ node = created;
806
+ }
807
+ }
808
+ node[segments[segments.length - 1]] = value;
809
+ }
810
+ //# sourceMappingURL=in_memory.js.map