howone 0.1.11 → 0.1.13
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-sdk/SKILL.md +53 -63
- package/templates/vite/.howone/skills/howone-sdk/references/01-client-setup.md +278 -0
- package/templates/vite/.howone/skills/howone-sdk/references/02-entity-operations.md +379 -0
- package/templates/vite/.howone/skills/howone-sdk/references/03-ai-actions.md +489 -0
- package/templates/vite/.howone/skills/howone-sdk/references/04-auth.md +484 -0
- package/templates/vite/.howone/skills/howone-sdk/references/05-file-upload.md +319 -0
- package/templates/vite/.howone/skills/howone-sdk/references/06-react-integration.md +394 -0
- package/templates/vite/.howone/skills/howone-sdk/references/07-raw-http.md +299 -0
- package/templates/vite/.howone/skills/howone-sdk/references/08-manifest-codegen.md +400 -0
- package/templates/vite/package.json +1 -1
- package/templates/vite/.howone/skills/howone-sdk/references/usage-patterns.md +0 -215
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# Entity Operations
|
|
2
|
+
|
|
3
|
+
## Core Concepts
|
|
4
|
+
|
|
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
|
|
22
|
+
isSample?: boolean
|
|
23
|
+
[key: string]: unknown // index signature — important for typing
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Defining entity types
|
|
28
|
+
|
|
29
|
+
Always define all three types explicitly. **Do not** use `Omit<EntityRecord & ...>` for create types — the index signature widens the payload.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { type EntityRecord } from '@howone/sdk'
|
|
33
|
+
|
|
34
|
+
// ── Story entity ──────────────────────────────────────────────
|
|
35
|
+
export type StoryRecord = EntityRecord & {
|
|
36
|
+
title: string
|
|
37
|
+
content: string
|
|
38
|
+
authorId: string
|
|
39
|
+
status: 'draft' | 'published' | 'archived'
|
|
40
|
+
wordCount: number
|
|
41
|
+
tags: string[]
|
|
42
|
+
coverUrl?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type StoryCreate = {
|
|
46
|
+
title: string
|
|
47
|
+
content: string
|
|
48
|
+
authorId: string
|
|
49
|
+
status: 'draft' | 'published' | 'archived'
|
|
50
|
+
wordCount: number
|
|
51
|
+
tags?: string[]
|
|
52
|
+
coverUrl?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type StoryUpdate = Partial<StoryCreate>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Binding entities
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { createClient, defineEntities, withEntities } from '@howone/sdk'
|
|
62
|
+
|
|
63
|
+
const client = createClient({
|
|
64
|
+
projectId: import.meta.env.VITE_HOWONE_PROJECT_ID,
|
|
65
|
+
env: import.meta.env.VITE_HOWONE_ENV,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
export const entities = defineEntities({
|
|
69
|
+
Story: client.entity<StoryRecord, StoryCreate, StoryUpdate>('Story'),
|
|
70
|
+
Comment: client.entity<CommentRecord, CommentCreate, CommentUpdate>('Comment'),
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const howone = withEntities(client, entities)
|
|
74
|
+
export default howone
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## EntityClient API
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
type EntityClient<TRecord, TCreate, TUpdate> = {
|
|
83
|
+
name: string
|
|
84
|
+
|
|
85
|
+
// ── Read ──────────────────────────────────────────────────
|
|
86
|
+
list(options?: ListOptions): Promise<TRecord[]>
|
|
87
|
+
query(options?: QueryOptions<TRecord>): Promise<QueryResult<TRecord>>
|
|
88
|
+
query.mine(options?: QueryOptions<TRecord>): Promise<QueryResult<TRecord>>
|
|
89
|
+
get(id: string): Promise<TRecord | null>
|
|
90
|
+
getOrThrow(id: string): Promise<TRecord>
|
|
91
|
+
aggregate<TResult = unknown>(pipeline: unknown[]): Promise<TResult[]>
|
|
92
|
+
|
|
93
|
+
// ── Write ─────────────────────────────────────────────────
|
|
94
|
+
create(data: TCreate): Promise<TRecord>
|
|
95
|
+
update(id: string, data: TUpdate): Promise<TRecord>
|
|
96
|
+
delete(id: string): Promise<DeleteResult>
|
|
97
|
+
bulkCreate(records: TCreate[], options?: BulkCreateOptions): Promise<TRecord[]>
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type DeleteResult = {
|
|
101
|
+
deleted: boolean
|
|
102
|
+
id: string
|
|
103
|
+
message?: string
|
|
104
|
+
traceId?: string | number
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## CRUD Examples
|
|
111
|
+
|
|
112
|
+
### Create
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
const story = await howone.entities.Story.create({
|
|
116
|
+
title: 'My First Story',
|
|
117
|
+
content: 'Once upon a time...',
|
|
118
|
+
authorId: user.id,
|
|
119
|
+
status: 'draft',
|
|
120
|
+
wordCount: 4,
|
|
121
|
+
tags: ['fantasy'],
|
|
122
|
+
})
|
|
123
|
+
// story is fully typed as StoryRecord
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Read — get by ID
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
const story = await howone.entities.Story.get(storyId)
|
|
130
|
+
// Returns StoryRecord | null
|
|
131
|
+
|
|
132
|
+
const story = await howone.entities.Story.getOrThrow(storyId)
|
|
133
|
+
// Returns StoryRecord, throws if not found
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Update
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const updated = await howone.entities.Story.update(storyId, {
|
|
140
|
+
status: 'published',
|
|
141
|
+
wordCount: 320,
|
|
142
|
+
})
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Delete
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const result = await howone.entities.Story.delete(storyId)
|
|
149
|
+
// result.deleted === true on success
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Querying
|
|
155
|
+
|
|
156
|
+
### QueryOptions type
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
type QueryOptions<TRecord> = {
|
|
160
|
+
where?: WhereInput<TRecord> // field filters
|
|
161
|
+
search?: string // full-text search
|
|
162
|
+
page?: PageInput // pagination
|
|
163
|
+
orderBy?: OrderByInput<TRecord> // sorting
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
type PageInput = { number?: number; size?: number }
|
|
167
|
+
type OrderByInput<TRecord> = Partial<Record<keyof TRecord | string, 'asc' | 'desc'>>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### QueryResult type
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
type QueryResult<TRecord> = {
|
|
174
|
+
items: TRecord[]
|
|
175
|
+
page: {
|
|
176
|
+
number: number
|
|
177
|
+
size: number
|
|
178
|
+
total: number
|
|
179
|
+
totalPages: number
|
|
180
|
+
hasNext: boolean
|
|
181
|
+
hasPrev: boolean
|
|
182
|
+
}
|
|
183
|
+
traceId?: string | number
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Basic query
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const result = await howone.entities.Story.query({
|
|
191
|
+
page: { number: 1, size: 20 },
|
|
192
|
+
orderBy: { createdDate: 'desc' },
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const { items, page } = result
|
|
196
|
+
// items: StoryRecord[]
|
|
197
|
+
// page.total, page.hasNext, etc.
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Search + filter
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const result = await howone.entities.Story.query({
|
|
204
|
+
search: 'dragon',
|
|
205
|
+
where: { status: 'published' },
|
|
206
|
+
page: { number: 1, size: 10 },
|
|
207
|
+
orderBy: { wordCount: 'desc' },
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### query.mine — only current user's records
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const myStories = await howone.entities.Story.query.mine({
|
|
215
|
+
page: { number: 1, size: 20 },
|
|
216
|
+
orderBy: { updatedDate: 'desc' },
|
|
217
|
+
})
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### WhereInput — field operators
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
type FieldOperator<T> = {
|
|
224
|
+
eq?: T // exact match (same as plain value)
|
|
225
|
+
ne?: T // not equal
|
|
226
|
+
gt?: T // greater than
|
|
227
|
+
gte?: T // greater than or equal
|
|
228
|
+
lt?: T // less than
|
|
229
|
+
lte?: T // less than or equal
|
|
230
|
+
contains?: string // substring (string fields)
|
|
231
|
+
like?: string // SQL LIKE pattern
|
|
232
|
+
startsWith?: string
|
|
233
|
+
endsWith?: string
|
|
234
|
+
in?: T[] // value in array
|
|
235
|
+
notIn?: T[] // value not in array
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Examples
|
|
239
|
+
const result = await howone.entities.Story.query({
|
|
240
|
+
where: {
|
|
241
|
+
status: 'published', // plain value = eq
|
|
242
|
+
wordCount: { gte: 100, lte: 5000 },
|
|
243
|
+
title: { contains: 'magic' },
|
|
244
|
+
tags: { in: ['fantasy', 'sci-fi'] },
|
|
245
|
+
},
|
|
246
|
+
})
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## list() — Simple Array Read
|
|
252
|
+
|
|
253
|
+
Use `list()` only for simple reads that don't need pagination metadata.
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
// ListOptions type
|
|
257
|
+
type ListOptions = {
|
|
258
|
+
page?: number
|
|
259
|
+
limit?: number
|
|
260
|
+
sort?: string
|
|
261
|
+
order?: 'asc' | 'desc'
|
|
262
|
+
[key: string]: unknown // pass extra filters as top-level keys
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const stories = await howone.entities.Story.list({ limit: 50, sort: 'title' })
|
|
266
|
+
// Returns StoryRecord[] (no pagination metadata)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Bulk Create
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
const sampleStories = await howone.entities.Story.bulkCreate(
|
|
275
|
+
[
|
|
276
|
+
{ title: 'Sample 1', content: 'Content 1', authorId: 'sys', status: 'published', wordCount: 10 },
|
|
277
|
+
{ title: 'Sample 2', content: 'Content 2', authorId: 'sys', status: 'published', wordCount: 15 },
|
|
278
|
+
],
|
|
279
|
+
{ sample: true }, // mark as sample data
|
|
280
|
+
)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Aggregation
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
// MongoDB-style aggregation pipeline
|
|
289
|
+
const stats = await howone.entities.Story.aggregate<{ _id: string; count: number }>([
|
|
290
|
+
{ $match: { status: 'published' } },
|
|
291
|
+
{ $group: { _id: '$authorId', count: { $sum: 1 } } },
|
|
292
|
+
{ $sort: { count: -1 } },
|
|
293
|
+
{ $limit: 10 },
|
|
294
|
+
])
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## React Patterns
|
|
300
|
+
|
|
301
|
+
React integration provides no hooks — use `useEffect` + `useState` or a library like TanStack Query.
|
|
302
|
+
|
|
303
|
+
### Simple useEffect pattern
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
import { useEffect, useState } from 'react'
|
|
307
|
+
import howone, { type StoryRecord } from '@/lib/sdk'
|
|
308
|
+
|
|
309
|
+
function StoryList() {
|
|
310
|
+
const [stories, setStories] = useState<StoryRecord[]>([])
|
|
311
|
+
const [loading, setLoading] = useState(true)
|
|
312
|
+
const [error, setError] = useState<Error | null>(null)
|
|
313
|
+
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
let cancelled = false
|
|
316
|
+
howone.entities.Story.query({ page: { number: 1, size: 20 } })
|
|
317
|
+
.then(result => { if (!cancelled) setStories(result.items) })
|
|
318
|
+
.catch(err => { if (!cancelled) setError(err) })
|
|
319
|
+
.finally(() => { if (!cancelled) setLoading(false) })
|
|
320
|
+
return () => { cancelled = true }
|
|
321
|
+
}, [])
|
|
322
|
+
|
|
323
|
+
if (loading) return <div>Loading...</div>
|
|
324
|
+
if (error) return <div>Error: {error.message}</div>
|
|
325
|
+
return (
|
|
326
|
+
<ul>
|
|
327
|
+
{stories.map(s => <li key={s.id}>{s.title}</li>)}
|
|
328
|
+
</ul>
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### TanStack Query pattern
|
|
334
|
+
|
|
335
|
+
```tsx
|
|
336
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
|
337
|
+
import howone, { type StoryCreate } from '@/lib/sdk'
|
|
338
|
+
|
|
339
|
+
function useStories(page = 1) {
|
|
340
|
+
return useQuery({
|
|
341
|
+
queryKey: ['stories', page],
|
|
342
|
+
queryFn: () => howone.entities.Story.query({
|
|
343
|
+
page: { number: page, size: 20 },
|
|
344
|
+
orderBy: { createdDate: 'desc' },
|
|
345
|
+
}),
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function useCreateStory() {
|
|
350
|
+
const queryClient = useQueryClient()
|
|
351
|
+
return useMutation({
|
|
352
|
+
mutationFn: (data: StoryCreate) => howone.entities.Story.create(data),
|
|
353
|
+
onSuccess: () => {
|
|
354
|
+
queryClient.invalidateQueries({ queryKey: ['stories'] })
|
|
355
|
+
},
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function useDeleteStory() {
|
|
360
|
+
const queryClient = useQueryClient()
|
|
361
|
+
return useMutation({
|
|
362
|
+
mutationFn: (id: string) => howone.entities.Story.delete(id),
|
|
363
|
+
onSuccess: () => {
|
|
364
|
+
queryClient.invalidateQueries({ queryKey: ['stories'] })
|
|
365
|
+
},
|
|
366
|
+
})
|
|
367
|
+
}
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Common Mistakes
|
|
373
|
+
|
|
374
|
+
| Mistake | Correct Pattern |
|
|
375
|
+
|---|---|
|
|
376
|
+
| `type StoryCreate = Omit<StoryRecord, 'id' \| 'createdDate'>` | Define `StoryCreate` explicitly with exact fields |
|
|
377
|
+
| `client.entity('Story')` without generics | `client.entity<StoryRecord, StoryCreate, StoryUpdate>('Story')` |
|
|
378
|
+
| Using `list()` when you need pagination | Use `query()` for paginated UIs |
|
|
379
|
+
| Calling `query()` inside render without guarding re-runs | Wrap in `useEffect` with cancellation or use TanStack Query |
|