howone 0.1.10 → 0.1.12

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.
@@ -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 |