@jskit-ai/agent-docs 0.1.15 → 0.1.17
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/app-extras/assistant.md +2 -2
- package/guide/agent/app-setup/initial-scaffolding.md +1 -3
- package/guide/agent/app-setup/multi-homing.md +21 -0
- package/guide/agent/generators/advanced-cruds.md +208 -110
- package/guide/agent/generators/crud-generators.md +4 -0
- package/guide/agent/index.md +1 -1
- package/package.json +1 -1
- package/patterns/INDEX.md +1 -1
- package/patterns/client-requests.md +3 -0
- package/patterns/crud-repository-mapping.md +45 -23
- package/patterns/filters.md +8 -8
- package/reference/autogen/KERNEL_MAP.md +94 -40
- package/reference/autogen/README.md +3 -0
- package/reference/autogen/packages/assistant-core.md +0 -12
- package/reference/autogen/packages/assistant-runtime.md +11 -3
- package/reference/autogen/packages/auth-core.md +34 -23
- package/reference/autogen/packages/auth-provider-supabase-core.md +4 -7
- package/reference/autogen/packages/auth-web.md +3 -0
- package/reference/autogen/packages/console-core.md +6 -29
- package/reference/autogen/packages/crud-core.md +35 -28
- package/reference/autogen/packages/crud-server-generator.md +28 -50
- package/reference/autogen/packages/crud-ui-generator.md +12 -7
- package/reference/autogen/packages/http-runtime.md +171 -21
- package/reference/autogen/packages/json-rest-api-core.md +54 -0
- package/reference/autogen/packages/kernel.md +118 -55
- package/reference/autogen/packages/realtime.md +1 -0
- package/reference/autogen/packages/resource-core.md +35 -0
- package/reference/autogen/packages/resource-crud-core.md +51 -0
- package/reference/autogen/packages/shell-web.md +31 -0
- package/reference/autogen/packages/users-core.md +34 -77
- package/reference/autogen/packages/users-web.md +29 -20
- package/reference/autogen/packages/workspaces-core.md +44 -76
- package/reference/autogen/packages/workspaces-web.md +1 -3
- package/reference/autogen/tooling/jskit-cli.md +5 -4
- package/workflow/feature-delivery.md +2 -1
|
@@ -181,7 +181,7 @@ For example:
|
|
|
181
181
|
require: "all",
|
|
182
182
|
permissions: ["workspace.members.invite"]
|
|
183
183
|
},
|
|
184
|
-
|
|
184
|
+
input: {
|
|
185
185
|
schema: {
|
|
186
186
|
type: "object",
|
|
187
187
|
properties: {
|
|
@@ -191,7 +191,7 @@ For example:
|
|
|
191
191
|
additionalProperties: false
|
|
192
192
|
}
|
|
193
193
|
},
|
|
194
|
-
|
|
194
|
+
output: {
|
|
195
195
|
schema: {
|
|
196
196
|
type: "object",
|
|
197
197
|
properties: {
|
|
@@ -576,8 +576,6 @@ The core of that startup path looks like this:
|
|
|
576
576
|
```js
|
|
577
577
|
async function createServer() {
|
|
578
578
|
const app = Fastify({ logger: true });
|
|
579
|
-
registerTypeBoxFormats();
|
|
580
|
-
app.setValidatorCompiler(TypeBoxValidatorCompiler);
|
|
581
579
|
|
|
582
580
|
app.get("/api/health", async () => {
|
|
583
581
|
return {
|
|
@@ -610,7 +608,7 @@ async function createServer() {
|
|
|
610
608
|
}
|
|
611
609
|
```
|
|
612
610
|
|
|
613
|
-
The health route is built in, but the more important idea is that the server is already prepared to validate HTTP input, load the JSKIT provider runtime from the app itself, and constrain requests by surface.
|
|
611
|
+
The health route is built in, but the more important idea is that the server is already prepared to validate HTTP input with Fastify's normal JSON Schema path, load the JSKIT provider runtime from the app itself, and constrain requests by surface.
|
|
614
612
|
|
|
615
613
|
You will also notice `config/server.js`. In the base shell it is intentionally almost empty. It is there to reserve a clear place for server-side configuration as backend features are added, without pretending the starter app already has server behavior it does not yet need.
|
|
616
614
|
|
|
@@ -612,6 +612,27 @@ For this chapter's `tenancyMode = "personal"` setup, that contributor now does r
|
|
|
612
612
|
|
|
613
613
|
So even in this chapter, the package boundary is already correct: users owns user creation/sync, and workspaces reacts through an extension point.
|
|
614
614
|
|
|
615
|
+
### Workspace auth context comes from the auth policy resolver registry
|
|
616
|
+
|
|
617
|
+
There is a second registry seam in this chapter that matters just as much for the runtime model.
|
|
618
|
+
|
|
619
|
+
Workspace-aware routes do not hard-code workspace lookup inside the auth plugin itself. Instead, `workspaces-core` registers an auth policy context resolver contribution, and `auth-core` composes the registered resolvers at request time.
|
|
620
|
+
|
|
621
|
+
The important consequence is:
|
|
622
|
+
|
|
623
|
+
- `auth-core` stays generic
|
|
624
|
+
- `workspaces-core` contributes workspace-specific context
|
|
625
|
+
- the request only resolves workspace membership and permissions when a route actually asks for auth context or permissions
|
|
626
|
+
|
|
627
|
+
So the flow is:
|
|
628
|
+
|
|
629
|
+
1. a workspace-aware route declares auth and, when needed, permission requirements
|
|
630
|
+
2. the auth policy runtime authenticates the actor
|
|
631
|
+
3. the composed auth policy context resolver asks the workspace layer for the current workspace context
|
|
632
|
+
4. the request gains normalized `workspace`, `membership`, and `permissions` values
|
|
633
|
+
|
|
634
|
+
That is why the workspace package does not need to patch the auth plugin directly, and it is also why this setup scales better than a one-off global hook. It uses the same tagged-registry pattern as other JSKIT extension seams, but for request-time auth context instead of bootstrap payloads or lifecycle events.
|
|
635
|
+
|
|
615
636
|
### Why this chapter is the real routing pivot
|
|
616
637
|
|
|
617
638
|
Earlier chapters added features inside a flat top-level app.
|
|
@@ -211,9 +211,7 @@ packages/contacts/
|
|
|
211
211
|
package.json
|
|
212
212
|
package.descriptor.mjs
|
|
213
213
|
src/server/ContactsProvider.js
|
|
214
|
-
src/server/actionIds.js
|
|
215
214
|
src/server/actions.js
|
|
216
|
-
src/server/listConfig.js
|
|
217
215
|
src/server/registerRoutes.js
|
|
218
216
|
src/server/repository.js
|
|
219
217
|
src/server/service.js
|
|
@@ -265,15 +263,61 @@ If you come from an ORM stack, this is the key adjustment:
|
|
|
265
263
|
The resource file owns:
|
|
266
264
|
|
|
267
265
|
- the resource name and table name
|
|
268
|
-
- the
|
|
269
|
-
-
|
|
270
|
-
-
|
|
266
|
+
- the canonical `schema`
|
|
267
|
+
- `searchSchema`
|
|
268
|
+
- `defaultSort`
|
|
269
|
+
- `autofilter`
|
|
271
270
|
- lookup contract configuration
|
|
272
|
-
- messages
|
|
273
|
-
- field metadata
|
|
271
|
+
- messages
|
|
272
|
+
- field metadata, including which fields participate in `output`, `create`, `replace`, and `patch`
|
|
274
273
|
|
|
275
274
|
This file is the bridge between the server and the client. The UI generator reads it, and the server runtime also depends on it.
|
|
276
275
|
|
|
276
|
+
For a standard CRUD resource like `contacts`, the authored file is intentionally compact. It uses `defineCrudResource(...)` from `@jskit-ai/resource-crud-core`:
|
|
277
|
+
|
|
278
|
+
```js
|
|
279
|
+
import { defineCrudResource } from "@jskit-ai/resource-crud-core/shared/crudResource";
|
|
280
|
+
|
|
281
|
+
const resource = defineCrudResource({
|
|
282
|
+
namespace: "contacts",
|
|
283
|
+
tableName: "contacts",
|
|
284
|
+
schema: {
|
|
285
|
+
name: {
|
|
286
|
+
type: "string",
|
|
287
|
+
maxLength: 190,
|
|
288
|
+
required: true,
|
|
289
|
+
search: true,
|
|
290
|
+
operations: {
|
|
291
|
+
output: { required: true },
|
|
292
|
+
create: { required: true },
|
|
293
|
+
patch: { required: false }
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
searchSchema: {
|
|
298
|
+
id: { type: "id", actualField: "id" }
|
|
299
|
+
},
|
|
300
|
+
defaultSort: ["-createdAt"],
|
|
301
|
+
autofilter: "workspace",
|
|
302
|
+
messages: {
|
|
303
|
+
saveSuccess: "Record saved."
|
|
304
|
+
},
|
|
305
|
+
contract: {
|
|
306
|
+
lookup: {
|
|
307
|
+
containerKey: "lookups"
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
`defineCrudResource(...)` derives the standard CRUD operation contracts once at module load time and exposes them on `resource.operations`. That means:
|
|
314
|
+
|
|
315
|
+
- you author the canonical resource shape once
|
|
316
|
+
- JSKIT derives the standard `list` / `view` / `create` / `replace` / `patch` / `delete` contracts
|
|
317
|
+
- routes, actions, client code, and generators can keep reading `resource.operations.*` without each resource file repeating that boilerplate
|
|
318
|
+
|
|
319
|
+
For non-CRUD or heavily custom resources, use `defineResource(...)` from `@jskit-ai/resource-core` instead. That keeps standard CRUD derivation and custom operation bundles clearly separated.
|
|
320
|
+
|
|
277
321
|
### `src/shared/index.js`
|
|
278
322
|
|
|
279
323
|
This is just the shared package barrel. It re-exports the resource contract and shared symbols.
|
|
@@ -292,22 +336,6 @@ It owns:
|
|
|
292
336
|
|
|
293
337
|
It is wiring, not business logic. If you need to change how contacts are validated or saved, this is usually **not** the file to edit first.
|
|
294
338
|
|
|
295
|
-
### `src/server/actionIds.js`
|
|
296
|
-
|
|
297
|
-
This is the stable list of action ids:
|
|
298
|
-
|
|
299
|
-
```js
|
|
300
|
-
const actionIds = Object.freeze({
|
|
301
|
-
list: "crud.contacts.list",
|
|
302
|
-
view: "crud.contacts.view",
|
|
303
|
-
create: "crud.contacts.create",
|
|
304
|
-
update: "crud.contacts.update",
|
|
305
|
-
delete: "crud.contacts.delete"
|
|
306
|
-
});
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
Keep this file boring. It is identity, not behavior.
|
|
310
|
-
|
|
311
339
|
### `src/server/actions.js`
|
|
312
340
|
|
|
313
341
|
This is the action contract boundary.
|
|
@@ -344,33 +372,6 @@ In other words:
|
|
|
344
372
|
|
|
345
373
|
Those are related, but not the same concern.
|
|
346
374
|
|
|
347
|
-
### `src/server/listConfig.js`
|
|
348
|
-
|
|
349
|
-
This is the baseline list behavior for the repository.
|
|
350
|
-
|
|
351
|
-
It owns things like:
|
|
352
|
-
|
|
353
|
-
- `defaultLimit`
|
|
354
|
-
- `maxLimit`
|
|
355
|
-
- `orderBy`
|
|
356
|
-
- `searchColumns`
|
|
357
|
-
|
|
358
|
-
One subtle but important detail: `searchColumns` are **database column names**, not camelCase resource field keys.
|
|
359
|
-
|
|
360
|
-
Example:
|
|
361
|
-
|
|
362
|
-
```js
|
|
363
|
-
const LIST_CONFIG = Object.freeze({
|
|
364
|
-
searchColumns: ["full_name", "email", "phone"],
|
|
365
|
-
orderBy: [
|
|
366
|
-
{
|
|
367
|
-
column: "created_at",
|
|
368
|
-
direction: "desc"
|
|
369
|
-
}
|
|
370
|
-
]
|
|
371
|
-
});
|
|
372
|
-
```
|
|
373
|
-
|
|
374
375
|
### `src/server/repository.js`
|
|
375
376
|
|
|
376
377
|
This is the data-access layer.
|
|
@@ -656,6 +657,44 @@ const paths = usePaths();
|
|
|
656
657
|
const reportsApiPath = computed(() => paths.api("/reports"));
|
|
657
658
|
```
|
|
658
659
|
|
|
660
|
+
Use `requestQueryParams` when a runtime needs endpoint query parameters. Do not put query strings into `apiUrlTemplate`; URL templates are for path shape only.
|
|
661
|
+
|
|
662
|
+
For routed CRUD edit forms, pass request query params through `addEditOptions`:
|
|
663
|
+
|
|
664
|
+
```js
|
|
665
|
+
const formRuntime = useCrudAddEdit({
|
|
666
|
+
resource: uiResource,
|
|
667
|
+
operationName: "patch",
|
|
668
|
+
formFields: UI_EDIT_FORM_FIELDS,
|
|
669
|
+
addEditOptions: {
|
|
670
|
+
apiUrlTemplate: "/products/:productId",
|
|
671
|
+
requestQueryParams: {
|
|
672
|
+
include: "serviceId,bookingSteps,bookingSteps.requiredRoleId"
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
`requestQueryParams` may also be a callback. The add/edit callback receives the same scoped request context used by view/list, plus the current record id and model:
|
|
679
|
+
|
|
680
|
+
```js
|
|
681
|
+
const formRuntime = useCrudAddEdit({
|
|
682
|
+
resource: uiResource,
|
|
683
|
+
operationName: "patch",
|
|
684
|
+
formFields: UI_EDIT_FORM_FIELDS,
|
|
685
|
+
addEditOptions: {
|
|
686
|
+
apiUrlTemplate: "/products/:productId",
|
|
687
|
+
requestQueryParams({ recordId, model }) {
|
|
688
|
+
return {
|
|
689
|
+
include: "serviceId,bookingSteps,bookingSteps.requiredRoleId"
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
For add/edit runtimes, these request query params apply to both the initial load and the save request path. That keeps the saved response shape aligned with the loaded form shape when an endpoint supports response includes.
|
|
697
|
+
|
|
659
698
|
The safe mental model is:
|
|
660
699
|
|
|
661
700
|
- do not raw `fetch(...)` for normal app work
|
|
@@ -663,6 +702,7 @@ The safe mental model is:
|
|
|
663
702
|
- use the operation/runtime composable that matches the UI interaction
|
|
664
703
|
- drop to `usersWebHttpClient.request(...)` only for exceptional low-level cases
|
|
665
704
|
- use `usePaths().api(...)` when you need a custom scoped API path and the higher-level runtime does not already resolve it for you
|
|
705
|
+
- keep `apiUrlTemplate` path-only and put endpoint query strings in `requestQueryParams`
|
|
666
706
|
|
|
667
707
|
### `_components/CrudAddEditForm.vue`
|
|
668
708
|
|
|
@@ -703,7 +743,7 @@ A generated CRUD works because several layers cooperate:
|
|
|
703
743
|
4. the route executes an action from `actions.js`
|
|
704
744
|
5. the action delegates to the service in `service.js`
|
|
705
745
|
6. the service calls the repository in `repository.js`
|
|
706
|
-
7. the repository uses `crud-core` helpers and the
|
|
746
|
+
7. the repository uses the shared resource contract, `crud-core` helpers, and the internal JSON:API host to talk to the database
|
|
707
747
|
8. the response comes back through validators and is rendered by the page
|
|
708
748
|
|
|
709
749
|
That is why the generated route files are mostly containers: they are the outermost layer of a larger pipeline.
|
|
@@ -714,10 +754,10 @@ Use this rule of thumb when deciding where to edit:
|
|
|
714
754
|
|
|
715
755
|
| Need | Primary owner | Why |
|
|
716
756
|
| --- | --- | --- |
|
|
717
|
-
| Change API/input/output contract | `contactResource.js`
|
|
757
|
+
| Change API/input/output contract | `contactResource.js` first, then `actions.js` only if the action boundary must diverge | Standard CRUD validators are derived from the resource; `actions.js` owns channels, permissions, and any transport-specific input composition |
|
|
718
758
|
| Change route path or HTTP transport | `registerRoutes.js` | This is the HTTP layer |
|
|
719
759
|
| Change permissions or channels | `actions.js` | This is the action contract boundary |
|
|
720
|
-
| Change default ordering,
|
|
760
|
+
| Change default ordering, searchable fields, or ownership autofilter | `contactResource.js` | The shared resource is the single source of truth for both CRUD contract and internal JSON:API resource config |
|
|
721
761
|
| Change SQL, joins, parent filters, or advanced search | `repository.js` | This is the data-access layer |
|
|
722
762
|
| Add cross-record or domain rules on save/delete | `service.js` | This is business logic |
|
|
723
763
|
| Change page layout and display behavior | the route pages and app-owned composables | This is presentation |
|
|
@@ -788,8 +828,8 @@ The client runtime debounces the search input, writes the query string to `q`, a
|
|
|
788
828
|
The generic CRUD stack already understands `q`.
|
|
789
829
|
|
|
790
830
|
- `listSearchQueryValidator` reads and normalizes the `q` query param
|
|
791
|
-
- the repository applies search
|
|
792
|
-
-
|
|
831
|
+
- the repository applies search through `resource.searchSchema.q`
|
|
832
|
+
- the generated CRUD starts with an explicit `q` search definition in `contactResource.js`, so you edit that definition directly when you want to narrow or expand the search surface
|
|
793
833
|
|
|
794
834
|
For the tutorial `contacts` table, that usually means the columns behind:
|
|
795
835
|
|
|
@@ -800,27 +840,31 @@ For the tutorial `contacts` table, that usually means the columns behind:
|
|
|
800
840
|
|
|
801
841
|
#### Best practices
|
|
802
842
|
|
|
803
|
-
- Once the UX is stable, set
|
|
804
|
-
- Keep search focused on the
|
|
805
|
-
- Remember that
|
|
843
|
+
- Once the UX is stable, edit `contactResource.js` and set `resource.searchSchema.q.oneOf` explicitly instead of relying on the initial generated defaults.
|
|
844
|
+
- Keep search focused on the fields users actually expect.
|
|
845
|
+
- Remember that these are JSON:API/internal resource search keys, not a free-form UI concern.
|
|
806
846
|
|
|
807
|
-
### Pattern 2: explicit `contacts` search
|
|
847
|
+
### Pattern 2: explicit `contacts` search fields
|
|
808
848
|
|
|
809
|
-
This is the first thing to do when the
|
|
849
|
+
This is the first thing to do when the generated `q` search becomes too broad or too accidental.
|
|
810
850
|
|
|
811
851
|
#### Server side
|
|
812
852
|
|
|
813
|
-
Set the searchable
|
|
853
|
+
Set the searchable fields explicitly in `contactResource.js`:
|
|
814
854
|
|
|
815
855
|
```js
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
{
|
|
820
|
-
|
|
821
|
-
|
|
856
|
+
const resource = Object.freeze({
|
|
857
|
+
searchSchema: {
|
|
858
|
+
id: { type: "id", actualField: "id" },
|
|
859
|
+
q: {
|
|
860
|
+
type: "string",
|
|
861
|
+
oneOf: ["fullName", "email", "phone"],
|
|
862
|
+
filterOperator: "like",
|
|
863
|
+
splitBy: " ",
|
|
864
|
+
matchAll: true
|
|
822
865
|
}
|
|
823
|
-
|
|
866
|
+
},
|
|
867
|
+
defaultSort: ["-createdAt"]
|
|
824
868
|
});
|
|
825
869
|
```
|
|
826
870
|
|
|
@@ -841,30 +885,58 @@ Do not treat the output schema as if it also defined database storage.
|
|
|
841
885
|
In JSKIT CRUD:
|
|
842
886
|
|
|
843
887
|
- the schema defines the API contract
|
|
844
|
-
-
|
|
888
|
+
- field definitions may also carry storage/lookup/ui metadata
|
|
845
889
|
- the repository runtime owns computed SQL projections
|
|
846
890
|
|
|
847
891
|
Use these rules:
|
|
848
892
|
|
|
849
|
-
- for explicit DB column overrides, use `
|
|
850
|
-
-
|
|
893
|
+
- for explicit DB column overrides, use `actualField`
|
|
894
|
+
- standard writable `date-time` fields are serialized automatically during CRUD writes
|
|
895
|
+
- use `storage.writeSerializer` only for non-default DB write serialization
|
|
896
|
+
- for computed output fields, use `storage: { virtual: true }`
|
|
851
897
|
- do not put computed fields in create/patch write schemas
|
|
852
898
|
|
|
853
899
|
Example field metadata:
|
|
854
900
|
|
|
855
901
|
```js
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
902
|
+
import { defineCrudResource } from "@jskit-ai/resource-crud-core/shared/crudResource";
|
|
903
|
+
|
|
904
|
+
const resource = defineCrudResource({
|
|
905
|
+
namespace: "receivals",
|
|
906
|
+
tableName: "receivals",
|
|
907
|
+
schema: {
|
|
908
|
+
createdAt: {
|
|
909
|
+
type: "dateTime",
|
|
910
|
+
required: true,
|
|
911
|
+
actualField: "created_at",
|
|
912
|
+
operations: {
|
|
913
|
+
output: { required: true }
|
|
914
|
+
}
|
|
915
|
+
},
|
|
916
|
+
arrivalDatetime: {
|
|
917
|
+
type: "dateTime",
|
|
918
|
+
required: true,
|
|
919
|
+
storage: {
|
|
920
|
+
writeSerializer: "datetime-utc"
|
|
921
|
+
},
|
|
922
|
+
operations: {
|
|
923
|
+
output: { required: true },
|
|
924
|
+
create: { required: true },
|
|
925
|
+
patch: { required: false }
|
|
926
|
+
}
|
|
927
|
+
},
|
|
928
|
+
remainingBatchWeight: {
|
|
929
|
+
type: "number",
|
|
930
|
+
required: true,
|
|
931
|
+
storage: {
|
|
932
|
+
virtual: true
|
|
933
|
+
},
|
|
934
|
+
operations: {
|
|
935
|
+
output: { required: true }
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
crudOperations: ["list", "view", "create", "patch"]
|
|
868
940
|
});
|
|
869
941
|
```
|
|
870
942
|
|
|
@@ -892,6 +964,8 @@ Once registered there:
|
|
|
892
964
|
- generic CRUD `listByIds`
|
|
893
965
|
- generic CRUD `listByForeignIds`
|
|
894
966
|
|
|
967
|
+
and generic CRUD writes automatically serialize standard writable `date-time` fields during create/update payload mapping, so normal datetime DB formatting does not need per-field metadata or repository-specific `preparePayload` hooks. Keep `storage.writeSerializer` for non-default cases only.
|
|
968
|
+
|
|
895
969
|
all pick up the projection automatically, so you should not hand-patch `clearSelect()` / re-select logic into each method.
|
|
896
970
|
|
|
897
971
|
Important limits:
|
|
@@ -917,7 +991,7 @@ Instead:
|
|
|
917
991
|
|
|
918
992
|
1. define the filters once in a shared CRUD-package module
|
|
919
993
|
2. build the server runtime from that definition with `createCrudListFilters(...)`
|
|
920
|
-
3. choose the route/action invalid-value contract explicitly
|
|
994
|
+
3. choose the route/action invalid-value contract explicitly on each structured filter field with `createCrudListFilterQueryField(...)`
|
|
921
995
|
4. build the client runtime from that same definition with `useCrudListFilters(...)`
|
|
922
996
|
|
|
923
997
|
For example, a shared filter-definition module can look like this:
|
|
@@ -944,7 +1018,7 @@ export const CONTACTS_LIST_FILTER_DEFINITIONS = Object.freeze({
|
|
|
944
1018
|
For a generated CRUD, treat this as the concrete file plan:
|
|
945
1019
|
|
|
946
1020
|
- create `packages/contacts/src/shared/contactListFilters.js`
|
|
947
|
-
- update `packages/contacts/src/server/registerRoutes.js` so the list route query validator
|
|
1021
|
+
- 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`
|
|
948
1022
|
- update `packages/contacts/src/server/actions.js` so the list action input validator includes that same explicit contract choice
|
|
949
1023
|
- update `packages/contacts/src/server/repository.js` so the list query path builds the `createCrudListFilters(...)` runtime and calls `applyQuery(...)`
|
|
950
1024
|
- update the app-owned list page or list-runtime composable, usually under `src/pages/.../contacts/` or `src/composables/...`, so it builds `useCrudListFilters(...)`, passes `queryParams` into `useCrudList(...)`, and renders chips / reset behavior from that runtime
|
|
@@ -1071,46 +1145,66 @@ const contactsListFiltersRuntime = createCrudListFilters(
|
|
|
1071
1145
|
);
|
|
1072
1146
|
```
|
|
1073
1147
|
|
|
1074
|
-
There is no default query-validator mode
|
|
1148
|
+
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.
|
|
1075
1149
|
|
|
1076
1150
|
Strict contract example:
|
|
1077
1151
|
|
|
1078
1152
|
```js
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
1153
|
+
const contactsListFilters = CONTACTS_LIST_FILTER_DEFINITIONS;
|
|
1154
|
+
|
|
1155
|
+
const contactsListFiltersQueryValidator = {
|
|
1156
|
+
schema: createCrudListFilterQuerySchema({
|
|
1157
|
+
status: createCrudListFilterQueryField(contactsListFilters.status, {
|
|
1158
|
+
invalidValues: "reject"
|
|
1159
|
+
}),
|
|
1160
|
+
arrivalDate: createCrudListFilterQueryField(contactsListFilters.arrivalDate, {
|
|
1161
|
+
invalidValues: "reject"
|
|
1162
|
+
})
|
|
1163
|
+
}),
|
|
1164
|
+
mode: "patch"
|
|
1165
|
+
};
|
|
1082
1166
|
```
|
|
1083
1167
|
|
|
1084
1168
|
Lenient contract example:
|
|
1085
1169
|
|
|
1086
1170
|
```js
|
|
1087
|
-
const contactsListFiltersQueryValidator =
|
|
1088
|
-
|
|
1089
|
-
|
|
1171
|
+
const contactsListFiltersQueryValidator = {
|
|
1172
|
+
schema: createCrudListFilterQuerySchema({
|
|
1173
|
+
status: createCrudListFilterQueryField(CONTACTS_LIST_FILTER_DEFINITIONS.status, {
|
|
1174
|
+
invalidValues: "discard"
|
|
1175
|
+
}),
|
|
1176
|
+
arrivalDate: createCrudListFilterQueryField(CONTACTS_LIST_FILTER_DEFINITIONS.arrivalDate, {
|
|
1177
|
+
invalidValues: "discard"
|
|
1178
|
+
})
|
|
1179
|
+
}),
|
|
1180
|
+
mode: "patch"
|
|
1181
|
+
};
|
|
1090
1182
|
```
|
|
1091
1183
|
|
|
1092
|
-
Wire the
|
|
1184
|
+
Wire the explicit query contract into the list validator and the repository:
|
|
1093
1185
|
|
|
1094
1186
|
```js
|
|
1095
|
-
|
|
1187
|
+
const listRouteQueryValidator = composeSchemaDefinitions([
|
|
1096
1188
|
listCursorPaginationQueryValidator,
|
|
1097
1189
|
listSearchQueryValidator,
|
|
1098
1190
|
contactsListFiltersQueryValidator,
|
|
1099
1191
|
listParentFilterQueryValidator,
|
|
1100
1192
|
lookupIncludeQueryValidator
|
|
1101
|
-
]
|
|
1193
|
+
], {
|
|
1194
|
+
mode: "patch"
|
|
1195
|
+
});
|
|
1102
1196
|
```
|
|
1103
1197
|
|
|
1104
|
-
Use that same `contactsListFiltersQueryValidator` anywhere else the list query is validated, such as the list action input validator if your CRUD package validates query shape at both the route and action boundaries.
|
|
1198
|
+
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.
|
|
1105
1199
|
|
|
1106
1200
|
Choose the invalid-value contract deliberately:
|
|
1107
1201
|
|
|
1108
|
-
- there is no default mode and no fallback alias, so every
|
|
1202
|
+
- there is no default mode and no fallback alias, so every structured filter field must choose `invalidValues: "reject"` or `invalidValues: "discard"` explicitly
|
|
1109
1203
|
- use `invalidValues: "reject"` when malformed filter values should fail validation and produce a 400-style contract error
|
|
1110
1204
|
- use `invalidValues: "discard"` when malformed filter values should be ignored and normalization should drop them
|
|
1111
1205
|
- route query validation runs before auth, so this choice changes whether malformed unauthenticated requests fail at validation or fall through to auth
|
|
1112
|
-
- for normal HTTP CRUD handlers, route-level `discard` means the handler receives already-
|
|
1113
|
-
-
|
|
1206
|
+
- 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
|
|
1207
|
+
- 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
|
|
1114
1208
|
|
|
1115
1209
|
```js
|
|
1116
1210
|
async function list(query = {}, callOptions = {}) {
|
|
@@ -1126,11 +1220,11 @@ async function list(query = {}, callOptions = {}) {
|
|
|
1126
1220
|
|
|
1127
1221
|
- Put the filter definitions in the CRUD package, not the page. Both server and client need them.
|
|
1128
1222
|
- Keep the filter keys identical all the way through: definition key, query param key, and repository meaning.
|
|
1129
|
-
-
|
|
1223
|
+
- Prefer `createCrudListFilterQueryField(...)` plus `createCrudListFilterQuerySchema(...)` at route/action boundaries so accepted structured-filter params stay visible in app code.
|
|
1130
1224
|
- 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(...)`.
|
|
1131
1225
|
- Use `createCrudListFilters(...)` unless the list semantics are truly unusual.
|
|
1132
1226
|
- Use `q` for free-text and explicit query params for structured filters.
|
|
1133
|
-
- Run `jskit doctor` after wiring filters. It flags inline/local filter-definition objects passed into `useCrudListFilters(...)` or `createCrudListFilters(...)
|
|
1227
|
+
- Run `jskit doctor` after wiring filters. It flags inline/local filter-definition objects passed into `useCrudListFilters(...)` or `createCrudListFilters(...)`.
|
|
1134
1228
|
|
|
1135
1229
|
### Pattern 4: lookup-backed structured filters
|
|
1136
1230
|
|
|
@@ -1290,14 +1384,18 @@ The CRUD stack can derive parent filter keys from the resource contract via `cre
|
|
|
1290
1384
|
For the tutorial `addresses` table, the list search itself can stay very simple:
|
|
1291
1385
|
|
|
1292
1386
|
```js
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
{
|
|
1297
|
-
|
|
1298
|
-
|
|
1387
|
+
const resource = Object.freeze({
|
|
1388
|
+
searchSchema: {
|
|
1389
|
+
id: { type: "id", actualField: "id" },
|
|
1390
|
+
q: {
|
|
1391
|
+
type: "string",
|
|
1392
|
+
oneOf: ["label", "line1", "line2", "suburb", "state", "postcode"],
|
|
1393
|
+
filterOperator: "like",
|
|
1394
|
+
splitBy: " ",
|
|
1395
|
+
matchAll: true
|
|
1299
1396
|
}
|
|
1300
|
-
|
|
1397
|
+
},
|
|
1398
|
+
defaultSort: ["-createdAt"]
|
|
1301
1399
|
});
|
|
1302
1400
|
```
|
|
1303
1401
|
|
|
@@ -1406,14 +1504,14 @@ Touch:
|
|
|
1406
1504
|
- `CrudAddEditFormFields.js`
|
|
1407
1505
|
- the relevant page/table display
|
|
1408
1506
|
|
|
1409
|
-
Use `scaffold-field` when it fits, then review the generated result.
|
|
1507
|
+
Use `scaffold-field` when it fits, then review the generated result. It patches the canonical `schema` in the shared resource file; the standard CRUD validators are derived from that schema automatically.
|
|
1410
1508
|
|
|
1411
1509
|
### "I want a new boolean or enum list filter."
|
|
1412
1510
|
|
|
1413
1511
|
Touch:
|
|
1414
1512
|
|
|
1415
1513
|
- `packages/<crud>/src/shared/<crud>ListFilters.js` and make it the only authored filter-definition module
|
|
1416
|
-
- `packages/<crud>/src/server/registerRoutes.js` and `packages/<crud>/src/server/actions.js` so the list validator
|
|
1514
|
+
- `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
|
|
1417
1515
|
- `packages/<crud>/src/server/repository.js` so the list query applies the runtime
|
|
1418
1516
|
- the app-owned list page or list-runtime composable that calls `useCrudList(...)`
|
|
1419
1517
|
|
|
@@ -284,6 +284,8 @@ That file is the shared CRUD contract. The UI generator reads it to decide:
|
|
|
284
284
|
|
|
285
285
|
So even though the server scaffold writes many files, the resource file is the bridge between the server and UI halves.
|
|
286
286
|
|
|
287
|
+
For standard CRUDs, that file is now intentionally compact. It uses `defineCrudResource(...)` from `@jskit-ai/resource-crud-core`, authors the canonical `schema` / `searchSchema` / `defaultSort` / `autofilter` shape once, and lets JSKIT derive the standard CRUD operation contracts from it.
|
|
288
|
+
|
|
287
289
|
### Step 3: scaffold the UI
|
|
288
290
|
|
|
289
291
|
Once the resource file exists, generate the UI route tree:
|
|
@@ -566,6 +568,8 @@ Use this when:
|
|
|
566
568
|
|
|
567
569
|
It is a maintenance tool, not the first step in the workflow.
|
|
568
570
|
|
|
571
|
+
It patches the canonical `schema` inside the shared resource file. That matters because the standard CRUD validators are derived from that canonical schema; you are no longer maintaining separate authored `create` / `patch` / `view` validator blocks by hand.
|
|
572
|
+
|
|
569
573
|
## Summary
|
|
570
574
|
|
|
571
575
|
The important thing is not memorizing commands. It is learning the three shapes:
|
package/guide/agent/index.md
CHANGED
|
@@ -16,8 +16,8 @@ It starts with the smallest scaffold, then explains how to work with the JSKIT C
|
|
|
16
16
|
- [Authentication](/guide/app-setup/authentication)
|
|
17
17
|
- [Database Layer](/guide/app-setup/database-layer)
|
|
18
18
|
- [Users](/guide/app-setup/users)
|
|
19
|
-
- [Console](/guide/app-setup/console)
|
|
20
19
|
- [Multi-homing](/guide/app-setup/multi-homing)
|
|
20
|
+
- [Console](/guide/app-setup/console)
|
|
21
21
|
|
|
22
22
|
### App Extras
|
|
23
23
|
|
package/package.json
CHANGED
package/patterns/INDEX.md
CHANGED
|
@@ -28,7 +28,7 @@ How to use it:
|
|
|
28
28
|
- `ui-testing.md`
|
|
29
29
|
- filter, filters, search facets, chips, date range, enum filter, lookup filter, `useCrudListFilters`, `createCrudListFilters`
|
|
30
30
|
- `filters.md`
|
|
31
|
-
-
|
|
31
|
+
- `actualField`, `storage.writeSerializer`, `storage.virtual`, computed field, virtual field, projection, `createCrudResourceRuntime`, `remainingBatchWeight`, datetime write serialization
|
|
32
32
|
- `crud-repository-mapping.md`
|
|
33
33
|
|
|
34
34
|
## Current Patterns
|
|
@@ -47,6 +47,8 @@ Why this is the standard JSKIT shape:
|
|
|
47
47
|
- The higher-level list, view, add/edit, and command runtimes send requests through the shared HTTP runtime.
|
|
48
48
|
- `usersWebHttpClient` already handles credentials and CSRF behavior.
|
|
49
49
|
- `useEndpointResource()` is the shared endpoint primitive for loading, saving, and standard load/save error handling. Higher-level runtimes add UI feedback and field-error handling on top.
|
|
50
|
+
- Use `requestQueryParams` for endpoint query strings on list, view, and add/edit runtimes.
|
|
51
|
+
- Keep `apiUrlTemplate` path-only. Do not put `?include=...` or other query strings in URL templates.
|
|
50
52
|
|
|
51
53
|
Avoid:
|
|
52
54
|
|
|
@@ -54,3 +56,4 @@ Avoid:
|
|
|
54
56
|
- page-local HTTP helpers that duplicate JSKIT runtime seams
|
|
55
57
|
- manually concatenating scoped route params into API URLs
|
|
56
58
|
- using a lower-level seam when a higher-level routed CRUD or command runtime already fits
|
|
59
|
+
- smuggling query params into `apiUrlTemplate`
|