cabloy 5.1.60 → 5.1.61

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 (76) hide show
  1. package/.claude/hooks/contract-loop-gate.ts +296 -0
  2. package/.claude/settings.json +16 -0
  3. package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +1 -0
  4. package/.claude/skills/cabloy-contract-loop/SKILL.md +89 -16
  5. package/.claude/skills/cabloy-contract-loop/references/contract-loop-map.md +102 -14
  6. package/.claude/skills/cabloy-contract-loop/references/resource-custom-state-pattern.md +4 -0
  7. package/.claude/skills/cabloy-contract-loop/references/verification-checklist.md +32 -14
  8. package/.claude/skills/cabloy-frontend-scaffold/SKILL.md +11 -0
  9. package/.claude/skills/cabloy-frontend-scaffold/references/follow-up-checklist.md +2 -0
  10. package/.claude/skills/cabloy-module-removal/SKILL.md +144 -0
  11. package/.claude/skills/cabloy-resource-field-update/SKILL.md +7 -0
  12. package/.claude/skills/cabloy-zova-source-reading/SKILL.md +221 -0
  13. package/.claude/skills/cabloy-zova-source-reading/references/analysis-modes.md +91 -0
  14. package/.claude/skills/cabloy-zova-source-reading/references/core-reading-paths.md +117 -0
  15. package/CHANGELOG.md +22 -0
  16. package/CLAUDE.md +10 -0
  17. package/cabloy-docs/.vitepress/config.mjs +50 -4
  18. package/cabloy-docs/ai/cli-to-skill-map.md +7 -0
  19. package/cabloy-docs/ai/docs-skills-rules-mapping.md +14 -0
  20. package/cabloy-docs/ai/future-skill-roadmap.md +10 -7
  21. package/cabloy-docs/ai/introduction.md +1 -0
  22. package/cabloy-docs/ai/playbook-backend-module.md +6 -0
  23. package/cabloy-docs/ai/playbook-module-removal.md +164 -0
  24. package/cabloy-docs/ai/skills.md +11 -0
  25. package/cabloy-docs/backend/dto-guide.md +6 -0
  26. package/cabloy-docs/backend/entity-guide.md +18 -0
  27. package/cabloy-docs/backend/introduction.md +2 -0
  28. package/cabloy-docs/backend/serialization-guide.md +10 -0
  29. package/cabloy-docs/backend/status-guide.md +271 -0
  30. package/cabloy-docs/frontend/api-guide.md +2 -0
  31. package/cabloy-docs/frontend/bean-scene-authoring.md +2 -0
  32. package/cabloy-docs/frontend/cli.md +12 -0
  33. package/cabloy-docs/frontend/command-scene-authoring.md +495 -0
  34. package/cabloy-docs/frontend/design-principles.md +6 -0
  35. package/cabloy-docs/frontend/fetch-interceptor-guide.md +440 -0
  36. package/cabloy-docs/frontend/form-guide.md +795 -0
  37. package/cabloy-docs/frontend/foundation.md +29 -0
  38. package/cabloy-docs/frontend/introduction.md +12 -1
  39. package/cabloy-docs/frontend/ioc-and-beans.md +6 -0
  40. package/cabloy-docs/frontend/mock-guide.md +1 -0
  41. package/cabloy-docs/frontend/model-architecture.md +252 -39
  42. package/cabloy-docs/frontend/model-resource-best-practices.md +379 -0
  43. package/cabloy-docs/frontend/model-resource-cookbook.md +505 -0
  44. package/cabloy-docs/frontend/model-resource-owner-pattern.md +382 -0
  45. package/cabloy-docs/frontend/model-resource-usage-guide.md +318 -0
  46. package/cabloy-docs/frontend/model-state-guide.md +366 -13
  47. package/cabloy-docs/frontend/openapi-sdk-guide.md +5 -2
  48. package/cabloy-docs/frontend/page-guide.md +6 -0
  49. package/cabloy-docs/frontend/quickstart.md +4 -0
  50. package/cabloy-docs/frontend/reading-zova-for-vue-developers.md +266 -0
  51. package/cabloy-docs/frontend/server-data.md +2 -0
  52. package/cabloy-docs/frontend/zova-form-source-reading-map.md +295 -0
  53. package/cabloy-docs/frontend/zova-form-under-the-hood.md +556 -0
  54. package/cabloy-docs/frontend/zova-reactivity-under-the-hood.md +320 -0
  55. package/cabloy-docs/frontend/zova-source-reading-map.md +327 -0
  56. package/cabloy-docs/frontend/zova-vs-vue3-comparison.md +308 -0
  57. package/cabloy-docs/fullstack/contract-loop-playbook.md +350 -0
  58. package/cabloy-docs/fullstack/frontend-metadata-to-backend.md +44 -1
  59. package/cabloy-docs/fullstack/introduction.md +12 -1
  60. package/cabloy-docs/fullstack/openapi-to-sdk.md +19 -9
  61. package/cabloy-docs/fullstack/tutorial-3-frontend-metadata-sharing.md +2 -2
  62. package/cabloy-docs/fullstack/tutorial-4-custom-level-renderers.md +30 -5
  63. package/cabloy-docs/fullstack/tutorial-5-backend-contract-sharing.md +9 -7
  64. package/cabloy-docs/fullstack/tutorial-6-one-contract-four-uses.md +2 -0
  65. package/cabloy-docs/fullstack/tutorials-overview.md +16 -3
  66. package/cabloy-docs/reference/bean-scene-boilerplates.md +2 -0
  67. package/package.json +2 -1
  68. package/scripts/init.ts +2 -18
  69. package/scripts/initTestData.ts +25 -0
  70. package/scripts/upgrade.ts +17 -2
  71. package/vona/pnpm-lock.yaml +94 -4
  72. package/zova/packages-cli/cli/package.json +2 -2
  73. package/zova/packages-cli/cli-set-front/cli/templates/openapi/config/boilerplate/module/openapi.config.ts +6 -1
  74. package/zova/packages-cli/cli-set-front/package.json +1 -1
  75. package/zova/packages-cli/cli-set-front/src/lib/bean/cli.openapi.generate.ts +34 -4
  76. package/zova/pnpm-lock.yaml +20 -20
@@ -0,0 +1,505 @@
1
+ # Resource Model Cookbook
2
+
3
+ This cookbook collects common extension scenarios for resource-oriented models built on top of `ModelResource`.
4
+
5
+ It is the practical companion to:
6
+
7
+ - [Model Resource Owner Pattern](/frontend/model-resource-owner-pattern)
8
+ - [Using `ModelResource` in Your Module](/frontend/model-resource-usage-guide)
9
+ - [Resource Model Best Practices and Anti-Patterns](/frontend/model-resource-best-practices)
10
+
11
+ Use this page when you want implementation templates instead of only architectural explanation.
12
+
13
+ ## Why this page exists
14
+
15
+ The earlier pages explain the architecture, application model, and review rules.
16
+
17
+ The next practical need is usually:
18
+
19
+ > show me the common cases I will actually implement.
20
+
21
+ This page focuses on that need.
22
+
23
+ ## How to use this cookbook
24
+
25
+ Unless a recipe says otherwise, the examples below assume the thin-facade pattern from Recipe 1.
26
+
27
+ That means the business-facing model delegates resource ownership to `this.$$modelResource` instead of becoming another `ModelResource` owner itself.
28
+
29
+ For each recipe below:
30
+
31
+ 1. identify whether the scenario is query-oriented, mutation-oriented, or invalidation-oriented
32
+ 2. keep the resource semantics either on `ModelResource` directly or in a thin facade that delegates to it
33
+ 3. reuse `queryItem(...)`, `mutationItem(...)`, `select(...)`, `view(...)`, and the existing key structure when possible
34
+ 4. keep generic page or form blocks compatible unless you have a real reason to break that contract
35
+
36
+ ## Forward-chain principle: prefer a thin business facade over a competing cache owner
37
+
38
+ In Cabloy’s bidirectional contract loop, this cookbook most often applies to the **forward chain**:
39
+
40
+ - backend contract truth changes first
41
+ - frontend API consumers are regenerated or refreshed from that truth
42
+ - frontend follow-up stays thin and semantic
43
+
44
+ For resource-oriented frontend work, that usually means:
45
+
46
+ > create a thin business model facade and reuse the existing resource-owner instead of creating a competing cache owner.
47
+
48
+ A strong pattern is:
49
+
50
+ - let `ModelResource` continue to own cache identity, list/item invalidation, and resource-level state
51
+ - let the business-facing model add semantic methods such as `summary(...)` or `deleteForce(...)`
52
+ - let custom cells, commands, or page controllers consume the thin business model facade, not invent a second owner for the same resource state
53
+
54
+ That pattern is especially important after forward-chain contract evolution, because it keeps generated or contract-aligned resource ownership centralized while still giving the business module a clean semantic API.
55
+
56
+ ## Recipe 1: add a thin business facade over the existing resource-owner
57
+
58
+ ### Use this when
59
+
60
+ The business module needs custom semantic methods, but the resource cache owner already exists and should remain authoritative.
61
+
62
+ Typical forward-chain case:
63
+
64
+ - backend adds a custom action such as `summary` or `deleteForce`
65
+ - frontend contract is regenerated or refreshed
66
+ - frontend needs a clean business-facing model method
67
+ - but should **not** create a second cache owner for the same resource
68
+
69
+ ### Recommended shape
70
+
71
+ ```typescript
72
+ const StudentResource = 'demo-student:student';
73
+
74
+ @Model()
75
+ export class ModelStudent extends BeanModelBase {
76
+ @Use({ beanFullName: 'rest-resource.model.resource' })
77
+ protected get $$modelResource(): ModelResource {
78
+ return usePrepareArg(StudentResource, true);
79
+ }
80
+
81
+ summary(id: TableIdentity) {
82
+ return this.$$modelResource.queryItem({
83
+ id,
84
+ action: 'summary',
85
+ queryFn: async () => {
86
+ const res = await this.scope.api.demoStudent.summary({ params: { id } });
87
+ return res ?? null;
88
+ },
89
+ meta: {
90
+ disableSuspenseOnInit: true,
91
+ },
92
+ });
93
+ }
94
+
95
+ deleteForce(id: TableIdentity) {
96
+ return this.$$modelResource.mutationItem<void, void>({
97
+ id,
98
+ action: 'deleteForce',
99
+ mutationFn: async () => {
100
+ await this.scope.api.demoStudent.deleteForce({ params: { id } });
101
+ },
102
+ });
103
+ }
104
+ }
105
+ ```
106
+
107
+ ### Why this works well
108
+
109
+ - the business module gets a semantic model surface
110
+ - the existing `rest-resource` owner remains the single cache owner
111
+ - item keys, invalidation policy, and selector-based resource identity stay centralized
112
+ - custom UI code can call `ModelStudent.summary(...)` without rebuilding resource semantics locally
113
+
114
+ ### Typical consumer shape
115
+
116
+ A custom cell or command can consume the thin facade and trigger the semantic method:
117
+
118
+ ```typescript
119
+ const modelStudent = (await ctx.bean._getBean('demo-student.model.student', true)) as ModelStudent;
120
+ const querySummary = modelStudent.summary(id);
121
+ const { data: summary } = await querySummary.refetch();
122
+ ```
123
+
124
+ ### Avoid
125
+
126
+ Do not create a second model that independently caches the same student resource list/item state just because a custom action was added.
127
+
128
+ That usually creates competing cache ownership and makes forward-chain maintenance harder.
129
+
130
+ ## Recipe 2: inside that facade, add a row-level custom query
131
+
132
+ ### Use this when
133
+
134
+ You already chose the thin-facade pattern from Recipe 1, and the business module needs more than the standard `view(id)` query.
135
+
136
+ Examples:
137
+
138
+ - summary detail
139
+ - audit detail
140
+ - timeline detail
141
+ - metrics detail
142
+
143
+ ### Recommended shape
144
+
145
+ ```typescript
146
+ summary(id: TableIdentity) {
147
+ return this.$$modelResource.queryItem({
148
+ id,
149
+ action: 'summary',
150
+ queryFn: async () => {
151
+ const res = await this.scope.api.demoStudent.summary({ params: { id } });
152
+ return res ?? null;
153
+ },
154
+ meta: {
155
+ disableSuspenseOnInit: true,
156
+ },
157
+ });
158
+ }
159
+ ```
160
+
161
+ ### Why this works well
162
+
163
+ - the thin facade adds business semantics without becoming a second owner
164
+ - row-level cache identity still stays under the existing resource-owner
165
+ - item-level key structure and selector-based isolation remain centralized in `rest-resource`
166
+
167
+ ### Avoid
168
+
169
+ Do not rewrite this as an independent `$useStateData(...)` owner in the business model when the same row already belongs to the existing resource-owner.
170
+
171
+ ## Recipe 3: inside that facade, add a list-level query variant
172
+
173
+ ### Use this when
174
+
175
+ You already chose the thin-facade pattern from Recipe 1, and the business module needs a second list-style endpoint beyond the default `select(query)`.
176
+
177
+ Examples:
178
+
179
+ - archived list
180
+ - pending list
181
+ - dashboard list
182
+ - lightweight picker list
183
+
184
+ ### Recommended shape
185
+
186
+ ```typescript
187
+ selectArchived(query?: ITableQuery) {
188
+ return this.$$modelResource.selectGeneral('archived', query);
189
+ }
190
+ ```
191
+
192
+ ### Why this works well
193
+
194
+ - the business-facing model stays thin
195
+ - list-level cache identity still belongs to the existing resource-owner
196
+ - list variants remain compatible with the same invalidation and selector semantics
197
+
198
+ ### Avoid
199
+
200
+ Do not create another list owner with a parallel `keySelect(...)` convention when the list is still part of the same resource boundary.
201
+
202
+ ## Recipe 4: inside that facade, add a row-level custom mutation
203
+
204
+ ### Use this when
205
+
206
+ You already chose the thin-facade pattern from Recipe 1, and the business module needs a business action beyond create/update/delete.
207
+
208
+ Examples:
209
+
210
+ - deleteForce
211
+ - archive
212
+ - publish
213
+ - approve
214
+ - restore
215
+
216
+ ### Recommended shape
217
+
218
+ ```typescript
219
+ deleteForce(id: TableIdentity) {
220
+ return this.$$modelResource.mutationItem<void, void>({
221
+ id,
222
+ action: 'deleteForce',
223
+ mutationFn: async () => {
224
+ await this.scope.api.demoStudent.deleteForce({ params: { id } });
225
+ },
226
+ });
227
+ }
228
+ ```
229
+
230
+ ### Why this works well
231
+
232
+ - mutation ownership still stays under the existing resource-owner
233
+ - item/list invalidation remains centralized
234
+ - the business-facing model exposes semantic actions without competing for cache ownership
235
+
236
+ ## Recipe 5: customize invalidation for a special mutation
237
+
238
+ ### Use this when
239
+
240
+ A custom mutation should not follow the default invalidation strategy, but cache ownership should still remain inside the existing resource-owner.
241
+
242
+ ### Recommended shape
243
+
244
+ ```typescript
245
+ publish(id: TableIdentity) {
246
+ return this.$$modelResource.mutationItem<void, void>({
247
+ id,
248
+ action: 'publish',
249
+ invalidateSelect: false,
250
+ mutationFn: async () => {
251
+ await this.scope.api.demoStudent.publish({ params: { id } });
252
+ },
253
+ onSuccess: async () => {
254
+ await this.$$modelResource.$invalidateQueries({ queryKey: ['select'] });
255
+ await this.$$modelResource.$invalidateQueries({ queryKey: ['item', id] });
256
+ await this.$$modelResource.$invalidateQueries({ queryKey: ['select', 'dashboard'] });
257
+ },
258
+ });
259
+ }
260
+ ```
261
+
262
+ ### Why this works well
263
+
264
+ - the existing resource-owner remains the source of truth for consistency rules
265
+ - special cache dependencies stay explicit
266
+ - pages do not need to remember hidden follow-up refetch rules
267
+
268
+ ### Avoid
269
+
270
+ Do not spread this invalidation policy across page code, modal code, and button handlers.
271
+
272
+ ## Recipe 6: expose permission-oriented helpers
273
+
274
+ ### Use this when
275
+
276
+ Several screens need the same permission-based business decision.
277
+
278
+ Examples:
279
+
280
+ - can archive this resource?
281
+ - can publish this row?
282
+ - should a bulk action be visible?
283
+
284
+ ### Recommended shape
285
+
286
+ ```typescript
287
+ canArchive() {
288
+ return this.$computed(() => {
289
+ return !!this.$$modelResource.permissions?.actions?.archive;
290
+ });
291
+ }
292
+ ```
293
+
294
+ Or row-oriented logic:
295
+
296
+ ```typescript
297
+ canPublishRow(row: Entity) {
298
+ return !!this.$$modelResource.permissions?.actions?.publish && row.status === 'draft';
299
+ }
300
+ ```
301
+
302
+ ### Why this works well
303
+
304
+ - permission semantics stay close to the existing resource owner
305
+ - multiple pages reuse the same rule
306
+ - UI code gets simpler and more consistent
307
+
308
+ ## Recipe 7: expose schema-oriented convenience helpers
309
+
310
+ ### Use this when
311
+
312
+ Multiple forms or blocks need the same schema-based interpretation.
313
+
314
+ Examples:
315
+
316
+ - whether a field should be hidden in a certain scene
317
+ - whether a resource uses a special title field
318
+ - default schema chosen for a custom scene
319
+
320
+ ### Recommended shape
321
+
322
+ ```typescript
323
+ getTitleField() {
324
+ return this.$computed(() => {
325
+ return this.$$modelResource.schemaRow?.properties?.title ? 'title' : 'name';
326
+ });
327
+ }
328
+ ```
329
+
330
+ ### Why this works well
331
+
332
+ - schema interpretation stays reusable
333
+ - pages do not duplicate metadata reading logic
334
+ - metadata ownership still stays under the existing resource-owner
335
+
336
+ ## Recipe 8: add custom default form data
337
+
338
+ ### Use this when
339
+
340
+ Create forms need richer default values than the generic schema default alone.
341
+
342
+ ### Recommended shape
343
+
344
+ ```typescript
345
+ getCreateDefaultData() {
346
+ const data = this.$$modelResource.getQueryDataDefaultValue(this.$$modelResource.schemaCreate) ?? {};
347
+ return {
348
+ ...data,
349
+ status: 'draft',
350
+ enabled: true,
351
+ };
352
+ }
353
+ ```
354
+
355
+ Then integrate it through the business-facing form helper:
356
+
357
+ ```typescript
358
+ getFormData(formMeta: IFormMeta, id?: TableIdentity) {
359
+ if (formMeta.formMode === 'edit' && formMeta.editMode === 'create') {
360
+ return this.getCreateDefaultData();
361
+ }
362
+ return this.$$modelResource.getFormData(formMeta, id);
363
+ }
364
+ ```
365
+
366
+ ### Why this works well
367
+
368
+ - create defaults stay resource-owned
369
+ - form behavior remains consistent across entry points
370
+ - the thin facade only adds semantic customization on top of the existing form flow
371
+
372
+ ## Recipe 9: add batch action support
373
+
374
+ ### Use this when
375
+
376
+ The resource supports multi-row operations.
377
+
378
+ Examples:
379
+
380
+ - batch delete
381
+ - batch archive
382
+ - batch enable/disable
383
+
384
+ ### Recommended shape
385
+
386
+ ```typescript
387
+ batchArchive(ids: TableIdentity[]) {
388
+ return this.$$modelResource.$useMutationData<void, TableIdentity[]>({
389
+ mutationKey: ['batchArchive'],
390
+ mutationFn: async ids => {
391
+ await this.scope.api.demoStudent.batchArchive({ ids });
392
+ },
393
+ onSuccess: async () => {
394
+ await this.$$modelResource.$invalidateQueries({ queryKey: ['select'] });
395
+ },
396
+ });
397
+ }
398
+ ```
399
+
400
+ ### Why this works well
401
+
402
+ - batch behavior stays modeled explicitly
403
+ - list invalidation policy remains visible
404
+ - cache ownership still stays with the existing resource-owner even when row-level helpers are not the right fit
405
+
406
+ ## Recipe 10: keep generic blocks working while adding resource semantics
407
+
408
+ ### Use this when
409
+
410
+ You want richer resource behavior but still rely on generic blocks such as page or page-entry consumers.
411
+
412
+ ### Recommended approach
413
+
414
+ - preserve stable surfaces like `select`, `view`, `schemaFilter`, `schemaRow`, `getFormSchema`, and `getFormData`
415
+ - add new methods around those stable surfaces instead of replacing them
416
+ - keep custom semantics additive where possible
417
+
418
+ ### Why this works well
419
+
420
+ You get stronger business behavior without sacrificing reusable infrastructure.
421
+
422
+ ## Recipe 11: move repeated page logic back into the model
423
+
424
+ ### Use this when
425
+
426
+ You notice the same resource-specific logic appearing across several page controllers.
427
+
428
+ Typical signals:
429
+
430
+ - repeated action visibility checks
431
+ - repeated fetches to the same custom endpoint
432
+ - repeated default-form preparation
433
+ - repeated manual invalidation after the same mutation
434
+
435
+ ### Recommended refactor direction
436
+
437
+ 1. identify the repeated resource semantic
438
+ 2. add one model method or computed helper for it
439
+ 3. update pages to consume that stable model surface
440
+ 4. keep the invalidation or schema logic centralized in the model
441
+
442
+ ### Why this works well
443
+
444
+ It restores the resource-owner boundary and reduces drift.
445
+
446
+ ## Recipe 12: start direct, then add a thin facade only when semantics appear
447
+
448
+ ### Use this when
449
+
450
+ A resource does not yet need custom business methods, but you expect it may later.
451
+
452
+ ### Recommended approach
453
+
454
+ - start with direct `ModelResource` usage
455
+ - let generic pages or forms consume the existing resource-owner surface
456
+ - add a thin business facade only when the UI really needs semantic methods such as `summary(...)` or `deleteForce(...)`
457
+
458
+ ### Why this works well
459
+
460
+ - the programming model stays more uniform
461
+ - you avoid premature wrapper growth
462
+ - later business semantics can still be added without changing the single-owner boundary
463
+
464
+ ## Recipe 13: decide whether logic belongs in model or page
465
+
466
+ ### Put it in the model when
467
+
468
+ - it is resource semantics
469
+ - multiple pages should share it
470
+ - it affects query or mutation ownership
471
+ - it affects invalidation or form/schema decisions
472
+
473
+ ### Keep it in the page when
474
+
475
+ - it is only local presentation flow
476
+ - it has no resource reuse value
477
+ - it is temporary UI behavior with no resource meaning
478
+
479
+ ### Quick test
480
+
481
+ Ask:
482
+
483
+ > if another screen reused this resource, should it inherit the same rule?
484
+
485
+ If yes, the logic probably belongs in the model.
486
+
487
+ ## A compact cookbook workflow
488
+
489
+ When adding a new resource behavior, use this decision order:
490
+
491
+ 1. Is this list-level, row-level, batch-level, or form-level?
492
+ 2. Can I reuse `selectGeneral`, `queryItem`, or `mutationItem`?
493
+ 3. Does the behavior need custom invalidation?
494
+ 4. Should generic pages or forms keep working unchanged?
495
+ 5. Is this resource semantics or only page presentation?
496
+
497
+ That sequence usually leads to a cleaner implementation.
498
+
499
+ ## Final takeaway
500
+
501
+ The most useful cookbook habit is simple:
502
+
503
+ > when a resource behavior repeats, give it a named model-owned surface instead of letting pages rediscover it independently.
504
+
505
+ That is how a resource model grows in a clean and Zova-native way.