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,398 @@
|
|
|
1
|
+
# Schema Operations
|
|
2
|
+
|
|
3
|
+
Use this reference when applying backend entity schema changes through HowOne runtime tools or
|
|
4
|
+
`client.schema.*`. It answers: **how do I safely change the schema and keep app code in sync?**
|
|
5
|
+
|
|
6
|
+
For schema design decisions, read `01-schema-design.md` first.
|
|
7
|
+
|
|
8
|
+
## Source Of Truth
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
agent proposal = draft
|
|
12
|
+
backend preview/apply result = validated contract
|
|
13
|
+
synced .howone/database files = local copy of backend version
|
|
14
|
+
src/lib/sdk.ts = generated app binding from synced manifest
|
|
15
|
+
frontend code = consumer of generated bindings
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Do not hand-write `.howone/database/*`. Sync artifacts from a concrete backend version.
|
|
19
|
+
|
|
20
|
+
## Preferred Patch Flow
|
|
21
|
+
|
|
22
|
+
Use patch flow for most feature-level changes:
|
|
23
|
+
|
|
24
|
+
1. Inspect current `schema.getState()` and current entity definitions.
|
|
25
|
+
2. Design a full patch containing all related operations.
|
|
26
|
+
3. Preview the patch.
|
|
27
|
+
4. Review risk, diff, and current version.
|
|
28
|
+
5. Apply the exact same patch with `expectedVersionId`.
|
|
29
|
+
6. Sync schema artifacts from `next.versionId`.
|
|
30
|
+
7. Read `.howone/database/manifest.json`.
|
|
31
|
+
8. Regenerate/update `src/lib/sdk.ts` bindings.
|
|
32
|
+
9. Update frontend calls according to `access`.
|
|
33
|
+
10. Run typecheck/build/tests.
|
|
34
|
+
|
|
35
|
+
Do not preview one patch and apply a different operation set.
|
|
36
|
+
|
|
37
|
+
## SDK Schema Client
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
const state = await client.schema.getState()
|
|
41
|
+
const definitions = await client.schema.listDefinitions()
|
|
42
|
+
const todo = await client.schema.getDefinition('Todo')
|
|
43
|
+
|
|
44
|
+
const preview = await client.schema.previewPatch(patch, {
|
|
45
|
+
expectedVersionId: state.currentVersionId,
|
|
46
|
+
reason: 'Add priority field to Todo',
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
if (preview.risk?.level === 'dangerous') {
|
|
50
|
+
// stop and ask for confirmation
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const applied = await client.schema.applyPatch(patch, {
|
|
54
|
+
expectedVersionId: state.currentVersionId,
|
|
55
|
+
reason: 'Add priority field to Todo',
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Available schema methods:
|
|
60
|
+
|
|
61
|
+
| Method | Use |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `listDefinitions()` | Get current entity definitions. |
|
|
64
|
+
| `getDefinition(entityName)` | Inspect one entity. |
|
|
65
|
+
| `upsertDefinitions(definition | definition[])` | Direct definition upsert; prefer patch for agent changes. |
|
|
66
|
+
| `operate(operation)` | Single operation endpoint. |
|
|
67
|
+
| `previewPatch(patch, options)` | Risk/diff preview without applying. |
|
|
68
|
+
| `applyPatch(patch, options)` | Apply versioned patch. |
|
|
69
|
+
| `getState()` | Current schema version pointer. |
|
|
70
|
+
| `listVersions()` | Version history. |
|
|
71
|
+
| `getVersion(versionId)` | Inspect version manifest. |
|
|
72
|
+
| `restore(versionId, reason?)` | Restore definitions by creating a new restore version. |
|
|
73
|
+
|
|
74
|
+
## Operation Types
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
type EntitySchemaOperationType =
|
|
78
|
+
| 'list_entities'
|
|
79
|
+
| 'get_entity'
|
|
80
|
+
| 'create_entity'
|
|
81
|
+
| 'update_entity'
|
|
82
|
+
| 'delete_entity'
|
|
83
|
+
| 'add_field'
|
|
84
|
+
| 'update_field'
|
|
85
|
+
| 'delete_field'
|
|
86
|
+
| 'set_field_required'
|
|
87
|
+
| 'unset_field_required'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Operation shape:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
type EntitySchemaOperation = {
|
|
94
|
+
type: EntitySchemaOperationType
|
|
95
|
+
entityName?: string
|
|
96
|
+
payload?: Record<string, unknown>
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Patch shape:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
type EntitySchemaPatch = {
|
|
104
|
+
operations: EntitySchemaOperation[]
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Operation Payloads
|
|
109
|
+
|
|
110
|
+
### create_entity
|
|
111
|
+
|
|
112
|
+
Use when the entity does not exist.
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"type": "create_entity",
|
|
117
|
+
"entityName": "Todo",
|
|
118
|
+
"payload": {
|
|
119
|
+
"definition": {
|
|
120
|
+
"name": "Todo",
|
|
121
|
+
"type": "object",
|
|
122
|
+
"visibility": "private",
|
|
123
|
+
"properties": {
|
|
124
|
+
"text": { "type": "string" },
|
|
125
|
+
"completed": { "type": "boolean", "default": false }
|
|
126
|
+
},
|
|
127
|
+
"required": ["text"],
|
|
128
|
+
"access": {
|
|
129
|
+
"authenticated": { "read": "own", "create": "own", "update": "own", "delete": "own" },
|
|
130
|
+
"public": { "read": "none", "create": "none", "update": "none", "delete": "none" }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Rules:
|
|
138
|
+
|
|
139
|
+
- `payload.definition.properties` is required.
|
|
140
|
+
- `required` must reference existing fields.
|
|
141
|
+
- Include explicit `access`, even if `visibility` seems obvious.
|
|
142
|
+
|
|
143
|
+
### update_entity
|
|
144
|
+
|
|
145
|
+
Use for metadata and contract sections, not individual field changes.
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"type": "update_entity",
|
|
150
|
+
"entityName": "Article",
|
|
151
|
+
"payload": {
|
|
152
|
+
"patch": {
|
|
153
|
+
"access": {
|
|
154
|
+
"authenticated": { "read": "all", "create": "all", "update": "all", "delete": "all" },
|
|
155
|
+
"public": {
|
|
156
|
+
"read": "list",
|
|
157
|
+
"create": "none",
|
|
158
|
+
"update": "none",
|
|
159
|
+
"delete": "none",
|
|
160
|
+
"allowedFilters": ["status", "slug"],
|
|
161
|
+
"allowedSorts": ["publishedAt"],
|
|
162
|
+
"defaultLimit": 20,
|
|
163
|
+
"maxLimit": 100
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
"performance": {
|
|
167
|
+
"defaultLimit": 20,
|
|
168
|
+
"maxLimit": 100,
|
|
169
|
+
"allowedSorts": ["publishedAt", "updatedDate"]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Use this for:
|
|
177
|
+
|
|
178
|
+
- `description`
|
|
179
|
+
- `visibility`
|
|
180
|
+
- `access`
|
|
181
|
+
- `indexes`
|
|
182
|
+
- `relations`
|
|
183
|
+
- `presentation`
|
|
184
|
+
- `lifecycle`
|
|
185
|
+
- `performance`
|
|
186
|
+
|
|
187
|
+
### add_field
|
|
188
|
+
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"type": "add_field",
|
|
192
|
+
"entityName": "Todo",
|
|
193
|
+
"payload": {
|
|
194
|
+
"fieldName": "priority",
|
|
195
|
+
"field": {
|
|
196
|
+
"type": "string",
|
|
197
|
+
"enum": ["low", "medium", "high"],
|
|
198
|
+
"default": "medium"
|
|
199
|
+
},
|
|
200
|
+
"required": false
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Rules:
|
|
206
|
+
|
|
207
|
+
- If adding a required field to an existing entity, prefer a default.
|
|
208
|
+
- If no default exists and records already exist, treat as risky and ask for migration policy.
|
|
209
|
+
- Add indexes only when new query paths need them.
|
|
210
|
+
|
|
211
|
+
### update_field
|
|
212
|
+
|
|
213
|
+
```json
|
|
214
|
+
{
|
|
215
|
+
"type": "update_field",
|
|
216
|
+
"entityName": "Todo",
|
|
217
|
+
"payload": {
|
|
218
|
+
"fieldName": "priority",
|
|
219
|
+
"patch": {
|
|
220
|
+
"enum": ["low", "medium", "high", "urgent"]
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Risk levels:
|
|
227
|
+
|
|
228
|
+
- Adding enum values: usually safe.
|
|
229
|
+
- Removing enum values: risky if existing records use them.
|
|
230
|
+
- Changing type: high risk.
|
|
231
|
+
- Making nullable field required: high risk.
|
|
232
|
+
- Removing default: risky for create flows.
|
|
233
|
+
|
|
234
|
+
### delete_field
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"type": "delete_field",
|
|
239
|
+
"entityName": "Todo",
|
|
240
|
+
"payload": {
|
|
241
|
+
"fieldName": "legacyTag",
|
|
242
|
+
"removeFromData": false
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Rules:
|
|
248
|
+
|
|
249
|
+
- Default `removeFromData` to `false`.
|
|
250
|
+
- Only use `removeFromData: true` after explicit confirmation.
|
|
251
|
+
- Removing from schema does not necessarily remove historical data.
|
|
252
|
+
|
|
253
|
+
### set_field_required / unset_field_required
|
|
254
|
+
|
|
255
|
+
```json
|
|
256
|
+
{
|
|
257
|
+
"type": "set_field_required",
|
|
258
|
+
"entityName": "Todo",
|
|
259
|
+
"payload": { "fieldName": "text" }
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Rules:
|
|
264
|
+
|
|
265
|
+
- Required fields must exist in `properties`.
|
|
266
|
+
- Setting required on an existing field is risky unless a default exists or old records are acceptable.
|
|
267
|
+
- Unsetting required is generally safe but may change frontend validation expectations.
|
|
268
|
+
|
|
269
|
+
### delete_entity
|
|
270
|
+
|
|
271
|
+
```json
|
|
272
|
+
{
|
|
273
|
+
"type": "delete_entity",
|
|
274
|
+
"entityName": "Todo",
|
|
275
|
+
"payload": {
|
|
276
|
+
"hard": false,
|
|
277
|
+
"deleteData": false
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Rules:
|
|
283
|
+
|
|
284
|
+
- Default to soft delete.
|
|
285
|
+
- Hard delete requires explicit user request.
|
|
286
|
+
- `deleteData: true` requires explicit confirmation because it destroys records.
|
|
287
|
+
|
|
288
|
+
## Risk Checklist
|
|
289
|
+
|
|
290
|
+
Stop and ask before applying when:
|
|
291
|
+
|
|
292
|
+
- deleting an entity;
|
|
293
|
+
- deleting a field used by UI or workflow output;
|
|
294
|
+
- removing historical data;
|
|
295
|
+
- changing field type;
|
|
296
|
+
- making a field required without default;
|
|
297
|
+
- broadening public access;
|
|
298
|
+
- enabling public write;
|
|
299
|
+
- reducing public guardrails such as removing required scopes or raising max limits;
|
|
300
|
+
- changing owner/public scope semantics.
|
|
301
|
+
|
|
302
|
+
Usually safe:
|
|
303
|
+
|
|
304
|
+
- adding optional field;
|
|
305
|
+
- adding field with default;
|
|
306
|
+
- adding enum value;
|
|
307
|
+
- adding index for existing query path;
|
|
308
|
+
- adding presentation metadata;
|
|
309
|
+
- adding stricter public filter/sort limits.
|
|
310
|
+
|
|
311
|
+
## Version Semantics
|
|
312
|
+
|
|
313
|
+
Schema versions manage definitions, not business records.
|
|
314
|
+
|
|
315
|
+
Restore behavior:
|
|
316
|
+
|
|
317
|
+
```text
|
|
318
|
+
restore(versionId)
|
|
319
|
+
-> creates a new current schema version from old manifest
|
|
320
|
+
-> future create/update uses restored definitions
|
|
321
|
+
-> old entitydatashares records are not rolled back
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Data records may still carry old fields after schema changes. Do not assume restore deletes or
|
|
325
|
+
rewrites data.
|
|
326
|
+
|
|
327
|
+
## Manifest Sync Handoff
|
|
328
|
+
|
|
329
|
+
After apply, the result should include a version hint:
|
|
330
|
+
|
|
331
|
+
```json
|
|
332
|
+
{
|
|
333
|
+
"next": {
|
|
334
|
+
"recommendedAction": "sync_schema_artifacts",
|
|
335
|
+
"versionId": "dbv_next"
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
Agent handoff sequence:
|
|
341
|
+
|
|
342
|
+
1. Sync artifacts for `versionId`.
|
|
343
|
+
2. Read `.howone/database/manifest.json`.
|
|
344
|
+
3. Update `src/lib/sdk.ts` generated entity bindings.
|
|
345
|
+
4. Update frontend CRUD/query code.
|
|
346
|
+
5. Validate build.
|
|
347
|
+
|
|
348
|
+
Never update frontend entity types from the draft patch alone when a synced manifest exists.
|
|
349
|
+
|
|
350
|
+
## Common Agent Mistakes
|
|
351
|
+
|
|
352
|
+
| Mistake | Correct behavior |
|
|
353
|
+
|---|---|
|
|
354
|
+
| Hand-writing `.howone/database/manifest.json` | Sync from backend version. |
|
|
355
|
+
| Applying one operation after previewing a different patch | Apply the exact previewed patch. |
|
|
356
|
+
| Adding required field without default | Treat as risk; ask migration/default policy. |
|
|
357
|
+
| Public read list without `allowedFilters` / `allowedSorts` | Add public guardrails. |
|
|
358
|
+
| Using `visibility: "public"` as permission model | Write explicit `access.public`. |
|
|
359
|
+
| Deleting data because schema changed | Schema changes do not imply data deletion. |
|
|
360
|
+
| Updating `src/lib/sdk.ts` before manifest sync | Wait for synced manifest. |
|
|
361
|
+
|
|
362
|
+
## Minimal Safe Patch Template
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
const state = await client.schema.getState()
|
|
366
|
+
|
|
367
|
+
const patch = {
|
|
368
|
+
operations: [
|
|
369
|
+
{
|
|
370
|
+
type: 'add_field',
|
|
371
|
+
entityName: 'Todo',
|
|
372
|
+
payload: {
|
|
373
|
+
fieldName: 'priority',
|
|
374
|
+
field: {
|
|
375
|
+
type: 'string',
|
|
376
|
+
enum: ['low', 'medium', 'high'],
|
|
377
|
+
default: 'medium',
|
|
378
|
+
},
|
|
379
|
+
required: false,
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const preview = await client.schema.previewPatch(patch, {
|
|
386
|
+
expectedVersionId: state.currentVersionId,
|
|
387
|
+
reason: 'Add todo priority',
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
if (preview.risk?.level === 'dangerous') {
|
|
391
|
+
throw new Error('User confirmation required before applying schema patch')
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const applied = await client.schema.applyPatch(patch, {
|
|
395
|
+
expectedVersionId: state.currentVersionId,
|
|
396
|
+
reason: 'Add todo priority',
|
|
397
|
+
})
|
|
398
|
+
```
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# Data Access Patterns
|
|
2
|
+
|
|
3
|
+
Use this reference to connect backend `access` design to frontend SDK calls. It answers:
|
|
4
|
+
**which namespace should the app call, which filters are legal, and what must not be persisted?**
|
|
5
|
+
|
|
6
|
+
For schema design, read `01-schema-design.md`. For query syntax details, read
|
|
7
|
+
`04-query-dsl-and-responses.md`.
|
|
8
|
+
|
|
9
|
+
## Namespace Decision
|
|
10
|
+
|
|
11
|
+
| Schema / page need | SDK namespace | Auth header | Typical method |
|
|
12
|
+
|---|---|---|---|
|
|
13
|
+
| Current user's private records | `howone.entities.Entity` | yes | `query.mine`, `create`, `update`, `delete` |
|
|
14
|
+
| Logged-in shared records | `howone.entities.Entity` | yes | `query`, `get`, CRUD |
|
|
15
|
+
| Public list page | `howone.public.entities.Entity` | no | `query` |
|
|
16
|
+
| Public scoped share/detail | `howone.public.entities.Entity` | no | `queryScoped`, `get` with scope options |
|
|
17
|
+
| Schema tooling | `howone.schema` | yes | `previewPatch`, `applyPatch` |
|
|
18
|
+
| Low-level fallback | `howone.raw` / `howone.public.raw` | depends | only when typed method missing |
|
|
19
|
+
|
|
20
|
+
Do not mix authenticated and public namespaces for the same page without a clear reason.
|
|
21
|
+
|
|
22
|
+
## Pattern A: Private Per-User Data
|
|
23
|
+
|
|
24
|
+
Use for todos, notes, journals, saved generations, personal dashboards, private settings.
|
|
25
|
+
|
|
26
|
+
Schema:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"visibility": "private",
|
|
31
|
+
"access": {
|
|
32
|
+
"authenticated": { "read": "own", "create": "own", "update": "own", "delete": "own" },
|
|
33
|
+
"public": { "read": "none", "create": "none", "update": "none", "delete": "none" }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Frontend:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const list = await howone.entities.Todo.query.mine({
|
|
42
|
+
page: { number: 1, size: 50 },
|
|
43
|
+
orderBy: { updatedDate: 'desc' },
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
await howone.entities.Todo.create({
|
|
47
|
+
text,
|
|
48
|
+
completed: false,
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Rules:
|
|
53
|
+
|
|
54
|
+
- Do not pass `ownerId`, `created_by_id`, `createdById`, `created_by_user_id`, or `puid`.
|
|
55
|
+
- Backend derives owner from JWT/session.
|
|
56
|
+
- Use `query.mine()` for owned lists.
|
|
57
|
+
- For first auth load, call `await howone.me()` or `await howone.requireMe()`.
|
|
58
|
+
|
|
59
|
+
## Pattern B: Authenticated Shared Data
|
|
60
|
+
|
|
61
|
+
Use when logged-in app users can see shared records: team projects, CMS admin, internal catalogs.
|
|
62
|
+
|
|
63
|
+
Schema:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"visibility": "private",
|
|
68
|
+
"access": {
|
|
69
|
+
"authenticated": { "read": "all", "create": "all", "update": "all", "delete": "all" },
|
|
70
|
+
"public": { "read": "none", "create": "none", "update": "none", "delete": "none" }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Frontend:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
const list = await howone.entities.Project.query({
|
|
79
|
+
page: { number: 1, size: 20 },
|
|
80
|
+
orderBy: { updatedDate: 'desc' },
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Rules:
|
|
85
|
+
|
|
86
|
+
- Use `query()`, not `query.mine()`.
|
|
87
|
+
- Still require auth.
|
|
88
|
+
- Be conservative with `update: "all"` and `delete: "all"` unless the app has a real role model.
|
|
89
|
+
|
|
90
|
+
## Pattern C: Public Read-Only Content
|
|
91
|
+
|
|
92
|
+
Use for public articles, templates, products, profiles, published galleries.
|
|
93
|
+
|
|
94
|
+
Schema:
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"visibility": "public",
|
|
99
|
+
"access": {
|
|
100
|
+
"authenticated": { "read": "all", "create": "all", "update": "all", "delete": "all" },
|
|
101
|
+
"public": {
|
|
102
|
+
"read": "list",
|
|
103
|
+
"create": "none",
|
|
104
|
+
"update": "none",
|
|
105
|
+
"delete": "none",
|
|
106
|
+
"allowedFilters": ["slug", "status", "category"],
|
|
107
|
+
"allowedSorts": ["publishedAt", "updatedDate"],
|
|
108
|
+
"defaultLimit": 20,
|
|
109
|
+
"maxLimit": 100
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Frontend:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
const list = await howone.public.entities.Article.query({
|
|
119
|
+
where: { status: 'published', category },
|
|
120
|
+
page: { number: 1, size: 20 },
|
|
121
|
+
orderBy: { publishedAt: 'desc' },
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Rules:
|
|
126
|
+
|
|
127
|
+
- Public filters must be in `allowedFilters`.
|
|
128
|
+
- Public sorts must be in `allowedSorts`.
|
|
129
|
+
- Never pass tokens or use authenticated namespace for anonymous landing pages.
|
|
130
|
+
- Keep public result fields safe for anonymous users.
|
|
131
|
+
|
|
132
|
+
## Pattern D: Public Scoped Share Pages
|
|
133
|
+
|
|
134
|
+
Use for public URLs exposing one scoped record: QR profile, public report, resume, invite page.
|
|
135
|
+
|
|
136
|
+
Schema:
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{
|
|
140
|
+
"visibility": "public",
|
|
141
|
+
"access": {
|
|
142
|
+
"authenticated": { "read": "own", "create": "own", "update": "own", "delete": "own" },
|
|
143
|
+
"public": {
|
|
144
|
+
"read": "scoped",
|
|
145
|
+
"create": "none",
|
|
146
|
+
"update": "none",
|
|
147
|
+
"delete": "none",
|
|
148
|
+
"requiredScopes": ["ownerId", "slug"],
|
|
149
|
+
"allowedFilters": ["slug", "active"],
|
|
150
|
+
"allowedSorts": ["updatedDate"],
|
|
151
|
+
"defaultLimit": 1,
|
|
152
|
+
"maxLimit": 10
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Frontend:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
const result = await howone.public.entities.QrProfile.queryScoped({
|
|
162
|
+
where: { ownerId, slug, active: true },
|
|
163
|
+
page: { number: 1, size: 1 },
|
|
164
|
+
})
|
|
165
|
+
const profile = result.items[0] ?? null
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Rules:
|
|
169
|
+
|
|
170
|
+
- Pass every `requiredScopes` field.
|
|
171
|
+
- Do not use current JWT `puid` as `ownerId` unless schema explicitly stores that as public scope.
|
|
172
|
+
- Do not turn scoped pages into broad list pages.
|
|
173
|
+
- Keep `maxLimit` small.
|
|
174
|
+
|
|
175
|
+
## Pattern E: Public Create / Anonymous Submission
|
|
176
|
+
|
|
177
|
+
Use only for forms that must accept anonymous/public submissions: waitlist, contact, feedback,
|
|
178
|
+
public RSVP.
|
|
179
|
+
|
|
180
|
+
Schema:
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"visibility": "public",
|
|
185
|
+
"access": {
|
|
186
|
+
"authenticated": { "read": "all", "create": "all", "update": "all", "delete": "all" },
|
|
187
|
+
"public": {
|
|
188
|
+
"read": "none",
|
|
189
|
+
"create": "scoped",
|
|
190
|
+
"update": "none",
|
|
191
|
+
"delete": "none",
|
|
192
|
+
"requiredScopes": ["created_by_user_id"],
|
|
193
|
+
"allowedFilters": [],
|
|
194
|
+
"allowedSorts": [],
|
|
195
|
+
"defaultLimit": 1,
|
|
196
|
+
"maxLimit": 1
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Frontend:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
await howone.public.entities.Feedback.create({
|
|
206
|
+
created_by_user_id: projectUserId,
|
|
207
|
+
message,
|
|
208
|
+
rating,
|
|
209
|
+
})
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Rules:
|
|
213
|
+
|
|
214
|
+
- Public create needs a clear `created_by_user_id` source when backend requires ownership mapping.
|
|
215
|
+
- Do not expose public read unless needed.
|
|
216
|
+
- Add anti-abuse UX/server constraints outside the dynamic schema when needed.
|
|
217
|
+
- Never persist UI-only fields from form components.
|
|
218
|
+
|
|
219
|
+
## Pattern F: AI Workflow Output Persistence
|
|
220
|
+
|
|
221
|
+
Use for generation/analyze/report products that need history and refresh resilience.
|
|
222
|
+
|
|
223
|
+
Recommended flow:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const pending = await howone.entities.Generation.create({
|
|
227
|
+
prompt,
|
|
228
|
+
status: 'pending',
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const output = await howone.ai.generateImage.run({ prompt })
|
|
233
|
+
await howone.entities.Generation.update(pending.id, {
|
|
234
|
+
status: 'completed',
|
|
235
|
+
resultUrl: output.imageUrl,
|
|
236
|
+
completedAt: new Date().toISOString(),
|
|
237
|
+
})
|
|
238
|
+
} catch (error) {
|
|
239
|
+
await howone.entities.Generation.update(pending.id, {
|
|
240
|
+
status: 'failed',
|
|
241
|
+
errorMessage: error instanceof Error ? error.message : 'Generation failed',
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const history = await howone.entities.Generation.query.mine({
|
|
246
|
+
orderBy: { updatedDate: 'desc' },
|
|
247
|
+
page: { number: 1, size: 20 },
|
|
248
|
+
})
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Rules:
|
|
252
|
+
|
|
253
|
+
- Persist only fields declared in the entity schema.
|
|
254
|
+
- Do not persist raw workflow event streams unless schema defines an object/array field for them.
|
|
255
|
+
- Failure branch must persist failure if the product shows history.
|
|
256
|
+
- Latest result, history list, and detail pages should reload from data API, not only local state.
|
|
257
|
+
|
|
258
|
+
## Payload Whitelist Rule
|
|
259
|
+
|
|
260
|
+
Before every create/update, mentally compute:
|
|
261
|
+
|
|
262
|
+
```text
|
|
263
|
+
payload keys ⊆ entity.properties keys
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Allowed:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
await howone.entities.Todo.create({
|
|
270
|
+
text,
|
|
271
|
+
completed: false,
|
|
272
|
+
})
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Forbidden:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
await howone.entities.Todo.create({
|
|
279
|
+
text,
|
|
280
|
+
completed: false,
|
|
281
|
+
gradient_direction: 'to right', // UI-only
|
|
282
|
+
created_by_id: user.id, // system/owner field
|
|
283
|
+
workflowRawResult: output, // undeclared workflow envelope
|
|
284
|
+
})
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
If the app truly needs a new persisted field, update schema first.
|
|
288
|
+
|
|
289
|
+
## Access-to-SDK Mapping
|
|
290
|
+
|
|
291
|
+
| Access posture | Read | Create | Update/Delete |
|
|
292
|
+
|---|---|---|---|
|
|
293
|
+
| authenticated `own` | `entities.X.query.mine()` | `entities.X.create()` | `entities.X.update/delete()` |
|
|
294
|
+
| authenticated `all` | `entities.X.query()` | `entities.X.create()` | `entities.X.update/delete()` |
|
|
295
|
+
| public `list` | `public.entities.X.query()` | no | no |
|
|
296
|
+
| public `scoped` | `public.entities.X.queryScoped()` | only if `create: scoped/any` | only if `update: scoped/any` |
|
|
297
|
+
| public `none` | no public call | no public call | no public call |
|
|
298
|
+
|
|
299
|
+
## Common Mistakes
|
|
300
|
+
|
|
301
|
+
| Mistake | Fix |
|
|
302
|
+
|---|---|
|
|
303
|
+
| Passing `created_by_user_id` for normal private data | Omit owner fields; backend derives owner. |
|
|
304
|
+
| Using `entities.*` on public page | Use `public.entities.*`. |
|
|
305
|
+
| Public filter not in `allowedFilters` | Add filter to schema or remove query. |
|
|
306
|
+
| Public sort not in `allowedSorts` | Add sort to schema or change UI. |
|
|
307
|
+
| Saving workflow output object directly | Map only declared fields. |
|
|
308
|
+
| Rendering history only from local state | Reload via `query.mine()` / public query. |
|
|
309
|
+
| Treating `visibility: "public"` as enough | Always define `access.public`. |
|