howone 0.2.3 → 0.2.5
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 +8 -7
- package/templates/vite/.howone/skills/howone/01-architect/02-manifest-codegen.md +121 -436
- package/templates/vite/.howone/skills/howone/03-ai-capabilities/04-workflow-operations.md +13 -4
- package/templates/vite/.howone/skills/howone/04-app-sdk/01-client-setup.md +94 -261
- package/templates/vite/.howone/skills/howone/04-app-sdk/02-entity-operations.md +85 -465
- package/templates/vite/.howone/skills/howone/04-app-sdk/03-auth.md +11 -7
- package/templates/vite/.howone/skills/howone/04-app-sdk/04-react-integration.md +84 -137
- package/templates/vite/.howone/skills/howone/04-app-sdk/05-file-upload.md +66 -273
- package/templates/vite/.howone/skills/howone/04-app-sdk/06-raw-http.md +72 -249
- package/templates/vite/.howone/skills/howone/04-app-sdk/07-ai-action-calls.md +135 -499
- package/templates/vite/.howone/skills/howone/04-app-sdk/08-ai-manifest-handoff.md +49 -196
- package/templates/vite/.howone/skills/howone/04-app-sdk/09-extension-boundaries.md +4 -4
- package/templates/vite/.howone/skills/howone/04-app-sdk/10-workflow-execute-sse.md +94 -61
- package/templates/vite/.howone/skills/howone/04-app-sdk/11-entity-data-access-patterns.md +4 -3
- package/templates/vite/.howone/skills/howone/SKILL.md +48 -8
- package/templates/vite/.howone/skills/howone/references/common-errors.md +27 -0
- package/templates/vite/.howone/skills/howone/references/version-evidence.md +47 -0
- package/templates/vite/.howone/skills/howone/scripts/verify-project.mjs +151 -0
- package/templates/vite/package.json +1 -1
- package/templates/vite/src/App.tsx +9 -5
- package/templates/vite/src/lib/sdk.ts +7 -5
- package/templates/vite/bun.lock +0 -1478
|
@@ -1,543 +1,163 @@
|
|
|
1
1
|
# Entity Operations
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use generated bindings from `src/lib/sdk.ts` for manifest entities. Read
|
|
4
|
+
`01-architect/02-manifest-codegen.md` before adding or changing a binding.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
- `defineEntities({ ... })` groups entity clients.
|
|
7
|
-
- `withEntities(client, entities)` merges them onto the composed client as `howone.entities.*`.
|
|
8
|
-
- All entity calls are **plain async functions** — no hooks, no subscriptions.
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## Type Definitions
|
|
13
|
-
|
|
14
|
-
### EntityRecord (base type)
|
|
15
|
-
|
|
16
|
-
```ts
|
|
17
|
-
type EntityRecord = {
|
|
18
|
-
id: string
|
|
19
|
-
createdDate?: string
|
|
20
|
-
updatedDate?: string
|
|
21
|
-
createdById?: string // backend owner id from created_by_id
|
|
22
|
-
schemaVersionId?: string
|
|
23
|
-
schemaVersionNumber?: number
|
|
24
|
-
isSample?: boolean
|
|
25
|
-
[key: string]: unknown // index signature — important for typing
|
|
26
|
-
}
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
### Defining entity types
|
|
30
|
-
|
|
31
|
-
Always define all three types explicitly. **Do not** use `Omit<EntityRecord & ...>` for create types — the index signature widens the payload.
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
import { type EntityRecord } from '@howone/sdk'
|
|
35
|
-
|
|
36
|
-
// ── Story entity ──────────────────────────────────────────────
|
|
37
|
-
export type StoryRecord = EntityRecord & {
|
|
38
|
-
title: string
|
|
39
|
-
content: string
|
|
40
|
-
authorId: string
|
|
41
|
-
status: 'draft' | 'published' | 'archived'
|
|
42
|
-
wordCount: number
|
|
43
|
-
tags: string[]
|
|
44
|
-
coverUrl?: string
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export type StoryCreate = {
|
|
48
|
-
title: string
|
|
49
|
-
content: string
|
|
50
|
-
authorId: string
|
|
51
|
-
status: 'draft' | 'published' | 'archived'
|
|
52
|
-
wordCount: number
|
|
53
|
-
tags?: string[]
|
|
54
|
-
coverUrl?: string
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export type StoryUpdate = Partial<StoryCreate>
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### Binding entities
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
import { createClient, defineEntities, withEntities } from '@howone/sdk'
|
|
64
|
-
|
|
65
|
-
const client = createClient({
|
|
66
|
-
projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
|
|
67
|
-
env: import.meta.env.VITE_HOWONE_ENV,
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
export const entities = defineEntities({
|
|
71
|
-
Story: client.entity<StoryRecord, StoryCreate, StoryUpdate>('Story'),
|
|
72
|
-
Comment: client.entity<CommentRecord, CommentCreate, CommentUpdate>('Comment'),
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
const howone = withEntities(client, entities)
|
|
76
|
-
export default howone
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
## EntityClient API
|
|
82
|
-
|
|
83
|
-
```ts
|
|
84
|
-
type EntityClient<TRecord, TCreate, TUpdate> = {
|
|
85
|
-
name: string
|
|
86
|
-
|
|
87
|
-
// ── Read ──────────────────────────────────────────────────
|
|
88
|
-
list(options?: ListOptions): Promise<TRecord[]>
|
|
89
|
-
query(options?: QueryOptions<TRecord>): Promise<QueryResult<TRecord>>
|
|
90
|
-
query.mine(options?: QueryOptions<TRecord>): Promise<QueryResult<TRecord>>
|
|
91
|
-
get(id: string): Promise<TRecord | null>
|
|
92
|
-
getOrThrow(id: string): Promise<TRecord>
|
|
93
|
-
aggregate<TResult = unknown>(pipeline: unknown[]): Promise<TResult[]>
|
|
94
|
-
|
|
95
|
-
// ── Write ─────────────────────────────────────────────────
|
|
96
|
-
create(data: TCreate): Promise<TRecord>
|
|
97
|
-
update(id: string, data: TUpdate): Promise<TRecord>
|
|
98
|
-
delete(id: string): Promise<DeleteResult>
|
|
99
|
-
bulkCreate(records: TCreate[], options?: BulkCreateOptions): Promise<TRecord[]>
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
type DeleteResult = {
|
|
103
|
-
deleted: boolean
|
|
104
|
-
id: string
|
|
105
|
-
message?: string
|
|
106
|
-
traceId?: string | number
|
|
107
|
-
}
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
## PublicEntityClient API
|
|
111
|
-
|
|
112
|
-
Public entity calls use `/api/entities/public/apps/:appId/...` and do not send auth headers.
|
|
113
|
-
Only use them when the entity manifest explicitly allows `access.public.read`,
|
|
114
|
-
`access.public.create`, or `access.public.update`.
|
|
115
|
-
|
|
116
|
-
```ts
|
|
117
|
-
type PublicEntityClient<TRecord, TPublicCreate, TPublicUpdate> = {
|
|
118
|
-
name: string
|
|
119
|
-
query(options?: QueryOptions<TRecord>): Promise<QueryResult<TRecord>>
|
|
120
|
-
query.scoped(options: QueryOptions<TRecord>): Promise<QueryResult<TRecord>>
|
|
121
|
-
queryScoped(options: QueryOptions<TRecord>): Promise<QueryResult<TRecord>>
|
|
122
|
-
get(id: string, options?: QueryOptions<TRecord>): Promise<TRecord | null>
|
|
123
|
-
getOrThrow(id: string, options?: QueryOptions<TRecord>): Promise<TRecord>
|
|
124
|
-
create(data: TPublicCreate): Promise<TRecord>
|
|
125
|
-
update(id: string, data: TPublicUpdate): Promise<TRecord>
|
|
126
|
-
}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
---
|
|
130
|
-
|
|
131
|
-
## CRUD Examples
|
|
132
|
-
|
|
133
|
-
### Create
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
const story = await howone.entities.Story.create({
|
|
137
|
-
title: 'My First Story',
|
|
138
|
-
content: 'Once upon a time...',
|
|
139
|
-
authorId: user.id,
|
|
140
|
-
status: 'draft',
|
|
141
|
-
wordCount: 4,
|
|
142
|
-
tags: ['fantasy'],
|
|
143
|
-
})
|
|
144
|
-
// story is fully typed as StoryRecord
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
### Read — get by ID
|
|
148
|
-
|
|
149
|
-
```ts
|
|
150
|
-
const story = await howone.entities.Story.get(storyId)
|
|
151
|
-
// Returns StoryRecord | null
|
|
152
|
-
|
|
153
|
-
const story = await howone.entities.Story.getOrThrow(storyId)
|
|
154
|
-
// Returns StoryRecord, throws if not found
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### Update
|
|
158
|
-
|
|
159
|
-
```ts
|
|
160
|
-
const updated = await howone.entities.Story.update(storyId, {
|
|
161
|
-
status: 'published',
|
|
162
|
-
wordCount: 320,
|
|
163
|
-
})
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### Delete
|
|
6
|
+
## Authenticated entities
|
|
167
7
|
|
|
168
8
|
```ts
|
|
169
|
-
|
|
170
|
-
// result.deleted === true on success
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
---
|
|
174
|
-
|
|
175
|
-
## Querying
|
|
176
|
-
|
|
177
|
-
### QueryOptions type
|
|
178
|
-
|
|
179
|
-
```ts
|
|
180
|
-
type QueryOptions<TRecord> = {
|
|
181
|
-
where?: WhereInput<TRecord> // field filters
|
|
182
|
-
search?: string // full-text search
|
|
183
|
-
page?: PageInput // pagination
|
|
184
|
-
orderBy?: OrderByInput<TRecord> // sorting
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
type PageInput = { number?: number; size?: number }
|
|
188
|
-
type OrderByInput<TRecord> = Partial<Record<keyof TRecord | string, 'asc' | 'desc'>>
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
### QueryResult type
|
|
192
|
-
|
|
193
|
-
```ts
|
|
194
|
-
type QueryResult<TRecord> = {
|
|
195
|
-
items: TRecord[]
|
|
196
|
-
page: {
|
|
197
|
-
number: number
|
|
198
|
-
size: number
|
|
199
|
-
total: number
|
|
200
|
-
totalPages: number
|
|
201
|
-
hasNext: boolean
|
|
202
|
-
hasPrev: boolean
|
|
203
|
-
}
|
|
204
|
-
traceId?: string | number
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
### Basic query
|
|
9
|
+
import howone from '@/lib/sdk'
|
|
209
10
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
orderBy: { createdDate: 'desc' },
|
|
11
|
+
const created = await howone.entities.Todo.create({
|
|
12
|
+
title: 'Ship the agent app',
|
|
13
|
+
status: 'pending',
|
|
214
14
|
})
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
// page.total, page.hasNext, etc.
|
|
15
|
+
const item = await howone.entities.Todo.get(created.id)
|
|
16
|
+
const updated = await howone.entities.Todo.update(created.id, { status: 'done' })
|
|
17
|
+
await howone.entities.Todo.delete(created.id)
|
|
219
18
|
```
|
|
220
19
|
|
|
221
|
-
|
|
20
|
+
The generated entity contract validates business fields before writes. System fields and ownership
|
|
21
|
+
fields are not app inputs. The backend derives ownership from the authenticated token.
|
|
222
22
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
search: 'dragon',
|
|
226
|
-
where: { status: 'published' },
|
|
227
|
-
page: { number: 1, size: 10 },
|
|
228
|
-
orderBy: { wordCount: 'desc' },
|
|
229
|
-
})
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
### query.mine — current authenticated user's records
|
|
23
|
+
If the manifest says an operation is `none`, its generated client does not expose that method.
|
|
24
|
+
Treat a missing property as a contract signal; do not cast it to `any` or call raw `/data` routes.
|
|
233
25
|
|
|
234
|
-
|
|
235
|
-
It requires a valid auth token, then lets the backend derive owner from the JWT. Do not
|
|
236
|
-
manually pass `created_by_id`, `created_by_user_id`, `ownerId`, or `puid` in authenticated
|
|
237
|
-
queries or writes.
|
|
26
|
+
## Queries
|
|
238
27
|
|
|
239
28
|
```ts
|
|
240
|
-
const
|
|
29
|
+
const result = await howone.entities.Todo.query({
|
|
30
|
+
where: {
|
|
31
|
+
status: 'pending',
|
|
32
|
+
title: { contains: 'agent' },
|
|
33
|
+
},
|
|
241
34
|
page: { number: 1, size: 20 },
|
|
242
35
|
orderBy: { updatedDate: 'desc' },
|
|
243
36
|
})
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
For public pages that cannot use the current auth session, switch to `howone.public`.
|
|
247
|
-
The public endpoint is controlled by `access.public.allowedFilters` and
|
|
248
|
-
`access.public.requiredScopes`:
|
|
249
37
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
page: { number: 1, size: 20 },
|
|
254
|
-
orderBy: { publishedAt: 'desc' },
|
|
255
|
-
})
|
|
38
|
+
result.items // TodoRecord[]
|
|
39
|
+
result.page.total
|
|
40
|
+
result.page.hasNext
|
|
256
41
|
```
|
|
257
42
|
|
|
258
|
-
|
|
43
|
+
Use the typed `where` object. Supported operators include `eq`, `ne`, `gt`, `gte`, `lt`, `lte`,
|
|
44
|
+
`contains`, `startsWith`, `endsWith`, `in`, `notIn`, `null`, `empty`, and `exists` (subject to the
|
|
45
|
+
backend contract):
|
|
259
46
|
|
|
260
47
|
```ts
|
|
261
|
-
|
|
262
|
-
eq?: T // exact match (same as plain value)
|
|
263
|
-
equals?: T // alias for eq
|
|
264
|
-
ne?: T // not equal
|
|
265
|
-
not?: T // not equal alias
|
|
266
|
-
gt?: T // greater than
|
|
267
|
-
gte?: T // greater than or equal
|
|
268
|
-
lt?: T // less than
|
|
269
|
-
lte?: T // less than or equal
|
|
270
|
-
contains?: string // substring (string fields)
|
|
271
|
-
like?: string // SQL LIKE pattern
|
|
272
|
-
startsWith?: string
|
|
273
|
-
starts?: string
|
|
274
|
-
endsWith?: string
|
|
275
|
-
ends?: string
|
|
276
|
-
in?: T[] // value in array
|
|
277
|
-
notIn?: T[] // value not in array
|
|
278
|
-
null?: boolean // null / not null
|
|
279
|
-
empty?: boolean // empty / not empty
|
|
280
|
-
exists?: boolean // field exists / missing
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Examples
|
|
284
|
-
const result = await howone.entities.Story.query({
|
|
48
|
+
await howone.entities.Todo.query({
|
|
285
49
|
where: {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
title: { contains: 'magic' },
|
|
289
|
-
tags: { in: ['fantasy', 'sci-fi'] },
|
|
50
|
+
priority: { gte: 3, lte: 5 },
|
|
51
|
+
tags: { in: ['ai', 'agent'] },
|
|
290
52
|
},
|
|
291
53
|
})
|
|
292
54
|
```
|
|
293
55
|
|
|
294
|
-
|
|
56
|
+
Generated Record types are strict. Unknown field names in `where` and `orderBy` should fail at
|
|
57
|
+
typecheck. If an app needs an untyped escape hatch, review it explicitly and keep it at a boundary;
|
|
58
|
+
do not widen every generated record.
|
|
295
59
|
|
|
296
|
-
|
|
297
|
-
Entity-specific relation names must come from the manifest `relations` contract.
|
|
60
|
+
### Current user's records
|
|
298
61
|
|
|
299
62
|
```ts
|
|
300
|
-
const
|
|
301
|
-
include: ['user', 'author'],
|
|
63
|
+
const mine = await howone.entities.Generation.query.mine({
|
|
302
64
|
page: { number: 1, size: 20 },
|
|
65
|
+
orderBy: { createdDate: 'desc' },
|
|
303
66
|
})
|
|
304
67
|
```
|
|
305
68
|
|
|
306
|
-
|
|
69
|
+
`query.mine()` verifies that a usable token exists. Never add `created_by_id`, `createdById`,
|
|
70
|
+
`ownerId`, or `puid` to the query. The server owns that scope.
|
|
307
71
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
Use `list()` only for simple reads that don't need pagination metadata.
|
|
72
|
+
### Include and pagination
|
|
311
73
|
|
|
312
74
|
```ts
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
page
|
|
316
|
-
|
|
317
|
-
sort?: string
|
|
318
|
-
order?: 'asc' | 'desc'
|
|
319
|
-
[key: string]: unknown // pass extra filters as top-level keys
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const stories = await howone.entities.Story.list({ limit: 50, sort: 'title' })
|
|
323
|
-
// Returns StoryRecord[] (no pagination metadata)
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
---
|
|
327
|
-
|
|
328
|
-
## Public Reads
|
|
329
|
-
|
|
330
|
-
Use public reads for public lists and scoped public pages.
|
|
331
|
-
|
|
332
|
-
```ts
|
|
333
|
-
const articles = await howone.public.entities.Article.query({
|
|
334
|
-
published: true,
|
|
335
|
-
category: 'ai',
|
|
336
|
-
page: { number: 1, size: 20 },
|
|
337
|
-
orderBy: { publishedAt: 'desc' },
|
|
75
|
+
await howone.entities.Project.query({
|
|
76
|
+
include: ['owner', 'latestGeneration'],
|
|
77
|
+
page: { number: 2, size: 25 },
|
|
78
|
+
exactCount: true,
|
|
338
79
|
})
|
|
339
80
|
```
|
|
340
81
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
```ts
|
|
344
|
-
const profile = await howone.public.entities.QrProfile.queryScoped({
|
|
345
|
-
ownerId: ownerSharedUserId,
|
|
346
|
-
slug: 'wechat',
|
|
347
|
-
active: true,
|
|
348
|
-
limit: 1,
|
|
349
|
-
})
|
|
350
|
-
```
|
|
82
|
+
Relation names must come from the manifest. Use `query()` when pagination metadata matters and
|
|
83
|
+
`list()` only for a simple array read where the API contract permits it.
|
|
351
84
|
|
|
352
|
-
Public
|
|
353
|
-
fields must be present in `access.public.allowedSorts`. If a public query is rejected,
|
|
354
|
-
fix the schema access contract instead of falling back to authenticated APIs.
|
|
85
|
+
## Public entities
|
|
355
86
|
|
|
356
|
-
|
|
357
|
-
|
|
87
|
+
Public bindings are generated separately and are guarded against manifest filters, sorts, limits,
|
|
88
|
+
and scopes:
|
|
358
89
|
|
|
359
90
|
```ts
|
|
360
|
-
|
|
361
|
-
import { articleEntityDefinition } from '@/lib/sdk'
|
|
362
|
-
|
|
363
|
-
const query = {
|
|
91
|
+
const articles = await howone.public.entities.Article.query({
|
|
364
92
|
where: { status: 'published' },
|
|
365
93
|
orderBy: { publishedAt: 'desc' },
|
|
366
94
|
page: { number: 1, size: 20 },
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
assertPublicEntityQuery(articleEntityDefinition, query)
|
|
370
|
-
const result = await howone.public.entities.Article.query(query)
|
|
95
|
+
})
|
|
371
96
|
```
|
|
372
97
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
---
|
|
377
|
-
|
|
378
|
-
## Public Writes
|
|
379
|
-
|
|
380
|
-
Only generate public writes when `access.public.create` / `access.public.update` allows
|
|
381
|
-
them. Public create must include `created_by_user_id` unless the schema defines a
|
|
382
|
-
different required owner scope.
|
|
98
|
+
For `access.public.read: 'scoped'`, use the generated scoped method and provide every required
|
|
99
|
+
scope under `where`:
|
|
383
100
|
|
|
384
101
|
```ts
|
|
385
|
-
await howone.public.entities.
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
email: 'ada@example.com',
|
|
389
|
-
message: 'Please contact me',
|
|
102
|
+
const profile = await howone.public.entities.Profile.query.scoped({
|
|
103
|
+
where: { shareId, active: true },
|
|
104
|
+
page: { number: 1, size: 1 },
|
|
390
105
|
})
|
|
391
106
|
```
|
|
392
107
|
|
|
393
|
-
|
|
108
|
+
`queryScoped(options)` is also available. Public create/update methods exist only when the manifest
|
|
109
|
+
allows them. Public queries are never a way to bypass authenticated access.
|
|
394
110
|
|
|
395
|
-
## Payload
|
|
111
|
+
## Payload utilities
|
|
396
112
|
|
|
397
|
-
Use these
|
|
398
|
-
They prevent the common mistake of sending UI-only, workflow-envelope, ownership, or system fields.
|
|
113
|
+
Use these when mapping broad UI state or AI output to an entity payload:
|
|
399
114
|
|
|
400
115
|
```ts
|
|
401
|
-
import {
|
|
116
|
+
import { assertEntityPayload, pickEntityPayload } from '@howone/sdk'
|
|
402
117
|
import { generationEntityDefinition } from '@/lib/sdk'
|
|
403
118
|
|
|
404
|
-
const
|
|
119
|
+
const payload = pickEntityPayload(generationEntityDefinition, {
|
|
405
120
|
prompt,
|
|
406
121
|
status: 'pending',
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const payload = pickEntityPayload(generationEntityDefinition, draft)
|
|
122
|
+
gradientDirection: direction, // removed unless declared in the manifest
|
|
123
|
+
})
|
|
412
124
|
assertEntityPayload(generationEntityDefinition, payload)
|
|
413
|
-
|
|
414
125
|
await howone.entities.Generation.create(payload)
|
|
415
126
|
```
|
|
416
127
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
- Use `pickEntityPayload()` when transforming broad UI objects into narrow create/update payloads.
|
|
420
|
-
- Use `validateEntityPayload()` to collect issues for app-owned validation UI.
|
|
421
|
-
- Use `assertEntityPayload()` before writes in generated helper functions.
|
|
422
|
-
- For updates, pass `{ partial: true }` to avoid requiring create-time fields.
|
|
423
|
-
- These helpers do not replace backend validation; they make generated frontend code fail earlier
|
|
424
|
-
with clearer errors.
|
|
425
|
-
|
|
426
|
-
```ts
|
|
427
|
-
assertEntityPayload(generationEntityDefinition, update, { partial: true })
|
|
428
|
-
await howone.entities.Generation.update(id, update)
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
---
|
|
128
|
+
For updates, use `assertEntityPayload(definition, payload, { partial: true })`. Use
|
|
129
|
+
`validateEntityPayload` when the UI needs structured field errors instead of an exception.
|
|
432
130
|
|
|
433
|
-
## Bulk
|
|
131
|
+
## Bulk and aggregation
|
|
434
132
|
|
|
435
133
|
```ts
|
|
436
|
-
|
|
437
|
-
[
|
|
438
|
-
{ title: 'Sample 1', content: 'Content 1', authorId: 'sys', status: 'published', wordCount: 10 },
|
|
439
|
-
{ title: 'Sample 2', content: 'Content 2', authorId: 'sys', status: 'published', wordCount: 15 },
|
|
440
|
-
],
|
|
441
|
-
{ sample: true }, // mark as sample data
|
|
442
|
-
)
|
|
443
|
-
```
|
|
134
|
+
await howone.entities.Sample.bulkCreate(records, { sample: true })
|
|
444
135
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
```ts
|
|
450
|
-
// MongoDB-style aggregation pipeline
|
|
451
|
-
const stats = await howone.entities.Story.aggregate<{ _id: string; count: number }>([
|
|
452
|
-
{ $match: { status: 'published' } },
|
|
453
|
-
{ $group: { _id: '$authorId', count: { $sum: 1 } } },
|
|
454
|
-
{ $sort: { count: -1 } },
|
|
455
|
-
{ $limit: 10 },
|
|
136
|
+
const counts = await howone.entities.Generation.aggregate<{ _id: string; count: number }>([
|
|
137
|
+
{ $match: { status: 'completed' } },
|
|
138
|
+
{ $group: { _id: '$model', count: { $sum: 1 } } },
|
|
456
139
|
])
|
|
457
140
|
```
|
|
458
141
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
## React Patterns
|
|
142
|
+
These methods exist only when the generated access contract permits their underlying capability.
|
|
462
143
|
|
|
463
|
-
|
|
144
|
+
## Response contract
|
|
464
145
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
import { useEffect, useState } from 'react'
|
|
469
|
-
import howone, { type StoryRecord } from '@/lib/sdk'
|
|
470
|
-
|
|
471
|
-
function StoryList() {
|
|
472
|
-
const [stories, setStories] = useState<StoryRecord[]>([])
|
|
473
|
-
const [loading, setLoading] = useState(true)
|
|
474
|
-
const [error, setError] = useState<Error | null>(null)
|
|
475
|
-
|
|
476
|
-
useEffect(() => {
|
|
477
|
-
let cancelled = false
|
|
478
|
-
howone.entities.Story.query({ page: { number: 1, size: 20 } })
|
|
479
|
-
.then(result => { if (!cancelled) setStories(result.items) })
|
|
480
|
-
.catch(err => { if (!cancelled) setError(err) })
|
|
481
|
-
.finally(() => { if (!cancelled) setLoading(false) })
|
|
482
|
-
return () => { cancelled = true }
|
|
483
|
-
}, [])
|
|
484
|
-
|
|
485
|
-
if (loading) return <div>Loading...</div>
|
|
486
|
-
if (error) return <div>Error: {error.message}</div>
|
|
487
|
-
return (
|
|
488
|
-
<ul>
|
|
489
|
-
{stories.map(s => <li key={s.id}>{s.title}</li>)}
|
|
490
|
-
</ul>
|
|
491
|
-
)
|
|
492
|
-
}
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
### TanStack Query pattern
|
|
496
|
-
|
|
497
|
-
```tsx
|
|
498
|
-
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
499
|
-
import howone, { type StoryCreate } from '@/lib/sdk'
|
|
500
|
-
|
|
501
|
-
function useStories(page = 1) {
|
|
502
|
-
return useQuery({
|
|
503
|
-
queryKey: ['stories', page],
|
|
504
|
-
queryFn: () => howone.entities.Story.query({
|
|
505
|
-
page: { number: page, size: 20 },
|
|
506
|
-
orderBy: { createdDate: 'desc' },
|
|
507
|
-
}),
|
|
508
|
-
})
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
function useCreateStory() {
|
|
512
|
-
const queryClient = useQueryClient()
|
|
513
|
-
return useMutation({
|
|
514
|
-
mutationFn: (data: StoryCreate) => howone.entities.Story.create(data),
|
|
515
|
-
onSuccess: () => {
|
|
516
|
-
queryClient.invalidateQueries({ queryKey: ['stories'] })
|
|
517
|
-
},
|
|
518
|
-
})
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
function useDeleteStory() {
|
|
522
|
-
const queryClient = useQueryClient()
|
|
523
|
-
return useMutation({
|
|
524
|
-
mutationFn: (id: string) => howone.entities.Story.delete(id),
|
|
525
|
-
onSuccess: () => {
|
|
526
|
-
queryClient.invalidateQueries({ queryKey: ['stories'] })
|
|
527
|
-
},
|
|
528
|
-
})
|
|
529
|
-
}
|
|
530
|
-
```
|
|
146
|
+
The SDK accepts known envelope variants but rejects malformed list/query/single responses with
|
|
147
|
+
`HowOneProtocolError`. It no longer turns an unexpected response into an empty array, because that
|
|
148
|
+
would make a backend regression look like “no data.”
|
|
531
149
|
|
|
532
|
-
|
|
150
|
+
By default, only top-level response keys are converted between snake_case and camelCase. Nested
|
|
151
|
+
business JSON is preserved. Use `caseDepth: 'deep'` only when the entire payload is a controlled
|
|
152
|
+
transport object.
|
|
533
153
|
|
|
534
|
-
## Common
|
|
154
|
+
## Common mistakes
|
|
535
155
|
|
|
536
|
-
| Mistake |
|
|
156
|
+
| Mistake | Fix |
|
|
537
157
|
|---|---|
|
|
538
|
-
|
|
|
539
|
-
|
|
|
540
|
-
|
|
|
541
|
-
|
|
|
542
|
-
|
|
|
543
|
-
|
|
|
158
|
+
| Manual owner field | Remove it; auth/scope derives ownership. |
|
|
159
|
+
| Public query with top-level filter keys | Use `where: { ... }`. |
|
|
160
|
+
| Any unknown filter/sort | Read manifest allowed fields and use generated types. |
|
|
161
|
+
| Empty array after a backend shape regression | Inspect `HowOneProtocolError`; fix the contract/server. |
|
|
162
|
+
| Nested settings keys renamed unexpectedly | Keep default `caseDepth: 'top-level'`. |
|
|
163
|
+
| Entity CRUD via `raw` | Use generated entity bindings. |
|