@cfast/db 0.0.1
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/LICENSE +21 -0
- package/README.md +686 -0
- package/dist/index.d.ts +636 -0
- package/dist/index.js +628 -0
- package/package.json +52 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
import { DrizzleTable, PermissionDescriptor, Grant } from '@cfast/permissions';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A lazy, permission-aware database operation.
|
|
5
|
+
*
|
|
6
|
+
* Every method on {@link Db} returns an `Operation` instead of a promise. The operation
|
|
7
|
+
* exposes its permission requirements via `.permissions` for inspection and executes with
|
|
8
|
+
* full permission checking via `.run()`. This two-phase design enables UI adaptation,
|
|
9
|
+
* upfront composition via {@link compose}, and introspection before any SQL is executed.
|
|
10
|
+
*
|
|
11
|
+
* @typeParam TResult - The type of the result returned by `.run()`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const op = db.query(posts).findMany();
|
|
16
|
+
*
|
|
17
|
+
* // Inspect permissions without executing
|
|
18
|
+
* console.log(op.permissions);
|
|
19
|
+
* // => [{ action: "read", table: "posts" }]
|
|
20
|
+
*
|
|
21
|
+
* // Execute with permission checks
|
|
22
|
+
* const rows = await op.run({});
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
type Operation<TResult> = {
|
|
26
|
+
/** Structural permission requirements. Available immediately without execution. */
|
|
27
|
+
permissions: PermissionDescriptor[];
|
|
28
|
+
/**
|
|
29
|
+
* Checks permissions, applies permission WHERE clauses, executes the query via Drizzle,
|
|
30
|
+
* and returns the result. Throws `ForbiddenError` if the user's role lacks a required grant.
|
|
31
|
+
*
|
|
32
|
+
* @param params - Placeholder values for `sql.placeholder()` calls. Pass `{}` when no placeholders are used.
|
|
33
|
+
*/
|
|
34
|
+
run: (params: Record<string, unknown>) => Promise<TResult>;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Supported cache backend types for {@link CacheConfig}.
|
|
38
|
+
*
|
|
39
|
+
* - `"cache-api"` — Edge-local Cloudflare Cache API (~0ms latency, per-edge-node).
|
|
40
|
+
* - `"kv"` — Global Cloudflare KV (10-50ms latency, eventually consistent).
|
|
41
|
+
*/
|
|
42
|
+
type CacheBackend = "cache-api" | "kv";
|
|
43
|
+
/**
|
|
44
|
+
* Configuration for the database cache layer.
|
|
45
|
+
*
|
|
46
|
+
* Controls how query results are cached and invalidated. Mutations automatically
|
|
47
|
+
* bump table version counters, causing subsequent reads to miss the cache.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* const db = createDb({
|
|
52
|
+
* d1: env.DB,
|
|
53
|
+
* schema,
|
|
54
|
+
* grants: resolvedGrants,
|
|
55
|
+
* user: currentUser,
|
|
56
|
+
* cache: {
|
|
57
|
+
* backend: "cache-api",
|
|
58
|
+
* ttl: "30s",
|
|
59
|
+
* staleWhileRevalidate: "5m",
|
|
60
|
+
* exclude: ["sessions"],
|
|
61
|
+
* },
|
|
62
|
+
* });
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
type CacheConfig = {
|
|
66
|
+
/** Which cache backend to use: edge-local Cache API or global KV. */
|
|
67
|
+
backend: CacheBackend;
|
|
68
|
+
/** KV namespace binding. Required when {@link backend} is `"kv"`. */
|
|
69
|
+
kv?: KVNamespace;
|
|
70
|
+
/** Default TTL for cached queries (e.g., `"30s"`, `"5m"`, `"1h"`). Defaults to `"60s"`. */
|
|
71
|
+
ttl?: string;
|
|
72
|
+
/** Stale-while-revalidate window (e.g., `"5m"`). Serves stale data while revalidating in the background. */
|
|
73
|
+
staleWhileRevalidate?: string;
|
|
74
|
+
/** Table names that should never be cached (e.g., `["sessions", "tokens"]`). */
|
|
75
|
+
exclude?: string[];
|
|
76
|
+
/** Observability hook called on cache hits. */
|
|
77
|
+
onHit?: (key: string, table: string) => void;
|
|
78
|
+
/** Observability hook called on cache misses. */
|
|
79
|
+
onMiss?: (key: string, table: string) => void;
|
|
80
|
+
/** Observability hook called when tables are invalidated by mutations. */
|
|
81
|
+
onInvalidate?: (tables: string[]) => void;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Per-query cache control options.
|
|
85
|
+
*
|
|
86
|
+
* Pass `false` to skip caching for a specific query, or an options object to
|
|
87
|
+
* override the default {@link CacheConfig} for that query.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* // Skip cache entirely
|
|
92
|
+
* db.query(posts).findMany({ cache: false });
|
|
93
|
+
*
|
|
94
|
+
* // Custom TTL and tags
|
|
95
|
+
* db.query(posts).findMany({ cache: { ttl: "5m", tags: ["user-posts"] } });
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
type QueryCacheOptions = false | {
|
|
99
|
+
/** Override the default TTL for this query (e.g., `"5m"`, `"1h"`). */
|
|
100
|
+
ttl?: string;
|
|
101
|
+
/** Override the default stale-while-revalidate window for this query. */
|
|
102
|
+
staleWhileRevalidate?: string;
|
|
103
|
+
/** Tags for targeted manual invalidation via `db.cache.invalidate({ tags })`. */
|
|
104
|
+
tags?: string[];
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Configuration for {@link createDb}.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```ts
|
|
111
|
+
* import { createDb } from "@cfast/db";
|
|
112
|
+
* import * as schema from "./schema";
|
|
113
|
+
*
|
|
114
|
+
* const db = createDb({
|
|
115
|
+
* d1: env.DB,
|
|
116
|
+
* schema,
|
|
117
|
+
* grants: resolvedGrants,
|
|
118
|
+
* user: { id: "user-123" },
|
|
119
|
+
* cache: { backend: "cache-api" },
|
|
120
|
+
* });
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
type DbConfig = {
|
|
124
|
+
/** The Cloudflare D1 database binding from `env.DB`. */
|
|
125
|
+
d1: D1Database;
|
|
126
|
+
/**
|
|
127
|
+
* Drizzle schema object. Must be `import * as schema` so that keys match
|
|
128
|
+
* table variable names (required by Drizzle's relational query API).
|
|
129
|
+
*/
|
|
130
|
+
schema: Record<string, DrizzleTable>;
|
|
131
|
+
/** Resolved permission grants for the current user's role, from `resolveGrants()`. */
|
|
132
|
+
grants: Grant[];
|
|
133
|
+
/**
|
|
134
|
+
* The current user, or `null` for anonymous access.
|
|
135
|
+
* When `null`, the `"anonymous"` role is used for permission checks.
|
|
136
|
+
*/
|
|
137
|
+
user: {
|
|
138
|
+
id: string;
|
|
139
|
+
} | null;
|
|
140
|
+
/** Cache configuration, or `false` to disable caching entirely. Defaults to `{ backend: "cache-api" }`. */
|
|
141
|
+
cache?: CacheConfig | false;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Options for `db.query(table).findMany()`.
|
|
145
|
+
*
|
|
146
|
+
* The `where` condition is AND'd with any permission-based WHERE clauses
|
|
147
|
+
* resolved from the user's grants.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* import { eq, desc } from "drizzle-orm";
|
|
152
|
+
*
|
|
153
|
+
* db.query(posts).findMany({
|
|
154
|
+
* columns: { id: true, title: true },
|
|
155
|
+
* where: eq(posts.category, "tech"),
|
|
156
|
+
* orderBy: desc(posts.createdAt),
|
|
157
|
+
* limit: 10,
|
|
158
|
+
* offset: 20,
|
|
159
|
+
* with: { comments: true },
|
|
160
|
+
* cache: { ttl: "5m", tags: ["posts"] },
|
|
161
|
+
* });
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
type FindManyOptions = {
|
|
165
|
+
/** Column selection (e.g., `{ id: true, title: true }`). Omit to select all columns. */
|
|
166
|
+
columns?: Record<string, boolean>;
|
|
167
|
+
/** User-supplied filter condition (AND'd with permission filters at `.run()` time). */
|
|
168
|
+
where?: unknown;
|
|
169
|
+
/** Ordering expression (e.g., `desc(posts.createdAt)`). */
|
|
170
|
+
orderBy?: unknown;
|
|
171
|
+
/** Maximum number of rows to return. */
|
|
172
|
+
limit?: number;
|
|
173
|
+
/** Number of rows to skip (for offset-based pagination). */
|
|
174
|
+
offset?: number;
|
|
175
|
+
/**
|
|
176
|
+
* Drizzle relational query includes (e.g., `{ comments: true }`).
|
|
177
|
+
*
|
|
178
|
+
* Note: Permission filters are only applied to the root table, not to joined relations.
|
|
179
|
+
*/
|
|
180
|
+
with?: Record<string, unknown>;
|
|
181
|
+
/** Per-query cache control. Pass `false` to skip caching, or an object to customize. */
|
|
182
|
+
cache?: QueryCacheOptions;
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Options for `db.query(table).findFirst()`.
|
|
186
|
+
*
|
|
187
|
+
* Same as {@link FindManyOptions} without `limit`/`offset` (returns the first match or `undefined`).
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```ts
|
|
191
|
+
* db.query(posts).findFirst({
|
|
192
|
+
* where: eq(posts.id, "abc-123"),
|
|
193
|
+
* });
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
type FindFirstOptions = Omit<FindManyOptions, "limit" | "offset">;
|
|
197
|
+
/**
|
|
198
|
+
* Parsed cursor-based pagination parameters from a request URL.
|
|
199
|
+
*
|
|
200
|
+
* Produced by {@link parseCursorParams}. Pass to `db.query(table).paginate()` for
|
|
201
|
+
* keyset pagination that avoids the offset performance cliff on large datasets.
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* ```ts
|
|
205
|
+
* const params = parseCursorParams(request, { defaultLimit: 20 });
|
|
206
|
+
* const page = await db.query(posts).paginate(params).run({});
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
type CursorParams = {
|
|
210
|
+
/** Discriminant for cursor-based pagination. Always `"cursor"`. */
|
|
211
|
+
type: "cursor";
|
|
212
|
+
/** The opaque cursor string from the previous page, or `null` for the first page. */
|
|
213
|
+
cursor: string | null;
|
|
214
|
+
/** Maximum items per page (clamped between 1 and `maxLimit`). */
|
|
215
|
+
limit: number;
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* Parsed offset-based pagination parameters from a request URL.
|
|
219
|
+
*
|
|
220
|
+
* Produced by {@link parseOffsetParams}. Pass to `db.query(table).paginate()` for
|
|
221
|
+
* traditional page-number-based pagination with total counts.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```ts
|
|
225
|
+
* const params = parseOffsetParams(request, { defaultLimit: 20 });
|
|
226
|
+
* const page = await db.query(posts).paginate(params).run({});
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
type OffsetParams = {
|
|
230
|
+
/** Discriminant for offset-based pagination. Always `"offset"`. */
|
|
231
|
+
type: "offset";
|
|
232
|
+
/** The 1-based page number. */
|
|
233
|
+
page: number;
|
|
234
|
+
/** Maximum items per page (clamped between 1 and `maxLimit`). */
|
|
235
|
+
limit: number;
|
|
236
|
+
};
|
|
237
|
+
/**
|
|
238
|
+
* Union of cursor and offset pagination parameters.
|
|
239
|
+
*
|
|
240
|
+
* Use the `type` discriminant to determine which pagination strategy is in use.
|
|
241
|
+
* Accepted by `db.query(table).paginate()`.
|
|
242
|
+
*/
|
|
243
|
+
type PaginateParams = CursorParams | OffsetParams;
|
|
244
|
+
/**
|
|
245
|
+
* A page of results from cursor-based pagination.
|
|
246
|
+
*
|
|
247
|
+
* Use `nextCursor` to fetch the next page. When `nextCursor` is `null`, there are no more pages.
|
|
248
|
+
*
|
|
249
|
+
* @typeParam T - The row type.
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* ```ts
|
|
253
|
+
* const page: CursorPage<Post> = await db.query(posts)
|
|
254
|
+
* .paginate({ type: "cursor", cursor: null, limit: 20 })
|
|
255
|
+
* .run({});
|
|
256
|
+
*
|
|
257
|
+
* if (page.nextCursor) {
|
|
258
|
+
* // Fetch next page with page.nextCursor
|
|
259
|
+
* }
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
type CursorPage<T> = {
|
|
263
|
+
/** The items on this page. */
|
|
264
|
+
items: T[];
|
|
265
|
+
/** Opaque cursor for the next page, or `null` if this is the last page. */
|
|
266
|
+
nextCursor: string | null;
|
|
267
|
+
};
|
|
268
|
+
/**
|
|
269
|
+
* A page of results from offset-based pagination.
|
|
270
|
+
*
|
|
271
|
+
* Includes total counts for rendering page navigation controls.
|
|
272
|
+
*
|
|
273
|
+
* @typeParam T - The row type.
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```ts
|
|
277
|
+
* const page: OffsetPage<Post> = await db.query(posts)
|
|
278
|
+
* .paginate({ type: "offset", page: 1, limit: 20 })
|
|
279
|
+
* .run({});
|
|
280
|
+
*
|
|
281
|
+
* console.log(`Page ${page.page} of ${page.totalPages} (${page.total} total)`);
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
type OffsetPage<T> = {
|
|
285
|
+
/** The items on this page. */
|
|
286
|
+
items: T[];
|
|
287
|
+
/** Total number of matching rows across all pages. */
|
|
288
|
+
total: number;
|
|
289
|
+
/** The current 1-based page number. */
|
|
290
|
+
page: number;
|
|
291
|
+
/** Total number of pages (computed as `Math.ceil(total / limit)`). */
|
|
292
|
+
totalPages: number;
|
|
293
|
+
};
|
|
294
|
+
/**
|
|
295
|
+
* Options for `db.query(table).paginate()`.
|
|
296
|
+
*
|
|
297
|
+
* Combines query filtering with pagination-specific settings. The actual pagination
|
|
298
|
+
* strategy (cursor vs. offset) is determined by the {@link PaginateParams} passed
|
|
299
|
+
* alongside these options.
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* ```ts
|
|
303
|
+
* db.query(posts).paginate(params, {
|
|
304
|
+
* where: eq(posts.published, true),
|
|
305
|
+
* orderBy: desc(posts.createdAt),
|
|
306
|
+
* cursorColumns: [posts.createdAt, posts.id],
|
|
307
|
+
* orderDirection: "desc",
|
|
308
|
+
* });
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
type PaginateOptions = {
|
|
312
|
+
/** Column selection (e.g., `{ id: true, title: true }`). Omit to select all columns. */
|
|
313
|
+
columns?: Record<string, boolean>;
|
|
314
|
+
/** User-supplied filter condition (AND'd with permission filters at `.run()` time). */
|
|
315
|
+
where?: unknown;
|
|
316
|
+
/** Ordering expression for offset pagination. Ignored for cursor pagination (uses `cursorColumns` instead). */
|
|
317
|
+
orderBy?: unknown;
|
|
318
|
+
/** Drizzle column references used for cursor-based ordering and comparison. */
|
|
319
|
+
cursorColumns?: unknown[];
|
|
320
|
+
/** Sort direction for cursor pagination. Defaults to `"desc"`. */
|
|
321
|
+
orderDirection?: "asc" | "desc";
|
|
322
|
+
/**
|
|
323
|
+
* Drizzle relational query includes (e.g., `{ comments: true }`).
|
|
324
|
+
*
|
|
325
|
+
* Note: Permission filters are only applied to the root table, not to joined relations.
|
|
326
|
+
*/
|
|
327
|
+
with?: Record<string, unknown>;
|
|
328
|
+
/** Per-query cache control. Pass `false` to skip caching, or an object to customize. */
|
|
329
|
+
cache?: QueryCacheOptions;
|
|
330
|
+
};
|
|
331
|
+
/**
|
|
332
|
+
* A permission-aware database instance bound to a specific user.
|
|
333
|
+
*
|
|
334
|
+
* Created by {@link createDb}. All query and mutation methods return lazy {@link Operation}
|
|
335
|
+
* objects that check permissions at `.run()` time. Create a new instance per request --
|
|
336
|
+
* sharing across requests would apply one user's permissions to another's queries.
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* ```ts
|
|
340
|
+
* // Read
|
|
341
|
+
* const posts = await db.query(postsTable).findMany().run({});
|
|
342
|
+
*
|
|
343
|
+
* // Write
|
|
344
|
+
* await db.insert(postsTable).values({ title: "Hello" }).run({});
|
|
345
|
+
*
|
|
346
|
+
* // Bypass permissions for system tasks
|
|
347
|
+
* await db.unsafe().delete(sessionsTable).where(expired).run({});
|
|
348
|
+
* ```
|
|
349
|
+
*/
|
|
350
|
+
type Db = {
|
|
351
|
+
/** Creates a {@link QueryBuilder} for reading rows from the given table. */
|
|
352
|
+
query: (table: DrizzleTable) => QueryBuilder;
|
|
353
|
+
/** Creates an {@link InsertBuilder} for inserting rows into the given table. */
|
|
354
|
+
insert: (table: DrizzleTable) => InsertBuilder;
|
|
355
|
+
/** Creates an {@link UpdateBuilder} for updating rows in the given table. */
|
|
356
|
+
update: (table: DrizzleTable) => UpdateBuilder;
|
|
357
|
+
/** Creates a {@link DeleteBuilder} for deleting rows from the given table. */
|
|
358
|
+
delete: (table: DrizzleTable) => DeleteBuilder;
|
|
359
|
+
/**
|
|
360
|
+
* Returns a new `Db` instance that skips all permission checks.
|
|
361
|
+
*
|
|
362
|
+
* Use for cron jobs, migrations, and system operations without an authenticated user.
|
|
363
|
+
* Every call site is greppable via `git grep '.unsafe()'`.
|
|
364
|
+
*/
|
|
365
|
+
unsafe: () => Db;
|
|
366
|
+
/**
|
|
367
|
+
* Groups multiple operations into a single {@link Operation} with merged, deduplicated permissions.
|
|
368
|
+
* Operations are executed sequentially (not via D1 native batch).
|
|
369
|
+
*/
|
|
370
|
+
batch: (operations: Operation<unknown>[]) => Operation<unknown[]>;
|
|
371
|
+
/** Cache control methods for manual invalidation. */
|
|
372
|
+
cache: {
|
|
373
|
+
/** Invalidate cached queries by tag names and/or table names. */
|
|
374
|
+
invalidate: (options: {
|
|
375
|
+
/** Tag names to invalidate (from {@link QueryCacheOptions} `tags`). */
|
|
376
|
+
tags?: string[];
|
|
377
|
+
/** Table names to invalidate (bumps their version counters). */
|
|
378
|
+
tables?: string[];
|
|
379
|
+
}) => Promise<void>;
|
|
380
|
+
};
|
|
381
|
+
};
|
|
382
|
+
/**
|
|
383
|
+
* Builder for read queries on a single table.
|
|
384
|
+
*
|
|
385
|
+
* Returned by `db.query(table)`. Provides `findMany`, `findFirst`, and `paginate` methods
|
|
386
|
+
* that each return an {@link Operation} with permission-aware execution.
|
|
387
|
+
*
|
|
388
|
+
* @example
|
|
389
|
+
* ```ts
|
|
390
|
+
* const builder = db.query(posts);
|
|
391
|
+
*
|
|
392
|
+
* // Fetch all visible posts
|
|
393
|
+
* const all = await builder.findMany().run({});
|
|
394
|
+
*
|
|
395
|
+
* // Fetch a single post
|
|
396
|
+
* const post = await builder.findFirst({ where: eq(posts.id, id) }).run({});
|
|
397
|
+
*
|
|
398
|
+
* // Paginate
|
|
399
|
+
* const page = await builder.paginate(params, { orderBy: desc(posts.createdAt) }).run({});
|
|
400
|
+
* ```
|
|
401
|
+
*/
|
|
402
|
+
type QueryBuilder = {
|
|
403
|
+
/** Returns an {@link Operation} that fetches multiple rows matching the given options. */
|
|
404
|
+
findMany: (options?: FindManyOptions) => Operation<unknown[]>;
|
|
405
|
+
/** Returns an {@link Operation} that fetches the first matching row, or `undefined` if none match. */
|
|
406
|
+
findFirst: (options?: FindFirstOptions) => Operation<unknown | undefined>;
|
|
407
|
+
/**
|
|
408
|
+
* Returns a paginated {@link Operation} using either cursor-based or offset-based strategy.
|
|
409
|
+
*
|
|
410
|
+
* The return type depends on the `params.type` discriminant: {@link CursorPage} for `"cursor"`,
|
|
411
|
+
* {@link OffsetPage} for `"offset"`.
|
|
412
|
+
*/
|
|
413
|
+
paginate: (params: CursorParams | OffsetParams, options?: PaginateOptions) => Operation<CursorPage<unknown>> | Operation<OffsetPage<unknown>>;
|
|
414
|
+
};
|
|
415
|
+
/**
|
|
416
|
+
* Builder for insert operations on a single table.
|
|
417
|
+
*
|
|
418
|
+
* Returned by `db.insert(table)`. Chain `.values()` to set the row data,
|
|
419
|
+
* then optionally `.returning()` to get the inserted row back.
|
|
420
|
+
*
|
|
421
|
+
* @example
|
|
422
|
+
* ```ts
|
|
423
|
+
* // Insert without returning
|
|
424
|
+
* await db.insert(posts).values({ title: "Hello", authorId: user.id }).run({});
|
|
425
|
+
*
|
|
426
|
+
* // Insert with returning
|
|
427
|
+
* const row = await db.insert(posts)
|
|
428
|
+
* .values({ title: "Hello", authorId: user.id })
|
|
429
|
+
* .returning()
|
|
430
|
+
* .run({});
|
|
431
|
+
* ```
|
|
432
|
+
*/
|
|
433
|
+
type InsertBuilder = {
|
|
434
|
+
/** Specifies the column values to insert, returning an {@link InsertReturningBuilder}. */
|
|
435
|
+
values: (values: Record<string, unknown>) => InsertReturningBuilder;
|
|
436
|
+
};
|
|
437
|
+
/**
|
|
438
|
+
* An insert {@link Operation} that optionally returns the inserted row via `.returning()`.
|
|
439
|
+
*
|
|
440
|
+
* Without `.returning()`, the operation resolves to `void`. With `.returning()`,
|
|
441
|
+
* it resolves to the full inserted row.
|
|
442
|
+
*/
|
|
443
|
+
type InsertReturningBuilder = Operation<void> & {
|
|
444
|
+
/** Chains `.returning()` to get the inserted row back from D1. */
|
|
445
|
+
returning: () => Operation<unknown>;
|
|
446
|
+
};
|
|
447
|
+
/**
|
|
448
|
+
* Builder for update operations on a single table.
|
|
449
|
+
*
|
|
450
|
+
* Returned by `db.update(table)`. Chain `.set()` to specify values, then `.where()`
|
|
451
|
+
* to add a condition, and optionally `.returning()` to get the updated row back.
|
|
452
|
+
*
|
|
453
|
+
* @example
|
|
454
|
+
* ```ts
|
|
455
|
+
* await db.update(posts)
|
|
456
|
+
* .set({ published: true })
|
|
457
|
+
* .where(eq(posts.id, "abc-123"))
|
|
458
|
+
* .run({});
|
|
459
|
+
* ```
|
|
460
|
+
*/
|
|
461
|
+
type UpdateBuilder = {
|
|
462
|
+
/** Specifies the column values to update, returning an {@link UpdateWhereBuilder}. */
|
|
463
|
+
set: (values: Record<string, unknown>) => UpdateWhereBuilder;
|
|
464
|
+
};
|
|
465
|
+
/**
|
|
466
|
+
* Intermediate builder requiring a WHERE condition before the update can execute.
|
|
467
|
+
*
|
|
468
|
+
* The WHERE condition is AND'd with any permission-based WHERE clauses from the user's grants.
|
|
469
|
+
*/
|
|
470
|
+
type UpdateWhereBuilder = {
|
|
471
|
+
/** Specifies the WHERE condition (AND'd with permission filters at `.run()` time). */
|
|
472
|
+
where: (condition: unknown) => UpdateReturningBuilder;
|
|
473
|
+
};
|
|
474
|
+
/**
|
|
475
|
+
* An update {@link Operation} that optionally returns the updated row via `.returning()`.
|
|
476
|
+
*
|
|
477
|
+
* Without `.returning()`, the operation resolves to `void`. With `.returning()`,
|
|
478
|
+
* it resolves to the full updated row.
|
|
479
|
+
*/
|
|
480
|
+
type UpdateReturningBuilder = Operation<void> & {
|
|
481
|
+
/** Chains `.returning()` to get the updated row back from D1. */
|
|
482
|
+
returning: () => Operation<unknown>;
|
|
483
|
+
};
|
|
484
|
+
/**
|
|
485
|
+
* Builder for delete operations on a single table.
|
|
486
|
+
*
|
|
487
|
+
* Returned by `db.delete(table)`. Chain `.where()` to add a condition,
|
|
488
|
+
* and optionally `.returning()` to get the deleted row back.
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* ```ts
|
|
492
|
+
* await db.delete(posts).where(eq(posts.id, "abc-123")).run({});
|
|
493
|
+
* ```
|
|
494
|
+
*/
|
|
495
|
+
type DeleteBuilder = {
|
|
496
|
+
/** Specifies the WHERE condition (AND'd with permission filters at `.run()` time). */
|
|
497
|
+
where: (condition: unknown) => DeleteReturningBuilder;
|
|
498
|
+
};
|
|
499
|
+
/**
|
|
500
|
+
* A delete {@link Operation} that optionally returns the deleted row via `.returning()`.
|
|
501
|
+
*
|
|
502
|
+
* Without `.returning()`, the operation resolves to `void`. With `.returning()`,
|
|
503
|
+
* it resolves to the full deleted row.
|
|
504
|
+
*/
|
|
505
|
+
type DeleteReturningBuilder = Operation<void> & {
|
|
506
|
+
/** Chains `.returning()` to get the deleted row back from D1. */
|
|
507
|
+
returning: () => Operation<unknown>;
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Creates a permission-aware database instance bound to the given user.
|
|
512
|
+
*
|
|
513
|
+
* Call this once per request, passing the authenticated user. The returned {@link Db} instance
|
|
514
|
+
* applies permission checks and WHERE clause injection on every {@link Operation}.
|
|
515
|
+
* Sharing a `Db` across requests would apply one user's permissions to another's queries.
|
|
516
|
+
*
|
|
517
|
+
* @param config - Database configuration including D1 binding, schema, grants, and user.
|
|
518
|
+
* @returns A {@link Db} instance with query, insert, update, delete, unsafe, batch, and cache methods.
|
|
519
|
+
*
|
|
520
|
+
* @example
|
|
521
|
+
* ```ts
|
|
522
|
+
* import { createDb } from "@cfast/db";
|
|
523
|
+
* import * as schema from "./schema";
|
|
524
|
+
*
|
|
525
|
+
* const db = createDb({
|
|
526
|
+
* d1: env.DB,
|
|
527
|
+
* schema,
|
|
528
|
+
* grants: resolvedGrants,
|
|
529
|
+
* user: currentUser,
|
|
530
|
+
* cache: { backend: "cache-api" },
|
|
531
|
+
* });
|
|
532
|
+
*
|
|
533
|
+
* // All operations check permissions at .run() time
|
|
534
|
+
* const posts = await db.query(postsTable).findMany().run({});
|
|
535
|
+
* ```
|
|
536
|
+
*/
|
|
537
|
+
declare function createDb(config: DbConfig): Db;
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* A function that executes a single sub-operation within a {@link compose} executor.
|
|
541
|
+
*
|
|
542
|
+
* Each `RunFn` corresponds to one of the operations passed to `compose()`,
|
|
543
|
+
* preserving the same positional order.
|
|
544
|
+
*/
|
|
545
|
+
type RunFn = (params: Record<string, unknown>) => Promise<unknown>;
|
|
546
|
+
/**
|
|
547
|
+
* Merges multiple {@link Operation | Operations} into a single operation with combined,
|
|
548
|
+
* deduplicated permissions and an executor function for controlling data flow.
|
|
549
|
+
*
|
|
550
|
+
* `compose()` itself does not check permissions -- it only merges them. Each sub-operation's
|
|
551
|
+
* `.run()` still performs its own permission check when the executor calls it. This enables
|
|
552
|
+
* data dependencies between operations (e.g., using an insert result's ID in an audit log).
|
|
553
|
+
*
|
|
554
|
+
* @typeParam TResult - The return type of the executor function.
|
|
555
|
+
* @param operations - The operations to compose. Their permissions are merged and deduplicated.
|
|
556
|
+
* @param executor - A function that receives a `run` function for each operation (in order).
|
|
557
|
+
* You control execution order, data flow between operations, and the return value.
|
|
558
|
+
* @returns A single {@link Operation} with combined permissions.
|
|
559
|
+
*
|
|
560
|
+
* @example
|
|
561
|
+
* ```ts
|
|
562
|
+
* import { compose } from "@cfast/db";
|
|
563
|
+
*
|
|
564
|
+
* const publishWorkflow = compose(
|
|
565
|
+
* [updatePost, insertAuditLog],
|
|
566
|
+
* async (doUpdate, doAudit) => {
|
|
567
|
+
* const updated = await doUpdate({});
|
|
568
|
+
* await doAudit({});
|
|
569
|
+
* return { published: true };
|
|
570
|
+
* },
|
|
571
|
+
* );
|
|
572
|
+
*
|
|
573
|
+
* // Inspect combined permissions
|
|
574
|
+
* publishWorkflow.permissions;
|
|
575
|
+
* // => [{ action: "update", table: "posts" }, { action: "create", table: "audit_logs" }]
|
|
576
|
+
*
|
|
577
|
+
* // Execute all sub-operations
|
|
578
|
+
* await publishWorkflow.run({});
|
|
579
|
+
* ```
|
|
580
|
+
*/
|
|
581
|
+
declare function compose<TResult>(operations: Operation<unknown>[], executor: (...runs: RunFn[]) => TResult | Promise<TResult>): Operation<TResult>;
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Options for controlling default and maximum pagination limits.
|
|
585
|
+
*
|
|
586
|
+
* Shared by {@link parseCursorParams} and {@link parseOffsetParams}.
|
|
587
|
+
*/
|
|
588
|
+
type PaginationOptions = {
|
|
589
|
+
/** Default number of items per page when `limit` is not in the URL. Defaults to `20`. */
|
|
590
|
+
defaultLimit?: number;
|
|
591
|
+
/** Maximum allowed `limit` value (URL values are clamped to this). Defaults to `100`. */
|
|
592
|
+
maxLimit?: number;
|
|
593
|
+
};
|
|
594
|
+
/**
|
|
595
|
+
* Parses cursor-based pagination parameters from a request URL's search params.
|
|
596
|
+
*
|
|
597
|
+
* Reads `cursor` and `limit` from the URL query string. Clamps `limit` between 1 and `maxLimit`.
|
|
598
|
+
* Returns a {@link CursorParams} object ready to pass to `db.query(table).paginate()`.
|
|
599
|
+
*
|
|
600
|
+
* @param request - The incoming HTTP request whose URL contains `?cursor=...&limit=...`.
|
|
601
|
+
* @param options - Optional defaults and limits for pagination.
|
|
602
|
+
* @returns Parsed {@link CursorParams} with `type`, `cursor`, and `limit`.
|
|
603
|
+
*
|
|
604
|
+
* @example
|
|
605
|
+
* ```ts
|
|
606
|
+
* import { parseCursorParams } from "@cfast/db";
|
|
607
|
+
*
|
|
608
|
+
* const params = parseCursorParams(request, { defaultLimit: 20, maxLimit: 100 });
|
|
609
|
+
* const page = await db.query(posts).paginate(params).run({});
|
|
610
|
+
* // page => { items: [...], nextCursor: "..." | null }
|
|
611
|
+
* ```
|
|
612
|
+
*/
|
|
613
|
+
declare function parseCursorParams(request: Request, options?: PaginationOptions): CursorParams;
|
|
614
|
+
/**
|
|
615
|
+
* Parses offset-based pagination parameters from a request URL's search params.
|
|
616
|
+
*
|
|
617
|
+
* Reads `page` and `limit` from the URL query string. Clamps `limit` between 1 and `maxLimit`,
|
|
618
|
+
* and ensures `page` is at least 1. Returns an {@link OffsetParams} object ready to pass
|
|
619
|
+
* to `db.query(table).paginate()`.
|
|
620
|
+
*
|
|
621
|
+
* @param request - The incoming HTTP request whose URL contains `?page=...&limit=...`.
|
|
622
|
+
* @param options - Optional defaults and limits for pagination.
|
|
623
|
+
* @returns Parsed {@link OffsetParams} with `type`, `page`, and `limit`.
|
|
624
|
+
*
|
|
625
|
+
* @example
|
|
626
|
+
* ```ts
|
|
627
|
+
* import { parseOffsetParams } from "@cfast/db";
|
|
628
|
+
*
|
|
629
|
+
* const params = parseOffsetParams(request, { defaultLimit: 20 });
|
|
630
|
+
* const page = await db.query(posts).paginate(params).run({});
|
|
631
|
+
* // page => { items: [...], total: 100, page: 1, totalPages: 5 }
|
|
632
|
+
* ```
|
|
633
|
+
*/
|
|
634
|
+
declare function parseOffsetParams(request: Request, options?: PaginationOptions): OffsetParams;
|
|
635
|
+
|
|
636
|
+
export { type CacheBackend, type CacheConfig, type CursorPage, type CursorParams, type Db, type DbConfig, type DeleteBuilder, type DeleteReturningBuilder, type FindFirstOptions, type FindManyOptions, type InsertBuilder, type InsertReturningBuilder, type OffsetPage, type OffsetParams, type Operation, type PaginateOptions, type PaginateParams, type QueryBuilder, type QueryCacheOptions, type UpdateBuilder, type UpdateReturningBuilder, type UpdateWhereBuilder, compose, createDb, parseCursorParams, parseOffsetParams };
|