@opensaas/stack-core 0.29.0 → 0.31.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 (133) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +443 -0
  3. package/CLAUDE.md +4 -4
  4. package/dist/access/access-filter.d.ts.map +1 -1
  5. package/dist/access/access-filter.js +14 -1
  6. package/dist/access/access-filter.js.map +1 -1
  7. package/dist/access/access-filter.test.js +50 -0
  8. package/dist/access/access-filter.test.js.map +1 -1
  9. package/dist/access/field-access.js +6 -6
  10. package/dist/access/field-access.js.map +1 -1
  11. package/dist/access/multi-column-read-write.test.js +46 -12
  12. package/dist/access/multi-column-read-write.test.js.map +1 -1
  13. package/dist/access/relationship-count.d.ts +60 -0
  14. package/dist/access/relationship-count.d.ts.map +1 -0
  15. package/dist/access/relationship-count.js +233 -0
  16. package/dist/access/relationship-count.js.map +1 -0
  17. package/dist/access/relationship-count.test.d.ts +2 -0
  18. package/dist/access/relationship-count.test.d.ts.map +1 -0
  19. package/dist/access/relationship-count.test.js +185 -0
  20. package/dist/access/relationship-count.test.js.map +1 -0
  21. package/dist/access/relationship-label-filter.d.ts +45 -0
  22. package/dist/access/relationship-label-filter.d.ts.map +1 -0
  23. package/dist/access/relationship-label-filter.js +93 -0
  24. package/dist/access/relationship-label-filter.js.map +1 -0
  25. package/dist/access/relationship-label-filter.test.d.ts +2 -0
  26. package/dist/access/relationship-label-filter.test.d.ts.map +1 -0
  27. package/dist/access/relationship-label-filter.test.js +126 -0
  28. package/dist/access/relationship-label-filter.test.js.map +1 -0
  29. package/dist/config/index.d.ts +1 -1
  30. package/dist/config/index.d.ts.map +1 -1
  31. package/dist/config/nav-count.d.ts +34 -0
  32. package/dist/config/nav-count.d.ts.map +1 -0
  33. package/dist/config/nav-count.js +71 -0
  34. package/dist/config/nav-count.js.map +1 -0
  35. package/dist/config/types.d.ts +305 -4
  36. package/dist/config/types.d.ts.map +1 -1
  37. package/dist/context/hook-pipeline.d.ts.map +1 -1
  38. package/dist/context/hook-pipeline.js +11 -1
  39. package/dist/context/hook-pipeline.js.map +1 -1
  40. package/dist/context/index.d.ts +28 -0
  41. package/dist/context/index.d.ts.map +1 -1
  42. package/dist/context/index.js +206 -1
  43. package/dist/context/index.js.map +1 -1
  44. package/dist/context/nested-operations.d.ts.map +1 -1
  45. package/dist/context/nested-operations.js +7 -1
  46. package/dist/context/nested-operations.js.map +1 -1
  47. package/dist/extend.d.ts +1 -0
  48. package/dist/extend.d.ts.map +1 -1
  49. package/dist/fields/index.d.ts +1 -1
  50. package/dist/fields/index.d.ts.map +1 -1
  51. package/dist/fields/index.js +166 -0
  52. package/dist/fields/index.js.map +1 -1
  53. package/dist/filter/collect.d.ts +32 -0
  54. package/dist/filter/collect.d.ts.map +1 -0
  55. package/dist/filter/collect.js +60 -0
  56. package/dist/filter/collect.js.map +1 -0
  57. package/dist/filter/filter.test.d.ts +2 -0
  58. package/dist/filter/filter.test.d.ts.map +1 -0
  59. package/dist/filter/filter.test.js +348 -0
  60. package/dist/filter/filter.test.js.map +1 -0
  61. package/dist/filter/index.d.ts +7 -0
  62. package/dist/filter/index.d.ts.map +1 -0
  63. package/dist/filter/index.js +16 -0
  64. package/dist/filter/index.js.map +1 -0
  65. package/dist/filter/map.d.ts +18 -0
  66. package/dist/filter/map.d.ts.map +1 -0
  67. package/dist/filter/map.js +60 -0
  68. package/dist/filter/map.js.map +1 -0
  69. package/dist/filter/parse.d.ts +33 -0
  70. package/dist/filter/parse.d.ts.map +1 -0
  71. package/dist/filter/parse.js +103 -0
  72. package/dist/filter/parse.js.map +1 -0
  73. package/dist/filter/serialize.d.ts +25 -0
  74. package/dist/filter/serialize.d.ts.map +1 -0
  75. package/dist/filter/serialize.js +68 -0
  76. package/dist/filter/serialize.js.map +1 -0
  77. package/dist/filter/serialize.test.d.ts +2 -0
  78. package/dist/filter/serialize.test.d.ts.map +1 -0
  79. package/dist/filter/serialize.test.js +83 -0
  80. package/dist/filter/serialize.test.js.map +1 -0
  81. package/dist/filter/types.d.ts +122 -0
  82. package/dist/filter/types.d.ts.map +1 -0
  83. package/dist/filter/types.js +24 -0
  84. package/dist/filter/types.js.map +1 -0
  85. package/dist/hooks/index.d.ts +30 -0
  86. package/dist/hooks/index.d.ts.map +1 -1
  87. package/dist/hooks/index.js +79 -59
  88. package/dist/hooks/index.js.map +1 -1
  89. package/dist/index.d.ts +6 -1
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +19 -0
  92. package/dist/index.js.map +1 -1
  93. package/dist/internal.d.ts +1 -0
  94. package/dist/internal.d.ts.map +1 -1
  95. package/dist/internal.js +4 -0
  96. package/dist/internal.js.map +1 -1
  97. package/dist/mcp/handler.d.ts +11 -3
  98. package/dist/mcp/handler.d.ts.map +1 -1
  99. package/dist/mcp/handler.js +96 -20
  100. package/dist/mcp/handler.js.map +1 -1
  101. package/package.json +1 -1
  102. package/src/access/access-filter.test.ts +79 -0
  103. package/src/access/access-filter.ts +15 -4
  104. package/src/access/field-access.ts +6 -6
  105. package/src/access/multi-column-read-write.test.ts +68 -19
  106. package/src/access/relationship-count.test.ts +269 -0
  107. package/src/access/relationship-count.ts +311 -0
  108. package/src/access/relationship-label-filter.test.ts +177 -0
  109. package/src/access/relationship-label-filter.ts +140 -0
  110. package/src/config/index.ts +6 -0
  111. package/src/config/nav-count.ts +85 -0
  112. package/src/config/types.ts +319 -1
  113. package/src/context/hook-pipeline.ts +19 -0
  114. package/src/context/index.ts +286 -4
  115. package/src/context/nested-operations.ts +22 -0
  116. package/src/extend.ts +9 -0
  117. package/src/fields/index.ts +168 -0
  118. package/src/filter/collect.ts +72 -0
  119. package/src/filter/filter.test.ts +400 -0
  120. package/src/filter/index.ts +25 -0
  121. package/src/filter/map.ts +68 -0
  122. package/src/filter/parse.ts +106 -0
  123. package/src/filter/serialize.test.ts +115 -0
  124. package/src/filter/serialize.ts +75 -0
  125. package/src/filter/types.ts +120 -0
  126. package/src/hooks/index.ts +87 -57
  127. package/src/index.ts +54 -0
  128. package/src/internal.ts +5 -0
  129. package/src/mcp/handler.ts +118 -28
  130. package/tests/context.test.ts +690 -0
  131. package/tests/multi-column-validation-ordering.test.ts +305 -0
  132. package/tests/nav-count.test.ts +189 -0
  133. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,348 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseFilterQuery } from './parse.js';
3
+ import { buildFilterWhere } from './map.js';
4
+ import { buildListFilterWhere, collectFilterSpecs, collectFilterSuggestions } from './collect.js';
5
+ import { list } from '../config/index.js';
6
+ import { text, integer, select, checkbox, timestamp, relationship } from '../fields/index.js';
7
+ // ─────────────────────────────────────────────────────────────
8
+ // parseFilterQuery — the pure query-string → tokens boundary (ADR-0017)
9
+ // ─────────────────────────────────────────────────────────────
10
+ describe('parseFilterQuery', () => {
11
+ it('parses a bare word as a free-text token (field: null)', () => {
12
+ expect(parseFilterQuery('beta')).toEqual([
13
+ { field: null, operator: 'eq', value: 'beta', raw: 'beta' },
14
+ ]);
15
+ });
16
+ it('parses a field:value token with the default eq operator', () => {
17
+ expect(parseFilterQuery('role:Editor')).toEqual([
18
+ { field: 'role', operator: 'eq', value: 'Editor', raw: 'role:Editor' },
19
+ ]);
20
+ });
21
+ it('parses comparison operators on a field token', () => {
22
+ expect(parseFilterQuery('orders:>5')).toEqual([
23
+ { field: 'orders', operator: 'gt', value: '5', raw: 'orders:>5' },
24
+ ]);
25
+ expect(parseFilterQuery('orders:>=5')[0].operator).toBe('gte');
26
+ expect(parseFilterQuery('orders:<5')[0].operator).toBe('lt');
27
+ expect(parseFilterQuery('orders:<=5')[0].operator).toBe('lte');
28
+ });
29
+ it('keeps spaces inside a quoted value', () => {
30
+ expect(parseFilterQuery('name:"Ada Lovelace"')).toEqual([
31
+ { field: 'name', operator: 'eq', value: 'Ada Lovelace', raw: 'name:"Ada Lovelace"' },
32
+ ]);
33
+ });
34
+ it('parses a bare quoted phrase as a single free-text token', () => {
35
+ expect(parseFilterQuery('"multi word"')).toEqual([
36
+ { field: null, operator: 'eq', value: 'multi word', raw: '"multi word"' },
37
+ ]);
38
+ });
39
+ it('parses implicit-AND: multiple whitespace-separated tokens', () => {
40
+ const tokens = parseFilterQuery('role:Editor status:Active orders:>5 name:"Ada Lovelace" beta');
41
+ expect(tokens.map((t) => ({ field: t.field, operator: t.operator, value: t.value }))).toEqual([
42
+ { field: 'role', operator: 'eq', value: 'Editor' },
43
+ { field: 'status', operator: 'eq', value: 'Active' },
44
+ { field: 'orders', operator: 'gt', value: '5' },
45
+ { field: 'name', operator: 'eq', value: 'Ada Lovelace' },
46
+ { field: null, operator: 'eq', value: 'beta' },
47
+ ]);
48
+ });
49
+ it('collapses surrounding / repeated whitespace', () => {
50
+ expect(parseFilterQuery(' a b ').map((t) => t.value)).toEqual(['a', 'b']);
51
+ });
52
+ it('returns no tokens for an empty query', () => {
53
+ expect(parseFilterQuery('')).toEqual([]);
54
+ expect(parseFilterQuery(' ')).toEqual([]);
55
+ });
56
+ it('never throws on odd input — a leading comparison on a bare word stays free text', () => {
57
+ expect(parseFilterQuery('>5')).toEqual([
58
+ { field: null, operator: 'eq', value: '>5', raw: '>5' },
59
+ ]);
60
+ });
61
+ it('treats a URL scheme (`http://…`) as whole free text, not a field prefix', () => {
62
+ // `http:` must NOT be parsed as a `field:` prefix — the `//` after the
63
+ // colon marks a URL scheme, so the entire URL stays a free-text token.
64
+ expect(() => parseFilterQuery('http://x')).not.toThrow();
65
+ expect(parseFilterQuery('http://x')).toEqual([
66
+ { field: null, operator: 'eq', value: 'http://x', raw: 'http://x' },
67
+ ]);
68
+ expect(parseFilterQuery('https://example.com/path?q=1')).toEqual([
69
+ {
70
+ field: null,
71
+ operator: 'eq',
72
+ value: 'https://example.com/path?q=1',
73
+ raw: 'https://example.com/path?q=1',
74
+ },
75
+ ]);
76
+ });
77
+ });
78
+ // ─────────────────────────────────────────────────────────────
79
+ // buildFilterWhere — tokens + specs → Prisma where (the mapping seam)
80
+ // ─────────────────────────────────────────────────────────────
81
+ // Small hand-rolled specs so the mapping is tested independently of the field
82
+ // builders.
83
+ const nameSpec = {
84
+ operators: ['eq'],
85
+ freeText: true,
86
+ toCondition: (op, value) => (op === 'eq' ? { name: { contains: value } } : null),
87
+ suggestions: { valueSource: { kind: 'none' } },
88
+ };
89
+ const ordersSpec = {
90
+ operators: ['eq', 'gt', 'gte', 'lt', 'lte'],
91
+ toCondition: (op, value) => {
92
+ if (!/^-?\d+$/.test(value))
93
+ return null;
94
+ return { orders: { [op === 'eq' ? 'equals' : op]: Number(value) } };
95
+ },
96
+ suggestions: { valueSource: { kind: 'none' } },
97
+ };
98
+ const statusSpec = {
99
+ operators: ['eq'],
100
+ toCondition: (op, value) => op === 'eq' && ['active', 'inactive'].includes(value.toLowerCase())
101
+ ? { status: { equals: value.toLowerCase() } }
102
+ : null,
103
+ suggestions: {
104
+ valueSource: {
105
+ kind: 'enum',
106
+ options: [
107
+ { value: 'active', label: 'Active' },
108
+ { value: 'inactive', label: 'Inactive' },
109
+ ],
110
+ },
111
+ },
112
+ };
113
+ const specs = { name: nameSpec, orders: ordersSpec, status: statusSpec };
114
+ describe('buildFilterWhere', () => {
115
+ it('returns undefined when nothing filters', () => {
116
+ expect(buildFilterWhere([], specs)).toBeUndefined();
117
+ });
118
+ it('maps a single field token to its condition (no AND wrapper)', () => {
119
+ expect(buildFilterWhere(parseFilterQuery('orders:>5'), specs)).toEqual({
120
+ orders: { gt: 5 },
121
+ });
122
+ });
123
+ it('maps eq on a numeric field to `equals`', () => {
124
+ expect(buildFilterWhere(parseFilterQuery('orders:5'), specs)).toEqual({
125
+ orders: { equals: 5 },
126
+ });
127
+ });
128
+ it('ANDs multiple field tokens (implicit AND)', () => {
129
+ expect(buildFilterWhere(parseFilterQuery('status:Active orders:>5'), specs)).toEqual({
130
+ AND: [{ status: { equals: 'active' } }, { orders: { gt: 5 } }],
131
+ });
132
+ });
133
+ it('maps a bare word to an OR across free-text fields', () => {
134
+ // Only `name` is free-text here.
135
+ expect(buildFilterWhere(parseFilterQuery('beta'), specs)).toEqual({
136
+ name: { contains: 'beta' },
137
+ });
138
+ });
139
+ it('ANDs multiple bare words (each word must match)', () => {
140
+ const multiFreeText = {
141
+ name: nameSpec,
142
+ title: {
143
+ ...nameSpec,
144
+ toCondition: (op, v) => op === 'eq' ? { title: { contains: v } } : null,
145
+ },
146
+ };
147
+ expect(buildFilterWhere(parseFilterQuery('beta gamma'), multiFreeText)).toEqual({
148
+ AND: [
149
+ { OR: [{ name: { contains: 'beta' } }, { title: { contains: 'beta' } }] },
150
+ { OR: [{ name: { contains: 'gamma' } }, { title: { contains: 'gamma' } }] },
151
+ ],
152
+ });
153
+ });
154
+ // ─── Graceful degradation (ADR-0017): unknown syntax → free text, never error
155
+ it('degrades an unknown field to free text', () => {
156
+ // `role` has no spec → search "Editor" across free-text fields.
157
+ expect(buildFilterWhere(parseFilterQuery('role:Editor'), specs)).toEqual({
158
+ name: { contains: 'Editor' },
159
+ });
160
+ });
161
+ it('degrades an unsupported operator to free text', () => {
162
+ // `status` supports only eq → `status:>x` degrades, searching "x".
163
+ expect(buildFilterWhere(parseFilterQuery('status:>x'), specs)).toEqual({
164
+ name: { contains: 'x' },
165
+ });
166
+ });
167
+ it('degrades an unmappable value (non-numeric on a numeric field) to free text', () => {
168
+ expect(buildFilterWhere(parseFilterQuery('orders:abc'), specs)).toEqual({
169
+ name: { contains: 'abc' },
170
+ });
171
+ });
172
+ it('degrades an unknown enum value to free text', () => {
173
+ expect(buildFilterWhere(parseFilterQuery('status:pending'), specs)).toEqual({
174
+ name: { contains: 'pending' },
175
+ });
176
+ });
177
+ it('drops a degraded token entirely when there are no free-text fields', () => {
178
+ const noFreeText = { orders: ordersSpec };
179
+ expect(buildFilterWhere(parseFilterQuery('role:Editor'), noFreeText)).toBeUndefined();
180
+ });
181
+ it('searches a pasted URL as whole free text (scheme not dropped)', () => {
182
+ // `http://x` must be searched in full, not mangled to `//x` by a spurious
183
+ // `http:` field prefix.
184
+ expect(buildFilterWhere(parseFilterQuery('http://x'), specs)).toEqual({
185
+ name: { contains: 'http://x' },
186
+ });
187
+ expect(buildFilterWhere(parseFilterQuery('https://example.com'), specs)).toEqual({
188
+ name: { contains: 'https://example.com' },
189
+ });
190
+ });
191
+ });
192
+ // ─────────────────────────────────────────────────────────────
193
+ // Field-builder Filter specs + collect helpers (config-aware layer)
194
+ // ─────────────────────────────────────────────────────────────
195
+ function makeConfig() {
196
+ return {
197
+ db: { provider: 'sqlite', prismaClientConstructor: () => null },
198
+ lists: {
199
+ User: list({ fields: { name: text() } }),
200
+ Post: list({
201
+ fields: {
202
+ title: text(),
203
+ views: integer(),
204
+ status: select({
205
+ options: [
206
+ { label: 'Draft', value: 'draft' },
207
+ { label: 'Published', value: 'published' },
208
+ ],
209
+ }),
210
+ featured: checkbox(),
211
+ publishedAt: timestamp(),
212
+ author: relationship({ ref: 'User.posts' }),
213
+ },
214
+ }),
215
+ },
216
+ };
217
+ }
218
+ describe('core field Filter specs', () => {
219
+ const config = makeConfig();
220
+ const postConfig = config.lists.Post;
221
+ it('collectFilterSpecs resolves a spec for every filterable core field', () => {
222
+ const collected = collectFilterSpecs(postConfig, 'Post', config);
223
+ expect(Object.keys(collected).sort()).toEqual(['author', 'featured', 'publishedAt', 'status', 'title', 'views'].sort());
224
+ });
225
+ it('text is free-text and maps to contains', () => {
226
+ const spec = postConfig.fields.title.getFilterSpec('title', 'Post', config);
227
+ expect(spec.freeText).toBe(true);
228
+ expect(spec.toCondition('eq', 'hello')).toEqual({ title: { contains: 'hello' } });
229
+ });
230
+ it('integer supports comparisons and rejects non-integers', () => {
231
+ const spec = postConfig.fields.views.getFilterSpec('views', 'Post', config);
232
+ expect(spec.toCondition('gt', '5')).toEqual({ views: { gt: 5 } });
233
+ expect(spec.toCondition('eq', '5')).toEqual({ views: { equals: 5 } });
234
+ expect(spec.toCondition('eq', 'abc')).toBeNull();
235
+ });
236
+ it('select matches by value or label and resolves to the stored value', () => {
237
+ const spec = postConfig.fields.status.getFilterSpec('status', 'Post', config);
238
+ expect(spec.toCondition('eq', 'Published')).toEqual({ status: { equals: 'published' } });
239
+ expect(spec.toCondition('eq', 'published')).toEqual({ status: { equals: 'published' } });
240
+ expect(spec.toCondition('eq', 'archived')).toBeNull();
241
+ expect(spec.suggestions.valueSource).toEqual({
242
+ kind: 'enum',
243
+ options: [
244
+ { value: 'draft', label: 'Draft' },
245
+ { value: 'published', label: 'Published' },
246
+ ],
247
+ });
248
+ });
249
+ it('checkbox maps true/false and rejects anything else', () => {
250
+ const spec = postConfig.fields.featured.getFilterSpec('featured', 'Post', config);
251
+ expect(spec.toCondition('eq', 'true')).toEqual({ featured: { equals: true } });
252
+ expect(spec.toCondition('eq', 'False')).toEqual({ featured: { equals: false } });
253
+ expect(spec.toCondition('eq', 'yes')).toBeNull();
254
+ });
255
+ it('timestamp compares parsed dates and rejects unparseable values', () => {
256
+ const spec = postConfig.fields.publishedAt.getFilterSpec('publishedAt', 'Post', config);
257
+ const condition = spec.toCondition('gt', '2024-01-01');
258
+ expect(condition.publishedAt.gt).toBeInstanceOf(Date);
259
+ expect(spec.toCondition('gt', 'not-a-date')).toBeNull();
260
+ });
261
+ it('relationship maps to a nested label filter (is for to-one)', () => {
262
+ const spec = postConfig.fields.author.getFilterSpec('author', 'Post', config);
263
+ // User's label field falls back to `name`.
264
+ expect(spec.toCondition('eq', 'Ada')).toEqual({ author: { is: { name: { contains: 'Ada' } } } });
265
+ expect(spec.suggestions.valueSource).toEqual({
266
+ kind: 'relationship',
267
+ listKey: 'User',
268
+ many: false,
269
+ });
270
+ });
271
+ it('does not produce specs for password/json/virtual (absence degrades gracefully)', () => {
272
+ // A list of non-filterable fields yields no specs at all.
273
+ const specsForUser = collectFilterSpecs(config.lists.User, 'User', config);
274
+ // User only has `name` (text) → exactly one spec, proving non-text absence.
275
+ expect(Object.keys(specsForUser)).toEqual(['name']);
276
+ });
277
+ });
278
+ describe('buildListFilterWhere (end-to-end over a real list config)', () => {
279
+ const config = makeConfig();
280
+ it('composes parse + specs into a merged where', () => {
281
+ const where = buildListFilterWhere('status:Published views:>10 author:"Ada" hello', config.lists.Post, 'Post', config);
282
+ expect(where).toEqual({
283
+ AND: [
284
+ { status: { equals: 'published' } },
285
+ { views: { gt: 10 } },
286
+ { author: { is: { name: { contains: 'Ada' } } } },
287
+ // `hello` is free text over the two text fields (title only — status is
288
+ // not free-text) → but title is the only free-text field here.
289
+ { title: { contains: 'hello' } },
290
+ ],
291
+ });
292
+ });
293
+ it('returns undefined for an all-whitespace query', () => {
294
+ expect(buildListFilterWhere(' ', config.lists.Post, 'Post', config)).toBeUndefined();
295
+ });
296
+ });
297
+ describe('to-many relationship Filter spec (count comparisons — issue #732)', () => {
298
+ function countConfig() {
299
+ return {
300
+ db: { provider: 'sqlite', prismaClientConstructor: () => null },
301
+ lists: {
302
+ User: list({
303
+ fields: {
304
+ name: text(),
305
+ posts: relationship({ ref: 'Post.author', many: true }),
306
+ },
307
+ }),
308
+ Post: list({
309
+ fields: { title: text(), author: relationship({ ref: 'User.posts' }) },
310
+ }),
311
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- minimal config for unit test
312
+ },
313
+ };
314
+ }
315
+ it('supports numeric comparisons and emits a count marker (not a label filter)', () => {
316
+ const config = countConfig();
317
+ const spec = config.lists.User.fields.posts.getFilterSpec('posts', 'User', config);
318
+ expect(spec.operators).toEqual(['eq', 'gt', 'gte', 'lt', 'lte']);
319
+ expect(spec.toCondition('gt', '5')).toEqual({
320
+ posts: { _countFilter: { operator: 'gt', value: 5 } },
321
+ });
322
+ expect(spec.toCondition('eq', '0')).toEqual({
323
+ posts: { _countFilter: { operator: 'eq', value: 0 } },
324
+ });
325
+ // A non-integer count degrades to free text (like an integer field).
326
+ expect(spec.toCondition('gt', 'lots')).toBeNull();
327
+ // No enumerated value source — a count is a plain numeric compare.
328
+ expect(spec.suggestions.valueSource).toEqual({ kind: 'none' });
329
+ });
330
+ it('end-to-end: buildListFilterWhere carries a count marker for `posts:>5`', () => {
331
+ const config = countConfig();
332
+ const where = buildListFilterWhere('posts:>5', config.lists.User, 'User', config);
333
+ expect(where).toEqual({ posts: { _countFilter: { operator: 'gt', value: 5 } } });
334
+ });
335
+ });
336
+ describe('collectFilterSuggestions', () => {
337
+ it('returns serializable, function-free suggestion metadata', () => {
338
+ const config = makeConfig();
339
+ const suggestions = collectFilterSuggestions(config.lists.Post, 'Post', config);
340
+ // No functions anywhere → JSON round-trips cleanly.
341
+ expect(JSON.parse(JSON.stringify(suggestions))).toEqual(suggestions);
342
+ const status = suggestions.find((s) => s.field === 'status');
343
+ expect(status.valueSource.kind).toBe('enum');
344
+ const author = suggestions.find((s) => s.field === 'author');
345
+ expect(author.valueSource).toEqual({ kind: 'relationship', listKey: 'User', many: false });
346
+ });
347
+ });
348
+ //# sourceMappingURL=filter.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter.test.js","sourceRoot":"","sources":["../../src/filter/filter.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export { parseFilterQuery } from './parse.js';
2
+ export { serializeFilterQuery } from './serialize.js';
3
+ export { buildFilterWhere } from './map.js';
4
+ export { collectFilterSpecs, buildListFilterWhere, collectFilterSuggestions } from './collect.js';
5
+ export { RELATIONSHIP_COUNT_FILTER_KEY } from './types.js';
6
+ export type { FilterOperator, FilterToken, FilterCondition, FilterSpec, FilterValueSource, FilterFieldSuggestion, RelationshipCountFilterMarker, } from './types.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/filter/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAC7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAC3C,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAA;AACjG,OAAO,EAAE,6BAA6B,EAAE,MAAM,YAAY,CAAA;AAC1D,YAAY,EACV,cAAc,EACd,WAAW,EACX,eAAe,EACf,UAAU,EACV,iBAAiB,EACjB,qBAAqB,EACrB,6BAA6B,GAC9B,MAAM,YAAY,CAAA"}
@@ -0,0 +1,16 @@
1
+ // ───────────────────────────────────────────────────────────────
2
+ // Filter engine (ADR-0017)
3
+ //
4
+ // The pure boundary behind the admin UI's Filter builder:
5
+ // • parseFilterQuery — query string → Filter tokens
6
+ // • buildFilterWhere — tokens + Filter specs → Prisma where fragment
7
+ // plus config-aware helpers that collect each field's Filter spec and compose
8
+ // the two steps for a list. All of this stays free of DB/Prisma imports so the
9
+ // grammar contract (including graceful degradation) is unit-testable.
10
+ // ───────────────────────────────────────────────────────────────
11
+ export { parseFilterQuery } from './parse.js';
12
+ export { serializeFilterQuery } from './serialize.js';
13
+ export { buildFilterWhere } from './map.js';
14
+ export { collectFilterSpecs, buildListFilterWhere, collectFilterSuggestions } from './collect.js';
15
+ export { RELATIONSHIP_COUNT_FILTER_KEY } from './types.js';
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/filter/index.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,2BAA2B;AAC3B,EAAE;AACF,0DAA0D;AAC1D,uDAAuD;AACvD,wEAAwE;AACxE,8EAA8E;AAC9E,+EAA+E;AAC/E,sEAAsE;AACtE,kEAAkE;AAElE,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAC7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAA;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAC3C,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAA;AACjG,OAAO,EAAE,6BAA6B,EAAE,MAAM,YAAY,CAAA"}
@@ -0,0 +1,18 @@
1
+ import type { FilterCondition, FilterSpec, FilterToken } from './types.js';
2
+ /**
3
+ * Map parsed {@link FilterToken}s to a single Prisma `where` fragment, given the
4
+ * fields' {@link FilterSpec}s (ADR-0017).
5
+ *
6
+ * Pure — no DB/Prisma imports. This is the unit-tested half of the filter seam.
7
+ * Tokens combine with implicit AND. A token degrades to free text (never an
8
+ * error) when its field is unknown, has no spec, uses an unsupported operator,
9
+ * has an empty value, or maps to `null`. Bare words (and degraded tokens) are
10
+ * OR-matched across every free-text field, and each such word is ANDed with the
11
+ * rest — so `beta gamma` requires both.
12
+ *
13
+ * @param tokens Parsed tokens from {@link parseFilterQuery}.
14
+ * @param specs Field-name → Filter spec (from {@link collectFilterSpecs}).
15
+ * @returns A Prisma `where` fragment, or `undefined` when nothing filters.
16
+ */
17
+ export declare function buildFilterWhere(tokens: FilterToken[], specs: Record<string, FilterSpec>): FilterCondition | undefined;
18
+ //# sourceMappingURL=map.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map.d.ts","sourceRoot":"","sources":["../../src/filter/map.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE1E;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,WAAW,EAAE,EACrB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAChC,eAAe,GAAG,SAAS,CA+C7B"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Map parsed {@link FilterToken}s to a single Prisma `where` fragment, given the
3
+ * fields' {@link FilterSpec}s (ADR-0017).
4
+ *
5
+ * Pure — no DB/Prisma imports. This is the unit-tested half of the filter seam.
6
+ * Tokens combine with implicit AND. A token degrades to free text (never an
7
+ * error) when its field is unknown, has no spec, uses an unsupported operator,
8
+ * has an empty value, or maps to `null`. Bare words (and degraded tokens) are
9
+ * OR-matched across every free-text field, and each such word is ANDed with the
10
+ * rest — so `beta gamma` requires both.
11
+ *
12
+ * @param tokens Parsed tokens from {@link parseFilterQuery}.
13
+ * @param specs Field-name → Filter spec (from {@link collectFilterSpecs}).
14
+ * @returns A Prisma `where` fragment, or `undefined` when nothing filters.
15
+ */
16
+ export function buildFilterWhere(tokens, specs) {
17
+ const andConditions = [];
18
+ const freeTextWords = [];
19
+ const freeTextFields = Object.keys(specs).filter((field) => specs[field].freeText);
20
+ for (const token of tokens) {
21
+ // Bare free-text word.
22
+ if (token.field === null) {
23
+ if (token.value)
24
+ freeTextWords.push(token.value);
25
+ continue;
26
+ }
27
+ const spec = specs[token.field];
28
+ // Unknown field / no spec / unsupported operator / empty value →
29
+ // degrade to free text (search the value across free-text fields).
30
+ if (!spec || !spec.operators.includes(token.operator) || token.value === '') {
31
+ if (token.value)
32
+ freeTextWords.push(token.value);
33
+ continue;
34
+ }
35
+ const condition = spec.toCondition(token.operator, token.value);
36
+ if (condition === null) {
37
+ // Value couldn't be interpreted for this field → free text.
38
+ freeTextWords.push(token.value);
39
+ continue;
40
+ }
41
+ andConditions.push(condition);
42
+ }
43
+ // Each free-text word: OR across every free-text field's `eq` mapping.
44
+ if (freeTextFields.length > 0) {
45
+ for (const word of freeTextWords) {
46
+ const orConditions = freeTextFields
47
+ .map((field) => specs[field].toCondition('eq', word))
48
+ .filter((condition) => condition !== null);
49
+ if (orConditions.length > 0) {
50
+ andConditions.push(orConditions.length === 1 ? orConditions[0] : { OR: orConditions });
51
+ }
52
+ }
53
+ }
54
+ if (andConditions.length === 0)
55
+ return undefined;
56
+ if (andConditions.length === 1)
57
+ return andConditions[0];
58
+ return { AND: andConditions };
59
+ }
60
+ //# sourceMappingURL=map.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"map.js","sourceRoot":"","sources":["../../src/filter/map.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAAqB,EACrB,KAAiC;IAEjC,MAAM,aAAa,GAAsB,EAAE,CAAA;IAC3C,MAAM,aAAa,GAAa,EAAE,CAAA;IAElC,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,CAAA;IAElF,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,uBAAuB;QACvB,IAAI,KAAK,CAAC,KAAK,KAAK,IAAI,EAAE,CAAC;YACzB,IAAI,KAAK,CAAC,KAAK;gBAAE,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAChD,SAAQ;QACV,CAAC;QAED,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QAE/B,iEAAiE;QACjE,mEAAmE;QACnE,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,KAAK,KAAK,EAAE,EAAE,CAAC;YAC5E,IAAI,KAAK,CAAC,KAAK;gBAAE,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAChD,SAAQ;QACV,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,CAAA;QAC/D,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;YACvB,4DAA4D;YAC5D,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAC/B,SAAQ;QACV,CAAC;QAED,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IAC/B,CAAC;IAED,uEAAuE;IACvE,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9B,KAAK,MAAM,IAAI,IAAI,aAAa,EAAE,CAAC;YACjC,MAAM,YAAY,GAAG,cAAc;iBAChC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;iBACpD,MAAM,CAAC,CAAC,SAAS,EAAgC,EAAE,CAAC,SAAS,KAAK,IAAI,CAAC,CAAA;YAC1E,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC5B,aAAa,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,YAAY,EAAE,CAAC,CAAA;YACxF,CAAC;QACH,CAAC;IACH,CAAC;IAED,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IAChD,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,aAAa,CAAC,CAAC,CAAC,CAAA;IACvD,OAAO,EAAE,GAAG,EAAE,aAAa,EAAE,CAAA;AAC/B,CAAC"}
@@ -0,0 +1,33 @@
1
+ import type { FilterToken } from './types.js';
2
+ /**
3
+ * Parse a filter query string into {@link FilterToken}s (ADR-0017 grammar).
4
+ *
5
+ * Pure and field-agnostic: it recognises the *syntax* (field prefix, comparison
6
+ * operator, quoted value, bare word) but never validates field names or values
7
+ * — that is the mapping step's job, where unknown/unmappable tokens degrade to
8
+ * free text. This parser therefore never throws; malformed input simply parses
9
+ * as bare free-text words.
10
+ *
11
+ * Grammar:
12
+ * - `field:value` → a field token with the `eq` operator.
13
+ * - `field:>value`, `field:>=value`, `field:<value`, `field:<=value` →
14
+ * comparison operators.
15
+ * - `field:"multi word"` / bare `"multi word"` → quoted values keep spaces.
16
+ * - anything else (`beta`, `>5`) → a bare free-text token (`field: null`).
17
+ *
18
+ * A `word:` is *not* treated as a field prefix when the colon is immediately
19
+ * followed by `//` (a URL scheme like `http://x` or `https://…`); the whole
20
+ * token stays free text so a pasted URL is searched verbatim rather than
21
+ * mangled into `{ field: 'http', value: '//x' }`.
22
+ *
23
+ * @example
24
+ * parseFilterQuery('role:Editor orders:>5 name:"Ada Lovelace" beta')
25
+ * // [
26
+ * // { field: 'role', operator: 'eq', value: 'Editor', raw: 'role:Editor' },
27
+ * // { field: 'orders', operator: 'gt', value: '5', raw: 'orders:>5' },
28
+ * // { field: 'name', operator: 'eq', value: 'Ada Lovelace', raw: 'name:"Ada Lovelace"' },
29
+ * // { field: null, operator: 'eq', value: 'beta', raw: 'beta' },
30
+ * // ]
31
+ */
32
+ export declare function parseFilterQuery(query: string): FilterToken[];
33
+ //# sourceMappingURL=parse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/filter/parse.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAkB,WAAW,EAAE,MAAM,YAAY,CAAA;AAY7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,EAAE,CA+D7D"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * A field prefix is an identifier (letter/underscore, then word chars)
3
+ * immediately followed by a colon, e.g. `role:` or `orders:`.
4
+ */
5
+ const FIELD_PREFIX_RE = /^([A-Za-z_][A-Za-z0-9_]*):/;
6
+ function isWhitespace(ch) {
7
+ return ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r';
8
+ }
9
+ /**
10
+ * Parse a filter query string into {@link FilterToken}s (ADR-0017 grammar).
11
+ *
12
+ * Pure and field-agnostic: it recognises the *syntax* (field prefix, comparison
13
+ * operator, quoted value, bare word) but never validates field names or values
14
+ * — that is the mapping step's job, where unknown/unmappable tokens degrade to
15
+ * free text. This parser therefore never throws; malformed input simply parses
16
+ * as bare free-text words.
17
+ *
18
+ * Grammar:
19
+ * - `field:value` → a field token with the `eq` operator.
20
+ * - `field:>value`, `field:>=value`, `field:<value`, `field:<=value` →
21
+ * comparison operators.
22
+ * - `field:"multi word"` / bare `"multi word"` → quoted values keep spaces.
23
+ * - anything else (`beta`, `>5`) → a bare free-text token (`field: null`).
24
+ *
25
+ * A `word:` is *not* treated as a field prefix when the colon is immediately
26
+ * followed by `//` (a URL scheme like `http://x` or `https://…`); the whole
27
+ * token stays free text so a pasted URL is searched verbatim rather than
28
+ * mangled into `{ field: 'http', value: '//x' }`.
29
+ *
30
+ * @example
31
+ * parseFilterQuery('role:Editor orders:>5 name:"Ada Lovelace" beta')
32
+ * // [
33
+ * // { field: 'role', operator: 'eq', value: 'Editor', raw: 'role:Editor' },
34
+ * // { field: 'orders', operator: 'gt', value: '5', raw: 'orders:>5' },
35
+ * // { field: 'name', operator: 'eq', value: 'Ada Lovelace', raw: 'name:"Ada Lovelace"' },
36
+ * // { field: null, operator: 'eq', value: 'beta', raw: 'beta' },
37
+ * // ]
38
+ */
39
+ export function parseFilterQuery(query) {
40
+ const tokens = [];
41
+ if (!query)
42
+ return tokens;
43
+ const n = query.length;
44
+ let i = 0;
45
+ while (i < n) {
46
+ // Skip leading whitespace between tokens.
47
+ while (i < n && isWhitespace(query[i]))
48
+ i++;
49
+ if (i >= n)
50
+ break;
51
+ const start = i;
52
+ // Optional `field:` prefix. A URL scheme (`http://…`) is not a field
53
+ // prefix: when the colon is immediately followed by `//`, keep the whole
54
+ // token as free text so pasted URLs are searched verbatim.
55
+ let field = null;
56
+ const prefixMatch = FIELD_PREFIX_RE.exec(query.slice(i));
57
+ if (prefixMatch && !query.startsWith('//', i + prefixMatch[0].length)) {
58
+ field = prefixMatch[1];
59
+ i += prefixMatch[0].length;
60
+ }
61
+ // Comparison operator prefix — only meaningful once a field is present.
62
+ // A leading `>`/`<` on a bare word is left as part of the free-text value.
63
+ let operator = 'eq';
64
+ if (field) {
65
+ if (query.startsWith('>=', i)) {
66
+ operator = 'gte';
67
+ i += 2;
68
+ }
69
+ else if (query.startsWith('<=', i)) {
70
+ operator = 'lte';
71
+ i += 2;
72
+ }
73
+ else if (query[i] === '>') {
74
+ operator = 'gt';
75
+ i += 1;
76
+ }
77
+ else if (query[i] === '<') {
78
+ operator = 'lt';
79
+ i += 1;
80
+ }
81
+ }
82
+ // Value: quoted (spaces kept) or a run up to the next whitespace.
83
+ let value = '';
84
+ if (query[i] === '"') {
85
+ i++; // opening quote
86
+ while (i < n && query[i] !== '"') {
87
+ value += query[i];
88
+ i++;
89
+ }
90
+ if (i < n)
91
+ i++; // closing quote
92
+ }
93
+ else {
94
+ while (i < n && !isWhitespace(query[i])) {
95
+ value += query[i];
96
+ i++;
97
+ }
98
+ }
99
+ tokens.push({ field, operator, value, raw: query.slice(start, i) });
100
+ }
101
+ return tokens;
102
+ }
103
+ //# sourceMappingURL=parse.js.map