@adonis-agora/filter 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/dist/commands/main.d.ts +17 -0
  4. package/dist/commands/main.d.ts.map +1 -0
  5. package/dist/commands/main.js +17 -0
  6. package/dist/commands/main.js.map +1 -0
  7. package/dist/commands/make_filter_client.d.ts +35 -0
  8. package/dist/commands/make_filter_client.d.ts.map +1 -0
  9. package/dist/commands/make_filter_client.js +85 -0
  10. package/dist/commands/make_filter_client.js.map +1 -0
  11. package/dist/src/apply_from_request.d.ts +69 -0
  12. package/dist/src/apply_from_request.d.ts.map +1 -0
  13. package/dist/src/apply_from_request.js +115 -0
  14. package/dist/src/apply_from_request.js.map +1 -0
  15. package/dist/src/cursor.d.ts +92 -0
  16. package/dist/src/cursor.d.ts.map +1 -0
  17. package/dist/src/cursor.js +107 -0
  18. package/dist/src/cursor.js.map +1 -0
  19. package/dist/src/escape-like.d.ts +10 -0
  20. package/dist/src/escape-like.d.ts.map +1 -0
  21. package/dist/src/escape-like.js +15 -0
  22. package/dist/src/escape-like.js.map +1 -0
  23. package/dist/src/field_aliases.d.ts +28 -0
  24. package/dist/src/field_aliases.d.ts.map +1 -0
  25. package/dist/src/field_aliases.js +61 -0
  26. package/dist/src/field_aliases.js.map +1 -0
  27. package/dist/src/filter_spec.d.ts +146 -0
  28. package/dist/src/filter_spec.d.ts.map +1 -0
  29. package/dist/src/filter_spec.js +140 -0
  30. package/dist/src/filter_spec.js.map +1 -0
  31. package/dist/src/generate_client.d.ts +107 -0
  32. package/dist/src/generate_client.d.ts.map +1 -0
  33. package/dist/src/generate_client.js +154 -0
  34. package/dist/src/generate_client.js.map +1 -0
  35. package/dist/src/index.d.ts +28 -0
  36. package/dist/src/index.d.ts.map +1 -0
  37. package/dist/src/index.js +18 -0
  38. package/dist/src/index.js.map +1 -0
  39. package/dist/src/lucid_adapter.d.ts +213 -0
  40. package/dist/src/lucid_adapter.d.ts.map +1 -0
  41. package/dist/src/lucid_adapter.js +303 -0
  42. package/dist/src/lucid_adapter.js.map +1 -0
  43. package/dist/src/normalizer.d.ts +21 -0
  44. package/dist/src/normalizer.d.ts.map +1 -0
  45. package/dist/src/normalizer.js +61 -0
  46. package/dist/src/normalizer.js.map +1 -0
  47. package/dist/src/operators.d.ts +37 -0
  48. package/dist/src/operators.d.ts.map +1 -0
  49. package/dist/src/operators.js +43 -0
  50. package/dist/src/operators.js.map +1 -0
  51. package/dist/src/parse_request.d.ts +22 -0
  52. package/dist/src/parse_request.d.ts.map +1 -0
  53. package/dist/src/parse_request.js +107 -0
  54. package/dist/src/parse_request.js.map +1 -0
  55. package/dist/src/runner.d.ts +46 -0
  56. package/dist/src/runner.d.ts.map +1 -0
  57. package/dist/src/runner.js +190 -0
  58. package/dist/src/runner.js.map +1 -0
  59. package/dist/src/source_resolver.d.ts +18 -0
  60. package/dist/src/source_resolver.d.ts.map +1 -0
  61. package/dist/src/source_resolver.js +64 -0
  62. package/dist/src/source_resolver.js.map +1 -0
  63. package/dist/src/spatie_parser.d.ts +38 -0
  64. package/dist/src/spatie_parser.d.ts.map +1 -0
  65. package/dist/src/spatie_parser.js +130 -0
  66. package/dist/src/spatie_parser.js.map +1 -0
  67. package/dist/src/testing.d.ts +62 -0
  68. package/dist/src/testing.d.ts.map +1 -0
  69. package/dist/src/testing.js +113 -0
  70. package/dist/src/testing.js.map +1 -0
  71. package/dist/src/types.d.ts +130 -0
  72. package/dist/src/types.d.ts.map +1 -0
  73. package/dist/src/types.js +2 -0
  74. package/dist/src/types.js.map +1 -0
  75. package/dist/src/validate-column-filter.d.ts +30 -0
  76. package/dist/src/validate-column-filter.d.ts.map +1 -0
  77. package/dist/src/validate-column-filter.js +148 -0
  78. package/dist/src/validate-column-filter.js.map +1 -0
  79. package/dist/src/value-shape.d.ts +18 -0
  80. package/dist/src/value-shape.d.ts.map +1 -0
  81. package/dist/src/value-shape.js +36 -0
  82. package/dist/src/value-shape.js.map +1 -0
  83. package/package.json +71 -0
@@ -0,0 +1,46 @@
1
+ import { type CursorParams, type ResolvedCursor } from './cursor.js';
2
+ import { type QueryBuilderLike } from './lucid_adapter.js';
3
+ import type { FilterConfig, FilterInput } from './types.js';
4
+ /** The resolved offset pagination to hand to Lucid's `query.paginate(page, size)`. */
5
+ export interface ResolvedPagination {
6
+ page: number;
7
+ size: number;
8
+ }
9
+ /**
10
+ * Apply a parsed {@link FilterInput} to a Lucid query builder under a
11
+ * {@link FilterConfig} policy, and return the resolved offset pagination.
12
+ *
13
+ * The allow-lists are the security boundary — fields not in `allowed`/`sortable`/
14
+ * `searchable` are dropped (or rejected with `throwOnInvalid`). Pagination is
15
+ * returned (not applied) so the caller drives Lucid's `query.paginate()`:
16
+ *
17
+ * ```ts
18
+ * const { page, size } = applyFilter(Users.query(), input, { allowed: ['name', 'age'] })
19
+ * const result = await Users.query()... // or: await query.paginate(page, size)
20
+ * ```
21
+ */
22
+ export declare function applyFilter(qb: QueryBuilderLike, input: FilterInput, config: FilterConfig): ResolvedPagination;
23
+ /** Per-call policy for {@link applyCursor} — a {@link FilterConfig} plus the keyset tiebreaker. */
24
+ export interface CursorConfig extends FilterConfig {
25
+ /** Primary-key column appended to the keyset as a stable tiebreaker. Default `'id'`. */
26
+ primaryKey?: string;
27
+ }
28
+ /**
29
+ * Apply a parsed {@link FilterInput} plus {@link CursorParams} to a Lucid query
30
+ * builder as a **keyset (cursor) page** under a {@link CursorConfig} policy.
31
+ *
32
+ * Filters/search go through the same allow-list boundary as {@link applyFilter}.
33
+ * The effective (allow-listed) sort, plus the primary-key tiebreaker, forms the
34
+ * keyset; a supplied `after`/`before` cursor becomes a row-value seek predicate,
35
+ * and the builder is ordered + limited to `size + 1` (one extra row to detect a
36
+ * further page). Feed the fetched rows and the returned {@link ResolvedCursor}
37
+ * to {@link buildCursorPage} to assemble the page and its boundary cursors:
38
+ *
39
+ * ```ts
40
+ * const resolved = applyCursor(Users.query(), input, { allowed: ['name'], primaryKey: 'id' })
41
+ * const rows = await Users.query()...exec()
42
+ * const page = buildCursorPage(rows, resolved)
43
+ * ```
44
+ */
45
+ export declare function applyCursor(qb: QueryBuilderLike, input: FilterInput & CursorParams, config: CursorConfig): ResolvedCursor;
46
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/runner.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,cAAc,EAIpB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,KAAK,gBAAgB,EAOtB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAa,YAAY,EAAE,WAAW,EAAY,MAAM,YAAY,CAAC;AAGjF,sFAAsF;AACtF,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AA8HD;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CACzB,EAAE,EAAE,gBAAgB,EACpB,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,YAAY,GACnB,kBAAkB,CA+BpB;AAED,mGAAmG;AACnG,MAAM,WAAW,YAAa,SAAQ,YAAY;IAChD,wFAAwF;IACxF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CACzB,EAAE,EAAE,gBAAgB,EACpB,KAAK,EAAE,WAAW,GAAG,YAAY,EACjC,MAAM,EAAE,YAAY,GACnB,cAAc,CA0BhB"}
@@ -0,0 +1,190 @@
1
+ import { buildKeyset, decodeCursor, reverseKeyset, } from './cursor.js';
2
+ import { remapFilterAliases, remapSortAliases } from './field_aliases.js';
3
+ import { applyColumnFilters, applyFullTextSearch, applyKeyset, applySearch, applySort, applyVectorSimilarity, } from './lucid_adapter.js';
4
+ import { InvalidColumnFilterError, validateColumnFilters } from './validate-column-filter.js';
5
+ function isAllowed(field, allow) {
6
+ if (allow === '*')
7
+ return true;
8
+ if (typeof allow === 'function')
9
+ return allow(field);
10
+ return allow.includes(field);
11
+ }
12
+ function clamp(n, min, max) {
13
+ return Math.min(max, Math.max(min, n));
14
+ }
15
+ /**
16
+ * Recursively prune a filter against the allow-list. A leaf on a disallowed field
17
+ * is dropped (or throws when `throwOnInvalid`). Group nodes keep only their
18
+ * surviving children; a group left empty is itself dropped (returns `null`).
19
+ */
20
+ function prune(filter, allowed, throwOnInvalid) {
21
+ const hasField = typeof filter.field === 'string' && filter.field.length > 0;
22
+ if (hasField && !isAllowed(filter.field, allowed)) {
23
+ if (throwOnInvalid) {
24
+ throw new InvalidColumnFilterError(`Field "${filter.field}" is not filterable.`);
25
+ }
26
+ return null;
27
+ }
28
+ const next = { field: filter.field, operator: filter.operator };
29
+ if (filter.value !== undefined)
30
+ next.value = filter.value;
31
+ if (filter.AND) {
32
+ const kept = filter.AND.map((f) => prune(f, allowed, throwOnInvalid)).filter((f) => f !== null);
33
+ if (kept.length > 0)
34
+ next.AND = kept;
35
+ }
36
+ if (filter.OR) {
37
+ const kept = filter.OR.map((f) => prune(f, allowed, throwOnInvalid)).filter((f) => f !== null);
38
+ if (kept.length > 0)
39
+ next.OR = kept;
40
+ }
41
+ // A pure group node (no field of its own) with no surviving children is empty.
42
+ if (!hasField && next.AND === undefined && next.OR === undefined) {
43
+ return null;
44
+ }
45
+ return next;
46
+ }
47
+ /**
48
+ * Resolve aliases, structurally validate, and prune `input.filters`/`input.search`
49
+ * against the allow-lists, applying the survivors to the builder. Shared by both
50
+ * {@link applyFilter} (offset) and {@link applyCursor} (keyset) so the security
51
+ * boundary is identical for either pagination style.
52
+ */
53
+ function applyFilterConditions(qb, input, config) {
54
+ const throwOnInvalid = config.throwOnInvalid ?? false;
55
+ if (input.filters && input.filters.length > 0) {
56
+ // Alias resolution runs FIRST — the allow-list, validation and query builder
57
+ // all see the resolved target column, never the client-facing alias key.
58
+ const aliased = config.aliases
59
+ ? input.filters.map((f) => remapFilterAliases(f, config.aliases))
60
+ : input.filters;
61
+ // Structural validation next (operator/value shape, depth, field charset).
62
+ validateColumnFilters(aliased);
63
+ const safe = aliased
64
+ .map((f) => prune(f, config.allowed, throwOnInvalid))
65
+ .filter((f) => f !== null);
66
+ if (safe.length > 0) {
67
+ applyColumnFilters(qb, safe);
68
+ }
69
+ }
70
+ if (input.search) {
71
+ const term = input.search.trim();
72
+ if (term.length > 0) {
73
+ // tsvector full-text search is the primary path when the policy declares
74
+ // it; otherwise fall back to the portable ILIKE scan across `searchable`.
75
+ if (config.fullText) {
76
+ applyFullTextSearch(qb, {
77
+ query: term,
78
+ column: config.fullText.column,
79
+ ...(config.fullText.language !== undefined && { language: config.fullText.language }),
80
+ ...(config.fullText.rank !== undefined && { rank: config.fullText.rank }),
81
+ ...(config.fullText.columnKind !== undefined && {
82
+ columnKind: config.fullText.columnKind,
83
+ }),
84
+ });
85
+ }
86
+ else if (config.searchable && config.searchable.length > 0) {
87
+ applySearch(qb, term, config.searchable);
88
+ }
89
+ }
90
+ }
91
+ }
92
+ /** Resolve the alias-mapped, allow-listed sort directives for the request. */
93
+ function resolveSafeSort(sort, config) {
94
+ const throwOnInvalid = config.throwOnInvalid ?? false;
95
+ const sortable = config.sortable ?? config.allowed;
96
+ const aliased = config.aliases
97
+ ? remapSortAliases(sort, config.aliases)
98
+ : sort;
99
+ const safe = [];
100
+ for (const item of aliased) {
101
+ if (isAllowed(item.field, sortable)) {
102
+ safe.push(item);
103
+ }
104
+ else if (throwOnInvalid) {
105
+ throw new InvalidColumnFilterError(`Field "${item.field}" is not sortable.`);
106
+ }
107
+ }
108
+ return safe;
109
+ }
110
+ /**
111
+ * Apply a parsed {@link FilterInput} to a Lucid query builder under a
112
+ * {@link FilterConfig} policy, and return the resolved offset pagination.
113
+ *
114
+ * The allow-lists are the security boundary — fields not in `allowed`/`sortable`/
115
+ * `searchable` are dropped (or rejected with `throwOnInvalid`). Pagination is
116
+ * returned (not applied) so the caller drives Lucid's `query.paginate()`:
117
+ *
118
+ * ```ts
119
+ * const { page, size } = applyFilter(Users.query(), input, { allowed: ['name', 'age'] })
120
+ * const result = await Users.query()... // or: await query.paginate(page, size)
121
+ * ```
122
+ */
123
+ export function applyFilter(qb, input, config) {
124
+ applyFilterConditions(qb, input, config);
125
+ // Embedding-similarity ranking (opt-in, additive): only when the policy declares
126
+ // a similarity column AND the request carries a query embedding. Applied before
127
+ // the user sort so nearest-first distance is the primary ordering; any allowed
128
+ // sort then acts as a tiebreaker.
129
+ if (config.vectorSimilarity && input.vectorSimilarity && input.vectorSimilarity.length > 0) {
130
+ applyVectorSimilarity(qb, {
131
+ column: config.vectorSimilarity.column,
132
+ vector: input.vectorSimilarity,
133
+ ...(config.vectorSimilarity.metric !== undefined && {
134
+ metric: config.vectorSimilarity.metric,
135
+ }),
136
+ ...(config.vectorSimilarity.threshold !== undefined && {
137
+ threshold: config.vectorSimilarity.threshold,
138
+ }),
139
+ ...(config.vectorSimilarity.topK !== undefined && { topK: config.vectorSimilarity.topK }),
140
+ });
141
+ }
142
+ if (input.sort && input.sort.length > 0) {
143
+ const safeSort = resolveSafeSort(input.sort, config);
144
+ if (safeSort.length > 0) {
145
+ applySort(qb, safeSort);
146
+ }
147
+ }
148
+ const size = clamp(input.size ?? config.defaultSize ?? 25, 1, config.maxSize ?? 100);
149
+ const page = Math.max(1, input.page ?? 1);
150
+ return { page, size };
151
+ }
152
+ /**
153
+ * Apply a parsed {@link FilterInput} plus {@link CursorParams} to a Lucid query
154
+ * builder as a **keyset (cursor) page** under a {@link CursorConfig} policy.
155
+ *
156
+ * Filters/search go through the same allow-list boundary as {@link applyFilter}.
157
+ * The effective (allow-listed) sort, plus the primary-key tiebreaker, forms the
158
+ * keyset; a supplied `after`/`before` cursor becomes a row-value seek predicate,
159
+ * and the builder is ordered + limited to `size + 1` (one extra row to detect a
160
+ * further page). Feed the fetched rows and the returned {@link ResolvedCursor}
161
+ * to {@link buildCursorPage} to assemble the page and its boundary cursors:
162
+ *
163
+ * ```ts
164
+ * const resolved = applyCursor(Users.query(), input, { allowed: ['name'], primaryKey: 'id' })
165
+ * const rows = await Users.query()...exec()
166
+ * const page = buildCursorPage(rows, resolved)
167
+ * ```
168
+ */
169
+ export function applyCursor(qb, input, config) {
170
+ applyFilterConditions(qb, input, config);
171
+ const safeSort = input.sort && input.sort.length > 0 ? resolveSafeSort(input.sort, config) : [];
172
+ const baseKeyset = buildKeyset(safeSort, config.primaryKey ?? 'id');
173
+ const backward = input.before !== undefined && input.after === undefined;
174
+ const cursorStr = input.after ?? input.before;
175
+ const requested = backward ? input.last : input.first;
176
+ const size = clamp(requested ?? config.defaultSize ?? 25, 1, config.maxSize ?? 100);
177
+ // For backward paging, reverse keyset directions so the boundary seek and
178
+ // ordering walk the other way; buildCursorPage re-reverses the rows.
179
+ const queryKeyset = backward ? reverseKeyset(baseKeyset) : baseKeyset;
180
+ if (cursorStr !== undefined) {
181
+ const values = decodeCursor(cursorStr);
182
+ if (values && values.length === queryKeyset.length) {
183
+ applyKeyset(qb, queryKeyset, values);
184
+ }
185
+ }
186
+ applySort(qb, queryKeyset);
187
+ qb.limit(size + 1);
188
+ return { keyset: baseKeyset, size, backward, hasCursor: cursorStr !== undefined };
189
+ }
190
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/runner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { InputSource } from './types.js';
2
+ /**
3
+ * Resolve the raw structured input from a request according to an
4
+ * {@link InputSource}:
5
+ *
6
+ * - a function — a custom extractor, called with the request.
7
+ * - `'query'` / `'body'` — always that container.
8
+ * - `'auto'` (or any other bare token) — query on reads (GET/HEAD), query+body
9
+ * merged (body wins) on writes (POST/PUT/PATCH/DELETE).
10
+ * - a dot-path (e.g. `'body.filters'`) — the nested object at that path, or `{}`
11
+ * when the path is missing or does not resolve to an object.
12
+ *
13
+ * The `req` is treated structurally (`{ method, query, body }`), so an AdonisJS
14
+ * `HttpContext.request` — whose `qs()`/`body()` you pass in — or a plain object
15
+ * both work. The result is always a fresh shallow copy.
16
+ */
17
+ export declare function resolveInputFromRequest(req: unknown, source: InputSource): Record<string, unknown>;
18
+ //# sourceMappingURL=source_resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source_resolver.d.ts","sourceRoot":"","sources":["../../src/source_resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAW9C;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,WAAW,GAClB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA+BzB"}
@@ -0,0 +1,64 @@
1
+ const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
2
+ const READ_METHODS = new Set(['GET', 'HEAD']);
3
+ /**
4
+ * Resolve the raw structured input from a request according to an
5
+ * {@link InputSource}:
6
+ *
7
+ * - a function — a custom extractor, called with the request.
8
+ * - `'query'` / `'body'` — always that container.
9
+ * - `'auto'` (or any other bare token) — query on reads (GET/HEAD), query+body
10
+ * merged (body wins) on writes (POST/PUT/PATCH/DELETE).
11
+ * - a dot-path (e.g. `'body.filters'`) — the nested object at that path, or `{}`
12
+ * when the path is missing or does not resolve to an object.
13
+ *
14
+ * The `req` is treated structurally (`{ method, query, body }`), so an AdonisJS
15
+ * `HttpContext.request` — whose `qs()`/`body()` you pass in — or a plain object
16
+ * both work. The result is always a fresh shallow copy.
17
+ */
18
+ export function resolveInputFromRequest(req, source) {
19
+ if (typeof source === 'function') {
20
+ const r = source(req);
21
+ return { ...(r ?? {}) };
22
+ }
23
+ const r = (req ?? {});
24
+ const query = (r.query ?? {});
25
+ const body = (r.body ?? {});
26
+ switch (source) {
27
+ case 'query':
28
+ return { ...query };
29
+ case 'body':
30
+ return { ...body };
31
+ case 'auto': {
32
+ const method = (r.method ?? 'GET').toUpperCase();
33
+ if (READ_METHODS.has(method))
34
+ return { ...query };
35
+ if (WRITE_METHODS.has(method))
36
+ return { ...query, ...body };
37
+ return { ...query };
38
+ }
39
+ default: {
40
+ if (source.includes('.')) {
41
+ const resolved = resolveDotPath(r, source);
42
+ return { ...(resolved ?? {}) };
43
+ }
44
+ const method = (r.method ?? 'GET').toUpperCase();
45
+ if (READ_METHODS.has(method))
46
+ return { ...query };
47
+ if (WRITE_METHODS.has(method))
48
+ return { ...query, ...body };
49
+ return { ...query };
50
+ }
51
+ }
52
+ }
53
+ function resolveDotPath(obj, path) {
54
+ let current = obj;
55
+ for (const segment of path.split('.')) {
56
+ if (current == null || typeof current !== 'object')
57
+ return undefined;
58
+ current = current[segment];
59
+ }
60
+ if (current == null || typeof current !== 'object')
61
+ return undefined;
62
+ return current;
63
+ }
64
+ //# sourceMappingURL=source_resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source_resolver.js","sourceRoot":"","sources":["../../src/source_resolver.ts"],"names":[],"mappings":"AAEA,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC;AAClE,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;AAQ9C;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,uBAAuB,CACrC,GAAY,EACZ,MAAmB;IAEnB,IAAI,OAAO,MAAM,KAAK,UAAU,EAAE,CAAC;QACjC,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QACtB,OAAO,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,EAA6B,CAAC;IACrD,CAAC;IACD,MAAM,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,CAAY,CAAC;IACjC,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAA4B,CAAC;IACzD,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAA4B,CAAC;IAEvD,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,OAAO;YACV,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;QACtB,KAAK,MAAM;YACT,OAAO,EAAE,GAAG,IAAI,EAAE,CAAC;QACrB,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;YACjD,IAAI,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;YAClD,IAAI,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,OAAO,EAAE,GAAG,KAAK,EAAE,GAAG,IAAI,EAAE,CAAC;YAC5D,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;QACtB,CAAC;QACD,OAAO,CAAC,CAAC,CAAC;YACR,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBACzB,MAAM,QAAQ,GAAG,cAAc,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;gBAC3C,OAAO,EAAE,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,EAAE,CAAC;YACjC,CAAC;YACD,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM,IAAI,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;YACjD,IAAI,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;YAClD,IAAI,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,OAAO,EAAE,GAAG,KAAK,EAAE,GAAG,IAAI,EAAE,CAAC;YAC5D,OAAO,EAAE,GAAG,KAAK,EAAE,CAAC;QACtB,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,GAAY,EAAE,IAAY;IAChD,IAAI,OAAO,GAAY,GAAG,CAAC;IAC3B,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACtC,IAAI,OAAO,IAAI,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QACrE,OAAO,GAAI,OAAmC,CAAC,OAAO,CAAC,CAAC;IAC1D,CAAC;IACD,IAAI,OAAO,IAAI,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IACrE,OAAO,OAAkC,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,38 @@
1
+ import type { CursorParams } from './cursor.js';
2
+ import type { FilterInput } from './types.js';
3
+ /**
4
+ * The richer Spatie / JSON:API input shape produced by {@link parseSpatieRequest}.
5
+ * A superset of {@link FilterInput}: it adds cursor pagination
6
+ * ({@link CursorParams}), JSON:API sparse fieldsets (`select`) and relation
7
+ * includes (`include`). Ready to hand to `applyFilter` (offset) or `applyCursor`
8
+ * (keyset) — the extra fields are ignored by whichever runner does not use them.
9
+ */
10
+ export interface SpatieInput extends FilterInput, CursorParams {
11
+ /** JSON:API sparse fieldsets (`fields[resource]=a,b`), flattened + de-duped. */
12
+ select?: string[];
13
+ /** Relation includes (`include=posts,comments`). */
14
+ include?: string[];
15
+ }
16
+ /**
17
+ * Parse a decoded, Spatie-laravel-query-builder / JSON:API-style query object
18
+ * into a {@link SpatieInput}. This is the richer, additive counterpart to
19
+ * {@link parseFilterRequest}: it understands the same `filter`/`sort`/`search`
20
+ * shapes plus JSON:API `include`, sparse `fields[resource]` sets, and cursor
21
+ * pagination (`page[after]`/`page[before]`).
22
+ *
23
+ * | Query string | Decoded input | Mapped to |
24
+ * | ------------------------------- | ------------------------------------ | ------------------------------------------ |
25
+ * | `filter[name]=Al` | `{ filter: { name: 'Al' } }` | `equals` column filter |
26
+ * | `filter[id]=1,2,3` | `{ filter: { id: '1,2,3' } }` | `in` column filter |
27
+ * | `filter[age][gte]=18` | `{ filter: { age: { gte: '18' } } }` | operator column filter |
28
+ * | `sort=-createdAt,name` | `{ sort: '-createdAt,name' }` | sort items |
29
+ * | `include=posts,comments` | `{ include: 'posts,comments' }` | `include: ['posts', 'comments']` |
30
+ * | `fields[users]=id,name` | `{ fields: { users: 'id,name' } }` | `select: ['id', 'name']` |
31
+ * | `page[number]=2&page[size]=10` | `{ page: { number, size } }` | offset (`page`/`size`) |
32
+ * | `page[after]=cur&page[size]=10`| `{ page: { after, size } }` | cursor (`after`/`first`) |
33
+ *
34
+ * A pure reshape — no validation or allow-listing here; that happens downstream
35
+ * in `applyFilter` / `applyCursor` against the `FilterConfig`.
36
+ */
37
+ export declare function parseSpatieRequest(qs: unknown): SpatieInput;
38
+ //# sourceMappingURL=spatie_parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spatie_parser.d.ts","sourceRoot":"","sources":["../../src/spatie_parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;GAMG;AACH,MAAM,WAAW,WAAY,SAAQ,WAAW,EAAE,YAAY;IAC5D,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAWD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,OAAO,GAAG,WAAW,CA2B3D"}
@@ -0,0 +1,130 @@
1
+ import { parseSort, toColumnFilters } from './parse_request.js';
2
+ function toInt(value) {
3
+ if (typeof value === 'number' && Number.isFinite(value))
4
+ return value;
5
+ if (typeof value === 'string' && value.trim() !== '') {
6
+ const n = Number(value);
7
+ if (Number.isFinite(n))
8
+ return n;
9
+ }
10
+ return undefined;
11
+ }
12
+ /**
13
+ * Parse a decoded, Spatie-laravel-query-builder / JSON:API-style query object
14
+ * into a {@link SpatieInput}. This is the richer, additive counterpart to
15
+ * {@link parseFilterRequest}: it understands the same `filter`/`sort`/`search`
16
+ * shapes plus JSON:API `include`, sparse `fields[resource]` sets, and cursor
17
+ * pagination (`page[after]`/`page[before]`).
18
+ *
19
+ * | Query string | Decoded input | Mapped to |
20
+ * | ------------------------------- | ------------------------------------ | ------------------------------------------ |
21
+ * | `filter[name]=Al` | `{ filter: { name: 'Al' } }` | `equals` column filter |
22
+ * | `filter[id]=1,2,3` | `{ filter: { id: '1,2,3' } }` | `in` column filter |
23
+ * | `filter[age][gte]=18` | `{ filter: { age: { gte: '18' } } }` | operator column filter |
24
+ * | `sort=-createdAt,name` | `{ sort: '-createdAt,name' }` | sort items |
25
+ * | `include=posts,comments` | `{ include: 'posts,comments' }` | `include: ['posts', 'comments']` |
26
+ * | `fields[users]=id,name` | `{ fields: { users: 'id,name' } }` | `select: ['id', 'name']` |
27
+ * | `page[number]=2&page[size]=10` | `{ page: { number, size } }` | offset (`page`/`size`) |
28
+ * | `page[after]=cur&page[size]=10`| `{ page: { after, size } }` | cursor (`after`/`first`) |
29
+ *
30
+ * A pure reshape — no validation or allow-listing here; that happens downstream
31
+ * in `applyFilter` / `applyCursor` against the `FilterConfig`.
32
+ */
33
+ export function parseSpatieRequest(qs) {
34
+ if (qs == null || typeof qs !== 'object')
35
+ return {};
36
+ const src = qs;
37
+ const out = {};
38
+ if (src.filter != null && typeof src.filter === 'object' && !Array.isArray(src.filter)) {
39
+ const filters = [];
40
+ for (const [field, value] of Object.entries(src.filter)) {
41
+ filters.push(...toColumnFilters(field, value));
42
+ }
43
+ if (filters.length > 0)
44
+ out.filters = filters;
45
+ }
46
+ const sort = parseSort(src.sort);
47
+ if (sort.length > 0)
48
+ out.sort = sort;
49
+ if (typeof src.search === 'string' && src.search.length > 0)
50
+ out.search = src.search;
51
+ const include = parseInclude(src.include);
52
+ if (include.length > 0)
53
+ out.include = include;
54
+ const select = parseSparseFieldsets(src.fields);
55
+ if (select.length > 0)
56
+ out.select = select;
57
+ applyPage(src.page, out);
58
+ return out;
59
+ }
60
+ /** Parse `include=posts,comments` (string or array) into a relation-name list. */
61
+ function parseInclude(include) {
62
+ if (typeof include === 'string') {
63
+ return include
64
+ .split(',')
65
+ .map((s) => s.trim())
66
+ .filter((s) => s.length > 0);
67
+ }
68
+ if (Array.isArray(include)) {
69
+ return include.filter((s) => typeof s === 'string' && s.length > 0);
70
+ }
71
+ return [];
72
+ }
73
+ /**
74
+ * Flatten JSON:API sparse fieldsets (`fields[resource]=a,b`) into a single
75
+ * order-preserving, de-duplicated list of column names. Per-resource keys are
76
+ * collapsed because a single SELECT applies to the primary entity.
77
+ */
78
+ function parseSparseFieldsets(fields) {
79
+ if (fields == null || typeof fields !== 'object' || Array.isArray(fields))
80
+ return [];
81
+ const seen = new Set();
82
+ const out = [];
83
+ for (const value of Object.values(fields)) {
84
+ const cols = typeof value === 'string'
85
+ ? value.split(',')
86
+ : Array.isArray(value)
87
+ ? value.filter((s) => typeof s === 'string')
88
+ : [];
89
+ for (const raw of cols) {
90
+ const col = raw.trim();
91
+ if (col.length > 0 && !seen.has(col)) {
92
+ seen.add(col);
93
+ out.push(col);
94
+ }
95
+ }
96
+ }
97
+ return out;
98
+ }
99
+ /**
100
+ * Map a JSON:API `page` object onto `out`: `page[after]`/`page[before]` →
101
+ * cursor (`after`/`first`, `before`/`last`); otherwise `page[number]`/`page[size]`
102
+ * → offset (`page`/`size`). `after` wins if both cursor bounds are present.
103
+ */
104
+ function applyPage(page, out) {
105
+ if (page == null || typeof page !== 'object' || Array.isArray(page))
106
+ return;
107
+ const p = page;
108
+ const after = typeof p.after === 'string' ? p.after : undefined;
109
+ const before = typeof p.before === 'string' ? p.before : undefined;
110
+ const size = toInt(p.size);
111
+ if (after !== undefined || before !== undefined) {
112
+ if (after !== undefined) {
113
+ out.after = after;
114
+ if (size !== undefined)
115
+ out.first = size;
116
+ }
117
+ else if (before !== undefined) {
118
+ out.before = before;
119
+ if (size !== undefined)
120
+ out.last = size;
121
+ }
122
+ return;
123
+ }
124
+ const number = toInt(p.number);
125
+ if (number !== undefined)
126
+ out.page = number;
127
+ if (size !== undefined)
128
+ out.size = size;
129
+ }
130
+ //# sourceMappingURL=spatie_parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spatie_parser.js","sourceRoot":"","sources":["../../src/spatie_parser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import type { QueryBuilderLike } from './lucid_adapter.js';
2
+ /** A recorded query-builder call: method name + its scalar args. */
3
+ export interface RecordedCall {
4
+ method: string;
5
+ args: unknown[];
6
+ }
7
+ /**
8
+ * A recording stand-in for a Lucid query builder, published under
9
+ * `@adonis-agora/filter/testing` so consumers can unit-test their own filter
10
+ * definitions without a database. It satisfies {@link QueryBuilderLike}, so any
11
+ * of the adapter/runner helpers (or {@link applyFilterFromRequest}) can be
12
+ * driven against it and the resulting translation asserted.
13
+ *
14
+ * Nested `where`/`orWhere` closures run against child recorders, and a dotted
15
+ * relation-path filter's `whereHas` records the relation name plus runs its
16
+ * nested callback against a child. {@link flatten} collects every leaf call
17
+ * (across all groups and relation subqueries) so tests can assert a translation
18
+ * appeared without caring about the exact grouping nesting.
19
+ */
20
+ export declare class MockQueryBuilder implements QueryBuilderLike {
21
+ calls: RecordedCall[];
22
+ children: MockQueryBuilder[];
23
+ private record;
24
+ private group;
25
+ where(a: any, b?: any, c?: any): this;
26
+ orWhere(cb: (qb: QueryBuilderLike) => void): this;
27
+ /**
28
+ * Record a relation subquery: the `whereHas` call is captured with the
29
+ * relation name (so `find('whereHas')?.args[0]` is the relation), and the
30
+ * nested callback runs against a child recorder whose calls surface via
31
+ * {@link flatten}.
32
+ */
33
+ whereHas(relation: string, cb: (qb: QueryBuilderLike) => void): this;
34
+ whereNot(column: string, value: unknown): this;
35
+ whereIn(column: string, values: unknown[]): this;
36
+ whereNotIn(column: string, values: unknown[]): this;
37
+ whereNull(column: string): this;
38
+ whereNotNull(column: string): this;
39
+ whereBetween(column: string, range: [unknown, unknown]): this;
40
+ whereNotBetween(column: string, range: [unknown, unknown]): this;
41
+ whereILike(column: string, value: string): this;
42
+ orWhereILike(column: string, value: string): this;
43
+ orderBy(column: string, direction: 'asc' | 'desc'): this;
44
+ /** Record a raw predicate: `args` are `[sql, bindings]` (bindings default `[]`). */
45
+ whereRaw(sql: string, bindings?: readonly unknown[]): this;
46
+ /** Record a raw ordering: `args` are `[sql, bindings]` (bindings default `[]`). */
47
+ orderByRaw(sql: string, bindings?: readonly unknown[]): this;
48
+ limit(count: number): this;
49
+ /**
50
+ * Every recorded call across this builder and all nested groups and relation
51
+ * subqueries (excludes the anonymous group markers, but keeps `whereHas` since
52
+ * its recorded arg is the relation name, not a marker).
53
+ */
54
+ flatten(): RecordedCall[];
55
+ /** Find the first flattened call matching `method`. */
56
+ find(method: string): RecordedCall | undefined;
57
+ /** Every flattened call matching `method` (handy for asserting relation subqueries). */
58
+ findAll(method: string): RecordedCall[];
59
+ }
60
+ /** Factory for a fresh {@link MockQueryBuilder} — the recording Lucid stand-in. */
61
+ export declare function makeMockQueryBuilder(): MockQueryBuilder;
62
+ //# sourceMappingURL=testing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../src/testing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,EAAE,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,gBAAiB,YAAW,gBAAgB;IACvD,KAAK,EAAE,YAAY,EAAE,CAAM;IAC3B,QAAQ,EAAE,gBAAgB,EAAE,CAAM;IAElC,OAAO,CAAC,MAAM;IAKd,OAAO,CAAC,KAAK;IASb,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,GAAG,IAAI;IAIrC,OAAO,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,gBAAgB,KAAK,IAAI,GAAG,IAAI;IAGjD;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,gBAAgB,KAAK,IAAI,GAAG,IAAI;IAOpE,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;IAG9C,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI;IAGhD,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI;IAGnD,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAG/B,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAGlC,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,IAAI;IAG7D,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,IAAI;IAGhE,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAG/C,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAGjD,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,GAAG,MAAM,GAAG,IAAI;IAGxD,oFAAoF;IACpF,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,SAAS,OAAO,EAAO,GAAG,IAAI;IAG9D,mFAAmF;IACnF,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,SAAS,OAAO,EAAO,GAAG,IAAI;IAGhE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAI1B;;;;OAIG;IACH,OAAO,IAAI,YAAY,EAAE;IAKzB,uDAAuD;IACvD,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS;IAI9C,wFAAwF;IACxF,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE;CAGxC;AAED,mFAAmF;AACnF,wBAAgB,oBAAoB,IAAI,gBAAgB,CAEvD"}