@cfast/db 0.5.0 → 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 +5 -305
- package/dist/index.js +79 -33
- package/dist/seed.d.ts +258 -0
- package/dist/seed.js +378 -0
- package/dist/types-FUFR36h1.d.ts +221 -0
- package/llms.txt +142 -16
- package/package.json +11 -4
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 };
|
package/dist/seed.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// src/seed-generator.ts
|
|
2
|
+
import { faker } from "@faker-js/faker";
|
|
3
|
+
import { getTableColumns, getTableName } from "drizzle-orm";
|
|
4
|
+
function asTable(t) {
|
|
5
|
+
return t;
|
|
6
|
+
}
|
|
7
|
+
var columnSeedMap = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
var tableSeedMap = /* @__PURE__ */ new WeakMap();
|
|
9
|
+
function getColumnSeedFn(col) {
|
|
10
|
+
const config = col.config;
|
|
11
|
+
if (config) {
|
|
12
|
+
const fn = columnSeedMap.get(config);
|
|
13
|
+
if (fn) return fn;
|
|
14
|
+
}
|
|
15
|
+
return columnSeedMap.get(col);
|
|
16
|
+
}
|
|
17
|
+
function seedConfig(column, fn) {
|
|
18
|
+
const config = column.config;
|
|
19
|
+
if (config && typeof config === "object") {
|
|
20
|
+
columnSeedMap.set(config, fn);
|
|
21
|
+
}
|
|
22
|
+
columnSeedMap.set(column, fn);
|
|
23
|
+
return column;
|
|
24
|
+
}
|
|
25
|
+
function tableSeed(table, config) {
|
|
26
|
+
tableSeedMap.set(table, config);
|
|
27
|
+
return table;
|
|
28
|
+
}
|
|
29
|
+
function extractForeignKeys(table) {
|
|
30
|
+
const fkSymbol = Object.getOwnPropertySymbols(table).find(
|
|
31
|
+
(s) => s.toString().includes("InlineForeignKeys")
|
|
32
|
+
);
|
|
33
|
+
if (!fkSymbol) return [];
|
|
34
|
+
const fks = table[fkSymbol];
|
|
35
|
+
const columns = getTableColumns(asTable(table));
|
|
36
|
+
const sqlNameToKey = /* @__PURE__ */ new Map();
|
|
37
|
+
for (const [key, col] of Object.entries(columns)) {
|
|
38
|
+
sqlNameToKey.set(col.name, key);
|
|
39
|
+
}
|
|
40
|
+
const result = [];
|
|
41
|
+
for (const fk of fks) {
|
|
42
|
+
if (typeof fk?.reference !== "function") continue;
|
|
43
|
+
const ref = fk.reference();
|
|
44
|
+
if (!ref?.foreignTable || !ref.foreignColumns?.length || !ref.columns?.length) continue;
|
|
45
|
+
const colSqlName = ref.columns[0].name;
|
|
46
|
+
result.push({
|
|
47
|
+
columnName: colSqlName,
|
|
48
|
+
columnKey: sqlNameToKey.get(colSqlName) ?? colSqlName,
|
|
49
|
+
foreignTable: ref.foreignTable,
|
|
50
|
+
foreignColumnName: ref.foreignColumns[0].name
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
function findPrimaryKeyColumn(table) {
|
|
56
|
+
const columns = getTableColumns(asTable(table));
|
|
57
|
+
for (const [key, col] of Object.entries(columns)) {
|
|
58
|
+
if (col.primary) {
|
|
59
|
+
return { key, column: col };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return void 0;
|
|
63
|
+
}
|
|
64
|
+
function generateDefaultValue(col, isPk, isNullable) {
|
|
65
|
+
if (isPk) return faker.string.uuid();
|
|
66
|
+
if (isNullable && faker.number.int({ min: 0, max: 9 }) === 0) return null;
|
|
67
|
+
const { dataType, columnType } = col;
|
|
68
|
+
if (columnType === "SQLiteBoolean" || dataType === "boolean") {
|
|
69
|
+
return faker.datatype.boolean();
|
|
70
|
+
}
|
|
71
|
+
if (columnType === "SQLiteTimestamp" || dataType === "date") {
|
|
72
|
+
return faker.date.recent();
|
|
73
|
+
}
|
|
74
|
+
if (columnType === "SQLiteReal") {
|
|
75
|
+
return faker.number.float({ min: 0, max: 1e3, fractionDigits: 2 });
|
|
76
|
+
}
|
|
77
|
+
if (columnType === "SQLiteInteger" || dataType === "number") {
|
|
78
|
+
return faker.number.int({ min: 0, max: 1e4 });
|
|
79
|
+
}
|
|
80
|
+
if (columnType === "SQLiteText" || dataType === "string") {
|
|
81
|
+
return faker.lorem.words(3);
|
|
82
|
+
}
|
|
83
|
+
if (dataType === "buffer") {
|
|
84
|
+
return faker.string.alphanumeric(16);
|
|
85
|
+
}
|
|
86
|
+
return faker.lorem.words(2);
|
|
87
|
+
}
|
|
88
|
+
var AUTH_TABLE_NAME = "users";
|
|
89
|
+
function isAuthUsersTable(table) {
|
|
90
|
+
return getTableName(asTable(table)) === AUTH_TABLE_NAME;
|
|
91
|
+
}
|
|
92
|
+
function generateAuthEmail(index) {
|
|
93
|
+
const roles = ["admin", "user", "editor", "viewer", "moderator"];
|
|
94
|
+
if (index < roles.length) {
|
|
95
|
+
return `${roles[index]}@example.com`;
|
|
96
|
+
}
|
|
97
|
+
return faker.internet.email().toLowerCase();
|
|
98
|
+
}
|
|
99
|
+
function topologicalSort(tables, fkMap) {
|
|
100
|
+
const tableSet = new Set(tables);
|
|
101
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
102
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
103
|
+
for (const t of tables) {
|
|
104
|
+
if (!inDegree.has(t)) inDegree.set(t, 0);
|
|
105
|
+
if (!dependents.has(t)) dependents.set(t, /* @__PURE__ */ new Set());
|
|
106
|
+
}
|
|
107
|
+
for (const t of tables) {
|
|
108
|
+
const fks = fkMap.get(t) ?? [];
|
|
109
|
+
const deps = /* @__PURE__ */ new Set();
|
|
110
|
+
for (const fk of fks) {
|
|
111
|
+
if (tableSet.has(fk.foreignTable) && fk.foreignTable !== t) {
|
|
112
|
+
deps.add(fk.foreignTable);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const tConfig = tableSeedMap.get(t);
|
|
116
|
+
if (tConfig?.per && tableSet.has(tConfig.per) && tConfig.per !== t) {
|
|
117
|
+
deps.add(tConfig.per);
|
|
118
|
+
}
|
|
119
|
+
inDegree.set(t, deps.size);
|
|
120
|
+
for (const dep of deps) {
|
|
121
|
+
if (!dependents.has(dep)) dependents.set(dep, /* @__PURE__ */ new Set());
|
|
122
|
+
dependents.get(dep).add(t);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const queue = [];
|
|
126
|
+
for (const [t, deg] of inDegree) {
|
|
127
|
+
if (deg === 0) queue.push(t);
|
|
128
|
+
}
|
|
129
|
+
const sorted = [];
|
|
130
|
+
while (queue.length > 0) {
|
|
131
|
+
const current = queue.shift();
|
|
132
|
+
sorted.push(current);
|
|
133
|
+
for (const dep of dependents.get(current) ?? []) {
|
|
134
|
+
const newDeg = (inDegree.get(dep) ?? 1) - 1;
|
|
135
|
+
inDegree.set(dep, newDeg);
|
|
136
|
+
if (newDeg === 0) queue.push(dep);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const t of tables) {
|
|
140
|
+
if (!sorted.includes(t)) sorted.push(t);
|
|
141
|
+
}
|
|
142
|
+
return sorted;
|
|
143
|
+
}
|
|
144
|
+
function getDeduplicationKeys(_table, fks) {
|
|
145
|
+
if (fks.length >= 2) {
|
|
146
|
+
return fks.map((f) => f.columnKey);
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
function createSeedEngine(schema) {
|
|
151
|
+
const tables = [];
|
|
152
|
+
for (const value of Object.values(schema)) {
|
|
153
|
+
if (isTable(value)) {
|
|
154
|
+
tables.push(value);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const fkMap = /* @__PURE__ */ new Map();
|
|
158
|
+
for (const table of tables) {
|
|
159
|
+
fkMap.set(table, extractForeignKeys(table));
|
|
160
|
+
}
|
|
161
|
+
const sorted = topologicalSort(tables, fkMap);
|
|
162
|
+
return {
|
|
163
|
+
tables: sorted,
|
|
164
|
+
fkMap,
|
|
165
|
+
/**
|
|
166
|
+
* Generate rows for all tables (or a subset).
|
|
167
|
+
* Returns a map of table -> rows[].
|
|
168
|
+
*/
|
|
169
|
+
generate(tableOverrides) {
|
|
170
|
+
const generated = /* @__PURE__ */ new Map();
|
|
171
|
+
for (const table of sorted) {
|
|
172
|
+
const config = tableOverrides?.get(table) ?? tableSeedMap.get(table);
|
|
173
|
+
const fks = fkMap.get(table) ?? [];
|
|
174
|
+
const columns = getTableColumns(asTable(table));
|
|
175
|
+
const pk = findPrimaryKeyColumn(table);
|
|
176
|
+
const isAuth = isAuthUsersTable(table);
|
|
177
|
+
const dedupKeys = getDeduplicationKeys(table, fks);
|
|
178
|
+
const count = config?.count ?? 10;
|
|
179
|
+
const perTable = config?.per;
|
|
180
|
+
const parentRows = perTable ? generated.get(perTable) ?? [] : [void 0];
|
|
181
|
+
const allRows = [];
|
|
182
|
+
const seenCombos = /* @__PURE__ */ new Set();
|
|
183
|
+
let globalIndex = 0;
|
|
184
|
+
for (const parentRow of parentRows) {
|
|
185
|
+
for (let i = 0; i < count; i++) {
|
|
186
|
+
const ctx = {
|
|
187
|
+
parent: parentRow,
|
|
188
|
+
ref: (t) => {
|
|
189
|
+
const rows = generated.get(t);
|
|
190
|
+
if (!rows || rows.length === 0) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`seedConfig ctx.ref(${getTableName(asTable(t))}): no rows generated yet`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return rows[faker.number.int({ min: 0, max: rows.length - 1 })];
|
|
196
|
+
},
|
|
197
|
+
index: i,
|
|
198
|
+
all: (t) => generated.get(t) ?? []
|
|
199
|
+
};
|
|
200
|
+
const row = {};
|
|
201
|
+
for (const [key, col] of Object.entries(columns)) {
|
|
202
|
+
const colObj = col;
|
|
203
|
+
const isPk = pk?.key === key;
|
|
204
|
+
const isNullable = !colObj.notNull;
|
|
205
|
+
const customSeedFn = getColumnSeedFn(colObj);
|
|
206
|
+
if (customSeedFn) {
|
|
207
|
+
row[key] = customSeedFn(faker, ctx);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const fk = fks.find((f) => f.columnKey === key);
|
|
211
|
+
if (fk) {
|
|
212
|
+
if (perTable && parentRow && getTableName(asTable(fk.foreignTable)) === getTableName(asTable(perTable))) {
|
|
213
|
+
const foreignColumns = getTableColumns(asTable(fk.foreignTable));
|
|
214
|
+
const foreignKey = Object.entries(foreignColumns).find(
|
|
215
|
+
([, c]) => c.name === fk.foreignColumnName
|
|
216
|
+
);
|
|
217
|
+
if (foreignKey) {
|
|
218
|
+
row[key] = parentRow[foreignKey[0]];
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const refRows = generated.get(fk.foreignTable);
|
|
223
|
+
if (refRows && refRows.length > 0) {
|
|
224
|
+
const randomRef = refRows[faker.number.int({ min: 0, max: refRows.length - 1 })];
|
|
225
|
+
const foreignColumns = getTableColumns(asTable(fk.foreignTable));
|
|
226
|
+
const foreignKey = Object.entries(foreignColumns).find(
|
|
227
|
+
([, c]) => c.name === fk.foreignColumnName
|
|
228
|
+
);
|
|
229
|
+
if (foreignKey) {
|
|
230
|
+
row[key] = randomRef[foreignKey[0]];
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (isAuth && key === "email") {
|
|
236
|
+
row[key] = generateAuthEmail(globalIndex);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (isAuth && key === "name") {
|
|
240
|
+
row[key] = faker.person.fullName();
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const colAny = colObj;
|
|
244
|
+
if (colAny.defaultFn || colAny.onUpdateFn) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (colAny.hasDefault && colAny.default !== void 0) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
row[key] = generateDefaultValue(colObj, isPk, isNullable);
|
|
251
|
+
}
|
|
252
|
+
if (dedupKeys) {
|
|
253
|
+
const comboKey = dedupKeys.map((k) => String(row[k])).join(":");
|
|
254
|
+
if (seenCombos.has(comboKey)) continue;
|
|
255
|
+
seenCombos.add(comboKey);
|
|
256
|
+
}
|
|
257
|
+
allRows.push(row);
|
|
258
|
+
globalIndex++;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
generated.set(table, allRows);
|
|
262
|
+
}
|
|
263
|
+
return generated;
|
|
264
|
+
},
|
|
265
|
+
/**
|
|
266
|
+
* Generate and insert seed data into the database.
|
|
267
|
+
*/
|
|
268
|
+
async run(db, options) {
|
|
269
|
+
const generated = this.generate(options?.tableOverrides);
|
|
270
|
+
const unsafeDb = db.unsafe();
|
|
271
|
+
const transcriptLines = [];
|
|
272
|
+
for (const table of sorted) {
|
|
273
|
+
const rows = generated.get(table);
|
|
274
|
+
if (!rows || rows.length === 0) continue;
|
|
275
|
+
const tableName = getTableName(asTable(table));
|
|
276
|
+
if (options?.transcript) {
|
|
277
|
+
for (const row of rows) {
|
|
278
|
+
const colNames = Object.keys(row);
|
|
279
|
+
const values = colNames.map((k) => {
|
|
280
|
+
const v = row[k];
|
|
281
|
+
if (v === null) return "NULL";
|
|
282
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
283
|
+
if (v instanceof Date) return `'${v.toISOString()}'`;
|
|
284
|
+
return `'${String(v).replace(/'/g, "''")}'`;
|
|
285
|
+
});
|
|
286
|
+
transcriptLines.push(
|
|
287
|
+
`INSERT INTO "${tableName}" (${colNames.map((c) => `"${c}"`).join(", ")}) VALUES (${values.join(", ")});`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const ops = rows.map(
|
|
292
|
+
(row) => unsafeDb.insert(table).values(row)
|
|
293
|
+
);
|
|
294
|
+
if (ops.length === 1) {
|
|
295
|
+
await ops[0].run({});
|
|
296
|
+
} else {
|
|
297
|
+
await unsafeDb.batch(ops).run({});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (options?.transcript && transcriptLines.length > 0) {
|
|
301
|
+
try {
|
|
302
|
+
const fs = await new Function(
|
|
303
|
+
'return import("node:fs/promises")'
|
|
304
|
+
)();
|
|
305
|
+
await fs.writeFile(
|
|
306
|
+
options.transcript,
|
|
307
|
+
transcriptLines.join("\n") + "\n",
|
|
308
|
+
"utf-8"
|
|
309
|
+
);
|
|
310
|
+
} catch {
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return generated;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function createSingleTableSeed(schema, table, count) {
|
|
318
|
+
const engine = createSeedEngine(schema);
|
|
319
|
+
const overrides = /* @__PURE__ */ new Map();
|
|
320
|
+
overrides.set(table, { count });
|
|
321
|
+
for (const t of engine.tables) {
|
|
322
|
+
if (t !== table && !overrides.has(t)) {
|
|
323
|
+
overrides.set(t, { count: 0 });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
generate: () => engine.generate(overrides),
|
|
328
|
+
run: (db, options) => engine.run(db, { ...options, tableOverrides: overrides })
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function isTable(value) {
|
|
332
|
+
if (!value || typeof value !== "object") return false;
|
|
333
|
+
const symbols = Object.getOwnPropertySymbols(value);
|
|
334
|
+
return symbols.some((s) => s.toString().includes("IsDrizzleTable"));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/seed.ts
|
|
338
|
+
async function seed(db, options) {
|
|
339
|
+
const engine = createSeedEngine(db._schema);
|
|
340
|
+
await engine.run(db, options);
|
|
341
|
+
}
|
|
342
|
+
function defineSeed(config) {
|
|
343
|
+
const entries = Object.freeze(
|
|
344
|
+
config.entries.map((entry) => ({
|
|
345
|
+
table: entry.table,
|
|
346
|
+
rows: Object.freeze([...entry.rows])
|
|
347
|
+
}))
|
|
348
|
+
);
|
|
349
|
+
return {
|
|
350
|
+
entries,
|
|
351
|
+
async run(db) {
|
|
352
|
+
const unsafeDb = db.unsafe();
|
|
353
|
+
for (const entry of entries) {
|
|
354
|
+
if (entry.rows.length === 0) continue;
|
|
355
|
+
const ops = entry.rows.map(
|
|
356
|
+
(row) => unsafeDb.insert(entry.table).values(row)
|
|
357
|
+
);
|
|
358
|
+
if (ops.length === 1) {
|
|
359
|
+
await ops[0].run({});
|
|
360
|
+
} else {
|
|
361
|
+
await unsafeDb.batch(ops).run({});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
export {
|
|
368
|
+
createSeedEngine,
|
|
369
|
+
createSingleTableSeed,
|
|
370
|
+
defineSeed,
|
|
371
|
+
extractForeignKeys,
|
|
372
|
+
findPrimaryKeyColumn,
|
|
373
|
+
isTable,
|
|
374
|
+
seed,
|
|
375
|
+
seedConfig,
|
|
376
|
+
tableSeed,
|
|
377
|
+
topologicalSort
|
|
378
|
+
};
|