admindb 2.2.1 → 2.3.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.
Files changed (87) hide show
  1. package/README.md +368 -364
  2. package/dist/app.js +6 -9
  3. package/dist/auth/config.d.ts +1 -1
  4. package/dist/auth/config.js +18 -30
  5. package/dist/auth/crypto.js +11 -21
  6. package/dist/auth/middleware.js +9 -16
  7. package/dist/auth/routes.js +9 -15
  8. package/dist/cli/args.d.ts +1 -1
  9. package/dist/cli/args.js +15 -26
  10. package/dist/cli/config.js +6 -16
  11. package/dist/cli/runner.js +4 -7
  12. package/dist/data/chain.d.ts +1 -1
  13. package/dist/data/chain.js +40 -617
  14. package/dist/data/datasets.d.ts +1 -1
  15. package/dist/data/datasets.js +43 -1
  16. package/dist/data/detector.js +45 -21
  17. package/dist/data/engine.d.ts +1 -1
  18. package/dist/data/engine.js +483 -142
  19. package/dist/data/index.d.ts +1 -1
  20. package/dist/data/index.js +8 -0
  21. package/dist/data/prng.d.ts +1 -0
  22. package/dist/data/prng.js +73 -0
  23. package/dist/data/registry.d.ts +1 -0
  24. package/dist/data/registry.js +805 -0
  25. package/dist/data/schema-graph.d.ts +1 -0
  26. package/dist/data/schema-graph.js +210 -0
  27. package/dist/data/strategies.d.ts +1 -1
  28. package/dist/data/strategies.js +44 -283
  29. package/dist/data/templates.d.ts +1 -0
  30. package/dist/data/templates.js +134 -0
  31. package/dist/data/types.d.ts +1 -1
  32. package/dist/data/validator.d.ts +1 -0
  33. package/dist/data/validator.js +160 -0
  34. package/dist/db/database.js +9 -14
  35. package/dist/db/export.js +32 -55
  36. package/dist/db/filters.d.ts +1 -1
  37. package/dist/db/filters.js +15 -38
  38. package/dist/db/introspection.js +4 -7
  39. package/dist/db/manager.js +27 -51
  40. package/dist/db/migrations.js +7 -14
  41. package/dist/db/postgres.js +30 -78
  42. package/dist/index.d.ts +1 -1
  43. package/dist/public/css/app.css +1 -1
  44. package/dist/public/js/browse.js +1 -1
  45. package/dist/public/js/inspector.js +1 -1
  46. package/dist/public/js/query.js +1 -1
  47. package/dist/public/js/seed.js +1 -1
  48. package/dist/routes/api/erd.js +3 -8
  49. package/dist/routes/api/helpers.d.ts +1 -1
  50. package/dist/routes/api/helpers.js +75 -25
  51. package/dist/routes/api/import-export.js +1 -2
  52. package/dist/routes/api/query.js +2 -6
  53. package/dist/routes/api/rows.js +4 -8
  54. package/dist/routes/api/seed.js +35 -21
  55. package/dist/routes/api/tables.js +15 -30
  56. package/dist/routes/databases.js +8 -26
  57. package/dist/routes/pages.js +67 -18
  58. package/dist/serverless.js +15 -40
  59. package/dist/sql/generator.d.ts +1 -1
  60. package/dist/sql/generator.js +35 -109
  61. package/dist/types/api.d.ts +1 -1
  62. package/dist/utils/common.d.ts +1 -1
  63. package/dist/utils/common.js +45 -85
  64. package/dist/utils/csv.d.ts +1 -1
  65. package/dist/utils/csv.js +11 -17
  66. package/dist/utils/datatype.js +46 -60
  67. package/dist/utils/icons.js +8 -8
  68. package/dist/views/layouts/main.hbs +1 -87
  69. package/dist/views/pages/databases.hbs +1 -233
  70. package/dist/views/pages/designer.hbs +1 -89
  71. package/dist/views/pages/erd.hbs +1 -1241
  72. package/dist/views/pages/error.hbs +1 -10
  73. package/dist/views/pages/form.hbs +1 -59
  74. package/dist/views/pages/home.hbs +1 -220
  75. package/dist/views/pages/info.hbs +1 -107
  76. package/dist/views/pages/login.hbs +1 -98
  77. package/dist/views/pages/query.hbs +1 -129
  78. package/dist/views/pages/schema.hbs +1 -298
  79. package/dist/views/pages/seed-select.hbs +251 -0
  80. package/dist/views/pages/seed.hbs +256 -414
  81. package/dist/views/pages/table.hbs +1 -327
  82. package/dist/views/partials/navbar.hbs +1 -83
  83. package/dist/views/partials/sidebar.hbs +1 -129
  84. package/docs/API.md +149 -149
  85. package/docs/EXAMPLES.md +579 -579
  86. package/docs/SECURITY.md +265 -265
  87. package/package.json +79 -79
@@ -1,33 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.generateRows = generateRows;
3
+ exports.SeedEngine = void 0;
4
4
  exports.buildSeedInsertSql = buildSeedInsertSql;
5
+ exports.buildChainInsertSql = buildChainInsertSql;
6
+ exports.generateRows = generateRows;
5
7
  const generator_1 = require("../sql/generator");
6
- const types_1 = require("./types");
7
8
  const detector_1 = require("./detector");
8
- const strategies_1 = require("./strategies");
9
- async function sampleFkValues(db, fk) {
10
- if (!fk)
11
- return [];
12
- try {
13
- const ref = await db.getTableInfo(fk.table);
14
- if (!ref.success || !ref.data || !ref.data.columns.length)
15
- return [];
16
- const refCol = fk.to || ref.data.primaryKey[0] || ref.data.columns[0].name;
17
- if (!refCol)
18
- return [];
19
- const r = await db.all(`SELECT DISTINCT ${(0, generator_1.quoteIdentifier)(refCol)} AS v FROM ${(0, generator_1.quoteIdentifier)(fk.table)} WHERE ${(0, generator_1.quoteIdentifier)(refCol)} IS NOT NULL LIMIT 1000`);
20
- return (r.data ?? []).map((row) => row.v).filter((v) => v != null);
21
- }
22
- catch {
23
- return [];
24
- }
25
- }
26
- function stringifyKey(v) {
27
- if (v instanceof Uint8Array || Buffer.isBuffer(v))
28
- return `buf:${Buffer.from(v).toString('hex')}`;
29
- return `${typeof v}:${String(v)}`;
30
- }
9
+ const prng_1 = require("./prng");
10
+ const registry_1 = require("./registry");
11
+ const schema_graph_1 = require("./schema-graph");
12
+ const templates_1 = require("./templates");
13
+ const types_1 = require("./types");
14
+ const validator_1 = require("./validator");
15
+ const stringifyKey = (v) => v instanceof Uint8Array || Buffer.isBuffer(v) ? `buf:${Buffer.from(v).toString('hex')}` : `${typeof v}:${String(v)}`;
31
16
  function orderedValue(value, base, dir, orEqual) {
32
17
  const v = value;
33
18
  const b = base;
@@ -68,23 +53,19 @@ function fitStringLength(val, len) {
68
53
  if (!len || typeof val !== 'string')
69
54
  return val;
70
55
  let v = val;
56
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
71
57
  if (len.exact != null) {
72
58
  if (v.length > len.exact)
73
59
  v = v.slice(0, len.exact);
74
- const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
75
- while (v.length < len.exact) {
60
+ while (v.length < len.exact)
76
61
  v += chars[Math.floor(Math.random() * chars.length)];
77
- }
78
62
  return v;
79
63
  }
80
- if (len.max != null && v.length > len.max) {
64
+ if (len.max != null && v.length > len.max)
81
65
  v = v.slice(0, len.max);
82
- }
83
66
  if (len.min != null && v.length < len.min) {
84
- const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
85
- while (v.length < len.min) {
67
+ while (v.length < len.min)
86
68
  v += chars[Math.floor(Math.random() * chars.length)];
87
- }
88
69
  }
89
70
  return v;
90
71
  }
@@ -108,132 +89,441 @@ function applyOrdering(fields, ordering) {
108
89
  }
109
90
  }
110
91
  }
111
- async function generateRows(db, info, configs, count, plan = {}, truncate = false) {
112
- const fkByColumn = new Map();
113
- for (const fk of info.foreignKeys)
114
- fkByColumn.set(fk.from, fk);
115
- const uniqueCols = new Set();
116
- for (const cfg of configs)
117
- if (cfg.unique)
118
- uniqueCols.add(cfg.name);
119
- const checkInfo = (0, detector_1.parseChecks)(info.sql ?? null);
120
- const warnings = [];
121
- const prepared = [];
122
- const fkPools = new Map();
123
- const fkOmit = new Set();
124
- for (const col of info.columns) {
125
- const cfg = configs.find((c) => c.name === col.name);
126
- const raw = plan[col.name];
127
- const p = raw && raw.strategy ? raw : (cfg?.defaultPlan ?? { strategy: 'skip' });
128
- if (p.strategy === 'list' && (!p.values || !p.values.length)) {
129
- const allowed = (0, detector_1.findInMap)(checkInfo.columnAllowed, col.name);
130
- if (allowed && allowed.length) {
131
- p.values = [...allowed];
92
+ async function sampleColumnValues(db, table, column, limit = 1000) {
93
+ try {
94
+ const r = await db.all(`SELECT DISTINCT ${(0, generator_1.quoteIdentifier)(column)} AS v FROM ${(0, generator_1.quoteIdentifier)(table)} WHERE ${(0, generator_1.quoteIdentifier)(column)} IS NOT NULL LIMIT ${limit}`);
95
+ return (r.data ?? []).map((row) => row.v).filter((v) => v != null);
96
+ }
97
+ catch {
98
+ return [];
99
+ }
100
+ }
101
+ class SeedEngine {
102
+ static async executePlan(db, plan, optionsOverride) {
103
+ const options = {
104
+ seed: plan.options?.seed ?? null,
105
+ transaction: plan.options?.transaction ?? true,
106
+ rollbackOnError: plan.options?.rollbackOnError ?? true,
107
+ truncateAll: plan.options?.truncateAll ?? false,
108
+ batchSize: plan.options?.batchSize ?? 500,
109
+ previewLimit: plan.options?.previewLimit ?? 50,
110
+ ...optionsOverride,
111
+ };
112
+ const prng = (0, prng_1.createPrng)(options.seed);
113
+ const warnings = [];
114
+ const tableListRes = await db.listTables();
115
+ const tableNames = (tableListRes.data ?? [])
116
+ .map((t) => t.name)
117
+ .filter((name) => !name.startsWith('sqlite_') && name !== '_saved_queries');
118
+ const tableInfos = new Map();
119
+ const allTableData = [];
120
+ for (const name of tableNames) {
121
+ const info = await db.getTableInfo(name);
122
+ if (info.success && info.data) {
123
+ tableInfos.set(name, info.data);
124
+ allTableData.push(info.data);
132
125
  }
133
126
  }
134
- if (!cfg || !cfg.strategies.some((s) => s.id === p.strategy)) {
135
- throw new Error(`Unsupported generator strategy "${p.strategy}" for column "${col.name}".`);
127
+ const validation = await (0, validator_1.validateGenerationPlan)(db, plan, tableInfos);
128
+ for (const issue of validation.issues) {
129
+ if (issue.type === 'warning')
130
+ warnings.push(`[${issue.table || 'Plan'}] ${issue.message}`);
136
131
  }
137
- if (p.strategy === 'list' && !(p.values ?? []).some((v) => String(v).trim() !== '')) {
138
- warnings.push(`Column "${col.name}": the list is empty, so it will be skipped (DB default applies).`);
132
+ const graph = (0, schema_graph_1.buildSchemaGraph)(allTableData);
133
+ if (plan.scope && plan.scope !== 'single' && plan.rootTable) {
134
+ const scopeNodes = (0, schema_graph_1.resolveScopeTables)(graph, plan.rootTable, plan.scope);
135
+ for (const node of scopeNodes) {
136
+ if (!plan.tables[node.name]) {
137
+ const colConfigs = (0, detector_1.buildColumnConfigs)(node.info);
138
+ const columns = {};
139
+ colConfigs.forEach((c) => {
140
+ columns[c.name] = c.defaultPlan;
141
+ });
142
+ plan.tables[node.name] = {
143
+ mode: 'generate',
144
+ rows: node.depth === 0 ? 5 : node.depth === 1 ? 15 : 25,
145
+ columns,
146
+ truncate: options.truncateAll,
147
+ };
148
+ }
149
+ }
139
150
  }
140
- if ((p.strategy === 'skip' || p.strategy === 'null') && !col.pk && col.notnull && col.dflt_value == null) {
141
- warnings.push(`Column "${col.name}" is NOT NULL without a default — "${p.strategy}" rows may fail to insert.`);
151
+ if (plan.scope && plan.scope !== 'single') {
152
+ let expanded = true;
153
+ while (expanded) {
154
+ expanded = false;
155
+ const currentPlanTables = Object.keys(plan.tables);
156
+ for (const tName of currentPlanTables) {
157
+ const spec = plan.tables[tName];
158
+ if (spec && spec.mode === 'skip')
159
+ continue;
160
+ const info = tableInfos.get(tName);
161
+ if (!info)
162
+ continue;
163
+ for (const fk of info.foreignKeys) {
164
+ if (fk.table && fk.table !== tName && !plan.tables[fk.table] && tableInfos.has(fk.table)) {
165
+ const countRes = await db.getRowCount(fk.table);
166
+ const curCount = countRes.success && typeof countRes.data === 'number' ? countRes.data : 0;
167
+ if (curCount === 0) {
168
+ const pInfo = tableInfos.get(fk.table);
169
+ const pColConfigs = (0, detector_1.buildColumnConfigs)(pInfo);
170
+ const pColumns = {};
171
+ pColConfigs.forEach((c) => {
172
+ pColumns[c.name] = c.defaultPlan;
173
+ });
174
+ plan.tables[fk.table] = {
175
+ mode: 'generate',
176
+ rows: 10,
177
+ columns: pColumns,
178
+ truncate: options.truncateAll,
179
+ };
180
+ expanded = true;
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
142
186
  }
143
- if (p.strategy === 'fk') {
144
- const pool = await sampleFkValues(db, fkByColumn.get(col.name) ?? null);
145
- fkPools.set(col.name, pool);
146
- const refTable = fkByColumn.get(col.name)?.table ?? '?';
147
- if (!pool.length) {
148
- if (col.notnull && !col.pk) {
149
- fkOmit.add(col.name);
150
- warnings.push(`Column "${col.name}" is a NOT NULL foreign key to "${refTable}", which has no rows. It is omitted from generated rows — seed "${refTable}" first so the insert can reference real values.`);
187
+ const planTableNames = Object.keys(plan.tables);
188
+ const planNodes = planTableNames.map((t) => graph.nodes.get(t)).filter((n) => n != null);
189
+ const { sorted } = (0, schema_graph_1.sortTopologically)(planNodes.length ? planNodes : Array.from(graph.nodes.values()));
190
+ const executionOrder = sorted.map((s) => s.name).filter((name) => plan.tables[name] !== undefined);
191
+ const relationalPool = new Map();
192
+ const tableResults = {};
193
+ let totalRows = 0;
194
+ for (const tableName of executionOrder) {
195
+ const spec = plan.tables[tableName];
196
+ const info = tableInfos.get(tableName);
197
+ if (!info)
198
+ continue;
199
+ let mode = spec.mode || 'generate';
200
+ if (mode === 'generate_if_empty') {
201
+ try {
202
+ const countRes = await db.getRowCount(tableName);
203
+ const curCount = countRes.success && typeof countRes.data === 'number' ? countRes.data : 0;
204
+ mode = curCount === 0 ? 'generate' : 'use_existing';
151
205
  }
152
- else {
153
- warnings.push(`Column "${col.name}": "${refTable}" is empty, so NULL will be inserted.`);
206
+ catch {
207
+ mode = 'generate';
154
208
  }
155
209
  }
156
- }
157
- prepared.push({ col, plan: p });
158
- }
159
- const rows = [];
160
- const previewRows = [];
161
- const uniqueSeen = new Map();
162
- if (!truncate) {
163
- for (const col of uniqueCols) {
164
- try {
165
- const existing = await db.all(`SELECT ${(0, generator_1.quoteIdentifier)(col)} AS v FROM ${(0, generator_1.quoteIdentifier)(info.table)} WHERE ${(0, generator_1.quoteIdentifier)(col)} IS NOT NULL LIMIT 10000`);
166
- const seen = new Set();
167
- for (const r of (existing.data ?? [])) {
168
- if (r.v != null)
169
- seen.add(stringifyKey(r.v));
170
- }
171
- uniqueSeen.set(col, seen);
172
- }
173
- catch { }
174
- }
175
- }
176
- for (let i = 0; i < count; i++) {
177
- const fields = [];
178
- const previewRow = {};
179
- let rowValid = true;
180
- let attempt = 0;
181
- while (attempt < 100 && rowValid) {
182
- fields.length = 0;
183
- let allColsValid = true;
184
- for (const { col, plan: p } of prepared) {
185
- if (p.strategy === 'fk' && fkOmit.has(col.name)) {
186
- previewRow[col.name] = null;
187
- continue;
188
- }
189
- let v = (0, strategies_1.generateOne)(col, p, fkPools.get(col.name), i + attempt * count);
190
- if (v === types_1.SKIP) {
191
- previewRow[col.name] = '(default)';
192
- continue;
193
- }
194
- if (uniqueCols.has(col.name)) {
195
- const seen = uniqueSeen.get(col.name) ?? new Set();
196
- let singleAtt = 0;
197
- while (seen.has(stringifyKey(v)) && singleAtt < 100) {
198
- v = (0, strategies_1.generateOne)(col, p, fkPools.get(col.name), i + attempt * count + singleAtt * 100);
199
- singleAtt += 1;
210
+ if (mode === 'skip') {
211
+ tableResults[tableName] = { count: 0, rows: [], previewRows: [], warnings: [] };
212
+ continue;
213
+ }
214
+ const pkColName = info.primaryKey[0] || info.columns[0]?.name || 'id';
215
+ if (mode === 'use_existing') {
216
+ for (const col of info.columns) {
217
+ const pool = await sampleColumnValues(db, tableName, col.name, 1000);
218
+ if (pool.length)
219
+ relationalPool.set(`${tableName}.${col.name}`, pool);
220
+ }
221
+ tableResults[tableName] = { count: 0, rows: [], previewRows: [], warnings: [] };
222
+ continue;
223
+ }
224
+ let requestedCount = Math.max(1, Math.min(types_1.MAX_SEED_ROWS, spec.rows ?? 10));
225
+ const checkInfo = (0, detector_1.parseChecks)(info.sql ?? null);
226
+ const fkByColumn = new Map(info.foreignKeys.map((fk) => [fk.from, fk]));
227
+ const uniqueCols = new Set();
228
+ const compositeUniques = [];
229
+ for (const col of info.columns) {
230
+ if (spec.columns[col.name]?.unique)
231
+ uniqueCols.add(col.name);
232
+ }
233
+ for (const ix of info.indexes ?? []) {
234
+ if (ix.unique) {
235
+ if (ix.columns.length === 1 && ix.columns[0])
236
+ uniqueCols.add(ix.columns[0]);
237
+ else if (ix.columns.length > 1)
238
+ compositeUniques.push(ix.columns);
239
+ }
240
+ }
241
+ if (info.primaryKey.length > 1 && !compositeUniques.some((cu) => cu.join(',') === info.primaryKey.join(','))) {
242
+ compositeUniques.push(info.primaryKey);
243
+ }
244
+ if (info.sql) {
245
+ const uqRe = /UNIQUE\s*\(\s*([^)]+)\s*\)/gi;
246
+ let uqM;
247
+ while ((uqM = uqRe.exec(info.sql))) {
248
+ const rawCols = uqM[1].split(',').map((s) => s.trim().replace(/[`"\[\]]/g, ''));
249
+ if (rawCols.length === 1 && rawCols[0])
250
+ uniqueCols.add(rawCols[0]);
251
+ else if (rawCols.length > 1 && !compositeUniques.some((cu) => cu.join(',') === rawCols.join(','))) {
252
+ compositeUniques.push(rawCols);
253
+ }
254
+ }
255
+ }
256
+ const fkPools = new Map();
257
+ const samplePools = new Map();
258
+ const fkOmit = new Set();
259
+ for (const col of info.columns) {
260
+ const p = spec.columns[col.name] || { strategy: 'skip' };
261
+ if (p.strategy === 'sampleExisting') {
262
+ const refT = p.refTable || tableName;
263
+ const refC = p.refColumn || col.name;
264
+ samplePools.set(col.name, await sampleColumnValues(db, refT, refC, 1000));
265
+ }
266
+ if (p.strategy === 'fk' || p.strategy === 'sequentialFk') {
267
+ const fk = fkByColumn.get(col.name);
268
+ const refTable = p.refTable || fk?.table || '';
269
+ let refCol = p.refColumn || fk?.to || (refTable ? tableInfos.get(refTable)?.primaryKey[0] : 'id') || 'id';
270
+ const poolKey = `${refTable}.${refCol}`;
271
+ let pool = relationalPool.get(poolKey) ?? [];
272
+ if (!pool.length) {
273
+ for (const [k, v] of relationalPool.entries()) {
274
+ if (k.toLowerCase() === poolKey.toLowerCase() && v.length) {
275
+ pool = v;
276
+ break;
277
+ }
278
+ }
279
+ }
280
+ if (!pool.length) {
281
+ for (const [k, v] of relationalPool.entries()) {
282
+ const [tName] = k.split('.');
283
+ if (tName && tName.toLowerCase() === refTable.toLowerCase() && v.length) {
284
+ pool = v;
285
+ break;
286
+ }
287
+ }
288
+ }
289
+ if (!pool.length && refTable) {
290
+ pool = await sampleColumnValues(db, refTable, refCol, 1000);
200
291
  }
201
- if (seen.has(stringifyKey(v))) {
202
- allColsValid = false;
203
- break;
292
+ if (!pool.length) {
293
+ const isSelfFk = fkByColumn.get(col.name)?.table === tableName;
294
+ if (isSelfFk) {
295
+ }
296
+ else if (col.notnull || (col.pk ?? 0) > 0) {
297
+ if (col.dflt_value != null) {
298
+ fkOmit.add(col.name);
299
+ }
300
+ else {
301
+ pool = [1];
302
+ warnings.push(`Table "${tableName}" Column "${col.name}" is a NOT NULL foreign key to "${refTable}". Parent table had no records; fallback ID 1 was used.`);
303
+ }
304
+ }
305
+ else {
306
+ warnings.push(`Table "${tableName}" Column "${col.name}": "${refTable}" has no rows; NULL will be used.`);
307
+ }
308
+ }
309
+ fkPools.set(col.name, pool);
310
+ }
311
+ }
312
+ const uniqueSeen = new Map();
313
+ const compositeSeen = new Map();
314
+ for (const cu of compositeUniques)
315
+ compositeSeen.set(cu.join('::'), new Set());
316
+ if (!spec.truncate && !options.truncateAll) {
317
+ for (const col of uniqueCols) {
318
+ try {
319
+ const existing = await sampleColumnValues(db, tableName, col, 10000);
320
+ uniqueSeen.set(col, new Set(existing.map(stringifyKey)));
204
321
  }
322
+ catch { }
205
323
  }
206
- v = fitStringLength(v, (0, detector_1.findInMap)(checkInfo.columnLength, col.name));
207
- fields.push({ column: col.name, value: v });
208
- previewRow[col.name] = v;
209
324
  }
210
- if (allColsValid) {
211
- break;
325
+ let pkStartOffset = 0;
326
+ for (const col of info.columns) {
327
+ if (col.pk && (col.type || '').toUpperCase().includes('INT')) {
328
+ try {
329
+ const maxRes = await db.all(`SELECT MAX(${(0, generator_1.quoteIdentifier)(col.name)}) AS mx FROM ${(0, generator_1.quoteIdentifier)(tableName)}`);
330
+ const maxVal = (maxRes.data ?? [])[0]?.mx;
331
+ if (typeof maxVal === 'number' && Number.isFinite(maxVal))
332
+ pkStartOffset = maxVal;
333
+ }
334
+ catch { }
335
+ }
212
336
  }
213
- attempt += 1;
214
- }
215
- if (attempt >= 100) {
216
- warnings.push(`Stopped generating rows early at row ${i} because unique generator exhausted possible values without collision.`);
217
- break;
218
- }
219
- for (const f of fields) {
220
- if (uniqueCols.has(f.column)) {
221
- const seen = uniqueSeen.get(f.column) ?? new Set();
222
- seen.add(stringifyKey(f.value));
223
- uniqueSeen.set(f.column, seen);
337
+ const allColNames = info.columns.map((c) => c.name);
338
+ const { orderedColumns } = (0, templates_1.orderColumnDependencies)(allColNames, spec.columns);
339
+ const colMap = new Map(info.columns.map((c) => [c.name, c]));
340
+ const isUniqueCol = (name) => {
341
+ const norm = name.toLowerCase().replace(/[`"\[\]]/g, '');
342
+ for (const u of uniqueCols) {
343
+ if (u.toLowerCase().replace(/[`"\[\]]/g, '') === norm)
344
+ return true;
345
+ }
346
+ return false;
347
+ };
348
+ for (const colName of orderedColumns) {
349
+ const p = spec.columns[colName];
350
+ const col = colMap.get(colName);
351
+ if (col && p && p.strategy === 'fk' && ((info.primaryKey.length === 1 && col.pk) || isUniqueCol(col.name))) {
352
+ const pool = fkPools.get(col.name);
353
+ if (pool && pool.length > 0 && pool.length < requestedCount)
354
+ requestedCount = pool.length;
355
+ }
224
356
  }
357
+ if (compositeUniques.length > 0) {
358
+ for (const cu of compositeUniques) {
359
+ const isAllFk = cu.every((cName) => {
360
+ const p = spec.columns[cName];
361
+ return p && (p.strategy === 'fk' || p.strategy === 'sequentialFk');
362
+ });
363
+ if (isAllFk) {
364
+ const maxCombos = cu.reduce((acc, cName) => acc * Math.max(1, (fkPools.get(cName) ?? []).length), 1);
365
+ if (maxCombos > 0 && maxCombos < requestedCount)
366
+ requestedCount = maxCombos;
367
+ }
368
+ }
369
+ }
370
+ const rows = [];
371
+ const previewRows = [];
372
+ const tableGeneratedPkVals = {};
373
+ for (let i = 0; i < requestedCount; i++) {
374
+ let fields = [];
375
+ let previewRow = {};
376
+ let rowValid = false;
377
+ let attempt = 0;
378
+ while (!rowValid && attempt < 35) {
379
+ fields = [];
380
+ previewRow = {};
381
+ const rowValues = {};
382
+ for (const colName of orderedColumns) {
383
+ const col = colMap.get(colName);
384
+ const p = spec.columns[colName] || { strategy: 'skip' };
385
+ if ((p.strategy === 'fk' || p.strategy === 'sequentialFk') && fkOmit.has(col.name)) {
386
+ previewRow[col.name] = null;
387
+ rowValues[col.name] = null;
388
+ continue;
389
+ }
390
+ const isSelfFk = fkByColumn.get(col.name)?.table === tableName;
391
+ let pool = fkPools.get(col.name);
392
+ if (isSelfFk) {
393
+ const selfIds = tableGeneratedPkVals[fkByColumn.get(col.name)?.to || pkColName] ?? [];
394
+ pool = (i === 0 || selfIds.length === 0 || (prng.chance(35) && !col.notnull)) ? [] : selfIds;
395
+ }
396
+ let v = p.strategy === 'template' && p.template
397
+ ? (0, templates_1.evaluateTemplate)(p.template, rowValues, prng, i + attempt * 50)
398
+ : (0, registry_1.generateColumnValue)(col, p, prng, i + attempt * 50, rowValues, pool, samplePools.get(col.name));
399
+ if (v === types_1.SKIP && col.pk) {
400
+ const isInt = (col.type || '').toUpperCase().includes('INT') || (col.type || '').toUpperCase().includes('SERIAL') || col.type === '' || col.type === 'NUMBER';
401
+ if (isInt)
402
+ v = pkStartOffset + i + 1;
403
+ }
404
+ if ((col.notnull || (col.pk ?? 0) > 0) && col.dflt_value == null && (v == null || v === types_1.SKIP)) {
405
+ if (p.strategy === 'fk' || p.strategy === 'sequentialFk') {
406
+ v = pool && pool.length > 0 ? pool[0] : 1;
407
+ }
408
+ else if (col.pk) {
409
+ v = pkStartOffset + i + 1;
410
+ }
411
+ else {
412
+ const fbPlan = registry_1.GeneratorRegistry.get('words') ? { strategy: 'words', minLen: 1, maxLen: 3 } : { strategy: 'skip' };
413
+ v = (0, registry_1.generateColumnValue)(col, fbPlan, prng, i + attempt * 50, rowValues);
414
+ if (v == null || v === types_1.SKIP) {
415
+ v = (col.type || '').toUpperCase().includes('INT') ? i + 1 : `val_${i + 1}`;
416
+ }
417
+ }
418
+ }
419
+ if (v === types_1.SKIP) {
420
+ previewRow[col.name] = '(default)';
421
+ rowValues[col.name] = null;
422
+ continue;
423
+ }
424
+ if (isUniqueCol(col.name)) {
425
+ const seen = uniqueSeen.get(col.name) ?? new Set();
426
+ if (p.strategy === 'fk' || p.strategy === 'sequentialFk') {
427
+ const unused = (pool ?? []).filter((x) => !seen.has(stringifyKey(x)));
428
+ if (unused.length > 0) {
429
+ v = unused[0];
430
+ }
431
+ else {
432
+ rowValid = false;
433
+ break;
434
+ }
435
+ }
436
+ else {
437
+ let singleAtt = 0;
438
+ while (seen.has(stringifyKey(v)) && singleAtt < 50) {
439
+ v = (0, registry_1.generateColumnValue)(col, p, prng, i + attempt * 50 + singleAtt * 100, rowValues, pool, samplePools.get(col.name));
440
+ singleAtt++;
441
+ }
442
+ if (seen.has(stringifyKey(v))) {
443
+ rowValid = false;
444
+ break;
445
+ }
446
+ }
447
+ }
448
+ v = fitStringLength(v, (0, detector_1.findInMap)(checkInfo.columnLength, col.name));
449
+ fields.push({ column: col.name, value: v });
450
+ previewRow[col.name] = v;
451
+ rowValues[col.name] = v;
452
+ }
453
+ applyOrdering(fields, checkInfo.ordering);
454
+ for (const f of fields)
455
+ previewRow[f.column] = f.value;
456
+ let compositeCollision = false;
457
+ if (compositeUniques.length > 0) {
458
+ const fieldMap = new Map(fields.map((f) => [f.column, f.value]));
459
+ for (const cu of compositeUniques) {
460
+ const key = cu.join('::');
461
+ const compositeVal = cu.map((cName) => stringifyKey(fieldMap.get(cName))).join('|');
462
+ const seenSet = compositeSeen.get(key) ?? new Set();
463
+ if (seenSet.has(compositeVal)) {
464
+ compositeCollision = true;
465
+ break;
466
+ }
467
+ }
468
+ }
469
+ if (!compositeCollision && fields.length > 0) {
470
+ rowValid = true;
471
+ if (compositeUniques.length > 0) {
472
+ const fieldMap = new Map(fields.map((f) => [f.column, f.value]));
473
+ for (const cu of compositeUniques) {
474
+ const key = cu.join('::');
475
+ const compositeVal = cu.map((cName) => stringifyKey(fieldMap.get(cName))).join('|');
476
+ const seenSet = compositeSeen.get(key) ?? new Set();
477
+ seenSet.add(compositeVal);
478
+ compositeSeen.set(key, seenSet);
479
+ }
480
+ }
481
+ }
482
+ attempt++;
483
+ }
484
+ if (!rowValid)
485
+ break;
486
+ for (const f of fields) {
487
+ if (isUniqueCol(f.column)) {
488
+ const seen = uniqueSeen.get(f.column) ?? new Set();
489
+ seen.add(stringifyKey(f.value));
490
+ uniqueSeen.set(f.column, seen);
491
+ }
492
+ const poolKey = `${tableName}.${f.column}`;
493
+ const cur = relationalPool.get(poolKey) ?? [];
494
+ cur.push(f.value);
495
+ relationalPool.set(poolKey, cur);
496
+ const selfCur = tableGeneratedPkVals[f.column] ?? [];
497
+ selfCur.push(f.value);
498
+ tableGeneratedPkVals[f.column] = selfCur;
499
+ }
500
+ rows.push(fields);
501
+ if (previewRows.length < (options.previewLimit ?? 50)) {
502
+ previewRows.push(previewRow);
503
+ }
504
+ }
505
+ tableResults[tableName] = {
506
+ count: rows.length,
507
+ rows,
508
+ previewRows,
509
+ warnings: [],
510
+ };
511
+ totalRows += rows.length;
225
512
  }
226
- applyOrdering(fields, checkInfo.ordering);
227
- for (const f of fields) {
228
- previewRow[f.column] = f.value;
229
- }
230
- rows.push(fields);
231
- if (previewRows.length < 50) {
232
- previewRows.push(previewRow);
233
- }
513
+ const sql = buildChainInsertSql(executionOrder, tableResults);
514
+ return {
515
+ rootTable: plan.rootTable,
516
+ scope: plan.scope,
517
+ executionOrder,
518
+ tableResults,
519
+ totalRows,
520
+ sql,
521
+ warnings,
522
+ validation,
523
+ };
234
524
  }
235
- return { rows, previewRows, warnings };
236
525
  }
526
+ exports.SeedEngine = SeedEngine;
237
527
  function buildSeedInsertSql(table, rows) {
238
528
  return rows
239
529
  .map((fields) => {
@@ -243,3 +533,54 @@ function buildSeedInsertSql(table, rows) {
243
533
  })
244
534
  .join('\n');
245
535
  }
536
+ function buildChainInsertSql(executionOrder, tableResults) {
537
+ const sqlParts = [
538
+ '-- AdminDB Relational Seed Script',
539
+ `-- Order of execution: ${executionOrder.join(' -> ')}`,
540
+ 'BEGIN TRANSACTION;\n',
541
+ ];
542
+ for (const table of executionOrder) {
543
+ const res = tableResults[table];
544
+ if (!res || !res.rows.length)
545
+ continue;
546
+ sqlParts.push('-- --------------------------------------------------------');
547
+ sqlParts.push(`-- Table: ${table} (${res.rows.length} rows)`);
548
+ sqlParts.push('-- --------------------------------------------------------');
549
+ for (const fields of res.rows) {
550
+ if (!fields.length)
551
+ continue;
552
+ const cols = fields.map((f) => (0, generator_1.quoteIdentifier)(f.column)).join(', ');
553
+ const vals = fields.map((f) => (0, generator_1.sqlValue)(f.value)).join(', ');
554
+ sqlParts.push(`INSERT INTO ${(0, generator_1.quoteIdentifier)(table)} (${cols}) VALUES (${vals});`);
555
+ }
556
+ sqlParts.push('');
557
+ }
558
+ sqlParts.push('COMMIT;');
559
+ return sqlParts.join('\n');
560
+ }
561
+ async function generateRows(db, info, configs, count, plan = {}, truncate = false) {
562
+ const tableSpec = {
563
+ mode: 'generate',
564
+ rows: count,
565
+ columns: {},
566
+ truncate,
567
+ };
568
+ for (const col of info.columns) {
569
+ const raw = plan[col.name];
570
+ const cfg = configs.find((c) => c.name === col.name);
571
+ tableSpec.columns[col.name] = raw && raw.strategy ? raw : (cfg?.defaultPlan ?? { strategy: 'skip' });
572
+ }
573
+ const generationPlan = {
574
+ rootTable: info.table,
575
+ scope: 'single',
576
+ tables: { [info.table]: tableSpec },
577
+ options: { truncateAll: truncate },
578
+ };
579
+ const res = await SeedEngine.executePlan(db, generationPlan);
580
+ const tableRes = res.tableResults[info.table] || { rows: [], previewRows: [], warnings: [] };
581
+ return {
582
+ rows: tableRes.rows,
583
+ previewRows: tableRes.previewRows,
584
+ warnings: [...res.warnings, ...tableRes.warnings],
585
+ };
586
+ }