@jskit-ai/agent-docs 0.1.53 → 0.1.54

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.
@@ -948,6 +948,7 @@ In JSKIT CRUD:
948
948
  - the schema defines the API contract
949
949
  - field definitions may also carry storage/lookup/ui metadata
950
950
  - the repository runtime owns computed SQL projections
951
+ - internal JSON REST resources can expose SQL-selected query projections through `createJsonRestResourceScopeOptions(...)`
951
952
 
952
953
  Use these rules:
953
954
 
@@ -1018,7 +1019,31 @@ const repositoryRuntime = createCrudResourceRuntime(resource, {
1018
1019
  });
1019
1020
  ```
1020
1021
 
1021
- Once registered there:
1022
+ For JSON REST-backed generated CRUD packages, keep the output field virtual in the resource and register the SQL projection at the JSON REST resource boundary:
1023
+
1024
+ ```js
1025
+ await addResourceIfMissing(
1026
+ api,
1027
+ "receivals",
1028
+ createJsonRestResourceScopeOptions(resource, {
1029
+ queryFields: {
1030
+ remainingBatchWeight: {
1031
+ type: "number",
1032
+ select({ knex, column }) {
1033
+ return knex.raw("?? - coalesce(??, 0)", [
1034
+ column("received_weight"),
1035
+ column("processed_weight")
1036
+ ]);
1037
+ }
1038
+ }
1039
+ }
1040
+ })
1041
+ );
1042
+ ```
1043
+
1044
+ `createJsonRestResourceScopeOptions(...)` moves matching virtual fields out of the storage schema and into JSON REST `queryFields`, where they are selected for reads and ignored for writes. Prefer the `queryFields` option when the resource module is imported by both server and client code; use `storage.queryProjection` only in server-only resource modules.
1045
+
1046
+ For the repository runtime, once registered there:
1022
1047
 
1023
1048
  - generic CRUD `list`
1024
1049
  - generic CRUD `findById`
@@ -1126,14 +1151,16 @@ Keep bulk action definitions page-local unless another page needs to share them.
1126
1151
  For a generated CRUD, treat this as the concrete file plan:
1127
1152
 
1128
1153
  - edit `src/pages/.../contacts/listFilters.js`
1129
- - make sure the matching server route/action/repository code already accepts and applies the declared query params
1154
+ - make sure the matching server route/action/repository code accepts and applies the declared query params when filters are server-backed
1130
1155
 
1131
1156
  If the filter contract should be shared with server code, promote it into a CRUD package module and import it from both sides:
1132
1157
 
1133
1158
  - create `packages/contacts/src/shared/contactListFilters.js`
1134
- - update `packages/contacts/src/server/registerRoutes.js` so the list route query validator lists structured filter params explicitly with `createCrudListFilterQueryField(...)`, unless you already extracted list-query composition into `packages/contacts/src/server/listQueryValidators.js`
1135
- - update `packages/contacts/src/server/actions.js` so the list action input validator includes that same explicit contract choice
1136
- - update `packages/contacts/src/server/repository.js` so the list query path builds the `createCrudListFilters(...)` runtime and calls `applyQuery(...)`
1159
+ - create `packages/contacts/src/server/contactListFilterContract.js` with `createCrudListFilterContract(...)`
1160
+ - update `packages/contacts/src/server/registerRoutes.js` so the list route query validator includes `contactListFilterContract.queryValidator`
1161
+ - update `packages/contacts/src/server/actions.js` so the list action input validator includes the same `queryValidator`
1162
+ - update the provider's `createJsonRestResourceScopeOptions(...)` call so it merges `searchSchema: contactListFilterContract.jsonRestSearchSchema`
1163
+ - update `packages/contacts/src/server/repository.js` so the list query path passes `contactListFilterContract.toJsonRestQuery(query)` into `buildJsonRestQueryParams(...)`
1137
1164
 
1138
1165
  #### Client side
1139
1166
 
@@ -1231,101 +1258,105 @@ Use `mode: "merge"` when a preset should only change one filter group, such as t
1231
1258
 
1232
1259
  #### Server side
1233
1260
 
1234
- Build the server runtime from the same shared definitions:
1261
+ Build the server contract from the same shared definitions:
1235
1262
 
1236
1263
  ```js
1237
- const contactsListFiltersRuntime = createCrudListFilters(
1264
+ import { createCrudListFilterContract } from "@jskit-ai/crud-core/server/listFilters";
1265
+ import { CONTACTS_LIST_FILTER_DEFINITIONS } from "../shared/contactListFilters.js";
1266
+
1267
+ const contactListFilterContract = createCrudListFilterContract(
1238
1268
  CONTACTS_LIST_FILTER_DEFINITIONS,
1239
1269
  {
1240
1270
  columns: {
1241
- onlyStaff: "is_staff",
1242
- onlyVip: "vip",
1271
+ status: "status",
1272
+ supplierContactId: "supplier_contact_id",
1273
+ arrivalDate: "arrival_datetime",
1243
1274
  onlyArchived: "archived"
1244
- }
1275
+ },
1276
+ invalidValues: "reject"
1245
1277
  }
1246
1278
  );
1247
- ```
1248
-
1249
- There is no default query-validator mode or route-runtime alias. Keep the filter runtime for repository semantics, but author route/action query params explicitly.
1250
-
1251
- Strict contract example:
1252
1279
 
1253
- ```js
1254
- const contactsListFilters = CONTACTS_LIST_FILTER_DEFINITIONS;
1255
-
1256
- const contactsListFiltersQueryValidator = {
1257
- schema: createCrudListFilterQuerySchema({
1258
- status: createCrudListFilterQueryField(contactsListFilters.status, {
1259
- invalidValues: "reject"
1260
- }),
1261
- arrivalDate: createCrudListFilterQueryField(contactsListFilters.arrivalDate, {
1262
- invalidValues: "reject"
1263
- })
1264
- }),
1265
- mode: "patch"
1266
- };
1280
+ export { contactListFilterContract };
1267
1281
  ```
1268
1282
 
1269
- Lenient contract example:
1283
+ That one contract gives the server:
1270
1284
 
1271
- ```js
1272
- const contactsListFiltersQueryValidator = {
1273
- schema: createCrudListFilterQuerySchema({
1274
- status: createCrudListFilterQueryField(CONTACTS_LIST_FILTER_DEFINITIONS.status, {
1275
- invalidValues: "discard"
1276
- }),
1277
- arrivalDate: createCrudListFilterQueryField(CONTACTS_LIST_FILTER_DEFINITIONS.arrivalDate, {
1278
- invalidValues: "discard"
1279
- })
1280
- }),
1281
- mode: "patch"
1282
- };
1283
- ```
1285
+ - `queryValidator` for route/action input validation
1286
+ - `jsonRestSearchSchema` for JSON REST resource registration
1287
+ - `toJsonRestQuery(query)` for repository query normalization
1288
+ - `applyQuery(...)` when a non-JSON REST repository still needs direct Knex filtering
1284
1289
 
1285
- Wire the explicit query contract into the list validator and the repository:
1290
+ Wire the contract into route and action validators:
1286
1291
 
1287
1292
  ```js
1288
1293
  const listRouteQueryValidator = composeSchemaDefinitions([
1289
1294
  listCursorPaginationQueryValidator,
1290
1295
  listSearchQueryValidator,
1291
- contactsListFiltersQueryValidator,
1292
1296
  listParentFilterQueryValidator,
1297
+ contactListFilterContract.queryValidator,
1293
1298
  lookupIncludeQueryValidator
1294
1299
  ], {
1295
1300
  mode: "patch"
1296
1301
  });
1297
1302
  ```
1298
1303
 
1299
- Use that same explicit `contactsListFiltersQueryValidator` anywhere else the list query is validated, such as the composed list action input validator if your CRUD package validates query shape at both the route and action boundaries.
1304
+ Use the same `contactListFilterContract.queryValidator` anywhere else the list query is validated, such as the composed list action input validator if your CRUD package validates query shape at both the route and action boundaries.
1305
+
1306
+ Merge the JSON REST search schema when the provider registers the resource:
1307
+
1308
+ ```js
1309
+ await addResourceIfMissing(
1310
+ api,
1311
+ JSON_REST_SCOPE_NAME,
1312
+ createJsonRestResourceScopeOptions(resource, {
1313
+ searchSchema: contactListFilterContract.jsonRestSearchSchema,
1314
+ writeSerializers: {
1315
+ "datetime-utc": toDatabaseDateTimeUtc
1316
+ }
1317
+ })
1318
+ );
1319
+ ```
1320
+
1321
+ Normalize the list query before building JSON REST query params:
1322
+
1323
+ ```js
1324
+ async function queryDocuments(query = {}, options = {}) {
1325
+ return api.resources.contacts.query(
1326
+ {
1327
+ queryParams: buildJsonRestQueryParams(
1328
+ JSON_REST_SCOPE_NAME,
1329
+ contactListFilterContract.toJsonRestQuery(query)
1330
+ ),
1331
+ transaction: options?.trx || null,
1332
+ simplified: false
1333
+ },
1334
+ createJsonRestContext(options?.context || null)
1335
+ );
1336
+ }
1337
+ ```
1338
+
1339
+ `enumMany` and `recordIdMany` filters stay arrays. Date and number ranges become internal JSON REST search keys, so callers keep one public query key such as `arrivalDate` while the backend receives precise lower/upper filter operations.
1300
1340
 
1301
1341
  Choose the invalid-value contract deliberately:
1302
1342
 
1303
- - there is no default mode and no fallback alias, so every structured filter field must choose `invalidValues: "reject"` or `invalidValues: "discard"` explicitly
1343
+ - `createCrudListFilterContract(...)` defaults to `invalidValues: "reject"` for a strict server boundary
1344
+ - set `invalidValues` explicitly when a package is choosing a non-default validation posture
1304
1345
  - use `invalidValues: "reject"` when malformed filter values should fail validation and produce a 400-style contract error
1305
1346
  - use `invalidValues: "discard"` when malformed filter values should be ignored and normalization should drop them
1306
1347
  - route query validation runs before auth, so this choice changes whether malformed unauthenticated requests fail at validation or fall through to auth
1307
1348
  - for normal HTTP CRUD handlers, route-level `discard` means the handler receives already-parsed filter values for the explicit fields you listed, so the action layer will not see those discarded bad values again later
1308
- - the filter runtime is still a deliberate two-phase exception: schema parsing owns query-field values, then `applyQuery(...)` maps those parsed values back through filter keys and SQL semantics
1309
-
1310
- ```js
1311
- async function list(query = {}, callOptions = {}) {
1312
- return crudRepositoryList(repositoryRuntime, knex, query, options, callOptions, {
1313
- modifyQuery(dbQuery, context = {}) {
1314
- return contactsListFiltersRuntime.applyQuery(dbQuery, context.query || {});
1315
- }
1316
- });
1317
- }
1318
- ```
1349
+ - the filter contract is still a deliberate two-phase exception: schema parsing owns public query-field values, then `toJsonRestQuery(...)` maps those parsed values to JSON REST filter keys and SQL semantics
1319
1350
 
1320
1351
  #### Best practices
1321
1352
 
1322
1353
  - Keep client-only filters in the generated page-local `listFilters.js`. Move definitions into a CRUD package only when server code or another page needs to share them.
1323
1354
  - Keep the filter keys identical all the way through: definition key, query param key, and repository meaning.
1324
- - Prefer `createCrudListFilterQueryField(...)` plus `createCrudListFilterQuerySchema(...)` at route/action boundaries so accepted structured-filter params stay visible in app code.
1355
+ - Prefer `createCrudListFilterContract(...)` for server-backed structured filters so route/action validators, JSON REST search schema, and repository query normalization stay derived from one shared definition.
1325
1356
  - Use `type: "presence"` for null/not-null filters such as assigned vs unassigned storage. Do not model those as custom enums plus `applyQuery(...)` overrides unless the SQL semantics are genuinely different from `whereNotNull(...)` / `whereNull(...)`.
1326
- - Use `createCrudListFilters(...)` unless the list semantics are truly unusual.
1357
+ - Use `createCrudListFilters(...)` directly only for non-JSON REST repository code that needs direct Knex filtering without JSON REST registration.
1327
1358
  - Use `q` for free-text and explicit query params for structured filters.
1328
- - Run `jskit doctor` after wiring filters. It flags inline filter-definition objects passed into `useCrudListFilters(...)` or `createCrudListFilters(...)`.
1359
+ - Run `jskit doctor` after wiring filters.
1329
1360
 
1330
1361
  ### Pattern 4: lookup-backed structured filters
1331
1362
 
@@ -1456,7 +1487,7 @@ The runtime already handles both together.
1456
1487
 
1457
1488
  #### Server side
1458
1489
 
1459
- Let the generic list search handle `q`, and let your custom validator/repository code handle the structured filters.
1490
+ Let the generic list search handle `q`, and let `createCrudListFilterContract(...)` handle the structured route/action validators, JSON REST search schema, and repository query normalization.
1460
1491
 
1461
1492
  #### Best practices
1462
1493
 
@@ -1612,8 +1643,10 @@ Use `scaffold-field` when it fits, then review the generated result. It patches
1612
1643
  Touch:
1613
1644
 
1614
1645
  - `packages/<crud>/src/shared/<crud>ListFilters.js` and make it the only authored filter-definition module
1615
- - `packages/<crud>/src/server/registerRoutes.js` and `packages/<crud>/src/server/actions.js` so the list validator lists structured filter params explicitly with `createCrudListFilterQueryField(...)`, or `packages/<crud>/src/server/listQueryValidators.js` if you extracted list-query composition there
1616
- - `packages/<crud>/src/server/repository.js` so the list query applies the runtime
1646
+ - `packages/<crud>/src/server/<crud>ListFilterContract.js` with `createCrudListFilterContract(...)`
1647
+ - `packages/<crud>/src/server/registerRoutes.js` and `packages/<crud>/src/server/actions.js` so list validators include `<crud>ListFilterContract.queryValidator`, or `packages/<crud>/src/server/listQueryValidators.js` if you extracted list-query composition there
1648
+ - the provider registration so `createJsonRestResourceScopeOptions(resource, { searchSchema: <crud>ListFilterContract.jsonRestSearchSchema })` merges the JSON REST search schema
1649
+ - `packages/<crud>/src/server/repository.js` so the list query calls `<crud>ListFilterContract.toJsonRestQuery(query)` before `buildJsonRestQueryParams(...)`
1617
1650
  - the app-owned list page or list-runtime composable that calls `useCrudList(...)`
1618
1651
 
1619
1652
  If the filter is lookup-backed, touch that same client file again to wire `useCrudListFilterLookups(...)`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/agent-docs",
3
- "version": "0.1.53",
3
+ "version": "0.1.54",
4
4
  "description": "Distributed JSKIT agent references, prompts, guides, and generated reference maps.",
5
5
  "type": "module",
6
6
  "files": [
@@ -12,8 +12,9 @@ Default JSKIT pattern:
12
12
  4. Let generic CRUD runtime handle standard writable `date-time` fields automatically at the DB write seam.
13
13
  5. Use `storage.writeSerializer` only for non-default write serialization.
14
14
  6. Use `storage: { virtual: true }` for computed output fields.
15
- 7. Register computed SQL projections once in the repository runtime with `virtualFields`.
16
- 8. Let generic CRUD read paths apply those projections automatically.
15
+ 7. For `createCrudResourceRuntime(...)` repositories, register computed SQL projections once with `virtualFields`.
16
+ 8. For internal JSON REST repositories, register SQL-selected output fields with `createJsonRestResourceScopeOptions(resource, { queryFields })`.
17
+ 9. Let generic read paths apply those projections automatically.
17
18
 
18
19
  Field metadata rules:
19
20
  - for a normal override, write:
@@ -80,6 +81,35 @@ const repositoryRuntime = createCrudResourceRuntime(resource, {
80
81
  });
81
82
  ```
82
83
 
84
+ Internal JSON REST pattern:
85
+ - keep the API field in the resource schema as `storage: { virtual: true }`
86
+ - pass server-only SQL projection callbacks through `createJsonRestResourceScopeOptions(...)`
87
+ - keep query projection SQL in the provider/server registration file, not in browser-imported page code
88
+
89
+ Example:
90
+
91
+ ```js
92
+ await addResourceIfMissing(
93
+ api,
94
+ "receivals",
95
+ createJsonRestResourceScopeOptions(resource, {
96
+ queryFields: {
97
+ remainingProcessableWeight: {
98
+ type: "number",
99
+ select({ knex, column }) {
100
+ return knex.raw("?? - coalesce(??, 0)", [
101
+ column("received_weight"),
102
+ column("processed_weight")
103
+ ]);
104
+ }
105
+ }
106
+ }
107
+ })
108
+ );
109
+ ```
110
+
111
+ If the resource module is server-only, a field may also declare `storage.queryProjection`; `createJsonRestResourceScopeOptions(...)` moves it into JSON REST `queryFields` and removes the virtual field from the storage schema. Prefer the `queryFields` option when the resource module is shared with client code.
112
+
83
113
  What CRUD core does for you:
84
114
  - default select columns include only column-backed output fields
85
115
  - create/update write payloads serialize standard writable `date-time` fields centrally
@@ -99,5 +129,5 @@ Review checks:
99
129
  - standard datetime DB write serialization stays automatic and runtime-owned
100
130
  - any non-default DB write serialization lives in `storage.writeSerializer`, not in per-repository hooks
101
131
  - computed fields use `storage: { virtual: true }`
102
- - repository runtime registers matching `virtualFields`
132
+ - repository runtime registers matching `virtualFields`, or the JSON REST provider registers matching `queryFields`
103
133
  - no per-method projection duplication when generic CRUD reads already cover the field
@@ -35,25 +35,30 @@ Generated client shape:
35
35
  - `<CrudListFilterSurface :filters="listFilters" :runtime="filterRuntime" />`
36
36
 
37
37
  Server-side pattern, only when implementing real backend filter semantics:
38
- 1. Put reusable server-aware filter definitions in the CRUD package if the filter contract needs to be shared outside one generated page.
38
+ 1. Put reusable filter definitions in the CRUD package if server code or multiple pages need the same contract.
39
39
  Example path: `packages/<crud>/src/shared/<crud>ListFilters.js`
40
- 2. Build the server runtime from that module with `createCrudListFilters(...)`.
41
- 3. Build route/action validators explicitly with `createCrudListFilterQueryField(...)` plus `createCrudListFilterQuerySchema(...)`, and use `applyQuery(...)` in the repository. There is no default validator mode or route-runtime alias.
40
+ 2. Build a server contract from that module with `createCrudListFilterContract(...)`.
41
+ 3. Use the contract's `queryValidator` in route/action input composition.
42
+ 4. Pass the contract's `jsonRestSearchSchema` into `createJsonRestResourceScopeOptions(..., { searchSchema })`.
43
+ 5. Call `contract.toJsonRestQuery(query)` before `buildJsonRestQueryParams(...)` in the JSON REST repository path.
42
44
 
43
45
  Exact file checklist:
44
46
  - create `packages/<crud>/src/shared/<crud>ListFilters.js`
45
- - update `packages/<crud>/src/server/registerRoutes.js` and `packages/<crud>/src/server/actions.js` so the list query validator lists structured filter params explicitly with `createCrudListFilterQueryField(...)` inside `createCrudListFilterQuerySchema(...)`, or update `packages/<crud>/src/server/listQueryValidators.js` if that package already extracts list-query composition there
46
- - update `packages/<crud>/src/server/repository.js` so list queries call the runtime's `applyQuery(...)`
47
+ - create `packages/<crud>/src/server/<crud>ListFilterContract.js` with `createCrudListFilterContract(...)`
48
+ - update `packages/<crud>/src/server/registerRoutes.js` and `packages/<crud>/src/server/actions.js` so the list query validator includes `listFilterContract.queryValidator`
49
+ - update the provider's `createJsonRestResourceScopeOptions(...)` call so `searchSchema: listFilterContract.jsonRestSearchSchema` is merged into the internal JSON REST resource
50
+ - update `packages/<crud>/src/server/repository.js` so list queries pass `listFilterContract.toJsonRestQuery(query)` into `buildJsonRestQueryParams(...)`
47
51
  - update the generated page-local `listFilters.js` first; only edit `index.vue` if a specialist lookup label/runtime integration is needed
48
52
  - for lookup-backed filters, wire `useCrudListFilterLookups(...)` beside the existing generated filter runtime instead of replacing `CrudListFilterSurface`
49
53
 
50
54
  Validation mode is part of the contract:
51
- - there is no default mode and no fallback alias, so every explicit structured filter field must choose `invalidValues: "reject"` or `invalidValues: "discard"` deliberately
55
+ - `createCrudListFilterContract(...)` defaults to `invalidValues: "reject"` for a strict server boundary
56
+ - set `invalidValues` explicitly when a package is choosing a non-default validation posture
52
57
  - use `invalidValues: "reject"` when malformed filter values should fail validation and return a 400-style contract error
53
58
  - use `invalidValues: "discard"` when malformed filter values should be ignored and normalization should drop them
54
59
  - route query validation runs before auth, so this choice affects whether malformed unauthenticated requests fail at validation or fall through to auth
55
60
  - for normal HTTP CRUD handlers, route-level `discard` means the action layer receives already-parsed values for those explicit filter fields; do not assume route `discard` plus action `reject` will still reject malformed HTTP query strings later
56
- - CRUD list filters are still a deliberate two-phase exception: schema parsing owns query-field values, then the server runtime reprojects those parsed values through filter keys and `applyQuery(...)`
61
+ - CRUD list filters are still a deliberate two-phase exception: schema parsing owns public query-field values, then the server contract reprojects parsed values to JSON REST filters with `toJsonRestQuery(...)`
57
62
 
58
63
  Keep separate:
59
64
  - free-text search uses `records.searchQuery` and `q`
@@ -77,18 +82,18 @@ Use runtime presets when:
77
82
 
78
83
  Put unusual SQL semantics on the server:
79
84
  - examples: `pending` meaning `whereNull("ccp1_passed")`, or business-specific status buckets that combine multiple columns
80
- - implement those in `createCrudListFilters(..., { apply: { ... } })`
85
+ - implement those in `createCrudListFilterContract(..., { apply: { ... } })`
81
86
  - do not use custom `apply` just for null/not-null checks when `type: "presence"` fits
82
87
 
83
88
  Avoid:
84
89
  - local filter composables that duplicate the same keys the server already knows about
85
90
  - a custom validator shape that does not match the page state
86
- - hiding accepted structured filter params behind whole-query validator generation when the route/action contract can list them directly
91
+ - hand-rolled route/action validators or repository filters that duplicate `createCrudListFilterContract(...)`
87
92
  - hand-rolled preset apply/reset/active-state helpers when `useCrudListFilters(..., { presets })`, `applyPreset(...)`, and `matchesPreset(...)` fit
88
93
  - per-screen `useList()` wrappers for lookup-backed filters when `useCrudListFilterLookups(...)` fits
89
94
  - editing generated `.vue` files just to add basic filter controls; use the page-local `listFilters.js` seam first
90
95
  - overloading `q` with structured filter meaning
91
- - inline filter-definition objects passed into `useCrudListFilters(...)` or `createCrudListFilters(...)`; keep definitions in a named module
96
+ - inline filter-definition objects passed into `useCrudListFilters(...)`, `createCrudListFilters(...)`, or `createCrudListFilterContract(...)`; keep definitions in a named module
92
97
 
93
98
  Good shape:
94
99
  - `src/pages/home/customers/listFilters.js`
@@ -96,7 +101,9 @@ Good shape:
96
101
  - generated page passes `listFilters` into `useCrudListScreen(...)`
97
102
  - shared screen runtime passes `filterRuntime.queryParams` into the list request
98
103
  - `packages/receivals/src/shared/receivalListFilters.js`
99
- - `createCrudListFilters(RECEIVAL_LIST_FILTER_DEFINITIONS, ...)`
104
+ - `createCrudListFilterContract(RECEIVAL_LIST_FILTER_DEFINITIONS, { columns, invalidValues: "reject" })`
105
+ - `createJsonRestResourceScopeOptions(resource, { searchSchema: receivalListFilterContract.jsonRestSearchSchema })`
106
+ - `buildJsonRestQueryParams(JSON_REST_SCOPE_NAME, receivalListFilterContract.toJsonRestQuery(query))`
100
107
  - `const listFilters = useCrudListFilters(RECEIVAL_LIST_FILTER_DEFINITIONS, { presets: [...] })`
101
108
  - `const filterLookups = useCrudListFilterLookups(RECEIVAL_LIST_FILTER_DEFINITIONS, { values: listFilters.values, ... })`
102
109
  - `queryParams: listFilters.queryParams`
@@ -108,7 +115,7 @@ Preset contract notes:
108
115
 
109
116
  Review checks:
110
117
  - one filter definition source of truth: generated page-local `listFilters.js` for client-only filters, or a shared CRUD-package module when server code imports the same definitions
111
- - server validator/repository logic derived from that source, with route/action query params still listed explicitly
118
+ - server validator, JSON REST search schema, and repository query projection derived from that source through `createCrudListFilterContract(...)`
112
119
  - client query params/chips/reset logic derived from that source
113
120
  - lookup-backed filters use the shared lookup helper, not a page-local mini-framework
114
121
  - the two-phase server exception is intentional and documented, not accidental drift
@@ -55,7 +55,7 @@ Generated JSKIT apps should feel like real adaptive apps by default, not framewo
55
55
  - Item 3 is complete for current generated surfaces: page, CRUD, shell, and starter outputs require compact/medium/expanded coverage, horizontal overflow checks, generated screen checks, and 48px tap target checks. Calendar/grid/bottom-sheet specialist generators remain future scope until those generators exist.
56
56
  - Item 5 is complete for current generators: `primary`, `secondary`, `utility`, `detail`, `workflow`, and `none` are centralized in the generated UI contract, generators consume that contract, and `utility` resolves to seeded `shell.global-actions` topology.
57
57
  - Item 6 is complete for the default shell: compact app bars use compact density and bounded top-left/top-right regions, while primary navigation remains in semantic bottom navigation instead of app-bar chrome.
58
- - CRUD filters are client-side by default: generated list pages create a page-local `listFilters.js` and pass it into `useCrudListScreen(...)`; the shared list screen wires `filterRuntime.queryParams` into the list request and renders `CrudListFilterSurface`. Server filter behavior remains explicit app/package work.
58
+ - CRUD filters are client-side by default: generated list pages create a page-local `listFilters.js` and pass it into `useCrudListScreen(...)`; the shared list screen wires `filterRuntime.queryParams` into the list request and renders `CrudListFilterSurface`. When server filtering is needed, promote the definitions into a shared package module and use `createCrudListFilterContract(...)` so route/action validators, JSON REST search schema, and repository query normalization stay derived from the same definition.
59
59
  - CRUD bulk actions are client-side by default: generated list pages create a page-local `listBulkActions.js` and pass it into `useCrudListScreen(...)`; the shared list screen wires `useCrudListBulkActions(...)` and keeps selection controls hidden until actions are declared.
60
60
  - Generated CRUD page templates delegate their screen chrome to shared `users-web` screen components (`CrudListScreen`, `CrudViewScreen`, and `CrudAddEditScreen`) so list/view/form load states, retry actions, responsive shell layout, filters, and bulk actions do not drift across generated pages.
61
61
  - Routine resource-load errors stay local to the generated screen and retry affordance. Action feedback uses the shell error policy through `action-feedback`.
@@ -10,20 +10,21 @@ Core rule:
10
10
  - for current JSKIT CRUD apps, the server search contract lives in `resource.searchSchema` on the internal JSON REST path
11
11
  - route validators decide which public query keys HTTP callers may send
12
12
  - repositories may add internal-only filter keys before forwarding the query to JSON REST
13
+ - structured list filters should use `createCrudListFilterContract(...)` so route/action validators, JSON REST search schema, and repository query normalization are derived from one definition
13
14
 
14
15
  Fresh generated CRUD repositories already follow this path:
15
16
 
16
17
  ```js
17
18
  return api.resources.contacts.query({
18
- queryParams: buildJsonRestQueryParams(RESOURCE_TYPE, query),
19
+ queryParams: buildJsonRestQueryParams(RESOURCE_TYPE, contactListFilterContract.toJsonRestQuery(query)),
19
20
  simplified: false
20
21
  }, createJsonRestContext(context));
21
22
  ```
22
23
 
23
24
  The normal layer split is:
24
25
  1. `actions.js` / `registerRoutes.js`: accept and validate public query keys
25
- 2. `repository.js`: add internal-only filter keys such as visibility scope
26
- 3. `*Resource.js`: define the backend filter behavior in `searchSchema`
26
+ 2. `repository.js`: add internal-only filter keys such as visibility scope, or call `listFilterContract.toJsonRestQuery(query)` for structured filters
27
+ 3. provider/resource registration: merge backend filter behavior into JSON REST with `createJsonRestResourceScopeOptions(resource, { searchSchema })`
27
28
 
28
29
  ## Decision Rules
29
30
 
@@ -147,6 +148,26 @@ That means:
147
148
  - `q` and the structured filter keys are accepted publicly
148
149
  - the repository may still add internal-only keys later
149
150
 
151
+ For structured list filters, prefer the generated contract shape:
152
+
153
+ ```js
154
+ const receivalListFilterContract = createCrudListFilterContract(
155
+ RECEIVAL_LIST_FILTER_DEFINITIONS,
156
+ {
157
+ columns: {
158
+ status: "status",
159
+ supplierContactId: "supplier_contact_id",
160
+ arrivalDate: "arrival_datetime"
161
+ },
162
+ invalidValues: "reject"
163
+ }
164
+ );
165
+ ```
166
+
167
+ Use `receivalListFilterContract.queryValidator` at route/action boundaries. Use `receivalListFilterContract.jsonRestSearchSchema` when registering the JSON REST resource. Use `receivalListFilterContract.toJsonRestQuery(query)` before `buildJsonRestQueryParams(...)`.
168
+
169
+ That contract preserves arrays for `enumMany` and `recordIdMany`, maps range filters to internal JSON REST search keys, and keeps those internal keys out of the public HTTP query contract.
170
+
150
171
  Do not add route validators for:
151
172
  - repository-injected internal keys such as `"__viewerAccess"`
152
173
  - filters that must never be supplied by the client directly
@@ -197,6 +218,7 @@ function buildScopedQuery(query = {}, context = null) {
197
218
  ## Keep These Boundaries Clean
198
219
 
199
220
  - Put backend filter behavior in `searchSchema`, not in page code.
221
+ - Use `createCrudListFilterContract(...)` for structured list filters before writing custom JSON REST query glue.
200
222
  - Put permission checks in the action/service layer, not in `applyFilter`.
201
223
  - Put repository-only scoping in the repository, not in route query validation.
202
224
  - Do not expose a query key publicly just because the repository uses it internally.
@@ -133,7 +133,9 @@ Exports
133
133
  - `CRUD_LIST_FILTER_INVALID_VALUES_DISCARD`
134
134
  - `createCrudListFilterQueryField(filterDefinition = {}, { invalidValues = CRUD_LIST_FILTER_INVALID_VALUES_REJECT } = {})`
135
135
  - `createCrudListFilterQuerySchema(structure = {})`
136
+ - `createCrudListFilterJsonRestSearchSchema(runtime = {}, { columns = {}, apply = {} } = {})`
136
137
  - `createCrudListFilters(definitions = {}, { columns = {}, apply = {} } = {})`
138
+ - `createCrudListFilterContract(definitions = {}, { columns = {}, apply = {}, invalidValues = CRUD_LIST_FILTER_INVALID_VALUES_REJECT } = {})`
137
139
  Local functions
138
140
  - `cloneTransportSchema(value)`
139
141
  - `buildSingleOrMultiTransportSchema(itemSchema)`
@@ -144,6 +146,18 @@ Local functions
144
146
  - `buildFilterQuerySchemaDefinition(filterEntries = [], { invalidValues = CRUD_LIST_FILTER_INVALID_VALUES_REJECT } = {})`
145
147
  - `projectNormalizedFilterValues(filterEntries = [], source = {}, errors = {})`
146
148
  - `normalizeColumnsMap(columns = {})`
149
+ - `resolveJsonRestFilterActualField(filter = {}, columns = {})`
150
+ - `renderInternalJsonRestFilterKey(filter = {}, suffix = "")`
151
+ - `createJsonRestSearchSchemaEntry({ type = "string", actualField = "", filterOperator = "=", extra = {} } = {})`
152
+ - `buildJsonRestFilterItemSchema(filter = {})`
153
+ - `buildJsonRestSimpleFilterSearchSchemaEntry(filter = {}, { actualField = "" } = {})`
154
+ - `buildJsonRestRangeFilterSearchSchemaEntries(filter = {}, { actualField = "" } = {})`
155
+ - `buildJsonRestPresenceFilterSearchSchemaEntry(filter = {}, { actualField = "" } = {})`
156
+ - `buildJsonRestCustomFilterSearchSchemaEntry(filter = {}, customApply = null)`
157
+ - `resolveCrudListFilterEntries(value = {})`
158
+ - `addJsonRestDateFilterValues(target = {}, filter = {}, value = "")`
159
+ - `addJsonRestFilterValue(target = {}, filter = {}, value)`
160
+ - `normalizeCrudListFilterJsonRestQuery(runtime = {}, query = {})`
147
161
  - `applyDefaultFilterQuery(queryBuilder, filter = {}, value, column = "")`
148
162
 
149
163
  ### `src/server/listQueryValidators.js`
@@ -295,7 +309,7 @@ Local functions
295
309
 
296
310
  ### `src/server/routeContracts.js`
297
311
  Exports
298
- - `createCrudJsonApiRouteContracts({ resource = {}, routeParamsValidator = null, listSearchQueryValidator = defaultListSearchQueryValidator, lookupIncludeQueryValidator = defaultLookupIncludeQueryValidator } = {})`
312
+ - `createCrudJsonApiRouteContracts({ resource = {}, routeParamsValidator = null, listSearchQueryValidator = defaultListSearchQueryValidator, lookupIncludeQueryValidator = defaultLookupIncludeQueryValidator, listFilterQueryValidator = null } = {})`
299
313
  Local functions
300
314
  - `isRecord(value)`
301
315
  - `resolveSchemaFieldDefinitions(definition = null)`
@@ -26,7 +26,7 @@ Exports
26
26
  - `buildJsonRestQueryParams(resourceType = "", query = {}, { include = undefined } = {})`
27
27
  - `createJsonApiInputRecord(resourceType = "", attributes = {}, { id = null, relationships = null, resource = null } = {})`
28
28
  - `createJsonApiRelationship(resourceType = "", id = null)`
29
- - `createJsonRestResourceScopeOptions(resource = {}, { writeSerializers = {}, normalizeId = null } = {})`
29
+ - `createJsonRestResourceScopeOptions(resource = {}, { writeSerializers = {}, normalizeId = null, searchSchema = null, queryFields = null } = {})`
30
30
  - `createJsonRestContext(context = null)`
31
31
  - `extractJsonRestCollectionRows(payload = null)`
32
32
  - `isJsonRestResourceMissingError(error = null)`
@@ -40,9 +40,13 @@ Local functions
40
40
  - `cloneJsonRestResourceValue(value, { writeSerializers = {} } = {})`
41
41
  - `normalizeScopeValue(value)`
42
42
  - `normalizeJsonRestText(value, { fallback = "" } = {})`
43
+ - `normalizeJsonRestFilterValue(value)`
43
44
  - `normalizeJsonRestObject(value)`
44
45
  - `normalizeJsonRestList(value)`
45
46
  - `resolveJsonRestCollectionRelationships(resource = {})`
47
+ - `normalizeJsonRestQueryField(fieldName = "", fieldDefinition = {}, projectionDefinition = null)`
48
+ - `isJsonRestVirtualField(fieldDefinition = null)`
49
+ - `applyJsonRestQueryFields(scopeOptions = {}, extraQueryFields = {})`
46
50
  - `extractJsonApiInputRelationships(attributes = {}, resource = null, relationships = null)`
47
51
 
48
52
  ### root