create-questpie 2.0.4 → 2.1.0

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 (152) hide show
  1. package/dist/index.mjs +362 -119
  2. package/package.json +2 -3
  3. package/templates/elysia/AGENTS.md +56 -0
  4. package/templates/elysia/CLAUDE.md +39 -0
  5. package/templates/elysia/Dockerfile +24 -0
  6. package/templates/elysia/README.md +148 -0
  7. package/templates/elysia/docker/init-extensions.sql +11 -0
  8. package/templates/elysia/docker-compose.yml +21 -0
  9. package/templates/elysia/env.example +16 -0
  10. package/templates/elysia/gitignore +6 -0
  11. package/templates/elysia/package.json +47 -0
  12. package/templates/elysia/questpie.config.ts +12 -0
  13. package/templates/elysia/src/index.ts +21 -0
  14. package/templates/elysia/src/lib/auth-client.ts +32 -0
  15. package/templates/elysia/src/lib/client.ts +13 -0
  16. package/templates/elysia/src/lib/env.ts +24 -0
  17. package/templates/elysia/src/lib/query-client.ts +18 -0
  18. package/templates/elysia/src/lib/query.ts +18 -0
  19. package/templates/elysia/src/questpie/server/.generated/context.gen.ts +200 -0
  20. package/templates/elysia/src/questpie/server/.generated/entities.gen.ts +84 -0
  21. package/templates/elysia/src/questpie/server/.generated/factories.ts +65 -0
  22. package/templates/elysia/src/questpie/server/.generated/index.ts +131 -0
  23. package/templates/elysia/src/questpie/server/.generated/names.gen.ts +25 -0
  24. package/templates/elysia/src/questpie/server/app.ts +10 -0
  25. package/templates/elysia/src/questpie/server/collections/index.ts +1 -0
  26. package/templates/elysia/src/questpie/server/collections/posts.collection.ts +10 -0
  27. package/templates/elysia/src/questpie/server/config/auth.ts +8 -0
  28. package/templates/elysia/src/questpie/server/config/openapi.ts +10 -0
  29. package/templates/elysia/src/questpie/server/globals/index.ts +1 -0
  30. package/templates/elysia/src/questpie/server/globals/site-settings.global.ts +10 -0
  31. package/templates/elysia/src/questpie/server/modules.ts +8 -0
  32. package/templates/elysia/src/questpie/server/questpie.config.ts +21 -0
  33. package/templates/elysia/tsconfig.json +28 -0
  34. package/templates/hono/AGENTS.md +56 -0
  35. package/templates/hono/CLAUDE.md +39 -0
  36. package/templates/hono/Dockerfile +24 -0
  37. package/templates/hono/README.md +148 -0
  38. package/templates/hono/docker/init-extensions.sql +11 -0
  39. package/templates/hono/docker-compose.yml +21 -0
  40. package/templates/hono/env.example +16 -0
  41. package/templates/hono/gitignore +6 -0
  42. package/templates/hono/package.json +47 -0
  43. package/templates/hono/questpie.config.ts +12 -0
  44. package/templates/hono/src/index.ts +30 -0
  45. package/templates/hono/src/lib/auth-client.ts +32 -0
  46. package/templates/hono/src/lib/client.ts +13 -0
  47. package/templates/hono/src/lib/env.ts +24 -0
  48. package/templates/hono/src/lib/query-client.ts +18 -0
  49. package/templates/hono/src/lib/query.ts +18 -0
  50. package/templates/hono/src/questpie/server/.generated/context.gen.ts +200 -0
  51. package/templates/hono/src/questpie/server/.generated/entities.gen.ts +84 -0
  52. package/templates/hono/src/questpie/server/.generated/factories.ts +65 -0
  53. package/templates/hono/src/questpie/server/.generated/index.ts +131 -0
  54. package/templates/hono/src/questpie/server/.generated/names.gen.ts +25 -0
  55. package/templates/hono/src/questpie/server/app.ts +10 -0
  56. package/templates/hono/src/questpie/server/collections/index.ts +1 -0
  57. package/templates/hono/src/questpie/server/collections/posts.collection.ts +10 -0
  58. package/templates/hono/src/questpie/server/config/auth.ts +8 -0
  59. package/templates/hono/src/questpie/server/config/openapi.ts +10 -0
  60. package/templates/hono/src/questpie/server/globals/index.ts +1 -0
  61. package/templates/hono/src/questpie/server/globals/site-settings.global.ts +10 -0
  62. package/templates/hono/src/questpie/server/modules.ts +8 -0
  63. package/templates/hono/src/questpie/server/questpie.config.ts +21 -0
  64. package/templates/hono/tsconfig.json +28 -0
  65. package/templates/next/AGENTS.md +55 -0
  66. package/templates/next/CLAUDE.md +39 -0
  67. package/templates/next/Dockerfile +25 -0
  68. package/templates/next/README.md +148 -0
  69. package/templates/next/components.json +22 -0
  70. package/templates/next/docker/init-extensions.sql +11 -0
  71. package/templates/next/docker-compose.yml +21 -0
  72. package/templates/next/env.example +16 -0
  73. package/templates/next/gitignore +10 -0
  74. package/templates/next/next-env.d.ts +5 -0
  75. package/templates/next/next.config.ts +20 -0
  76. package/templates/next/package.json +54 -0
  77. package/templates/next/postcss.config.mjs +8 -0
  78. package/templates/next/public/.gitkeep +0 -0
  79. package/templates/next/questpie.config.ts +12 -0
  80. package/templates/next/src/app/admin/[[...all]]/page.tsx +34 -0
  81. package/templates/next/src/app/admin/admin.css +4 -0
  82. package/templates/next/src/app/admin/layout.tsx +63 -0
  83. package/templates/next/src/app/api/[...all]/route.ts +24 -0
  84. package/templates/next/src/app/layout.tsx +24 -0
  85. package/templates/next/src/app/not-found.tsx +18 -0
  86. package/templates/next/src/app/page.tsx +74 -0
  87. package/templates/next/src/app/providers.tsx +11 -0
  88. package/templates/next/src/lib/auth-client.ts +12 -0
  89. package/templates/next/src/lib/client.ts +13 -0
  90. package/templates/next/src/lib/env.ts +24 -0
  91. package/templates/next/src/lib/query-client.ts +18 -0
  92. package/templates/next/src/lib/query.ts +18 -0
  93. package/templates/next/src/questpie/admin/.generated/client.ts +13 -0
  94. package/templates/next/src/questpie/admin/admin.ts +9 -0
  95. package/templates/next/src/questpie/admin/modules.ts +3 -0
  96. package/templates/next/src/questpie/server/.generated/context.gen.ts +204 -0
  97. package/templates/next/src/questpie/server/.generated/entities.gen.ts +100 -0
  98. package/templates/next/src/questpie/server/.generated/factories.ts +204 -0
  99. package/templates/next/src/questpie/server/.generated/index.ts +139 -0
  100. package/templates/next/src/questpie/server/.generated/names.gen.ts +31 -0
  101. package/templates/next/src/questpie/server/app.ts +10 -0
  102. package/templates/next/src/questpie/server/collections/index.ts +1 -0
  103. package/templates/next/src/questpie/server/collections/posts.collection.ts +58 -0
  104. package/templates/next/src/questpie/server/config/admin.ts +80 -0
  105. package/templates/next/src/questpie/server/config/auth.ts +8 -0
  106. package/templates/next/src/questpie/server/config/openapi.ts +10 -0
  107. package/templates/next/src/questpie/server/globals/index.ts +1 -0
  108. package/templates/next/src/questpie/server/globals/site-settings.global.ts +19 -0
  109. package/templates/next/src/questpie/server/modules.ts +9 -0
  110. package/templates/next/src/questpie/server/questpie.config.ts +21 -0
  111. package/templates/next/src/styles.css +125 -0
  112. package/templates/next/tsconfig.json +37 -0
  113. package/templates/tanstack-start/AGENTS.md +35 -607
  114. package/templates/tanstack-start/CLAUDE.md +26 -134
  115. package/templates/tanstack-start/README.md +13 -1
  116. package/templates/tanstack-start/docker/init-extensions.sql +11 -0
  117. package/templates/tanstack-start/docker-compose.yml +1 -0
  118. package/templates/tanstack-start/src/lib/auth-client.ts +1 -1
  119. package/templates/tanstack-start/src/lib/client.ts +1 -1
  120. package/templates/tanstack-start/src/lib/query.ts +18 -0
  121. package/templates/tanstack-start/src/questpie/server/collections/index.ts +1 -1
  122. package/templates/tanstack-start/src/questpie/server/globals/index.ts +1 -1
  123. package/templates/tanstack-start/src/questpie/server/questpie.config.ts +1 -1
  124. package/templates/tanstack-start/src/routes/__root.tsx +31 -1
  125. package/templates/tanstack-start/src/routes/api/$.ts +1 -1
  126. package/templates/tanstack-start/src/routes/index.tsx +97 -0
  127. package/skills/questpie/AGENTS.md +0 -2871
  128. package/skills/questpie/SKILL.md +0 -293
  129. package/skills/questpie/coverage.json +0 -213
  130. package/skills/questpie/references/auth.md +0 -236
  131. package/skills/questpie/references/business-logic.md +0 -620
  132. package/skills/questpie/references/codegen-plugin-api.md +0 -382
  133. package/skills/questpie/references/crud-api.md +0 -580
  134. package/skills/questpie/references/data-modeling.md +0 -509
  135. package/skills/questpie/references/extend.md +0 -584
  136. package/skills/questpie/references/field-types.md +0 -398
  137. package/skills/questpie/references/infrastructure-adapters.md +0 -720
  138. package/skills/questpie/references/mcp.md +0 -147
  139. package/skills/questpie/references/multi-tenancy.md +0 -363
  140. package/skills/questpie/references/production.md +0 -640
  141. package/skills/questpie/references/query-operators.md +0 -125
  142. package/skills/questpie/references/quickstart.md +0 -562
  143. package/skills/questpie/references/rules.md +0 -454
  144. package/skills/questpie/references/sandbox.md +0 -110
  145. package/skills/questpie/references/tanstack-query.md +0 -543
  146. package/skills/questpie/references/type-inference.md +0 -167
  147. package/skills/questpie/references/workflows.md +0 -155
  148. package/skills/questpie-admin/AGENTS.md +0 -1515
  149. package/skills/questpie-admin/SKILL.md +0 -443
  150. package/skills/questpie-admin/references/blocks.md +0 -331
  151. package/skills/questpie-admin/references/custom-ui.md +0 -305
  152. package/skills/questpie-admin/references/views.md +0 -449
@@ -1,580 +0,0 @@
1
- ---
2
- name: questpie-core/crud-api
3
- description: QUESTPIE CRUD API find findOne create updateById updateMany updateBatch deleteById deleteMany restoreById count atomic conditional update claim optimistic locking query operators where filter sort orderBy pagination limit offset with select relations depth context accessMode collections globals client server typesafe
4
- - questpie-core
5
- ---
6
-
7
- This skill builds on questpie-core.
8
-
9
- ## Two API Surfaces
10
-
11
- QUESTPIE exposes CRUD operations in two ways depending on where you call them:
12
-
13
- ### 1. Handler Context (routes, hooks, jobs)
14
-
15
- Inside any handler, `collections` and `globals` are injected via context. The current request context (session, locale, access mode) is implicit:
16
-
17
- ```ts
18
- // routes/get-published.ts
19
- import { route } from "questpie/services";
20
- export default route()
21
- .get()
22
- .handler(async ({ collections }) => {
23
- const result = await collections.posts.find({
24
- where: { status: "published" },
25
- limit: 10,
26
- orderBy: { createdAt: "desc" },
27
- });
28
- return result.docs;
29
- });
30
- ```
31
-
32
- ### 2. App Instance (scripts, seeds, external)
33
-
34
- Outside handlers, use `app.collections.*` and pass an explicit context as the second argument:
35
-
36
- ```ts
37
- import { app } from "#questpie";
38
-
39
- const ctx = await app.createContext({ accessMode: "system", locale: "en" });
40
-
41
- const result = await app.collections.posts.find(
42
- { where: { status: "published" }, limit: 10 },
43
- ctx,
44
- );
45
- ```
46
-
47
- ## Collection Operations
48
-
49
- One vocabulary on both surfaces (server CRUD and client SDK):
50
-
51
- | Concept | Method | Returns |
52
- | --- | --- | --- |
53
- | list (paginated) | `find(options)` | `{ docs: T[], totalDocs: number }` |
54
- | single by query | `findOne(options)` | `T \| null` |
55
- | count | `count(options)` | `number` |
56
- | create | `create(data)` | `T` |
57
- | update by id | `updateById({ id, data })` | `T` (throws notFound) |
58
- | bulk update by where | `updateMany({ where, data })` | `T[]` (winners) |
59
- | per-record batch | `updateBatch({ updates })` | `T[]` |
60
- | delete by id | `deleteById({ id })` | `{ success }` (throws notFound) |
61
- | bulk delete by where | `deleteMany({ where })` | `{ success, count }` |
62
- | restore by id | `restoreById({ id })` | `T` (softDelete only) |
63
-
64
- Deprecated aliases (removed in v4): server `update`/`delete` = bulk (`updateMany`/`deleteMany`); client `update`/`delete`/`restore` = by-id (`updateById`/`deleteById`/`restoreById`). Avoid them — the same names mean different things on each surface. Accessing a method that does not exist on server CRUD throws a `TypeError` listing valid methods (it does NOT return `undefined`).
65
-
66
- ### `find(options)`
67
-
68
- List documents with filtering, sorting, and pagination.
69
-
70
- ```ts
71
- const result = await collections.posts.find({
72
- where: { status: "published", price: { gte: 1000 } },
73
- orderBy: { createdAt: "desc" },
74
- limit: 20,
75
- offset: 0,
76
- with: { author: true, category: true },
77
- select: { title: true, status: true, createdAt: true },
78
- });
79
- // result: { docs: T[], totalDocs: number }
80
- ```
81
-
82
- **Return type:** `{ docs: T[], totalDocs: number }`
83
-
84
- ### `findOne(options)`
85
-
86
- Fetch a single document. Returns `null` if not found.
87
-
88
- ```ts
89
- const post = await collections.posts.findOne({
90
- where: { slug: "hello-world" },
91
- with: { author: true },
92
- });
93
- // post: T | null
94
- ```
95
-
96
- ### `create(data)`
97
-
98
- Create a new document. Pass field values as a flat object.
99
-
100
- ```ts
101
- const post = await collections.posts.create({
102
- title: "Hello World",
103
- body: "Content here",
104
- status: "draft",
105
- author: "user-id-123",
106
- });
107
- // post: T (created record with id)
108
- ```
109
-
110
- ### `updateById(options)`
111
-
112
- Update a single document by id. Returns the updated record; throws `notFound` if the record does not exist (or vanished concurrently).
113
-
114
- ```ts
115
- const updated = await collections.posts.updateById({
116
- id: "abc-123",
117
- data: { status: "published" },
118
- });
119
- // updated: T (updated record)
120
- ```
121
-
122
- ### `updateMany(options)`
123
-
124
- Bulk update all documents matching `where`. Returns an **array** of the updated records — never a single object.
125
-
126
- ```ts
127
- const updated = await collections.posts.updateMany({
128
- where: { status: "draft" },
129
- data: { status: "archived" },
130
- });
131
- // updated: T[] — exactly the rows that were written
132
- ```
133
-
134
- `updateMany` is claim-checked: inside the write transaction the matched rows are locked and `where` is re-evaluated, so rows changed by a concurrent writer are skipped instead of silently overwritten. The returned array reports exactly the winners.
135
-
136
- #### Atomic conditional updates (claims, optimistic locking)
137
-
138
- Use a conditional `where` + the array length as the win/lose signal:
139
-
140
- ```ts
141
- // Claim: of two parallel claims, EXACTLY ONE wins
142
- const claimed = await collections.event_members.updateMany(
143
- {
144
- where: { id: memberId, user: { isNull: true } },
145
- data: { user: newUserId },
146
- },
147
- { accessMode: "system" },
148
- );
149
- if (claimed.length === 0) {
150
- // Lost the race (or row vanished) — handle explicitly
151
- }
152
-
153
- // Optimistic concurrency: write only if the revision is unchanged
154
- const bumped = await collections.documents.updateMany(
155
- { where: { id, revision: doc.revision }, data: { body, revision: doc.revision + 1 } },
156
- ctx,
157
- );
158
- if (bumped.length === 0) throw new Error("Conflict — reload and retry");
159
- ```
160
-
161
- Hook timing: `beforeValidate`/`beforeChange` run before the transaction on candidates (intent — may fire for losers); `afterChange`, versioning, and the return value are winners-only (fact).
162
-
163
- ### `updateBatch(options)`
164
-
165
- Distinct data per record, one transaction.
166
-
167
- ```ts
168
- const updated = await collections.posts.updateBatch({
169
- updates: [
170
- { id: "a", data: { order: 1 } },
171
- { id: "b", data: { order: 2 } },
172
- ],
173
- });
174
- // updated: T[]
175
- ```
176
-
177
- ### `deleteById(options)`
178
-
179
- Delete a single document by id (soft delete when enabled). Throws `notFound` if missing.
180
-
181
- ```ts
182
- await collections.posts.deleteById({ id: "abc-123" });
183
- // { success: true }
184
- ```
185
-
186
- ### `deleteMany(options)`
187
-
188
- Bulk delete all documents matching `where`. Claim-checked like `updateMany` — `count` is the number of rows that still matched at delete time.
189
-
190
- ```ts
191
- const result = await collections.posts.deleteMany({
192
- where: { status: "archived" },
193
- });
194
- // result: { success: true, count: number }
195
- ```
196
-
197
- ### `restoreById(options)`
198
-
199
- Restore a soft-deleted document (collections with `softDelete: true`).
200
-
201
- ```ts
202
- const restored = await collections.posts.restoreById({ id: "abc-123" });
203
- // restored: T
204
- ```
205
-
206
- ### `count(options)`
207
-
208
- Count documents matching a filter.
209
-
210
- ```ts
211
- const total = await collections.posts.count({
212
- where: { status: "published" },
213
- });
214
- // total: number
215
- ```
216
-
217
- ## Global Operations
218
-
219
- Globals have only two operations:
220
-
221
- ```ts
222
- // Read global
223
- const settings = await globals.siteSettings.get({});
224
-
225
- // Update global
226
- const updated = await globals.siteSettings.update({
227
- siteName: "New Name",
228
- });
229
- ```
230
-
231
- Via app instance:
232
-
233
- ```ts
234
- const settings = await app.globals.siteSettings.get({}, ctx);
235
- await app.globals.siteSettings.update(
236
- { siteName: "New Name" },
237
- ctx,
238
- );
239
- ```
240
-
241
- ## Query Operators
242
-
243
- Operators are always nested inside field objects in `where`. See `references/query-operators.md` for the full reference.
244
-
245
- ```ts
246
- // Multiple fields = AND
247
- where: {
248
- status: "published", // equality shorthand
249
- price: { gte: 1000, lt: 5000 }, // range (AND within same field)
250
- title: { contains: "guide" }, // substring
251
- category: { in: ["news", "blog"] }, // one-of
252
- }
253
- ```
254
-
255
- ### Equality Shorthand
256
-
257
- All field types support direct equality:
258
-
259
- ```ts
260
- where: {
261
- status: "published";
262
- }
263
- // equivalent to: where: { status: { eq: "published" } }
264
- ```
265
-
266
- ## Sorting
267
-
268
- Use `orderBy` with `"asc"` or `"desc"`:
269
-
270
- ```ts
271
- const result = await collections.posts.find({
272
- orderBy: { createdAt: "desc" },
273
- });
274
- ```
275
-
276
- Multi-field sorting: order determines priority (first = primary sort). All
277
- three syntaxes work, including inside relation `with` options:
278
-
279
- ```ts
280
- // Array syntax (preferred for explicit priority)
281
- orderBy: [{ status: "desc" }, { createdAt: "desc" }]
282
-
283
- // Object syntax (key order = priority)
284
- orderBy: { status: "desc", createdAt: "desc" }
285
-
286
- // Function syntax
287
- orderBy: (table, { asc, desc }) => [desc(table.status), asc(table.title)]
288
- ```
289
-
290
- ## Pagination
291
-
292
- Use `limit` and `offset`:
293
-
294
- ```ts
295
- const page2 = await collections.posts.find({
296
- limit: 20,
297
- offset: 20,
298
- });
299
- // page2.totalDocs = total count across all pages
300
- ```
301
-
302
- ### Keyset (cursor) pagination
303
-
304
- For stable pagination over changing data, use a tuple cursor of
305
- `(createdAt, id)` with a matching multi-field `orderBy`. System timestamps
306
- are stored with millisecond precision (`timestamp(3)`), so a `Date` you read
307
- back equals the stored value exactly — cursor comparisons are exact:
308
-
309
- ```ts
310
- const page = await collections.posts.find({
311
- where: cursor
312
- ? {
313
- OR: [
314
- { createdAt: { lt: cursor.createdAt } },
315
- {
316
- AND: [
317
- { createdAt: { eq: cursor.createdAt } },
318
- { id: { lt: cursor.id } },
319
- ],
320
- },
321
- ],
322
- }
323
- : undefined,
324
- orderBy: [{ createdAt: "desc" }, { id: "desc" }],
325
- limit: 20,
326
- });
327
- const last = page.docs.at(-1);
328
- const nextCursor = last ? { createdAt: last.createdAt, id: last.id } : null;
329
- ```
330
-
331
- Always use the explicit `{ eq: ... }` operator for `Date` cursor values —
332
- do not pass a bare `Date` as an equality shorthand.
333
-
334
- ## Relations
335
-
336
- Relations are NOT populated by default. Use `with` to eager-load:
337
-
338
- ```ts
339
- const post = await collections.posts.findOne({
340
- where: { id: "abc" },
341
- with: { author: true, category: true },
342
- });
343
- // post.author is now the full author object, not just an ID
344
- ```
345
-
346
- Use `select` to pick specific fields:
347
-
348
- ```ts
349
- const posts = await collections.posts.find({
350
- select: { title: true, status: true },
351
- });
352
- ```
353
-
354
- ## Context and Access Modes
355
-
356
- ### In Handlers
357
-
358
- Context is automatic. The current user's session determines access:
359
-
360
- ```ts
361
- export default route()
362
- .get()
363
- .handler(async ({ collections, session }) => {
364
- // Access control is enforced based on session
365
- const posts = await collections.posts.find({});
366
- return posts;
367
- });
368
- ```
369
-
370
- ### Partial Overrides (Inside Request Scope)
371
-
372
- The optional second argument of every CRUD call merges with the ambient request scope. Priority: **explicit param → ALS scope (`runWithContext`) → defaults** (`accessMode: "system"`, `locale: "en"`). A bare `{ accessMode: "system" }` elevates only the mode — the request's `session`, `db`, and `locale` ride along automatically. The inverse holds too:
373
-
374
- ```ts
375
- // Inside any handler / hook / Better Auth callback:
376
- await collections.posts.updateMany(
377
- { where: { author: oldId }, data: { author: newId } },
378
- { accessMode: "system" }, // mode elevated; session/db/locale inherited
379
- );
380
-
381
- await collections.posts.find({}, { accessMode: "user" }); // rules re-enabled against inherited session
382
- ```
383
-
384
- Never re-thread `session`/`locale` by hand when you only want a different access mode.
385
-
386
- ### Transactions
387
-
388
- `withTransaction(db, fn)` (from `questpie`) runs multiple CRUD calls atomically — calls inside the callback inherit the transaction connection through the ALS scope, and nested `withTransaction` calls reuse the open transaction. Queue side effects for after COMMIT with `onAfterCommit`:
389
-
390
- ```ts
391
- import { onAfterCommit, withTransaction } from "questpie";
392
-
393
- await withTransaction(db, async () => {
394
- const order = await collections.orders.create({ ... });
395
- await collections.inventory.updateMany({
396
- where: { sku: order.sku, status: "available" },
397
- data: { status: "reserved" },
398
- });
399
- onAfterCommit(async () => {
400
- await queue.notifyWarehouse.publish({ orderId: order.id });
401
- });
402
- });
403
- ```
404
-
405
- Do not run output-hook-heavy reads (blocks/upload `afterRead`) inside an open transaction unless necessary — they inherit the tx connection too.
406
-
407
- ### In Scripts / Seeds
408
-
409
- Outside any request scope, create an explicit context with `app.createContext()`:
410
-
411
- ```ts
412
- // System mode -- bypasses all access control
413
- const ctx = await app.createContext({ accessMode: "system", locale: "en" });
414
-
415
- // User mode -- enforces access control (requires session)
416
- const ctx = await app.createContext({ accessMode: "user" });
417
- ```
418
-
419
- ## Client API
420
-
421
- The client SDK uses the same vocabulary:
422
-
423
- ```ts
424
- const posts = await client.collections.posts.find({ limit: 10 });
425
- const post = await client.collections.posts.findOne({ where: { id: "abc" } });
426
- const created = await client.collections.posts.create({ title: "New" });
427
- const updated = await client.collections.posts.updateById({
428
- id: "abc",
429
- data: { title: "Updated" },
430
- });
431
- await client.collections.posts.deleteById({ id: "abc" });
432
- const many = await client.collections.posts.updateMany({
433
- where: { status: "draft" },
434
- data: { status: "review" },
435
- });
436
- await client.collections.posts.deleteMany({ where: { status: "archived" } });
437
- const count = await client.collections.posts.count({
438
- where: { status: "draft" },
439
- });
440
- ```
441
-
442
- ### Live Queries (Client Only)
443
-
444
- Every read has a live form — `live()` mirrors `find()` (same options, same snapshot type) and pushes access-controlled snapshots over SSE. Globals mirror `get()`: `client.globals.<name>.live(...)`. See AGENTS.md §19 Realtime:
445
-
446
- ```ts
447
- const stop = client.collections.posts.live(
448
- { where: { status: "published" }, with: { author: true } },
449
- (snap) => render(snap.docs), // typed find() result
450
- );
451
- stop(); // unsubscribe
452
-
453
- // AsyncGenerator form (workers, agents, tests)
454
- for await (const snap of client.collections.posts.liveIter({ limit: 10 })) {
455
- render(snap.docs);
456
- }
457
- ```
458
-
459
- ### Upload (Client Only)
460
-
461
- For upload collections:
462
-
463
- ```ts
464
- const asset = await client.collections.assets.upload(file, {
465
- onProgress: (percent) => console.log(`${percent}%`),
466
- });
467
-
468
- const assets = await client.collections.assets.uploadMany(files, {
469
- onProgress: (percent) => console.log(`${percent}%`),
470
- });
471
- ```
472
-
473
- ## Common Mistakes
474
-
475
- ### CRITICAL: Missing context in app.collections calls
476
-
477
- When using `app.collections.*` outside handlers, you MUST pass a context. Without it, the call has no session, no locale, and no access mode.
478
-
479
- ```ts
480
- // WRONG -- no context
481
- const posts = await app.collections.posts.find({});
482
-
483
- // CORRECT -- explicit context
484
- const ctx = await app.createContext({ accessMode: "system" });
485
- const posts = await app.collections.posts.find({}, ctx);
486
- ```
487
-
488
- Inside handlers (route handlers, hooks, jobs), context is injected automatically -- use `collections.*` directly.
489
-
490
- ### HIGH: Expecting find() to return an array
491
-
492
- `find()` returns `{ docs: T[], totalDocs: number }`, not an array.
493
-
494
- ```ts
495
- // WRONG
496
- const posts = await collections.posts.find({});
497
- posts.forEach((p) => console.log(p.title)); // TypeError
498
-
499
- // CORRECT
500
- const { docs, totalDocs } = await collections.posts.find({});
501
- docs.forEach((p) => console.log(p.title));
502
- ```
503
-
504
- ### HIGH: Relations not populated
505
-
506
- Relations return only the ID by default. Use `with` to populate:
507
-
508
- ```ts
509
- // Returns { author: "user-id-123" }
510
- const post = await collections.posts.findOne({ where: { id: "abc" } });
511
-
512
- // Returns { author: { id: "user-id-123", name: "John", ... } }
513
- const post = await collections.posts.findOne({
514
- where: { id: "abc" },
515
- with: { author: true },
516
- });
517
- ```
518
-
519
- ### MEDIUM: Using accessMode "system" in HTTP handlers
520
-
521
- System mode bypasses all access control. Only use it in background jobs, seeds, and scripts -- never in request handlers.
522
-
523
- ```ts
524
- // WRONG -- in an HTTP route handler
525
- export default route()
526
- .get()
527
- .handler(async ({ app }) => {
528
- const ctx = await app.createContext({ accessMode: "system" });
529
- return app.collections.posts.find({}, ctx); // bypasses access control!
530
- });
531
-
532
- // CORRECT -- use injected collections (respects session access rules)
533
- export default route()
534
- .get()
535
- .handler(async ({ collections }) => {
536
- return collections.posts.find({});
537
- });
538
- ```
539
-
540
- ### HIGH: Expecting updateMany() to return a single record
541
-
542
- Server bulk update returns an **array** of updated records:
543
-
544
- ```ts
545
- // WRONG -- updateMany returns T[], not T
546
- const updated = await collections.posts.updateMany({
547
- where: { id: "abc" },
548
- data: { status: "published" },
549
- });
550
- console.log(updated.status); // undefined!
551
-
552
- // CORRECT
553
- const [updated] = await collections.posts.updateMany({
554
- where: { id: "abc" },
555
- data: { status: "published" },
556
- });
557
- // or, for a single record by id:
558
- const updated2 = await collections.posts.updateById({
559
- id: "abc",
560
- data: { status: "published" },
561
- });
562
- ```
563
-
564
- ### HIGH: `update`/`delete` mean different things on server vs client
565
-
566
- On server CRUD, `update`/`delete` are deprecated aliases of the BULK operations (`{ where, data }` → `T[]`). On the client SDK they are by-id operations (`{ id, data }` → `T`). Always use the unambiguous names: `updateById`/`deleteById`/`restoreById` for single records, `updateMany`/`deleteMany` for bulk. Calling a method that does not exist (e.g. a typo) on server CRUD throws a `TypeError` listing the valid methods.
567
-
568
- ### MEDIUM: Wrong create() signature
569
-
570
- `create()` takes a flat data object, NOT `{ data: {...} }`:
571
-
572
- ```ts
573
- // WRONG
574
- await collections.posts.create({ data: { title: "Hello" } });
575
-
576
- // CORRECT
577
- await collections.posts.create({ title: "Hello", body: "World" });
578
- ```
579
-
580
- Note: `updateById()`/`updateMany()` DO use `{ id/where, data }` -- only `create()` is flat.