@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.d.ts +47 -794
- package/dist/index.js +147 -65
- package/dist/seed.d.ts +258 -0
- package/dist/seed.js +378 -0
- package/dist/types-FUFR36h1.d.ts +221 -0
- package/llms.txt +212 -44
- package/package.json +11 -4
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
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
|
-
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(
|
|
587
|
-
return `cfast:${role}:v${tableVersion}:${simpleHash(
|
|
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
|
|
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
|
-
|
|
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,
|
|
774
|
-
if (
|
|
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
|
-
|
|
781
|
-
return callback(nestedTx);
|
|
865
|
+
return callback(createTxHandle(db, parentCtx));
|
|
782
866
|
}
|
|
783
|
-
const
|
|
784
|
-
|
|
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
|
-
|
|
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 =
|
|
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/
|
|
1099
|
-
function
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
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 };
|