@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/dist/index.d.ts +5 -305
- package/dist/index.js +79 -33
- package/dist/seed.d.ts +328 -0
- package/dist/seed.js +399 -0
- package/dist/types-FUFR36h1.d.ts +221 -0
- package/llms.txt +163 -16
- package/package.json +15 -5
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
|
-
###
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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.
|
|
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":
|
|
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.
|
|
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"
|