@jskit-ai/agent-docs 0.1.131 → 0.1.133

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.
Files changed (171) hide show
  1. package/guide/agent/app-extras/assistant.md +29 -605
  2. package/guide/agent/app-extras/mobile-capacitor.md +29 -362
  3. package/guide/agent/app-extras/realtime.md +29 -277
  4. package/guide/agent/app-setup/a-more-interesting-shell.md +44 -815
  5. package/guide/agent/app-setup/authentication.md +43 -1073
  6. package/guide/agent/app-setup/console.md +26 -298
  7. package/guide/agent/app-setup/database-layer.md +110 -790
  8. package/guide/agent/app-setup/existing-application-migration.md +273 -0
  9. package/guide/agent/app-setup/initial-scaffolding.md +50 -784
  10. package/guide/agent/app-setup/multi-homing.md +39 -712
  11. package/guide/agent/app-setup/quickstart.md +43 -179
  12. package/guide/agent/app-setup/users.md +34 -353
  13. package/guide/agent/index.md +17 -23
  14. package/package.json +2 -2
  15. package/patterns/INDEX.md +7 -7
  16. package/patterns/child-cruds.md +3 -3
  17. package/patterns/client-requests.md +6 -6
  18. package/patterns/crud-authoring.md +94 -0
  19. package/patterns/crud-links.md +1 -1
  20. package/patterns/feature-package/PATTERN.md +108 -0
  21. package/patterns/feature-package/example/booking-engine/package.json +48 -0
  22. package/patterns/feature-package/example/booking-engine/src/server/BookingEngineProvider.js +33 -0
  23. package/patterns/feature-package/example/booking-engine/src/server/actions.js +26 -0
  24. package/patterns/feature-package/example/booking-engine/src/server/inputSchemas.js +19 -0
  25. package/patterns/feature-package/example/variations/AvailabilityEngineProvider.js +31 -0
  26. package/patterns/feature-package/example/variations/InvoiceRollupProvider.js +36 -0
  27. package/patterns/feature-package/example/variations/customKnexRepository.js +34 -0
  28. package/patterns/feature-package/example/variations/orchestratorService.js +23 -0
  29. package/patterns/filters.md +8 -8
  30. package/patterns/live-actions.md +5 -18
  31. package/patterns/minimal-foundation/PATTERN.md +98 -0
  32. package/patterns/minimal-foundation/example/.nvmrc +1 -0
  33. package/patterns/minimal-foundation/example/AGENTS.md +17 -0
  34. package/patterns/minimal-foundation/example/Procfile +2 -0
  35. package/patterns/minimal-foundation/example/app.json +14 -0
  36. package/patterns/minimal-foundation/example/bin/develop.js +71 -0
  37. package/patterns/minimal-foundation/example/bin/server.js +8 -0
  38. package/patterns/minimal-foundation/example/config/public.js +40 -0
  39. package/patterns/minimal-foundation/example/config/server.js +1 -0
  40. package/patterns/minimal-foundation/example/config/surfaceAccessPolicies.js +3 -0
  41. package/patterns/minimal-foundation/example/eslint.config.mjs +19 -0
  42. package/patterns/minimal-foundation/example/favicon.svg +7 -0
  43. package/patterns/minimal-foundation/example/gitignore +9 -0
  44. package/patterns/minimal-foundation/example/index.html +13 -0
  45. package/patterns/minimal-foundation/example/jsconfig.json +8 -0
  46. package/patterns/minimal-foundation/example/package.json +57 -0
  47. package/patterns/minimal-foundation/example/packages/main/package.json +42 -0
  48. package/patterns/minimal-foundation/example/packages/main/src/shared/index.js +10 -0
  49. package/patterns/minimal-foundation/example/packages/main/src/shared/schemas/index.js +22 -0
  50. package/patterns/minimal-foundation/example/playwright.config.mjs +31 -0
  51. package/patterns/minimal-foundation/example/server/lib/runtimeEnv.js +45 -0
  52. package/patterns/minimal-foundation/example/server/lib/surfaceRuntime.js +10 -0
  53. package/patterns/minimal-foundation/example/server.js +195 -0
  54. package/patterns/minimal-foundation/example/src/App.vue +13 -0
  55. package/patterns/minimal-foundation/example/src/main.js +85 -0
  56. package/patterns/minimal-foundation/example/src/pages/home/index.vue +48 -0
  57. package/patterns/minimal-foundation/example/src/pages/home.vue +13 -0
  58. package/patterns/minimal-foundation/example/src/views/NotFound.vue +13 -0
  59. package/patterns/minimal-foundation/example/tests/client/smoke.vitest.js +7 -0
  60. package/patterns/minimal-foundation/example/tests/e2e/base-shell.spec.ts +23 -0
  61. package/patterns/minimal-foundation/example/tests/server/smoke.test.js +16 -0
  62. package/patterns/minimal-foundation/example/vite.config.mjs +81 -0
  63. package/patterns/page-scaffolding.md +20 -17
  64. package/patterns/placements.md +17 -15
  65. package/patterns/row-policies.md +4 -5
  66. package/patterns/server-search.md +3 -3
  67. package/patterns/shell-foundation/PATTERN.md +104 -0
  68. package/patterns/shell-foundation/example/.nvmrc +1 -0
  69. package/patterns/shell-foundation/example/AGENTS.md +17 -0
  70. package/patterns/shell-foundation/example/Procfile +2 -0
  71. package/patterns/shell-foundation/example/app.json +14 -0
  72. package/patterns/shell-foundation/example/bin/develop.js +71 -0
  73. package/patterns/shell-foundation/example/bin/server.js +8 -0
  74. package/patterns/shell-foundation/example/config/public.js +40 -0
  75. package/patterns/shell-foundation/example/config/server.js +1 -0
  76. package/patterns/shell-foundation/example/config/surfaceAccessPolicies.js +3 -0
  77. package/patterns/shell-foundation/example/eslint.config.mjs +19 -0
  78. package/patterns/shell-foundation/example/favicon.svg +7 -0
  79. package/patterns/shell-foundation/example/gitignore +9 -0
  80. package/patterns/shell-foundation/example/index.html +13 -0
  81. package/patterns/shell-foundation/example/jsconfig.json +8 -0
  82. package/patterns/shell-foundation/example/package.json +59 -0
  83. package/patterns/shell-foundation/example/packages/main/package.json +56 -0
  84. package/patterns/shell-foundation/example/packages/main/src/client/index.js +9 -0
  85. package/patterns/shell-foundation/example/packages/main/src/client/providers/MainClientProvider.js +18 -0
  86. package/patterns/shell-foundation/example/packages/main/src/shared/index.js +10 -0
  87. package/patterns/shell-foundation/example/packages/main/src/shared/schemas/index.js +22 -0
  88. package/patterns/shell-foundation/example/playwright.config.mjs +31 -0
  89. package/patterns/shell-foundation/example/server/lib/runtimeEnv.js +45 -0
  90. package/patterns/shell-foundation/example/server/lib/surfaceRuntime.js +10 -0
  91. package/patterns/shell-foundation/example/server.js +195 -0
  92. package/patterns/shell-foundation/example/src/App.vue +11 -0
  93. package/patterns/shell-foundation/example/src/components/ShellLayout.vue +12 -0
  94. package/patterns/shell-foundation/example/src/components/menus/MenuLinkItem.vue +30 -0
  95. package/patterns/shell-foundation/example/src/components/menus/SurfaceAwareMenuLinkItem.vue +42 -0
  96. package/patterns/shell-foundation/example/src/components/menus/TabLinkItem.vue +42 -0
  97. package/patterns/shell-foundation/example/src/error.js +19 -0
  98. package/patterns/shell-foundation/example/src/main.js +85 -0
  99. package/patterns/shell-foundation/example/src/pages/home/index.vue +116 -0
  100. package/patterns/shell-foundation/example/src/pages/home/settings/general/index.vue +40 -0
  101. package/patterns/shell-foundation/example/src/pages/home/settings/index.vue +7 -0
  102. package/patterns/shell-foundation/example/src/pages/home/settings.vue +109 -0
  103. package/patterns/shell-foundation/example/src/pages/home.vue +20 -0
  104. package/patterns/shell-foundation/example/src/placement.js +56 -0
  105. package/patterns/shell-foundation/example/src/placementTopology.js +149 -0
  106. package/patterns/shell-foundation/example/src/views/NotFound.vue +13 -0
  107. package/patterns/shell-foundation/example/tests/client/smoke.vitest.js +7 -0
  108. package/patterns/shell-foundation/example/tests/e2e/adaptive-shell.spec.ts +10 -0
  109. package/patterns/shell-foundation/example/tests/e2e/base-shell.spec.ts +23 -0
  110. package/patterns/shell-foundation/example/tests/server/smoke.test.js +16 -0
  111. package/patterns/shell-foundation/example/vite.config.mjs +81 -0
  112. package/patterns/ui-contract.md +56 -0
  113. package/patterns/ui-testing.md +10 -12
  114. package/reference/autogen/KERNEL_MAP.md +29 -107
  115. package/reference/autogen/PATTERN_INDEX.md +230 -0
  116. package/reference/autogen/README.md +4 -8
  117. package/reference/autogen/packages/agent-docs.md +259 -0
  118. package/reference/autogen/packages/assistant-core.md +3 -3
  119. package/reference/autogen/packages/assistant-runtime.md +32 -17
  120. package/reference/autogen/packages/auth-core.md +31 -33
  121. package/reference/autogen/packages/auth-provider-local-core.md +4 -12
  122. package/reference/autogen/packages/auth-provider-local-db-core.md +4 -4
  123. package/reference/autogen/packages/auth-provider-supabase-core.md +14 -18
  124. package/reference/autogen/packages/auth-web.md +42 -22
  125. package/reference/autogen/packages/console-core.md +8 -25
  126. package/reference/autogen/packages/console-web.md +5 -5
  127. package/reference/autogen/packages/crud-core.md +61 -17
  128. package/reference/autogen/packages/database-runtime-mysql.md +12 -2
  129. package/reference/autogen/packages/database-runtime-postgres.md +12 -2
  130. package/reference/autogen/packages/database-runtime.md +26 -25
  131. package/reference/autogen/packages/google-rewarded-core.md +19 -104
  132. package/reference/autogen/packages/http-runtime.md +4 -8
  133. package/reference/autogen/packages/http-web.md +32 -0
  134. package/reference/autogen/packages/json-rest-api-core.md +4 -6
  135. package/reference/autogen/packages/kernel.md +109 -390
  136. package/reference/autogen/packages/mobile-capacitor.md +2 -13
  137. package/reference/autogen/packages/realtime.md +29 -26
  138. package/reference/autogen/packages/resource-crud-core.md +6 -0
  139. package/reference/autogen/packages/shell-web.md +69 -54
  140. package/reference/autogen/packages/storage-runtime.md +3 -3
  141. package/reference/autogen/packages/uploads-image-web.md +0 -1
  142. package/reference/autogen/packages/uploads-runtime.md +3 -3
  143. package/reference/autogen/packages/users-core.md +45 -90
  144. package/reference/autogen/packages/users-web.md +5 -7
  145. package/reference/autogen/packages/workspaces-core.md +53 -74
  146. package/reference/autogen/packages/workspaces-web.md +15 -16
  147. package/reference/autogen/tooling/jskit-catalog.md +34 -0
  148. package/reference/autogen/tooling/testUtils.md +4 -4
  149. package/skills/jskit/SKILL.md +38 -31
  150. package/skills/jskit/references/app-operations.md +68 -53
  151. package/skills/jskit/references/crud-operations.md +58 -106
  152. package/skills/jskit/references/material-3.md +1 -1
  153. package/skills/jskit/references/ui-operations.md +41 -44
  154. package/templates/app/AGENTS.md +7 -3
  155. package/guide/agent/app-setup/upgrade-beta-1-to-final.md +0 -252
  156. package/guide/agent/app-setup/working-with-the-jskit-cli.md +0 -325
  157. package/guide/agent/generators/advanced-cruds.md +0 -1935
  158. package/guide/agent/generators/crud-generators.md +0 -948
  159. package/guide/agent/generators/intro.md +0 -65
  160. package/guide/agent/generators/row-policies.md +0 -537
  161. package/guide/agent/generators/ui-generators.md +0 -690
  162. package/patterns/crud-scaffolding.md +0 -198
  163. package/patterns/generated-ui-contract-tracking.md +0 -66
  164. package/reference/autogen/packages/assistant.md +0 -68
  165. package/reference/autogen/packages/crud-server-generator.md +0 -215
  166. package/reference/autogen/packages/crud-ui-generator.md +0 -192
  167. package/reference/autogen/packages/feature-server-generator.md +0 -65
  168. package/reference/autogen/packages/ui-generator.md +0 -127
  169. package/reference/autogen/tooling/create-app.md +0 -317
  170. package/reference/autogen/tooling/jskit-cli.md +0 -933
  171. 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.