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,237 @@
|
|
|
1
|
+
# Query DSL And Responses
|
|
2
|
+
|
|
3
|
+
Use this reference when implementing list/detail pages, filters, sorting, pagination, or response
|
|
4
|
+
normalization for HowOne dynamic entities.
|
|
5
|
+
|
|
6
|
+
## SDK Query Shape
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
await howone.entities.Todo.query({
|
|
10
|
+
where: {
|
|
11
|
+
completed: false,
|
|
12
|
+
priority: { in: ['medium', 'high'] },
|
|
13
|
+
updatedDate: { gte: '2026-01-01T00:00:00.000Z' },
|
|
14
|
+
},
|
|
15
|
+
search: 'invoice',
|
|
16
|
+
page: { number: 1, size: 20 },
|
|
17
|
+
orderBy: { updatedDate: 'desc' },
|
|
18
|
+
include: ['owner'],
|
|
19
|
+
exactCount: true,
|
|
20
|
+
})
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Private owner-scoped list:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
await howone.entities.Todo.query.mine({
|
|
27
|
+
page: { number: 1, size: 50 },
|
|
28
|
+
orderBy: { updatedDate: 'desc' },
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Public list:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
await howone.public.entities.Article.query({
|
|
36
|
+
where: { status: 'published' },
|
|
37
|
+
orderBy: { publishedAt: 'desc' },
|
|
38
|
+
page: { number: 1, size: 20 },
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Public scoped:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
await howone.public.entities.QrProfile.queryScoped({
|
|
46
|
+
where: { ownerId, slug, active: true },
|
|
47
|
+
page: { number: 1, size: 1 },
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Operators
|
|
52
|
+
|
|
53
|
+
Supported field operators:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
type FieldOperator<T> = {
|
|
57
|
+
eq?: T
|
|
58
|
+
equals?: T
|
|
59
|
+
ne?: T
|
|
60
|
+
not?: T
|
|
61
|
+
gt?: T
|
|
62
|
+
gte?: T
|
|
63
|
+
lt?: T
|
|
64
|
+
lte?: T
|
|
65
|
+
contains?: string
|
|
66
|
+
like?: string
|
|
67
|
+
startsWith?: string
|
|
68
|
+
starts?: string
|
|
69
|
+
endsWith?: string
|
|
70
|
+
ends?: string
|
|
71
|
+
in?: T[]
|
|
72
|
+
notIn?: T[]
|
|
73
|
+
null?: boolean
|
|
74
|
+
empty?: boolean
|
|
75
|
+
exists?: boolean
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
where: { status: 'published' }
|
|
83
|
+
where: { status: { eq: 'published' } }
|
|
84
|
+
where: { score: { gte: 80, lt: 100 } }
|
|
85
|
+
where: { category: { in: ['news', 'guide'] } }
|
|
86
|
+
where: { title: { contains: 'AI' } }
|
|
87
|
+
where: { deletedAt: { null: true } }
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Pagination
|
|
91
|
+
|
|
92
|
+
Use SDK page object:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
page: { number: 1, size: 20 }
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Response:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
type QueryResult<T> = {
|
|
102
|
+
items: T[]
|
|
103
|
+
page: {
|
|
104
|
+
number: number
|
|
105
|
+
size: number
|
|
106
|
+
total: number
|
|
107
|
+
totalPages: number
|
|
108
|
+
hasNext: boolean
|
|
109
|
+
hasPrev: boolean
|
|
110
|
+
}
|
|
111
|
+
traceId?: string | number
|
|
112
|
+
raw?: unknown
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Always render from a normalized array:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const result = await howone.entities.Todo.query.mine(...)
|
|
120
|
+
const items = Array.isArray(result.items) ? result.items : []
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Sorting
|
|
124
|
+
|
|
125
|
+
SDK uses:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
orderBy: { updatedDate: 'desc' }
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Rules:
|
|
132
|
+
|
|
133
|
+
- Public sort fields must be in `access.public.allowedSorts`.
|
|
134
|
+
- Private sort fields should be in `performance.allowedSorts` and covered by indexes.
|
|
135
|
+
- Prefer `updatedDate` for recently changed lists and `createdDate` for creation history.
|
|
136
|
+
- Do not expose arbitrary public sort fields.
|
|
137
|
+
|
|
138
|
+
## Include / Relations
|
|
139
|
+
|
|
140
|
+
Use `include` only for relation names declared in schema `relations`.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
await howone.entities.Article.query({
|
|
144
|
+
include: ['author'],
|
|
145
|
+
})
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Rules:
|
|
149
|
+
|
|
150
|
+
- Do not invent include names.
|
|
151
|
+
- Public includes must be safe for anonymous exposure.
|
|
152
|
+
- If a list needs a small display field frequently, consider denormalizing it instead of requiring include on every row.
|
|
153
|
+
|
|
154
|
+
## Detail Reads
|
|
155
|
+
|
|
156
|
+
Authenticated:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const item = await howone.entities.Todo.get(id)
|
|
160
|
+
const item = await howone.entities.Todo.getOrThrow(id)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Public:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
const item = await howone.public.entities.Article.get(id)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
For public scoped detail, prefer `queryScoped` when the route naturally has scope fields like
|
|
170
|
+
`ownerId + slug`.
|
|
171
|
+
|
|
172
|
+
## Response Field Names
|
|
173
|
+
|
|
174
|
+
SDK defaults to `caseStyle: 'camel'`, so responses are normalized toward camelCase where supported.
|
|
175
|
+
Still know the backend system field meanings:
|
|
176
|
+
|
|
177
|
+
| Backend concept | Common SDK field |
|
|
178
|
+
|---|---|
|
|
179
|
+
| record id | `id` |
|
|
180
|
+
| created date | `createdDate` or `created_date` depending case style |
|
|
181
|
+
| updated date | `updatedDate` or `updated_date` |
|
|
182
|
+
| owner | `createdById` or `created_by_id` |
|
|
183
|
+
| schema version id | `schemaVersionId` or `schema_version_id` |
|
|
184
|
+
| schema version number | `schemaVersionNumber` or `schema_version_number` |
|
|
185
|
+
|
|
186
|
+
Do not use `_id` in app code unless inspecting raw backend payloads.
|
|
187
|
+
|
|
188
|
+
## Public Guardrails
|
|
189
|
+
|
|
190
|
+
Before writing public query code, check schema:
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
"public": {
|
|
194
|
+
"read": "list",
|
|
195
|
+
"allowedFilters": ["status", "slug"],
|
|
196
|
+
"allowedSorts": ["publishedAt"],
|
|
197
|
+
"defaultLimit": 20,
|
|
198
|
+
"maxLimit": 100
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Then only use allowed filters and sorts:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
// OK
|
|
206
|
+
where: { status: 'published' }
|
|
207
|
+
orderBy: { publishedAt: 'desc' }
|
|
208
|
+
|
|
209
|
+
// Not OK unless listed in access.public
|
|
210
|
+
where: { internalReviewState: 'approved' }
|
|
211
|
+
orderBy: { revenue: 'desc' }
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Search
|
|
215
|
+
|
|
216
|
+
Use `search` for broad text search only when the backend/schema supports the desired behavior:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
await howone.public.entities.Article.query({
|
|
220
|
+
search: query,
|
|
221
|
+
where: { status: 'published' },
|
|
222
|
+
})
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Do not use `search` as a substitute for required public scopes.
|
|
226
|
+
|
|
227
|
+
## Common Mistakes
|
|
228
|
+
|
|
229
|
+
| Mistake | Fix |
|
|
230
|
+
|---|---|
|
|
231
|
+
| Rendering `res.data.data` from SDK query | SDK returns `QueryResult.items`; render `result.items`. |
|
|
232
|
+
| Using `_id` as record id | Use `id`. |
|
|
233
|
+
| Public query with unlisted filter | Add to `allowedFilters` or remove it. |
|
|
234
|
+
| Public query with unlisted sort | Add to `allowedSorts` or change sorting. |
|
|
235
|
+
| Passing owner filters in authenticated `own` queries | Use `query.mine()` and omit owner fields. |
|
|
236
|
+
| Using include without schema relation | Add relation first or remove include. |
|
|
237
|
+
| No pagination on list pages | Always pass `page` and respect `maxLimit`. |
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# AI Persistence Patterns
|
|
2
|
+
|
|
3
|
+
Use this reference when an AI workflow output must become durable product data: generation history,
|
|
4
|
+
saved results, analysis reports, retryable jobs, share pages, or user libraries.
|
|
5
|
+
|
|
6
|
+
AI workflow contracts describe **how to produce an output**. Entity schemas describe **what the app
|
|
7
|
+
persists, lists, edits, shares, and reloads**. Do not merge those two responsibilities.
|
|
8
|
+
|
|
9
|
+
## Core Rule
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
AI capability outputSchema != database entity schema
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The output schema is the workflow return contract. It can contain transient execution details,
|
|
16
|
+
intermediate data, model metadata, or provider-shaped structures. The database schema is a product
|
|
17
|
+
contract. It should store only fields that the app needs after refresh, across sessions, or on public
|
|
18
|
+
pages.
|
|
19
|
+
|
|
20
|
+
For every AI feature, ask:
|
|
21
|
+
|
|
22
|
+
| Question | Persist? | Where |
|
|
23
|
+
|---|---:|---|
|
|
24
|
+
| Must the user see this after refresh? | yes | Entity field |
|
|
25
|
+
| Must it appear in history/library/search? | yes | Entity field + index if queried |
|
|
26
|
+
| Is it only needed while streaming/running? | no | Local UI state / runtime event |
|
|
27
|
+
| Is it provider debug data? | usually no | Logs, not entity data |
|
|
28
|
+
| Is it needed to retry or resume? | yes | Entity field |
|
|
29
|
+
| Is it sensitive model/provider metadata? | usually no | Avoid public entity fields |
|
|
30
|
+
|
|
31
|
+
## Recommended Flow
|
|
32
|
+
|
|
33
|
+
For long-running or user-visible AI operations, create a pending record before calling the workflow.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { runAiActionAndPersist } from '@howone/sdk'
|
|
37
|
+
|
|
38
|
+
await runAiActionAndPersist({
|
|
39
|
+
entity: howone.entities.Generation,
|
|
40
|
+
input: { prompt },
|
|
41
|
+
createPending: (input) => ({
|
|
42
|
+
prompt: input.prompt,
|
|
43
|
+
status: 'pending',
|
|
44
|
+
requestedAt: new Date().toISOString(),
|
|
45
|
+
}),
|
|
46
|
+
run: (input) => howone.ai.generateImage.run(input),
|
|
47
|
+
mapCompleted: ({ output }) => ({
|
|
48
|
+
status: 'completed',
|
|
49
|
+
resultUrl: output.imageUrl,
|
|
50
|
+
completedAt: new Date().toISOString(),
|
|
51
|
+
}),
|
|
52
|
+
mapFailed: ({ error }) => ({
|
|
53
|
+
status: 'failed',
|
|
54
|
+
errorMessage: error instanceof Error ? error.message : 'Generation failed',
|
|
55
|
+
}),
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Why pending-first:
|
|
60
|
+
|
|
61
|
+
- refresh can show an in-progress item instead of losing the request;
|
|
62
|
+
- failure can be displayed in history;
|
|
63
|
+
- retry can reuse the original prompt/options;
|
|
64
|
+
- support/debug can identify which input produced the failed state;
|
|
65
|
+
- UI can render from persisted data instead of assuming local state survived.
|
|
66
|
+
|
|
67
|
+
For very fast, disposable AI actions, persistence may be unnecessary. Do not create an entity just
|
|
68
|
+
because an AI workflow exists.
|
|
69
|
+
|
|
70
|
+
## Status Fields
|
|
71
|
+
|
|
72
|
+
Every persisted AI job/history entity should have a status field.
|
|
73
|
+
|
|
74
|
+
Recommended:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"status": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"description": "pending | running | completed | failed | canceled",
|
|
81
|
+
"default": "pending"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Use stable string values:
|
|
87
|
+
|
|
88
|
+
| Status | Meaning |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `pending` | Record exists, workflow has not produced final output. |
|
|
91
|
+
| `running` | Optional if the app receives a running state after submission. |
|
|
92
|
+
| `completed` | Output fields are valid for display. |
|
|
93
|
+
| `failed` | `errorMessage` or failure fields explain the failure. |
|
|
94
|
+
| `canceled` | User/system canceled and no final output should be expected. |
|
|
95
|
+
|
|
96
|
+
Rules:
|
|
97
|
+
|
|
98
|
+
- Do not infer completion only from `resultUrl` or another output field.
|
|
99
|
+
- Keep failed records when the product has history or retry UX.
|
|
100
|
+
- If the UI shows a spinner from persisted data, it must also handle stale `pending/running` records.
|
|
101
|
+
|
|
102
|
+
## Minimal Generation History Schema
|
|
103
|
+
|
|
104
|
+
Use for image/text/report/music/video generation history.
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"name": "Generation",
|
|
109
|
+
"type": "object",
|
|
110
|
+
"visibility": "private",
|
|
111
|
+
"properties": {
|
|
112
|
+
"prompt": { "type": "string" },
|
|
113
|
+
"status": { "type": "string", "default": "pending" },
|
|
114
|
+
"resultUrl": { "type": ["string", "null"], "default": null },
|
|
115
|
+
"resultText": { "type": ["string", "null"], "default": null },
|
|
116
|
+
"errorMessage": { "type": ["string", "null"], "default": null },
|
|
117
|
+
"requestedAt": { "type": "date" },
|
|
118
|
+
"completedAt": { "type": ["date", "null"], "default": null }
|
|
119
|
+
},
|
|
120
|
+
"required": ["prompt", "status", "requestedAt"],
|
|
121
|
+
"access": {
|
|
122
|
+
"authenticated": { "read": "own", "create": "own", "update": "own", "delete": "own" },
|
|
123
|
+
"public": { "read": "none", "create": "none", "update": "none", "delete": "none" }
|
|
124
|
+
},
|
|
125
|
+
"indexes": [
|
|
126
|
+
{ "name": "owner_updated", "fields": ["updatedDate"], "scope": "owner" },
|
|
127
|
+
{ "name": "owner_status_updated", "fields": ["status", "updatedDate"], "scope": "owner" }
|
|
128
|
+
],
|
|
129
|
+
"performance": {
|
|
130
|
+
"defaultLimit": 20,
|
|
131
|
+
"maxLimit": 100,
|
|
132
|
+
"allowedSorts": ["updatedDate", "requestedAt"]
|
|
133
|
+
},
|
|
134
|
+
"presentation": {
|
|
135
|
+
"titleField": "prompt",
|
|
136
|
+
"subtitleField": "status"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Use separate result fields instead of one opaque `result` object when the UI lists or filters them.
|
|
142
|
+
Use an object field only for genuinely nested, product-level structured results.
|
|
143
|
+
|
|
144
|
+
## Analysis Report Schema
|
|
145
|
+
|
|
146
|
+
Use when AI returns a structured report that users browse later.
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"name": "AnalysisReport",
|
|
151
|
+
"type": "object",
|
|
152
|
+
"visibility": "private",
|
|
153
|
+
"properties": {
|
|
154
|
+
"sourceTitle": { "type": "string" },
|
|
155
|
+
"sourceUrl": { "type": ["string", "null"], "default": null },
|
|
156
|
+
"status": { "type": "string", "default": "pending" },
|
|
157
|
+
"summary": { "type": ["string", "null"], "default": null },
|
|
158
|
+
"insights": { "type": "array", "default": [] },
|
|
159
|
+
"score": { "type": ["number", "null"], "default": null },
|
|
160
|
+
"errorMessage": { "type": ["string", "null"], "default": null },
|
|
161
|
+
"requestedAt": { "type": "date" },
|
|
162
|
+
"completedAt": { "type": ["date", "null"], "default": null }
|
|
163
|
+
},
|
|
164
|
+
"required": ["sourceTitle", "status", "requestedAt"],
|
|
165
|
+
"access": {
|
|
166
|
+
"authenticated": { "read": "own", "create": "own", "update": "own", "delete": "own" },
|
|
167
|
+
"public": { "read": "none", "create": "none", "update": "none", "delete": "none" }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Mapping rule:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
const output = await howone.ai.analyzeDocument.run(input)
|
|
176
|
+
|
|
177
|
+
await howone.entities.AnalysisReport.update(report.id, {
|
|
178
|
+
status: 'completed',
|
|
179
|
+
summary: output.summary,
|
|
180
|
+
insights: output.insights,
|
|
181
|
+
score: output.score,
|
|
182
|
+
completedAt: new Date().toISOString(),
|
|
183
|
+
})
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Do not save the whole workflow envelope unless the entity has an explicitly designed field for that
|
|
187
|
+
envelope and the product needs it.
|
|
188
|
+
|
|
189
|
+
## Public AI Result Share Page
|
|
190
|
+
|
|
191
|
+
Use when a user can share one generated result publicly.
|
|
192
|
+
|
|
193
|
+
Recommended split:
|
|
194
|
+
|
|
195
|
+
- private `Generation` entity for user history and edits;
|
|
196
|
+
- public or scoped `SharedGeneration` entity for anonymous viewing.
|
|
197
|
+
|
|
198
|
+
Why split:
|
|
199
|
+
|
|
200
|
+
- private history may include prompts, failures, drafts, and internal metadata;
|
|
201
|
+
- public page should expose only curated fields;
|
|
202
|
+
- public access rules stay simple and auditable;
|
|
203
|
+
- unsharing can delete or deactivate the shared record without destroying private history.
|
|
204
|
+
|
|
205
|
+
Public scoped schema:
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"name": "SharedGeneration",
|
|
210
|
+
"type": "object",
|
|
211
|
+
"visibility": "public",
|
|
212
|
+
"properties": {
|
|
213
|
+
"shareId": {
|
|
214
|
+
"type": "string",
|
|
215
|
+
"autoGenerate": { "strategy": "uuid" }
|
|
216
|
+
},
|
|
217
|
+
"title": { "type": "string" },
|
|
218
|
+
"resultUrl": { "type": "string" },
|
|
219
|
+
"active": { "type": "boolean", "default": true },
|
|
220
|
+
"sourceGenerationId": { "type": "string" }
|
|
221
|
+
},
|
|
222
|
+
"required": ["shareId", "title", "resultUrl", "active", "sourceGenerationId"],
|
|
223
|
+
"access": {
|
|
224
|
+
"authenticated": { "read": "own", "create": "own", "update": "own", "delete": "own" },
|
|
225
|
+
"public": {
|
|
226
|
+
"read": "scoped",
|
|
227
|
+
"create": "none",
|
|
228
|
+
"update": "none",
|
|
229
|
+
"delete": "none",
|
|
230
|
+
"requiredScopes": ["shareId"],
|
|
231
|
+
"allowedFilters": ["shareId", "active"],
|
|
232
|
+
"allowedSorts": ["updatedDate"],
|
|
233
|
+
"defaultLimit": 1,
|
|
234
|
+
"maxLimit": 1
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
"indexes": [
|
|
238
|
+
{ "name": "share_id_unique", "fields": ["shareId"], "unique": true },
|
|
239
|
+
{ "name": "owner_updated", "fields": ["updatedDate"], "scope": "owner" }
|
|
240
|
+
]
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Public page:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
const result = await howone.public.entities.SharedGeneration.queryScoped({
|
|
248
|
+
where: { shareId, active: true },
|
|
249
|
+
page: { number: 1, size: 1 },
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
const shared = result.items[0] ?? null
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Rules:
|
|
256
|
+
|
|
257
|
+
- Do not expose private prompt fields unless the product explicitly wants that.
|
|
258
|
+
- Use `active` or a deletion flow for unshare.
|
|
259
|
+
- Keep public `maxLimit` at `1` for one-share pages.
|
|
260
|
+
|
|
261
|
+
## Retry Design
|
|
262
|
+
|
|
263
|
+
If the product has retry, store enough input fields to rebuild the workflow request.
|
|
264
|
+
|
|
265
|
+
Persist:
|
|
266
|
+
|
|
267
|
+
- user prompt or source content reference;
|
|
268
|
+
- selected mode/model/style options that affect output;
|
|
269
|
+
- uploaded file IDs/URLs needed by the workflow;
|
|
270
|
+
- status and failure message;
|
|
271
|
+
- timestamps.
|
|
272
|
+
|
|
273
|
+
Do not persist:
|
|
274
|
+
|
|
275
|
+
- temporary UI component state;
|
|
276
|
+
- auth/session/token values;
|
|
277
|
+
- raw streaming chunks;
|
|
278
|
+
- provider secrets;
|
|
279
|
+
- hidden prompt text that should stay server-side;
|
|
280
|
+
- large binary content when file upload should store a URL or file id.
|
|
281
|
+
|
|
282
|
+
Retry example:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
const old = await howone.entities.Generation.getOrThrow(id)
|
|
286
|
+
|
|
287
|
+
const retry = await howone.entities.Generation.create({
|
|
288
|
+
prompt: old.prompt,
|
|
289
|
+
status: 'pending',
|
|
290
|
+
requestedAt: new Date().toISOString(),
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
const output = await howone.ai.generateImage.run({
|
|
294
|
+
prompt: old.prompt,
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
await howone.entities.Generation.update(retry.id, {
|
|
298
|
+
status: 'completed',
|
|
299
|
+
resultUrl: output.imageUrl,
|
|
300
|
+
completedAt: new Date().toISOString(),
|
|
301
|
+
})
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Prefer creating a new retry record when the product is history-oriented. Prefer updating the same
|
|
305
|
+
record only when the product treats retry as replacing the original attempt.
|
|
306
|
+
|
|
307
|
+
## Resume Design
|
|
308
|
+
|
|
309
|
+
On page load, query persisted records instead of relying on local state:
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
const history = await howone.entities.Generation.query.mine({
|
|
313
|
+
where: { status: { in: ['pending', 'running', 'completed', 'failed'] } },
|
|
314
|
+
orderBy: { updatedDate: 'desc' },
|
|
315
|
+
page: { number: 1, size: 20 },
|
|
316
|
+
})
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Then:
|
|
320
|
+
|
|
321
|
+
- render `completed` from output fields;
|
|
322
|
+
- render `failed` from `errorMessage`;
|
|
323
|
+
- render `pending/running` as in progress only if the app has a way to poll status;
|
|
324
|
+
- mark stale pending records as failed/canceled when product rules define a timeout.
|
|
325
|
+
|
|
326
|
+
Do not leave indefinite pending rows with no recovery path.
|
|
327
|
+
|
|
328
|
+
## Field Mapping Checklist
|
|
329
|
+
|
|
330
|
+
Before implementing persistence, write the mapping explicitly:
|
|
331
|
+
|
|
332
|
+
```text
|
|
333
|
+
workflow input.prompt -> Generation.prompt
|
|
334
|
+
workflow input.style -> Generation.style
|
|
335
|
+
workflow output.imageUrl -> Generation.resultUrl
|
|
336
|
+
workflow output.caption -> Generation.resultText
|
|
337
|
+
workflow error.message -> Generation.errorMessage
|
|
338
|
+
runtime request started -> Generation.requestedAt
|
|
339
|
+
runtime request completed -> Generation.completedAt
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
If a workflow output field has no mapping, decide whether it is intentionally transient or whether
|
|
343
|
+
the entity schema is missing a product field.
|
|
344
|
+
|
|
345
|
+
## Access Checklist
|
|
346
|
+
|
|
347
|
+
Choose access based on product behavior:
|
|
348
|
+
|
|
349
|
+
| Product behavior | Entity access |
|
|
350
|
+
|---|---|
|
|
351
|
+
| User-only private generation history | authenticated own, public none |
|
|
352
|
+
| Team/shared authenticated library | authenticated all or future role-scoped model |
|
|
353
|
+
| Anonymous public gallery | public list with safe fields only |
|
|
354
|
+
| One public share link | public scoped with share id |
|
|
355
|
+
| Public submission to AI queue | public create only if abuse constraints are handled |
|
|
356
|
+
|
|
357
|
+
Never make the main generation history public just to support a public share page. Create a scoped
|
|
358
|
+
share entity or a curated public entity.
|
|
359
|
+
|
|
360
|
+
## Common Mistakes
|
|
361
|
+
|
|
362
|
+
| Mistake | Fix |
|
|
363
|
+
|---|---|
|
|
364
|
+
| Treating `outputSchema` as the entity schema | Design product persistence separately. |
|
|
365
|
+
| Saving raw workflow envelopes | Map only fields the product needs after refresh. |
|
|
366
|
+
| No status field | Add `status` and explicit failure fields. |
|
|
367
|
+
| Creating history only after success | Create pending first when history/resume matters. |
|
|
368
|
+
| Losing failures | Persist `failed` state and `errorMessage`. |
|
|
369
|
+
| Publicly exposing private prompt/history | Use a separate public scoped/share entity. |
|
|
370
|
+
| Retrying without stored inputs | Persist the inputs/options needed for retry. |
|
|
371
|
+
| Rendering from local state only | Reload from entity queries on page load. |
|
|
372
|
+
| Leaving stale pending forever | Add timeout/recovery behavior in product logic. |
|