admindb 2.2.1 → 2.3.1
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/README.md +368 -364
- package/dist/app.js +25 -16
- package/dist/auth/config.d.ts +1 -1
- package/dist/auth/config.js +18 -30
- package/dist/auth/crypto.js +11 -21
- package/dist/auth/middleware.js +9 -16
- package/dist/auth/routes.js +9 -15
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.js +20 -29
- package/dist/cli/config.d.ts +1 -1
- package/dist/cli/config.js +15 -18
- package/dist/cli/runner.js +13 -13
- package/dist/data/chain.d.ts +1 -1
- package/dist/data/chain.js +40 -617
- package/dist/data/datasets.d.ts +1 -1
- package/dist/data/datasets.js +43 -1
- package/dist/data/detector.js +45 -21
- package/dist/data/engine.d.ts +1 -1
- package/dist/data/engine.js +483 -142
- package/dist/data/index.d.ts +1 -1
- package/dist/data/index.js +8 -0
- package/dist/data/prng.d.ts +1 -0
- package/dist/data/prng.js +73 -0
- package/dist/data/registry.d.ts +1 -0
- package/dist/data/registry.js +805 -0
- package/dist/data/schema-graph.d.ts +1 -0
- package/dist/data/schema-graph.js +210 -0
- package/dist/data/strategies.d.ts +1 -1
- package/dist/data/strategies.js +44 -283
- package/dist/data/templates.d.ts +1 -0
- package/dist/data/templates.js +134 -0
- package/dist/data/types.d.ts +1 -1
- package/dist/data/validator.d.ts +1 -0
- package/dist/data/validator.js +160 -0
- package/dist/db/database.js +9 -14
- package/dist/db/export.js +32 -55
- package/dist/db/filters.d.ts +1 -1
- package/dist/db/filters.js +15 -38
- package/dist/db/introspection.js +4 -7
- package/dist/db/manager.d.ts +1 -1
- package/dist/db/manager.js +48 -53
- package/dist/db/migrations.js +7 -14
- package/dist/db/postgres.js +30 -78
- package/dist/index.d.ts +1 -1
- package/dist/public/css/app.css +1 -1
- package/dist/public/js/browse.js +1 -1
- package/dist/public/js/inspector.js +1 -1
- package/dist/public/js/query.js +1 -1
- package/dist/public/js/seed.js +1 -1
- package/dist/routes/api/erd.js +3 -8
- package/dist/routes/api/helpers.d.ts +1 -1
- package/dist/routes/api/helpers.js +75 -25
- package/dist/routes/api/import-export.js +1 -2
- package/dist/routes/api/query.js +2 -6
- package/dist/routes/api/rows.js +4 -8
- package/dist/routes/api/seed.js +35 -21
- package/dist/routes/api/tables.js +15 -30
- package/dist/routes/databases.js +8 -26
- package/dist/routes/pages.js +67 -18
- package/dist/serverless.js +31 -44
- package/dist/sql/generator.d.ts +1 -1
- package/dist/sql/generator.js +35 -109
- package/dist/types/api.d.ts +1 -1
- package/dist/utils/common.d.ts +1 -1
- package/dist/utils/common.js +45 -85
- package/dist/utils/csv.d.ts +1 -1
- package/dist/utils/csv.js +11 -17
- package/dist/utils/datatype.js +46 -60
- package/dist/utils/icons.js +8 -8
- package/dist/views/layouts/main.hbs +1 -87
- package/dist/views/pages/databases.hbs +1 -233
- package/dist/views/pages/designer.hbs +1 -89
- package/dist/views/pages/erd.hbs +1 -1241
- package/dist/views/pages/error.hbs +1 -10
- package/dist/views/pages/form.hbs +1 -59
- package/dist/views/pages/home.hbs +1 -220
- package/dist/views/pages/info.hbs +1 -107
- package/dist/views/pages/login.hbs +1 -98
- package/dist/views/pages/query.hbs +1 -129
- package/dist/views/pages/schema.hbs +1 -298
- package/dist/views/pages/seed-select.hbs +251 -0
- package/dist/views/pages/seed.hbs +256 -414
- package/dist/views/pages/table.hbs +1 -327
- package/dist/views/partials/navbar.hbs +1 -83
- package/dist/views/partials/sidebar.hbs +1 -129
- package/docs/API.md +149 -149
- package/docs/EXAMPLES.md +579 -579
- package/docs/SECURITY.md +265 -265
- package/package.json +80 -79
package/dist/data/engine.js
CHANGED
|
@@ -1,33 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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 (
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
153
|
-
|
|
206
|
+
catch {
|
|
207
|
+
mode = 'generate';
|
|
154
208
|
}
|
|
155
209
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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 (
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
+
}
|