@cfast/db 0.3.0 → 0.4.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/README.md +15 -15
- package/dist/index.d.ts +457 -26
- package/dist/index.js +221 -6
- package/llms.txt +317 -11
- package/package.json +7 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DrizzleTable, PermissionDescriptor
|
|
1
|
+
import { Grant, DrizzleTable, PermissionDescriptor } from '@cfast/permissions';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Extracts the row type from a Drizzle table reference.
|
|
@@ -33,7 +33,7 @@ type InferRow<TTable> = TTable extends {
|
|
|
33
33
|
* // => [{ action: "read", table: "posts" }]
|
|
34
34
|
*
|
|
35
35
|
* // Execute with permission checks
|
|
36
|
-
* const rows = await op.run(
|
|
36
|
+
* const rows = await op.run();
|
|
37
37
|
* ```
|
|
38
38
|
*/
|
|
39
39
|
type Operation<TResult> = {
|
|
@@ -43,7 +43,8 @@ type Operation<TResult> = {
|
|
|
43
43
|
* Checks permissions, applies permission WHERE clauses, executes the query via Drizzle,
|
|
44
44
|
* and returns the result. Throws `ForbiddenError` if the user's role lacks a required grant.
|
|
45
45
|
*
|
|
46
|
-
* @param params -
|
|
46
|
+
* @param params - Optional placeholder values for `sql.placeholder()` calls.
|
|
47
|
+
* Omit when your query does not use placeholders — the default is `{}`.
|
|
47
48
|
*/
|
|
48
49
|
run: (params?: Record<string, unknown>) => Promise<TResult>;
|
|
49
50
|
};
|
|
@@ -222,7 +223,7 @@ type FindFirstOptions = Omit<FindManyOptions, "limit" | "offset">;
|
|
|
222
223
|
* @example
|
|
223
224
|
* ```ts
|
|
224
225
|
* const params = parseCursorParams(request, { defaultLimit: 20 });
|
|
225
|
-
* const page = await db.query(posts).paginate(params).run(
|
|
226
|
+
* const page = await db.query(posts).paginate(params).run();
|
|
226
227
|
* ```
|
|
227
228
|
*/
|
|
228
229
|
type CursorParams = {
|
|
@@ -242,7 +243,7 @@ type CursorParams = {
|
|
|
242
243
|
* @example
|
|
243
244
|
* ```ts
|
|
244
245
|
* const params = parseOffsetParams(request, { defaultLimit: 20 });
|
|
245
|
-
* const page = await db.query(posts).paginate(params).run(
|
|
246
|
+
* const page = await db.query(posts).paginate(params).run();
|
|
246
247
|
* ```
|
|
247
248
|
*/
|
|
248
249
|
type OffsetParams = {
|
|
@@ -271,7 +272,7 @@ type PaginateParams = CursorParams | OffsetParams;
|
|
|
271
272
|
* ```ts
|
|
272
273
|
* const page: CursorPage<Post> = await db.query(posts)
|
|
273
274
|
* .paginate({ type: "cursor", cursor: null, limit: 20 })
|
|
274
|
-
* .run(
|
|
275
|
+
* .run();
|
|
275
276
|
*
|
|
276
277
|
* if (page.nextCursor) {
|
|
277
278
|
* // Fetch next page with page.nextCursor
|
|
@@ -295,7 +296,7 @@ type CursorPage<T> = {
|
|
|
295
296
|
* ```ts
|
|
296
297
|
* const page: OffsetPage<Post> = await db.query(posts)
|
|
297
298
|
* .paginate({ type: "offset", page: 1, limit: 20 })
|
|
298
|
-
* .run(
|
|
299
|
+
* .run();
|
|
299
300
|
*
|
|
300
301
|
* console.log(`Page ${page.page} of ${page.totalPages} (${page.total} total)`);
|
|
301
302
|
* ```
|
|
@@ -347,6 +348,54 @@ type PaginateOptions = {
|
|
|
347
348
|
/** Per-query cache control. Pass `false` to skip caching, or an object to customize. */
|
|
348
349
|
cache?: QueryCacheOptions;
|
|
349
350
|
};
|
|
351
|
+
/**
|
|
352
|
+
* The transaction handle passed to the `db.transaction()` callback.
|
|
353
|
+
*
|
|
354
|
+
* Exposes the read/write builder surface of {@link Db} but intentionally omits
|
|
355
|
+
* `unsafe`, `batch`, `transaction`, and `cache`. Those are off-limits inside a
|
|
356
|
+
* transaction:
|
|
357
|
+
*
|
|
358
|
+
* - `unsafe()` would let you bypass permissions mid-tx — if you need system
|
|
359
|
+
* access, use `db.unsafe().transaction(...)` on the outer handle instead.
|
|
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
|
+
*/
|
|
377
|
+
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
|
+
update: <TTable extends DrizzleTable>(table: TTable) => UpdateBuilder<TTable>;
|
|
385
|
+
/** Same as {@link Db.delete}: records the delete into the transaction's pending queue. */
|
|
386
|
+
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>;
|
|
398
|
+
};
|
|
350
399
|
/**
|
|
351
400
|
* A permission-aware database instance bound to a specific user.
|
|
352
401
|
*
|
|
@@ -357,13 +406,13 @@ type PaginateOptions = {
|
|
|
357
406
|
* @example
|
|
358
407
|
* ```ts
|
|
359
408
|
* // Read
|
|
360
|
-
* const posts = await db.query(postsTable).findMany().run(
|
|
409
|
+
* const posts = await db.query(postsTable).findMany().run();
|
|
361
410
|
*
|
|
362
411
|
* // Write
|
|
363
|
-
* await db.insert(postsTable).values({ title: "Hello" }).run(
|
|
412
|
+
* await db.insert(postsTable).values({ title: "Hello" }).run();
|
|
364
413
|
*
|
|
365
414
|
* // Bypass permissions for system tasks
|
|
366
|
-
* await db.unsafe().delete(sessionsTable).where(expired).run(
|
|
415
|
+
* await db.unsafe().delete(sessionsTable).where(expired).run();
|
|
367
416
|
* ```
|
|
368
417
|
*/
|
|
369
418
|
type Db = {
|
|
@@ -405,6 +454,44 @@ type Db = {
|
|
|
405
454
|
* compositions but loses the atomicity guarantee.
|
|
406
455
|
*/
|
|
407
456
|
batch: (operations: Operation<unknown>[]) => Operation<unknown[]>;
|
|
457
|
+
/**
|
|
458
|
+
* Runs a callback inside a transaction with atomic commit-or-rollback semantics.
|
|
459
|
+
*
|
|
460
|
+
* All writes (`tx.insert`/`tx.update`/`tx.delete`) recorded inside the callback
|
|
461
|
+
* are deferred and flushed together as a single atomic `db.batch([...])` when
|
|
462
|
+
* the callback returns successfully. If the callback throws, pending writes are
|
|
463
|
+
* discarded and the error is re-thrown — nothing reaches D1.
|
|
464
|
+
*
|
|
465
|
+
* Reads (`tx.query(...)`) execute eagerly so the caller can branch on their
|
|
466
|
+
* results. **D1 does not provide snapshot isolation across async code**, so
|
|
467
|
+
* reads inside a transaction can see concurrent writes. For concurrency-safe
|
|
468
|
+
* read-modify-write (e.g. stock decrement), combine the transaction with a
|
|
469
|
+
* relative SQL update and a WHERE guard:
|
|
470
|
+
*
|
|
471
|
+
* ```ts
|
|
472
|
+
* await db.transaction(async (tx) => {
|
|
473
|
+
* // The WHERE guard ensures the decrement only applies when stock is
|
|
474
|
+
* // still >= qty at commit time. Two concurrent transactions cannot
|
|
475
|
+
* // both oversell because D1's atomic batch re-evaluates the WHERE.
|
|
476
|
+
* await tx.update(products)
|
|
477
|
+
* .set({ stock: sql`stock - ${qty}` })
|
|
478
|
+
* .where(and(eq(products.id, pid), gte(products.stock, qty)))
|
|
479
|
+
* .run();
|
|
480
|
+
* return tx.insert(orders).values({ productId: pid, qty }).returning().run();
|
|
481
|
+
* });
|
|
482
|
+
* ```
|
|
483
|
+
*
|
|
484
|
+
* Nested `db.transaction()` calls inside the callback are flattened into the
|
|
485
|
+
* parent's pending queue so everything still commits atomically.
|
|
486
|
+
*
|
|
487
|
+
* @typeParam T - The return type of the callback.
|
|
488
|
+
* @param callback - The transaction body. Receives a `tx` handle with
|
|
489
|
+
* `query`/`insert`/`update`/`delete` methods (no `unsafe`, `batch`, or
|
|
490
|
+
* `cache` — those are intentionally off-limits inside a transaction).
|
|
491
|
+
* @returns The value returned by the callback, or rejects with whatever the
|
|
492
|
+
* callback threw (after rolling back pending writes).
|
|
493
|
+
*/
|
|
494
|
+
transaction: <T>(callback: (tx: Tx) => Promise<T>) => Promise<T>;
|
|
408
495
|
/** Cache control methods for manual invalidation. */
|
|
409
496
|
cache: {
|
|
410
497
|
/** Invalidate cached queries by tag names and/or table names. */
|
|
@@ -415,6 +502,29 @@ type Db = {
|
|
|
415
502
|
tables?: string[];
|
|
416
503
|
}) => Promise<void>;
|
|
417
504
|
};
|
|
505
|
+
/**
|
|
506
|
+
* Clears the per-instance `with` lookup cache so that the next query
|
|
507
|
+
* re-runs every grant lookup function.
|
|
508
|
+
*
|
|
509
|
+
* In production this is rarely needed because each request gets a fresh
|
|
510
|
+
* `Db` via `createDb()`. In tests that reuse a single `Db` across grant
|
|
511
|
+
* mutations (e.g. inserting a new friendship and then querying recipes),
|
|
512
|
+
* call this after the mutation to avoid stale lookup results.
|
|
513
|
+
*
|
|
514
|
+
* For finer-grained control, wrap each logical request in
|
|
515
|
+
* {@link runWithLookupCache} instead -- that scopes the cache via
|
|
516
|
+
* `AsyncLocalStorage` so it is automatically discarded at scope exit.
|
|
517
|
+
*
|
|
518
|
+
* @example
|
|
519
|
+
* ```ts
|
|
520
|
+
* const db = createDb({ ... });
|
|
521
|
+
* await db.query(recipes).findMany().run(); // populates lookup cache
|
|
522
|
+
* await db.insert(friendGrants).values({ ... }).run(); // adds new grant
|
|
523
|
+
* db.clearLookupCache(); // drop stale lookups
|
|
524
|
+
* await db.query(recipes).findMany().run(); // sees new grant
|
|
525
|
+
* ```
|
|
526
|
+
*/
|
|
527
|
+
clearLookupCache: () => void;
|
|
418
528
|
};
|
|
419
529
|
/**
|
|
420
530
|
* Builder for read queries on a single table.
|
|
@@ -427,13 +537,13 @@ type Db = {
|
|
|
427
537
|
* const builder = db.query(posts);
|
|
428
538
|
*
|
|
429
539
|
* // Fetch all visible posts
|
|
430
|
-
* const all = await builder.findMany().run(
|
|
540
|
+
* const all = await builder.findMany().run();
|
|
431
541
|
*
|
|
432
542
|
* // Fetch a single post
|
|
433
|
-
* const post = await builder.findFirst({ where: eq(posts.id, id) }).run(
|
|
543
|
+
* const post = await builder.findFirst({ where: eq(posts.id, id) }).run();
|
|
434
544
|
*
|
|
435
545
|
* // Paginate
|
|
436
|
-
* const page = await builder.paginate(params, { orderBy: desc(posts.createdAt) }).run(
|
|
546
|
+
* const page = await builder.paginate(params, { orderBy: desc(posts.createdAt) }).run();
|
|
437
547
|
* ```
|
|
438
548
|
*/
|
|
439
549
|
type QueryBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
@@ -442,20 +552,50 @@ type QueryBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
|
442
552
|
*
|
|
443
553
|
* The result is typed via `InferRow<TTable>`, so callers get IntelliSense on
|
|
444
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
|
+
* ```
|
|
445
571
|
*/
|
|
446
|
-
findMany: (options?: FindManyOptions) => Operation<
|
|
572
|
+
findMany: <TRow = InferRow<TTable>>(options?: FindManyOptions) => Operation<TRow[]>;
|
|
447
573
|
/**
|
|
448
574
|
* Returns an {@link Operation} that fetches the first matching row, or `undefined`
|
|
449
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
|
+
* ```
|
|
450
588
|
*/
|
|
451
|
-
findFirst: (options?: FindFirstOptions) => Operation<
|
|
589
|
+
findFirst: <TRow = InferRow<TTable>>(options?: FindFirstOptions) => Operation<TRow | undefined>;
|
|
452
590
|
/**
|
|
453
591
|
* Returns a paginated {@link Operation} using either cursor-based or offset-based strategy.
|
|
454
592
|
*
|
|
455
593
|
* The return type depends on the `params.type` discriminant: {@link CursorPage} for `"cursor"`,
|
|
456
|
-
* {@link OffsetPage} for `"offset"`. Each page's items are typed as `InferRow<TTable
|
|
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).
|
|
457
597
|
*/
|
|
458
|
-
paginate: (params: CursorParams | OffsetParams, options?: PaginateOptions) => Operation<CursorPage<
|
|
598
|
+
paginate: <TRow = InferRow<TTable>>(params: CursorParams | OffsetParams, options?: PaginateOptions) => Operation<CursorPage<TRow>> | Operation<OffsetPage<TRow>>;
|
|
459
599
|
};
|
|
460
600
|
/**
|
|
461
601
|
* Builder for insert operations on a single table.
|
|
@@ -466,13 +606,13 @@ type QueryBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
|
466
606
|
* @example
|
|
467
607
|
* ```ts
|
|
468
608
|
* // Insert without returning
|
|
469
|
-
* await db.insert(posts).values({ title: "Hello", authorId: user.id }).run(
|
|
609
|
+
* await db.insert(posts).values({ title: "Hello", authorId: user.id }).run();
|
|
470
610
|
*
|
|
471
611
|
* // Insert with returning
|
|
472
612
|
* const row = await db.insert(posts)
|
|
473
613
|
* .values({ title: "Hello", authorId: user.id })
|
|
474
614
|
* .returning()
|
|
475
|
-
* .run(
|
|
615
|
+
* .run();
|
|
476
616
|
* ```
|
|
477
617
|
*/
|
|
478
618
|
type InsertBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
@@ -500,7 +640,7 @@ type InsertReturningBuilder<TTable extends DrizzleTable = DrizzleTable> = Operat
|
|
|
500
640
|
* await db.update(posts)
|
|
501
641
|
* .set({ published: true })
|
|
502
642
|
* .where(eq(posts.id, "abc-123"))
|
|
503
|
-
* .run(
|
|
643
|
+
* .run();
|
|
504
644
|
* ```
|
|
505
645
|
*/
|
|
506
646
|
type UpdateBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
@@ -534,7 +674,7 @@ type UpdateReturningBuilder<TTable extends DrizzleTable = DrizzleTable> = Operat
|
|
|
534
674
|
*
|
|
535
675
|
* @example
|
|
536
676
|
* ```ts
|
|
537
|
-
* await db.delete(posts).where(eq(posts.id, "abc-123")).run(
|
|
677
|
+
* await db.delete(posts).where(eq(posts.id, "abc-123")).run();
|
|
538
678
|
* ```
|
|
539
679
|
*/
|
|
540
680
|
type DeleteBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
@@ -576,10 +716,102 @@ type DeleteReturningBuilder<TTable extends DrizzleTable = DrizzleTable> = Operat
|
|
|
576
716
|
* });
|
|
577
717
|
*
|
|
578
718
|
* // All operations check permissions at .run() time
|
|
579
|
-
* const posts = await db.query(postsTable).findMany().run(
|
|
719
|
+
* const posts = await db.query(postsTable).findMany().run();
|
|
580
720
|
* ```
|
|
581
721
|
*/
|
|
582
722
|
declare function createDb(config: DbConfig): Db;
|
|
723
|
+
/**
|
|
724
|
+
* App-level db factory configuration. The "constants" — D1 binding,
|
|
725
|
+
* Drizzle schema, cache config — that don't change between requests.
|
|
726
|
+
*
|
|
727
|
+
* Returned by {@link createAppDb}, called per-request with the user's
|
|
728
|
+
* resolved grants and identity to produce a fresh permission-aware {@link Db}.
|
|
729
|
+
*/
|
|
730
|
+
type AppDbConfig = {
|
|
731
|
+
/**
|
|
732
|
+
* The Cloudflare D1 database binding. Pass either the binding directly
|
|
733
|
+
* (for tests, where the mock D1 is in scope at module load) or a
|
|
734
|
+
* `() => D1Database` getter (for Workers, where `env.DB` is only
|
|
735
|
+
* available per-request -- typical pattern: `() => env.get().DB`).
|
|
736
|
+
*
|
|
737
|
+
* The lazy form lets `createAppDb()` be called once at module-load time
|
|
738
|
+
* even though the binding itself isn't materialized until the first
|
|
739
|
+
* request, so the entire app shares a single factory definition.
|
|
740
|
+
*/
|
|
741
|
+
d1: D1Database | (() => D1Database);
|
|
742
|
+
/**
|
|
743
|
+
* Drizzle schema. Same shape as {@link DbConfig.schema} -- pass
|
|
744
|
+
* `import * as schema from "./schema"`.
|
|
745
|
+
*/
|
|
746
|
+
schema: Record<string, unknown>;
|
|
747
|
+
/**
|
|
748
|
+
* Cache configuration shared across every per-request `Db`. Defaults to
|
|
749
|
+
* `{ backend: "cache-api" }` to match `createDb`. Pass `false` to opt out.
|
|
750
|
+
*/
|
|
751
|
+
cache?: DbConfig["cache"];
|
|
752
|
+
};
|
|
753
|
+
/**
|
|
754
|
+
* The per-request factory returned by {@link createAppDb}. Closes over the
|
|
755
|
+
* app-level config and accepts the request-scoped grants + user, returning a
|
|
756
|
+
* fresh permission-aware {@link Db}.
|
|
757
|
+
*
|
|
758
|
+
* Stable type so callers (e.g. `@cfast/admin`'s `createDbForAdmin`,
|
|
759
|
+
* `@cfast/core`'s `dbPlugin`, route loaders) can pass the factory around
|
|
760
|
+
* without re-deriving its signature.
|
|
761
|
+
*/
|
|
762
|
+
type AppDbFactory = (grants: Grant[], user: {
|
|
763
|
+
id: string;
|
|
764
|
+
} | null) => Db;
|
|
765
|
+
/**
|
|
766
|
+
* Consolidates the three near-identical `createDb` factories that every
|
|
767
|
+
* cfast app used to define (#149):
|
|
768
|
+
*
|
|
769
|
+
* - `app/cfast.server.ts` (`dbPlugin` setup)
|
|
770
|
+
* - `app/admin.server.ts` (`createDbForAdmin`)
|
|
771
|
+
* - any route handler that built `Db` ad-hoc from `env.DB`
|
|
772
|
+
*
|
|
773
|
+
* Splits the configuration into "app constants" (D1 binding, schema, cache)
|
|
774
|
+
* and "per-request inputs" (grants, user). The returned factory captures the
|
|
775
|
+
* constants once at module load and accepts the request-scoped inputs at
|
|
776
|
+
* call time.
|
|
777
|
+
*
|
|
778
|
+
* Use this whenever the same shape of `createDb({ d1, schema, grants, user, cache })`
|
|
779
|
+
* shows up in more than one file in your app. The factory is a stable
|
|
780
|
+
* `(grants, user) => Db` callable that plugs directly into:
|
|
781
|
+
*
|
|
782
|
+
* - `@cfast/core`'s db plugin: `setup(ctx) { return { client: appDb(ctx.auth.grants, ctx.auth.user) } }`
|
|
783
|
+
* - `@cfast/admin`'s `db: createDbForAdmin` config: `db: appDb`
|
|
784
|
+
* - any route handler: `const db = appDb(ctx.grants, ctx.user)`
|
|
785
|
+
*
|
|
786
|
+
* @example
|
|
787
|
+
* ```ts
|
|
788
|
+
* // app/db/factory.ts -- defined once
|
|
789
|
+
* import { createAppDb } from "@cfast/db";
|
|
790
|
+
* import * as schema from "./schema";
|
|
791
|
+
*
|
|
792
|
+
* export const appDb = createAppDb({
|
|
793
|
+
* d1: env.get().DB,
|
|
794
|
+
* schema,
|
|
795
|
+
* cache: false,
|
|
796
|
+
* });
|
|
797
|
+
*
|
|
798
|
+
* // app/cfast.server.ts -- consume from the plugin
|
|
799
|
+
* const dbPlugin = definePlugin({
|
|
800
|
+
* name: "db",
|
|
801
|
+
* requires: [authPlugin],
|
|
802
|
+
* setup(ctx) {
|
|
803
|
+
* return { client: appDb(ctx.auth.grants, ctx.auth.user) };
|
|
804
|
+
* },
|
|
805
|
+
* });
|
|
806
|
+
*
|
|
807
|
+
* // app/admin.server.ts -- consume from admin
|
|
808
|
+
* const adminConfig = {
|
|
809
|
+
* db: appDb, // (grants, user) => Db -- already the right shape
|
|
810
|
+
* ...
|
|
811
|
+
* };
|
|
812
|
+
* ```
|
|
813
|
+
*/
|
|
814
|
+
declare function createAppDb(config: AppDbConfig): AppDbFactory;
|
|
583
815
|
|
|
584
816
|
/**
|
|
585
817
|
* A function that executes a single sub-operation within a {@link compose} executor.
|
|
@@ -596,10 +828,51 @@ type RunFn = (params?: Record<string, unknown>) => Promise<unknown>;
|
|
|
596
828
|
* `.run()` still performs its own permission check when the executor calls it. This enables
|
|
597
829
|
* data dependencies between operations (e.g., using an insert result's ID in an audit log).
|
|
598
830
|
*
|
|
831
|
+
* ### ⚠️ Positional callback footgun
|
|
832
|
+
*
|
|
833
|
+
* The `executor` callback receives one `run` function per entry in `operations`,
|
|
834
|
+
* **in array order**. TypeScript cannot enforce that the parameter names line
|
|
835
|
+
* up with the operations they correspond to, so a callback that swaps
|
|
836
|
+
* `(runUpdate, runVersion)` for `(runVersion, runUpdate)` -- or that simply
|
|
837
|
+
* forgets a parameter -- silently invokes the wrong sub-operation. The wiki
|
|
838
|
+
* tracking issue (#182) was a real bug of this shape: a `runVersion` parameter
|
|
839
|
+
* shadowed an outer-scope variable and was never invoked.
|
|
840
|
+
*
|
|
841
|
+
* **Prefer {@link composeSequentialCallback} for any workflow with data
|
|
842
|
+
* dependencies.** It accepts an `async (db) => ...` callback that uses the
|
|
843
|
+
* normal db builders by reference, so the binding is by name (not by
|
|
844
|
+
* position) and there is no way to wire the wrong op to the wrong handler:
|
|
845
|
+
*
|
|
846
|
+
* ```ts
|
|
847
|
+
* // ✗ Footgun: parameter order must match array order, not type-checked.
|
|
848
|
+
* const op = compose(
|
|
849
|
+
* [updatePost, bumpVersion],
|
|
850
|
+
* async (runUpdate, runVersion) => {
|
|
851
|
+
* await runUpdate(); // What if `runVersion` was renamed and never called?
|
|
852
|
+
* await runVersion();
|
|
853
|
+
* },
|
|
854
|
+
* );
|
|
855
|
+
*
|
|
856
|
+
* // ✓ Preferred: by-name binding via composeSequentialCallback.
|
|
857
|
+
* const op = composeSequentialCallback(db, async (tx) => {
|
|
858
|
+
* await tx.update(posts).set({ ... }).where(...).run();
|
|
859
|
+
* await tx.update(postVersions).set({ ... }).where(...).run();
|
|
860
|
+
* });
|
|
861
|
+
* ```
|
|
862
|
+
*
|
|
863
|
+
* Use `compose()` only when you genuinely need to interleave non-db logic
|
|
864
|
+
* between sub-operations and you have a small, fixed number of ops where the
|
|
865
|
+
* footgun is easy to read around. For everything else, reach for
|
|
866
|
+
* {@link composeSequential} (no callback at all) or
|
|
867
|
+
* {@link composeSequentialCallback} (by-name binding).
|
|
868
|
+
*
|
|
599
869
|
* @typeParam TResult - The return type of the executor function.
|
|
600
870
|
* @param operations - The operations to compose. Their permissions are merged and deduplicated.
|
|
601
871
|
* @param executor - A function that receives a `run` function for each operation (in order).
|
|
602
872
|
* You control execution order, data flow between operations, and the return value.
|
|
873
|
+
* **Must declare exactly one parameter per operation** -- TypeScript cannot enforce this,
|
|
874
|
+
* but a regression test in this package asserts that swapping the count surfaces undefined
|
|
875
|
+
* `run` calls at runtime.
|
|
603
876
|
* @returns A single {@link Operation} with combined permissions.
|
|
604
877
|
*
|
|
605
878
|
* @example
|
|
@@ -609,8 +882,8 @@ type RunFn = (params?: Record<string, unknown>) => Promise<unknown>;
|
|
|
609
882
|
* const publishWorkflow = compose(
|
|
610
883
|
* [updatePost, insertAuditLog],
|
|
611
884
|
* async (doUpdate, doAudit) => {
|
|
612
|
-
* const updated = await doUpdate(
|
|
613
|
-
* await doAudit(
|
|
885
|
+
* const updated = await doUpdate();
|
|
886
|
+
* await doAudit();
|
|
614
887
|
* return { published: true };
|
|
615
888
|
* },
|
|
616
889
|
* );
|
|
@@ -620,7 +893,7 @@ type RunFn = (params?: Record<string, unknown>) => Promise<unknown>;
|
|
|
620
893
|
* // => [{ action: "update", table: "posts" }, { action: "create", table: "audit_logs" }]
|
|
621
894
|
*
|
|
622
895
|
* // Execute all sub-operations
|
|
623
|
-
* await publishWorkflow.run(
|
|
896
|
+
* await publishWorkflow.run();
|
|
624
897
|
* ```
|
|
625
898
|
*/
|
|
626
899
|
declare function compose<TResult>(operations: Operation<unknown>[], executor: (...runs: RunFn[]) => TResult | Promise<TResult>): Operation<TResult>;
|
|
@@ -749,4 +1022,162 @@ declare function parseCursorParams(request: Request, options?: PaginationOptions
|
|
|
749
1022
|
*/
|
|
750
1023
|
declare function parseOffsetParams(request: Request, options?: PaginationOptions): OffsetParams;
|
|
751
1024
|
|
|
752
|
-
|
|
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
|
+
declare class TransactionError extends Error {
|
|
1030
|
+
constructor(message: string);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* A single seed entry — every row in `rows` is inserted into `table` at seed
|
|
1035
|
+
* time. Row shape is inferred from the Drizzle table so typos in column names
|
|
1036
|
+
* are caught by `tsc` instead of failing at runtime when `INSERT` rejects
|
|
1037
|
+
* the statement.
|
|
1038
|
+
*
|
|
1039
|
+
* @typeParam TTable - The Drizzle table reference (e.g. `typeof usersTable`).
|
|
1040
|
+
*/
|
|
1041
|
+
type SeedEntry<TTable extends DrizzleTable = DrizzleTable> = {
|
|
1042
|
+
/**
|
|
1043
|
+
* The Drizzle table to insert into. Must be imported from your schema
|
|
1044
|
+
* (`import { users } from "~/db/schema"`) rather than passed as a string
|
|
1045
|
+
* so the helper can infer row types and forward the reference to
|
|
1046
|
+
* `db.insert()`.
|
|
1047
|
+
*/
|
|
1048
|
+
table: TTable;
|
|
1049
|
+
/**
|
|
1050
|
+
* Rows to insert. The row shape is inferred from the table's
|
|
1051
|
+
* `$inferSelect` — making a typo in a column name is a compile-time error.
|
|
1052
|
+
*
|
|
1053
|
+
* Entries are inserted in the order they appear, which lets you control
|
|
1054
|
+
* foreign-key ordering just by ordering your `entries` array
|
|
1055
|
+
* (`{ users }` before `{ posts }`, etc.).
|
|
1056
|
+
*/
|
|
1057
|
+
rows: readonly InferRow<TTable>[];
|
|
1058
|
+
};
|
|
1059
|
+
/**
|
|
1060
|
+
* Configuration passed to {@link defineSeed}.
|
|
1061
|
+
*/
|
|
1062
|
+
type SeedConfig = {
|
|
1063
|
+
/**
|
|
1064
|
+
* Ordered list of seed entries. Each entry is flushed as a batched insert
|
|
1065
|
+
* in list order, so place parent tables (users, orgs) before child tables
|
|
1066
|
+
* (posts, memberships) that reference them via foreign keys.
|
|
1067
|
+
*/
|
|
1068
|
+
entries: readonly SeedEntry[];
|
|
1069
|
+
};
|
|
1070
|
+
/**
|
|
1071
|
+
* The compiled seed returned by {@link defineSeed}.
|
|
1072
|
+
*
|
|
1073
|
+
* Holds a frozen copy of the entry list so runner callers can introspect
|
|
1074
|
+
* what would be seeded, plus a `run(db)` method that actually executes the
|
|
1075
|
+
* inserts against a real {@link Db} instance.
|
|
1076
|
+
*/
|
|
1077
|
+
type Seed = {
|
|
1078
|
+
/** The frozen list of entries this seed will insert, in order. */
|
|
1079
|
+
readonly entries: readonly SeedEntry[];
|
|
1080
|
+
/**
|
|
1081
|
+
* Executes every entry against the given {@link Db} in the order they were
|
|
1082
|
+
* declared. Uses `db.unsafe()` internally so seed scripts don't need
|
|
1083
|
+
* their own grants plumbing — seeding is a system task by definition.
|
|
1084
|
+
*
|
|
1085
|
+
* Entries with an empty `rows` array are skipped so callers can leave
|
|
1086
|
+
* placeholder entries in the config without crashing the seed.
|
|
1087
|
+
*
|
|
1088
|
+
* @param db - A {@link Db} instance, typically created once at the top
|
|
1089
|
+
* of a `scripts/seed.ts` file via `createDb({...})`.
|
|
1090
|
+
*/
|
|
1091
|
+
run: (db: Db) => Promise<void>;
|
|
1092
|
+
};
|
|
1093
|
+
/**
|
|
1094
|
+
* Declares a database seed in a portable, type-safe way.
|
|
1095
|
+
*
|
|
1096
|
+
* The canonical replacement for hand-rolled `scripts/seed.ts` files that
|
|
1097
|
+
* called `db.mutate("tablename")` (a method that never existed) or reached
|
|
1098
|
+
* straight into raw Drizzle. Use the scaffolded `scripts/seed.ts` in a
|
|
1099
|
+
* `create-cfast` project for a ready-made example.
|
|
1100
|
+
*
|
|
1101
|
+
* @example
|
|
1102
|
+
* ```ts
|
|
1103
|
+
* // scripts/seed.ts
|
|
1104
|
+
* import { defineSeed, createDb } from "@cfast/db";
|
|
1105
|
+
* import * as schema from "~/db/schema";
|
|
1106
|
+
*
|
|
1107
|
+
* const seed = defineSeed({
|
|
1108
|
+
* entries: [
|
|
1109
|
+
* {
|
|
1110
|
+
* table: schema.users,
|
|
1111
|
+
* rows: [
|
|
1112
|
+
* { id: "u-1", email: "ada@example.com", name: "Ada" },
|
|
1113
|
+
* { id: "u-2", email: "grace@example.com", name: "Grace" },
|
|
1114
|
+
* ],
|
|
1115
|
+
* },
|
|
1116
|
+
* {
|
|
1117
|
+
* table: schema.posts,
|
|
1118
|
+
* rows: [
|
|
1119
|
+
* { id: "p-1", authorId: "u-1", title: "Hello" },
|
|
1120
|
+
* ],
|
|
1121
|
+
* },
|
|
1122
|
+
* ],
|
|
1123
|
+
* });
|
|
1124
|
+
*
|
|
1125
|
+
* // In a worker/runner that already has a real D1 binding:
|
|
1126
|
+
* const db = createDb({ d1, schema, grants: [], user: null });
|
|
1127
|
+
* await seed.run(db);
|
|
1128
|
+
* ```
|
|
1129
|
+
*
|
|
1130
|
+
* @param config - The {@link SeedConfig} with the ordered list of entries.
|
|
1131
|
+
* @returns A {@link Seed} with a `.run(db)` executor.
|
|
1132
|
+
*/
|
|
1133
|
+
declare function defineSeed(config: SeedConfig): Seed;
|
|
1134
|
+
|
|
1135
|
+
/**
|
|
1136
|
+
* Per-request cache that holds the resolved `with` lookup map for each grant.
|
|
1137
|
+
*
|
|
1138
|
+
* Keyed by grant object identity so two queries that consult the same grant
|
|
1139
|
+
* within a single request reuse the same lookup promise (each underlying
|
|
1140
|
+
* `LookupFn` runs at most once).
|
|
1141
|
+
*
|
|
1142
|
+
* The cache's lifetime is tied to the surrounding async context rather than
|
|
1143
|
+
* to any specific `Db` instance: `createDb()` calls transparently run inside
|
|
1144
|
+
* an {@link AsyncLocalStorage} scope, and nested operations resolve their
|
|
1145
|
+
* lookups through the active scope's cache. Tests (or anything sharing a
|
|
1146
|
+
* single `Db` across logical requests) can scope an explicit cache via
|
|
1147
|
+
* {@link runWithLookupCache}.
|
|
1148
|
+
*/
|
|
1149
|
+
type LookupCache = Map<Grant, Promise<Record<string, unknown>>>;
|
|
1150
|
+
/**
|
|
1151
|
+
* Creates a fresh per-request lookup cache. Exposed for tests and for the
|
|
1152
|
+
* `Db` wrapper that establishes the ALS scope; normal framework consumers
|
|
1153
|
+
* should rely on {@link runWithLookupCache} or the automatic scope that
|
|
1154
|
+
* `createDb()` sets up on every invocation.
|
|
1155
|
+
*/
|
|
1156
|
+
declare function createLookupCache(): LookupCache;
|
|
1157
|
+
/**
|
|
1158
|
+
* Runs `fn` inside a fresh per-request lookup cache scope.
|
|
1159
|
+
*
|
|
1160
|
+
* Use this when you need to reuse a single `Db` across multiple logical
|
|
1161
|
+
* requests (e.g. in tests that insert new grants mid-run) and want each
|
|
1162
|
+
* logical request to see a clean cache. All nested `Db` operations inside
|
|
1163
|
+
* `fn` (and any async work they start) share the same cache.
|
|
1164
|
+
*
|
|
1165
|
+
* @param fn - The callback to run inside the scope. Its return value is
|
|
1166
|
+
* forwarded back to the caller.
|
|
1167
|
+
* @param cache - Optional pre-existing cache to install. Defaults to a fresh
|
|
1168
|
+
* empty cache.
|
|
1169
|
+
*
|
|
1170
|
+
* @example
|
|
1171
|
+
* ```ts
|
|
1172
|
+
* // Test setup reusing a single Db:
|
|
1173
|
+
* const db = createDb({ ... });
|
|
1174
|
+
*
|
|
1175
|
+
* await runWithLookupCache(async () => {
|
|
1176
|
+
* await db.insert(friendGrants).values(...).run();
|
|
1177
|
+
* await db.query(posts).findMany().run(); // sees the new grant
|
|
1178
|
+
* });
|
|
1179
|
+
* ```
|
|
1180
|
+
*/
|
|
1181
|
+
declare function runWithLookupCache<T>(fn: () => T, cache?: LookupCache): T;
|
|
1182
|
+
|
|
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 };
|