@jskit-ai/agent-docs 0.1.131 → 0.1.132
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 +29 -605
- package/guide/agent/app-extras/mobile-capacitor.md +29 -362
- package/guide/agent/app-extras/realtime.md +29 -277
- package/guide/agent/app-setup/a-more-interesting-shell.md +44 -815
- package/guide/agent/app-setup/authentication.md +43 -1073
- package/guide/agent/app-setup/console.md +26 -298
- package/guide/agent/app-setup/database-layer.md +110 -790
- package/guide/agent/app-setup/initial-scaffolding.md +50 -784
- package/guide/agent/app-setup/multi-homing.md +39 -712
- package/guide/agent/app-setup/quickstart.md +43 -179
- package/guide/agent/app-setup/users.md +34 -353
- package/guide/agent/index.md +16 -23
- package/package.json +2 -2
- package/patterns/INDEX.md +7 -7
- package/patterns/child-cruds.md +3 -3
- package/patterns/client-requests.md +6 -6
- package/patterns/crud-authoring.md +94 -0
- package/patterns/crud-links.md +1 -1
- package/patterns/feature-package/PATTERN.md +108 -0
- package/patterns/feature-package/example/booking-engine/package.json +48 -0
- package/patterns/feature-package/example/booking-engine/src/server/BookingEngineProvider.js +33 -0
- package/patterns/feature-package/example/booking-engine/src/server/actions.js +26 -0
- package/patterns/feature-package/example/booking-engine/src/server/inputSchemas.js +19 -0
- package/patterns/feature-package/example/variations/AvailabilityEngineProvider.js +31 -0
- package/patterns/feature-package/example/variations/InvoiceRollupProvider.js +36 -0
- package/patterns/feature-package/example/variations/customKnexRepository.js +34 -0
- package/patterns/feature-package/example/variations/orchestratorService.js +23 -0
- package/patterns/filters.md +8 -8
- package/patterns/live-actions.md +5 -18
- package/patterns/minimal-foundation/PATTERN.md +98 -0
- package/patterns/minimal-foundation/example/.nvmrc +1 -0
- package/patterns/minimal-foundation/example/AGENTS.md +17 -0
- package/patterns/minimal-foundation/example/Procfile +2 -0
- package/patterns/minimal-foundation/example/app.json +14 -0
- package/patterns/minimal-foundation/example/bin/develop.js +71 -0
- package/patterns/minimal-foundation/example/bin/server.js +8 -0
- package/patterns/minimal-foundation/example/config/public.js +40 -0
- package/patterns/minimal-foundation/example/config/server.js +1 -0
- package/patterns/minimal-foundation/example/config/surfaceAccessPolicies.js +3 -0
- package/patterns/minimal-foundation/example/eslint.config.mjs +19 -0
- package/patterns/minimal-foundation/example/favicon.svg +7 -0
- package/patterns/minimal-foundation/example/gitignore +9 -0
- package/patterns/minimal-foundation/example/index.html +13 -0
- package/patterns/minimal-foundation/example/jsconfig.json +8 -0
- package/patterns/minimal-foundation/example/package.json +57 -0
- package/patterns/minimal-foundation/example/packages/main/package.json +42 -0
- package/patterns/minimal-foundation/example/packages/main/src/shared/index.js +10 -0
- package/patterns/minimal-foundation/example/packages/main/src/shared/schemas/index.js +22 -0
- package/patterns/minimal-foundation/example/playwright.config.mjs +31 -0
- package/patterns/minimal-foundation/example/server/lib/runtimeEnv.js +45 -0
- package/patterns/minimal-foundation/example/server/lib/surfaceRuntime.js +10 -0
- package/patterns/minimal-foundation/example/server.js +195 -0
- package/patterns/minimal-foundation/example/src/App.vue +13 -0
- package/patterns/minimal-foundation/example/src/main.js +85 -0
- package/patterns/minimal-foundation/example/src/pages/home/index.vue +48 -0
- package/patterns/minimal-foundation/example/src/pages/home.vue +13 -0
- package/patterns/minimal-foundation/example/src/views/NotFound.vue +13 -0
- package/patterns/minimal-foundation/example/tests/client/smoke.vitest.js +7 -0
- package/patterns/minimal-foundation/example/tests/e2e/base-shell.spec.ts +23 -0
- package/patterns/minimal-foundation/example/tests/server/smoke.test.js +16 -0
- package/patterns/minimal-foundation/example/vite.config.mjs +81 -0
- package/patterns/page-scaffolding.md +20 -17
- package/patterns/placements.md +17 -15
- package/patterns/row-policies.md +4 -5
- package/patterns/server-search.md +3 -3
- package/patterns/shell-foundation/PATTERN.md +104 -0
- package/patterns/shell-foundation/example/.nvmrc +1 -0
- package/patterns/shell-foundation/example/AGENTS.md +17 -0
- package/patterns/shell-foundation/example/Procfile +2 -0
- package/patterns/shell-foundation/example/app.json +14 -0
- package/patterns/shell-foundation/example/bin/develop.js +71 -0
- package/patterns/shell-foundation/example/bin/server.js +8 -0
- package/patterns/shell-foundation/example/config/public.js +40 -0
- package/patterns/shell-foundation/example/config/server.js +1 -0
- package/patterns/shell-foundation/example/config/surfaceAccessPolicies.js +3 -0
- package/patterns/shell-foundation/example/eslint.config.mjs +19 -0
- package/patterns/shell-foundation/example/favicon.svg +7 -0
- package/patterns/shell-foundation/example/gitignore +9 -0
- package/patterns/shell-foundation/example/index.html +13 -0
- package/patterns/shell-foundation/example/jsconfig.json +8 -0
- package/patterns/shell-foundation/example/package.json +59 -0
- package/patterns/shell-foundation/example/packages/main/package.json +56 -0
- package/patterns/shell-foundation/example/packages/main/src/client/index.js +9 -0
- package/patterns/shell-foundation/example/packages/main/src/client/providers/MainClientProvider.js +18 -0
- package/patterns/shell-foundation/example/packages/main/src/shared/index.js +10 -0
- package/patterns/shell-foundation/example/packages/main/src/shared/schemas/index.js +22 -0
- package/patterns/shell-foundation/example/playwright.config.mjs +31 -0
- package/patterns/shell-foundation/example/server/lib/runtimeEnv.js +45 -0
- package/patterns/shell-foundation/example/server/lib/surfaceRuntime.js +10 -0
- package/patterns/shell-foundation/example/server.js +195 -0
- package/patterns/shell-foundation/example/src/App.vue +11 -0
- package/patterns/shell-foundation/example/src/components/ShellLayout.vue +12 -0
- package/patterns/shell-foundation/example/src/components/menus/MenuLinkItem.vue +30 -0
- package/patterns/shell-foundation/example/src/components/menus/SurfaceAwareMenuLinkItem.vue +42 -0
- package/patterns/shell-foundation/example/src/components/menus/TabLinkItem.vue +42 -0
- package/patterns/shell-foundation/example/src/error.js +19 -0
- package/patterns/shell-foundation/example/src/main.js +85 -0
- package/patterns/shell-foundation/example/src/pages/home/index.vue +116 -0
- package/patterns/shell-foundation/example/src/pages/home/settings/general/index.vue +40 -0
- package/patterns/shell-foundation/example/src/pages/home/settings/index.vue +7 -0
- package/patterns/shell-foundation/example/src/pages/home/settings.vue +109 -0
- package/patterns/shell-foundation/example/src/pages/home.vue +20 -0
- package/patterns/shell-foundation/example/src/placement.js +56 -0
- package/patterns/shell-foundation/example/src/placementTopology.js +149 -0
- package/patterns/shell-foundation/example/src/views/NotFound.vue +13 -0
- package/patterns/shell-foundation/example/tests/client/smoke.vitest.js +7 -0
- package/patterns/shell-foundation/example/tests/e2e/adaptive-shell.spec.ts +10 -0
- package/patterns/shell-foundation/example/tests/e2e/base-shell.spec.ts +23 -0
- package/patterns/shell-foundation/example/tests/server/smoke.test.js +16 -0
- package/patterns/shell-foundation/example/vite.config.mjs +81 -0
- package/patterns/ui-contract.md +56 -0
- package/patterns/ui-testing.md +10 -12
- package/reference/autogen/KERNEL_MAP.md +29 -107
- package/reference/autogen/PATTERN_INDEX.md +230 -0
- package/reference/autogen/README.md +4 -8
- package/reference/autogen/packages/agent-docs.md +259 -0
- package/reference/autogen/packages/assistant-core.md +3 -3
- package/reference/autogen/packages/assistant-runtime.md +32 -17
- package/reference/autogen/packages/auth-core.md +31 -33
- package/reference/autogen/packages/auth-provider-local-core.md +4 -12
- package/reference/autogen/packages/auth-provider-local-db-core.md +4 -4
- package/reference/autogen/packages/auth-provider-supabase-core.md +14 -18
- package/reference/autogen/packages/auth-web.md +42 -22
- package/reference/autogen/packages/console-core.md +8 -25
- package/reference/autogen/packages/console-web.md +5 -5
- package/reference/autogen/packages/crud-core.md +61 -17
- package/reference/autogen/packages/database-runtime-mysql.md +12 -2
- package/reference/autogen/packages/database-runtime-postgres.md +12 -2
- package/reference/autogen/packages/database-runtime.md +26 -25
- package/reference/autogen/packages/google-rewarded-core.md +19 -104
- package/reference/autogen/packages/http-runtime.md +4 -8
- package/reference/autogen/packages/http-web.md +32 -0
- package/reference/autogen/packages/json-rest-api-core.md +4 -6
- package/reference/autogen/packages/kernel.md +109 -390
- package/reference/autogen/packages/mobile-capacitor.md +2 -13
- package/reference/autogen/packages/realtime.md +29 -26
- package/reference/autogen/packages/resource-crud-core.md +6 -0
- package/reference/autogen/packages/shell-web.md +69 -54
- package/reference/autogen/packages/storage-runtime.md +3 -3
- package/reference/autogen/packages/uploads-image-web.md +0 -1
- package/reference/autogen/packages/uploads-runtime.md +3 -3
- package/reference/autogen/packages/users-core.md +45 -90
- package/reference/autogen/packages/users-web.md +5 -7
- package/reference/autogen/packages/workspaces-core.md +53 -74
- package/reference/autogen/packages/workspaces-web.md +15 -16
- package/reference/autogen/tooling/jskit-catalog.md +34 -0
- package/reference/autogen/tooling/testUtils.md +4 -4
- package/skills/jskit/SKILL.md +34 -29
- package/skills/jskit/references/app-operations.md +68 -53
- package/skills/jskit/references/crud-operations.md +58 -106
- package/skills/jskit/references/material-3.md +1 -1
- package/skills/jskit/references/ui-operations.md +41 -44
- package/templates/app/AGENTS.md +7 -3
- package/guide/agent/app-setup/upgrade-beta-1-to-final.md +0 -252
- package/guide/agent/app-setup/working-with-the-jskit-cli.md +0 -325
- package/guide/agent/generators/advanced-cruds.md +0 -1935
- package/guide/agent/generators/crud-generators.md +0 -948
- package/guide/agent/generators/intro.md +0 -65
- package/guide/agent/generators/row-policies.md +0 -537
- package/guide/agent/generators/ui-generators.md +0 -690
- package/patterns/crud-scaffolding.md +0 -198
- package/patterns/generated-ui-contract-tracking.md +0 -66
- package/reference/autogen/packages/assistant.md +0 -68
- package/reference/autogen/packages/crud-server-generator.md +0 -215
- package/reference/autogen/packages/crud-ui-generator.md +0 -192
- package/reference/autogen/packages/feature-server-generator.md +0 -65
- package/reference/autogen/packages/ui-generator.md +0 -127
- package/reference/autogen/tooling/create-app.md +0 -317
- package/reference/autogen/tooling/jskit-cli.md +0 -933
- package/reference/autogen/tooling/test-support.md +0 -27
|
@@ -1,948 +0,0 @@
|
|
|
1
|
-
<!-- Generated by `npm run agent-docs:build` from `packages/agent-docs/site/guide/generators/crud-generators.md`. Do not edit manually. -->
|
|
2
|
-
|
|
3
|
-
# CRUD Generators
|
|
4
|
-
|
|
5
|
-
CRUD generation in JSKIT is really one workflow split across two generator packages:
|
|
6
|
-
|
|
7
|
-
- `crud-server-generator` creates the app-local server/resource package
|
|
8
|
-
- `crud-ui-generator` creates the route tree that uses that shared resource contract
|
|
9
|
-
|
|
10
|
-
So the real order is always:
|
|
11
|
-
|
|
12
|
-
1. create a real table
|
|
13
|
-
2. scaffold the server package around it
|
|
14
|
-
3. scaffold the UI around the generated resource file
|
|
15
|
-
|
|
16
|
-
This chapter uses three examples, in increasing complexity:
|
|
17
|
-
|
|
18
|
-
- `contacts`: the baseline full CRUD walkthrough
|
|
19
|
-
- `addresses`: a child CRUD with its own routed list page reached from the parent record view
|
|
20
|
-
- `comments`: a child CRUD that lives under the parent record host as a child page
|
|
21
|
-
|
|
22
|
-
The examples assume the guide app already has the database and workspace/admin setup from the earlier chapters. That is why the route roots below live under `w/[workspaceSlug]/admin/...`.
|
|
23
|
-
|
|
24
|
-
This is the workflow chapter.
|
|
25
|
-
|
|
26
|
-
It is meant to answer:
|
|
27
|
-
|
|
28
|
-
- which generator do I run first?
|
|
29
|
-
- what order do the server and UI steps go in?
|
|
30
|
-
- what shape of CRUD should I generate for this feature?
|
|
31
|
-
|
|
32
|
-
Once that workflow is clear, continue with [Advanced CRUDs](/guide/generators/advanced-cruds) for the generated package anatomy, ownership model, and customization boundaries.
|
|
33
|
-
|
|
34
|
-
## Fresh minimal Notes app: the complete command order
|
|
35
|
-
|
|
36
|
-
The dependency install boundaries are part of the workflow. After the first
|
|
37
|
-
install, use `npx --no-install jskit` so the command fails if the app-local CLI
|
|
38
|
-
is unavailable instead of fetching a different copy.
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
npx @jskit-ai/create-app notes \
|
|
42
|
-
--target . \
|
|
43
|
-
--force \
|
|
44
|
-
--tenancy-mode none \
|
|
45
|
-
--minimal
|
|
46
|
-
npm install
|
|
47
|
-
|
|
48
|
-
npx --no-install jskit add package database-runtime-mysql
|
|
49
|
-
|
|
50
|
-
# Create and select a fresh disposable database, then create the live `notes` table.
|
|
51
|
-
|
|
52
|
-
npx --no-install jskit generate crud-server-generator scaffold \
|
|
53
|
-
--namespace notes \
|
|
54
|
-
--surface home \
|
|
55
|
-
--ownership-filter public \
|
|
56
|
-
--access public \
|
|
57
|
-
--table-name notes
|
|
58
|
-
|
|
59
|
-
npx --no-install jskit generate crud-ui-generator crud notes \
|
|
60
|
-
--resource-file packages/notes/src/shared/noteResource.js \
|
|
61
|
-
--id-param noteId \
|
|
62
|
-
--display-fields title,body \
|
|
63
|
-
--parent-title contextual \
|
|
64
|
-
--navigation-role primary \
|
|
65
|
-
--delete-confirmation
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
The server generator resolves the complete package closure. `shell-web`
|
|
69
|
-
establishes and owns `src/placement.js` before `realtime` contributes its
|
|
70
|
-
placement, so do not pre-install the shell as a workaround.
|
|
71
|
-
|
|
72
|
-
## Temporal values
|
|
73
|
-
|
|
74
|
-
Temporal resource values are strings. `date` is `YYYY-MM-DD`; `time` is
|
|
75
|
-
offset-free `HH:MM[:SS[.fraction]]`; and `dateTime` is RFC 3339 with seconds
|
|
76
|
-
and a `Z` or numeric offset. Numeric epochs use `epochMilliseconds` or
|
|
77
|
-
`epochSeconds`. Preserve `temporalPrecision`.
|
|
78
|
-
|
|
79
|
-
Generated generic CRUD repositories convert database temporal values at the
|
|
80
|
-
resource boundary. Custom repositories still need to return strict temporal
|
|
81
|
-
strings and write ISO/RFC 3339 strings explicitly.
|
|
82
|
-
|
|
83
|
-
## The two generator packages
|
|
84
|
-
|
|
85
|
-
### `crud-server-generator` `@jskit-ai/crud-server-generator` `(0.1.47)`
|
|
86
|
-
|
|
87
|
-
This is the server-side half.
|
|
88
|
-
|
|
89
|
-
It introspects a real table and writes an app-local package with things like:
|
|
90
|
-
|
|
91
|
-
- repository logic
|
|
92
|
-
- service logic
|
|
93
|
-
- route registration
|
|
94
|
-
- action definitions
|
|
95
|
-
- the shared resource module that describes the CRUD contract
|
|
96
|
-
|
|
97
|
-
The main subcommand is:
|
|
98
|
-
|
|
99
|
-
- `scaffold`
|
|
100
|
-
|
|
101
|
-
There is also:
|
|
102
|
-
|
|
103
|
-
- `scaffold-field`
|
|
104
|
-
|
|
105
|
-
which is for patching one extra field into an already-generated resource module after a schema change.
|
|
106
|
-
|
|
107
|
-
### `crud-ui-generator` `@jskit-ai/crud-ui-generator` `(0.1.22)`
|
|
108
|
-
|
|
109
|
-
This is the UI half.
|
|
110
|
-
|
|
111
|
-
It reads the generated shared resource module and creates app-owned pages under an explicit route root relative to `src/pages/`.
|
|
112
|
-
|
|
113
|
-
The main subcommand is:
|
|
114
|
-
|
|
115
|
-
- `crud`
|
|
116
|
-
|
|
117
|
-
That command can generate any combination of:
|
|
118
|
-
|
|
119
|
-
- `list`
|
|
120
|
-
- `view`
|
|
121
|
-
- `new`
|
|
122
|
-
- `edit`
|
|
123
|
-
|
|
124
|
-
That flexibility is what makes the later `comments` example possible.
|
|
125
|
-
|
|
126
|
-
## Understanding ownership filters
|
|
127
|
-
|
|
128
|
-
`--ownership-filter` is one of the most important CRUD generator options.
|
|
129
|
-
|
|
130
|
-
It is not just "one more flag." It tells JSKIT what kind of ownership the generated CRUD assumes:
|
|
131
|
-
|
|
132
|
-
- whether records are shared or owner-scoped
|
|
133
|
-
- which owner columns the CRUD expects
|
|
134
|
-
- what request context the generated routes and repository use to filter records
|
|
135
|
-
|
|
136
|
-
In plain English, it answers:
|
|
137
|
-
|
|
138
|
-
- who should be able to see these rows?
|
|
139
|
-
- what kind of owner does each row belong to?
|
|
140
|
-
|
|
141
|
-
The supported values are:
|
|
142
|
-
|
|
143
|
-
| Value | Plain meaning | Typical owner columns |
|
|
144
|
-
| --- | --- | --- |
|
|
145
|
-
| `auto` | infer ownership from the real table during generation | inferred from table shape |
|
|
146
|
-
| `public` | no owner scoping | no `workspace_id` or `user_id` required |
|
|
147
|
-
| `user` | rows belong to one user | `user_id` |
|
|
148
|
-
| `workspace` | rows belong to one workspace | `workspace_id` |
|
|
149
|
-
| `workspace_user` | rows belong to one workspace and one user together | `workspace_id` and `user_id` |
|
|
150
|
-
|
|
151
|
-
### Ownership column names are exact and reserved
|
|
152
|
-
|
|
153
|
-
JSKIT ownership is explicit and materialized. Only these exact database columns carry the standard generated ownership contract:
|
|
154
|
-
|
|
155
|
-
- `workspace_id`
|
|
156
|
-
- `user_id`
|
|
157
|
-
|
|
158
|
-
They are not ordinary foreign-key names. Generated CRUD infrastructure uses them for visibility filtering, create-time owner stamping, and hidden owner-field handling.
|
|
159
|
-
|
|
160
|
-
Other foreign keys are domain relationships, even when their names end in `_user_id` or `_workspace_id`. For example:
|
|
161
|
-
|
|
162
|
-
- `recipient_user_id` identifies a recipient
|
|
163
|
-
- `created_by_user_id` records an author
|
|
164
|
-
- `assignee_user_id` identifies an assignee
|
|
165
|
-
|
|
166
|
-
Those fields do not become ownership aliases merely because they point to a user-owned table.
|
|
167
|
-
|
|
168
|
-
The ownership filter must match the direct reserved columns exactly. A table with only `workspace_id` is `workspace`; one with only `user_id` is `user`; and one with both is `workspace_user`. Neither the CLI declaration nor generated metadata overrides those columns.
|
|
169
|
-
|
|
170
|
-
Decide ownership first, select the matching ownership filter, and then model other actors with specifically named foreign keys. A workspace-owned notification outbox item that targets a user normally has `workspace_id` for ownership and `recipient_user_id` for the recipient relationship. Do not rename `recipient_user_id` to `user_id` merely to satisfy a generator or Doctor check; that would change the resource's ownership semantics.
|
|
171
|
-
|
|
172
|
-
### What each value means
|
|
173
|
-
|
|
174
|
-
#### `public`
|
|
175
|
-
|
|
176
|
-
Choose `public` when the same records should be visible across the app, subject only to the usual route/action permissions.
|
|
177
|
-
|
|
178
|
-
This is the right mental model for things like:
|
|
179
|
-
|
|
180
|
-
- global lookup tables
|
|
181
|
-
- app-wide reference data
|
|
182
|
-
- shared configuration records that are not owned by one user or one workspace
|
|
183
|
-
|
|
184
|
-
A `public` CRUD does **not** expect owner scoping columns.
|
|
185
|
-
|
|
186
|
-
#### `user`
|
|
187
|
-
|
|
188
|
-
Choose `user` when each record belongs to one user, regardless of which app surface is showing it.
|
|
189
|
-
|
|
190
|
-
This is the right mental model for things like:
|
|
191
|
-
|
|
192
|
-
- my own saved items
|
|
193
|
-
- my saved views
|
|
194
|
-
- my personal preferences records when they are stored as CRUD rows
|
|
195
|
-
|
|
196
|
-
The important owner column is:
|
|
197
|
-
|
|
198
|
-
- `user_id`
|
|
199
|
-
|
|
200
|
-
#### `workspace`
|
|
201
|
-
|
|
202
|
-
Choose `workspace` when records belong to the workspace as a whole.
|
|
203
|
-
|
|
204
|
-
This is the most common choice for workspace-admin CRUDs such as:
|
|
205
|
-
|
|
206
|
-
- contacts
|
|
207
|
-
- appointments
|
|
208
|
-
- products
|
|
209
|
-
- workspace-wide CRM records
|
|
210
|
-
|
|
211
|
-
The important owner column is:
|
|
212
|
-
|
|
213
|
-
- `workspace_id`
|
|
214
|
-
|
|
215
|
-
This is why the chapter's `contacts` example uses `workspace`: the records belong to the workspace, not to one specific member inside it.
|
|
216
|
-
|
|
217
|
-
#### `workspace_user`
|
|
218
|
-
|
|
219
|
-
Choose `workspace_user` when a record belongs to a workspace **and** to a specific user within that workspace.
|
|
220
|
-
|
|
221
|
-
This is the right mental model for things like:
|
|
222
|
-
|
|
223
|
-
- personal notes inside a workspace
|
|
224
|
-
- per-user workspace drafts
|
|
225
|
-
- user-owned workspace artifacts that should not be visible to every workspace member
|
|
226
|
-
|
|
227
|
-
The important owner columns are:
|
|
228
|
-
|
|
229
|
-
- `workspace_id`
|
|
230
|
-
- `user_id`
|
|
231
|
-
|
|
232
|
-
This is stricter than plain `workspace`. The row is not only "inside this workspace." It is "inside this workspace, for this user."
|
|
233
|
-
|
|
234
|
-
#### `auto`
|
|
235
|
-
|
|
236
|
-
`auto` is not a runtime mode. It is a **generation-time inference**.
|
|
237
|
-
|
|
238
|
-
When you scaffold against a real table, the generator inspects the table columns and resolves `auto` like this:
|
|
239
|
-
|
|
240
|
-
- `workspace_id` and `user_id` present -> `workspace_user`
|
|
241
|
-
- `workspace_id` only -> `workspace`
|
|
242
|
-
- `user_id` only -> `user`
|
|
243
|
-
- neither present -> `public`
|
|
244
|
-
|
|
245
|
-
So `auto` is useful when the table already exists and its ownership columns already tell the truth.
|
|
246
|
-
|
|
247
|
-
If you already know the intended ownership model and want the command to fail when the table does not match it, choose the explicit value instead of `auto`.
|
|
248
|
-
|
|
249
|
-
### Which one should I choose?
|
|
250
|
-
|
|
251
|
-
Use this rule of thumb:
|
|
252
|
-
|
|
253
|
-
- choose `public` for shared reference/config data
|
|
254
|
-
- choose `user` for "my own" records
|
|
255
|
-
- choose `workspace` for workspace-wide business data
|
|
256
|
-
- choose `workspace_user` for personal records that still live inside a workspace
|
|
257
|
-
- choose `auto` when you are scaffolding from an existing table and want JSKIT to infer ownership from the owner columns already present
|
|
258
|
-
|
|
259
|
-
Another useful way to think about it is this:
|
|
260
|
-
|
|
261
|
-
- if two different users in the same workspace should normally see the same row, that is probably `workspace`
|
|
262
|
-
- if they should **not** normally see the same row, that is probably `workspace_user`
|
|
263
|
-
- if the row is not workspace-specific at all, it is probably `public` or `user`
|
|
264
|
-
|
|
265
|
-
### One more important constraint
|
|
266
|
-
|
|
267
|
-
Ownership also has to make sense for the target surface.
|
|
268
|
-
|
|
269
|
-
In practice:
|
|
270
|
-
|
|
271
|
-
- `workspace` and `workspace_user` belong on workspace-enabled surfaces
|
|
272
|
-
- `public` and `user` can live on non-workspace surfaces too
|
|
273
|
-
|
|
274
|
-
So if you are generating under a route like:
|
|
275
|
-
|
|
276
|
-
```text
|
|
277
|
-
w/[workspaceSlug]/admin/...
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
then `workspace` is usually the normal default. If you are generating a CRUD for a global operator or account area, `public` or `user` may make more sense.
|
|
281
|
-
|
|
282
|
-
## Schema contract for generated CRUD
|
|
283
|
-
|
|
284
|
-
Generated CRUD tables use one single-column integer primary key, normally
|
|
285
|
-
`id`. Each foreign key must also be single-column and must reference the target
|
|
286
|
-
table's single-column primary key. Composite indexes remain useful for business
|
|
287
|
-
uniqueness such as `(workspace_id, slug)`, but they are never relationship
|
|
288
|
-
targets.
|
|
289
|
-
|
|
290
|
-
Tenant isolation comes from a direct `workspace_id` and/or `user_id` ownership
|
|
291
|
-
column, the exactly matching generated ownership filter, scoped service
|
|
292
|
-
lookups, and cross-tenant tests. Do not encode ownership into a composite
|
|
293
|
-
foreign key such as `(workspace_id, parent_id) -> (workspace_id, id)`.
|
|
294
|
-
|
|
295
|
-
The generator rejects unsupported key shapes. If an approved application truly
|
|
296
|
-
needs a different persistence model, record the durable decision in
|
|
297
|
-
`.jskit/APP_BLUEPRINT.md` and use the explicit table-ownership exception path
|
|
298
|
-
instead of modifying generated CRUD files.
|
|
299
|
-
|
|
300
|
-
Mutual foreign keys are valid when each side still follows that key contract.
|
|
301
|
-
The server generator keeps table creation and foreign-key installation in
|
|
302
|
-
separate migration phases: every ordinary migration creates its table first,
|
|
303
|
-
then migrations under `migrations/constraints/` add the constraints. Do not
|
|
304
|
-
move those constraints back into the table-creation migrations or disable
|
|
305
|
-
foreign-key checks to work around ordering.
|
|
306
|
-
|
|
307
|
-
## Example 1: `contacts`
|
|
308
|
-
|
|
309
|
-
This is the baseline pattern. If you understand this example, the rest of the chapter becomes much easier.
|
|
310
|
-
|
|
311
|
-
### Step 1: create the table first
|
|
312
|
-
|
|
313
|
-
Start with a real table. The server generator reads the database schema; it does not invent it for you.
|
|
314
|
-
|
|
315
|
-
Example:
|
|
316
|
-
|
|
317
|
-
```sql
|
|
318
|
-
CREATE TABLE contacts (
|
|
319
|
-
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
320
|
-
workspace_id BIGINT UNSIGNED NOT NULL,
|
|
321
|
-
full_name VARCHAR(190) NOT NULL,
|
|
322
|
-
email VARCHAR(190) NULL,
|
|
323
|
-
phone VARCHAR(50) NULL,
|
|
324
|
-
notes TEXT NULL,
|
|
325
|
-
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
326
|
-
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
327
|
-
KEY idx_contacts_workspace_id (workspace_id)
|
|
328
|
-
);
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
The exact fields will vary by app. What matters for the generator is:
|
|
332
|
-
|
|
333
|
-
- the table already exists
|
|
334
|
-
- the ownership columns match the ownership filter you are going to choose
|
|
335
|
-
- the column names are stable enough to become part of your app's resource contract
|
|
336
|
-
- if you are using `crud-server-generator`, do **not** hand-write a separate CRUD migration for this table; the server generator installs the CRUD migration scaffold itself
|
|
337
|
-
|
|
338
|
-
That last rule governs the generated baseline, not every future schema change.
|
|
339
|
-
Never modify or replace a generator-owned baseline migration after it has been
|
|
340
|
-
installed. Later schema evolution must use a new immutable, package-owned
|
|
341
|
-
additive migration in the table's app-local package, declared through
|
|
342
|
-
`install-migration`.
|
|
343
|
-
|
|
344
|
-
Create that source and `package.json.jskit` mutation together with:
|
|
345
|
-
|
|
346
|
-
```bash
|
|
347
|
-
npx jskit create migration \
|
|
348
|
-
--package @local/contacts \
|
|
349
|
-
--id add-contact-status
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
Implement the generated template before running
|
|
353
|
-
`npx jskit migrations sync`. SQL or Knex schema operations
|
|
354
|
-
inside the source-controlled migration are supported. Running ad-hoc SQL
|
|
355
|
-
against only one database is not, because it creates schema drift.
|
|
356
|
-
|
|
357
|
-
In this table, `workspace_id` is the important ownership clue. That is why the next step uses:
|
|
358
|
-
|
|
359
|
-
```bash
|
|
360
|
-
--ownership-filter workspace
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
This CRUD is not "my contacts." It is "the workspace's contacts."
|
|
364
|
-
|
|
365
|
-
### Step 2: scaffold the server package
|
|
366
|
-
|
|
367
|
-
Now generate the server-side CRUD package:
|
|
368
|
-
|
|
369
|
-
```bash
|
|
370
|
-
npx jskit generate crud-server-generator scaffold \
|
|
371
|
-
--namespace contacts \
|
|
372
|
-
--surface admin \
|
|
373
|
-
--ownership-filter workspace \
|
|
374
|
-
--table-name contacts \
|
|
375
|
-
--grant-role member
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
This creates an app-local package under `packages/contacts/`.
|
|
379
|
-
|
|
380
|
-
If the table should already be CRUD-owned but should **not** expose public HTTP CRUD routes yet, add:
|
|
381
|
-
|
|
382
|
-
```bash
|
|
383
|
-
--internal
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
That keeps the generated repository, service, actions, provider, shared resource, and CRUD migration ownership chain exactly the same. The only difference is that the generated HTTP CRUD routes are marked internal, so the public HTTP runtime does not register them.
|
|
387
|
-
|
|
388
|
-
Use that when:
|
|
389
|
-
|
|
390
|
-
- other server modules need a proper CRUD-backed table and shared resource contract
|
|
391
|
-
- you want to avoid direct knex
|
|
392
|
-
- you are not ready to expose list/view/create/update/delete URLs yet
|
|
393
|
-
|
|
394
|
-
Do **not** use `--internal` as a substitute for ownership or permissions design. It is only about whether the public HTTP CRUD routes exist.
|
|
395
|
-
|
|
396
|
-
### Choose workspace role grants explicitly
|
|
397
|
-
|
|
398
|
-
Workspace-required CRUD actions use the normal named permission ids:
|
|
399
|
-
|
|
400
|
-
- `crud.<namespace>.list`
|
|
401
|
-
- `crud.<namespace>.view`
|
|
402
|
-
- `crud.<namespace>.create`
|
|
403
|
-
- `crud.<namespace>.update`
|
|
404
|
-
- `crud.<namespace>.delete`
|
|
405
|
-
|
|
406
|
-
The generator never guesses which role should receive all five permissions. Every workspace-required CRUD command must choose one of these options:
|
|
407
|
-
|
|
408
|
-
```bash
|
|
409
|
-
--grant-role administrator
|
|
410
|
-
```
|
|
411
|
-
|
|
412
|
-
or:
|
|
413
|
-
|
|
414
|
-
```bash
|
|
415
|
-
--no-role-grant
|
|
416
|
-
```
|
|
417
|
-
|
|
418
|
-
`--grant-role` must name a configured role. `--no-role-grant` keeps the generated actions and their permission requirements but leaves the app's role catalog unchanged. The options are mutually exclusive.
|
|
419
|
-
|
|
420
|
-
To assign the generated permissions to `member`, make that decision explicit:
|
|
421
|
-
|
|
422
|
-
```bash
|
|
423
|
-
--grant-role member
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
This decision is independent of `--internal`. For an internal, workspace-owned persistence resource whose generated actions should not be assigned to any actor role, use both:
|
|
427
|
-
|
|
428
|
-
```bash
|
|
429
|
-
--ownership-filter workspace \
|
|
430
|
-
--internal \
|
|
431
|
-
--no-role-grant
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
If neither explicit grant option is supplied, generation fails during preflight before writing files, even when a `member` role exists.
|
|
435
|
-
|
|
436
|
-
Before generating anything, decide these with the developer:
|
|
437
|
-
|
|
438
|
-
- which operations are allowed for this CRUD
|
|
439
|
-
- which fields belong in the list view if a list exists
|
|
440
|
-
- what the view form should look like
|
|
441
|
-
- what the edit/new form should look like
|
|
442
|
-
|
|
443
|
-
The most important file from the UI point of view is:
|
|
444
|
-
|
|
445
|
-
```text
|
|
446
|
-
packages/contacts/src/shared/contactResource.js
|
|
447
|
-
```
|
|
448
|
-
|
|
449
|
-
That file is the shared CRUD contract. The UI generator reads it to decide:
|
|
450
|
-
|
|
451
|
-
- which operations exist
|
|
452
|
-
- which fields are readable or writable
|
|
453
|
-
- which relations are exposed
|
|
454
|
-
- how to build the generated pages
|
|
455
|
-
|
|
456
|
-
So even though the server scaffold writes many files, the resource file is the bridge between the server and UI halves.
|
|
457
|
-
|
|
458
|
-
For standard CRUDs, that file is 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.
|
|
459
|
-
|
|
460
|
-
The generated server action validators are compact for the same reason.
|
|
461
|
-
Standard list actions compose
|
|
462
|
-
`createStandardCrudListQueryValidators({ resource })`; standard view actions
|
|
463
|
-
compose `createStandardCrudViewQueryValidators()`. Do not expand those groups
|
|
464
|
-
back into individual pagination, search, parent-filter, include, or
|
|
465
|
-
sparse-field validators.
|
|
466
|
-
|
|
467
|
-
When a CRUD adds a server-backed structured-filter contract, pass its
|
|
468
|
-
`queryValidator` through the list group's dedicated
|
|
469
|
-
`listFilterQueryValidator` option. If
|
|
470
|
-
`resource.contract.listFilters.queryValidator` already owns it, `{ resource }`
|
|
471
|
-
is sufficient. A validator belongs after the standard group only when it is
|
|
472
|
-
genuinely additional, non-filter query input. This keeps route and action
|
|
473
|
-
validation independent while preventing the two boundaries from drifting as
|
|
474
|
-
the standard query contract evolves.
|
|
475
|
-
|
|
476
|
-
### Step 3: scaffold the UI
|
|
477
|
-
|
|
478
|
-
Once the resource file exists, generate the UI route tree:
|
|
479
|
-
|
|
480
|
-
```bash
|
|
481
|
-
npx jskit generate crud-ui-generator crud \
|
|
482
|
-
w/[workspaceSlug]/admin/contacts \
|
|
483
|
-
--resource-file packages/contacts/src/shared/contactResource.js \
|
|
484
|
-
--id-param contactId \
|
|
485
|
-
--display-fields fullName,email,phone \
|
|
486
|
-
--delete-confirmation
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
That creates the baseline CRUD route tree:
|
|
490
|
-
|
|
491
|
-
- `w/[workspaceSlug]/admin/contacts/index.vue`
|
|
492
|
-
- `w/[workspaceSlug]/admin/contacts/new.vue`
|
|
493
|
-
- `w/[workspaceSlug]/admin/contacts/[contactId]/index.vue`
|
|
494
|
-
- `w/[workspaceSlug]/admin/contacts/[contactId]/edit.vue`
|
|
495
|
-
- shared form files under the mirrored non-routed
|
|
496
|
-
`src/components/w/[workspaceSlug]/admin/contacts/` root
|
|
497
|
-
|
|
498
|
-
The mirrored component root is intentional. The configured file router scans
|
|
499
|
-
Vue files below `src/pages/`, so reusable Vue helpers must stay outside that
|
|
500
|
-
directory or they become browser routes.
|
|
501
|
-
|
|
502
|
-
`--delete-confirmation` is opt-in. When present, the generated view extends
|
|
503
|
-
the public `CrudViewScreen` `actions` slot with `CrudDeleteAction`. That shared
|
|
504
|
-
component owns the destructive button and Vuetify alert dialog. The public
|
|
505
|
-
`useCrudDeleteAction()` composable resolves the current route id through the
|
|
506
|
-
CRUD runtime, runs the shared resource's `DELETE` operation through
|
|
507
|
-
`useCommand()`, disables duplicate submission, keeps a useful error on the
|
|
508
|
-
record screen, invalidates the list query, and navigates to the generated list
|
|
509
|
-
route after success. It supports custom `--id-param` names.
|
|
510
|
-
|
|
511
|
-
The generator rejects this option when list or view is omitted, or when the
|
|
512
|
-
shared resource has no `DELETE` operation. Without the flag, no delete control
|
|
513
|
-
is generated. Do not substitute raw `fetch()` or import private `http-web`
|
|
514
|
-
modules.
|
|
515
|
-
|
|
516
|
-
Generated list, view, and lookup reads use the resource contract as their
|
|
517
|
-
response authority. They return every field declared for output by default,
|
|
518
|
-
including the target resource's declared output when a lookup relation is
|
|
519
|
-
hydrated; generated pages do not repeat those fields in a second page-owned
|
|
520
|
-
allowlist. Adding an output field or lookup to the resource therefore does not
|
|
521
|
-
require regenerating endpoint projections.
|
|
522
|
-
|
|
523
|
-
Put exceptional large fields in the resource-owned default response blacklist,
|
|
524
|
-
`resource.contract.response.defaultExclude`:
|
|
525
|
-
|
|
526
|
-
```js
|
|
527
|
-
contract: {
|
|
528
|
-
response: {
|
|
529
|
-
defaultExclude: ["rawPayload", "largeAuditJson"]
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
Each included resource applies its own blacklist. A default-excluded field is
|
|
535
|
-
still part of the public output contract and can be requested through an
|
|
536
|
-
explicit typed JSON:API `fields[type]` fieldset. Use that sparse-field override
|
|
537
|
-
only for a specialised caller; do not add one to ordinary generated pages or
|
|
538
|
-
build a page-local query-string adapter. Fields that must never be exposed do
|
|
539
|
-
not belong in the output schema at all.
|
|
540
|
-
|
|
541
|
-
This is the most important mental model in the whole chapter:
|
|
542
|
-
|
|
543
|
-
- `crud-server-generator` creates the reusable CRUD contract and server package
|
|
544
|
-
- `crud-ui-generator` turns that contract into app-owned pages
|
|
545
|
-
|
|
546
|
-
If you want to see exactly what lands in `packages/contacts/`, which files own repository/service/action logic, and how search/filter customization is supposed to work, continue with [Advanced CRUDs](/guide/generators/advanced-cruds) once this baseline flow is clear.
|
|
547
|
-
|
|
548
|
-
### Why this is the baseline example
|
|
549
|
-
|
|
550
|
-
`contacts` is the cleanest first example because nothing fancy is happening yet:
|
|
551
|
-
|
|
552
|
-
- it has a normal routed list page
|
|
553
|
-
- it has a normal routed view page
|
|
554
|
-
- it has normal `new` and `edit` pages
|
|
555
|
-
- it uses one straightforward resource contract
|
|
556
|
-
|
|
557
|
-
That is the shape to learn first.
|
|
558
|
-
|
|
559
|
-
## Example 2: `addresses`
|
|
560
|
-
|
|
561
|
-
Now move to a child CRUD with its own URL.
|
|
562
|
-
|
|
563
|
-
This is the right pattern when the child collection deserves its own list page, but should still be reached from a specific parent record.
|
|
564
|
-
|
|
565
|
-
Example route:
|
|
566
|
-
|
|
567
|
-
```text
|
|
568
|
-
.../contacts/3/addresses
|
|
569
|
-
```
|
|
570
|
-
|
|
571
|
-
In the guide app's real route tree, the visible URL becomes:
|
|
572
|
-
|
|
573
|
-
```text
|
|
574
|
-
w/[workspaceSlug]/admin/contacts/[contactId]/addresses
|
|
575
|
-
```
|
|
576
|
-
|
|
577
|
-
This example is intentionally **not** an in-page child list and **not** a contact subtab.
|
|
578
|
-
|
|
579
|
-
The intended UX is:
|
|
580
|
-
|
|
581
|
-
- `addresses` gets its own routed list page
|
|
582
|
-
- the page lives under a specific contact
|
|
583
|
-
- the user reaches it from an explicit link or button on the contact view
|
|
584
|
-
- it does **not** appear in the global left menu
|
|
585
|
-
|
|
586
|
-
That makes it different from the later `comments` example, where the child list stays inside the parent page.
|
|
587
|
-
|
|
588
|
-
### Step 1: create the child table
|
|
589
|
-
|
|
590
|
-
Example:
|
|
591
|
-
|
|
592
|
-
```sql
|
|
593
|
-
CREATE TABLE addresses (
|
|
594
|
-
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
595
|
-
workspace_id BIGINT UNSIGNED NOT NULL,
|
|
596
|
-
contact_id BIGINT UNSIGNED NOT NULL,
|
|
597
|
-
label VARCHAR(100) NULL,
|
|
598
|
-
line_1 VARCHAR(190) NOT NULL,
|
|
599
|
-
line_2 VARCHAR(190) NULL,
|
|
600
|
-
suburb VARCHAR(120) NULL,
|
|
601
|
-
state VARCHAR(80) NULL,
|
|
602
|
-
postcode VARCHAR(20) NULL,
|
|
603
|
-
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
604
|
-
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
605
|
-
KEY idx_addresses_workspace_id (workspace_id),
|
|
606
|
-
KEY idx_addresses_contact_id (contact_id),
|
|
607
|
-
CONSTRAINT fk_addresses_contact_id
|
|
608
|
-
FOREIGN KEY (contact_id) REFERENCES contacts(id)
|
|
609
|
-
);
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
The important extra column here is `contact_id`. This is what makes the CRUD a child of a contact instead of a top-level resource.
|
|
613
|
-
|
|
614
|
-
The foreign key is not optional in this example. It is what lets `crud-server-generator` emit `contactId` as a real lookup relation in `packages/addresses/src/shared/addressResource.js`.
|
|
615
|
-
|
|
616
|
-
### Step 2: scaffold the server package
|
|
617
|
-
|
|
618
|
-
```bash
|
|
619
|
-
npx jskit generate crud-server-generator scaffold \
|
|
620
|
-
--namespace addresses \
|
|
621
|
-
--surface admin \
|
|
622
|
-
--ownership-filter workspace \
|
|
623
|
-
--table-name addresses \
|
|
624
|
-
--grant-role member
|
|
625
|
-
```
|
|
626
|
-
|
|
627
|
-
### Step 3: refine the generated lookup metadata by hand
|
|
628
|
-
|
|
629
|
-
Open `packages/addresses/src/shared/addressResource.js` and add `labelKey: "fullName"` to the generated `contactId` relation:
|
|
630
|
-
|
|
631
|
-
```js
|
|
632
|
-
contactId: {
|
|
633
|
-
type: "id",
|
|
634
|
-
required: true,
|
|
635
|
-
search: true,
|
|
636
|
-
relation: {
|
|
637
|
-
kind: "lookup",
|
|
638
|
-
namespace: "contacts",
|
|
639
|
-
valueKey: "id",
|
|
640
|
-
labelKey: "fullName"
|
|
641
|
-
},
|
|
642
|
-
belongsTo: "contacts",
|
|
643
|
-
as: "contact",
|
|
644
|
-
ui: { formControl: "autocomplete" },
|
|
645
|
-
...
|
|
646
|
-
}
|
|
647
|
-
```
|
|
648
|
-
|
|
649
|
-
This part is still manual today.
|
|
650
|
-
|
|
651
|
-
The scaffold can infer the lookup **relationship** from the foreign key, but it cannot infer which contact field should be used as the human-readable label. Without `labelKey: "fullName"`, the page falls back to generic headings like `Addresses for Contact #1`.
|
|
652
|
-
|
|
653
|
-
### Step 4: scaffold the UI route tree
|
|
654
|
-
|
|
655
|
-
```bash
|
|
656
|
-
npx jskit generate crud-ui-generator crud \
|
|
657
|
-
w/[workspaceSlug]/admin/contacts/[contactId]/addresses \
|
|
658
|
-
--resource-file packages/addresses/src/shared/addressResource.js \
|
|
659
|
-
--id-param addressId \
|
|
660
|
-
--parent-title contextual \
|
|
661
|
-
--display-fields label,line1,postcode
|
|
662
|
-
```
|
|
663
|
-
|
|
664
|
-
That gives you a normal child route tree:
|
|
665
|
-
|
|
666
|
-
- `w/[workspaceSlug]/admin/contacts/[contactId]/addresses/index.vue`
|
|
667
|
-
- `w/[workspaceSlug]/admin/contacts/[contactId]/addresses/new.vue`
|
|
668
|
-
- `w/[workspaceSlug]/admin/contacts/[contactId]/addresses/[addressId]/index.vue`
|
|
669
|
-
- `w/[workspaceSlug]/admin/contacts/[contactId]/addresses/[addressId]/edit.vue`
|
|
670
|
-
- shared form files under the mirrored non-routed
|
|
671
|
-
`src/components/w/[workspaceSlug]/admin/contacts/[contactId]/addresses/` root
|
|
672
|
-
|
|
673
|
-
### Step 5: remove the generated shell placement by hand
|
|
674
|
-
|
|
675
|
-
`crud-ui-generator` currently generates a list-page placement block in `src/placement.js`.
|
|
676
|
-
|
|
677
|
-
For this example, that block is **not** what you want.
|
|
678
|
-
|
|
679
|
-
Delete the generated `addresses` placement block from `src/placement.js`.
|
|
680
|
-
|
|
681
|
-
It will be the block added for the `w/[workspaceSlug]/admin/contacts/[contactId]/addresses` page link.
|
|
682
|
-
|
|
683
|
-
Depending on what outlets already exist in the app, that block may try to place `Addresses` in the shell menu or in the contact subpages outlet. For this example, remove it either way and keep navigation manual from the contact view.
|
|
684
|
-
|
|
685
|
-
This workflow has one intentional manual cleanup:
|
|
686
|
-
|
|
687
|
-
- keep the routed pages
|
|
688
|
-
- remove the auto menu link
|
|
689
|
-
- navigate to the page from the contact view instead
|
|
690
|
-
|
|
691
|
-
### Step 6: add a manual link from the contact view
|
|
692
|
-
|
|
693
|
-
Open `src/pages/w/[workspaceSlug]/admin/contacts/[contactId]/index.vue` and add a button that links to `./addresses`.
|
|
694
|
-
|
|
695
|
-
The simplest version is:
|
|
696
|
-
|
|
697
|
-
```vue
|
|
698
|
-
<v-btn
|
|
699
|
-
color="primary"
|
|
700
|
-
variant="tonal"
|
|
701
|
-
:to="{ path: view.resolveParams(UI_ADDRESSES_URL), query: $route.query }"
|
|
702
|
-
>
|
|
703
|
-
Addresses
|
|
704
|
-
</v-btn>
|
|
705
|
-
```
|
|
706
|
-
|
|
707
|
-
Then define the URL template in the script:
|
|
708
|
-
|
|
709
|
-
```js
|
|
710
|
-
const UI_ADDRESSES_URL = "./addresses";
|
|
711
|
-
```
|
|
712
|
-
|
|
713
|
-
That gives you a standalone child page with a clear entry point from the parent contact view, without mixing record-scoped pages into the global shell menu.
|
|
714
|
-
|
|
715
|
-
### The important refinement: parent title handling
|
|
716
|
-
|
|
717
|
-
This is where nested CRUD starts to feel real.
|
|
718
|
-
|
|
719
|
-
An addresses list page should not show a generic heading like `Addresses`. It should usually show whose addresses these are, for example:
|
|
720
|
-
|
|
721
|
-
- `Addresses for Jane Smith`
|
|
722
|
-
- `Addresses for Acme Pty Ltd`
|
|
723
|
-
|
|
724
|
-
The awkward case is when there are **no child rows yet**. If the list is empty, you cannot derive the parent title from the first address record.
|
|
725
|
-
|
|
726
|
-
That is why JSKIT has `useCrudListParentTitle()`:
|
|
727
|
-
|
|
728
|
-
```js
|
|
729
|
-
const parentTitle = useCrudListParentTitle({
|
|
730
|
-
listRuntime: records,
|
|
731
|
-
resource: uiResource,
|
|
732
|
-
recordIdParam: "addressId",
|
|
733
|
-
fallbackLoadError: "Unable to load contact.",
|
|
734
|
-
notFoundMessage: "Contact not found."
|
|
735
|
-
});
|
|
736
|
-
```
|
|
737
|
-
|
|
738
|
-
That helper does the right thing automatically:
|
|
739
|
-
|
|
740
|
-
- if the child list already has rows, it derives the parent title from the first child record when lookup data is available
|
|
741
|
-
- if the child list is empty, it loads the parent record directly
|
|
742
|
-
|
|
743
|
-
So the `addresses` example is important because it teaches the first truly practical child-page problem:
|
|
744
|
-
|
|
745
|
-
- the route is child-scoped
|
|
746
|
-
- the data is child-scoped
|
|
747
|
-
- the page is reached from a parent action, not from the main shell menu
|
|
748
|
-
- the page still needs a reliable parent identity even when the child list is empty
|
|
749
|
-
|
|
750
|
-
## Example 3: `comments` under the contact host
|
|
751
|
-
|
|
752
|
-
This is the advanced example.
|
|
753
|
-
|
|
754
|
-
Sometimes a child resource should stay attached to the parent record, but still deserves its own routed child page.
|
|
755
|
-
|
|
756
|
-
Comments are a good example:
|
|
757
|
-
|
|
758
|
-
- they matter
|
|
759
|
-
- they need persistence
|
|
760
|
-
- they benefit from their own list, `new`, `view`, and `edit` routes
|
|
761
|
-
- but they still belong inside the contact experience rather than as a top-level destination
|
|
762
|
-
|
|
763
|
-
So the pattern is:
|
|
764
|
-
|
|
765
|
-
- keep the contact page as the real host
|
|
766
|
-
- keep the child route tree under that host
|
|
767
|
-
- let the generated placement surface the comments page as a contact child page
|
|
768
|
-
|
|
769
|
-
### Step 1: create the table
|
|
770
|
-
|
|
771
|
-
Example:
|
|
772
|
-
|
|
773
|
-
```sql
|
|
774
|
-
CREATE TABLE comments (
|
|
775
|
-
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
776
|
-
workspace_id BIGINT UNSIGNED NOT NULL,
|
|
777
|
-
contact_id BIGINT UNSIGNED NOT NULL,
|
|
778
|
-
body TEXT NOT NULL,
|
|
779
|
-
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
780
|
-
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
781
|
-
KEY idx_comments_workspace_id (workspace_id),
|
|
782
|
-
KEY idx_comments_contact_id (contact_id),
|
|
783
|
-
CONSTRAINT fk_comments_contact_id
|
|
784
|
-
FOREIGN KEY (contact_id) REFERENCES contacts(id)
|
|
785
|
-
);
|
|
786
|
-
```
|
|
787
|
-
|
|
788
|
-
### Step 2: scaffold the server package
|
|
789
|
-
|
|
790
|
-
```bash
|
|
791
|
-
npx jskit generate crud-server-generator scaffold \
|
|
792
|
-
--namespace comments \
|
|
793
|
-
--surface admin \
|
|
794
|
-
--ownership-filter workspace \
|
|
795
|
-
--table-name comments \
|
|
796
|
-
--grant-role member
|
|
797
|
-
```
|
|
798
|
-
|
|
799
|
-
### Step 3: refine the generated lookup metadata by hand
|
|
800
|
-
|
|
801
|
-
Open `packages/comments/src/shared/commentResource.js` and add `labelKey: "fullName"` to the generated `contactId` relation:
|
|
802
|
-
|
|
803
|
-
```js
|
|
804
|
-
contactId: {
|
|
805
|
-
type: "id",
|
|
806
|
-
required: true,
|
|
807
|
-
search: true,
|
|
808
|
-
relation: {
|
|
809
|
-
kind: "lookup",
|
|
810
|
-
namespace: "contacts",
|
|
811
|
-
valueKey: "id",
|
|
812
|
-
labelKey: "fullName"
|
|
813
|
-
},
|
|
814
|
-
belongsTo: "contacts",
|
|
815
|
-
as: "contact",
|
|
816
|
-
ui: { formControl: "autocomplete" },
|
|
817
|
-
...
|
|
818
|
-
}
|
|
819
|
-
```
|
|
820
|
-
|
|
821
|
-
This is the same manual refinement used in the `addresses` example: the scaffold can infer the lookup relationship from the foreign key, but it cannot infer which parent field should be used as the display label.
|
|
822
|
-
|
|
823
|
-
The next command also makes the parent-title mode explicit on purpose. That way changing the heading behavior later is just changing the value and rerunning with `--force`, not adding a new flag.
|
|
824
|
-
|
|
825
|
-
### Step 4: make the parent page able to host routed children
|
|
826
|
-
|
|
827
|
-
If the contact view page should stay visible while child comment routes render underneath it, first upgrade it into a routed host:
|
|
828
|
-
|
|
829
|
-
```bash
|
|
830
|
-
npx jskit generate ui-generator add-subpages \
|
|
831
|
-
w/[workspaceSlug]/admin/contacts/[contactId]/index.vue \
|
|
832
|
-
--title "Contact" \
|
|
833
|
-
--subtitle "Contact activity and notes."
|
|
834
|
-
```
|
|
835
|
-
|
|
836
|
-
That is the point where the comments example deliberately overlaps with the `ui-generator` chapter. This pattern needs both:
|
|
837
|
-
|
|
838
|
-
- CRUD scaffolding for the comments resource
|
|
839
|
-
- a routed host in the parent contact page
|
|
840
|
-
|
|
841
|
-
### Step 4.5: make `comments` the default child page
|
|
842
|
-
|
|
843
|
-
This is a very common next step.
|
|
844
|
-
|
|
845
|
-
If the contact page should open directly on `Comments`, add an explicit redirect to the contact host page:
|
|
846
|
-
|
|
847
|
-
```vue
|
|
848
|
-
<script setup>
|
|
849
|
-
import { redirectToChild } from "@jskit-ai/kernel/client/pageRedirects";
|
|
850
|
-
|
|
851
|
-
definePage({
|
|
852
|
-
redirect: redirectToChild("comments")
|
|
853
|
-
});
|
|
854
|
-
</script>
|
|
855
|
-
```
|
|
856
|
-
|
|
857
|
-
In this example, that edit belongs in:
|
|
858
|
-
|
|
859
|
-
```text
|
|
860
|
-
src/pages/w/[workspaceSlug]/admin/contacts/[contactId]/index.vue
|
|
861
|
-
```
|
|
862
|
-
|
|
863
|
-
This is the recommended pattern. Do **not** try to infer the default child page from placement order or "the first tab". Keep it explicit.
|
|
864
|
-
|
|
865
|
-
When this redirect is present:
|
|
866
|
-
|
|
867
|
-
- opening `/w/<workspaceSlug>/admin/contacts/<contactId>` lands on `/w/<workspaceSlug>/admin/contacts/<contactId>/comments`
|
|
868
|
-
- the parent contact page still stays visible
|
|
869
|
-
- the child page renders underneath it
|
|
870
|
-
|
|
871
|
-
That last point matters. The contact page is still the host page. The redirect only changes which child route becomes the default landing destination.
|
|
872
|
-
|
|
873
|
-
### Step 5: generate the full child CRUD page
|
|
874
|
-
|
|
875
|
-
Now generate the comments UI under the contact host:
|
|
876
|
-
|
|
877
|
-
```bash
|
|
878
|
-
npx jskit generate crud-ui-generator crud \
|
|
879
|
-
w/[workspaceSlug]/admin/contacts/[contactId]/index/comments \
|
|
880
|
-
--resource-file packages/comments/src/shared/commentResource.js \
|
|
881
|
-
--id-param commentId \
|
|
882
|
-
--parent-title none \
|
|
883
|
-
--display-fields body
|
|
884
|
-
```
|
|
885
|
-
|
|
886
|
-
Do **not** pass `--operations view,new,edit` here. That would omit the list page and leave you with only child operation routes.
|
|
887
|
-
|
|
888
|
-
`--parent-title none` is the important difference from `addresses`.
|
|
889
|
-
|
|
890
|
-
The contact host page already shows the parent identity, so a generated heading like `Comments for Tony Mobily` is redundant. If you later decide you do want that heading, rerun the same command with `--parent-title contextual --force`.
|
|
891
|
-
|
|
892
|
-
This command gives you:
|
|
893
|
-
|
|
894
|
-
- `w/[workspaceSlug]/admin/contacts/[contactId]/index/comments/index.vue`
|
|
895
|
-
- `w/[workspaceSlug]/admin/contacts/[contactId]/index/comments/new.vue`
|
|
896
|
-
- `w/[workspaceSlug]/admin/contacts/[contactId]/index/comments/[commentId]/index.vue`
|
|
897
|
-
- `w/[workspaceSlug]/admin/contacts/[contactId]/index/comments/[commentId]/edit.vue`
|
|
898
|
-
|
|
899
|
-
Because this route root lives under `.../[contactId]/index/comments`, the generated placement is the desired one here: `Comments` appears as a child page under the contact host.
|
|
900
|
-
|
|
901
|
-
That is the real lesson of the `comments` example:
|
|
902
|
-
|
|
903
|
-
- the contact page stays the routed host
|
|
904
|
-
- the child CRUD gets its own list page
|
|
905
|
-
- the route still stays under the parent record experience
|
|
906
|
-
|
|
907
|
-
## When `scaffold-field` matters
|
|
908
|
-
|
|
909
|
-
After the initial scaffold, schema changes are normal.
|
|
910
|
-
|
|
911
|
-
Suppose you add a `preferred_name` column to `contacts`. You do not have to regenerate the whole package just to expose that one new writable field.
|
|
912
|
-
|
|
913
|
-
That is what `scaffold-field` is for:
|
|
914
|
-
|
|
915
|
-
```bash
|
|
916
|
-
npx jskit generate crud-server-generator scaffold-field \
|
|
917
|
-
preferredName \
|
|
918
|
-
packages/contacts/src/shared/contactResource.js \
|
|
919
|
-
--table-name contacts
|
|
920
|
-
```
|
|
921
|
-
|
|
922
|
-
Use this when:
|
|
923
|
-
|
|
924
|
-
- the CRUD package already exists
|
|
925
|
-
- the table has changed
|
|
926
|
-
- you want to patch one writable field into the generated resource file
|
|
927
|
-
|
|
928
|
-
It is a maintenance tool, not the first step in the workflow.
|
|
929
|
-
|
|
930
|
-
It patches the canonical `schema` inside the shared resource file. The standard CRUD validators are derived from that schema, so one authored field definition drives `create`, `patch`, and `view` validation.
|
|
931
|
-
|
|
932
|
-
## Summary
|
|
933
|
-
|
|
934
|
-
The important thing is not memorizing commands. It is learning the three shapes:
|
|
935
|
-
|
|
936
|
-
- `contacts`: a normal top-level CRUD
|
|
937
|
-
- `addresses`: a child CRUD with its own routed list page reached from the parent record view
|
|
938
|
-
- `comments`: a child CRUD that lives under the parent record host as a child page
|
|
939
|
-
|
|
940
|
-
Once you understand those three shapes, the two generator packages stop feeling like separate tools.
|
|
941
|
-
|
|
942
|
-
They become one workflow:
|
|
943
|
-
|
|
944
|
-
1. model the table
|
|
945
|
-
2. scaffold the server/resource package
|
|
946
|
-
3. scaffold only the UI shape that the feature actually needs
|
|
947
|
-
|
|
948
|
-
The next chapter is [Advanced CRUDs](/guide/generators/advanced-cruds). Read it as the structural follow-on to this one: this chapter teaches how to generate the CRUD, and the next one teaches how to reason about the generated files the app owns.
|