howone 0.1.23 → 0.1.26
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/package.json +1 -1
- package/templates/vite/.howone/skills/howone/01-architect/01-app-generation.md +215 -0
- package/templates/vite/.howone/skills/{howone-sdk → howone}/01-architect/02-manifest-codegen.md +67 -4
- package/templates/vite/.howone/skills/howone/02-database/01-schema-design.md +541 -0
- package/templates/vite/.howone/skills/howone/02-database/02-schema-operations.md +398 -0
- package/templates/vite/.howone/skills/howone/02-database/03-data-access-patterns.md +309 -0
- package/templates/vite/.howone/skills/howone/02-database/04-query-dsl-and-responses.md +237 -0
- package/templates/vite/.howone/skills/howone/02-database/05-ai-persistence-patterns.md +372 -0
- package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/01-client-setup.md +58 -36
- package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/02-entity-operations.md +67 -0
- package/templates/vite/.howone/skills/howone/03-sdk/03-auth.md +414 -0
- package/templates/vite/.howone/skills/howone/03-sdk/04-react-integration.md +191 -0
- package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/07-ai-action-calls.md +168 -64
- package/templates/vite/.howone/skills/howone/03-sdk/08-extension-boundaries.md +226 -0
- package/templates/vite/.howone/skills/howone/04-ai/01-ai-capability-architecture.md +205 -0
- package/templates/vite/.howone/skills/howone/04-ai/02-workflow-contract-rules.md +426 -0
- package/templates/vite/.howone/skills/howone/04-ai/03-ai-sdk-handoff.md +234 -0
- package/templates/vite/.howone/skills/howone/04-ai/04-service-capability-catalog.md +281 -0
- package/templates/vite/.howone/skills/howone/04-ai/05-workflow-operations.md +256 -0
- package/templates/vite/.howone/skills/howone/04-ai/06-ai-feature-playbooks.md +296 -0
- package/templates/vite/.howone/skills/{howone-sdk → howone}/SKILL.md +29 -12
- package/templates/vite/.howone/skills/howone/agents/openai.yaml +4 -0
- package/templates/vite/package.json +1 -1
- package/templates/vite/.howone/skills/howone-sdk/01-architect/01-app-generation.md +0 -126
- package/templates/vite/.howone/skills/howone-sdk/02-database/01-schema-design.md +0 -147
- package/templates/vite/.howone/skills/howone-sdk/02-database/02-schema-operations.md +0 -96
- package/templates/vite/.howone/skills/howone-sdk/02-database/03-data-access-patterns.md +0 -172
- package/templates/vite/.howone/skills/howone-sdk/03-sdk/03-auth.md +0 -616
- package/templates/vite/.howone/skills/howone-sdk/03-sdk/04-react-integration.md +0 -398
- package/templates/vite/.howone/skills/howone-sdk/04-ai/.gitkeep +0 -1
- package/templates/vite/.howone/skills/howone-sdk/04-ai/01-ai-capability-architecture.md +0 -142
- package/templates/vite/.howone/skills/howone-sdk/04-ai/02-workflow-contract-rules.md +0 -169
- package/templates/vite/.howone/skills/howone-sdk/04-ai/03-ai-sdk-handoff.md +0 -80
- package/templates/vite/.howone/skills/howone-sdk/agents/openai.yaml +0 -4
- /package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/05-file-upload.md +0 -0
- /package/templates/vite/.howone/skills/{howone-sdk → howone}/03-sdk/06-raw-http.md +0 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
# Database Schema Design
|
|
2
|
+
|
|
3
|
+
Use this reference when designing or changing HowOne backend entity schemas. It condenses the
|
|
4
|
+
runtime contract from `docs/dynamic-entity-architecture.zh.md` into instructions an AI agent can
|
|
5
|
+
actually apply.
|
|
6
|
+
|
|
7
|
+
This file answers: **what should the schema be?** For how to apply changes, read
|
|
8
|
+
`02-schema-operations.md`. For frontend calls, read `03-data-access-patterns.md` and
|
|
9
|
+
`03-sdk/02-entity-operations.md`.
|
|
10
|
+
|
|
11
|
+
## Mental Model
|
|
12
|
+
|
|
13
|
+
A HowOne Entity is a versioned app-level database contract. It is not a MongoDB collection exposed
|
|
14
|
+
directly and not a loose JSON form.
|
|
15
|
+
|
|
16
|
+
Design the whole contract:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
type EntityContract = {
|
|
20
|
+
name: string
|
|
21
|
+
type: 'object'
|
|
22
|
+
description?: string
|
|
23
|
+
visibility: 'private' | 'public'
|
|
24
|
+
properties: Record<string, EntityField>
|
|
25
|
+
required?: string[]
|
|
26
|
+
access: {
|
|
27
|
+
authenticated: AuthenticatedAccess
|
|
28
|
+
public: PublicAccess
|
|
29
|
+
}
|
|
30
|
+
indexes?: EntityIndex[]
|
|
31
|
+
relations?: Record<string, EntityRelation>
|
|
32
|
+
presentation?: EntityPresentation
|
|
33
|
+
lifecycle?: EntityLifecycle
|
|
34
|
+
performance?: EntityPerformance
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Each section has a different job:
|
|
39
|
+
|
|
40
|
+
| Section | Purpose | AI design question |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `properties` | Business fields and primitive validation | What data is persisted? |
|
|
43
|
+
| `required` | Create-time required fields | What must exist before first save? |
|
|
44
|
+
| `access.authenticated` | Logged-in/private API access | Who can read/write after auth? |
|
|
45
|
+
| `access.public` | Anonymous/public API access | Can a public page read or write it? |
|
|
46
|
+
| `indexes` | Query performance and uniqueness | What lists/details will be queried often? |
|
|
47
|
+
| `relations` | Valid include names | What can be joined/expanded? |
|
|
48
|
+
| `presentation` | Admin/generator hints | What fields identify the record in UI? |
|
|
49
|
+
| `lifecycle` | Audit/delete policy hints | Is this append-only, soft-deletable, audited? |
|
|
50
|
+
| `performance` | SDK/admin pagination/sort hints | What limits and sorts are safe? |
|
|
51
|
+
|
|
52
|
+
## Storage Reality
|
|
53
|
+
|
|
54
|
+
HowOne uses shared runtime collections:
|
|
55
|
+
|
|
56
|
+
| Collection | Meaning |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `entityshares` | Current active entity definitions. |
|
|
59
|
+
| `entitydatashares` | Real business records. |
|
|
60
|
+
| `entityschemaversions` | Historical schema snapshots. |
|
|
61
|
+
| `entityschemastates` | Current schema version pointer per app. |
|
|
62
|
+
| `usershares` | App user mapping used for ownership. |
|
|
63
|
+
|
|
64
|
+
Schema restore changes definitions only. It does **not** roll back existing business records.
|
|
65
|
+
|
|
66
|
+
Every data row may carry:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
{
|
|
70
|
+
id: string
|
|
71
|
+
created_date: string
|
|
72
|
+
updated_date: string
|
|
73
|
+
created_by_id: string
|
|
74
|
+
schema_version_id?: string
|
|
75
|
+
schema_version_number?: number
|
|
76
|
+
is_sample?: boolean
|
|
77
|
+
...businessFields
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Do not create business fields that collide with system fields.
|
|
82
|
+
|
|
83
|
+
## Field Design
|
|
84
|
+
|
|
85
|
+
### Naming
|
|
86
|
+
|
|
87
|
+
Entity and field names must match:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
^[a-zA-Z_][a-zA-Z0-9_]*$
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Conventions:
|
|
94
|
+
|
|
95
|
+
- Entity names: PascalCase, singular, domain noun: `Todo`, `Article`, `QrProfile`.
|
|
96
|
+
- Field names: camelCase: `qrImageUrl`, `publishedAt`, `moodScore`.
|
|
97
|
+
- Avoid ambiguous names like `data`, `info`, `value`, `result` unless the product really stores opaque blobs.
|
|
98
|
+
|
|
99
|
+
Forbidden business field names:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
id
|
|
103
|
+
_id
|
|
104
|
+
created_date
|
|
105
|
+
updated_date
|
|
106
|
+
created_by_id
|
|
107
|
+
createdById
|
|
108
|
+
ownerId
|
|
109
|
+
is_sample
|
|
110
|
+
schema_version_id
|
|
111
|
+
schema_version_number
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`created_by_user_id` is special: use it only when a public write/share flow explicitly needs a
|
|
115
|
+
project-user identifier. Do not use it as the owner field for normal authenticated private data;
|
|
116
|
+
the backend derives owner from JWT.
|
|
117
|
+
|
|
118
|
+
### Supported Types
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
string
|
|
122
|
+
number
|
|
123
|
+
boolean
|
|
124
|
+
date
|
|
125
|
+
array
|
|
126
|
+
object
|
|
127
|
+
integer
|
|
128
|
+
null
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Nullable:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{ "type": ["string", "null"], "default": null }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Backend runtime currently enforces:
|
|
138
|
+
|
|
139
|
+
- unknown field rejection;
|
|
140
|
+
- create required fields;
|
|
141
|
+
- basic primitive type checks;
|
|
142
|
+
- non-null required fields unless the type includes `null`;
|
|
143
|
+
- defaults and `autoGenerate`.
|
|
144
|
+
|
|
145
|
+
Do not assume full JSON Schema enforcement for every nested constraint. Use Zod/frontend validation
|
|
146
|
+
for stronger UX validation:
|
|
147
|
+
|
|
148
|
+
- `enum`
|
|
149
|
+
- `minimum` / `maximum`
|
|
150
|
+
- `minLength` / `maxLength`
|
|
151
|
+
- `pattern`
|
|
152
|
+
- nested `items` / `properties`
|
|
153
|
+
|
|
154
|
+
### Defaults and Generated Fields
|
|
155
|
+
|
|
156
|
+
Use `default` when a field has an obvious value at creation:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{ "completed": { "type": "boolean", "default": false } }
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Use nullable defaults for optional dates:
|
|
163
|
+
|
|
164
|
+
```json
|
|
165
|
+
{ "publishedAt": { "type": ["date", "null"], "default": null } }
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Use server generation for public IDs:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"publicId": {
|
|
173
|
+
"type": "string",
|
|
174
|
+
"autoGenerate": { "strategy": "uuid" }
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Current `autoGenerate.strategy` support:
|
|
180
|
+
|
|
181
|
+
```text
|
|
182
|
+
uuid
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
AI rule: Create input may omit fields with `default` or `autoGenerate`; response types should include
|
|
186
|
+
them as present/possible.
|
|
187
|
+
|
|
188
|
+
## Access Design
|
|
189
|
+
|
|
190
|
+
Always write both `authenticated` and `public`. Do not rely on `visibility` defaults for new schema.
|
|
191
|
+
|
|
192
|
+
Authenticated channel:
|
|
193
|
+
|
|
194
|
+
```text
|
|
195
|
+
/api/entities/apps/:appId/data/:entityName
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Public channel:
|
|
199
|
+
|
|
200
|
+
```text
|
|
201
|
+
/api/entities/public/apps/:appId/data/:entityName
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
These channels are independent.
|
|
205
|
+
|
|
206
|
+
### Authenticated Access
|
|
207
|
+
|
|
208
|
+
Each action accepts `own`, `all`, or `none`:
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
"authenticated": {
|
|
212
|
+
"read": "own",
|
|
213
|
+
"create": "own",
|
|
214
|
+
"update": "own",
|
|
215
|
+
"delete": "own"
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
| Value | Meaning |
|
|
220
|
+
|---|---|
|
|
221
|
+
| `own` | Backend scopes to current `usershares._id`; create assigns current owner. |
|
|
222
|
+
| `all` | Logged-in users can access all records for that operation. |
|
|
223
|
+
| `none` | Operation forbidden. |
|
|
224
|
+
|
|
225
|
+
Rules:
|
|
226
|
+
|
|
227
|
+
- For private user data, use all `own`.
|
|
228
|
+
- For authenticated shared dashboards/CMS, use `read: "all"` and be conservative on update/delete.
|
|
229
|
+
- Do not pass owner fields in authenticated payloads or filters. Backend derives owner from auth.
|
|
230
|
+
- `query.mine()` is the SDK shorthand for authenticated own lists.
|
|
231
|
+
|
|
232
|
+
### Public Access
|
|
233
|
+
|
|
234
|
+
Public read values:
|
|
235
|
+
|
|
236
|
+
| Value | Use for |
|
|
237
|
+
|---|---|
|
|
238
|
+
| `none` | Not visible without login. |
|
|
239
|
+
| `list` | Public feeds/catalogs/lists. |
|
|
240
|
+
| `scoped` | Public share/detail pages that require scope keys. |
|
|
241
|
+
|
|
242
|
+
Public write values:
|
|
243
|
+
|
|
244
|
+
| Value | Use for |
|
|
245
|
+
|---|---|
|
|
246
|
+
| `none` | No anonymous write. |
|
|
247
|
+
| `scoped` | Anonymous write only with required scope values. |
|
|
248
|
+
| `any` | Fully public write; use rarely. |
|
|
249
|
+
|
|
250
|
+
Guardrail fields:
|
|
251
|
+
|
|
252
|
+
```json
|
|
253
|
+
"public": {
|
|
254
|
+
"read": "scoped",
|
|
255
|
+
"create": "none",
|
|
256
|
+
"update": "none",
|
|
257
|
+
"requiredScopes": ["ownerId", "slug"],
|
|
258
|
+
"allowedFilters": ["slug", "active"],
|
|
259
|
+
"allowedSorts": ["updatedDate"],
|
|
260
|
+
"defaultLimit": 1,
|
|
261
|
+
"maxLimit": 10
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Rules:
|
|
266
|
+
|
|
267
|
+
- `scoped` requires every `requiredScopes` value in query/body.
|
|
268
|
+
- `list` must define `allowedFilters`, `allowedSorts`, `defaultLimit`, and `maxLimit`.
|
|
269
|
+
- Public create requires a clear ownership/scoping story. If it needs `created_by_user_id`, document where that value comes from.
|
|
270
|
+
- Never expose broad public write unless the product explicitly needs anonymous submissions.
|
|
271
|
+
|
|
272
|
+
## Standard Patterns
|
|
273
|
+
|
|
274
|
+
### A. User Private Data
|
|
275
|
+
|
|
276
|
+
Use for todos, notes, journals, saved generations, personal settings.
|
|
277
|
+
|
|
278
|
+
```json
|
|
279
|
+
{
|
|
280
|
+
"name": "Todo",
|
|
281
|
+
"type": "object",
|
|
282
|
+
"visibility": "private",
|
|
283
|
+
"properties": {
|
|
284
|
+
"text": { "type": "string" },
|
|
285
|
+
"completed": { "type": "boolean", "default": false }
|
|
286
|
+
},
|
|
287
|
+
"required": ["text"],
|
|
288
|
+
"access": {
|
|
289
|
+
"authenticated": { "read": "own", "create": "own", "update": "own", "delete": "own" },
|
|
290
|
+
"public": { "read": "none", "create": "none", "update": "none", "delete": "none" }
|
|
291
|
+
},
|
|
292
|
+
"indexes": [
|
|
293
|
+
{
|
|
294
|
+
"name": "owner_completed_updated",
|
|
295
|
+
"scope": "owner",
|
|
296
|
+
"fields": ["completed", "updatedDate"],
|
|
297
|
+
"order": { "updatedDate": "desc" }
|
|
298
|
+
}
|
|
299
|
+
],
|
|
300
|
+
"performance": {
|
|
301
|
+
"defaultLimit": 50,
|
|
302
|
+
"maxLimit": 100,
|
|
303
|
+
"allowedSorts": ["createdDate", "updatedDate"]
|
|
304
|
+
},
|
|
305
|
+
"presentation": {
|
|
306
|
+
"titleField": "text",
|
|
307
|
+
"defaultSort": { "field": "updatedDate", "order": "desc" },
|
|
308
|
+
"listFields": ["text", "completed", "updatedDate"]
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
SDK list:
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
await howone.entities.Todo.query.mine({
|
|
317
|
+
page: { number: 1, size: 50 },
|
|
318
|
+
orderBy: { updatedDate: 'desc' },
|
|
319
|
+
})
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### B. Public Read-Only Catalog
|
|
323
|
+
|
|
324
|
+
Use for articles, templates, listings, published galleries.
|
|
325
|
+
|
|
326
|
+
```json
|
|
327
|
+
{
|
|
328
|
+
"name": "Article",
|
|
329
|
+
"type": "object",
|
|
330
|
+
"visibility": "public",
|
|
331
|
+
"properties": {
|
|
332
|
+
"title": { "type": "string" },
|
|
333
|
+
"slug": { "type": "string" },
|
|
334
|
+
"status": { "type": "string", "enum": ["draft", "published"], "default": "draft" },
|
|
335
|
+
"publishedAt": { "type": ["date", "null"], "default": null }
|
|
336
|
+
},
|
|
337
|
+
"required": ["title", "slug"],
|
|
338
|
+
"access": {
|
|
339
|
+
"authenticated": { "read": "all", "create": "all", "update": "all", "delete": "all" },
|
|
340
|
+
"public": {
|
|
341
|
+
"read": "list",
|
|
342
|
+
"create": "none",
|
|
343
|
+
"update": "none",
|
|
344
|
+
"delete": "none",
|
|
345
|
+
"allowedFilters": ["slug", "status"],
|
|
346
|
+
"allowedSorts": ["publishedAt", "updatedDate"],
|
|
347
|
+
"defaultLimit": 20,
|
|
348
|
+
"maxLimit": 100
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
"indexes": [
|
|
352
|
+
{ "name": "slug_unique", "scope": "global", "fields": ["slug"], "unique": true },
|
|
353
|
+
{
|
|
354
|
+
"name": "status_published",
|
|
355
|
+
"scope": "global",
|
|
356
|
+
"fields": ["status", "publishedAt"],
|
|
357
|
+
"order": { "publishedAt": "desc" }
|
|
358
|
+
}
|
|
359
|
+
]
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
SDK public list:
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
await howone.public.entities.Article.query({
|
|
367
|
+
where: { status: 'published' },
|
|
368
|
+
orderBy: { publishedAt: 'desc' },
|
|
369
|
+
page: { number: 1, size: 20 },
|
|
370
|
+
})
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### C. Public Scoped Share Page
|
|
374
|
+
|
|
375
|
+
Use for QR profile, public invoice, public resume, shared report.
|
|
376
|
+
|
|
377
|
+
```json
|
|
378
|
+
{
|
|
379
|
+
"name": "QrProfile",
|
|
380
|
+
"type": "object",
|
|
381
|
+
"visibility": "public",
|
|
382
|
+
"properties": {
|
|
383
|
+
"slug": { "type": "string" },
|
|
384
|
+
"title": { "type": "string" },
|
|
385
|
+
"qrImageUrl": { "type": "string" },
|
|
386
|
+
"active": { "type": "boolean", "default": true }
|
|
387
|
+
},
|
|
388
|
+
"required": ["slug", "title", "qrImageUrl"],
|
|
389
|
+
"access": {
|
|
390
|
+
"authenticated": { "read": "own", "create": "own", "update": "own", "delete": "own" },
|
|
391
|
+
"public": {
|
|
392
|
+
"read": "scoped",
|
|
393
|
+
"create": "none",
|
|
394
|
+
"update": "none",
|
|
395
|
+
"delete": "none",
|
|
396
|
+
"requiredScopes": ["ownerId", "slug"],
|
|
397
|
+
"allowedFilters": ["slug", "active"],
|
|
398
|
+
"allowedSorts": ["updatedDate"],
|
|
399
|
+
"defaultLimit": 1,
|
|
400
|
+
"maxLimit": 10
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
"indexes": [
|
|
404
|
+
{ "name": "owner_slug_unique", "scope": "owner", "fields": ["slug"], "unique": true }
|
|
405
|
+
]
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
SDK public scoped read:
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
await howone.public.entities.QrProfile.queryScoped({
|
|
413
|
+
where: { ownerId, slug, active: true },
|
|
414
|
+
page: { number: 1, size: 1 },
|
|
415
|
+
})
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### D. Workflow Output History
|
|
419
|
+
|
|
420
|
+
Use for AI generation/analyze/report flows that need persisted history.
|
|
421
|
+
|
|
422
|
+
```json
|
|
423
|
+
{
|
|
424
|
+
"name": "Generation",
|
|
425
|
+
"type": "object",
|
|
426
|
+
"visibility": "private",
|
|
427
|
+
"properties": {
|
|
428
|
+
"prompt": { "type": "string" },
|
|
429
|
+
"status": { "type": "string", "enum": ["pending", "completed", "failed"], "default": "pending" },
|
|
430
|
+
"resultUrl": { "type": ["string", "null"], "default": null },
|
|
431
|
+
"errorMessage": { "type": ["string", "null"], "default": null },
|
|
432
|
+
"completedAt": { "type": ["date", "null"], "default": null }
|
|
433
|
+
},
|
|
434
|
+
"required": ["prompt", "status"],
|
|
435
|
+
"access": {
|
|
436
|
+
"authenticated": { "read": "own", "create": "own", "update": "own", "delete": "own" },
|
|
437
|
+
"public": { "read": "none", "create": "none", "update": "none", "delete": "none" }
|
|
438
|
+
},
|
|
439
|
+
"indexes": [
|
|
440
|
+
{
|
|
441
|
+
"name": "owner_status_updated",
|
|
442
|
+
"scope": "owner",
|
|
443
|
+
"fields": ["status", "updatedDate"],
|
|
444
|
+
"order": { "updatedDate": "desc" }
|
|
445
|
+
}
|
|
446
|
+
]
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Rules:
|
|
451
|
+
|
|
452
|
+
- Persist pending before long-running workflow when product needs history/resume.
|
|
453
|
+
- Persist completed output only into fields declared here.
|
|
454
|
+
- Persist failed state and error message if history must show failures.
|
|
455
|
+
- Do not persist raw workflow envelopes unless schema declares an object field for them.
|
|
456
|
+
|
|
457
|
+
## Index Design
|
|
458
|
+
|
|
459
|
+
Every list/detail path should map to an index.
|
|
460
|
+
|
|
461
|
+
| Query shape | Index recommendation |
|
|
462
|
+
|---|---|
|
|
463
|
+
| private user list by updated time | `scope: "owner"`, `fields: ["updatedDate"]` |
|
|
464
|
+
| private user list filtered by status | `scope: "owner"`, `fields: ["status", "updatedDate"]` |
|
|
465
|
+
| owner unique slug | `scope: "owner"`, `fields: ["slug"]`, `unique: true` |
|
|
466
|
+
| public slug detail | `scope: "global"`, `fields: ["slug"]`, `unique: true` |
|
|
467
|
+
| public feed by status/date | `scope: "global"`, `fields: ["status", "publishedAt"]` |
|
|
468
|
+
|
|
469
|
+
Index rules:
|
|
470
|
+
|
|
471
|
+
- Index fields should match real UI queries, not every field.
|
|
472
|
+
- Owner-scoped unique means unique per owner, not globally unique.
|
|
473
|
+
- Public filters/sorts must also be listed in `access.public.allowedFilters/allowedSorts`.
|
|
474
|
+
- Avoid designing public queries that require unbounded scans.
|
|
475
|
+
|
|
476
|
+
## Relations
|
|
477
|
+
|
|
478
|
+
Use `relations` only when frontend/admin needs `include`.
|
|
479
|
+
|
|
480
|
+
```json
|
|
481
|
+
"relations": {
|
|
482
|
+
"author": {
|
|
483
|
+
"type": "entity",
|
|
484
|
+
"entity": "Author",
|
|
485
|
+
"localField": "authorId",
|
|
486
|
+
"foreignField": "id",
|
|
487
|
+
"as": "author"
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Rules:
|
|
493
|
+
|
|
494
|
+
- Keep relation names stable; SDK/UI may use them in `include`.
|
|
495
|
+
- Do not use relations to hide missing denormalized fields required by list pages.
|
|
496
|
+
- Public include should be conservative; ensure related data is safe to expose.
|
|
497
|
+
|
|
498
|
+
## Presentation and Lifecycle
|
|
499
|
+
|
|
500
|
+
`presentation` is not API validation. It teaches admin UI, codegen, and agents how to display a record:
|
|
501
|
+
|
|
502
|
+
```json
|
|
503
|
+
"presentation": {
|
|
504
|
+
"titleField": "title",
|
|
505
|
+
"imageField": "coverImageUrl",
|
|
506
|
+
"defaultSort": { "field": "updatedDate", "order": "desc" },
|
|
507
|
+
"listFields": ["title", "status", "updatedDate"]
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
`lifecycle` is policy metadata:
|
|
512
|
+
|
|
513
|
+
```json
|
|
514
|
+
"lifecycle": {
|
|
515
|
+
"audit": true,
|
|
516
|
+
"softDelete": false
|
|
517
|
+
}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
Use it to document intent, but do not assume every lifecycle policy is fully enforced unless backend
|
|
521
|
+
source confirms it.
|
|
522
|
+
|
|
523
|
+
## Schema Review Checklist
|
|
524
|
+
|
|
525
|
+
Before applying a schema:
|
|
526
|
+
|
|
527
|
+
- Entity name and field names pass naming rules.
|
|
528
|
+
- No business field collides with system fields.
|
|
529
|
+
- Every required field exists in `properties`.
|
|
530
|
+
- Every optional nullable field has a deliberate `null` type/default.
|
|
531
|
+
- Defaults exist for fields that should not block create.
|
|
532
|
+
- Access has both `authenticated` and `public`.
|
|
533
|
+
- Public list/scoped flows have filters, sorts, scopes, and limits.
|
|
534
|
+
- Private owned data does not require app code to pass owner fields.
|
|
535
|
+
- Indexes match real list/detail queries.
|
|
536
|
+
- Presentation tells UI/admin which fields to show.
|
|
537
|
+
- Workflow output fields are explicitly declared before persistence.
|
|
538
|
+
- Dangerous public write is avoided or explicitly justified.
|
|
539
|
+
|
|
540
|
+
If any item cannot be answered from requirements, stop and ask for the missing contract instead of
|
|
541
|
+
inventing hidden fields or public exposure.
|