@take-out/docs 0.2.2 → 0.2.3

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 (2) hide show
  1. package/package.json +1 -1
  2. package/zero.md +0 -384
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@take-out/docs",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Documentation files for Takeout starter kit",
5
5
  "type": "module",
6
6
  "files": [
package/zero.md DELETED
@@ -1,384 +0,0 @@
1
- ---
2
- name: takeout-zero
3
- description: Zero data layer guide. useQuery, zql, mutations, CRUD, permissions, serverWhere, exists(), relations, .related(), pagination, cursor, convergence, optimistic updates.
4
- ---
5
-
6
- ## queries
7
-
8
- queries are plain exported functions in `src/data/queries/` that use the global
9
- `zql` builder:
10
-
11
- ```ts
12
- // src/data/queries/post.ts
13
- import { serverWhere, zql } from 'on-zero'
14
-
15
- const permission = serverWhere('post', () => true)
16
-
17
- export const allPosts = (props: { limit?: number }) => {
18
- return zql.post
19
- .where(permission)
20
- .orderBy('createdAt', 'desc')
21
- .limit(props.limit || 20)
22
- }
23
- ```
24
-
25
- use with `useQuery`:
26
-
27
- ```tsx
28
- import { useQuery } from '~/zero/client'
29
- import { allPosts } from '~/data/queries/post'
30
-
31
- const [posts, status] = useQuery(allPosts, { limit: 20 })
32
- ```
33
-
34
- ### useQuery patterns
35
-
36
- ```tsx
37
- // with params
38
- useQuery(queryFn, { param1, param2 })
39
-
40
- // with params + options
41
- useQuery(queryFn, { param1, param2 }, { enabled: true })
42
-
43
- // conditional - only runs when enabled is true
44
- const [data] = useQuery(userById, { userId }, { enabled: Boolean(userId) })
45
- ```
46
-
47
- ## permissions
48
-
49
- permissions only execute server-side. use `serverWhere` for query permissions:
50
-
51
- ```ts
52
- const permission = serverWhere('post', (q, auth) => {
53
- if (auth?.role === 'admin') return true
54
- return q.cmp('userId', auth?.id || '')
55
- })
56
- ```
57
-
58
- for reusable permissions, extract to `src/data/where/`:
59
-
60
- ```ts
61
- // src/data/where/notBlockedByViewer.ts
62
- export const notBlockedByViewer = serverWhere('post', (q, auth) => {
63
- if (!auth?.id) return true
64
- return q.not(q.exists('authorBlockedBy', (block) =>
65
- block.where('blockerId', auth.id)
66
- ))
67
- })
68
- ```
69
-
70
- `exists()` requires a relationship. add to `src/data/relationships.ts`:
71
-
72
- ```ts
73
- // post -> block via post.userId = block.blockedId
74
- authorBlockedBy: many({
75
- sourceField: ['userId'],
76
- destSchema: tables.block,
77
- destField: ['blockedId'],
78
- })
79
- ```
80
-
81
- this filters posts where author is blocked, without exposing block data to client.
82
-
83
- ## relations
84
-
85
- include related data with `.related()`:
86
-
87
- ```ts
88
- export const postWithComments = (props: { postId: string }) => {
89
- return zql.post
90
- .where('id', props.postId)
91
- .one()
92
- .related('user', (q) => q.one())
93
- .related('comments', (q) =>
94
- q.orderBy('createdAt', 'desc')
95
- .limit(50)
96
- .related('user', (u) => u.one())
97
- )
98
- }
99
- ```
100
-
101
- define relationships in `src/data/relationships.ts`:
102
-
103
- ```ts
104
- export const postRelationships = relationships(tables.post, ({ one, many }) => ({
105
- user: one({
106
- sourceField: ['userId'],
107
- destSchema: tables.userPublic,
108
- destField: ['id'],
109
- }),
110
- comments: many({
111
- sourceField: ['id'],
112
- destSchema: tables.comment,
113
- destField: ['postId'],
114
- }),
115
- }))
116
- ```
117
-
118
- ## pagination
119
-
120
- use `.start()` for cursor-based pagination:
121
-
122
- ```ts
123
- export const postsPaginated = (props: {
124
- pageSize: number
125
- cursor?: { id: string; createdAt: number } | null
126
- }) => {
127
- let query = zql.post
128
- .orderBy('createdAt', 'desc')
129
- .orderBy('id', 'desc')
130
- .limit(props.pageSize)
131
-
132
- if (props.cursor) {
133
- query = query.start(props.cursor)
134
- }
135
-
136
- return query
137
- }
138
- ```
139
-
140
- ## models
141
-
142
- models in `src/data/models/` define schema, permissions, and mutations:
143
-
144
- ```ts
145
- // src/data/models/post.ts
146
- import { boolean, number, string, table } from '@rocicorp/zero'
147
- import { mutations, serverWhere } from 'on-zero'
148
-
149
- export const schema = table('post')
150
- .columns({
151
- id: string(),
152
- userId: string(),
153
- caption: string().optional(),
154
- createdAt: number(),
155
- })
156
- .primaryKey('id')
157
-
158
- const permissions = serverWhere('post', (q, auth) => {
159
- return q.cmp('userId', auth?.id || '')
160
- })
161
-
162
- export const mutate = mutations(schema, permissions, {
163
- insert: async (ctx, post: Post) => {
164
- await ctx.tx.mutate.post.insert(post)
165
-
166
- if (ctx.server) {
167
- ctx.server.asyncTasks.push(() =>
168
- ctx.server.actions.analyticsActions().logEvent(ctx.authData.id, 'post_created')
169
- )
170
- }
171
- },
172
- })
173
- ```
174
-
175
- passing `schema` and `permissions` to `mutations()` generates CRUD:
176
-
177
- ```tsx
178
- zero.mutate.post.insert(post)
179
- zero.mutate.post.update(post)
180
- zero.mutate.post.delete(post)
181
- ```
182
-
183
- ### mutation context
184
-
185
- ```ts
186
- type MutatorContext = {
187
- tx: Transaction
188
- authData: AuthData | null
189
- environment: 'server' | 'client'
190
- can: (where, obj) => Promise<void>
191
- server?: {
192
- actions: ServerActions
193
- asyncTasks: AsyncAction[] // runs after transaction commits
194
- }
195
- }
196
- ```
197
-
198
- ### async tasks
199
-
200
- move slow work out of transactions:
201
-
202
- ```ts
203
- if (ctx.server) {
204
- ctx.server.asyncTasks.push(async () => {
205
- await ctx.server.actions.sendPushNotification(message)
206
- })
207
- }
208
- ```
209
-
210
- ## convergence
211
-
212
- mutations run on both client and server - they must produce the same result.
213
-
214
- **bad:**
215
-
216
- ```ts
217
- async insert(ctx, post) {
218
- await ctx.tx.mutate.post.insert({
219
- ...post,
220
- id: randomId(), // different on each run!
221
- createdAt: Date.now() // different timing!
222
- })
223
- }
224
- ```
225
-
226
- **good:**
227
-
228
- ```ts
229
- async insert(ctx, post) {
230
- // client generates id and timestamp once, passes to mutation
231
- await ctx.tx.mutate.post.insert(post)
232
-
233
- // server-only side effects are fine
234
- if (ctx.server) {
235
- await ctx.server.actions.sendEmail(post)
236
- }
237
- }
238
- ```
239
-
240
- ## calling mutations
241
-
242
- ```tsx
243
- // optimistic - updates UI immediately
244
- zero.mutate.post.update(post)
245
-
246
- // wait for server confirmation
247
- const result = await zero.mutate.post.update(post).server
248
- ```
249
-
250
- ## anti-patterns
251
-
252
- ### useAuth() vs useUser()
253
-
254
- **bad - waterfall:**
255
-
256
- ```tsx
257
- const { user } = useUser() // queries database, waits
258
- const [posts] = useQuery(postsByUserId, { userId: user?.id || '' })
259
- ```
260
-
261
- **good - immediate:**
262
-
263
- ```tsx
264
- const { user } = useAuth() // available immediately from jwt
265
- const [posts] = useQuery(postsByUserId, { userId: user?.id || '' })
266
- ```
267
-
268
- use `useAuth()` for query params. only use `useUser()` when you need full user
269
- record (profile data, settings).
270
-
271
- ### n+1 queries in lists
272
-
273
- **bad:**
274
-
275
- ```tsx
276
- function PostCard({ post }) {
277
- const [author] = useQuery(userById, { userId: post.userId }) // N+1!
278
- return <div>{author?.username}</div>
279
- }
280
- ```
281
-
282
- **good:**
283
-
284
- ```ts
285
- // include relation in query
286
- export const feedPosts = () => {
287
- return zql.post.related('user', (q) => q.one())
288
- }
289
-
290
- function PostCard({ post }) {
291
- return <div>{post.user?.username}</div> // already loaded
292
- }
293
- ```
294
-
295
- ### client-side filtering
296
-
297
- **bad:**
298
-
299
- ```tsx
300
- const [blockedUsers] = useQuery(blockedByMe, { userId })
301
- const blockedIds = blockedUsers.map(b => b.blockedId)
302
- const [posts] = useQuery(postsFiltered, { blockedUserIds: blockedIds })
303
- ```
304
-
305
- **good:**
306
-
307
- ```ts
308
- // filter server-side with exists()
309
- const notBlocked = serverWhere('post', (q, auth) => {
310
- if (!auth?.id) return true
311
- return q.not(q.exists('authorBlockedBy', (b) => b.where('blockerId', auth.id)))
312
- })
313
-
314
- export const feedPosts = () => zql.post.where(notBlocked)
315
- ```
316
-
317
- ### index vs detail page queries
318
-
319
- design queries so index pages load all data needed for detail pages:
320
-
321
- ```ts
322
- // feedPosts - used on index, includes everything detail page needs
323
- export const feedPosts = (props: { limit: number }) => {
324
- return zql.post
325
- .where(permission)
326
- .limit(props.limit)
327
- .related('user', (q) => q.one())
328
- .related('comments', (q) =>
329
- q.limit(50).related('user', (u) => u.one())
330
- )
331
- }
332
-
333
- // postDetail - used on detail page, same shape
334
- export const postDetail = (props: { postId: string }) => {
335
- return zql.post
336
- .where(permission)
337
- .where('id', props.postId)
338
- .one()
339
- .related('user', (q) => q.one())
340
- .related('comments', (q) =>
341
- q.limit(50).related('user', (u) => u.one())
342
- )
343
- }
344
- ```
345
-
346
- when navigating from feed to detail, Zero's local cache already has the data from
347
- `feedPosts`, so `postDetail` resolves instantly. the detail query is only slow on
348
- direct navigation (refresh, shared link).
349
-
350
- **key insight:** with Zero, "re-querying" isn't expensive if data is synced - the
351
- query runs against local cache. design queries with same relations so cache hits.
352
-
353
- ## debugging
354
-
355
- add `?debug=2` to url for detailed zero logs.
356
-
357
- ## soft deletes
358
-
359
- ```ts
360
- // mutation
361
- async delete(ctx, { id }) {
362
- await ctx.tx.mutate.post.update({ id, deleted: true })
363
- }
364
-
365
- // queries filter deleted
366
- .where('deleted', false)
367
- ```
368
-
369
- ## type generation
370
-
371
- regenerate after schema changes:
372
-
373
- ```bash
374
- bun tko zero generate
375
- ```
376
-
377
- `bun dev` watches and regenerates automatically.
378
-
379
- ## resources
380
-
381
- - zero docs: https://zero.rocicorp.dev
382
- - on-zero: packages/on-zero/readme.md
383
- - models: src/data/models/
384
- - queries: src/data/queries/