@cfast/db 0.5.0 → 0.7.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/llms.txt CHANGED
@@ -47,8 +47,8 @@ type Operation<TResult> = {
47
47
  ### Reads
48
48
 
49
49
  ```typescript
50
- db.query(table).findMany(options?): Operation<Row[]>
51
- db.query(table).findFirst(options?): Operation<Row | undefined>
50
+ db.query(table).findMany(options?): Operation<WithCan<Row>[]>
51
+ db.query(table).findFirst(options?): Operation<WithCan<Row> | undefined>
52
52
  db.query(table).paginate(params, options?): Operation<CursorPage<Row> | OffsetPage<Row>>
53
53
  ```
54
54
 
@@ -102,6 +102,34 @@ const recipes = await db
102
102
  .run();
103
103
  ```
104
104
 
105
+ #### Row-level `_can` annotations (#270)
106
+
107
+ Every `findMany` and `findFirst` result includes a `_can` object with per-action
108
+ booleans, evaluated from the user's grants at query time. No opt-in required --
109
+ permissions are first-class on every row.
110
+
111
+ ```typescript
112
+ const comments = await db.query(commentsTable).findMany().run();
113
+ // comments[0]._can -> { read: true, create: true, update: true, delete: false }
114
+
115
+ // Type: WithCan<Comment>[] -- each row has _can: Record<string, boolean>
116
+ type WithCan<T> = T & { _can: Record<string, boolean> };
117
+ ```
118
+
119
+ How `_can` is computed for each CRUD action:
120
+ - **Unrestricted grant** (no `where` clause): `_can.action = true` for every row (literal `1` in SQL, no CASE needed).
121
+ - **Restricted grant** (has `where` clause): `_can.action` varies per row via a SQL `CASE WHEN <condition> THEN 1 ELSE 0 END` computed column.
122
+ - **No grant**: `_can.action = false` for every row.
123
+ - **`manage` grant**: expands to all four CRUD actions (`read`, `create`, `update`, `delete`).
124
+
125
+ The `read` action is always `true` on returned rows because the permission WHERE
126
+ clause already filters out rows the user cannot read.
127
+
128
+ `_can` is **not** added in `unsafe()` mode or when `user` is `null`.
129
+
130
+ Performance: adds N computed columns per query (where N = distinct granted CRUD
131
+ actions, typically 3-5). Unrestricted grants are free (literal `1`).
132
+
105
133
  ### Writes
106
134
 
107
135
  ```typescript
@@ -403,19 +431,36 @@ cache: {
403
431
  Per-query: `db.query(t).findMany({ cache: false })` or `{ cache: { ttl: "5m", tags: ["posts"] } }`.
404
432
  Manual invalidation: `await db.cache.invalidate({ tags: ["posts"], tables: ["posts"] })`.
405
433
 
406
- ### defineSeed
434
+ ### One-liner seed
435
+
436
+ All seed functionality lives under the `@cfast/db/seed` entrypoint, which
437
+ bundles `@faker-js/faker` so you never install or import it yourself. The
438
+ simplest API is a one-liner:
439
+
440
+ ```typescript
441
+ import { seed } from "@cfast/db/seed";
442
+ import { createDb } from "@cfast/db";
443
+ import * as schema from "~/db/schema";
444
+
445
+ const db = createDb({ d1, schema, grants: [], user: null });
446
+ await seed(db);
447
+ ```
448
+
449
+ `seed(db)` introspects the schema from the Db instance, topologically sorts
450
+ tables, generates realistic data via faker, and inserts it through
451
+ `db.unsafe()`. Pass `{ transcript: "./seed.sql" }` as the second argument
452
+ to also write the INSERT statements to a file.
407
453
 
408
- `defineSeed({ entries })` is the canonical way to seed a local D1 database.
409
- Accepts an ordered list of `{ table, rows }` entries — put parent tables
410
- before child tables to respect FK ordering. `seed.run(db)` hops through
411
- `db.unsafe()` internally so seeds never need their own grants. Empty `rows`
412
- arrays are skipped, so placeholder entries are safe.
454
+ ### defineSeed (static fixtures)
455
+
456
+ For hand-authored fixture data (not generated), use `defineSeed`:
413
457
 
414
458
  ```typescript
415
- import { createDb, defineSeed } from "@cfast/db";
459
+ import { defineSeed } from "@cfast/db/seed";
460
+ import { createDb } from "@cfast/db";
416
461
  import * as schema from "~/db/schema";
417
462
 
418
- const seed = defineSeed({
463
+ const mySeed = defineSeed({
419
464
  entries: [
420
465
  { table: schema.users, rows: [{ id: "u-1", email: "ada@example.com", name: "Ada" }] },
421
466
  { table: schema.posts, rows: [{ id: "p-1", title: "Hello", authorId: "u-1" }] },
@@ -423,14 +468,116 @@ const seed = defineSeed({
423
468
  });
424
469
 
425
470
  const db = createDb({ d1, schema, grants: [], user: null });
426
- await seed.run(db);
471
+ await mySeed.run(db);
472
+ ```
473
+
474
+ ### Schema-driven seed generation (createSeedEngine)
475
+
476
+ `createSeedEngine(schema)` introspects Drizzle schema metadata to
477
+ auto-generate realistic test data using the bundled faker. Handles FK
478
+ resolution, topological ordering, `per` relational generation,
479
+ many-to-many deduplication, and auth table detection.
480
+
481
+ #### Column-level `.seed()` method
482
+
483
+ Chain `.seed(fn)` on any column builder to attach a custom generator:
484
+
485
+ ```typescript
486
+ import { table } from "@cfast/db/seed";
487
+ import { text, integer, real } from "drizzle-orm/sqlite-core";
488
+
489
+ const posts = table("posts", {
490
+ id: text("id").primaryKey(),
491
+ title: text("title").notNull().seed(f => f.lorem.sentence()),
492
+ content: text("content").seed(f => f.lorem.paragraphs(3)),
493
+ authorId: text("author_id").references(() => users.id),
494
+ }).seed({ count: 10 });
427
495
  ```
428
496
 
429
- Multi-row entries are flushed via `db.batch([...])` (atomic per table);
430
- single-row entries skip the batch path so Drizzle's non-empty tuple
431
- invariant holds. Use this from `scripts/seed.ts` and run it via
432
- `pnpm db:seed:local` (the scaffolded `create-cfast` package ships this
433
- script and command out of the box).
497
+ Fields without `.seed()` are auto-inferred from column type:
498
+ - `text` -> `faker.lorem.words()`
499
+ - `integer`/`real` -> `faker.number.int()`/`faker.number.float()`
500
+ - `timestamp` -> `faker.date.recent()`
501
+ - `boolean` -> `faker.datatype.boolean()`
502
+ - FK (`.references()`) -> random existing row from referenced table
503
+ - Primary key -> auto-generated UUID
504
+ - Nullable -> occasionally `null` (~10%)
505
+ - Columns with `$defaultFn` or static defaults are skipped
506
+
507
+ #### Table-level `.seed()` method
508
+
509
+ Chain `.seed({ count, per })` on a table created with `table()` to set
510
+ the row count and optional per-parent relationship:
511
+
512
+ ```typescript
513
+ import { table } from "@cfast/db/seed";
514
+
515
+ const users = table("users", { ... }).seed({ count: 10 });
516
+ const posts = table("posts", { ... }).seed({ count: 5, per: users });
517
+ // -> 5 per user = 50 total. authorId auto-filled with parent user's id.
518
+ ```
519
+
520
+ `table()` is a drop-in replacement for `sqliteTable()` from
521
+ `drizzle-orm/sqlite-core` that adds the `.seed()` method. The returned
522
+ table is a standard Drizzle table in every way.
523
+
524
+ #### Deprecated wrapper functions
525
+
526
+ The older `seedConfig()` and `tableSeed()` wrapper functions still work
527
+ and write to the same internal registries. They are interchangeable with
528
+ the `.seed()` methods but considered deprecated:
529
+
530
+ ```typescript
531
+ // Deprecated -- prefer .seed() method API above
532
+ import { seedConfig, tableSeed } from "@cfast/db/seed";
533
+ const posts = tableSeed(sqliteTable("posts", {
534
+ title: seedConfig(text("title"), f => f.lorem.sentence()),
535
+ }), { count: 5 });
536
+ ```
537
+
538
+ #### Seed context (ctx)
539
+
540
+ Column-level seed functions receive `(faker, ctx)` where `ctx` provides:
541
+
542
+ - `ctx.parent` -- the parent row (from `per`), `undefined` for root tables
543
+ - `ctx.ref(table)` -- pick a random existing row from any table
544
+ - `ctx.index` -- zero-based position within current batch
545
+ - `ctx.all(table)` -- all generated rows for a table
546
+
547
+ ```typescript
548
+ authorId: text("author_id").references(() => users.id)
549
+ .seed((faker, ctx) => ctx.parent?.id), // inherit from parent
550
+ role: text("role")
551
+ .seed((f, ctx) => ctx.index === 0 ? "admin" : f.helpers.arrayElement(["member", "viewer"])),
552
+ ```
553
+
554
+ #### Runtime API
555
+
556
+ ```typescript
557
+ import { createSeedEngine, createSingleTableSeed } from "@cfast/db/seed";
558
+
559
+ // Seed all tables from schema config
560
+ const engine = createSeedEngine(schema);
561
+ const generated = await engine.run(db);
562
+
563
+ // Also write SQL transcript
564
+ await engine.run(db, { transcript: "./seed.sql" });
565
+
566
+ // Single-table override
567
+ const singleTable = createSingleTableSeed(schema, posts, 5);
568
+ await singleTable.run(db);
569
+ ```
570
+
571
+ #### Many-to-many deduplication
572
+
573
+ Tables with 2+ FK columns are auto-detected as join tables.
574
+ Duplicate composite key combinations are silently skipped.
575
+
576
+ #### Auth integration
577
+
578
+ Tables named "users" get special handling: the first 5 rows use
579
+ `admin@example.com`, `user@example.com`, etc., and names use
580
+ `faker.person.fullName()`.
434
581
 
435
582
  ## Usage Examples
436
583
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cfast/db",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Permission-aware Drizzle queries for Cloudflare D1",
5
5
  "keywords": [
6
6
  "cfast",
@@ -23,16 +23,26 @@
23
23
  ".": {
24
24
  "import": "./dist/index.js",
25
25
  "types": "./dist/index.d.ts"
26
+ },
27
+ "./seed": {
28
+ "import": "./dist/seed.js",
29
+ "types": "./dist/seed.d.ts"
26
30
  }
27
31
  },
28
32
  "files": [
29
33
  "dist",
30
34
  "llms.txt"
31
35
  ],
32
- "sideEffects": false,
36
+ "sideEffects": [
37
+ "./dist/seed.js",
38
+ "./src/seed.ts"
39
+ ],
33
40
  "publishConfig": {
34
41
  "access": "public"
35
42
  },
43
+ "dependencies": {
44
+ "@faker-js/faker": "^9.0.0"
45
+ },
36
46
  "peerDependencies": {
37
47
  "@cfast/permissions": ">=0.3.0 <0.6.0",
38
48
  "drizzle-orm": ">=0.35"
@@ -51,11 +61,11 @@
51
61
  "tsup": "^8",
52
62
  "typescript": "^5.7",
53
63
  "vitest": "^4.1.0",
54
- "@cfast/permissions": "0.5.1"
64
+ "@cfast/permissions": "0.6.0"
55
65
  },
56
66
  "scripts": {
57
- "build": "tsup src/index.ts --format esm --dts",
58
- "dev": "tsup src/index.ts --format esm --dts --watch",
67
+ "build": "tsup src/index.ts src/seed.ts --format esm --dts",
68
+ "dev": "tsup src/index.ts src/seed.ts --format esm --dts --watch",
59
69
  "typecheck": "tsc --noEmit",
60
70
  "lint": "eslint src/",
61
71
  "test": "vitest run"