howone 0.2.3 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (23) hide show
  1. package/package.json +1 -1
  2. package/templates/vite/.howone/skills/howone/01-architect/01-app-generation.md +8 -7
  3. package/templates/vite/.howone/skills/howone/01-architect/02-manifest-codegen.md +121 -436
  4. package/templates/vite/.howone/skills/howone/03-ai-capabilities/04-workflow-operations.md +13 -4
  5. package/templates/vite/.howone/skills/howone/04-app-sdk/01-client-setup.md +94 -261
  6. package/templates/vite/.howone/skills/howone/04-app-sdk/02-entity-operations.md +85 -465
  7. package/templates/vite/.howone/skills/howone/04-app-sdk/03-auth.md +11 -7
  8. package/templates/vite/.howone/skills/howone/04-app-sdk/04-react-integration.md +84 -137
  9. package/templates/vite/.howone/skills/howone/04-app-sdk/05-file-upload.md +66 -273
  10. package/templates/vite/.howone/skills/howone/04-app-sdk/06-raw-http.md +72 -249
  11. package/templates/vite/.howone/skills/howone/04-app-sdk/07-ai-action-calls.md +135 -499
  12. package/templates/vite/.howone/skills/howone/04-app-sdk/08-ai-manifest-handoff.md +49 -196
  13. package/templates/vite/.howone/skills/howone/04-app-sdk/09-extension-boundaries.md +4 -4
  14. package/templates/vite/.howone/skills/howone/04-app-sdk/10-workflow-execute-sse.md +94 -61
  15. package/templates/vite/.howone/skills/howone/04-app-sdk/11-entity-data-access-patterns.md +4 -3
  16. package/templates/vite/.howone/skills/howone/SKILL.md +48 -8
  17. package/templates/vite/.howone/skills/howone/references/common-errors.md +27 -0
  18. package/templates/vite/.howone/skills/howone/references/version-evidence.md +47 -0
  19. package/templates/vite/.howone/skills/howone/scripts/verify-project.mjs +151 -0
  20. package/templates/vite/package.json +1 -1
  21. package/templates/vite/src/App.tsx +9 -5
  22. package/templates/vite/src/lib/sdk.ts +7 -5
  23. package/templates/vite/bun.lock +0 -1478
@@ -1,543 +1,163 @@
1
1
  # Entity Operations
2
2
 
3
- ## Core Concepts
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
- - `client.entity<TRecord, TCreate, TUpdate>(entityName)` creates a typed entity client.
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
- const result = await howone.entities.Story.delete(storyId)
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
- ```ts
211
- const result = await howone.entities.Story.query({
212
- page: { number: 1, size: 20 },
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 { items, page } = result
217
- // items: StoryRecord[]
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
- ### Search + filter
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
- ```ts
224
- const result = await howone.entities.Story.query({
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
- `query.mine()` is the preferred way to fetch records owned by the authenticated user.
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 myStories = await howone.entities.Story.query.mine({
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
- ```ts
251
- const result = await howone.public.entities.Story.query({
252
- published: true,
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
- ### WhereInput field operators
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
- type FieldOperator<T> = {
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
- status: 'published', // plain value = eq
287
- wordCount: { gte: 100, lte: 5000 },
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
- ### Include relations
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
- `include` accepts a string or string array. All entities support `include: 'user'`.
297
- Entity-specific relation names must come from the manifest `relations` contract.
60
+ ### Current user's records
298
61
 
299
62
  ```ts
300
- const result = await howone.entities.Story.query({
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
- ## list() Simple Array Read
309
-
310
- Use `list()` only for simple reads that don't need pagination metadata.
72
+ ### Include and pagination
311
73
 
312
74
  ```ts
313
- // ListOptions type
314
- type ListOptions = {
315
- page?: number
316
- limit?: number
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
- For `access.public.read = "scoped"`, pass every required scope:
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 query fields must be present in `access.public.allowedFilters`, and public sort
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
- When generated code has the synced entity definition available, validate public queries before
357
- calling the API:
87
+ Public bindings are generated separately and are guarded against manifest filters, sorts, limits,
88
+ and scopes:
358
89
 
359
90
  ```ts
360
- import { assertPublicEntityQuery } from '@howone/sdk'
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
- Use `validatePublicEntityQuery()` when the app wants to show its own validation UI instead of
374
- throwing.
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.ContactMessage.create({
386
- created_by_user_id: projectUserId,
387
- name: 'Ada',
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 Contract Utilities
111
+ ## Payload utilities
396
112
 
397
- Use these helpers when code maps form state, AI output, or mixed UI state into entity writes.
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 { pickEntityPayload, assertEntityPayload } from '@howone/sdk'
116
+ import { assertEntityPayload, pickEntityPayload } from '@howone/sdk'
402
117
  import { generationEntityDefinition } from '@/lib/sdk'
403
118
 
404
- const draft = {
119
+ const payload = pickEntityPayload(generationEntityDefinition, {
405
120
  prompt,
406
121
  status: 'pending',
407
- created_by_id: user.id, // stripped/rejected
408
- gradientDirection: 'to right', // stripped/rejected unless schema declares it
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
- Rules:
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 Create
131
+ ## Bulk and aggregation
434
132
 
435
133
  ```ts
436
- const sampleStories = await howone.entities.Story.bulkCreate(
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
- ## Aggregation
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
- React integration provides no hooks — use `useEffect` + `useState` or a library like TanStack Query.
144
+ ## Response contract
464
145
 
465
- ### Simple useEffect pattern
466
-
467
- ```tsx
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 Mistakes
154
+ ## Common mistakes
535
155
 
536
- | Mistake | Correct Pattern |
156
+ | Mistake | Fix |
537
157
  |---|---|
538
- | `type StoryCreate = Omit<StoryRecord, 'id' \| 'createdDate'>` | Define `StoryCreate` explicitly with exact fields |
539
- | `client.entity('Story')` without generics | `client.entity<StoryRecord, StoryCreate, StoryUpdate>('Story')` |
540
- | Using `list()` when you need pagination | Use `query()` for paginated UIs |
541
- | Calling `query()` inside render without guarding re-runs | Wrap in `useEffect` with cancellation or use TanStack Query |
542
- | Sending form/workflow object directly to `create()` | Use `pickEntityPayload()` and `assertEntityPayload()` |
543
- | Public query with illegal field/sort | Use `assertPublicEntityQuery()` and fix schema guardrails |
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. |