@cfast/db 0.4.1 → 0.6.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.js CHANGED
@@ -1,9 +1,10 @@
1
1
  // src/create-db.ts
2
- import { drizzle as drizzle3 } from "drizzle-orm/d1";
2
+ import { drizzle as drizzle4 } from "drizzle-orm/d1";
3
3
 
4
4
  // src/query-builder.ts
5
- import { count } from "drizzle-orm";
5
+ import { count, sql, or as drizzleOr } from "drizzle-orm";
6
6
  import { drizzle } from "drizzle-orm/d1";
7
+ import { getGrantedActions } from "@cfast/permissions";
7
8
 
8
9
  // src/permissions.ts
9
10
  import {
@@ -220,6 +221,53 @@ function getTableKey(schema, table) {
220
221
  function getQueryTable(db, key) {
221
222
  return db.query[key];
222
223
  }
224
+ var CAN_PREFIX = "_can_";
225
+ async function buildCanExtras(config, grantedActions, queriedAction) {
226
+ const extras = {};
227
+ for (const ga of grantedActions) {
228
+ const alias = `${CAN_PREFIX}${ga.action}`;
229
+ if (ga.action === queriedAction) {
230
+ extras[alias] = sql`1`.as(alias);
231
+ continue;
232
+ }
233
+ if (ga.unrestricted) {
234
+ extras[alias] = sql`1`.as(alias);
235
+ continue;
236
+ }
237
+ const needsLookupDb = ga.grants.some((g) => g.with !== void 0);
238
+ const lookupDb = needsLookupDb ? config.getLookupDb() : void 0;
239
+ const lookupSets = await Promise.all(
240
+ ga.grants.map((g) => resolveGrantLookups(g, config.user, lookupDb, config.lookupCache))
241
+ );
242
+ const columns = config.table;
243
+ const clauses = ga.grants.map(
244
+ (g, i) => g.where ? g.where(columns, config.user, lookupSets[i]) : void 0
245
+ ).filter((c) => c !== void 0);
246
+ if (clauses.length === 0) {
247
+ extras[alias] = sql`1`.as(alias);
248
+ continue;
249
+ }
250
+ const combined = clauses.length === 1 ? clauses[0] : drizzleOr(...clauses);
251
+ extras[alias] = sql`CASE WHEN ${combined} THEN 1 ELSE 0 END`.as(alias);
252
+ }
253
+ return extras;
254
+ }
255
+ function attachCan(row, grantedActions) {
256
+ const can = {};
257
+ const grantedSet = new Set(grantedActions.map((ga) => ga.action));
258
+ const allActions = ["read", "create", "update", "delete"];
259
+ for (const action of allActions) {
260
+ const key = `${CAN_PREFIX}${action}`;
261
+ if (key in row) {
262
+ can[action] = row[key] === 1 || row[key] === true;
263
+ delete row[key];
264
+ } else if (!grantedSet.has(action)) {
265
+ can[action] = false;
266
+ }
267
+ }
268
+ row._can = can;
269
+ return row;
270
+ }
223
271
  function buildQueryOperation(config, db, tableKey, method, options) {
224
272
  const permissions = makePermissions(config.unsafe, "read", config.table);
225
273
  return {
@@ -244,8 +292,32 @@ function buildQueryOperation(config, db, tableKey, method, options) {
244
292
  queryOptions.where = combinedWhere;
245
293
  }
246
294
  delete queryOptions.cache;
295
+ const grantedActions = !config.unsafe && config.user ? getGrantedActions(config.grants, config.table) : [];
296
+ if (grantedActions.length > 0) {
297
+ const canExtras = await buildCanExtras(config, grantedActions, "read");
298
+ const userExtras = queryOptions.extras;
299
+ queryOptions.extras = userExtras ? { ...userExtras, ...canExtras } : canExtras;
300
+ }
247
301
  const queryTable = getQueryTable(db, tableKey);
248
302
  const result = await queryTable[method](queryOptions);
303
+ if (grantedActions.length > 0) {
304
+ if (method === "findFirst" && result != null) {
305
+ attachCan(result, grantedActions);
306
+ } else if (method === "findMany" && Array.isArray(result)) {
307
+ for (const row of result) {
308
+ attachCan(row, grantedActions);
309
+ }
310
+ }
311
+ } else if (!config.unsafe && config.user) {
312
+ const emptyCan = { read: false, create: false, update: false, delete: false };
313
+ if (method === "findFirst" && result != null) {
314
+ result._can = { ...emptyCan };
315
+ } else if (method === "findMany" && Array.isArray(result)) {
316
+ for (const row of result) {
317
+ row._can = { ...emptyCan };
318
+ }
319
+ }
320
+ }
249
321
  return method === "findFirst" ? result ?? void 0 : result;
250
322
  }
251
323
  };
@@ -583,8 +655,8 @@ function createCacheManager(config) {
583
655
  const defaultTtl = config.ttl ?? "60s";
584
656
  const excludedTables = new Set(config.exclude ?? []);
585
657
  return {
586
- generateKey(sql, role, tableVersion) {
587
- return `cfast:${role}:v${tableVersion}:${simpleHash(sql)}`;
658
+ generateKey(sql2, role, tableVersion) {
659
+ return `cfast:${role}:v${tableVersion}:${simpleHash(sql2)}`;
588
660
  },
589
661
  getTableVersion(table) {
590
662
  return tableVersions.get(table) ?? 0;
@@ -675,6 +747,7 @@ function createCacheManager(config) {
675
747
  }
676
748
 
677
749
  // src/transaction.ts
750
+ import { drizzle as drizzle3 } from "drizzle-orm/d1";
678
751
  var TransactionError = class extends Error {
679
752
  constructor(message) {
680
753
  super(message);
@@ -684,14 +757,10 @@ var TransactionError = class extends Error {
684
757
  function wrapWriteOperation(ctx, op) {
685
758
  const record = (toQueue) => {
686
759
  if (ctx.closed) {
687
- throw new TransactionError(
688
- "Cannot run an operation after the transaction has committed or aborted."
689
- );
760
+ throw new TransactionError("Cannot run an operation after the transaction has committed or aborted.");
690
761
  }
691
762
  if (ctx.aborted) {
692
- throw new TransactionError(
693
- "Transaction has been aborted; no further operations may run."
694
- );
763
+ throw new TransactionError("Transaction has been aborted; no further operations may run.");
695
764
  }
696
765
  ctx.pending.push({ op: toQueue });
697
766
  };
@@ -718,9 +787,7 @@ function wrapWriteOperation(ctx, op) {
718
787
  }
719
788
  function createTxHandle(db, ctx) {
720
789
  return {
721
- query: (table) => {
722
- return db.query(table);
723
- },
790
+ query: (table) => db.query(table),
724
791
  insert: (table) => {
725
792
  const realBuilder = db.insert(table);
726
793
  return {
@@ -753,13 +820,11 @@ function createTxHandle(db, ctx) {
753
820
  }
754
821
  };
755
822
  },
756
- transaction: (callback) => {
757
- return runTransaction(db, callback, ctx);
758
- }
823
+ transaction: (callback) => runTransaction(db, callback, ctx)
759
824
  };
760
825
  }
761
- async function flushWrites(db, pending) {
762
- if (pending.length === 0) return;
826
+ async function flushWritesDirect(config, isUnsafe, pending) {
827
+ if (pending.length === 0) return { changes: 0, writeResults: [] };
763
828
  const ops = pending.map((p) => p.op);
764
829
  for (const op of ops) {
765
830
  if (getBatchable(op) === void 0) {
@@ -768,24 +833,39 @@ async function flushWrites(db, pending) {
768
833
  );
769
834
  }
770
835
  }
771
- await db.batch(ops).run({});
836
+ const allPermissions = deduplicateDescriptors(ops.flatMap((op) => op.permissions));
837
+ if (!isUnsafe) {
838
+ checkOperationPermissions(config.grants, allPermissions);
839
+ }
840
+ const batchables = ops.map((op) => getBatchable(op));
841
+ await Promise.all(batchables.map((b) => b.prepare?.() ?? Promise.resolve()));
842
+ const sharedDb = drizzle3(config.d1, { schema: config.schema });
843
+ const items = batchables.map((b) => b.build(sharedDb));
844
+ const batchResults = await sharedDb.batch(
845
+ items
846
+ );
847
+ const writeResults = batchResults.map((r) => {
848
+ if (r && typeof r === "object" && "meta" in r) {
849
+ return r;
850
+ }
851
+ return { results: [], success: true, meta: { changes: 0 } };
852
+ });
853
+ const changes = writeResults.reduce(
854
+ (sum, r) => sum + (r?.meta?.changes ?? 0),
855
+ 0
856
+ );
857
+ return { changes, writeResults };
772
858
  }
773
- async function runTransaction(db, callback, parentCtx) {
774
- if (parentCtx) {
859
+ async function runTransaction(db, callback, parentCtxOrConfig, isUnsafe) {
860
+ if (parentCtxOrConfig && "pending" in parentCtxOrConfig) {
861
+ const parentCtx = parentCtxOrConfig;
775
862
  if (parentCtx.closed || parentCtx.aborted) {
776
- throw new TransactionError(
777
- "Cannot start a nested transaction: parent has already committed or aborted."
778
- );
863
+ throw new TransactionError("Cannot start a nested transaction: parent has already committed or aborted.");
779
864
  }
780
- const nestedTx = createTxHandle(db, parentCtx);
781
- return callback(nestedTx);
865
+ return callback(createTxHandle(db, parentCtx));
782
866
  }
783
- const ctx = {
784
- pending: [],
785
- closed: false,
786
- aborted: false,
787
- nested: false
788
- };
867
+ const config = parentCtxOrConfig;
868
+ const ctx = { pending: [], closed: false, aborted: false, nested: false };
789
869
  const tx = createTxHandle(db, ctx);
790
870
  let result;
791
871
  try {
@@ -796,15 +876,21 @@ async function runTransaction(db, callback, parentCtx) {
796
876
  ctx.pending.length = 0;
797
877
  throw err;
798
878
  }
879
+ let flushResult;
799
880
  try {
800
- await flushWrites(db, ctx.pending);
881
+ if (config) {
882
+ flushResult = await flushWritesDirect(config, isUnsafe ?? false, ctx.pending);
883
+ } else {
884
+ if (ctx.pending.length > 0) await db.batch(ctx.pending.map((p) => p.op)).run({});
885
+ flushResult = { changes: 0, writeResults: [] };
886
+ }
801
887
  ctx.closed = true;
802
888
  } catch (err) {
803
889
  ctx.aborted = true;
804
890
  ctx.closed = true;
805
891
  throw err;
806
892
  }
807
- return result;
893
+ return { result, meta: { changes: flushResult.changes, writeResults: flushResult.writeResults } };
808
894
  }
809
895
 
810
896
  // src/create-db.ts
@@ -895,7 +981,7 @@ function buildDb(config, isUnsafe, lookupCache) {
895
981
  const batchables = operations.map((op) => getBatchable(op));
896
982
  const everyOpBatchable = batchables.every((b) => b !== void 0);
897
983
  if (everyOpBatchable) {
898
- const sharedDb = drizzle3(config.d1, { schema: config.schema });
984
+ const sharedDb = drizzle4(config.d1, { schema: config.schema });
899
985
  await Promise.all(
900
986
  batchables.map((b) => b.prepare?.() ?? Promise.resolve())
901
987
  );
@@ -921,7 +1007,7 @@ function buildDb(config, isUnsafe, lookupCache) {
921
1007
  };
922
1008
  },
923
1009
  transaction(callback) {
924
- return runTransaction(db, callback);
1010
+ return runTransaction(db, callback, { d1: config.d1, schema: config.schema, grants: config.grants }, isUnsafe);
925
1011
  },
926
1012
  cache: {
927
1013
  async invalidate(options) {
@@ -938,7 +1024,8 @@ function buildDb(config, isUnsafe, lookupCache) {
938
1024
  },
939
1025
  clearLookupCache() {
940
1026
  lookupCache.clear();
941
- }
1027
+ },
1028
+ _schema: config.schema
942
1029
  };
943
1030
  return db;
944
1031
  }
@@ -1054,7 +1141,7 @@ function createTrackingDb(real, perms) {
1054
1141
  insert: trackingDb.insert,
1055
1142
  update: trackingDb.update,
1056
1143
  delete: trackingDb.delete,
1057
- transaction: (cb) => trackingDb.transaction(cb)
1144
+ transaction: (cb) => trackingDb.transaction(cb).then((txr) => txr.result)
1058
1145
  };
1059
1146
  try {
1060
1147
  await callback(trackingTx);
@@ -1063,7 +1150,8 @@ function createTrackingDb(real, perms) {
1063
1150
  return createSentinel();
1064
1151
  },
1065
1152
  cache: real.cache,
1066
- clearLookupCache: () => real.clearLookupCache()
1153
+ clearLookupCache: () => real.clearLookupCache(),
1154
+ _schema: real._schema
1067
1155
  };
1068
1156
  return trackingDb;
1069
1157
  }
@@ -1095,31 +1183,25 @@ function composeSequentialCallback(db, callback) {
1095
1183
  };
1096
1184
  }
1097
1185
 
1098
- // src/seed.ts
1099
- function defineSeed(config) {
1100
- const entries = Object.freeze(
1101
- config.entries.map((entry) => ({
1102
- table: entry.table,
1103
- rows: Object.freeze([...entry.rows])
1104
- }))
1105
- );
1106
- return {
1107
- entries,
1108
- async run(db) {
1109
- const unsafeDb = db.unsafe();
1110
- for (const entry of entries) {
1111
- if (entry.rows.length === 0) continue;
1112
- const ops = entry.rows.map(
1113
- (row) => unsafeDb.insert(entry.table).values(row)
1114
- );
1115
- if (ops.length === 1) {
1116
- await ops[0].run({});
1117
- } else {
1118
- await unsafeDb.batch(ops).run({});
1119
- }
1120
- }
1186
+ // src/json.ts
1187
+ function toJSON(value) {
1188
+ return convertDates(value);
1189
+ }
1190
+ function convertDates(value) {
1191
+ if (value instanceof Date) {
1192
+ return value.toISOString();
1193
+ }
1194
+ if (Array.isArray(value)) {
1195
+ return value.map(convertDates);
1196
+ }
1197
+ if (value !== null && typeof value === "object") {
1198
+ const result = {};
1199
+ for (const [key, val] of Object.entries(value)) {
1200
+ result[key] = convertDates(val);
1121
1201
  }
1122
- };
1202
+ return result;
1203
+ }
1204
+ return value;
1123
1205
  }
1124
1206
  export {
1125
1207
  TransactionError,
@@ -1129,8 +1211,8 @@ export {
1129
1211
  createAppDb,
1130
1212
  createDb,
1131
1213
  createLookupCache,
1132
- defineSeed,
1133
1214
  parseCursorParams,
1134
1215
  parseOffsetParams,
1135
- runWithLookupCache
1216
+ runWithLookupCache,
1217
+ toJSON
1136
1218
  };
package/dist/seed.d.ts ADDED
@@ -0,0 +1,258 @@
1
+ import { DrizzleTable } from '@cfast/permissions';
2
+ import { a as Db, i as InferRow } from './types-FUFR36h1.js';
3
+ import { Column } from 'drizzle-orm';
4
+
5
+ /**
6
+ * Schema-driven seed generator for `@cfast/db`.
7
+ *
8
+ * Introspects Drizzle schema metadata (column types, foreign keys, primary keys)
9
+ * to auto-generate realistic test data using the bundled `@faker-js/faker`.
10
+ * Supports:
11
+ *
12
+ * - Column-level `.seed()` overrides via `seedConfig()` wrapper
13
+ * - Table-level `.seed()` overrides via `tableSeed()` wrapper (count, per)
14
+ * - Automatic FK resolution from generated parent rows
15
+ * - `per` relational generation (N children per parent row)
16
+ * - `ctx` API for parent access, ref, index, and all
17
+ * - Topological sort for correct insert order
18
+ * - Many-to-many deduplication
19
+ * - Auth table detection for realistic emails
20
+ * - SQL transcript generation
21
+ *
22
+ * @module seed-generator
23
+ */
24
+
25
+ type AnyColumn = Column<any, any, any>;
26
+ /** Faker instance type -- matches `@faker-js/faker`'s default export. */
27
+ type Faker = any;
28
+ /**
29
+ * Context passed to column-level seed functions as the second argument.
30
+ */
31
+ type SeedContext = {
32
+ /** The parent row when `per` is used on the table. `undefined` for root tables. */
33
+ parent: Record<string, unknown> | undefined;
34
+ /** Pick a random existing row from any table already seeded. */
35
+ ref: (table: DrizzleTable) => Record<string, unknown>;
36
+ /** Zero-based position within the current batch (per-parent or global). */
37
+ index: number;
38
+ /** All generated rows for the given table (available after that table is seeded). */
39
+ all: (table: DrizzleTable) => Record<string, unknown>[];
40
+ };
41
+ /** Column-level seed function. Receives faker instance and optional context. */
42
+ type ColumnSeedFn = (faker: Faker, ctx: SeedContext) => unknown;
43
+ /** Table-level seed config attached via `tableSeed()`. */
44
+ type TableSeedConfig = {
45
+ /** Total count (or per-parent count when `per` is set). */
46
+ count: number;
47
+ /** Generate `count` rows per row in this parent table. */
48
+ per?: DrizzleTable;
49
+ };
50
+ /** Options for `db.seed().run()`. */
51
+ type SeedRunOptions = {
52
+ /** If provided, write the equivalent SQL INSERT statements to this path. */
53
+ transcript?: string;
54
+ };
55
+ /**
56
+ * Attaches a seed generator function to a Drizzle column.
57
+ *
58
+ * The column object is returned unmodified so this can be used inline in
59
+ * schema definitions without breaking Drizzle types.
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * const posts = sqliteTable("posts", {
64
+ * title: seedConfig(text("title"), f => f.lorem.sentence()),
65
+ * });
66
+ * ```
67
+ */
68
+ declare function seedConfig<T>(column: T, fn: ColumnSeedFn): T;
69
+ /**
70
+ * Attaches table-level seed config (count, per) to a Drizzle table.
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * const posts = tableSeed(
75
+ * sqliteTable("posts", { ... }),
76
+ * { count: 5, per: users },
77
+ * );
78
+ * ```
79
+ */
80
+ declare function tableSeed<T extends DrizzleTable>(table: T, config: TableSeedConfig): T;
81
+ /** FK info extracted from Drizzle's internal symbol. */
82
+ type FkInfo = {
83
+ /** Column name in the current table (SQL name). */
84
+ columnName: string;
85
+ /** JS key of the column in the current table. */
86
+ columnKey: string;
87
+ /** Referenced table (Drizzle object). */
88
+ foreignTable: DrizzleTable;
89
+ /** Referenced column name (SQL name). */
90
+ foreignColumnName: string;
91
+ };
92
+ /** Extracts all inline foreign key definitions from a Drizzle table. */
93
+ declare function extractForeignKeys(table: DrizzleTable): FkInfo[];
94
+ /** Returns the JS key for the primary key column(s). Only supports single PK. */
95
+ declare function findPrimaryKeyColumn(table: DrizzleTable): {
96
+ key: string;
97
+ column: AnyColumn;
98
+ } | undefined;
99
+ /**
100
+ * Topologically sorts tables so parents are seeded before children.
101
+ * Uses Kahn's algorithm. Respects both FK and `per` dependencies.
102
+ */
103
+ declare function topologicalSort(tables: DrizzleTable[], fkMap: Map<DrizzleTable, FkInfo[]>): DrizzleTable[];
104
+ /**
105
+ * Generates seed data for all tables in a schema using Drizzle column metadata
106
+ * and the bundled `@faker-js/faker` instance.
107
+ *
108
+ * @param schema - The full Drizzle schema (`import * as schema from "./schema"`).
109
+ * @returns An engine with `generate()` and `run(db)` methods.
110
+ */
111
+ declare function createSeedEngine(schema: Record<string, unknown>): {
112
+ tables: object[];
113
+ fkMap: Map<object, FkInfo[]>;
114
+ /**
115
+ * Generate rows for all tables (or a subset).
116
+ * Returns a map of table -> rows[].
117
+ */
118
+ generate(tableOverrides?: Map<DrizzleTable, {
119
+ count: number;
120
+ }>): Map<DrizzleTable, Record<string, unknown>[]>;
121
+ /**
122
+ * Generate and insert seed data into the database.
123
+ */
124
+ run(db: Db, options?: SeedRunOptions & {
125
+ tableOverrides?: Map<DrizzleTable, {
126
+ count: number;
127
+ }>;
128
+ }): Promise<Map<DrizzleTable, Record<string, unknown>[]>>;
129
+ };
130
+ /**
131
+ * Creates a single-table seed generator for use with `db.query(table).seed(n)`.
132
+ */
133
+ declare function createSingleTableSeed(schema: Record<string, unknown>, table: DrizzleTable, count: number): {
134
+ generate: () => Map<object, Record<string, unknown>[]>;
135
+ run: (db: Db, options?: SeedRunOptions) => Promise<Map<object, Record<string, unknown>[]>>;
136
+ };
137
+ /**
138
+ * Checks if a value is a Drizzle table by looking for the IsDrizzleTable symbol.
139
+ */
140
+ declare function isTable(value: unknown): boolean;
141
+
142
+ /**
143
+ * One-liner seed: introspects the schema from the `db` instance, generates
144
+ * realistic data via the bundled `@faker-js/faker`, and inserts it.
145
+ *
146
+ * @example
147
+ * ```ts
148
+ * import { seed } from "@cfast/db/seed";
149
+ * await seed(db);
150
+ * ```
151
+ *
152
+ * @param db - A {@link Db} instance created via `createDb()`.
153
+ * @param options - Optional {@link SeedRunOptions} (e.g. `{ transcript: "./seed.sql" }`).
154
+ */
155
+ declare function seed(db: Db, options?: SeedRunOptions): Promise<void>;
156
+ /**
157
+ * A single seed entry — every row in `rows` is inserted into `table` at seed
158
+ * time. Row shape is inferred from the Drizzle table so typos in column names
159
+ * are caught by `tsc` instead of failing at runtime when `INSERT` rejects
160
+ * the statement.
161
+ *
162
+ * @typeParam TTable - The Drizzle table reference (e.g. `typeof usersTable`).
163
+ */
164
+ type SeedEntry<TTable extends DrizzleTable = DrizzleTable> = {
165
+ /**
166
+ * The Drizzle table to insert into. Must be imported from your schema
167
+ * (`import { users } from "~/db/schema"`) rather than passed as a string
168
+ * so the helper can infer row types and forward the reference to
169
+ * `db.insert()`.
170
+ */
171
+ table: TTable;
172
+ /**
173
+ * Rows to insert. The row shape is inferred from the table's
174
+ * `$inferSelect` — making a typo in a column name is a compile-time error.
175
+ *
176
+ * Entries are inserted in the order they appear, which lets you control
177
+ * foreign-key ordering just by ordering your `entries` array
178
+ * (`{ users }` before `{ posts }`, etc.).
179
+ */
180
+ rows: readonly InferRow<TTable>[];
181
+ };
182
+ /**
183
+ * Configuration passed to {@link defineSeed}.
184
+ */
185
+ type SeedConfig = {
186
+ /**
187
+ * Ordered list of seed entries. Each entry is flushed as a batched insert
188
+ * in list order, so place parent tables (users, orgs) before child tables
189
+ * (posts, memberships) that reference them via foreign keys.
190
+ */
191
+ entries: readonly SeedEntry[];
192
+ };
193
+ /**
194
+ * The compiled seed returned by {@link defineSeed}.
195
+ *
196
+ * Holds a frozen copy of the entry list so runner callers can introspect
197
+ * what would be seeded, plus a `run(db)` method that actually executes the
198
+ * inserts against a real {@link Db} instance.
199
+ */
200
+ type Seed = {
201
+ /** The frozen list of entries this seed will insert, in order. */
202
+ readonly entries: readonly SeedEntry[];
203
+ /**
204
+ * Executes every entry against the given {@link Db} in the order they were
205
+ * declared. Uses `db.unsafe()` internally so seed scripts don't need
206
+ * their own grants plumbing — seeding is a system task by definition.
207
+ *
208
+ * Entries with an empty `rows` array are skipped so callers can leave
209
+ * placeholder entries in the config without crashing the seed.
210
+ *
211
+ * @param db - A {@link Db} instance, typically created once at the top
212
+ * of a `scripts/seed.ts` file via `createDb({...})`.
213
+ */
214
+ run: (db: Db) => Promise<void>;
215
+ };
216
+ /**
217
+ * Declares a database seed in a portable, type-safe way.
218
+ *
219
+ * The canonical replacement for hand-rolled `scripts/seed.ts` files that
220
+ * called `db.mutate("tablename")` (a method that never existed) or reached
221
+ * straight into raw Drizzle. Use the scaffolded `scripts/seed.ts` in a
222
+ * `create-cfast` project for a ready-made example.
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * // scripts/seed.ts
227
+ * import { defineSeed, createDb } from "@cfast/db";
228
+ * import * as schema from "~/db/schema";
229
+ *
230
+ * const seed = defineSeed({
231
+ * entries: [
232
+ * {
233
+ * table: schema.users,
234
+ * rows: [
235
+ * { id: "u-1", email: "ada@example.com", name: "Ada" },
236
+ * { id: "u-2", email: "grace@example.com", name: "Grace" },
237
+ * ],
238
+ * },
239
+ * {
240
+ * table: schema.posts,
241
+ * rows: [
242
+ * { id: "p-1", authorId: "u-1", title: "Hello" },
243
+ * ],
244
+ * },
245
+ * ],
246
+ * });
247
+ *
248
+ * // In a worker/runner that already has a real D1 binding:
249
+ * const db = createDb({ d1, schema, grants: [], user: null });
250
+ * await seed.run(db);
251
+ * ```
252
+ *
253
+ * @param config - The {@link SeedConfig} with the ordered list of entries.
254
+ * @returns A {@link Seed} with a `.run(db)` executor.
255
+ */
256
+ declare function defineSeed(config: SeedConfig): Seed;
257
+
258
+ export { type ColumnSeedFn, type Faker, type Seed, type SeedConfig, type SeedContext, type SeedEntry, type SeedRunOptions, type TableSeedConfig, createSeedEngine, createSingleTableSeed, defineSeed, extractForeignKeys, findPrimaryKeyColumn, isTable, seed, seedConfig, tableSeed, topologicalSort };