@jskit-ai/agent-docs 0.1.16 → 0.1.18
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 +27 -4
- package/guide/agent/app-setup/working-with-the-jskit-cli.md +1 -0
- package/guide/agent/generators/advanced-cruds.md +206 -112
- package/guide/agent/generators/crud-generators.md +26 -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 +36 -35
- 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 +34 -30
- package/reference/autogen/packages/crud-server-generator.md +28 -83
- 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 +6 -5
- 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
|
|
|
@@ -591,12 +591,12 @@ registerProfileSyncLifecycleContributor(app, "workspaces.core.profileSyncLifecyc
|
|
|
591
591
|
return Object.freeze({
|
|
592
592
|
contributorId: "workspaces.core.profileSync",
|
|
593
593
|
order: 100,
|
|
594
|
-
async afterIdentityProfileSynced({ profile,
|
|
595
|
-
if (!
|
|
594
|
+
async afterIdentityProfileSynced({ profile, options } = {}) {
|
|
595
|
+
if (!profile || typeof workspaceService?.ensureProvisionedWorkspaceForAuthenticatedUser !== "function") {
|
|
596
596
|
return;
|
|
597
597
|
}
|
|
598
598
|
|
|
599
|
-
await workspaceService.
|
|
599
|
+
await workspaceService.ensureProvisionedWorkspaceForAuthenticatedUser(profile, options);
|
|
600
600
|
}
|
|
601
601
|
});
|
|
602
602
|
});
|
|
@@ -604,7 +604,9 @@ registerProfileSyncLifecycleContributor(app, "workspaces.core.profileSyncLifecyc
|
|
|
604
604
|
|
|
605
605
|
That means the workspace package does not need to patch auth directly to learn that a user was added. It listens through the `users-core` lifecycle registry instead.
|
|
606
606
|
|
|
607
|
-
For this chapter's `tenancyMode = "personal"` setup, that contributor now does real work. When
|
|
607
|
+
For this chapter's `tenancyMode = "personal"` setup, that contributor now does real work. When an authenticated JSKIT user is synchronized from auth, `workspaces-core` ensures that user's personal workspace exists.
|
|
608
|
+
|
|
609
|
+
That detail matters for the retrofit path described earlier in this chapter. If the app started on `none`, then later switched to `personal`, the first sign-in after that switch still needs to backfill the personal workspace for the already-existing user record. The lifecycle contributor handles that because the workspace provision step is idempotent.
|
|
608
610
|
|
|
609
611
|
- `users-core` owns the "user was synchronized" lifecycle
|
|
610
612
|
- `workspaces-core` subscribes to that lifecycle through the registry
|
|
@@ -612,6 +614,27 @@ For this chapter's `tenancyMode = "personal"` setup, that contributor now does r
|
|
|
612
614
|
|
|
613
615
|
So even in this chapter, the package boundary is already correct: users owns user creation/sync, and workspaces reacts through an extension point.
|
|
614
616
|
|
|
617
|
+
### Workspace auth context comes from the auth policy resolver registry
|
|
618
|
+
|
|
619
|
+
There is a second registry seam in this chapter that matters just as much for the runtime model.
|
|
620
|
+
|
|
621
|
+
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.
|
|
622
|
+
|
|
623
|
+
The important consequence is:
|
|
624
|
+
|
|
625
|
+
- `auth-core` stays generic
|
|
626
|
+
- `workspaces-core` contributes workspace-specific context
|
|
627
|
+
- the request only resolves workspace membership and permissions when a route actually asks for auth context or permissions
|
|
628
|
+
|
|
629
|
+
So the flow is:
|
|
630
|
+
|
|
631
|
+
1. a workspace-aware route declares auth and, when needed, permission requirements
|
|
632
|
+
2. the auth policy runtime authenticates the actor
|
|
633
|
+
3. the composed auth policy context resolver asks the workspace layer for the current workspace context
|
|
634
|
+
4. the request gains normalized `workspace`, `membership`, and `permissions` values
|
|
635
|
+
|
|
636
|
+
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.
|
|
637
|
+
|
|
615
638
|
### Why this chapter is the real routing pivot
|
|
616
639
|
|
|
617
640
|
Earlier chapters added features inside a flat top-level app.
|
|
@@ -568,6 +568,7 @@ So `update` is for reapplying package-owned managed changes, not for generic dep
|
|
|
568
568
|
The best guide example is the tenancy-mode recovery path from the multi-homing chapter. If you installed `workspaces-core` or `workspaces-web` while the app was still on `tenancyMode = "none"`, then later changed the app to `personal`, the missing gated scaffold does not backfill automatically. The recovery path is:
|
|
569
569
|
|
|
570
570
|
```bash
|
|
571
|
+
npx jskit update package users-core
|
|
571
572
|
npx jskit update package workspaces-core
|
|
572
573
|
npx jskit update package workspaces-web
|
|
573
574
|
```
|
|
@@ -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,32 +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 `
|
|
893
|
+
- for explicit DB column overrides, use `actualField`
|
|
850
894
|
- standard writable `date-time` fields are serialized automatically during CRUD writes
|
|
851
|
-
- use `
|
|
852
|
-
- for computed output fields, use `
|
|
895
|
+
- use `storage.writeSerializer` only for non-default DB write serialization
|
|
896
|
+
- for computed output fields, use `storage: { virtual: true }`
|
|
853
897
|
- do not put computed fields in create/patch write schemas
|
|
854
898
|
|
|
855
899
|
Example field metadata:
|
|
856
900
|
|
|
857
901
|
```js
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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"]
|
|
870
940
|
});
|
|
871
941
|
```
|
|
872
942
|
|
|
@@ -894,7 +964,7 @@ Once registered there:
|
|
|
894
964
|
- generic CRUD `listByIds`
|
|
895
965
|
- generic CRUD `listByForeignIds`
|
|
896
966
|
|
|
897
|
-
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 `
|
|
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.
|
|
898
968
|
|
|
899
969
|
all pick up the projection automatically, so you should not hand-patch `clearSelect()` / re-select logic into each method.
|
|
900
970
|
|
|
@@ -921,7 +991,7 @@ Instead:
|
|
|
921
991
|
|
|
922
992
|
1. define the filters once in a shared CRUD-package module
|
|
923
993
|
2. build the server runtime from that definition with `createCrudListFilters(...)`
|
|
924
|
-
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(...)`
|
|
925
995
|
4. build the client runtime from that same definition with `useCrudListFilters(...)`
|
|
926
996
|
|
|
927
997
|
For example, a shared filter-definition module can look like this:
|
|
@@ -948,7 +1018,7 @@ export const CONTACTS_LIST_FILTER_DEFINITIONS = Object.freeze({
|
|
|
948
1018
|
For a generated CRUD, treat this as the concrete file plan:
|
|
949
1019
|
|
|
950
1020
|
- create `packages/contacts/src/shared/contactListFilters.js`
|
|
951
|
-
- 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`
|
|
952
1022
|
- update `packages/contacts/src/server/actions.js` so the list action input validator includes that same explicit contract choice
|
|
953
1023
|
- update `packages/contacts/src/server/repository.js` so the list query path builds the `createCrudListFilters(...)` runtime and calls `applyQuery(...)`
|
|
954
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
|
|
@@ -1075,46 +1145,66 @@ const contactsListFiltersRuntime = createCrudListFilters(
|
|
|
1075
1145
|
);
|
|
1076
1146
|
```
|
|
1077
1147
|
|
|
1078
|
-
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.
|
|
1079
1149
|
|
|
1080
1150
|
Strict contract example:
|
|
1081
1151
|
|
|
1082
1152
|
```js
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
+
};
|
|
1086
1166
|
```
|
|
1087
1167
|
|
|
1088
1168
|
Lenient contract example:
|
|
1089
1169
|
|
|
1090
1170
|
```js
|
|
1091
|
-
const contactsListFiltersQueryValidator =
|
|
1092
|
-
|
|
1093
|
-
|
|
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
|
+
};
|
|
1094
1182
|
```
|
|
1095
1183
|
|
|
1096
|
-
Wire the
|
|
1184
|
+
Wire the explicit query contract into the list validator and the repository:
|
|
1097
1185
|
|
|
1098
1186
|
```js
|
|
1099
|
-
|
|
1187
|
+
const listRouteQueryValidator = composeSchemaDefinitions([
|
|
1100
1188
|
listCursorPaginationQueryValidator,
|
|
1101
1189
|
listSearchQueryValidator,
|
|
1102
1190
|
contactsListFiltersQueryValidator,
|
|
1103
1191
|
listParentFilterQueryValidator,
|
|
1104
1192
|
lookupIncludeQueryValidator
|
|
1105
|
-
]
|
|
1193
|
+
], {
|
|
1194
|
+
mode: "patch"
|
|
1195
|
+
});
|
|
1106
1196
|
```
|
|
1107
1197
|
|
|
1108
|
-
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.
|
|
1109
1199
|
|
|
1110
1200
|
Choose the invalid-value contract deliberately:
|
|
1111
1201
|
|
|
1112
|
-
- 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
|
|
1113
1203
|
- use `invalidValues: "reject"` when malformed filter values should fail validation and produce a 400-style contract error
|
|
1114
1204
|
- use `invalidValues: "discard"` when malformed filter values should be ignored and normalization should drop them
|
|
1115
1205
|
- route query validation runs before auth, so this choice changes whether malformed unauthenticated requests fail at validation or fall through to auth
|
|
1116
|
-
- for normal HTTP CRUD handlers, route-level `discard` means the handler receives already-
|
|
1117
|
-
-
|
|
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
|
|
1118
1208
|
|
|
1119
1209
|
```js
|
|
1120
1210
|
async function list(query = {}, callOptions = {}) {
|
|
@@ -1130,11 +1220,11 @@ async function list(query = {}, callOptions = {}) {
|
|
|
1130
1220
|
|
|
1131
1221
|
- Put the filter definitions in the CRUD package, not the page. Both server and client need them.
|
|
1132
1222
|
- Keep the filter keys identical all the way through: definition key, query param key, and repository meaning.
|
|
1133
|
-
-
|
|
1223
|
+
- Prefer `createCrudListFilterQueryField(...)` plus `createCrudListFilterQuerySchema(...)` at route/action boundaries so accepted structured-filter params stay visible in app code.
|
|
1134
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(...)`.
|
|
1135
1225
|
- Use `createCrudListFilters(...)` unless the list semantics are truly unusual.
|
|
1136
1226
|
- Use `q` for free-text and explicit query params for structured filters.
|
|
1137
|
-
- 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(...)`.
|
|
1138
1228
|
|
|
1139
1229
|
### Pattern 4: lookup-backed structured filters
|
|
1140
1230
|
|
|
@@ -1294,14 +1384,18 @@ The CRUD stack can derive parent filter keys from the resource contract via `cre
|
|
|
1294
1384
|
For the tutorial `addresses` table, the list search itself can stay very simple:
|
|
1295
1385
|
|
|
1296
1386
|
```js
|
|
1297
|
-
const
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
{
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
|
1303
1396
|
}
|
|
1304
|
-
|
|
1397
|
+
},
|
|
1398
|
+
defaultSort: ["-createdAt"]
|
|
1305
1399
|
});
|
|
1306
1400
|
```
|
|
1307
1401
|
|
|
@@ -1410,14 +1504,14 @@ Touch:
|
|
|
1410
1504
|
- `CrudAddEditFormFields.js`
|
|
1411
1505
|
- the relevant page/table display
|
|
1412
1506
|
|
|
1413
|
-
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.
|
|
1414
1508
|
|
|
1415
1509
|
### "I want a new boolean or enum list filter."
|
|
1416
1510
|
|
|
1417
1511
|
Touch:
|
|
1418
1512
|
|
|
1419
1513
|
- `packages/<crud>/src/shared/<crud>ListFilters.js` and make it the only authored filter-definition module
|
|
1420
|
-
- `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
|
|
1421
1515
|
- `packages/<crud>/src/server/repository.js` so the list query applies the runtime
|
|
1422
1516
|
- the app-owned list page or list-runtime composable that calls `useCrudList(...)`
|
|
1423
1517
|
|
|
@@ -284,6 +284,30 @@ 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
|
+
### One install boundary to remember
|
|
288
|
+
|
|
289
|
+
`crud-server-generator scaffold` also adds a new local app package dependency such as:
|
|
290
|
+
|
|
291
|
+
```text
|
|
292
|
+
@local/contacts
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
So before you build or run the app again, install that new local package:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
npm install
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
If you are verifying the guide against a local JSKIT checkout and have already been using local package links, rerun:
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
npm run devlinks
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
The same rule applies after later server scaffolds such as `addresses` and `comments`. The UI generator can still read the generated resource file directly, but the app runtime needs the local package install boundary to be completed before the CRUD can boot normally.
|
|
308
|
+
|
|
309
|
+
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.
|
|
310
|
+
|
|
287
311
|
### Step 3: scaffold the UI
|
|
288
312
|
|
|
289
313
|
Once the resource file exists, generate the UI route tree:
|
|
@@ -566,6 +590,8 @@ Use this when:
|
|
|
566
590
|
|
|
567
591
|
It is a maintenance tool, not the first step in the workflow.
|
|
568
592
|
|
|
593
|
+
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.
|
|
594
|
+
|
|
569
595
|
## Summary
|
|
570
596
|
|
|
571
597
|
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
|
|