@cfast/db 0.4.1 → 0.5.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.d.ts +85 -532
- package/dist/index.js +71 -35
- package/llms.txt +70 -28
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,458 +1,105 @@
|
|
|
1
1
|
import { Grant, DrizzleTable, PermissionDescriptor } from '@cfast/permissions';
|
|
2
|
+
import { Table, TablesRelationalConfig, ExtractTablesWithRelations, TableRelationalConfig, BuildQueryResult } from 'drizzle-orm';
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
-
* Extracts the row type from a Drizzle table reference.
|
|
5
|
-
*
|
|
6
|
-
* Drizzle tables expose `$inferSelect` (the row shape returned by `SELECT *`).
|
|
7
|
-
* `InferRow<typeof posts>` resolves to `{ id: string; title: string; ... }`.
|
|
8
|
-
*
|
|
9
|
-
* Falls back to `Record<string, unknown>` for opaque `DrizzleTable` references
|
|
10
|
-
* (e.g. when callers do not specify a concrete table type generic).
|
|
11
|
-
*
|
|
12
|
-
* @typeParam TTable - The Drizzle table type (e.g. `typeof posts`).
|
|
13
|
-
*/
|
|
14
4
|
type InferRow<TTable> = TTable extends {
|
|
15
5
|
$inferSelect: infer R;
|
|
16
6
|
} ? R : Record<string, unknown>;
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
*
|
|
25
|
-
* @typeParam TResult - The type of the result returned by `.run()`.
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* ```ts
|
|
29
|
-
* const op = db.query(posts).findMany();
|
|
30
|
-
*
|
|
31
|
-
* // Inspect permissions without executing
|
|
32
|
-
* console.log(op.permissions);
|
|
33
|
-
* // => [{ action: "read", table: "posts" }]
|
|
34
|
-
*
|
|
35
|
-
* // Execute with permission checks
|
|
36
|
-
* const rows = await op.run();
|
|
37
|
-
* ```
|
|
38
|
-
*/
|
|
7
|
+
/** @internal */
|
|
8
|
+
type FindTableKeyByName<TSchema extends TablesRelationalConfig, TTableName extends string> = {
|
|
9
|
+
[K in keyof TSchema]: TSchema[K]["dbName"] extends TTableName ? K : never;
|
|
10
|
+
}[keyof TSchema];
|
|
11
|
+
/** @internal */
|
|
12
|
+
type LookupTableConfig<TFullSchema extends Record<string, unknown>, TTable> = TTable extends Table<infer TTableConfig> ? FindTableKeyByName<ExtractTablesWithRelations<TFullSchema>, TTableConfig["name"]> extends infer TKey extends keyof ExtractTablesWithRelations<TFullSchema> ? ExtractTablesWithRelations<TFullSchema>[TKey] : never : never;
|
|
13
|
+
type InferQueryResult<TFullSchema extends Record<string, unknown>, TTable, TConfig> = [TFullSchema] extends [Record<string, never>] ? InferRow<TTable> : LookupTableConfig<TFullSchema, TTable> extends infer TTableConfig extends TableRelationalConfig ? BuildQueryResult<ExtractTablesWithRelations<TFullSchema>, TTableConfig, TConfig extends Record<string, unknown> ? TConfig : true> : InferRow<TTable>;
|
|
39
14
|
type Operation<TResult> = {
|
|
40
|
-
/** Structural permission requirements. Available immediately without execution. */
|
|
41
15
|
permissions: PermissionDescriptor[];
|
|
42
|
-
/**
|
|
43
|
-
* Checks permissions, applies permission WHERE clauses, executes the query via Drizzle,
|
|
44
|
-
* and returns the result. Throws `ForbiddenError` if the user's role lacks a required grant.
|
|
45
|
-
*
|
|
46
|
-
* @param params - Optional placeholder values for `sql.placeholder()` calls.
|
|
47
|
-
* Omit when your query does not use placeholders — the default is `{}`.
|
|
48
|
-
*/
|
|
49
16
|
run: (params?: Record<string, unknown>) => Promise<TResult>;
|
|
50
17
|
};
|
|
51
|
-
/**
|
|
52
|
-
* Supported cache backend types for {@link CacheConfig}.
|
|
53
|
-
*
|
|
54
|
-
* - `"cache-api"` — Edge-local Cloudflare Cache API (~0ms latency, per-edge-node).
|
|
55
|
-
* - `"kv"` — Global Cloudflare KV (10-50ms latency, eventually consistent).
|
|
56
|
-
*/
|
|
57
18
|
type CacheBackend = "cache-api" | "kv";
|
|
58
|
-
/**
|
|
59
|
-
* Configuration for the database cache layer.
|
|
60
|
-
*
|
|
61
|
-
* Controls how query results are cached and invalidated. Mutations automatically
|
|
62
|
-
* bump table version counters, causing subsequent reads to miss the cache.
|
|
63
|
-
*
|
|
64
|
-
* @example
|
|
65
|
-
* ```ts
|
|
66
|
-
* const db = createDb({
|
|
67
|
-
* d1: env.DB,
|
|
68
|
-
* schema,
|
|
69
|
-
* grants: resolvedGrants,
|
|
70
|
-
* user: currentUser,
|
|
71
|
-
* cache: {
|
|
72
|
-
* backend: "cache-api",
|
|
73
|
-
* ttl: "30s",
|
|
74
|
-
* staleWhileRevalidate: "5m",
|
|
75
|
-
* exclude: ["sessions"],
|
|
76
|
-
* },
|
|
77
|
-
* });
|
|
78
|
-
* ```
|
|
79
|
-
*/
|
|
80
19
|
type CacheConfig = {
|
|
81
|
-
/** Which cache backend to use: edge-local Cache API or global KV. */
|
|
82
20
|
backend: CacheBackend;
|
|
83
|
-
/** KV namespace binding. Required when {@link backend} is `"kv"`. */
|
|
84
21
|
kv?: KVNamespace;
|
|
85
|
-
/** Default TTL for cached queries (e.g., `"30s"`, `"5m"`, `"1h"`). Defaults to `"60s"`. */
|
|
86
22
|
ttl?: string;
|
|
87
|
-
/** Stale-while-revalidate window (e.g., `"5m"`). Serves stale data while revalidating in the background. */
|
|
88
23
|
staleWhileRevalidate?: string;
|
|
89
|
-
/** Table names that should never be cached (e.g., `["sessions", "tokens"]`). */
|
|
90
24
|
exclude?: string[];
|
|
91
|
-
/** Observability hook called on cache hits. */
|
|
92
25
|
onHit?: (key: string, table: string) => void;
|
|
93
|
-
/** Observability hook called on cache misses. */
|
|
94
26
|
onMiss?: (key: string, table: string) => void;
|
|
95
|
-
/** Observability hook called when tables are invalidated by mutations. */
|
|
96
27
|
onInvalidate?: (tables: string[]) => void;
|
|
97
28
|
};
|
|
98
|
-
/**
|
|
99
|
-
* Per-query cache control options.
|
|
100
|
-
*
|
|
101
|
-
* Pass `false` to skip caching for a specific query, or an options object to
|
|
102
|
-
* override the default {@link CacheConfig} for that query.
|
|
103
|
-
*
|
|
104
|
-
* @example
|
|
105
|
-
* ```ts
|
|
106
|
-
* // Skip cache entirely
|
|
107
|
-
* db.query(posts).findMany({ cache: false });
|
|
108
|
-
*
|
|
109
|
-
* // Custom TTL and tags
|
|
110
|
-
* db.query(posts).findMany({ cache: { ttl: "5m", tags: ["user-posts"] } });
|
|
111
|
-
* ```
|
|
112
|
-
*/
|
|
113
29
|
type QueryCacheOptions = false | {
|
|
114
|
-
/** Override the default TTL for this query (e.g., `"5m"`, `"1h"`). */
|
|
115
30
|
ttl?: string;
|
|
116
|
-
/** Override the default stale-while-revalidate window for this query. */
|
|
117
31
|
staleWhileRevalidate?: string;
|
|
118
|
-
/** Tags for targeted manual invalidation via `db.cache.invalidate({ tags })`. */
|
|
119
32
|
tags?: string[];
|
|
120
33
|
};
|
|
121
|
-
|
|
122
|
-
* Configuration for {@link createDb}.
|
|
123
|
-
*
|
|
124
|
-
* @example
|
|
125
|
-
* ```ts
|
|
126
|
-
* import { createDb } from "@cfast/db";
|
|
127
|
-
* import * as schema from "./schema";
|
|
128
|
-
*
|
|
129
|
-
* const db = createDb({
|
|
130
|
-
* d1: env.DB,
|
|
131
|
-
* schema,
|
|
132
|
-
* grants: resolvedGrants,
|
|
133
|
-
* user: { id: "user-123" },
|
|
134
|
-
* cache: { backend: "cache-api" },
|
|
135
|
-
* });
|
|
136
|
-
* ```
|
|
137
|
-
*/
|
|
138
|
-
type DbConfig = {
|
|
139
|
-
/** The Cloudflare D1 database binding from `env.DB`. */
|
|
34
|
+
type DbConfig<TSchema extends Record<string, unknown> = Record<string, unknown>> = {
|
|
140
35
|
d1: D1Database;
|
|
141
|
-
|
|
142
|
-
* Drizzle schema object. Must be `import * as schema` so that keys match
|
|
143
|
-
* table variable names (required by Drizzle's relational query API).
|
|
144
|
-
*
|
|
145
|
-
* Typed as `Record<string, unknown>` so callers can pass `import * as schema`
|
|
146
|
-
* directly without casting -- Drizzle schemas typically include `Relations`
|
|
147
|
-
* exports alongside tables, and the `@cfast/db` runtime ignores any non-table
|
|
148
|
-
* entries when looking up tables by key.
|
|
149
|
-
*/
|
|
150
|
-
schema: Record<string, unknown>;
|
|
151
|
-
/** Resolved permission grants for the current user's role, from `resolveGrants()`. */
|
|
36
|
+
schema: TSchema;
|
|
152
37
|
grants: Grant[];
|
|
153
|
-
/**
|
|
154
|
-
* The current user, or `null` for anonymous access.
|
|
155
|
-
* When `null`, the `"anonymous"` role is used for permission checks.
|
|
156
|
-
*/
|
|
157
38
|
user: {
|
|
158
39
|
id: string;
|
|
159
40
|
} | null;
|
|
160
|
-
/** Cache configuration, or `false` to disable caching entirely. Defaults to `{ backend: "cache-api" }`. */
|
|
161
41
|
cache?: CacheConfig | false;
|
|
162
42
|
};
|
|
163
|
-
/**
|
|
164
|
-
* Options for `db.query(table).findMany()`.
|
|
165
|
-
*
|
|
166
|
-
* The `where` condition is AND'd with any permission-based WHERE clauses
|
|
167
|
-
* resolved from the user's grants.
|
|
168
|
-
*
|
|
169
|
-
* @example
|
|
170
|
-
* ```ts
|
|
171
|
-
* import { eq, desc } from "drizzle-orm";
|
|
172
|
-
*
|
|
173
|
-
* db.query(posts).findMany({
|
|
174
|
-
* columns: { id: true, title: true },
|
|
175
|
-
* where: eq(posts.category, "tech"),
|
|
176
|
-
* orderBy: desc(posts.createdAt),
|
|
177
|
-
* limit: 10,
|
|
178
|
-
* offset: 20,
|
|
179
|
-
* with: { comments: true },
|
|
180
|
-
* cache: { ttl: "5m", tags: ["posts"] },
|
|
181
|
-
* });
|
|
182
|
-
* ```
|
|
183
|
-
*/
|
|
184
43
|
type FindManyOptions = {
|
|
185
|
-
/** Column selection (e.g., `{ id: true, title: true }`). Omit to select all columns. */
|
|
186
44
|
columns?: Record<string, boolean>;
|
|
187
|
-
/** User-supplied filter condition (AND'd with permission filters at `.run()` time). */
|
|
188
45
|
where?: unknown;
|
|
189
|
-
/** Ordering expression (e.g., `desc(posts.createdAt)`). */
|
|
190
46
|
orderBy?: unknown;
|
|
191
|
-
/** Maximum number of rows to return. */
|
|
192
47
|
limit?: number;
|
|
193
|
-
/** Number of rows to skip (for offset-based pagination). */
|
|
194
48
|
offset?: number;
|
|
195
|
-
/**
|
|
196
|
-
* Drizzle relational query includes (e.g., `{ comments: true }`).
|
|
197
|
-
*
|
|
198
|
-
* Note: Permission filters are only applied to the root table, not to joined relations.
|
|
199
|
-
*/
|
|
200
49
|
with?: Record<string, unknown>;
|
|
201
|
-
/** Per-query cache control. Pass `false` to skip caching, or an object to customize. */
|
|
202
50
|
cache?: QueryCacheOptions;
|
|
203
51
|
};
|
|
204
|
-
/**
|
|
205
|
-
* Options for `db.query(table).findFirst()`.
|
|
206
|
-
*
|
|
207
|
-
* Same as {@link FindManyOptions} without `limit`/`offset` (returns the first match or `undefined`).
|
|
208
|
-
*
|
|
209
|
-
* @example
|
|
210
|
-
* ```ts
|
|
211
|
-
* db.query(posts).findFirst({
|
|
212
|
-
* where: eq(posts.id, "abc-123"),
|
|
213
|
-
* });
|
|
214
|
-
* ```
|
|
215
|
-
*/
|
|
216
52
|
type FindFirstOptions = Omit<FindManyOptions, "limit" | "offset">;
|
|
217
|
-
/**
|
|
218
|
-
* Parsed cursor-based pagination parameters from a request URL.
|
|
219
|
-
*
|
|
220
|
-
* Produced by {@link parseCursorParams}. Pass to `db.query(table).paginate()` for
|
|
221
|
-
* keyset pagination that avoids the offset performance cliff on large datasets.
|
|
222
|
-
*
|
|
223
|
-
* @example
|
|
224
|
-
* ```ts
|
|
225
|
-
* const params = parseCursorParams(request, { defaultLimit: 20 });
|
|
226
|
-
* const page = await db.query(posts).paginate(params).run();
|
|
227
|
-
* ```
|
|
228
|
-
*/
|
|
229
53
|
type CursorParams = {
|
|
230
|
-
/** Discriminant for cursor-based pagination. Always `"cursor"`. */
|
|
231
54
|
type: "cursor";
|
|
232
|
-
/** The opaque cursor string from the previous page, or `null` for the first page. */
|
|
233
55
|
cursor: string | null;
|
|
234
|
-
/** Maximum items per page (clamped between 1 and `maxLimit`). */
|
|
235
56
|
limit: number;
|
|
236
57
|
};
|
|
237
|
-
/**
|
|
238
|
-
* Parsed offset-based pagination parameters from a request URL.
|
|
239
|
-
*
|
|
240
|
-
* Produced by {@link parseOffsetParams}. Pass to `db.query(table).paginate()` for
|
|
241
|
-
* traditional page-number-based pagination with total counts.
|
|
242
|
-
*
|
|
243
|
-
* @example
|
|
244
|
-
* ```ts
|
|
245
|
-
* const params = parseOffsetParams(request, { defaultLimit: 20 });
|
|
246
|
-
* const page = await db.query(posts).paginate(params).run();
|
|
247
|
-
* ```
|
|
248
|
-
*/
|
|
249
58
|
type OffsetParams = {
|
|
250
|
-
/** Discriminant for offset-based pagination. Always `"offset"`. */
|
|
251
59
|
type: "offset";
|
|
252
|
-
/** The 1-based page number. */
|
|
253
60
|
page: number;
|
|
254
|
-
/** Maximum items per page (clamped between 1 and `maxLimit`). */
|
|
255
61
|
limit: number;
|
|
256
62
|
};
|
|
257
|
-
/**
|
|
258
|
-
* Union of cursor and offset pagination parameters.
|
|
259
|
-
*
|
|
260
|
-
* Use the `type` discriminant to determine which pagination strategy is in use.
|
|
261
|
-
* Accepted by `db.query(table).paginate()`.
|
|
262
|
-
*/
|
|
263
63
|
type PaginateParams = CursorParams | OffsetParams;
|
|
264
|
-
/**
|
|
265
|
-
* A page of results from cursor-based pagination.
|
|
266
|
-
*
|
|
267
|
-
* Use `nextCursor` to fetch the next page. When `nextCursor` is `null`, there are no more pages.
|
|
268
|
-
*
|
|
269
|
-
* @typeParam T - The row type.
|
|
270
|
-
*
|
|
271
|
-
* @example
|
|
272
|
-
* ```ts
|
|
273
|
-
* const page: CursorPage<Post> = await db.query(posts)
|
|
274
|
-
* .paginate({ type: "cursor", cursor: null, limit: 20 })
|
|
275
|
-
* .run();
|
|
276
|
-
*
|
|
277
|
-
* if (page.nextCursor) {
|
|
278
|
-
* // Fetch next page with page.nextCursor
|
|
279
|
-
* }
|
|
280
|
-
* ```
|
|
281
|
-
*/
|
|
282
64
|
type CursorPage<T> = {
|
|
283
|
-
/** The items on this page. */
|
|
284
65
|
items: T[];
|
|
285
|
-
/** Opaque cursor for the next page, or `null` if this is the last page. */
|
|
286
66
|
nextCursor: string | null;
|
|
287
67
|
};
|
|
288
|
-
/**
|
|
289
|
-
* A page of results from offset-based pagination.
|
|
290
|
-
*
|
|
291
|
-
* Includes total counts for rendering page navigation controls.
|
|
292
|
-
*
|
|
293
|
-
* @typeParam T - The row type.
|
|
294
|
-
*
|
|
295
|
-
* @example
|
|
296
|
-
* ```ts
|
|
297
|
-
* const page: OffsetPage<Post> = await db.query(posts)
|
|
298
|
-
* .paginate({ type: "offset", page: 1, limit: 20 })
|
|
299
|
-
* .run();
|
|
300
|
-
*
|
|
301
|
-
* console.log(`Page ${page.page} of ${page.totalPages} (${page.total} total)`);
|
|
302
|
-
* ```
|
|
303
|
-
*/
|
|
304
68
|
type OffsetPage<T> = {
|
|
305
|
-
/** The items on this page. */
|
|
306
69
|
items: T[];
|
|
307
|
-
/** Total number of matching rows across all pages. */
|
|
308
70
|
total: number;
|
|
309
|
-
/** The current 1-based page number. */
|
|
310
71
|
page: number;
|
|
311
|
-
/** Total number of pages (computed as `Math.ceil(total / limit)`). */
|
|
312
72
|
totalPages: number;
|
|
313
73
|
};
|
|
314
|
-
/**
|
|
315
|
-
* Options for `db.query(table).paginate()`.
|
|
316
|
-
*
|
|
317
|
-
* Combines query filtering with pagination-specific settings. The actual pagination
|
|
318
|
-
* strategy (cursor vs. offset) is determined by the {@link PaginateParams} passed
|
|
319
|
-
* alongside these options.
|
|
320
|
-
*
|
|
321
|
-
* @example
|
|
322
|
-
* ```ts
|
|
323
|
-
* db.query(posts).paginate(params, {
|
|
324
|
-
* where: eq(posts.published, true),
|
|
325
|
-
* orderBy: desc(posts.createdAt),
|
|
326
|
-
* cursorColumns: [posts.createdAt, posts.id],
|
|
327
|
-
* orderDirection: "desc",
|
|
328
|
-
* });
|
|
329
|
-
* ```
|
|
330
|
-
*/
|
|
331
74
|
type PaginateOptions = {
|
|
332
|
-
/** Column selection (e.g., `{ id: true, title: true }`). Omit to select all columns. */
|
|
333
75
|
columns?: Record<string, boolean>;
|
|
334
|
-
/** User-supplied filter condition (AND'd with permission filters at `.run()` time). */
|
|
335
76
|
where?: unknown;
|
|
336
|
-
/** Ordering expression for offset pagination. Ignored for cursor pagination (uses `cursorColumns` instead). */
|
|
337
77
|
orderBy?: unknown;
|
|
338
|
-
/** Drizzle column references used for cursor-based ordering and comparison. */
|
|
339
78
|
cursorColumns?: unknown[];
|
|
340
|
-
/** Sort direction for cursor pagination. Defaults to `"desc"`. */
|
|
341
79
|
orderDirection?: "asc" | "desc";
|
|
342
|
-
/**
|
|
343
|
-
* Drizzle relational query includes (e.g., `{ comments: true }`).
|
|
344
|
-
*
|
|
345
|
-
* Note: Permission filters are only applied to the root table, not to joined relations.
|
|
346
|
-
*/
|
|
347
80
|
with?: Record<string, unknown>;
|
|
348
|
-
/** Per-query cache control. Pass `false` to skip caching, or an object to customize. */
|
|
349
81
|
cache?: QueryCacheOptions;
|
|
350
82
|
};
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
* - `batch()` / nested `transaction()` calls are flattened into the parent's
|
|
361
|
-
* pending queue automatically, so the plain mutate methods are all you need.
|
|
362
|
-
* - `cache` invalidation is driven by the single commit at the end of the
|
|
363
|
-
* transaction, not per-sub-op.
|
|
364
|
-
*
|
|
365
|
-
* Writes (`tx.insert` / `tx.update` / `tx.delete`) are recorded and flushed as
|
|
366
|
-
* an atomic batch when the callback returns. Reads (`tx.query`) execute
|
|
367
|
-
* eagerly so the caller can branch on their results.
|
|
368
|
-
*/
|
|
369
|
-
type Tx = {
|
|
370
|
-
/** Same as {@link Db.query}: reads execute eagerly against the underlying db. */
|
|
371
|
-
query: <TTable extends DrizzleTable>(table: TTable) => QueryBuilder<TTable>;
|
|
372
|
-
/**
|
|
373
|
-
* Same as {@link Db.insert}: the returned Operation's `.run()` records the
|
|
374
|
-
* insert into the transaction's pending queue. `.returning().run()` records
|
|
375
|
-
* the insert and returns a promise that resolves AFTER the batch flushes.
|
|
376
|
-
*/
|
|
83
|
+
type TransactionResult<T> = {
|
|
84
|
+
result: T;
|
|
85
|
+
meta: {
|
|
86
|
+
changes: number;
|
|
87
|
+
writeResults: D1Result[];
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
type Tx<TSchema extends Record<string, unknown> = Record<string, never>> = {
|
|
91
|
+
query: <TTable extends DrizzleTable>(table: TTable) => QueryBuilder<TTable, TSchema>;
|
|
377
92
|
insert: <TTable extends DrizzleTable>(table: TTable) => InsertBuilder<TTable>;
|
|
378
|
-
/**
|
|
379
|
-
* Same as {@link Db.update}: the returned Operation's `.run()` records the
|
|
380
|
-
* update into the transaction's pending queue. Use relative SQL with a
|
|
381
|
-
* WHERE guard (e.g. `set({ stock: sql\`stock - ${qty}\` }).where(gte(...))`)
|
|
382
|
-
* for concurrency-safe read-modify-write.
|
|
383
|
-
*/
|
|
384
93
|
update: <TTable extends DrizzleTable>(table: TTable) => UpdateBuilder<TTable>;
|
|
385
|
-
/** Same as {@link Db.delete}: records the delete into the transaction's pending queue. */
|
|
386
94
|
delete: <TTable extends DrizzleTable>(table: TTable) => DeleteBuilder<TTable>;
|
|
387
|
-
|
|
388
|
-
* Nested transaction. Flattens into the parent's pending queue so every
|
|
389
|
-
* write still commits in a single atomic batch. The nested callback's
|
|
390
|
-
* return value is propagated as usual; any error thrown aborts the
|
|
391
|
-
* entire outer transaction.
|
|
392
|
-
*
|
|
393
|
-
* Use this when a helper function wants to "own" its own tx scope but
|
|
394
|
-
* the caller already started a transaction — the helper can call
|
|
395
|
-
* `tx.transaction(...)` unconditionally and Just Work.
|
|
396
|
-
*/
|
|
397
|
-
transaction: <T>(callback: (tx: Tx) => Promise<T>) => Promise<T>;
|
|
95
|
+
transaction: <T>(callback: (tx: Tx<TSchema>) => Promise<T>) => Promise<T>;
|
|
398
96
|
};
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
*
|
|
402
|
-
* Created by {@link createDb}. All query and mutation methods return lazy {@link Operation}
|
|
403
|
-
* objects that check permissions at `.run()` time. Create a new instance per request --
|
|
404
|
-
* sharing across requests would apply one user's permissions to another's queries.
|
|
405
|
-
*
|
|
406
|
-
* @example
|
|
407
|
-
* ```ts
|
|
408
|
-
* // Read
|
|
409
|
-
* const posts = await db.query(postsTable).findMany().run();
|
|
410
|
-
*
|
|
411
|
-
* // Write
|
|
412
|
-
* await db.insert(postsTable).values({ title: "Hello" }).run();
|
|
413
|
-
*
|
|
414
|
-
* // Bypass permissions for system tasks
|
|
415
|
-
* await db.unsafe().delete(sessionsTable).where(expired).run();
|
|
416
|
-
* ```
|
|
417
|
-
*/
|
|
418
|
-
type Db = {
|
|
419
|
-
/**
|
|
420
|
-
* Creates a {@link QueryBuilder} for reading rows from the given table.
|
|
421
|
-
*
|
|
422
|
-
* The builder is generic over `TTable`, so `findMany`/`findFirst` return rows
|
|
423
|
-
* typed via `InferRow<TTable>` -- callers don't need to cast to `as any`.
|
|
424
|
-
*/
|
|
425
|
-
query: <TTable extends DrizzleTable>(table: TTable) => QueryBuilder<TTable>;
|
|
426
|
-
/** Creates an {@link InsertBuilder} for inserting rows into the given table. */
|
|
97
|
+
type Db<TSchema extends Record<string, unknown> = Record<string, never>> = {
|
|
98
|
+
query: <TTable extends DrizzleTable>(table: TTable) => QueryBuilder<TTable, TSchema>;
|
|
427
99
|
insert: <TTable extends DrizzleTable>(table: TTable) => InsertBuilder<TTable>;
|
|
428
|
-
/** Creates an {@link UpdateBuilder} for updating rows in the given table. */
|
|
429
100
|
update: <TTable extends DrizzleTable>(table: TTable) => UpdateBuilder<TTable>;
|
|
430
|
-
/** Creates a {@link DeleteBuilder} for deleting rows from the given table. */
|
|
431
101
|
delete: <TTable extends DrizzleTable>(table: TTable) => DeleteBuilder<TTable>;
|
|
432
|
-
|
|
433
|
-
* Returns a new `Db` instance that skips all permission checks.
|
|
434
|
-
*
|
|
435
|
-
* Use for cron jobs, migrations, and system operations without an authenticated user.
|
|
436
|
-
* Every call site is greppable via `git grep '.unsafe()'`.
|
|
437
|
-
*/
|
|
438
|
-
unsafe: () => Db;
|
|
439
|
-
/**
|
|
440
|
-
* Groups multiple operations into a single {@link Operation} with merged, deduplicated permissions.
|
|
441
|
-
*
|
|
442
|
-
* When every operation was produced by `db.insert/update/delete`, the batch is
|
|
443
|
-
* executed via D1's native `batch()` API, which is **atomic** -- if any
|
|
444
|
-
* statement fails, the entire batch is rolled back. This is the recommended
|
|
445
|
-
* way to perform multi-step mutations that need transactional safety, such as
|
|
446
|
-
* decrementing stock across multiple products during checkout.
|
|
447
|
-
*
|
|
448
|
-
* Permissions for every sub-operation are checked **upfront**: if the user
|
|
449
|
-
* lacks any required grant, the batch throws before any SQL is issued.
|
|
450
|
-
*
|
|
451
|
-
* Operations that don't carry the internal batchable hook (for example, ops
|
|
452
|
-
* produced by `compose()` executors) cause the batch to fall back to
|
|
453
|
-
* sequential execution. This preserves backward compatibility for non-trivial
|
|
454
|
-
* compositions but loses the atomicity guarantee.
|
|
455
|
-
*/
|
|
102
|
+
unsafe: () => Db<TSchema>;
|
|
456
103
|
batch: (operations: Operation<unknown>[]) => Operation<unknown[]>;
|
|
457
104
|
/**
|
|
458
105
|
* Runs a callback inside a transaction with atomic commit-or-rollback semantics.
|
|
@@ -488,10 +135,11 @@ type Db = {
|
|
|
488
135
|
* @param callback - The transaction body. Receives a `tx` handle with
|
|
489
136
|
* `query`/`insert`/`update`/`delete` methods (no `unsafe`, `batch`, or
|
|
490
137
|
* `cache` — those are intentionally off-limits inside a transaction).
|
|
491
|
-
* @returns
|
|
492
|
-
*
|
|
138
|
+
* @returns A {@link TransactionResult} containing the callback's return value
|
|
139
|
+
* and transaction metadata (`meta.changes`, `meta.writeResults`), or rejects
|
|
140
|
+
* with whatever the callback threw (after rolling back pending writes).
|
|
493
141
|
*/
|
|
494
|
-
transaction: <T>(callback: (tx: Tx) => Promise<T>) => Promise<T
|
|
142
|
+
transaction: <T>(callback: (tx: Tx<TSchema>) => Promise<T>) => Promise<TransactionResult<T>>;
|
|
495
143
|
/** Cache control methods for manual invalidation. */
|
|
496
144
|
cache: {
|
|
497
145
|
/** Invalidate cached queries by tag names and/or table names. */
|
|
@@ -526,169 +174,30 @@ type Db = {
|
|
|
526
174
|
*/
|
|
527
175
|
clearLookupCache: () => void;
|
|
528
176
|
};
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
* Returned by `db.query(table)`. Provides `findMany`, `findFirst`, and `paginate` methods
|
|
533
|
-
* that each return an {@link Operation} with permission-aware execution.
|
|
534
|
-
*
|
|
535
|
-
* @example
|
|
536
|
-
* ```ts
|
|
537
|
-
* const builder = db.query(posts);
|
|
538
|
-
*
|
|
539
|
-
* // Fetch all visible posts
|
|
540
|
-
* const all = await builder.findMany().run();
|
|
541
|
-
*
|
|
542
|
-
* // Fetch a single post
|
|
543
|
-
* const post = await builder.findFirst({ where: eq(posts.id, id) }).run();
|
|
544
|
-
*
|
|
545
|
-
* // Paginate
|
|
546
|
-
* const page = await builder.paginate(params, { orderBy: desc(posts.createdAt) }).run();
|
|
547
|
-
* ```
|
|
548
|
-
*/
|
|
549
|
-
type QueryBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
550
|
-
/**
|
|
551
|
-
* Returns an {@link Operation} that fetches multiple rows matching the given options.
|
|
552
|
-
*
|
|
553
|
-
* The result is typed via `InferRow<TTable>`, so callers get IntelliSense on
|
|
554
|
-
* `(row) => row.title` without any cast.
|
|
555
|
-
*
|
|
556
|
-
* **Relations escape hatch (#158):** when you pass `with: { relation: true }`,
|
|
557
|
-
* Drizzle's relational query builder embeds the joined rows into the result,
|
|
558
|
-
* but `@cfast/db` cannot statically infer that shape from the
|
|
559
|
-
* `Record<string, unknown>` schema we accept here. Override the row type via
|
|
560
|
-
* the optional `TRow` generic to claim the shape you know the query will
|
|
561
|
-
* produce, instead of having to `as any` the result downstream:
|
|
562
|
-
*
|
|
563
|
-
* ```ts
|
|
564
|
-
* type RecipeWithIngredients = Recipe & { ingredients: Ingredient[] };
|
|
565
|
-
* const recipes = await db
|
|
566
|
-
* .query(recipesTable)
|
|
567
|
-
* .findMany<RecipeWithIngredients>({ with: { ingredients: true } })
|
|
568
|
-
* .run();
|
|
569
|
-
* // recipes is RecipeWithIngredients[], no cast needed
|
|
570
|
-
* ```
|
|
571
|
-
*/
|
|
572
|
-
findMany: <TRow = InferRow<TTable>>(options?: FindManyOptions) => Operation<TRow[]>;
|
|
573
|
-
/**
|
|
574
|
-
* Returns an {@link Operation} that fetches the first matching row, or `undefined`
|
|
575
|
-
* if none match. The row type is propagated from `TTable`.
|
|
576
|
-
*
|
|
577
|
-
* Same `TRow` generic escape hatch as {@link findMany} for `with`-relation
|
|
578
|
-
* shapes that the framework can't infer from the runtime-typed schema.
|
|
579
|
-
*
|
|
580
|
-
* ```ts
|
|
581
|
-
* type RecipeWithIngredients = Recipe & { ingredients: Ingredient[] };
|
|
582
|
-
* const recipe = await db
|
|
583
|
-
* .query(recipesTable)
|
|
584
|
-
* .findFirst<RecipeWithIngredients>({ where: eq(recipes.id, id), with: { ingredients: true } })
|
|
585
|
-
* .run();
|
|
586
|
-
* // recipe is RecipeWithIngredients | undefined
|
|
587
|
-
* ```
|
|
588
|
-
*/
|
|
589
|
-
findFirst: <TRow = InferRow<TTable>>(options?: FindFirstOptions) => Operation<TRow | undefined>;
|
|
590
|
-
/**
|
|
591
|
-
* Returns a paginated {@link Operation} using either cursor-based or offset-based strategy.
|
|
592
|
-
*
|
|
593
|
-
* The return type depends on the `params.type` discriminant: {@link CursorPage} for `"cursor"`,
|
|
594
|
-
* {@link OffsetPage} for `"offset"`. Each page's items are typed as `InferRow<TTable>` by
|
|
595
|
-
* default; pass an explicit `TRow` to claim a wider shape (e.g. when using
|
|
596
|
-
* `with: { ... }` to embed related rows).
|
|
597
|
-
*/
|
|
177
|
+
type QueryBuilder<TTable extends DrizzleTable = DrizzleTable, TSchema extends Record<string, unknown> = Record<string, never>> = {
|
|
178
|
+
findMany: <TConfig extends FindManyOptions = Record<string, never>, TRow = InferQueryResult<TSchema, TTable, TConfig>>(options?: TConfig) => Operation<TRow[]>;
|
|
179
|
+
findFirst: <TConfig extends FindFirstOptions = Record<string, never>, TRow = InferQueryResult<TSchema, TTable, TConfig>>(options?: TConfig) => Operation<TRow | undefined>;
|
|
598
180
|
paginate: <TRow = InferRow<TTable>>(params: CursorParams | OffsetParams, options?: PaginateOptions) => Operation<CursorPage<TRow>> | Operation<OffsetPage<TRow>>;
|
|
599
181
|
};
|
|
600
|
-
/**
|
|
601
|
-
* Builder for insert operations on a single table.
|
|
602
|
-
*
|
|
603
|
-
* Returned by `db.insert(table)`. Chain `.values()` to set the row data,
|
|
604
|
-
* then optionally `.returning()` to get the inserted row back.
|
|
605
|
-
*
|
|
606
|
-
* @example
|
|
607
|
-
* ```ts
|
|
608
|
-
* // Insert without returning
|
|
609
|
-
* await db.insert(posts).values({ title: "Hello", authorId: user.id }).run();
|
|
610
|
-
*
|
|
611
|
-
* // Insert with returning
|
|
612
|
-
* const row = await db.insert(posts)
|
|
613
|
-
* .values({ title: "Hello", authorId: user.id })
|
|
614
|
-
* .returning()
|
|
615
|
-
* .run();
|
|
616
|
-
* ```
|
|
617
|
-
*/
|
|
618
182
|
type InsertBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
619
|
-
/** Specifies the column values to insert, returning an {@link InsertReturningBuilder}. */
|
|
620
183
|
values: (values: Record<string, unknown>) => InsertReturningBuilder<TTable>;
|
|
621
184
|
};
|
|
622
|
-
/**
|
|
623
|
-
* An insert {@link Operation} that optionally returns the inserted row via `.returning()`.
|
|
624
|
-
*
|
|
625
|
-
* Without `.returning()`, the operation resolves to `void`. With `.returning()`,
|
|
626
|
-
* it resolves to the full inserted row, typed as `InferRow<TTable>`.
|
|
627
|
-
*/
|
|
628
185
|
type InsertReturningBuilder<TTable extends DrizzleTable = DrizzleTable> = Operation<void> & {
|
|
629
|
-
/** Chains `.returning()` to get the inserted row back from D1. */
|
|
630
186
|
returning: () => Operation<InferRow<TTable>>;
|
|
631
187
|
};
|
|
632
|
-
/**
|
|
633
|
-
* Builder for update operations on a single table.
|
|
634
|
-
*
|
|
635
|
-
* Returned by `db.update(table)`. Chain `.set()` to specify values, then `.where()`
|
|
636
|
-
* to add a condition, and optionally `.returning()` to get the updated row back.
|
|
637
|
-
*
|
|
638
|
-
* @example
|
|
639
|
-
* ```ts
|
|
640
|
-
* await db.update(posts)
|
|
641
|
-
* .set({ published: true })
|
|
642
|
-
* .where(eq(posts.id, "abc-123"))
|
|
643
|
-
* .run();
|
|
644
|
-
* ```
|
|
645
|
-
*/
|
|
646
188
|
type UpdateBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
647
|
-
/** Specifies the column values to update, returning an {@link UpdateWhereBuilder}. */
|
|
648
189
|
set: (values: Record<string, unknown>) => UpdateWhereBuilder<TTable>;
|
|
649
190
|
};
|
|
650
|
-
/**
|
|
651
|
-
* Intermediate builder requiring a WHERE condition before the update can execute.
|
|
652
|
-
*
|
|
653
|
-
* The WHERE condition is AND'd with any permission-based WHERE clauses from the user's grants.
|
|
654
|
-
*/
|
|
655
191
|
type UpdateWhereBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
656
|
-
/** Specifies the WHERE condition (AND'd with permission filters at `.run()` time). */
|
|
657
192
|
where: (condition: unknown) => UpdateReturningBuilder<TTable>;
|
|
658
193
|
};
|
|
659
|
-
/**
|
|
660
|
-
* An update {@link Operation} that optionally returns the updated row via `.returning()`.
|
|
661
|
-
*
|
|
662
|
-
* Without `.returning()`, the operation resolves to `void`. With `.returning()`,
|
|
663
|
-
* it resolves to the full updated row, typed as `InferRow<TTable>`.
|
|
664
|
-
*/
|
|
665
194
|
type UpdateReturningBuilder<TTable extends DrizzleTable = DrizzleTable> = Operation<void> & {
|
|
666
|
-
/** Chains `.returning()` to get the updated row back from D1. */
|
|
667
195
|
returning: () => Operation<InferRow<TTable>>;
|
|
668
196
|
};
|
|
669
|
-
/**
|
|
670
|
-
* Builder for delete operations on a single table.
|
|
671
|
-
*
|
|
672
|
-
* Returned by `db.delete(table)`. Chain `.where()` to add a condition,
|
|
673
|
-
* and optionally `.returning()` to get the deleted row back.
|
|
674
|
-
*
|
|
675
|
-
* @example
|
|
676
|
-
* ```ts
|
|
677
|
-
* await db.delete(posts).where(eq(posts.id, "abc-123")).run();
|
|
678
|
-
* ```
|
|
679
|
-
*/
|
|
680
197
|
type DeleteBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
681
|
-
/** Specifies the WHERE condition (AND'd with permission filters at `.run()` time). */
|
|
682
198
|
where: (condition: unknown) => DeleteReturningBuilder<TTable>;
|
|
683
199
|
};
|
|
684
|
-
/**
|
|
685
|
-
* A delete {@link Operation} that optionally returns the deleted row via `.returning()`.
|
|
686
|
-
*
|
|
687
|
-
* Without `.returning()`, the operation resolves to `void`. With `.returning()`,
|
|
688
|
-
* it resolves to the full deleted row, typed as `InferRow<TTable>`.
|
|
689
|
-
*/
|
|
690
200
|
type DeleteReturningBuilder<TTable extends DrizzleTable = DrizzleTable> = Operation<void> & {
|
|
691
|
-
/** Chains `.returning()` to get the deleted row back from D1. */
|
|
692
201
|
returning: () => Operation<InferRow<TTable>>;
|
|
693
202
|
};
|
|
694
203
|
|
|
@@ -719,7 +228,7 @@ type DeleteReturningBuilder<TTable extends DrizzleTable = DrizzleTable> = Operat
|
|
|
719
228
|
* const posts = await db.query(postsTable).findMany().run();
|
|
720
229
|
* ```
|
|
721
230
|
*/
|
|
722
|
-
declare function createDb(config: DbConfig): Db
|
|
231
|
+
declare function createDb<TSchema extends Record<string, unknown>>(config: DbConfig<TSchema>): Db<TSchema>;
|
|
723
232
|
/**
|
|
724
233
|
* App-level db factory configuration. The "constants" — D1 binding,
|
|
725
234
|
* Drizzle schema, cache config — that don't change between requests.
|
|
@@ -727,7 +236,7 @@ declare function createDb(config: DbConfig): Db;
|
|
|
727
236
|
* Returned by {@link createAppDb}, called per-request with the user's
|
|
728
237
|
* resolved grants and identity to produce a fresh permission-aware {@link Db}.
|
|
729
238
|
*/
|
|
730
|
-
type AppDbConfig = {
|
|
239
|
+
type AppDbConfig<TSchema extends Record<string, unknown> = Record<string, unknown>> = {
|
|
731
240
|
/**
|
|
732
241
|
* The Cloudflare D1 database binding. Pass either the binding directly
|
|
733
242
|
* (for tests, where the mock D1 is in scope at module load) or a
|
|
@@ -743,7 +252,7 @@ type AppDbConfig = {
|
|
|
743
252
|
* Drizzle schema. Same shape as {@link DbConfig.schema} -- pass
|
|
744
253
|
* `import * as schema from "./schema"`.
|
|
745
254
|
*/
|
|
746
|
-
schema:
|
|
255
|
+
schema: TSchema;
|
|
747
256
|
/**
|
|
748
257
|
* Cache configuration shared across every per-request `Db`. Defaults to
|
|
749
258
|
* `{ backend: "cache-api" }` to match `createDb`. Pass `false` to opt out.
|
|
@@ -759,9 +268,9 @@ type AppDbConfig = {
|
|
|
759
268
|
* `@cfast/core`'s `dbPlugin`, route loaders) can pass the factory around
|
|
760
269
|
* without re-deriving its signature.
|
|
761
270
|
*/
|
|
762
|
-
type AppDbFactory = (grants: Grant[], user: {
|
|
271
|
+
type AppDbFactory<TSchema extends Record<string, unknown> = Record<string, never>> = (grants: Grant[], user: {
|
|
763
272
|
id: string;
|
|
764
|
-
} | null) => Db
|
|
273
|
+
} | null) => Db<TSchema>;
|
|
765
274
|
/**
|
|
766
275
|
* Consolidates the three near-identical `createDb` factories that every
|
|
767
276
|
* cfast app used to define (#149):
|
|
@@ -811,7 +320,7 @@ type AppDbFactory = (grants: Grant[], user: {
|
|
|
811
320
|
* };
|
|
812
321
|
* ```
|
|
813
322
|
*/
|
|
814
|
-
declare function createAppDb(config: AppDbConfig): AppDbFactory
|
|
323
|
+
declare function createAppDb<TSchema extends Record<string, unknown>>(config: AppDbConfig<TSchema>): AppDbFactory<TSchema>;
|
|
815
324
|
|
|
816
325
|
/**
|
|
817
326
|
* A function that executes a single sub-operation within a {@link compose} executor.
|
|
@@ -1022,10 +531,6 @@ declare function parseCursorParams(request: Request, options?: PaginationOptions
|
|
|
1022
531
|
*/
|
|
1023
532
|
declare function parseOffsetParams(request: Request, options?: PaginationOptions): OffsetParams;
|
|
1024
533
|
|
|
1025
|
-
/**
|
|
1026
|
-
* Error thrown when a transaction is misused (e.g. an op is recorded after the
|
|
1027
|
-
* transaction has already committed or aborted).
|
|
1028
|
-
*/
|
|
1029
534
|
declare class TransactionError extends Error {
|
|
1030
535
|
constructor(message: string);
|
|
1031
536
|
}
|
|
@@ -1132,6 +637,54 @@ type Seed = {
|
|
|
1132
637
|
*/
|
|
1133
638
|
declare function defineSeed(config: SeedConfig): Seed;
|
|
1134
639
|
|
|
640
|
+
/**
|
|
641
|
+
* Recursively converts Date fields in a value to ISO 8601 strings for
|
|
642
|
+
* JSON serialization.
|
|
643
|
+
*
|
|
644
|
+
* React Router loaders must return JSON-serializable data. Date objects
|
|
645
|
+
* are not JSON-serializable by default -- `JSON.stringify(new Date())`
|
|
646
|
+
* calls `Date.prototype.toJSON()` which produces an ISO string, but the
|
|
647
|
+
* resulting value on the client is a string, not a Date. This helper
|
|
648
|
+
* makes the conversion explicit and type-safe so every loader doesn't
|
|
649
|
+
* need to manually call `.toISOString()` on every date field.
|
|
650
|
+
*
|
|
651
|
+
* Works on:
|
|
652
|
+
* - Plain objects (shallow and nested)
|
|
653
|
+
* - Arrays of objects
|
|
654
|
+
* - Single Date values
|
|
655
|
+
* - Nested arrays and objects of arbitrary depth
|
|
656
|
+
*
|
|
657
|
+
* Non-Date primitives (string, number, boolean, null, undefined) pass
|
|
658
|
+
* through unchanged. The function is a no-op for values that don't
|
|
659
|
+
* contain Date instances.
|
|
660
|
+
*
|
|
661
|
+
* @param value - The value to convert. Typically a query result row or
|
|
662
|
+
* array of rows from `db.query(...).findMany().run()`.
|
|
663
|
+
* @returns A new value with every Date replaced by its ISO string.
|
|
664
|
+
*
|
|
665
|
+
* @example
|
|
666
|
+
* ```ts
|
|
667
|
+
* import { toJSON } from "@cfast/db";
|
|
668
|
+
*
|
|
669
|
+
* export async function loader({ context }) {
|
|
670
|
+
* const db = createDb({ ... });
|
|
671
|
+
* const posts = await db.query(postsTable).findMany().run();
|
|
672
|
+
* return { posts: toJSON(posts) };
|
|
673
|
+
* }
|
|
674
|
+
* ```
|
|
675
|
+
*/
|
|
676
|
+
declare function toJSON<T>(value: T): DateToString<T>;
|
|
677
|
+
/**
|
|
678
|
+
* Type-level utility that converts Date fields to string in a type.
|
|
679
|
+
*
|
|
680
|
+
* Maps `{ createdAt: Date; title: string }` to
|
|
681
|
+
* `{ createdAt: string; title: string }` so the return type of
|
|
682
|
+
* `toJSON(row)` accurately reflects the runtime shape.
|
|
683
|
+
*/
|
|
684
|
+
type DateToString<T> = T extends Date ? string : T extends Array<infer U> ? DateToString<U>[] : T extends Record<string, unknown> ? {
|
|
685
|
+
[K in keyof T]: DateToString<T[K]>;
|
|
686
|
+
} : T;
|
|
687
|
+
|
|
1135
688
|
/**
|
|
1136
689
|
* Per-request cache that holds the resolved `with` lookup map for each grant.
|
|
1137
690
|
*
|
|
@@ -1180,4 +733,4 @@ declare function createLookupCache(): LookupCache;
|
|
|
1180
733
|
*/
|
|
1181
734
|
declare function runWithLookupCache<T>(fn: () => T, cache?: LookupCache): T;
|
|
1182
735
|
|
|
1183
|
-
export { type AppDbConfig, type AppDbFactory, type CacheBackend, type CacheConfig, type CursorPage, type CursorParams, type Db, type DbConfig, type DeleteBuilder, type DeleteReturningBuilder, type FindFirstOptions, type FindManyOptions, type InferRow, type InsertBuilder, type InsertReturningBuilder, type LookupCache, type OffsetPage, type OffsetParams, type Operation, type PaginateOptions, type PaginateParams, type QueryBuilder, type QueryCacheOptions, type Seed, type SeedConfig, type SeedEntry, TransactionError, type Tx, type UpdateBuilder, type UpdateReturningBuilder, type UpdateWhereBuilder, compose, composeSequential, composeSequentialCallback, createAppDb, createDb, createLookupCache, defineSeed, parseCursorParams, parseOffsetParams, runWithLookupCache };
|
|
736
|
+
export { type AppDbConfig, type AppDbFactory, type CacheBackend, type CacheConfig, type CursorPage, type CursorParams, type DateToString, type Db, type DbConfig, type DeleteBuilder, type DeleteReturningBuilder, type FindFirstOptions, type FindManyOptions, type InferQueryResult, type InferRow, type InsertBuilder, type InsertReturningBuilder, type LookupCache, type OffsetPage, type OffsetParams, type Operation, type PaginateOptions, type PaginateParams, type QueryBuilder, type QueryCacheOptions, type Seed, type SeedConfig, type SeedEntry, TransactionError, type TransactionResult, type Tx, type UpdateBuilder, type UpdateReturningBuilder, type UpdateWhereBuilder, compose, composeSequential, composeSequentialCallback, createAppDb, createDb, createLookupCache, defineSeed, parseCursorParams, parseOffsetParams, runWithLookupCache, toJSON };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/create-db.ts
|
|
2
|
-
import { drizzle as
|
|
2
|
+
import { drizzle as drizzle4 } from "drizzle-orm/d1";
|
|
3
3
|
|
|
4
4
|
// src/query-builder.ts
|
|
5
5
|
import { count } from "drizzle-orm";
|
|
@@ -675,6 +675,7 @@ function createCacheManager(config) {
|
|
|
675
675
|
}
|
|
676
676
|
|
|
677
677
|
// src/transaction.ts
|
|
678
|
+
import { drizzle as drizzle3 } from "drizzle-orm/d1";
|
|
678
679
|
var TransactionError = class extends Error {
|
|
679
680
|
constructor(message) {
|
|
680
681
|
super(message);
|
|
@@ -684,14 +685,10 @@ var TransactionError = class extends Error {
|
|
|
684
685
|
function wrapWriteOperation(ctx, op) {
|
|
685
686
|
const record = (toQueue) => {
|
|
686
687
|
if (ctx.closed) {
|
|
687
|
-
throw new TransactionError(
|
|
688
|
-
"Cannot run an operation after the transaction has committed or aborted."
|
|
689
|
-
);
|
|
688
|
+
throw new TransactionError("Cannot run an operation after the transaction has committed or aborted.");
|
|
690
689
|
}
|
|
691
690
|
if (ctx.aborted) {
|
|
692
|
-
throw new TransactionError(
|
|
693
|
-
"Transaction has been aborted; no further operations may run."
|
|
694
|
-
);
|
|
691
|
+
throw new TransactionError("Transaction has been aborted; no further operations may run.");
|
|
695
692
|
}
|
|
696
693
|
ctx.pending.push({ op: toQueue });
|
|
697
694
|
};
|
|
@@ -718,9 +715,7 @@ function wrapWriteOperation(ctx, op) {
|
|
|
718
715
|
}
|
|
719
716
|
function createTxHandle(db, ctx) {
|
|
720
717
|
return {
|
|
721
|
-
query: (table) =>
|
|
722
|
-
return db.query(table);
|
|
723
|
-
},
|
|
718
|
+
query: (table) => db.query(table),
|
|
724
719
|
insert: (table) => {
|
|
725
720
|
const realBuilder = db.insert(table);
|
|
726
721
|
return {
|
|
@@ -753,13 +748,11 @@ function createTxHandle(db, ctx) {
|
|
|
753
748
|
}
|
|
754
749
|
};
|
|
755
750
|
},
|
|
756
|
-
transaction: (callback) =>
|
|
757
|
-
return runTransaction(db, callback, ctx);
|
|
758
|
-
}
|
|
751
|
+
transaction: (callback) => runTransaction(db, callback, ctx)
|
|
759
752
|
};
|
|
760
753
|
}
|
|
761
|
-
async function
|
|
762
|
-
if (pending.length === 0) return;
|
|
754
|
+
async function flushWritesDirect(config, isUnsafe, pending) {
|
|
755
|
+
if (pending.length === 0) return { changes: 0, writeResults: [] };
|
|
763
756
|
const ops = pending.map((p) => p.op);
|
|
764
757
|
for (const op of ops) {
|
|
765
758
|
if (getBatchable(op) === void 0) {
|
|
@@ -768,24 +761,39 @@ async function flushWrites(db, pending) {
|
|
|
768
761
|
);
|
|
769
762
|
}
|
|
770
763
|
}
|
|
771
|
-
|
|
764
|
+
const allPermissions = deduplicateDescriptors(ops.flatMap((op) => op.permissions));
|
|
765
|
+
if (!isUnsafe) {
|
|
766
|
+
checkOperationPermissions(config.grants, allPermissions);
|
|
767
|
+
}
|
|
768
|
+
const batchables = ops.map((op) => getBatchable(op));
|
|
769
|
+
await Promise.all(batchables.map((b) => b.prepare?.() ?? Promise.resolve()));
|
|
770
|
+
const sharedDb = drizzle3(config.d1, { schema: config.schema });
|
|
771
|
+
const items = batchables.map((b) => b.build(sharedDb));
|
|
772
|
+
const batchResults = await sharedDb.batch(
|
|
773
|
+
items
|
|
774
|
+
);
|
|
775
|
+
const writeResults = batchResults.map((r) => {
|
|
776
|
+
if (r && typeof r === "object" && "meta" in r) {
|
|
777
|
+
return r;
|
|
778
|
+
}
|
|
779
|
+
return { results: [], success: true, meta: { changes: 0 } };
|
|
780
|
+
});
|
|
781
|
+
const changes = writeResults.reduce(
|
|
782
|
+
(sum, r) => sum + (r?.meta?.changes ?? 0),
|
|
783
|
+
0
|
|
784
|
+
);
|
|
785
|
+
return { changes, writeResults };
|
|
772
786
|
}
|
|
773
|
-
async function runTransaction(db, callback,
|
|
774
|
-
if (
|
|
787
|
+
async function runTransaction(db, callback, parentCtxOrConfig, isUnsafe) {
|
|
788
|
+
if (parentCtxOrConfig && "pending" in parentCtxOrConfig) {
|
|
789
|
+
const parentCtx = parentCtxOrConfig;
|
|
775
790
|
if (parentCtx.closed || parentCtx.aborted) {
|
|
776
|
-
throw new TransactionError(
|
|
777
|
-
"Cannot start a nested transaction: parent has already committed or aborted."
|
|
778
|
-
);
|
|
791
|
+
throw new TransactionError("Cannot start a nested transaction: parent has already committed or aborted.");
|
|
779
792
|
}
|
|
780
|
-
|
|
781
|
-
return callback(nestedTx);
|
|
793
|
+
return callback(createTxHandle(db, parentCtx));
|
|
782
794
|
}
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
closed: false,
|
|
786
|
-
aborted: false,
|
|
787
|
-
nested: false
|
|
788
|
-
};
|
|
795
|
+
const config = parentCtxOrConfig;
|
|
796
|
+
const ctx = { pending: [], closed: false, aborted: false, nested: false };
|
|
789
797
|
const tx = createTxHandle(db, ctx);
|
|
790
798
|
let result;
|
|
791
799
|
try {
|
|
@@ -796,15 +804,21 @@ async function runTransaction(db, callback, parentCtx) {
|
|
|
796
804
|
ctx.pending.length = 0;
|
|
797
805
|
throw err;
|
|
798
806
|
}
|
|
807
|
+
let flushResult;
|
|
799
808
|
try {
|
|
800
|
-
|
|
809
|
+
if (config) {
|
|
810
|
+
flushResult = await flushWritesDirect(config, isUnsafe ?? false, ctx.pending);
|
|
811
|
+
} else {
|
|
812
|
+
if (ctx.pending.length > 0) await db.batch(ctx.pending.map((p) => p.op)).run({});
|
|
813
|
+
flushResult = { changes: 0, writeResults: [] };
|
|
814
|
+
}
|
|
801
815
|
ctx.closed = true;
|
|
802
816
|
} catch (err) {
|
|
803
817
|
ctx.aborted = true;
|
|
804
818
|
ctx.closed = true;
|
|
805
819
|
throw err;
|
|
806
820
|
}
|
|
807
|
-
return result;
|
|
821
|
+
return { result, meta: { changes: flushResult.changes, writeResults: flushResult.writeResults } };
|
|
808
822
|
}
|
|
809
823
|
|
|
810
824
|
// src/create-db.ts
|
|
@@ -895,7 +909,7 @@ function buildDb(config, isUnsafe, lookupCache) {
|
|
|
895
909
|
const batchables = operations.map((op) => getBatchable(op));
|
|
896
910
|
const everyOpBatchable = batchables.every((b) => b !== void 0);
|
|
897
911
|
if (everyOpBatchable) {
|
|
898
|
-
const sharedDb =
|
|
912
|
+
const sharedDb = drizzle4(config.d1, { schema: config.schema });
|
|
899
913
|
await Promise.all(
|
|
900
914
|
batchables.map((b) => b.prepare?.() ?? Promise.resolve())
|
|
901
915
|
);
|
|
@@ -921,7 +935,7 @@ function buildDb(config, isUnsafe, lookupCache) {
|
|
|
921
935
|
};
|
|
922
936
|
},
|
|
923
937
|
transaction(callback) {
|
|
924
|
-
return runTransaction(db, callback);
|
|
938
|
+
return runTransaction(db, callback, { d1: config.d1, schema: config.schema, grants: config.grants }, isUnsafe);
|
|
925
939
|
},
|
|
926
940
|
cache: {
|
|
927
941
|
async invalidate(options) {
|
|
@@ -1054,7 +1068,7 @@ function createTrackingDb(real, perms) {
|
|
|
1054
1068
|
insert: trackingDb.insert,
|
|
1055
1069
|
update: trackingDb.update,
|
|
1056
1070
|
delete: trackingDb.delete,
|
|
1057
|
-
transaction: (cb) => trackingDb.transaction(cb)
|
|
1071
|
+
transaction: (cb) => trackingDb.transaction(cb).then((txr) => txr.result)
|
|
1058
1072
|
};
|
|
1059
1073
|
try {
|
|
1060
1074
|
await callback(trackingTx);
|
|
@@ -1121,6 +1135,27 @@ function defineSeed(config) {
|
|
|
1121
1135
|
}
|
|
1122
1136
|
};
|
|
1123
1137
|
}
|
|
1138
|
+
|
|
1139
|
+
// src/json.ts
|
|
1140
|
+
function toJSON(value) {
|
|
1141
|
+
return convertDates(value);
|
|
1142
|
+
}
|
|
1143
|
+
function convertDates(value) {
|
|
1144
|
+
if (value instanceof Date) {
|
|
1145
|
+
return value.toISOString();
|
|
1146
|
+
}
|
|
1147
|
+
if (Array.isArray(value)) {
|
|
1148
|
+
return value.map(convertDates);
|
|
1149
|
+
}
|
|
1150
|
+
if (value !== null && typeof value === "object") {
|
|
1151
|
+
const result = {};
|
|
1152
|
+
for (const [key, val] of Object.entries(value)) {
|
|
1153
|
+
result[key] = convertDates(val);
|
|
1154
|
+
}
|
|
1155
|
+
return result;
|
|
1156
|
+
}
|
|
1157
|
+
return value;
|
|
1158
|
+
}
|
|
1124
1159
|
export {
|
|
1125
1160
|
TransactionError,
|
|
1126
1161
|
compose,
|
|
@@ -1132,5 +1167,6 @@ export {
|
|
|
1132
1167
|
defineSeed,
|
|
1133
1168
|
parseCursorParams,
|
|
1134
1169
|
parseOffsetParams,
|
|
1135
|
-
runWithLookupCache
|
|
1170
|
+
runWithLookupCache,
|
|
1171
|
+
toJSON
|
|
1136
1172
|
};
|
package/llms.txt
CHANGED
|
@@ -58,32 +58,48 @@ IntelliSense on `(row) => row.title` without any cast.
|
|
|
58
58
|
FindManyOptions: `{ columns?, where?, orderBy?, limit?, offset?, with?, cache? }`
|
|
59
59
|
FindFirstOptions: same without `limit`/`offset`.
|
|
60
60
|
|
|
61
|
-
####
|
|
61
|
+
#### Auto-inferred `.with()` relations (#240)
|
|
62
62
|
|
|
63
63
|
When you pass `with: { relation: true }`, Drizzle's relational query builder
|
|
64
|
-
embeds the joined rows into the result. `@cfast/db
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
embeds the joined rows into the result. As of `@cfast/db@0.5`, the result
|
|
65
|
+
type is **automatically inferred** from the schema -- no type cast needed.
|
|
66
|
+
`createDb` captures the full schema type via a generic, and `findMany`/
|
|
67
|
+
`findFirst` thread the `with` config through Drizzle's `BuildQueryResult`
|
|
68
|
+
to compute the exact result shape including nested relations.
|
|
69
69
|
|
|
70
70
|
```typescript
|
|
71
|
-
|
|
71
|
+
import { createDb } from "@cfast/db";
|
|
72
|
+
import * as schema from "./schema"; // includes relations()
|
|
73
|
+
|
|
74
|
+
const db = createDb({ d1, schema, grants, user });
|
|
72
75
|
|
|
76
|
+
// Auto-inferred -- no cast, no manual generic
|
|
73
77
|
const recipes = await db
|
|
74
|
-
.query(recipesTable)
|
|
75
|
-
.findMany
|
|
78
|
+
.query(schema.recipesTable)
|
|
79
|
+
.findMany({ with: { ingredients: true } })
|
|
80
|
+
.run();
|
|
81
|
+
// recipes is { id: string; title: string; ingredients: Ingredient[] }[]
|
|
82
|
+
|
|
83
|
+
// Nested relations work too
|
|
84
|
+
const plan = await db
|
|
85
|
+
.query(schema.mealPlans)
|
|
86
|
+
.findFirst({
|
|
87
|
+
with: { entries: { with: { recipe: { with: { ingredients: true } } } } },
|
|
88
|
+
})
|
|
76
89
|
.run();
|
|
77
|
-
//
|
|
90
|
+
// plan.entries[0].recipe.ingredients is fully typed
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Manual override:** pass `<TConfig, TRow>` to override when needed:
|
|
78
94
|
|
|
79
|
-
|
|
95
|
+
```typescript
|
|
96
|
+
type Custom = Recipe & { ingredients: Ingredient[] };
|
|
97
|
+
const recipes = await db
|
|
80
98
|
.query(recipesTable)
|
|
81
|
-
.
|
|
82
|
-
where: eq(recipesTable.id, id),
|
|
99
|
+
.findMany<{ with: { ingredients: true } }, Custom>({
|
|
83
100
|
with: { ingredients: true },
|
|
84
101
|
})
|
|
85
102
|
.run();
|
|
86
|
-
// recipe is RecipeWithIngredients | undefined.
|
|
87
103
|
```
|
|
88
104
|
|
|
89
105
|
### Writes
|
|
@@ -223,7 +239,7 @@ sequential execution (and loses the atomicity guarantee). For pure compose
|
|
|
223
239
|
workflows that need atomicity, build the underlying ops with `db.insert/update/
|
|
224
240
|
delete` directly and pass them straight to `db.batch([...])`.
|
|
225
241
|
|
|
226
|
-
### db.transaction(async tx => ...): Promise<T
|
|
242
|
+
### db.transaction(async tx => ...): Promise<TransactionResult<T>>
|
|
227
243
|
|
|
228
244
|
Runs a callback inside a transaction. Writes (`tx.insert`, `tx.update`,
|
|
229
245
|
`tx.delete`) are **recorded** as the callback runs and flushed together as a
|
|
@@ -231,6 +247,14 @@ single atomic `db.batch([...])` when the callback returns successfully. If the
|
|
|
231
247
|
callback throws, the pending writes are discarded and the error is re-thrown —
|
|
232
248
|
nothing reaches D1.
|
|
233
249
|
|
|
250
|
+
Returns a `TransactionResult<T>` with:
|
|
251
|
+
- `result: T` — the callback's return value
|
|
252
|
+
- `meta.changes: number` — total rows affected across all writes
|
|
253
|
+
- `meta.writeResults: D1Result[]` — per-statement D1 results
|
|
254
|
+
|
|
255
|
+
Use `meta.changes` to detect whether a WHERE-guarded UPDATE actually matched
|
|
256
|
+
any rows (the "out of stock" signal) without falling back to raw SQL.
|
|
257
|
+
|
|
234
258
|
Use `db.transaction` whenever the set of writes depends on logic inside the
|
|
235
259
|
callback (read-modify-write, conditional inserts, state machines):
|
|
236
260
|
|
|
@@ -238,11 +262,7 @@ callback (read-modify-write, conditional inserts, state machines):
|
|
|
238
262
|
import { and, eq, gte, sql } from "drizzle-orm";
|
|
239
263
|
|
|
240
264
|
// Oversell-safe checkout: atomic + guarded against concurrent decrements.
|
|
241
|
-
const order = await db.transaction(async (tx) => {
|
|
242
|
-
// Reads execute eagerly against the underlying db. They see whatever is
|
|
243
|
-
// committed right now — D1 does NOT provide snapshot isolation across
|
|
244
|
-
// async code, so another request can modify the row between read and
|
|
245
|
-
// write. The WHERE guard on the update is what keeps us concurrency-safe.
|
|
265
|
+
const { result: order, meta } = await db.transaction(async (tx) => {
|
|
246
266
|
const product = await tx.query(products).findFirst({
|
|
247
267
|
where: eq(products.id, pid),
|
|
248
268
|
}).run();
|
|
@@ -250,24 +270,22 @@ const order = await db.transaction(async (tx) => {
|
|
|
250
270
|
throw new Error("out of stock"); // rolls back, nothing is written
|
|
251
271
|
}
|
|
252
272
|
|
|
253
|
-
// Guarded decrement: relative SQL + WHERE stock >= qty.
|
|
254
|
-
// re-evaluated by D1 at commit time, so two concurrent transactions
|
|
255
|
-
// cannot BOTH decrement past zero. Either one succeeds and the other
|
|
256
|
-
// is a no-op (0 rows matched), or the application-level check above
|
|
257
|
-
// rejects the second one first.
|
|
273
|
+
// Guarded decrement: relative SQL + WHERE stock >= qty.
|
|
258
274
|
await tx.update(products)
|
|
259
275
|
.set({ stock: sql`stock - ${qty}` })
|
|
260
276
|
.where(and(eq(products.id, pid), gte(products.stock, qty)))
|
|
261
277
|
.run();
|
|
262
278
|
|
|
263
|
-
// Generate the order id client-side so we don't need `.returning()`
|
|
264
|
-
// inside the transaction (see "Returning inside a transaction" below).
|
|
265
279
|
const orderId = crypto.randomUUID();
|
|
266
280
|
await tx.insert(orders).values({ id: orderId, productId: pid, qty }).run();
|
|
267
281
|
|
|
268
|
-
// Whatever the callback returns becomes the transaction's return value.
|
|
269
282
|
return { orderId, productId: pid, qty };
|
|
270
283
|
});
|
|
284
|
+
|
|
285
|
+
// Check if the guarded decrement actually matched any rows.
|
|
286
|
+
if (meta.changes === 0) {
|
|
287
|
+
throw new Error("out of stock — concurrent decrement won");
|
|
288
|
+
}
|
|
271
289
|
```
|
|
272
290
|
|
|
273
291
|
**`tx` is a `Pick<Db, "query" | "insert" | "update" | "delete">`** plus a
|
|
@@ -631,6 +649,30 @@ export default defineConfig({
|
|
|
631
649
|
});
|
|
632
650
|
```
|
|
633
651
|
|
|
652
|
+
### toJSON(value): DateToString<T>
|
|
653
|
+
|
|
654
|
+
Recursively converts `Date` fields to ISO 8601 strings for JSON serialization.
|
|
655
|
+
React Router loaders must return JSON-serializable data; `toJSON()` makes the
|
|
656
|
+
Date-to-string conversion explicit so every loader doesn't need manual
|
|
657
|
+
`.toISOString()` calls.
|
|
658
|
+
|
|
659
|
+
```typescript
|
|
660
|
+
import { toJSON } from "@cfast/db";
|
|
661
|
+
|
|
662
|
+
export async function loader({ context }) {
|
|
663
|
+
const db = createDb({ ... });
|
|
664
|
+
const posts = await db.query(postsTable).findMany().run();
|
|
665
|
+
// Date fields (createdAt, updatedAt, etc.) become ISO strings automatically.
|
|
666
|
+
return { posts: toJSON(posts) };
|
|
667
|
+
}
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
The return type `DateToString<T>` maps every `Date` property to `string` at the
|
|
671
|
+
type level, so the client sees accurate types without manual casting.
|
|
672
|
+
|
|
673
|
+
Works on plain objects, arrays, nested structures, and single Date values.
|
|
674
|
+
Non-Date primitives pass through unchanged.
|
|
675
|
+
|
|
634
676
|
## Common Mistakes
|
|
635
677
|
|
|
636
678
|
- **Forgetting `.run()`** -- Operations are lazy. `db.query(t).findMany()` returns an Operation, not results. You must call `.run()`.
|