@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,82 @@
1
+ /**
2
+ * Coerce a raw filter value to its declared column kind, or reject it.
3
+ *
4
+ * Why this exists: a filter value arriving over a query string is ALWAYS a string
5
+ * (`?filter[dayOfWeek][equals]=3` yields `'3'`). Postgres papers over the benign cases with an
6
+ * implicit cast — `day_of_week = '3'` and `is_recurring = 'false'` both work — so the gap is
7
+ * invisible until a client sends something that ISN'T castable: `is_recurring = 'xyz'` raises
8
+ * `invalid input syntax for type boolean` at the database, which surfaces as a 500 on a public
9
+ * endpoint from pure user input. The allow-list guards which FIELD may be filtered; this guards
10
+ * what VALUE may reach the column.
11
+ *
12
+ * Rejection is not an exception here: the caller maps it onto the spec's existing `throwOnInvalid`
13
+ * semantics, so a bad value behaves exactly like a disallowed field — dropped by default, or a
14
+ * loud `InvalidColumnFilterError` (→ 400) when the spec asks for it.
15
+ */
16
+ export function coerceFilterValue(value, kind) {
17
+ // `null` is meaningful for every kind (`equals: null` is a legitimate IS NULL comparison) and
18
+ // never triggers a cast error, so it short-circuits before any kind-specific parsing.
19
+ if (value === null)
20
+ return { ok: true, value: null };
21
+ switch (kind) {
22
+ case 'number': {
23
+ if (typeof value === 'number') {
24
+ return Number.isFinite(value)
25
+ ? { ok: true, value }
26
+ : { ok: false, reason: 'not a finite number' };
27
+ }
28
+ if (typeof value === 'string') {
29
+ const trimmed = value.trim();
30
+ // Number('') === 0 and Number(' ') === 0 — both would silently become a valid 0, so an
31
+ // empty string is rejected up front rather than coerced into a filter nobody asked for.
32
+ if (trimmed === '')
33
+ return { ok: false, reason: 'empty string is not a number' };
34
+ const n = Number(trimmed);
35
+ // Number('12abc') is NaN, which is what rejects partial garbage.
36
+ return Number.isFinite(n) ? { ok: true, value: n } : { ok: false, reason: 'not a number' };
37
+ }
38
+ return { ok: false, reason: 'not a number' };
39
+ }
40
+ case 'boolean': {
41
+ if (typeof value === 'boolean')
42
+ return { ok: true, value };
43
+ if (typeof value === 'string') {
44
+ const v = value.trim().toLowerCase();
45
+ if (v === 'true' || v === '1')
46
+ return { ok: true, value: true };
47
+ if (v === 'false' || v === '0')
48
+ return { ok: true, value: false };
49
+ return { ok: false, reason: 'not a boolean' };
50
+ }
51
+ return { ok: false, reason: 'not a boolean' };
52
+ }
53
+ case 'date': {
54
+ if (value instanceof Date) {
55
+ return Number.isNaN(value.getTime())
56
+ ? { ok: false, reason: 'invalid date' }
57
+ : { ok: true, value };
58
+ }
59
+ if (typeof value === 'string') {
60
+ const t = Date.parse(value);
61
+ // The string is handed back VERBATIM rather than as a Date: the driver already parses ISO
62
+ // strings, and converting here would silently re-zone a date-only value ('2026-07-15'
63
+ // becomes midnight UTC, which shifts the day for any negative-offset client). Date.parse is
64
+ // used only to reject, never to rewrite.
65
+ return Number.isNaN(t) ? { ok: false, reason: 'invalid date' } : { ok: true, value };
66
+ }
67
+ return { ok: false, reason: 'not a date' };
68
+ }
69
+ case 'string': {
70
+ if (typeof value === 'string')
71
+ return { ok: true, value };
72
+ if (typeof value === 'number' || typeof value === 'boolean') {
73
+ return { ok: true, value: String(value) };
74
+ }
75
+ return { ok: false, reason: 'not a string' };
76
+ }
77
+ // No column-level contract to enforce — pass through untouched.
78
+ default:
79
+ return { ok: true, value };
80
+ }
81
+ }
82
+ //# sourceMappingURL=coerce_value.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coerce_value.js","sourceRoot":"","sources":["../../src/coerce_value.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ import type { SortItem } from './types.js';
2
+ /**
3
+ * A decoded keyset cursor: the ordered values of the keyset columns (the active
4
+ * sort columns plus a stable primary-key tiebreaker) captured from a boundary
5
+ * row. The values line up positionally with the keyset's {@link SortItem}[].
6
+ */
7
+ export type CursorValues = unknown[];
8
+ /**
9
+ * Cursor (keyset) pagination parameters, as parsed from a request:
10
+ *
11
+ * - `after` — return the page immediately following this opaque cursor (forward).
12
+ * - `before` — return the page immediately preceding this opaque cursor (backward).
13
+ * - `first` / `last` — page size for forward / backward paging respectively.
14
+ *
15
+ * `after` and `before` are mutually exclusive; if both are given, `after` wins.
16
+ */
17
+ export interface CursorParams {
18
+ after?: string;
19
+ before?: string;
20
+ first?: number;
21
+ last?: number;
22
+ }
23
+ /**
24
+ * A single page of keyset-paginated results assembled by {@link buildCursorPage}.
25
+ *
26
+ * - `items` — the rows for this page, in the requested order.
27
+ * - `nextCursor` — opaque cursor for the next forward page, or `null` when this
28
+ * is the last page.
29
+ * - `prevCursor` — opaque cursor for the previous (backward) page, or `null`
30
+ * when this is the first page.
31
+ * - `hasNext` / `hasPrev` — convenience booleans mirroring the cursors.
32
+ */
33
+ export interface CursorPage<T> {
34
+ items: T[];
35
+ nextCursor: string | null;
36
+ prevCursor: string | null;
37
+ hasNext: boolean;
38
+ hasPrev: boolean;
39
+ }
40
+ /**
41
+ * Encodes keyset cursor values into a compact, URL-safe opaque string
42
+ * (base64url of a JSON array). The shape is intentionally opaque to clients —
43
+ * only this module reads it back.
44
+ *
45
+ * `Date` values are encoded as `{ $d: <iso> }` so they round-trip to `Date`
46
+ * instances on decode (plain JSON would yield a string and break date keyset
47
+ * comparisons).
48
+ */
49
+ export declare function encodeCursor(values: CursorValues): string;
50
+ /**
51
+ * Decodes an opaque cursor string back into its keyset values. Returns `null`
52
+ * when the cursor is malformed (bad base64, bad JSON, or not an array) so the
53
+ * caller can ignore an invalid cursor instead of crashing.
54
+ */
55
+ export declare function decodeCursor(cursor: string): CursorValues | null;
56
+ /**
57
+ * Builds the keyset {@link SortItem}[] for cursor pagination: the caller's
58
+ * effective sorts, with a stable primary-key tiebreaker appended if it is not
59
+ * already present. The tiebreaker inherits the direction of the last sort column
60
+ * so the overall ordering stays monotonic (important for a correct
61
+ * `(cols, pk) > (...)` comparison).
62
+ */
63
+ export declare function buildKeyset(sorts: SortItem[], primaryKey: string): SortItem[];
64
+ /** Flips every keyset column's direction (for backward cursor paging). */
65
+ export declare function reverseKeyset(keyset: SortItem[]): SortItem[];
66
+ /**
67
+ * Extracts the keyset values from a fetched row, in keyset column order.
68
+ * Supports dotted relation paths (e.g. `author.name`) by walking the object.
69
+ */
70
+ export declare function extractCursorValues(row: Record<string, unknown>, keyset: SortItem[]): CursorValues;
71
+ /** The keyset shape {@link applyCursor} resolves and hands to {@link buildCursorPage}. */
72
+ export interface ResolvedCursor {
73
+ /** The base keyset (effective sort + primary-key tiebreaker), in forward order. */
74
+ keyset: SortItem[];
75
+ /** Effective page size — the number of rows to keep for this page. */
76
+ size: number;
77
+ /** True when paging backward (a `before` cursor was supplied). */
78
+ backward: boolean;
79
+ /** True when a cursor (`after`/`before`) was supplied — the opposite page is then known to exist. */
80
+ hasCursor: boolean;
81
+ }
82
+ /**
83
+ * Assembles a {@link CursorPage} from the rows fetched for a keyset query.
84
+ *
85
+ * The query is expected to have been built by {@link applyCursor}, which fetches
86
+ * one extra row (`limit = size + 1`) so we can detect a further page. For
87
+ * backward paging the rows come back reversed and are flipped here to restore
88
+ * the caller's requested order. Boundary cursors are computed from the base
89
+ * keyset so they round-trip regardless of paging direction.
90
+ */
91
+ export declare function buildCursorPage<T extends Record<string, unknown>>(rows: T[], resolved: ResolvedCursor): CursorPage<T>;
92
+ //# sourceMappingURL=cursor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor.d.ts","sourceRoot":"","sources":["../../src/cursor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,EAAE,CAAC;AAErC;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAOzD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAahE;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,QAAQ,EAAE,CAK7E;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,QAAQ,EAAE,CAK5D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,MAAM,EAAE,QAAQ,EAAE,GACjB,YAAY,CAUd;AAED,0FAA0F;AAC1F,MAAM,WAAW,cAAc;IAC7B,mFAAmF;IACnF,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnB,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,QAAQ,EAAE,OAAO,CAAC;IAClB,qGAAqG;IACrG,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,IAAI,EAAE,CAAC,EAAE,EACT,QAAQ,EAAE,cAAc,GACvB,UAAU,CAAC,CAAC,CAAC,CAwBf"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Encodes keyset cursor values into a compact, URL-safe opaque string
3
+ * (base64url of a JSON array). The shape is intentionally opaque to clients —
4
+ * only this module reads it back.
5
+ *
6
+ * `Date` values are encoded as `{ $d: <iso> }` so they round-trip to `Date`
7
+ * instances on decode (plain JSON would yield a string and break date keyset
8
+ * comparisons).
9
+ */
10
+ export function encodeCursor(values) {
11
+ // Pre-map Date values to a tagged form. We cannot detect dates in the
12
+ // JSON.stringify replacer because Date.toJSON() has already converted them to
13
+ // strings by the time the replacer sees the value.
14
+ const tagged = values.map((v) => (v instanceof Date ? { $d: v.toISOString() } : v));
15
+ const json = JSON.stringify(tagged);
16
+ return Buffer.from(json, 'utf8').toString('base64url');
17
+ }
18
+ /**
19
+ * Decodes an opaque cursor string back into its keyset values. Returns `null`
20
+ * when the cursor is malformed (bad base64, bad JSON, or not an array) so the
21
+ * caller can ignore an invalid cursor instead of crashing.
22
+ */
23
+ export function decodeCursor(cursor) {
24
+ try {
25
+ const json = Buffer.from(cursor, 'base64url').toString('utf8');
26
+ const parsed = JSON.parse(json, (_key, value) => {
27
+ if (value && typeof value === 'object' && typeof value.$d === 'string') {
28
+ return new Date(value.$d);
29
+ }
30
+ return value;
31
+ });
32
+ return Array.isArray(parsed) ? parsed : null;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ /**
39
+ * Builds the keyset {@link SortItem}[] for cursor pagination: the caller's
40
+ * effective sorts, with a stable primary-key tiebreaker appended if it is not
41
+ * already present. The tiebreaker inherits the direction of the last sort column
42
+ * so the overall ordering stays monotonic (important for a correct
43
+ * `(cols, pk) > (...)` comparison).
44
+ */
45
+ export function buildKeyset(sorts, primaryKey) {
46
+ const hasPk = sorts.some((s) => s.field === primaryKey);
47
+ if (hasPk)
48
+ return sorts;
49
+ const lastDirection = sorts[sorts.length - 1]?.direction ?? 'asc';
50
+ return [...sorts, { field: primaryKey, direction: lastDirection }];
51
+ }
52
+ /** Flips every keyset column's direction (for backward cursor paging). */
53
+ export function reverseKeyset(keyset) {
54
+ return keyset.map((s) => ({
55
+ field: s.field,
56
+ direction: s.direction === 'asc' ? 'desc' : 'asc',
57
+ }));
58
+ }
59
+ /**
60
+ * Extracts the keyset values from a fetched row, in keyset column order.
61
+ * Supports dotted relation paths (e.g. `author.name`) by walking the object.
62
+ */
63
+ export function extractCursorValues(row, keyset) {
64
+ return keyset.map((s) => {
65
+ if (!s.field.includes('.'))
66
+ return row[s.field];
67
+ let current = row;
68
+ for (const segment of s.field.split('.')) {
69
+ if (current == null || typeof current !== 'object')
70
+ return undefined;
71
+ current = current[segment];
72
+ }
73
+ return current;
74
+ });
75
+ }
76
+ /**
77
+ * Assembles a {@link CursorPage} from the rows fetched for a keyset query.
78
+ *
79
+ * The query is expected to have been built by {@link applyCursor}, which fetches
80
+ * one extra row (`limit = size + 1`) so we can detect a further page. For
81
+ * backward paging the rows come back reversed and are flipped here to restore
82
+ * the caller's requested order. Boundary cursors are computed from the base
83
+ * keyset so they round-trip regardless of paging direction.
84
+ */
85
+ export function buildCursorPage(rows, resolved) {
86
+ const { keyset, size, backward, hasCursor } = resolved;
87
+ const hasExtra = rows.length > size;
88
+ let pageRows = hasExtra ? rows.slice(0, size) : rows;
89
+ if (backward)
90
+ pageRows = pageRows.slice().reverse();
91
+ const firstRow = pageRows[0];
92
+ const lastRow = pageRows[pageRows.length - 1];
93
+ const startCursor = firstRow ? encodeCursor(extractCursorValues(firstRow, keyset)) : null;
94
+ const endCursor = lastRow ? encodeCursor(extractCursorValues(lastRow, keyset)) : null;
95
+ // With a cursor present, the opposite-direction page is known to exist; the
96
+ // same-direction page exists iff we saw the extra row.
97
+ const hasNext = backward ? hasCursor : hasExtra;
98
+ const hasPrev = backward ? hasExtra : hasCursor;
99
+ return {
100
+ items: pageRows,
101
+ nextCursor: hasNext ? endCursor : null,
102
+ prevCursor: hasPrev ? startCursor : null,
103
+ hasNext,
104
+ hasPrev,
105
+ };
106
+ }
107
+ //# sourceMappingURL=cursor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/cursor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import type { FilterRequestContext } from './apply_from_request.js';
2
+ import type { BaseFilter } from './base_filter.js';
3
+ import type { FilterInput } from './types.js';
4
+ /**
5
+ * A filter class over a custom backend, as the container hands it back: constructible, extending
6
+ * {@link BaseFilter} — the model form's {@link FilterClass} for drafts.
7
+ */
8
+ export type CustomFilterClass<D> = new (...args: any[]) => BaseFilter<D>;
9
+ /** Options for {@link applyCustomFilter}. */
10
+ export interface ApplyCustomFilterOptions {
11
+ /**
12
+ * Pre-parsed input to use instead of reading `ctx.request.qs()`. Useful for non-HTTP callers
13
+ * and tests; when omitted the query string is parsed via {@link parseFilterRequest}.
14
+ */
15
+ input?: FilterInput;
16
+ }
17
+ /**
18
+ * Run a custom filter class against a caller-created draft from a request context: bind the
19
+ * per-request state onto the instance, run `setup()`, then hand each filter to its owning method
20
+ * as `(value, operator, field)`.
21
+ *
22
+ * The custom-backend counterpart of
23
+ * {@link import('./apply_from_request.js').applyFilterFromRequest}: that helper ends in SQL (a
24
+ * `QueryBuilderLike`), this one ends in whatever the draft accumulates — a `RunQuery`, an engine
25
+ * client query, an in-memory predicate. The wire format, the parsing, the method dispatch and the
26
+ * error shapes are shared, so a console can move between a Lucid listing and a custom-backend one
27
+ * without relearning the spelling.
28
+ *
29
+ * ```ts
30
+ * const draft = new RunQueryDraft();
31
+ * await applyCustomFilter(draft, RunFilter, ctx);
32
+ * const runs = await engine.listRuns(draft.query);
33
+ * ```
34
+ */
35
+ export declare function applyCustomFilter<D>(draft: D, cls: CustomFilterClass<D>, ctx: FilterRequestContext | undefined, options?: ApplyCustomFilterOptions): Promise<void>;
36
+ //# sourceMappingURL=custom_filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom_filter.d.ts","sourceRoot":"","sources":["../../src/custom_filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAO9C;;;GAGG;AAEH,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAAI,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC;AAEzE,6CAA6C;AAC7C,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AA+GD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,KAAK,EAAE,CAAC,EACR,GAAG,EAAE,iBAAiB,CAAC,CAAC,CAAC,EACzB,GAAG,EAAE,oBAAoB,GAAG,SAAS,EACrC,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,IAAI,CAAC,CAyBf"}
@@ -0,0 +1,127 @@
1
+ import { methodForKey } from './filter_class.js';
2
+ import { FILTER_OPERATORS } from './operators.js';
3
+ import { parseFilterRequest } from './parse_request.js';
4
+ import { InvalidColumnFilterError, MAX_FILTER_DEPTH, normalizeOperator, } from './validate-column-filter.js';
5
+ /**
6
+ * Resolve a custom filter class through the request's IoC container when there is one — the same
7
+ * resolver a controller is constructed with, so `@inject()` on the filter behaves identically —
8
+ * and fall back to plain construction outside an AdonisJS request (a test, a script).
9
+ */
10
+ async function resolveCustomFilter(cls, ctx) {
11
+ const resolver = ctx?.containerResolver;
12
+ if (resolver && typeof resolver.make === 'function') {
13
+ return (await resolver.make(cls));
14
+ }
15
+ return new cls();
16
+ }
17
+ /**
18
+ * The method owning `field`: an exact `@filterFor`/own-method match first, else the head segment
19
+ * (`attr.tier` reaches `attr`) so one method can own a whole dynamic subtree. The full field still
20
+ * rides along as the method's third argument, so the method always knows which key it was reached
21
+ * through.
22
+ *
23
+ * The cast is sound: method dispatch only walks the prototype chain and reads static knobs, both
24
+ * of which a custom filter class carries exactly like a model filter class (the walk simply ends
25
+ * at `Object.prototype` instead of `BaseModelFilter.prototype`, with the same result — and at
26
+ * `BaseFilter.prototype` for the shared members, which are never dispatchable).
27
+ */
28
+ function resolveMethod(cls, field) {
29
+ const asModelFilter = cls;
30
+ const exact = methodForKey(asModelFilter, field);
31
+ if (exact !== undefined)
32
+ return exact;
33
+ const dot = field.indexOf('.');
34
+ if (dot > 0)
35
+ return methodForKey(asModelFilter, field.slice(0, dot));
36
+ return undefined;
37
+ }
38
+ /**
39
+ * Collect the calls a parsed input dispatches to: every structured filter to its owning method
40
+ * (`AND` groups recurse; `OR` is rejected — a draft is an ANDed predicate bag and cannot express
41
+ * cross-field OR), then every bare top-level key the wire format does not own (`?tag=etl`, the
42
+ * legacy spelling) to its same-named method. Unknown BARE keys are ignored — they are how old
43
+ * callers send pagination and other endpoint mechanics — while an unknown STRUCTURED field is
44
+ * rejected outright: the new spelling fails loudly on typos instead of silently widening.
45
+ */
46
+ function collectCalls(cls, raw, parsed) {
47
+ const calls = [];
48
+ const visit = (filter, depth) => {
49
+ if (depth > MAX_FILTER_DEPTH) {
50
+ throw new InvalidColumnFilterError(`Filter nesting exceeds maximum depth (${MAX_FILTER_DEPTH}).`);
51
+ }
52
+ const isGroupNode = (filter.AND !== undefined || filter.OR !== undefined) &&
53
+ (filter.field === undefined || filter.field === '');
54
+ if (isGroupNode) {
55
+ if (filter.OR !== undefined) {
56
+ throw new InvalidColumnFilterError('This filter does not support OR groups — a draft is an ANDed predicate bag.');
57
+ }
58
+ for (const sub of filter.AND ?? [])
59
+ visit(sub, depth + 1);
60
+ return;
61
+ }
62
+ if (typeof filter.field !== 'string' || filter.field === '') {
63
+ throw new InvalidColumnFilterError('Column filter needs a field.');
64
+ }
65
+ // SQL-symbol aliases (`=`, `!=`, …) normalize to canonical operators first, so methods only
66
+ // ever see the canonical spelling — the same normalization the SQL path applies.
67
+ const operator = normalizeOperator(String(filter.operator));
68
+ if (!FILTER_OPERATORS.includes(operator)) {
69
+ throw new InvalidColumnFilterError(`Unknown filter operator: "${String(filter.operator)}".`);
70
+ }
71
+ const method = resolveMethod(cls, filter.field);
72
+ if (method === undefined) {
73
+ throw new InvalidColumnFilterError(`Unknown filter field: "${filter.field}".`);
74
+ }
75
+ calls.push({ method, value: filter.value, operator, field: filter.field });
76
+ };
77
+ for (const filter of parsed.filters ?? [])
78
+ visit(filter, 0);
79
+ // A bare top-level key (`?tag=etl`) reaches a method of the same name too — the legacy spelling
80
+ // — as long as the wire format does not own the key and no structured filter already claimed it.
81
+ // The reserved gate inside `methodForKey` is what keeps endpoint mechanics (`limit`, `sort`, …)
82
+ // from ever reaching a method here.
83
+ for (const [key, value] of Object.entries(raw)) {
84
+ const method = resolveMethod(cls, key);
85
+ if (method !== undefined && !calls.some((call) => call.method === method)) {
86
+ calls.push({ method, value, operator: 'equals', field: key });
87
+ }
88
+ }
89
+ return calls;
90
+ }
91
+ /**
92
+ * Run a custom filter class against a caller-created draft from a request context: bind the
93
+ * per-request state onto the instance, run `setup()`, then hand each filter to its owning method
94
+ * as `(value, operator, field)`.
95
+ *
96
+ * The custom-backend counterpart of
97
+ * {@link import('./apply_from_request.js').applyFilterFromRequest}: that helper ends in SQL (a
98
+ * `QueryBuilderLike`), this one ends in whatever the draft accumulates — a `RunQuery`, an engine
99
+ * client query, an in-memory predicate. The wire format, the parsing, the method dispatch and the
100
+ * error shapes are shared, so a console can move between a Lucid listing and a custom-backend one
101
+ * without relearning the spelling.
102
+ *
103
+ * ```ts
104
+ * const draft = new RunQueryDraft();
105
+ * await applyCustomFilter(draft, RunFilter, ctx);
106
+ * const runs = await engine.listRuns(draft.query);
107
+ * ```
108
+ */
109
+ export async function applyCustomFilter(draft, cls, ctx, options = {}) {
110
+ const raw = ctx?.request?.qs?.() ?? {};
111
+ const parsed = options.input ?? parseFilterRequest(raw);
112
+ const instance = await resolveCustomFilter(cls, ctx);
113
+ Object.assign(instance, {
114
+ $query: draft,
115
+ $input: raw,
116
+ $parsed: parsed,
117
+ $ctx: ctx,
118
+ });
119
+ await instance.setup?.();
120
+ for (const { method, value, operator, field } of collectCalls(cls, raw, parsed)) {
121
+ const fn = instance[method];
122
+ if (typeof fn === 'function') {
123
+ await fn.call(instance, value, operator, field);
124
+ }
125
+ }
126
+ }
127
+ //# sourceMappingURL=custom_filter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom_filter.js","sourceRoot":"","sources":["../../src/custom_filter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import type { FilterFieldKind } from './types.js';
2
+ /**
3
+ * What the decorators record about a class, per declaration site.
4
+ *
5
+ * Two storage channels back this, because two decorator flavours exist in the wild:
6
+ *
7
+ * - **legacy** (`experimentalDecorators`, what AdonisJS uses today — Lucid's `@column` requires
8
+ * it) hands the decorator the *prototype*, so the record is kept in a `WeakMap` keyed by it;
9
+ * - **standard** (TC39 stage 3, TypeScript 5's default when the flag is off) hands the decorator
10
+ * a context object carrying a per-class `metadata` bag, so the record is kept in there.
11
+ *
12
+ * Both are read back the same way, so a class works under either flavour — and inheritance falls
13
+ * out of the walk: prototype chain for the first, the metadata object's own prototype chain
14
+ * (which the standard emit links to the base class's) for the second.
15
+ */
16
+ export interface FilterDecoratorRecord {
17
+ /** Method name → the request keys `@filterFor` bound it to. */
18
+ filterFor?: Record<string, string[]>;
19
+ /** Column name → the kind `@filterable` declared, or `null` when it declared none. */
20
+ filterable?: Record<string, FilterFieldKind | null>;
21
+ /** Columns marked `@sortable`. */
22
+ sortable?: string[];
23
+ /** Columns marked `@searchable`. */
24
+ searchable?: string[];
25
+ }
26
+ /** The record a legacy decorator writes into, created on first use. */
27
+ export declare function legacyRecord(prototype: object): FilterDecoratorRecord;
28
+ /**
29
+ * The record a standard decorator writes into. The bag a subclass receives *inherits* from its
30
+ * base's, so the record is defined as an **own** property — writing through the inherited one
31
+ * would leak a subclass's bindings back onto the parent.
32
+ */
33
+ export declare function standardRecord(metadata: object): FilterDecoratorRecord;
34
+ /**
35
+ * The merged declaration for a class — its own decorators layered over everything it inherits.
36
+ *
37
+ * Not memoized: decorators run at class-definition time, but a model's columns and a filter's
38
+ * methods are read once per compiled spec, which is itself cached by the caller.
39
+ */
40
+ export declare function readDecorators(cls: object): FilterDecoratorRecord;
41
+ //# sourceMappingURL=decorator_metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorator_metadata.d.ts","sourceRoot":"","sources":["../../src/decorator_metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,qBAAqB;IACpC,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACrC,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAAC,CAAC;IACpD,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAoBD,uEAAuE;AACvE,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,qBAAqB,CAMrE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,qBAAqB,CAUtE;AA6BD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,qBAAqB,CASjE"}
@@ -0,0 +1,83 @@
1
+ /** The key the record is filed under inside a standard decorator's metadata bag. */
2
+ const NAMESPACE = '@adonis-agora/filter';
3
+ /** Legacy channel: prototype (or model prototype) → record. */
4
+ const LEGACY = new WeakMap();
5
+ /**
6
+ * The symbols a standard decorator's metadata bag may be stamped on: the real `Symbol.metadata`
7
+ * when the runtime (or a polyfill) has it, and the registered fallback TypeScript's emit uses
8
+ * otherwise. Resolved on every read rather than captured once, so a polyfill installed after this
9
+ * module loads is still seen.
10
+ */
11
+ function metadataSymbols() {
12
+ const native = Symbol.metadata;
13
+ const fallback = Symbol.for('Symbol.metadata');
14
+ return native && native !== fallback ? [native, fallback] : [fallback];
15
+ }
16
+ /** The record a legacy decorator writes into, created on first use. */
17
+ export function legacyRecord(prototype) {
18
+ const existing = LEGACY.get(prototype);
19
+ if (existing)
20
+ return existing;
21
+ const record = {};
22
+ LEGACY.set(prototype, record);
23
+ return record;
24
+ }
25
+ /**
26
+ * The record a standard decorator writes into. The bag a subclass receives *inherits* from its
27
+ * base's, so the record is defined as an **own** property — writing through the inherited one
28
+ * would leak a subclass's bindings back onto the parent.
29
+ */
30
+ export function standardRecord(metadata) {
31
+ if (!Object.hasOwn(metadata, NAMESPACE)) {
32
+ Object.defineProperty(metadata, NAMESPACE, {
33
+ value: {},
34
+ enumerable: false,
35
+ writable: true,
36
+ configurable: true,
37
+ });
38
+ }
39
+ return metadata[NAMESPACE];
40
+ }
41
+ /** Every record that applies to a class, base first — so a subclass's declaration wins. */
42
+ function recordsFor(cls) {
43
+ const found = [];
44
+ const prototype = cls.prototype;
45
+ let proto = prototype ?? null;
46
+ while (proto !== null && proto !== Object.prototype) {
47
+ const record = LEGACY.get(proto);
48
+ if (record)
49
+ found.push(record);
50
+ proto = Object.getPrototypeOf(proto);
51
+ }
52
+ for (const symbol of metadataSymbols()) {
53
+ let bag = cls[symbol] ?? null;
54
+ while (bag !== null) {
55
+ if (Object.hasOwn(bag, NAMESPACE)) {
56
+ found.push(bag[NAMESPACE]);
57
+ }
58
+ bag = Object.getPrototypeOf(bag);
59
+ }
60
+ }
61
+ return found.reverse();
62
+ }
63
+ /**
64
+ * The merged declaration for a class — its own decorators layered over everything it inherits.
65
+ *
66
+ * Not memoized: decorators run at class-definition time, but a model's columns and a filter's
67
+ * methods are read once per compiled spec, which is itself cached by the caller.
68
+ */
69
+ export function readDecorators(cls) {
70
+ const merged = {};
71
+ for (const record of recordsFor(cls)) {
72
+ if (record.filterFor)
73
+ merged.filterFor = { ...merged.filterFor, ...record.filterFor };
74
+ if (record.filterable)
75
+ merged.filterable = { ...merged.filterable, ...record.filterable };
76
+ if (record.sortable)
77
+ merged.sortable = [...(merged.sortable ?? []), ...record.sortable];
78
+ if (record.searchable)
79
+ merged.searchable = [...(merged.searchable ?? []), ...record.searchable];
80
+ }
81
+ return merged;
82
+ }
83
+ //# sourceMappingURL=decorator_metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorator_metadata.js","sourceRoot":"","sources":["../../src/decorator_metadata.ts"],"names":[],"mappings":"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"}