schema-seed-core 0.1.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 ADDED
@@ -0,0 +1,908 @@
1
+ // src/random.ts
2
+ var Random = class {
3
+ state;
4
+ constructor(seed) {
5
+ this.state = typeof seed === "string" ? this.hashString(seed) : seed;
6
+ }
7
+ hashString(str) {
8
+ let hash = 0;
9
+ for (let i = 0; i < str.length; i++) {
10
+ const char = str.charCodeAt(i);
11
+ hash = (hash << 5) - hash + char;
12
+ hash |= 0;
13
+ }
14
+ return hash;
15
+ }
16
+ next() {
17
+ let t = this.state += 1831565813;
18
+ t = Math.imul(t ^ t >>> 15, t | 1);
19
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
20
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
21
+ }
22
+ nextInt(min, max) {
23
+ return Math.floor(this.next() * (max - min + 1)) + min;
24
+ }
25
+ pick(array, weights) {
26
+ if (!weights) {
27
+ return array[this.nextInt(0, array.length - 1)];
28
+ }
29
+ const totalWeight = weights.reduce((a, b) => a + b, 0);
30
+ let r = this.next() * totalWeight;
31
+ for (let i = 0; i < array.length; i++) {
32
+ r -= weights[i] || 0;
33
+ if (r <= 0) return array[i];
34
+ }
35
+ return array[array.length - 1];
36
+ }
37
+ boolean(probability = 0.5) {
38
+ return this.next() < probability;
39
+ }
40
+ };
41
+
42
+ // src/types.ts
43
+ var NormalizedSqlType = /* @__PURE__ */ ((NormalizedSqlType2) => {
44
+ NormalizedSqlType2["STRING"] = "string";
45
+ NormalizedSqlType2["TEXT"] = "text";
46
+ NormalizedSqlType2["INT"] = "int";
47
+ NormalizedSqlType2["BIGINT"] = "bigint";
48
+ NormalizedSqlType2["FLOAT"] = "float";
49
+ NormalizedSqlType2["DECIMAL"] = "decimal";
50
+ NormalizedSqlType2["BOOLEAN"] = "boolean";
51
+ NormalizedSqlType2["DATE"] = "date";
52
+ NormalizedSqlType2["DATETIME"] = "datetime";
53
+ NormalizedSqlType2["JSON"] = "json";
54
+ NormalizedSqlType2["UUID"] = "uuid";
55
+ NormalizedSqlType2["ENUM"] = "enum";
56
+ NormalizedSqlType2["BINARY"] = "binary";
57
+ NormalizedSqlType2["OBJECTID"] = "objectid";
58
+ return NormalizedSqlType2;
59
+ })(NormalizedSqlType || {});
60
+
61
+ // src/uniqueness.ts
62
+ var UniquenessRegistry = class {
63
+ registry = /* @__PURE__ */ new Map();
64
+ /**
65
+ * Generates a unique key for a table and column.
66
+ */
67
+ getRegistryKey(table, column) {
68
+ return `${table}.${column}`;
69
+ }
70
+ /**
71
+ * Attempts to generate a unique value using a generator function.
72
+ * If the generated value is already in the registry, it retries up to `maxRetries` times.
73
+ * If it still fails, it uses a deterministic fallback.
74
+ *
75
+ * @param table Table name
76
+ * @param column Column name
77
+ * @param generator Function that generates a value
78
+ * @param maxRetries Maximum number of retries before fallback
79
+ * @returns A unique value
80
+ */
81
+ async ensureUnique(table, column, generator, maxRetries = 100) {
82
+ const key = this.getRegistryKey(table, column);
83
+ if (!this.registry.has(key)) {
84
+ this.registry.set(key, /* @__PURE__ */ new Set());
85
+ }
86
+ const usedValues = this.registry.get(key);
87
+ for (let i = 0; i < maxRetries; i++) {
88
+ const value = await generator();
89
+ if (!usedValues.has(value)) {
90
+ usedValues.add(value);
91
+ return value;
92
+ }
93
+ }
94
+ const fallbackValue = await generator();
95
+ const finalValue = `${fallbackValue}_${usedValues.size}`;
96
+ usedValues.add(finalValue);
97
+ return finalValue;
98
+ }
99
+ /**
100
+ * Clears the registry for a specific table/column or all.
101
+ */
102
+ clear(table, column) {
103
+ if (table && column) {
104
+ this.registry.delete(this.getRegistryKey(table, column));
105
+ } else {
106
+ this.registry.clear();
107
+ }
108
+ }
109
+ };
110
+
111
+ // src/redaction.ts
112
+ var Redactor = class {
113
+ static SENSITIVE_PATTERNS = [
114
+ /password/i,
115
+ /token/i,
116
+ /secret/i,
117
+ /api_?key/i,
118
+ /auth/i,
119
+ /cookie/i,
120
+ /credit_?card/i,
121
+ /ssn/i
122
+ ];
123
+ /**
124
+ * Checks if a column name is considered sensitive.
125
+ */
126
+ static isSensitive(columnName) {
127
+ return this.SENSITIVE_PATTERNS.some((pattern) => pattern.test(columnName));
128
+ }
129
+ /**
130
+ * Redacts sensitive values in a row object.
131
+ * @param row The row data to redact
132
+ * @returns A new object with sensitive values replaced by a placeholder
133
+ */
134
+ static redactRow(row) {
135
+ const redacted = {};
136
+ for (const [key, value] of Object.entries(row)) {
137
+ if (this.isSensitive(key)) {
138
+ redacted[key] = "[REDACTED]";
139
+ } else {
140
+ redacted[key] = value;
141
+ }
142
+ }
143
+ return redacted;
144
+ }
145
+ /**
146
+ * Redacts sensitive values in a list of rows.
147
+ */
148
+ static redactRows(rows) {
149
+ return rows.map((row) => this.redactRow(row));
150
+ }
151
+ };
152
+
153
+ // src/planner/graph.ts
154
+ var DependencyGraph = class {
155
+ nodes = /* @__PURE__ */ new Map();
156
+ constructor(schema) {
157
+ for (const tableName of Object.keys(schema.tables)) {
158
+ this.nodes.set(tableName, {
159
+ tableName,
160
+ dependencies: /* @__PURE__ */ new Set(),
161
+ dependents: /* @__PURE__ */ new Set()
162
+ });
163
+ }
164
+ for (const [tableName, table] of Object.entries(schema.tables)) {
165
+ const node = this.nodes.get(tableName);
166
+ for (const fk of table.foreignKeys) {
167
+ if (fk.referencedTable === tableName) continue;
168
+ if (this.nodes.has(fk.referencedTable)) {
169
+ node.dependencies.add(fk.referencedTable);
170
+ this.nodes.get(fk.referencedTable).dependents.add(tableName);
171
+ }
172
+ }
173
+ }
174
+ }
175
+ getNodes() {
176
+ return Array.from(this.nodes.values());
177
+ }
178
+ getNode(tableName) {
179
+ return this.nodes.get(tableName);
180
+ }
181
+ };
182
+
183
+ // src/planner/toposort.ts
184
+ function topologicalSort(graph) {
185
+ const order = [];
186
+ const cycles = [];
187
+ const visited = /* @__PURE__ */ new Set();
188
+ const recStack = /* @__PURE__ */ new Set();
189
+ const path = [];
190
+ function visit(tableName) {
191
+ if (recStack.has(tableName)) {
192
+ const cycleStart = path.indexOf(tableName);
193
+ cycles.push(path.slice(cycleStart));
194
+ return;
195
+ }
196
+ if (visited.has(tableName)) return;
197
+ visited.add(tableName);
198
+ recStack.add(tableName);
199
+ path.push(tableName);
200
+ const node = graph.getNode(tableName);
201
+ if (node) {
202
+ for (const dep of node.dependencies) {
203
+ visit(dep);
204
+ }
205
+ }
206
+ recStack.delete(tableName);
207
+ path.pop();
208
+ order.push(tableName);
209
+ }
210
+ for (const node of graph.getNodes()) {
211
+ if (!visited.has(node.tableName)) {
212
+ visit(node.tableName);
213
+ }
214
+ }
215
+ return { order, cycles };
216
+ }
217
+
218
+ // src/planner/cycles.ts
219
+ function resolveCycles(cycles, schema, supportsDeferrable) {
220
+ return cycles.map((cycle) => {
221
+ if (supportsDeferrable) {
222
+ return { resolved: true, strategy: "DEFERRABLE" };
223
+ }
224
+ for (let i = 0; i < cycle.length; i++) {
225
+ const currentTable = cycle[i];
226
+ const nextTable = cycle[(i + 1) % cycle.length];
227
+ const tableSchema = schema.tables[currentTable];
228
+ const nullableFk = tableSchema.foreignKeys.find((fk) => {
229
+ if (fk.referencedTable !== nextTable) return false;
230
+ return fk.columns.every((colName) => tableSchema.columns[colName]?.nullable);
231
+ });
232
+ if (nullableFk) {
233
+ return {
234
+ resolved: true,
235
+ strategy: "NULLABLE_FK",
236
+ breakingTable: currentTable,
237
+ breakingColumn: nullableFk.columns[0]
238
+ // Simplified: just track one
239
+ };
240
+ }
241
+ }
242
+ return { resolved: false };
243
+ });
244
+ }
245
+
246
+ // src/planner/plan.ts
247
+ function createSeedPlan(schema, options, supportsDeferrable = false) {
248
+ const graph = new DependencyGraph(schema);
249
+ const { order, cycles } = topologicalSort(graph);
250
+ if (cycles.length > 0) {
251
+ const resolutions = resolveCycles(cycles, schema, supportsDeferrable);
252
+ const unresolvable = resolutions.filter((r) => !r.resolved);
253
+ if (unresolvable.length > 0) {
254
+ const cyclePaths = cycles.filter((_, i) => !resolutions[i].resolved).map((c) => c.join(" -> ")).join("\n");
255
+ throw new Error(
256
+ `Unresolvable cycles detected in schema:
257
+ ${cyclePaths}
258
+ Try making one of the foreign keys nullable or use a database that supports deferrable constraints.`
259
+ );
260
+ }
261
+ }
262
+ let finalOrder = order;
263
+ if (options.includeTables) {
264
+ const includeSet = new Set(options.includeTables);
265
+ if (options.withParents) {
266
+ const expanded = /* @__PURE__ */ new Set();
267
+ const walk = (tableName) => {
268
+ if (expanded.has(tableName)) return;
269
+ expanded.add(tableName);
270
+ const node = graph.getNode(tableName);
271
+ if (node) {
272
+ for (const dep of node.dependencies) {
273
+ walk(dep);
274
+ }
275
+ }
276
+ };
277
+ options.includeTables.forEach(walk);
278
+ finalOrder = finalOrder.filter((t) => expanded.has(t));
279
+ } else {
280
+ finalOrder = finalOrder.filter((t) => includeSet.has(t));
281
+ }
282
+ }
283
+ if (options.excludeTables) {
284
+ const excludeSet = new Set(options.excludeTables);
285
+ finalOrder = finalOrder.filter((t) => !excludeSet.has(t));
286
+ }
287
+ return {
288
+ insertOrder: finalOrder,
289
+ batches: [],
290
+ // To be populated by the generator
291
+ referencesMap: /* @__PURE__ */ new Map()
292
+ };
293
+ }
294
+ function isJoinTable(table) {
295
+ const fkColumnCount = new Set(table.foreignKeys.flatMap((fk) => fk.columns)).size;
296
+ const totalColumnCount = Object.keys(table.columns).length;
297
+ return table.foreignKeys.length >= 2 && fkColumnCount >= totalColumnCount * 0.7;
298
+ }
299
+
300
+ // src/generate/refs.ts
301
+ var ReferenceRegistry = class {
302
+ refs = /* @__PURE__ */ new Map();
303
+ /**
304
+ * Records an inserted primary key for a table.
305
+ */
306
+ addReference(table, pk) {
307
+ if (!this.refs.has(table)) {
308
+ this.refs.set(table, []);
309
+ }
310
+ this.refs.get(table).push(pk);
311
+ }
312
+ /**
313
+ * Picks a random primary key from the specified table.
314
+ */
315
+ getRandomReference(table, random) {
316
+ const tableRefs = this.refs.get(table);
317
+ if (!tableRefs || tableRefs.length === 0) {
318
+ return null;
319
+ }
320
+ return random.pick(tableRefs);
321
+ }
322
+ /**
323
+ * Returns all recorded references for a table.
324
+ */
325
+ getReferences(table) {
326
+ return this.refs.get(table) || [];
327
+ }
328
+ clear() {
329
+ this.refs.clear();
330
+ }
331
+ };
332
+
333
+ // src/generate/row.ts
334
+ async function generateRows(table, count, context) {
335
+ const rows = [];
336
+ for (let i = 0; i < count; i++) {
337
+ rows.push(await generateRow(table, { ...context, i }));
338
+ }
339
+ return rows;
340
+ }
341
+ async function generateRow(table, context) {
342
+ const row = {};
343
+ for (const column of Object.values(table.columns)) {
344
+ if (column.isAutoIncrement) continue;
345
+ const fk = table.foreignKeys.find((f) => f.columns.includes(column.name));
346
+ if (fk) {
347
+ const ref = context.refs.getRandomReference(fk.referencedTable, context.random);
348
+ if (ref !== null) {
349
+ row[column.name] = ref;
350
+ continue;
351
+ }
352
+ }
353
+ const tableOverride = context.overrides?.[table.name]?.[column.name];
354
+ const globalOverride = context.overrides?.[column.name];
355
+ const override = tableOverride !== void 0 ? tableOverride : globalOverride;
356
+ if (override !== void 0) {
357
+ if (typeof override === "function") {
358
+ row[column.name] = await override({ i: context.i, random: context.random, refs: context.refs });
359
+ continue;
360
+ } else if (typeof override === "object" && override !== null) {
361
+ if ("enum" in override) {
362
+ const values = override.enum;
363
+ const weights = override.weights;
364
+ row[column.name] = context.random.pick(values, weights);
365
+ continue;
366
+ } else if ("dateBetween" in override) {
367
+ const [start, end] = override.dateBetween;
368
+ const startTime = new Date(start).getTime();
369
+ const endTime = new Date(end).getTime();
370
+ const randomTime = context.random.nextInt(startTime, endTime);
371
+ row[column.name] = new Date(randomTime).toISOString();
372
+ continue;
373
+ }
374
+ }
375
+ row[column.name] = override;
376
+ continue;
377
+ }
378
+ const { generatorId, options } = context.inferGenerator(column.name, column.type);
379
+ const generator = context.generators[generatorId];
380
+ if (generator) {
381
+ const isUnique = table.uniqueConstraints.some((uc) => uc.columns.includes(column.name)) || table.primaryKey?.columns.includes(column.name);
382
+ if (isUnique) {
383
+ row[column.name] = await context.uniqueness.ensureUnique(
384
+ table.name,
385
+ column.name,
386
+ () => generator({ ...context, options })
387
+ );
388
+ } else {
389
+ row[column.name] = generator({ ...context, options });
390
+ }
391
+ } else {
392
+ row[column.name] = generateDefaultValue(column, context.random);
393
+ }
394
+ }
395
+ return row;
396
+ }
397
+ function generateDefaultValue(column, random) {
398
+ if (column.defaultValue !== void 0) return column.defaultValue;
399
+ if (column.nullable && random.boolean(0.1)) return null;
400
+ switch (column.type) {
401
+ case "int" /* INT */:
402
+ case "bigint" /* BIGINT */:
403
+ return random.nextInt(1, 1e3);
404
+ case "float" /* FLOAT */:
405
+ case "decimal" /* DECIMAL */:
406
+ return parseFloat((random.next() * 100).toFixed(2));
407
+ case "boolean" /* BOOLEAN */:
408
+ return random.boolean();
409
+ case "date" /* DATE */:
410
+ case "datetime" /* DATETIME */:
411
+ return (/* @__PURE__ */ new Date()).toISOString();
412
+ case "json" /* JSON */:
413
+ return { mock: "data" };
414
+ case "enum" /* ENUM */:
415
+ return column.enumValues ? random.pick(column.enumValues) : "VALUE";
416
+ default:
417
+ return "mock-string";
418
+ }
419
+ }
420
+
421
+ // src/plugins.ts
422
+ async function loadPlugins(options, context) {
423
+ if (!options.plugins) return;
424
+ for (const pluginRef of options.plugins) {
425
+ let plugin;
426
+ if (typeof pluginRef === "string") {
427
+ try {
428
+ const module = await import(pluginRef);
429
+ plugin = module.default || module;
430
+ } catch (err) {
431
+ throw new Error(`Failed to load plugin ${pluginRef}: ${err.message}`);
432
+ }
433
+ } else {
434
+ plugin = pluginRef;
435
+ }
436
+ if (plugin.generators) {
437
+ context.generators = { ...context.generators, ...plugin.generators };
438
+ }
439
+ if (plugin.overrides) {
440
+ options.overrides = mergeOverrides(options.overrides || {}, plugin.overrides);
441
+ }
442
+ if (plugin.hooks) {
443
+ options.hooks = mergeHooks(options.hooks || {}, plugin.hooks);
444
+ }
445
+ }
446
+ }
447
+ function mergeOverrides(base, plugin) {
448
+ const merged = { ...base };
449
+ for (const [table, tableOverrides] of Object.entries(plugin)) {
450
+ merged[table] = { ...merged[table] || {}, ...tableOverrides };
451
+ }
452
+ return merged;
453
+ }
454
+ function mergeHooks(base, plugin) {
455
+ return {
456
+ beforeInsert: async (table, rows) => {
457
+ let currentRows = rows;
458
+ if (base.beforeInsert) currentRows = await base.beforeInsert(table, currentRows);
459
+ if (plugin.beforeInsert) currentRows = await plugin.beforeInsert(table, currentRows);
460
+ return currentRows;
461
+ },
462
+ afterInsert: async (table, result) => {
463
+ if (base.afterInsert) await base.afterInsert(table, result);
464
+ if (plugin.afterInsert) await plugin.afterInsert(table, result);
465
+ }
466
+ };
467
+ }
468
+
469
+ // src/validation.ts
470
+ function validateOptions(options) {
471
+ if (options.overrides) {
472
+ for (const [tableName, tableOverrides] of Object.entries(options.overrides)) {
473
+ if (typeof tableOverrides !== "object" || tableOverrides === null) {
474
+ throw new Error(`Invalid override for table "${tableName}": must be an object.`);
475
+ }
476
+ for (const [columnName, override] of Object.entries(tableOverrides)) {
477
+ validateOverride(tableName, columnName, override);
478
+ }
479
+ }
480
+ }
481
+ }
482
+ function validateOverride(table, column, override) {
483
+ if (typeof override === "function") return;
484
+ if (typeof override === "object" && override !== null) {
485
+ if ("enum" in override) {
486
+ if (!Array.isArray(override.enum)) {
487
+ throw new Error(`Invalid enum override for "${table}.${column}": "enum" must be an array.`);
488
+ }
489
+ if (override.weights && (!Array.isArray(override.weights) || override.weights.length !== override.enum.length)) {
490
+ throw new Error(`Invalid weights for "${table}.${column}": "weights" must be an array of the same length as "enum".`);
491
+ }
492
+ return;
493
+ }
494
+ if ("dateBetween" in override) {
495
+ if (!Array.isArray(override.dateBetween) || override.dateBetween.length !== 2) {
496
+ throw new Error(`Invalid dateBetween override for "${table}.${column}": must be an array of [start, end].`);
497
+ }
498
+ return;
499
+ }
500
+ }
501
+ }
502
+
503
+ // src/runner.ts
504
+ function checkProductionSafety(options, dbUrl) {
505
+ if (options.allowProduction) return;
506
+ if (process.env.NODE_ENV === "production") {
507
+ throw new Error("Refusing to run in production environment (NODE_ENV=production). Use --allow-production to override.");
508
+ }
509
+ if (dbUrl) {
510
+ const prodKeywords = ["prod", "production", "live", "cloud", "aws", "azure", "gcp"];
511
+ try {
512
+ const url = new URL(dbUrl.includes("://") ? dbUrl : `sqlite://${dbUrl}`);
513
+ const host = url.hostname.toLowerCase();
514
+ if (prodKeywords.some((kw) => host.includes(kw))) {
515
+ throw new Error(`Refusing to run against a potential production database (${host}). Use --allow-production to override.`);
516
+ }
517
+ } catch (e) {
518
+ const lowerUrl = dbUrl.toLowerCase();
519
+ if (prodKeywords.some((kw) => lowerUrl.includes(kw))) {
520
+ throw new Error("Refusing to run against a potential production database. Use --allow-production to override.");
521
+ }
522
+ }
523
+ }
524
+ }
525
+ async function runSeedSql(adapter, schema, plan, options, context) {
526
+ const startTime = Date.now();
527
+ const report = {
528
+ success: true,
529
+ durationMs: 0,
530
+ tables: {},
531
+ errors: []
532
+ };
533
+ const random = new Random(options.seed || Date.now());
534
+ const uniqueness = new UniquenessRegistry();
535
+ const refs = new ReferenceRegistry();
536
+ await loadPlugins(options, context);
537
+ validateOptions(options);
538
+ checkProductionSafety(options, adapter.config?.connectionString || adapter.config);
539
+ try {
540
+ await adapter.connect();
541
+ if (options.truncate && plan.insertOrder.length > 0) {
542
+ if (!options.allowProduction) {
543
+ }
544
+ if (!options.dryRun) {
545
+ await adapter.truncateTables([...plan.insertOrder].reverse());
546
+ }
547
+ }
548
+ if (!options.dryRun) {
549
+ await adapter.begin();
550
+ }
551
+ for (const tableName of plan.insertOrder) {
552
+ const tableStartTime = Date.now();
553
+ const tableSchema = schema.tables[tableName];
554
+ const rowCount = typeof options.rows === "number" ? options.rows : options.rows?.[tableName] ?? 10;
555
+ try {
556
+ const genCtx = {
557
+ random,
558
+ uniqueness,
559
+ refs,
560
+ generators: context.generators,
561
+ inferGenerator: context.inferGenerator,
562
+ overrides: context.overrides
563
+ };
564
+ let rows = await generateRows(tableSchema, rowCount, genCtx);
565
+ if (!options.dryRun) {
566
+ if (options.hooks?.beforeInsert) {
567
+ rows = await options.hooks.beforeInsert(tableName, rows);
568
+ }
569
+ const batchSize = options.batchSize || 1e3;
570
+ for (let i = 0; i < rows.length; i += batchSize) {
571
+ const batch = rows.slice(i, i + batchSize);
572
+ await adapter.insertBatch({ tableName, rows: batch });
573
+ }
574
+ for (const row of rows) {
575
+ if (tableSchema.primaryKey) {
576
+ const pkCol = tableSchema.primaryKey.columns[0];
577
+ if (row[pkCol] !== void 0) {
578
+ refs.addReference(tableName, row[pkCol]);
579
+ }
580
+ }
581
+ }
582
+ }
583
+ report.tables[tableName] = {
584
+ insertedCount: rows.length,
585
+ durationMs: Date.now() - tableStartTime
586
+ };
587
+ if (!options.dryRun && options.hooks?.afterInsert) {
588
+ await options.hooks.afterInsert(tableName, report.tables[tableName]);
589
+ }
590
+ } catch (err) {
591
+ report.success = false;
592
+ report.tables[tableName] = {
593
+ insertedCount: 0,
594
+ durationMs: Date.now() - tableStartTime,
595
+ error: err.message
596
+ };
597
+ report.errors.push(err);
598
+ if (!options.dryRun) {
599
+ await adapter.rollback();
600
+ throw err;
601
+ }
602
+ }
603
+ }
604
+ if (!options.dryRun) {
605
+ await adapter.commit();
606
+ }
607
+ } catch (err) {
608
+ report.success = false;
609
+ report.errors.push(err);
610
+ } finally {
611
+ await adapter.disconnect();
612
+ report.durationMs = Date.now() - startTime;
613
+ }
614
+ return report;
615
+ }
616
+
617
+ // src/mongo/planner.ts
618
+ function createMongoPlan(config) {
619
+ const collections = Object.keys(config.mongodb.collections);
620
+ const adj = /* @__PURE__ */ new Map();
621
+ for (const [name, coll] of Object.entries(config.mongodb.collections)) {
622
+ const deps = [];
623
+ for (const field of Object.values(coll.fields)) {
624
+ if (typeof field === "object" && field.ref) {
625
+ const [refColl] = field.ref.split(".");
626
+ if (!config.mongodb.collections[refColl]) {
627
+ throw new Error(`Reference ${field.ref} not found. Ensure ${refColl} collection is defined in config.`);
628
+ }
629
+ if (refColl !== name) {
630
+ deps.push(refColl);
631
+ }
632
+ }
633
+ }
634
+ adj.set(name, deps);
635
+ }
636
+ const visited = /* @__PURE__ */ new Set();
637
+ const visiting = /* @__PURE__ */ new Set();
638
+ const order = [];
639
+ function visit(name) {
640
+ if (visiting.has(name)) {
641
+ throw new Error(`Circular reference detected involving collection: ${name}`);
642
+ }
643
+ if (visited.has(name)) return;
644
+ visiting.add(name);
645
+ for (const dep of adj.get(name) || []) {
646
+ visit(dep);
647
+ }
648
+ visiting.delete(name);
649
+ visited.add(name);
650
+ order.push(name);
651
+ }
652
+ for (const name of collections) {
653
+ visit(name);
654
+ }
655
+ return order;
656
+ }
657
+
658
+ // src/mongo/generator.ts
659
+ function generateMongoDocument(fields, ctx) {
660
+ const doc = {};
661
+ for (const [name, config] of Object.entries(fields)) {
662
+ doc[name] = generateFieldValue(config, ctx);
663
+ }
664
+ return doc;
665
+ }
666
+ function generateFieldValue(config, ctx) {
667
+ if (typeof config === "string") {
668
+ return generateByType(config, {}, ctx);
669
+ }
670
+ const cfg = config;
671
+ if (cfg.ref) {
672
+ const [refColl, refField] = cfg.ref.split(".");
673
+ const val = ctx.refs.getRandomReference(refColl, ctx.random);
674
+ if (val === null) {
675
+ throw new Error(`Reference ${cfg.ref} not found. Ensure ${refColl} collection is seeded first.`);
676
+ }
677
+ return val;
678
+ }
679
+ if (cfg.type === "enum") {
680
+ return ctx.random.pick(cfg.values || [], cfg.weights);
681
+ }
682
+ if (cfg.type === "object") {
683
+ return generateMongoDocument(cfg.fields || {}, ctx);
684
+ }
685
+ if (cfg.type === "array") {
686
+ const count = ctx.random.nextInt(cfg.minItems ?? 1, cfg.maxItems ?? 5);
687
+ return Array.from({ length: count }, () => generateFieldValue(cfg.of, ctx));
688
+ }
689
+ return generateByType(cfg.type, cfg, ctx);
690
+ }
691
+ function generateByType(type, config, ctx) {
692
+ switch (type) {
693
+ case "objectId":
694
+ return generateObjectId(ctx.random);
695
+ case "int":
696
+ return ctx.random.nextInt(config.min ?? 0, config.max ?? 1e6);
697
+ case "float":
698
+ case "decimal":
699
+ return parseFloat((ctx.random.next() * ((config.max ?? 100) - (config.min ?? 0)) + (config.min ?? 0)).toFixed(2));
700
+ case "boolean":
701
+ return ctx.random.boolean();
702
+ case "date":
703
+ return new Date(ctx.random.nextInt(0, Date.now())).toISOString();
704
+ case "dateRecent":
705
+ return ctx.generators.dateRecent(ctx);
706
+ case "dateBetween": {
707
+ const from = new Date(config.from ?? "2020-01-01").getTime();
708
+ const to = new Date(config.to ?? Date.now()).getTime();
709
+ return new Date(ctx.random.nextInt(from, to)).toISOString();
710
+ }
711
+ case "email":
712
+ return ctx.generators.email(ctx);
713
+ case "firstName":
714
+ return ctx.generators.firstName(ctx);
715
+ case "lastName":
716
+ return ctx.generators.lastName(ctx);
717
+ case "fullName":
718
+ return ctx.generators.fullName(ctx);
719
+ case "city":
720
+ return ctx.generators.city(ctx);
721
+ case "country":
722
+ return ctx.generators.country(ctx);
723
+ case "street":
724
+ return ctx.generators.address(ctx);
725
+ case "phone":
726
+ return ctx.generators.phone(ctx);
727
+ case "uuid":
728
+ return ctx.generators.uuid(ctx);
729
+ case "string":
730
+ return ctx.generators.firstName(ctx);
731
+ // Fallback
732
+ default:
733
+ if (ctx.generators[type]) {
734
+ return ctx.generators[type](ctx);
735
+ }
736
+ return null;
737
+ }
738
+ }
739
+ function generateObjectId(random) {
740
+ const chars = "0123456789abcdef";
741
+ let id = "";
742
+ for (let i = 0; i < 24; i++) {
743
+ id += chars[random.nextInt(0, 15)];
744
+ }
745
+ return id;
746
+ }
747
+
748
+ // src/mongo/runner.ts
749
+ function checkProductionSafety2(options, dbUrl) {
750
+ if (options.allowProduction) return;
751
+ if (process.env.NODE_ENV === "production") {
752
+ throw new Error("Refusing to run in production environment (NODE_ENV=production). Use --allow-production to override.");
753
+ }
754
+ if (dbUrl) {
755
+ const prodKeywords = ["prod", "production", "live", "cloud", "aws", "azure", "gcp"];
756
+ try {
757
+ const url = new URL(dbUrl.includes("://") ? dbUrl : `sqlite://${dbUrl}`);
758
+ const host = url.hostname.toLowerCase();
759
+ if (prodKeywords.some((kw) => host.includes(kw))) {
760
+ throw new Error(`Refusing to run against a potential production database (${host}). Use --allow-production to override.`);
761
+ }
762
+ } catch (e) {
763
+ const lowerUrl = dbUrl.toLowerCase();
764
+ if (prodKeywords.some((kw) => lowerUrl.includes(kw))) {
765
+ throw new Error("Refusing to run against a potential production database. Use --allow-production to override.");
766
+ }
767
+ }
768
+ }
769
+ }
770
+ async function runSeedMongo(adapter, config, options = {}, context) {
771
+ const startTime = Date.now();
772
+ const report = {
773
+ success: true,
774
+ durationMs: 0,
775
+ tables: {},
776
+ errors: []
777
+ };
778
+ const random = new Random(config.seed || Date.now());
779
+ const refs = new ReferenceRegistry();
780
+ checkProductionSafety2(options, config.mongodb.uri);
781
+ if (!config.mongodb?.collections || Object.keys(config.mongodb.collections).length === 0) {
782
+ throw new Error("No collections defined in MongoDB config.");
783
+ }
784
+ for (const [name, coll] of Object.entries(config.mongodb.collections)) {
785
+ if (coll.rows <= 0) {
786
+ throw new Error(`Collection ${name} must have rows > 0.`);
787
+ }
788
+ if (!coll.fields || Object.keys(coll.fields).length === 0) {
789
+ throw new Error(`Collection ${name} must have fields defined.`);
790
+ }
791
+ for (const [fieldName, field] of Object.entries(coll.fields)) {
792
+ if (typeof field === "object" && field !== null && "type" in field && field.type === "enum") {
793
+ const enumField = field;
794
+ if (enumField.weights && enumField.values && enumField.weights.length !== enumField.values.length) {
795
+ throw new Error(`Enum weights and values length mismatch in ${name}.${fieldName}`);
796
+ }
797
+ }
798
+ }
799
+ }
800
+ try {
801
+ await adapter.connect();
802
+ const plan = createMongoPlan(config);
803
+ for (const collName of plan) {
804
+ const collStartTime = Date.now();
805
+ const collConfig = config.mongodb.collections[collName];
806
+ const docs = [];
807
+ try {
808
+ for (let i = 0; i < collConfig.rows; i++) {
809
+ const doc = generateMongoDocument(collConfig.fields, {
810
+ random,
811
+ refs,
812
+ generators: context.generators
813
+ });
814
+ docs.push(doc);
815
+ }
816
+ if (!options.dryRun) {
817
+ await adapter.insertMany(collName, docs);
818
+ for (const doc of docs) {
819
+ if (doc._id) {
820
+ refs.addReference(collName, doc._id);
821
+ }
822
+ }
823
+ }
824
+ report.tables[collName] = {
825
+ insertedCount: docs.length,
826
+ durationMs: Date.now() - collStartTime
827
+ };
828
+ } catch (err) {
829
+ report.success = false;
830
+ report.tables[collName] = {
831
+ insertedCount: 0,
832
+ durationMs: Date.now() - collStartTime,
833
+ error: err.message
834
+ };
835
+ report.errors.push(err);
836
+ }
837
+ }
838
+ } catch (err) {
839
+ report.success = false;
840
+ report.errors.push(err);
841
+ } finally {
842
+ await adapter.disconnect();
843
+ report.durationMs = Date.now() - startTime;
844
+ }
845
+ return report;
846
+ }
847
+
848
+ // src/reporters/console.ts
849
+ function reportToConsole(report) {
850
+ console.log("\n\u{1F680} Seed Report");
851
+ console.log("====================================");
852
+ console.log(`Status: ${report.success ? "\u2705 Success" : "\u274C Failed"}`);
853
+ console.log(`Duration: ${report.durationMs}ms`);
854
+ console.log("------------------------------------");
855
+ console.table(
856
+ Object.entries(report.tables).map(([name, stats]) => ({
857
+ Table: name,
858
+ Rows: stats.insertedCount,
859
+ Time: `${stats.durationMs}ms`,
860
+ Status: stats.error ? "\u274C" : "\u2705"
861
+ }))
862
+ );
863
+ if (report.errors.length > 0) {
864
+ console.log("\nErrors:");
865
+ report.errors.forEach((err, i) => {
866
+ console.log(`${i + 1}. ${err.message}`);
867
+ });
868
+ }
869
+ console.log("====================================\n");
870
+ }
871
+
872
+ // src/reporters/json.ts
873
+ function reportToJson(report) {
874
+ return JSON.stringify(report, (key, value) => {
875
+ if (value instanceof Error) {
876
+ return { message: value.message, stack: value.stack };
877
+ }
878
+ return value;
879
+ }, 2);
880
+ }
881
+
882
+ // src/index.ts
883
+ var version = "0.0.1";
884
+ function defineSeed(options) {
885
+ return options;
886
+ }
887
+ export {
888
+ DependencyGraph,
889
+ NormalizedSqlType,
890
+ Random,
891
+ Redactor,
892
+ ReferenceRegistry,
893
+ UniquenessRegistry,
894
+ createSeedPlan,
895
+ defineSeed,
896
+ generateRow,
897
+ generateRows,
898
+ isJoinTable,
899
+ loadPlugins,
900
+ reportToConsole,
901
+ reportToJson,
902
+ resolveCycles,
903
+ runSeedMongo,
904
+ runSeedSql,
905
+ topologicalSort,
906
+ version
907
+ };
908
+ //# sourceMappingURL=index.js.map