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.
- package/dist/index.mjs +362 -119
- package/package.json +2 -3
- package/templates/elysia/AGENTS.md +56 -0
- package/templates/elysia/CLAUDE.md +39 -0
- package/templates/elysia/Dockerfile +24 -0
- package/templates/elysia/README.md +148 -0
- package/templates/elysia/docker/init-extensions.sql +11 -0
- package/templates/elysia/docker-compose.yml +21 -0
- package/templates/elysia/env.example +16 -0
- package/templates/elysia/gitignore +6 -0
- package/templates/elysia/package.json +47 -0
- package/templates/elysia/questpie.config.ts +12 -0
- package/templates/elysia/src/index.ts +21 -0
- package/templates/elysia/src/lib/auth-client.ts +32 -0
- package/templates/elysia/src/lib/client.ts +13 -0
- package/templates/elysia/src/lib/env.ts +24 -0
- package/templates/elysia/src/lib/query-client.ts +18 -0
- package/templates/elysia/src/lib/query.ts +18 -0
- package/templates/elysia/src/questpie/server/.generated/context.gen.ts +200 -0
- package/templates/elysia/src/questpie/server/.generated/entities.gen.ts +84 -0
- package/templates/elysia/src/questpie/server/.generated/factories.ts +65 -0
- package/templates/elysia/src/questpie/server/.generated/index.ts +131 -0
- package/templates/elysia/src/questpie/server/.generated/names.gen.ts +25 -0
- package/templates/elysia/src/questpie/server/app.ts +10 -0
- package/templates/elysia/src/questpie/server/collections/index.ts +1 -0
- package/templates/elysia/src/questpie/server/collections/posts.collection.ts +10 -0
- package/templates/elysia/src/questpie/server/config/auth.ts +8 -0
- package/templates/elysia/src/questpie/server/config/openapi.ts +10 -0
- package/templates/elysia/src/questpie/server/globals/index.ts +1 -0
- package/templates/elysia/src/questpie/server/globals/site-settings.global.ts +10 -0
- package/templates/elysia/src/questpie/server/modules.ts +8 -0
- package/templates/elysia/src/questpie/server/questpie.config.ts +21 -0
- package/templates/elysia/tsconfig.json +28 -0
- package/templates/hono/AGENTS.md +56 -0
- package/templates/hono/CLAUDE.md +39 -0
- package/templates/hono/Dockerfile +24 -0
- package/templates/hono/README.md +148 -0
- package/templates/hono/docker/init-extensions.sql +11 -0
- package/templates/hono/docker-compose.yml +21 -0
- package/templates/hono/env.example +16 -0
- package/templates/hono/gitignore +6 -0
- package/templates/hono/package.json +47 -0
- package/templates/hono/questpie.config.ts +12 -0
- package/templates/hono/src/index.ts +30 -0
- package/templates/hono/src/lib/auth-client.ts +32 -0
- package/templates/hono/src/lib/client.ts +13 -0
- package/templates/hono/src/lib/env.ts +24 -0
- package/templates/hono/src/lib/query-client.ts +18 -0
- package/templates/hono/src/lib/query.ts +18 -0
- package/templates/hono/src/questpie/server/.generated/context.gen.ts +200 -0
- package/templates/hono/src/questpie/server/.generated/entities.gen.ts +84 -0
- package/templates/hono/src/questpie/server/.generated/factories.ts +65 -0
- package/templates/hono/src/questpie/server/.generated/index.ts +131 -0
- package/templates/hono/src/questpie/server/.generated/names.gen.ts +25 -0
- package/templates/hono/src/questpie/server/app.ts +10 -0
- package/templates/hono/src/questpie/server/collections/index.ts +1 -0
- package/templates/hono/src/questpie/server/collections/posts.collection.ts +10 -0
- package/templates/hono/src/questpie/server/config/auth.ts +8 -0
- package/templates/hono/src/questpie/server/config/openapi.ts +10 -0
- package/templates/hono/src/questpie/server/globals/index.ts +1 -0
- package/templates/hono/src/questpie/server/globals/site-settings.global.ts +10 -0
- package/templates/hono/src/questpie/server/modules.ts +8 -0
- package/templates/hono/src/questpie/server/questpie.config.ts +21 -0
- package/templates/hono/tsconfig.json +28 -0
- package/templates/next/AGENTS.md +55 -0
- package/templates/next/CLAUDE.md +39 -0
- package/templates/next/Dockerfile +25 -0
- package/templates/next/README.md +148 -0
- package/templates/next/components.json +22 -0
- package/templates/next/docker/init-extensions.sql +11 -0
- package/templates/next/docker-compose.yml +21 -0
- package/templates/next/env.example +16 -0
- package/templates/next/gitignore +10 -0
- package/templates/next/next-env.d.ts +5 -0
- package/templates/next/next.config.ts +20 -0
- package/templates/next/package.json +54 -0
- package/templates/next/postcss.config.mjs +8 -0
- package/templates/next/public/.gitkeep +0 -0
- package/templates/next/questpie.config.ts +12 -0
- package/templates/next/src/app/admin/[[...all]]/page.tsx +34 -0
- package/templates/next/src/app/admin/admin.css +4 -0
- package/templates/next/src/app/admin/layout.tsx +63 -0
- package/templates/next/src/app/api/[...all]/route.ts +24 -0
- package/templates/next/src/app/layout.tsx +24 -0
- package/templates/next/src/app/not-found.tsx +18 -0
- package/templates/next/src/app/page.tsx +74 -0
- package/templates/next/src/app/providers.tsx +11 -0
- package/templates/next/src/lib/auth-client.ts +12 -0
- package/templates/next/src/lib/client.ts +13 -0
- package/templates/next/src/lib/env.ts +24 -0
- package/templates/next/src/lib/query-client.ts +18 -0
- package/templates/next/src/lib/query.ts +18 -0
- package/templates/next/src/questpie/admin/.generated/client.ts +13 -0
- package/templates/next/src/questpie/admin/admin.ts +9 -0
- package/templates/next/src/questpie/admin/modules.ts +3 -0
- package/templates/next/src/questpie/server/.generated/context.gen.ts +204 -0
- package/templates/next/src/questpie/server/.generated/entities.gen.ts +100 -0
- package/templates/next/src/questpie/server/.generated/factories.ts +204 -0
- package/templates/next/src/questpie/server/.generated/index.ts +139 -0
- package/templates/next/src/questpie/server/.generated/names.gen.ts +31 -0
- package/templates/next/src/questpie/server/app.ts +10 -0
- package/templates/next/src/questpie/server/collections/index.ts +1 -0
- package/templates/next/src/questpie/server/collections/posts.collection.ts +58 -0
- package/templates/next/src/questpie/server/config/admin.ts +80 -0
- package/templates/next/src/questpie/server/config/auth.ts +8 -0
- package/templates/next/src/questpie/server/config/openapi.ts +10 -0
- package/templates/next/src/questpie/server/globals/index.ts +1 -0
- package/templates/next/src/questpie/server/globals/site-settings.global.ts +19 -0
- package/templates/next/src/questpie/server/modules.ts +9 -0
- package/templates/next/src/questpie/server/questpie.config.ts +21 -0
- package/templates/next/src/styles.css +125 -0
- package/templates/next/tsconfig.json +37 -0
- package/templates/tanstack-start/AGENTS.md +35 -607
- package/templates/tanstack-start/CLAUDE.md +26 -134
- package/templates/tanstack-start/README.md +13 -1
- package/templates/tanstack-start/docker/init-extensions.sql +11 -0
- package/templates/tanstack-start/docker-compose.yml +1 -0
- package/templates/tanstack-start/src/lib/auth-client.ts +1 -1
- package/templates/tanstack-start/src/lib/client.ts +1 -1
- package/templates/tanstack-start/src/lib/query.ts +18 -0
- package/templates/tanstack-start/src/questpie/server/collections/index.ts +1 -1
- package/templates/tanstack-start/src/questpie/server/globals/index.ts +1 -1
- package/templates/tanstack-start/src/questpie/server/questpie.config.ts +1 -1
- package/templates/tanstack-start/src/routes/__root.tsx +31 -1
- package/templates/tanstack-start/src/routes/api/$.ts +1 -1
- package/templates/tanstack-start/src/routes/index.tsx +97 -0
- package/skills/questpie/AGENTS.md +0 -2871
- package/skills/questpie/SKILL.md +0 -293
- package/skills/questpie/coverage.json +0 -213
- package/skills/questpie/references/auth.md +0 -236
- package/skills/questpie/references/business-logic.md +0 -620
- package/skills/questpie/references/codegen-plugin-api.md +0 -382
- package/skills/questpie/references/crud-api.md +0 -580
- package/skills/questpie/references/data-modeling.md +0 -509
- package/skills/questpie/references/extend.md +0 -584
- package/skills/questpie/references/field-types.md +0 -398
- package/skills/questpie/references/infrastructure-adapters.md +0 -720
- package/skills/questpie/references/mcp.md +0 -147
- package/skills/questpie/references/multi-tenancy.md +0 -363
- package/skills/questpie/references/production.md +0 -640
- package/skills/questpie/references/query-operators.md +0 -125
- package/skills/questpie/references/quickstart.md +0 -562
- package/skills/questpie/references/rules.md +0 -454
- package/skills/questpie/references/sandbox.md +0 -110
- package/skills/questpie/references/tanstack-query.md +0 -543
- package/skills/questpie/references/type-inference.md +0 -167
- package/skills/questpie/references/workflows.md +0 -155
- package/skills/questpie-admin/AGENTS.md +0 -1515
- package/skills/questpie-admin/SKILL.md +0 -443
- package/skills/questpie-admin/references/blocks.md +0 -331
- package/skills/questpie-admin/references/custom-ui.md +0 -305
- 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.
|