@jskit-ai/agent-docs 0.1.53 → 0.1.55
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.
- package/guide/agent/generators/advanced-cruds.md +238 -65
- package/package.json +1 -1
- package/patterns/INDEX.md +1 -1
- package/patterns/client-requests.md +9 -0
- package/patterns/crud-repository-mapping.md +33 -3
- package/patterns/crud-scaffolding.md +2 -1
- package/patterns/filters.md +19 -12
- package/patterns/generated-ui-contract-tracking.md +5 -2
- package/patterns/server-search.md +25 -3
- package/reference/autogen/packages/crud-core.md +15 -1
- package/reference/autogen/packages/json-rest-api-core.md +5 -1
- package/reference/autogen/packages/users-web.md +37 -2
|
@@ -454,7 +454,8 @@ This is the list-page container.
|
|
|
454
454
|
Its job is usually to:
|
|
455
455
|
|
|
456
456
|
- call `useCrudListScreen()`
|
|
457
|
-
- pass page-local `listFilters` and `
|
|
457
|
+
- pass page-local `listFilters`, `listBulkActions`, and `listRowActions` when needed
|
|
458
|
+
- pass `syntheticRows` when the page needs non-CRUD display rows such as an owner/master row
|
|
458
459
|
- render the shared `CrudListScreen`
|
|
459
460
|
- resolve list/view/edit/new URLs
|
|
460
461
|
- pass route query state through when navigating deeper
|
|
@@ -470,6 +471,8 @@ Its job is usually to:
|
|
|
470
471
|
- call `useCrudViewScreen()`
|
|
471
472
|
- render the shared `CrudViewScreen`
|
|
472
473
|
- resolve "back" and "edit" navigation
|
|
474
|
+
- pass read options such as `requestQueryParams`, `readEnabled`, and `queryKeyFactory` when the detail read needs them
|
|
475
|
+
- use the shared view slots for page-specific sections around the generated field list
|
|
473
476
|
|
|
474
477
|
Again, the runtime behavior is mostly uphill. The page is a route-level composition layer.
|
|
475
478
|
|
|
@@ -822,6 +825,8 @@ Use this rule of thumb when deciding where to edit:
|
|
|
822
825
|
| Change SQL, joins, parent filters, or advanced search | `repository.js` | This is the data-access layer |
|
|
823
826
|
| Add cross-record or domain rules on save/delete | `service.js` | This is business logic |
|
|
824
827
|
| Change shared CRUD screen chrome, load states, or retry behavior | `users-web` shared screen components | Generated pages consume the shared screen contract |
|
|
828
|
+
| Add per-row commands to a generated list page | page-local `listRowActions.js`, usually calling `useCommand()`-backed composables | The shared list screen renders action chrome; the page owns explicit mutation behavior |
|
|
829
|
+
| Add non-CRUD display rows to a generated list page | route page `syntheticRows` input | Synthetic rows are presentation rows, not repository records |
|
|
825
830
|
| Change page-specific display behavior | the route pages, generated slots, and app-owned composables | This is presentation |
|
|
826
831
|
| Change form field layout and inputs | `_components/CrudAddEditForm.vue` and `CrudAddEditFormFields.js` | This is the generated form field layer |
|
|
827
832
|
|
|
@@ -832,6 +837,7 @@ The baseline generator output is only the start. As the tutorial's `contacts`, `
|
|
|
832
837
|
- `src/server/listQueryValidators.js` when a list needs extra query filters beyond `q`
|
|
833
838
|
- `src/server/service.test.js` once save/delete rules stop being trivial
|
|
834
839
|
- `packages/contacts/src/shared/contactListFilters.js` when the contacts CRUD gains structured list filters that both server and client should share
|
|
840
|
+
- `src/pages/.../contacts/listRowActions.js` when a generated list needs row-level commands such as Delete, Block, or Unblock
|
|
835
841
|
- `src/composables/addresses/useAddressDisplay.js` when addresses need app-specific display formatting
|
|
836
842
|
- `src/composables/comments/useCommentsListRuntime.js` when an embedded comments list needs local UI state
|
|
837
843
|
|
|
@@ -840,6 +846,55 @@ That is the right direction of growth:
|
|
|
840
846
|
- server customizations stay in the CRUD package
|
|
841
847
|
- presentation and page-specific UI state stay in app-owned client files
|
|
842
848
|
- shared structured list filters live best in a CRUD-package shared module that both server and client can import
|
|
849
|
+
- shared generated screen chrome stays in `users-web`; adapted pages feed it definitions, slots, and explicit command handlers
|
|
850
|
+
|
|
851
|
+
### Detail read options and extension slots
|
|
852
|
+
|
|
853
|
+
Use the shared detail screen first, even when the detail page needs includes or domain sections.
|
|
854
|
+
|
|
855
|
+
`useCrudViewScreen(...)` accepts the same read-pass-through options that adapted detail pages usually need:
|
|
856
|
+
|
|
857
|
+
- `requestQueryParams`
|
|
858
|
+
- `readEnabled`
|
|
859
|
+
- `queryKeyFactory`
|
|
860
|
+
|
|
861
|
+
For example:
|
|
862
|
+
|
|
863
|
+
```js
|
|
864
|
+
const screen = useCrudViewScreen({
|
|
865
|
+
resource: drumResource,
|
|
866
|
+
apiUrlTemplate: "/drums/:recordId",
|
|
867
|
+
requestQueryParams() {
|
|
868
|
+
return {
|
|
869
|
+
include: "drumSpecId,locationId,contents,contents.processingLotId"
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
Render domain sections through `CrudViewScreen` slots instead of replacing the shared load/error/retry chrome:
|
|
876
|
+
|
|
877
|
+
```vue
|
|
878
|
+
<CrudViewScreen :screen="screen" resource-singular-title="Drum">
|
|
879
|
+
<template #before-fields="{ view }">
|
|
880
|
+
<DrumArrivalSummary :drum="view.record" />
|
|
881
|
+
</template>
|
|
882
|
+
|
|
883
|
+
<template #fields="{ view }">
|
|
884
|
+
<GeneratedDrumFields :record="view.record" />
|
|
885
|
+
</template>
|
|
886
|
+
|
|
887
|
+
<template #after-fields="{ view }">
|
|
888
|
+
<DrumProvenancePanel :drum="view.record" />
|
|
889
|
+
</template>
|
|
890
|
+
|
|
891
|
+
<template #supporting-content="{ view }">
|
|
892
|
+
<DrumContentsPanel :drum="view.record" />
|
|
893
|
+
</template>
|
|
894
|
+
</CrudViewScreen>
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
The screen still owns loading, not-found, retry, title, back/edit actions, and responsive shell structure. The page owns only the domain panels.
|
|
843
898
|
|
|
844
899
|
## Search and filters: the deep dive
|
|
845
900
|
|
|
@@ -948,6 +1003,7 @@ In JSKIT CRUD:
|
|
|
948
1003
|
- the schema defines the API contract
|
|
949
1004
|
- field definitions may also carry storage/lookup/ui metadata
|
|
950
1005
|
- the repository runtime owns computed SQL projections
|
|
1006
|
+
- internal JSON REST resources can expose SQL-selected query projections through `createJsonRestResourceScopeOptions(...)`
|
|
951
1007
|
|
|
952
1008
|
Use these rules:
|
|
953
1009
|
|
|
@@ -1018,7 +1074,31 @@ const repositoryRuntime = createCrudResourceRuntime(resource, {
|
|
|
1018
1074
|
});
|
|
1019
1075
|
```
|
|
1020
1076
|
|
|
1021
|
-
|
|
1077
|
+
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:
|
|
1078
|
+
|
|
1079
|
+
```js
|
|
1080
|
+
await addResourceIfMissing(
|
|
1081
|
+
api,
|
|
1082
|
+
"receivals",
|
|
1083
|
+
createJsonRestResourceScopeOptions(resource, {
|
|
1084
|
+
queryFields: {
|
|
1085
|
+
remainingBatchWeight: {
|
|
1086
|
+
type: "number",
|
|
1087
|
+
select({ knex, column }) {
|
|
1088
|
+
return knex.raw("?? - coalesce(??, 0)", [
|
|
1089
|
+
column("received_weight"),
|
|
1090
|
+
column("processed_weight")
|
|
1091
|
+
]);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
})
|
|
1096
|
+
);
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
`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.
|
|
1100
|
+
|
|
1101
|
+
For the repository runtime, once registered there:
|
|
1022
1102
|
|
|
1023
1103
|
- generic CRUD `list`
|
|
1024
1104
|
- generic CRUD `findById`
|
|
@@ -1121,19 +1201,106 @@ The generated runtime passes action handlers:
|
|
|
1121
1201
|
|
|
1122
1202
|
Keep bulk action definitions page-local unless another page needs to share them.
|
|
1123
1203
|
|
|
1204
|
+
### Pattern 3C: row actions and synthetic display rows
|
|
1205
|
+
|
|
1206
|
+
Generated CRUD list pages can keep the shared list screen while adding per-row actions and non-CRUD display rows.
|
|
1207
|
+
|
|
1208
|
+
Use row actions for explicit commands on one record. JSKIT renders the action menu in card and table layouts, tracks per-row execution state, and passes the handler enough context to run app-owned commands. It does not invent or auto-replay writes.
|
|
1209
|
+
|
|
1210
|
+
For example:
|
|
1211
|
+
|
|
1212
|
+
```js
|
|
1213
|
+
import { defineCrudListRowActions } from "@jskit-ai/users-web/client/rowActions";
|
|
1214
|
+
|
|
1215
|
+
const listRowActions = defineCrudListRowActions([
|
|
1216
|
+
{
|
|
1217
|
+
key: "delete",
|
|
1218
|
+
label: "Delete",
|
|
1219
|
+
color: "error",
|
|
1220
|
+
visible: ({ record }) => record.isOwnerRow !== true,
|
|
1221
|
+
disabled: ({ record }) => record.isOwnerRow === true,
|
|
1222
|
+
loading: ({ record }) => deleteCommand.isRunningFor(record.id),
|
|
1223
|
+
async run({ record, reload }) {
|
|
1224
|
+
await deleteCommand.runFor(record);
|
|
1225
|
+
await reload();
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
]);
|
|
1229
|
+
|
|
1230
|
+
export { listRowActions };
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
Pass the actions into the generated list screen:
|
|
1234
|
+
|
|
1235
|
+
```js
|
|
1236
|
+
import { listRowActions } from "./listRowActions.js";
|
|
1237
|
+
|
|
1238
|
+
const screen = useCrudListScreen({
|
|
1239
|
+
resource: uiResource,
|
|
1240
|
+
apiSuffix: "/allowed-login-emails",
|
|
1241
|
+
listRowActions
|
|
1242
|
+
});
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
The row-action handler receives:
|
|
1246
|
+
|
|
1247
|
+
- `action`
|
|
1248
|
+
- `record`
|
|
1249
|
+
- `index`
|
|
1250
|
+
- `recordId`
|
|
1251
|
+
- `records`
|
|
1252
|
+
- `reload`
|
|
1253
|
+
|
|
1254
|
+
Use `syntheticRows` when the page needs display rows that do not come from the CRUD list response, such as an owner row at the top of an allowlist. An array is prepended by default. Use `{ prepend, append }` when placement matters.
|
|
1255
|
+
|
|
1256
|
+
```js
|
|
1257
|
+
const ownerRows = computed(() => owner.value
|
|
1258
|
+
? [
|
|
1259
|
+
{
|
|
1260
|
+
key: "owner",
|
|
1261
|
+
record: {
|
|
1262
|
+
id: owner.value.id,
|
|
1263
|
+
email: owner.value.email,
|
|
1264
|
+
role: "Owner",
|
|
1265
|
+
isOwnerRow: true
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
]
|
|
1269
|
+
: []);
|
|
1270
|
+
|
|
1271
|
+
const screen = useCrudListScreen({
|
|
1272
|
+
resource: uiResource,
|
|
1273
|
+
apiSuffix: "/allowed-login-emails",
|
|
1274
|
+
listRowActions,
|
|
1275
|
+
syntheticRows: ownerRows
|
|
1276
|
+
});
|
|
1277
|
+
```
|
|
1278
|
+
|
|
1279
|
+
Synthetic rows:
|
|
1280
|
+
|
|
1281
|
+
- render in the same card/table layouts as real rows
|
|
1282
|
+
- do not get standard Open/Edit CRUD navigation
|
|
1283
|
+
- are excluded from bulk selection by default
|
|
1284
|
+
- can still participate in row-action visibility/disabled logic
|
|
1285
|
+
|
|
1286
|
+
Use this seam for display rows only. If a row should be persisted, it should come from the CRUD list response.
|
|
1287
|
+
|
|
1124
1288
|
#### Exact file checklist
|
|
1125
1289
|
|
|
1126
1290
|
For a generated CRUD, treat this as the concrete file plan:
|
|
1127
1291
|
|
|
1128
1292
|
- edit `src/pages/.../contacts/listFilters.js`
|
|
1129
|
-
-
|
|
1293
|
+
- create `src/pages/.../contacts/listRowActions.js` when the list needs row-level commands
|
|
1294
|
+
- make sure the matching server route/action/repository code accepts and applies the declared query params when filters are server-backed
|
|
1130
1295
|
|
|
1131
1296
|
If the filter contract should be shared with server code, promote it into a CRUD package module and import it from both sides:
|
|
1132
1297
|
|
|
1133
1298
|
- create `packages/contacts/src/shared/contactListFilters.js`
|
|
1134
|
-
-
|
|
1135
|
-
- update `packages/contacts/src/server/
|
|
1136
|
-
- update `packages/contacts/src/server/
|
|
1299
|
+
- create `packages/contacts/src/server/contactListFilterContract.js` with `createCrudListFilterContract(...)`
|
|
1300
|
+
- update `packages/contacts/src/server/registerRoutes.js` so the list route query validator includes `contactListFilterContract.queryValidator`
|
|
1301
|
+
- update `packages/contacts/src/server/actions.js` so the list action input validator includes the same `queryValidator`
|
|
1302
|
+
- update the provider's `createJsonRestResourceScopeOptions(...)` call so it merges `searchSchema: contactListFilterContract.jsonRestSearchSchema`
|
|
1303
|
+
- update `packages/contacts/src/server/repository.js` so the list query path passes `contactListFilterContract.toJsonRestQuery(query)` into `buildJsonRestQueryParams(...)`
|
|
1137
1304
|
|
|
1138
1305
|
#### Client side
|
|
1139
1306
|
|
|
@@ -1231,101 +1398,105 @@ Use `mode: "merge"` when a preset should only change one filter group, such as t
|
|
|
1231
1398
|
|
|
1232
1399
|
#### Server side
|
|
1233
1400
|
|
|
1234
|
-
Build the server
|
|
1401
|
+
Build the server contract from the same shared definitions:
|
|
1235
1402
|
|
|
1236
1403
|
```js
|
|
1237
|
-
|
|
1404
|
+
import { createCrudListFilterContract } from "@jskit-ai/crud-core/server/listFilters";
|
|
1405
|
+
import { CONTACTS_LIST_FILTER_DEFINITIONS } from "../shared/contactListFilters.js";
|
|
1406
|
+
|
|
1407
|
+
const contactListFilterContract = createCrudListFilterContract(
|
|
1238
1408
|
CONTACTS_LIST_FILTER_DEFINITIONS,
|
|
1239
1409
|
{
|
|
1240
1410
|
columns: {
|
|
1241
|
-
|
|
1242
|
-
|
|
1411
|
+
status: "status",
|
|
1412
|
+
supplierContactId: "supplier_contact_id",
|
|
1413
|
+
arrivalDate: "arrival_datetime",
|
|
1243
1414
|
onlyArchived: "archived"
|
|
1244
|
-
}
|
|
1415
|
+
},
|
|
1416
|
+
invalidValues: "reject"
|
|
1245
1417
|
}
|
|
1246
1418
|
);
|
|
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
1419
|
|
|
1251
|
-
|
|
1252
|
-
|
|
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
|
-
};
|
|
1420
|
+
export { contactListFilterContract };
|
|
1267
1421
|
```
|
|
1268
1422
|
|
|
1269
|
-
|
|
1423
|
+
That one contract gives the server:
|
|
1270
1424
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
invalidValues: "discard"
|
|
1276
|
-
}),
|
|
1277
|
-
arrivalDate: createCrudListFilterQueryField(CONTACTS_LIST_FILTER_DEFINITIONS.arrivalDate, {
|
|
1278
|
-
invalidValues: "discard"
|
|
1279
|
-
})
|
|
1280
|
-
}),
|
|
1281
|
-
mode: "patch"
|
|
1282
|
-
};
|
|
1283
|
-
```
|
|
1425
|
+
- `queryValidator` for route/action input validation
|
|
1426
|
+
- `jsonRestSearchSchema` for JSON REST resource registration
|
|
1427
|
+
- `toJsonRestQuery(query)` for repository query normalization
|
|
1428
|
+
- `applyQuery(...)` when a non-JSON REST repository still needs direct Knex filtering
|
|
1284
1429
|
|
|
1285
|
-
Wire the
|
|
1430
|
+
Wire the contract into route and action validators:
|
|
1286
1431
|
|
|
1287
1432
|
```js
|
|
1288
1433
|
const listRouteQueryValidator = composeSchemaDefinitions([
|
|
1289
1434
|
listCursorPaginationQueryValidator,
|
|
1290
1435
|
listSearchQueryValidator,
|
|
1291
|
-
contactsListFiltersQueryValidator,
|
|
1292
1436
|
listParentFilterQueryValidator,
|
|
1437
|
+
contactListFilterContract.queryValidator,
|
|
1293
1438
|
lookupIncludeQueryValidator
|
|
1294
1439
|
], {
|
|
1295
1440
|
mode: "patch"
|
|
1296
1441
|
});
|
|
1297
1442
|
```
|
|
1298
1443
|
|
|
1299
|
-
Use
|
|
1444
|
+
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.
|
|
1445
|
+
|
|
1446
|
+
Merge the JSON REST search schema when the provider registers the resource:
|
|
1447
|
+
|
|
1448
|
+
```js
|
|
1449
|
+
await addResourceIfMissing(
|
|
1450
|
+
api,
|
|
1451
|
+
JSON_REST_SCOPE_NAME,
|
|
1452
|
+
createJsonRestResourceScopeOptions(resource, {
|
|
1453
|
+
searchSchema: contactListFilterContract.jsonRestSearchSchema,
|
|
1454
|
+
writeSerializers: {
|
|
1455
|
+
"datetime-utc": toDatabaseDateTimeUtc
|
|
1456
|
+
}
|
|
1457
|
+
})
|
|
1458
|
+
);
|
|
1459
|
+
```
|
|
1460
|
+
|
|
1461
|
+
Normalize the list query before building JSON REST query params:
|
|
1462
|
+
|
|
1463
|
+
```js
|
|
1464
|
+
async function queryDocuments(query = {}, options = {}) {
|
|
1465
|
+
return api.resources.contacts.query(
|
|
1466
|
+
{
|
|
1467
|
+
queryParams: buildJsonRestQueryParams(
|
|
1468
|
+
JSON_REST_SCOPE_NAME,
|
|
1469
|
+
contactListFilterContract.toJsonRestQuery(query)
|
|
1470
|
+
),
|
|
1471
|
+
transaction: options?.trx || null,
|
|
1472
|
+
simplified: false
|
|
1473
|
+
},
|
|
1474
|
+
createJsonRestContext(options?.context || null)
|
|
1475
|
+
);
|
|
1476
|
+
}
|
|
1477
|
+
```
|
|
1478
|
+
|
|
1479
|
+
`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
1480
|
|
|
1301
1481
|
Choose the invalid-value contract deliberately:
|
|
1302
1482
|
|
|
1303
|
-
-
|
|
1483
|
+
- `createCrudListFilterContract(...)` defaults to `invalidValues: "reject"` for a strict server boundary
|
|
1484
|
+
- set `invalidValues` explicitly when a package is choosing a non-default validation posture
|
|
1304
1485
|
- use `invalidValues: "reject"` when malformed filter values should fail validation and produce a 400-style contract error
|
|
1305
1486
|
- use `invalidValues: "discard"` when malformed filter values should be ignored and normalization should drop them
|
|
1306
1487
|
- route query validation runs before auth, so this choice changes whether malformed unauthenticated requests fail at validation or fall through to auth
|
|
1307
1488
|
- 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
|
|
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
|
-
```
|
|
1489
|
+
- 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
1490
|
|
|
1320
1491
|
#### Best practices
|
|
1321
1492
|
|
|
1322
1493
|
- 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
1494
|
- Keep the filter keys identical all the way through: definition key, query param key, and repository meaning.
|
|
1324
|
-
- Prefer `
|
|
1495
|
+
- 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
1496
|
- 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(...)`
|
|
1497
|
+
- Use `createCrudListFilters(...)` directly only for non-JSON REST repository code that needs direct Knex filtering without JSON REST registration.
|
|
1327
1498
|
- Use `q` for free-text and explicit query params for structured filters.
|
|
1328
|
-
- Run `jskit doctor` after wiring filters.
|
|
1499
|
+
- Run `jskit doctor` after wiring filters.
|
|
1329
1500
|
|
|
1330
1501
|
### Pattern 4: lookup-backed structured filters
|
|
1331
1502
|
|
|
@@ -1456,7 +1627,7 @@ The runtime already handles both together.
|
|
|
1456
1627
|
|
|
1457
1628
|
#### Server side
|
|
1458
1629
|
|
|
1459
|
-
Let the generic list search handle `q`, and let
|
|
1630
|
+
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
1631
|
|
|
1461
1632
|
#### Best practices
|
|
1462
1633
|
|
|
@@ -1612,8 +1783,10 @@ Use `scaffold-field` when it fits, then review the generated result. It patches
|
|
|
1612
1783
|
Touch:
|
|
1613
1784
|
|
|
1614
1785
|
- `packages/<crud>/src/shared/<crud>ListFilters.js` and make it the only authored filter-definition module
|
|
1615
|
-
- `packages/<crud>/src/server
|
|
1616
|
-
- `packages/<crud>/src/server/
|
|
1786
|
+
- `packages/<crud>/src/server/<crud>ListFilterContract.js` with `createCrudListFilterContract(...)`
|
|
1787
|
+
- `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
|
|
1788
|
+
- the provider registration so `createJsonRestResourceScopeOptions(resource, { searchSchema: <crud>ListFilterContract.jsonRestSearchSchema })` merges the JSON REST search schema
|
|
1789
|
+
- `packages/<crud>/src/server/repository.js` so the list query calls `<crud>ListFilterContract.toJsonRestQuery(query)` before `buildJsonRestQueryParams(...)`
|
|
1617
1790
|
- the app-owned list page or list-runtime composable that calls `useCrudList(...)`
|
|
1618
1791
|
|
|
1619
1792
|
If the filter is lookup-backed, touch that same client file again to wire `useCrudListFilterLookups(...)`.
|
package/package.json
CHANGED
package/patterns/INDEX.md
CHANGED
|
@@ -30,7 +30,7 @@ How to use it:
|
|
|
30
30
|
- `client-requests.md`
|
|
31
31
|
- playwright, browser test, e2e, ui verification, authenticated ui test, test auth, dev login as, dev auth bypass
|
|
32
32
|
- `ui-testing.md`
|
|
33
|
-
- generated UI contract, design contract, navigation roles, density, placeholder copy, card shells, shared CRUD screens
|
|
33
|
+
- generated UI contract, design contract, navigation roles, density, placeholder copy, card shells, shared CRUD screens, row actions, synthetic rows, detail slots
|
|
34
34
|
- `generated-ui-contract-tracking.md`
|
|
35
35
|
- filter, filters, search facets, chips, date range, enum filter, lookup filter, `useCrudListFilters`, `createCrudListFilters`
|
|
36
36
|
- `filters.md`
|
|
@@ -44,6 +44,14 @@ Use the shared CRUD screen wrappers when the route is a generated CRUD page:
|
|
|
44
44
|
- `useCrudViewScreen()` plus `CrudViewScreen` for record view route pages
|
|
45
45
|
- `useCrudAddEditScreen()` plus `CrudAddEditScreen` for generated new/edit route pages
|
|
46
46
|
|
|
47
|
+
Generated screen wrapper extension rules:
|
|
48
|
+
|
|
49
|
+
- Use `useCrudViewScreen({ requestQueryParams })` for detail includes instead of putting query strings in `apiUrlTemplate`.
|
|
50
|
+
- Use `readEnabled` and `queryKeyFactory` on `useCrudViewScreen()` when the detail read needs the same gating or cache identity control as `useCrudView()`.
|
|
51
|
+
- Use `CrudViewScreen` slots (`before-fields`, `fields`, `after-fields`, `supporting-content`) for page-specific domain sections while keeping shared load/error/retry chrome.
|
|
52
|
+
- Use `listRowActions` with `defineCrudListRowActions(...)` for row-level commands in `CrudListScreen`.
|
|
53
|
+
- Use `syntheticRows` for display-only rows that do not come from the CRUD response, such as owner/master rows.
|
|
54
|
+
|
|
47
55
|
Use the CRUD wrappers when they fit:
|
|
48
56
|
|
|
49
57
|
- `useCrudList()` for routed CRUD lists
|
|
@@ -86,5 +94,6 @@ Avoid:
|
|
|
86
94
|
- manually concatenating scoped route params into API URLs
|
|
87
95
|
- using a lower-level seam when a higher-level routed CRUD or command runtime already fits
|
|
88
96
|
- smuggling query params into `apiUrlTemplate`
|
|
97
|
+
- replacing `CrudListScreen` or `CrudViewScreen` only to add row actions, synthetic display rows, includes, or domain detail sections
|
|
89
98
|
- reporting routine resource load errors through hand-written global snackbar/banner calls
|
|
90
99
|
- calling `useShellRequestRecoveryRuntime().report(...)` from normal panels just to recover an HTTP read
|
|
@@ -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.
|
|
16
|
-
8.
|
|
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
|
|
@@ -29,7 +29,8 @@ Rules:
|
|
|
29
29
|
- Generated CRUD list screens need real loading, empty, and error states. Empty copy should name the resource, such as "No customers yet", and offer the create action when available.
|
|
30
30
|
- Generated CRUD view/new/edit screens should use page headers plus direct sheet panels. Do not use generic card shells as the page architecture.
|
|
31
31
|
- Compact CRUD actions should be reachable without a drawer. Use a mobile-visible primary action or FAB for create flows.
|
|
32
|
-
- Row actions should
|
|
32
|
+
- Row actions should be declared with `defineCrudListRowActions(...)` in a page-local `listRowActions.js` and passed into `useCrudListScreen(...)`; the shared list screen owns the compact/wide action rendering.
|
|
33
|
+
- Use `syntheticRows` for display-only owner/master rows that should appear inside the shared list layout without becoming CRUD records.
|
|
33
34
|
- Bulk actions should be declared in the generated page-local `listBulkActions.js`. The generated list owns selection state, keeps selection controls hidden until actions exist, and exposes selected ids/records to action handlers.
|
|
34
35
|
- Structured filters should use shared filter definitions and collapse to compact filter controls/sheets when they outgrow simple search. Do not stack dense desktop filter bars on phone widths.
|
|
35
36
|
- Use `--navigation-role` for CRUD list placement intent. Main resources can stay `primary`; nested/detail/workflow CRUD routes should usually be `secondary`, `workflow`, or `none`.
|
package/patterns/filters.md
CHANGED
|
@@ -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
|
|
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
|
|
41
|
-
3.
|
|
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
|
-
-
|
|
46
|
-
- update `packages/<crud>/src/server/
|
|
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
|
-
-
|
|
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
|
|
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 `
|
|
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
|
-
-
|
|
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 `
|
|
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
|
-
- `
|
|
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
|
|
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,8 +55,11 @@ 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`.
|
|
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
|
+
- CRUD row actions are client-side by default: generated list pages can create a page-local `listRowActions.js` with `defineCrudListRowActions(...)` and pass it into `useCrudListScreen(...)`; the shared list screen renders per-row actions in card and table layouts while action handlers stay explicit and page-owned.
|
|
61
|
+
- CRUD synthetic rows are display-only: pass `syntheticRows` into `useCrudListScreen(...)` for owner/master rows that are not repository records. Synthetic rows render through the shared list screen, skip standard Open/Edit links, and are excluded from bulk selection unless explicitly marked selectable.
|
|
62
|
+
- 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, bulk actions, row actions, and detail slots do not drift across generated pages.
|
|
63
|
+
- Generated CRUD detail pages should use `useCrudViewScreen({ requestQueryParams, readEnabled, queryKeyFactory })` for read pass-throughs and `CrudViewScreen` slots (`before-fields`, `fields`, `after-fields`, `supporting-content`) for domain sections instead of replacing the shared detail chrome.
|
|
61
64
|
- Routine resource-load errors stay local to the generated screen and retry affordance. Action feedback uses the shell error policy through `action-feedback`.
|
|
62
65
|
- `page.supporting-content` is a semantic supporting region in the default shell. Compact layout renders it as a closed bottom sheet; medium/expanded layouts render it as a closed side panel.
|
|
@@ -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.
|
|
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
|
|
@@ -66,6 +66,14 @@ Local functions
|
|
|
66
66
|
- `clearChip(chip = {})`
|
|
67
67
|
- `clearFilters()`
|
|
68
68
|
|
|
69
|
+
### `src/client/components/CrudListRecordActionMenu.vue`
|
|
70
|
+
Exports
|
|
71
|
+
- None
|
|
72
|
+
Local functions
|
|
73
|
+
- `isRowActionLoading(action = {})`
|
|
74
|
+
- `isRowActionDisabled(action = {})`
|
|
75
|
+
- `runRowAction(action = {})`
|
|
76
|
+
|
|
69
77
|
### `src/client/components/CrudListScreen.vue`
|
|
70
78
|
Exports
|
|
71
79
|
- None
|
|
@@ -74,6 +82,12 @@ Local functions
|
|
|
74
82
|
- `formatListCardValue(value)`
|
|
75
83
|
- `resolveViewLocation(record)`
|
|
76
84
|
- `resolveEditLocation(record)`
|
|
85
|
+
- `hasRowActionsFor(record, index)`
|
|
86
|
+
- `isBulkRowSelected(row = {})`
|
|
87
|
+
- `setBulkRowSelected(row = {}, selected = true)`
|
|
88
|
+
- `allSelectableRowsSelected()`
|
|
89
|
+
- `someSelectableRowsSelected()`
|
|
90
|
+
- `setSelectableRowsSelected(selected = true)`
|
|
77
91
|
|
|
78
92
|
### `src/client/components/CrudViewScreen.vue`
|
|
79
93
|
Exports
|
|
@@ -484,15 +498,30 @@ Exports
|
|
|
484
498
|
Local functions
|
|
485
499
|
- `normalizeQueryKeyPrefix(value = [])`
|
|
486
500
|
|
|
501
|
+
### `src/client/composables/useCrudListRowActions.js`
|
|
502
|
+
Exports
|
|
503
|
+
- `useCrudListRowActions(actions = [], { resolveRecordId = null, resolveContext = null } = {})`
|
|
504
|
+
Local functions
|
|
505
|
+
- `normalizeRowActionKey(actionOrKey = "")`
|
|
506
|
+
- `normalizeRowId(value = "")`
|
|
507
|
+
- `createExecutionKey(actionKey = "", recordId = "", index = 0)`
|
|
508
|
+
- `asContextObject(value = null)`
|
|
509
|
+
|
|
487
510
|
### `src/client/composables/useCrudListScreen.js`
|
|
488
511
|
Exports
|
|
489
|
-
- `useCrudListScreen({ adapter = null, resource = null, resourceNamespace = "resource", apiSuffix = "", recordIdParam = "recordId", recordIdSelector = null, titleFallbackFieldKey = "", viewUrlTemplate = "", editUrlTemplate = "", newUrlTemplate = "", recordChangedEvents = [], listFilters = {}, listBulkActions = [], routeQueryBlacklist = Object.freeze(["include", "cursor", "limit"]), requestRecoveryLabel = "Records", fallbackLoadError = "Unable to load records." } = {})`
|
|
512
|
+
- `useCrudListScreen({ adapter = null, resource = null, resourceNamespace = "resource", apiSuffix = "", recordIdParam = "recordId", recordIdSelector = null, titleFallbackFieldKey = "", viewUrlTemplate = "", editUrlTemplate = "", newUrlTemplate = "", recordChangedEvents = [], listFilters = {}, listBulkActions = [], listRowActions = [], syntheticRows = null, routeQueryBlacklist = Object.freeze(["include", "cursor", "limit"]), requestQueryParams = null, requestRecoveryLabel = "Records", fallbackLoadError = "Unable to load records." } = {})`
|
|
490
513
|
Local functions
|
|
491
514
|
- `formatCrudListCardValue(value)`
|
|
515
|
+
- `asList(value = [])`
|
|
516
|
+
- `hasSyntheticRowGroups(value = null)`
|
|
517
|
+
- `normalizeSyntheticDisplayRow(source = null, index = 0, placement = "prepend")`
|
|
518
|
+
- `normalizeSyntheticDisplayRows(value = [], placement = "prepend")`
|
|
519
|
+
- `normalizeSyntheticDisplayRowGroups(syntheticRows = null)`
|
|
520
|
+
- `createCrudListDisplayRow(record = {}, index = 0, records = {})`
|
|
492
521
|
|
|
493
522
|
### `src/client/composables/useCrudViewScreen.js`
|
|
494
523
|
Exports
|
|
495
|
-
- `useCrudViewScreen({ adapter = null, resource = null, resourceNamespace = "resource", apiUrlTemplate = "", recordIdParam = "recordId", titleFallbackFieldKey = "", listUrlTemplate = "", editUrlTemplate = "", recordChangedEvent = "", requestRecoveryLabel = "Record", fallbackLoadError = "Unable to load record.", notFoundMessage = "Record not found." } = {})`
|
|
524
|
+
- `useCrudViewScreen({ adapter = null, resource = null, resourceNamespace = "resource", apiUrlTemplate = "", recordIdParam = "recordId", titleFallbackFieldKey = "", listUrlTemplate = "", editUrlTemplate = "", recordChangedEvent = "", requestQueryParams = null, readEnabled = true, queryKeyFactory = null, requestRecoveryLabel = "Record", fallbackLoadError = "Unable to load record.", notFoundMessage = "Record not found." } = {})`
|
|
496
525
|
|
|
497
526
|
### `src/client/composables/usePagedCollection.js`
|
|
498
527
|
Exports
|
|
@@ -605,6 +634,12 @@ Local functions
|
|
|
605
634
|
Exports
|
|
606
635
|
- `UsersWebClientProvider`
|
|
607
636
|
|
|
637
|
+
### `src/client/rowActions.js`
|
|
638
|
+
Exports
|
|
639
|
+
- `defineCrudListRowActions(actions = [])`
|
|
640
|
+
Local functions
|
|
641
|
+
- `normalizeCrudListRowAction(rawAction = {}, index = 0)`
|
|
642
|
+
|
|
608
643
|
### `src/client/support/contractGuards.js`
|
|
609
644
|
Exports
|
|
610
645
|
- `requireRecord(value, label = "value", owner = "Contract")`
|