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.
- package/README.md +368 -364
- package/dist/app.js +6 -9
- 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 +15 -26
- package/dist/cli/config.js +6 -16
- package/dist/cli/runner.js +4 -7
- 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.js +27 -51
- 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 +15 -40
- 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 +79 -79
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.extractTemplateDependencies = extractTemplateDependencies;
|
|
4
|
+
exports.evaluateTemplate = evaluateTemplate;
|
|
5
|
+
exports.orderColumnDependencies = orderColumnDependencies;
|
|
6
|
+
const registry_1 = require("./registry");
|
|
7
|
+
function extractTemplateDependencies(template) {
|
|
8
|
+
if (!template)
|
|
9
|
+
return [];
|
|
10
|
+
const deps = [];
|
|
11
|
+
const re = /\{\{\s*([a-zA-Z0-9_]+)(?:\s*:[^}]+)?\s*\}\}/g;
|
|
12
|
+
let m;
|
|
13
|
+
while ((m = re.exec(template))) {
|
|
14
|
+
const name = m[1].trim();
|
|
15
|
+
if (!['sequence', 'random', 'year', 'uuid', 'slug', 'now', 'date'].includes(name.toLowerCase())) {
|
|
16
|
+
if (!deps.includes(name))
|
|
17
|
+
deps.push(name);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return deps;
|
|
21
|
+
}
|
|
22
|
+
function evaluateTemplate(template, rowValues, prng, rowIndex = 0) {
|
|
23
|
+
if (!template)
|
|
24
|
+
return '';
|
|
25
|
+
return template.replace(/\{\{\s*([a-zA-Z0-9_]+)(?::([^}]+))?\s*\}\}/g, (_, token, argsStr) => {
|
|
26
|
+
const t = token.trim();
|
|
27
|
+
const args = argsStr ? argsStr.split(/[:,]/).map((s) => s.trim()).filter(Boolean) : [];
|
|
28
|
+
switch (t.toLowerCase()) {
|
|
29
|
+
case 'year':
|
|
30
|
+
return String(new Date().getFullYear());
|
|
31
|
+
case 'uuid':
|
|
32
|
+
return (0, registry_1.makeUuid)(prng);
|
|
33
|
+
case 'slug':
|
|
34
|
+
return (0, registry_1.makeSlug)(prng);
|
|
35
|
+
case 'now':
|
|
36
|
+
return new Date().toISOString();
|
|
37
|
+
case 'date':
|
|
38
|
+
return new Date().toISOString().slice(0, 10);
|
|
39
|
+
case 'sequence': {
|
|
40
|
+
const start = args[0] ? Number.parseInt(args[0], 10) : 1;
|
|
41
|
+
const step = args[1] ? Number.parseInt(args[1], 10) : 1;
|
|
42
|
+
const s = Number.isFinite(start) ? start : 1;
|
|
43
|
+
const st = Number.isFinite(step) ? step : 1;
|
|
44
|
+
return String(s + rowIndex * st);
|
|
45
|
+
}
|
|
46
|
+
case 'random': {
|
|
47
|
+
const min = args[0] ? Number.parseInt(args[0], 10) : 1;
|
|
48
|
+
const max = args[1] ? Number.parseInt(args[1], 10) : 100;
|
|
49
|
+
const lo = Number.isFinite(min) ? min : 1;
|
|
50
|
+
const hi = Number.isFinite(max) ? max : 100;
|
|
51
|
+
return String(prng.int(lo, hi));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
let rawVal = rowValues[t];
|
|
55
|
+
if (rawVal === undefined) {
|
|
56
|
+
const lower = t.toLowerCase();
|
|
57
|
+
for (const [k, v] of Object.entries(rowValues)) {
|
|
58
|
+
if (k.toLowerCase() === lower) {
|
|
59
|
+
rawVal = v;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (rawVal != null) {
|
|
65
|
+
let str = String(rawVal);
|
|
66
|
+
if (template.includes('@') && template.indexOf(`{{${token}`) < template.indexOf('@')) {
|
|
67
|
+
str = str
|
|
68
|
+
.toLowerCase()
|
|
69
|
+
.trim()
|
|
70
|
+
.normalize('NFD')
|
|
71
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
72
|
+
.replace(/[^a-z0-9]+/g, '.')
|
|
73
|
+
.replace(/^\.+|\.+$/g, '');
|
|
74
|
+
}
|
|
75
|
+
return str;
|
|
76
|
+
}
|
|
77
|
+
return '';
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function orderColumnDependencies(columnNames, plans) {
|
|
81
|
+
const colSet = new Set(columnNames);
|
|
82
|
+
const inDegree = new Map();
|
|
83
|
+
const graph = new Map();
|
|
84
|
+
for (const col of columnNames) {
|
|
85
|
+
inDegree.set(col, 0);
|
|
86
|
+
graph.set(col, new Set());
|
|
87
|
+
}
|
|
88
|
+
const nameColumns = columnNames.filter((c) => ['first_name', 'last_name', 'full_name', 'fullname', 'name', 'customer_name'].includes(c.toLowerCase()));
|
|
89
|
+
for (const col of columnNames) {
|
|
90
|
+
const plan = plans[col];
|
|
91
|
+
if (plan?.strategy === 'template' && plan.template) {
|
|
92
|
+
const deps = extractTemplateDependencies(plan.template).filter((d) => colSet.has(d) && d !== col);
|
|
93
|
+
for (const dep of deps) {
|
|
94
|
+
graph.get(dep)?.add(col);
|
|
95
|
+
inDegree.set(col, (inDegree.get(col) ?? 0) + 1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (plan?.strategy === 'email' || plan?.strategy === 'username') {
|
|
99
|
+
for (const nameCol of nameColumns) {
|
|
100
|
+
if (nameCol !== col && !graph.get(nameCol)?.has(col)) {
|
|
101
|
+
graph.get(nameCol)?.add(col);
|
|
102
|
+
inDegree.set(col, (inDegree.get(col) ?? 0) + 1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const queue = [];
|
|
108
|
+
for (const [col, deg] of inDegree.entries()) {
|
|
109
|
+
if (deg === 0)
|
|
110
|
+
queue.push(col);
|
|
111
|
+
}
|
|
112
|
+
const orderedColumns = [];
|
|
113
|
+
while (queue.length > 0) {
|
|
114
|
+
const curr = queue.shift();
|
|
115
|
+
orderedColumns.push(curr);
|
|
116
|
+
for (const dependent of graph.get(curr) ?? []) {
|
|
117
|
+
const newDeg = (inDegree.get(dependent) ?? 1) - 1;
|
|
118
|
+
inDegree.set(dependent, newDeg);
|
|
119
|
+
if (newDeg === 0)
|
|
120
|
+
queue.push(dependent);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const hasCycle = orderedColumns.length < columnNames.length;
|
|
124
|
+
const cycleColumns = [];
|
|
125
|
+
if (hasCycle) {
|
|
126
|
+
for (const col of columnNames) {
|
|
127
|
+
if (!orderedColumns.includes(col))
|
|
128
|
+
cycleColumns.push(col);
|
|
129
|
+
}
|
|
130
|
+
for (const col of cycleColumns)
|
|
131
|
+
orderedColumns.push(col);
|
|
132
|
+
}
|
|
133
|
+
return { orderedColumns, hasCycle, cycleColumns };
|
|
134
|
+
}
|
package/dist/data/types.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import type {ForeignKeyInfo,WhereClause} from '../db/database';export declare const MAX_SEED_ROWS=5000;export type StrategyCategory='identity'|'contact_web'|'commerce'|'location'|'numeric'|'datetime'|'system_crypto'|'custom_control'|'relational';export type GeneratorStrategyId='skip'|'null'|'fixed'|'list'|'pattern'|'first'|'last'|'fullname'|'username'|'job'|'email'|'phone'|'url'|'domain'|'ip'|'avatar'|'company'|'currency'|'status'|'creditCard'|'city'|'country'|'state'|'postal'|'address'|'countryCode'|'latitude'|'longitude'|'words'|'sentence'|'paragraph'|'slug'|'int'|'decimal'|'sequence'|'date'|'datetime'|'time'|'timestampUnix'|'bool'|'bytes'|'json'|'hash'|'token'|'uuid'|'color'|'mac'|'fk';export interface ColumnPlan{strategy:GeneratorStrategyId;value?:string;values?:string[];min?:number;max?:number;precision?:number; from ?:string;to?:string;minLen?:number;maxLen?:number;jsonKeys?:string[];jsonValues?:Record<string,string[]>;nullPct?:number;prefix?:string;suffix?:string;start?:number;step?:number;pattern?:string;format?:string;}export interface StrategyDescriptor{id:GeneratorStrategyId;label:string;category:StrategyCategory;description?:string;}export interface ColumnGeneratorConfig{name:string;type:string;pk:boolean;notnull:boolean;hasDefault:boolean;unique:boolean;fk:ForeignKeyInfo|null;strategies:StrategyDescriptor[];defaultPlan:ColumnPlan;}export interface GenerateResult{rows:WhereClause[][];previewRows:Record<string,unknown>[];warnings:string[];}export type ErChainScope='chain'|'ancestors'|'descendants'|'all'|'single';export interface ErChainTableNode{name:string;order:number;depth:number;parents:{table:string; from :string;to:string;}[];children:{table:string; from :string;to:string;}[];columns:ColumnGeneratorConfig[];suggestedCount:number;rowCount:number;isRoot:boolean;hasSelfRef:boolean;}export interface ErChainConfig{rootTable:string;scope:ErChainScope;tables:ErChainTableNode[];cycleDetected:boolean;cycleTables?:string[];maxTotalRows:number;}export interface ErChainResult{rootTable
|
|
1
|
+
import type {ForeignKeyInfo,WhereClause} from '../db/database';export declare const MAX_SEED_ROWS=5000;export type StrategyCategory='identity'|'contact_web'|'commerce'|'location'|'numeric'|'datetime'|'system_crypto'|'custom_control'|'database_aware'|'relational';export type GeneratorStrategyId='skip'|'null'|'fixed'|'list'|'pattern'|'template'|'first'|'last'|'fullname'|'username'|'job'|'email'|'phone'|'url'|'domain'|'ip'|'avatar'|'company'|'currency'|'status'|'orderStatus'|'product'|'productCategory'|'department'|'paymentMethod'|'transactionType'|'techSkill'|'creditCard'|'city'|'country'|'state'|'postal'|'address'|'countryCode'|'latitude'|'longitude'|'words'|'sentence'|'paragraph'|'slug'|'int'|'decimal'|'sequence'|'percentage'|'date'|'datetime'|'time'|'timestampUnix'|'relativeDate'|'bool'|'bytes'|'json'|'hash'|'token'|'uuid'|'color'|'mac'|'fk'|'sampleExisting'|'sequentialFk';export interface ColumnPlan{strategy:GeneratorStrategyId;value?:string;values?:string[];template?:string;min?:number;max?:number;precision?:number; from ?:string;to?:string;minLen?:number;maxLen?:number;jsonKeys?:string[];jsonValues?:Record<string,string[]>;nullPct?:number;prefix?:string;suffix?:string;start?:number;step?:number;pattern?:string;format?:string;unique?:boolean;refTable?:string;refColumn?:string;}export interface StrategyDescriptor{id:GeneratorStrategyId;label:string;category:StrategyCategory;description?:string;}export interface ColumnGeneratorConfig{name:string;type:string;pk:boolean;notnull:boolean;hasDefault:boolean;unique:boolean;fk:ForeignKeyInfo|null;strategies:StrategyDescriptor[];defaultPlan:ColumnPlan;}export type TableGenerationMode='generate'|'use_existing'|'generate_if_empty'|'skip';export type CardinalityDistribution='uniform'|'fixed'|'normal'|'weighted';export interface RelationshipConfig{parentTable:string;parentColumn:string;childTable:string;childColumn:string;minPerParent?:number;maxPerParent?:number;distribution?:CardinalityDistribution;uniquePairs?:boolean;nullable?:boolean;nullChance?:number;}export interface TableGenerationSpec{mode:TableGenerationMode;rows?:number;columns:Record<string,ColumnPlan>;truncate?:boolean;}export interface GenerationOptions{seed?:number|null;transaction?:boolean;rollbackOnError?:boolean;truncateAll?:boolean;batchSize?:number;previewLimit?:number;}export interface GenerationPlan{name?:string;description?:string;version?:number;rootTable?:string;scope?:ErChainScope;tables:Record<string,TableGenerationSpec>;relationships?:RelationshipConfig[];options?:GenerationOptions;}export interface ValidationIssue{type:'error'|'warning'|'info';table?:string;column?:string;message:string;code?:string;}export interface ValidationReport{valid:boolean;canExecute:boolean;issues:ValidationIssue[];summary:{totalTables:number;tablesToGenerate:number;tablesToUseExisting:number;tablesToSkip:number;estimatedTotalRows:number;cyclesDetected:boolean;cycleTables:string[];executionOrder:string[];};}export interface GenerateResult{rows:WhereClause[][];previewRows:Record<string,unknown>[];warnings:string[];}export type ErChainScope='chain'|'ancestors'|'descendants'|'all'|'single';export interface ErChainTableNode{name:string;order:number;depth:number;parents:{table:string; from :string;to:string;notnull?:boolean;}[];children:{table:string; from :string;to:string;notnull?:boolean;}[];columns:ColumnGeneratorConfig[];suggestedCount:number;rowCount:number;isRoot:boolean;hasSelfRef:boolean;isJunction?:boolean;mode?:TableGenerationMode;}export interface ErChainConfig{rootTable:string;scope:ErChainScope;tables:ErChainTableNode[];cycleDetected:boolean;cycleTables?:string[];maxTotalRows:number;relationships?:RelationshipConfig[];}export interface ErChainResult{rootTable?:string;scope?:ErChainScope;executionOrder:string[];tableResults:Record<string,{count:number;rows:WhereClause[][];previewRows:Record<string,unknown>[];warnings:string[];}>;totalRows:number;sql:string;warnings:string[];validation?:ValidationReport;}export interface SeedProfile{id:string;name:string;description?:string;createdAt:string;updatedAt:string;plan:GenerationPlan;}export declare const SKIP:unique symbol;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import type {IDatabase,TableInfoData} from '../db/index';import type {GenerationPlan,ValidationReport} from './types';export declare function validateGenerationPlan(db:IDatabase,plan:GenerationPlan,tableInfos?:Map<string,TableInfoData>):Promise<ValidationReport>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateGenerationPlan = validateGenerationPlan;
|
|
4
|
+
const templates_1 = require("./templates");
|
|
5
|
+
async function validateGenerationPlan(db, plan, tableInfos) {
|
|
6
|
+
const issues = [];
|
|
7
|
+
const infos = tableInfos ?? new Map();
|
|
8
|
+
for (const tableName of Object.keys(plan.tables)) {
|
|
9
|
+
if (!infos.has(tableName)) {
|
|
10
|
+
try {
|
|
11
|
+
const res = await db.getTableInfo(tableName);
|
|
12
|
+
if (res.success && res.data?.columns?.length) {
|
|
13
|
+
infos.set(tableName, res.data);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
issues.push({
|
|
17
|
+
type: 'error',
|
|
18
|
+
table: tableName,
|
|
19
|
+
message: `Table "${tableName}" does not exist in the database.`,
|
|
20
|
+
code: 'TABLE_NOT_FOUND',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
issues.push({
|
|
26
|
+
type: 'error',
|
|
27
|
+
table: tableName,
|
|
28
|
+
message: `Failed to inspect table "${tableName}": ${err instanceof Error ? err.message : String(err)}`,
|
|
29
|
+
code: 'TABLE_INSPECT_ERROR',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
let estimatedTotalRows = 0;
|
|
35
|
+
let tablesToGenerate = 0;
|
|
36
|
+
let tablesToUseExisting = 0;
|
|
37
|
+
let tablesToSkip = 0;
|
|
38
|
+
for (const [tableName, spec] of Object.entries(plan.tables)) {
|
|
39
|
+
const info = infos.get(tableName);
|
|
40
|
+
if (!info)
|
|
41
|
+
continue;
|
|
42
|
+
const mode = spec.mode || 'generate';
|
|
43
|
+
const rowCount = Math.max(0, spec.rows ?? 10);
|
|
44
|
+
if (mode === 'generate') {
|
|
45
|
+
tablesToGenerate++;
|
|
46
|
+
estimatedTotalRows += rowCount;
|
|
47
|
+
}
|
|
48
|
+
else if (mode === 'use_existing') {
|
|
49
|
+
tablesToUseExisting++;
|
|
50
|
+
}
|
|
51
|
+
else if (mode === 'generate_if_empty') {
|
|
52
|
+
try {
|
|
53
|
+
const cRes = await db.getRowCount(tableName);
|
|
54
|
+
const curCount = cRes.success && typeof cRes.data === 'number' ? cRes.data : 0;
|
|
55
|
+
if (curCount === 0) {
|
|
56
|
+
tablesToGenerate++;
|
|
57
|
+
estimatedTotalRows += rowCount;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
tablesToUseExisting++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
tablesToGenerate++;
|
|
65
|
+
estimatedTotalRows += rowCount;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
tablesToSkip++;
|
|
70
|
+
}
|
|
71
|
+
if (mode === 'skip' || mode === 'use_existing')
|
|
72
|
+
continue;
|
|
73
|
+
const colMap = new Map(info.columns.map((c) => [c.name, c]));
|
|
74
|
+
const fkMap = new Map(info.foreignKeys.map((fk) => [fk.from, fk]));
|
|
75
|
+
const colNames = info.columns.map((c) => c.name);
|
|
76
|
+
const { hasCycle, cycleColumns } = (0, templates_1.orderColumnDependencies)(colNames, spec.columns || {});
|
|
77
|
+
if (hasCycle) {
|
|
78
|
+
issues.push({
|
|
79
|
+
type: 'error',
|
|
80
|
+
table: tableName,
|
|
81
|
+
message: `Circular column template dependency detected in table "${tableName}" between columns: ${cycleColumns.join(', ')}`,
|
|
82
|
+
code: 'TEMPLATE_CYCLE',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
for (const col of info.columns) {
|
|
86
|
+
const p = spec.columns[col.name] || { strategy: 'skip' };
|
|
87
|
+
if (p.strategy === 'template' && p.template) {
|
|
88
|
+
for (const dep of (0, templates_1.extractTemplateDependencies)(p.template)) {
|
|
89
|
+
if (!colMap.has(dep)) {
|
|
90
|
+
issues.push({
|
|
91
|
+
type: 'warning',
|
|
92
|
+
table: tableName,
|
|
93
|
+
column: col.name,
|
|
94
|
+
message: `Template in column "${col.name}" references non-existent column "${dep}".`,
|
|
95
|
+
code: 'TEMPLATE_INVALID_REF',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const isReq = Boolean(col.notnull || (col.pk ?? 0) > 0);
|
|
101
|
+
if (isReq && col.dflt_value == null) {
|
|
102
|
+
if (p.strategy === 'null') {
|
|
103
|
+
issues.push({
|
|
104
|
+
type: 'error',
|
|
105
|
+
table: tableName,
|
|
106
|
+
column: col.name,
|
|
107
|
+
message: `Column "${col.name}" is NOT NULL without a default value, but strategy is set to NULL.`,
|
|
108
|
+
code: 'NOT_NULL_VIOLATION',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (p.strategy === 'skip' && !col.pk) {
|
|
112
|
+
issues.push({
|
|
113
|
+
type: 'warning',
|
|
114
|
+
table: tableName,
|
|
115
|
+
column: col.name,
|
|
116
|
+
message: `Column "${col.name}" is NOT NULL without a default value, but strategy is set to Skip (database default).`,
|
|
117
|
+
code: 'NOT_NULL_SKIP',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const fk = fkMap.get(col.name);
|
|
122
|
+
if (p.strategy === 'fk' && fk) {
|
|
123
|
+
const parentSpec = plan.tables[fk.table];
|
|
124
|
+
if (parentSpec?.mode === 'skip') {
|
|
125
|
+
try {
|
|
126
|
+
const countRes = await db.getRowCount(fk.table);
|
|
127
|
+
const parentRows = countRes.success && typeof countRes.data === 'number' ? countRes.data : 0;
|
|
128
|
+
if (parentRows === 0 && (col.notnull || (col.pk ?? 0) > 0)) {
|
|
129
|
+
issues.push({
|
|
130
|
+
type: 'error',
|
|
131
|
+
table: tableName,
|
|
132
|
+
column: col.name,
|
|
133
|
+
message: `Foreign key "${col.name}" references table "${fk.table}" which is set to "Don't Touch" and is currently empty.`,
|
|
134
|
+
code: 'FK_EMPTY_PARENT',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch { }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const errors = issues.filter((i) => i.type === 'error');
|
|
144
|
+
const valid = errors.length === 0;
|
|
145
|
+
return {
|
|
146
|
+
valid,
|
|
147
|
+
canExecute: valid,
|
|
148
|
+
issues,
|
|
149
|
+
summary: {
|
|
150
|
+
totalTables: Object.keys(plan.tables).length,
|
|
151
|
+
tablesToGenerate,
|
|
152
|
+
tablesToUseExisting,
|
|
153
|
+
tablesToSkip,
|
|
154
|
+
estimatedTotalRows,
|
|
155
|
+
cyclesDetected: false,
|
|
156
|
+
cycleTables: [],
|
|
157
|
+
executionOrder: Object.keys(plan.tables),
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
package/dist/db/database.js
CHANGED
|
@@ -36,7 +36,7 @@ class SqliteDatabase {
|
|
|
36
36
|
constructor(dbPath, logger, options = {}) {
|
|
37
37
|
this.path = dbPath;
|
|
38
38
|
this.logger = logger;
|
|
39
|
-
this.isReadOnly =
|
|
39
|
+
this.isReadOnly = Boolean(options.readonly);
|
|
40
40
|
this.db = new better_sqlite3_1.default(dbPath, { readonly: this.isReadOnly });
|
|
41
41
|
this.db.exec('PRAGMA foreign_keys = ON;');
|
|
42
42
|
if (this.isReadOnly) {
|
|
@@ -71,8 +71,7 @@ class SqliteDatabase {
|
|
|
71
71
|
}
|
|
72
72
|
async tryRun(fn) {
|
|
73
73
|
try {
|
|
74
|
-
|
|
75
|
-
return { success: true, data };
|
|
74
|
+
return { success: true, data: await Promise.resolve(fn()) };
|
|
76
75
|
}
|
|
77
76
|
catch (err) {
|
|
78
77
|
const msg = (0, common_1.errorMessage)(err);
|
|
@@ -118,7 +117,7 @@ class SqliteDatabase {
|
|
|
118
117
|
const info = this.db.prepare(sql).run(...params);
|
|
119
118
|
return {
|
|
120
119
|
changes: Number(info.changes),
|
|
121
|
-
lastInsertRowid: info.lastInsertRowid
|
|
120
|
+
lastInsertRowid: info.lastInsertRowid == null ? null : Number(info.lastInsertRowid),
|
|
122
121
|
};
|
|
123
122
|
});
|
|
124
123
|
}
|
|
@@ -147,8 +146,7 @@ class SqliteDatabase {
|
|
|
147
146
|
return this.readonlyBlocked();
|
|
148
147
|
const trimmed = String(sql).trim().replace(/;+\s*$/, '');
|
|
149
148
|
try {
|
|
150
|
-
const
|
|
151
|
-
const info = stmt.run();
|
|
149
|
+
const info = this.db.prepare(trimmed).run();
|
|
152
150
|
return { success: true, data: { changes: Number(info.changes) } };
|
|
153
151
|
}
|
|
154
152
|
catch {
|
|
@@ -221,7 +219,7 @@ class SqliteDatabase {
|
|
|
221
219
|
const cols = fields.map((f) => (0, generator_1.quoteIdentifier)(f.column));
|
|
222
220
|
const sql = `INSERT INTO ${(0, generator_1.quoteIdentifier)(table)} (${cols.join(', ')}) VALUES (${fields.map(() => '?').join(', ')})`;
|
|
223
221
|
this.db.prepare(sql).run(...fields.map((f) => this.toBind(f.value)));
|
|
224
|
-
inserted
|
|
222
|
+
inserted++;
|
|
225
223
|
}
|
|
226
224
|
return { inserted, skipped: rows.length - inserted };
|
|
227
225
|
});
|
|
@@ -306,12 +304,10 @@ class SqliteDatabase {
|
|
|
306
304
|
for (const p of pragmas) {
|
|
307
305
|
try {
|
|
308
306
|
const row = this.db.prepare(`PRAGMA ${p}`).get();
|
|
309
|
-
if (row && p in row)
|
|
307
|
+
if (row && p in row)
|
|
310
308
|
settings[p] = String(row[p]);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
catch {
|
|
314
309
|
}
|
|
310
|
+
catch { }
|
|
315
311
|
}
|
|
316
312
|
return settings;
|
|
317
313
|
});
|
|
@@ -346,8 +342,7 @@ class SqliteDatabase {
|
|
|
346
342
|
}
|
|
347
343
|
try {
|
|
348
344
|
const colCopy = { ...col, unique: false };
|
|
349
|
-
|
|
350
|
-
this.db.exec(sql);
|
|
345
|
+
this.db.exec((0, generator_1.generateAddColumn)(table, colCopy));
|
|
351
346
|
if (col.unique) {
|
|
352
347
|
const idxName = `idx_${table}_${col.name}_unique`;
|
|
353
348
|
this.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS ${(0, generator_1.quoteIdentifier)(idxName)} ON ${(0, generator_1.quoteIdentifier)(table)} (${(0, generator_1.quoteIdentifier)(col.name)});`);
|
|
@@ -383,7 +378,7 @@ class SqliteDatabase {
|
|
|
383
378
|
}
|
|
384
379
|
const typeMatches = !newColDef.type || newColDef.type.toUpperCase() === existing.type.toUpperCase();
|
|
385
380
|
const pkMatches = newColDef.primaryKey === undefined || newColDef.primaryKey === (existing.pk > 0);
|
|
386
|
-
const notNullMatches = newColDef.notNull === undefined || newColDef.notNull === (
|
|
381
|
+
const notNullMatches = newColDef.notNull === undefined || newColDef.notNull === Boolean(existing.notnull);
|
|
387
382
|
const uniqueMatches = newColDef.unique === undefined || newColDef.unique === existingIsUnique;
|
|
388
383
|
const defaultMatches = newColDef.defaultValue === undefined || String(newColDef.defaultValue ?? '').trim() === String(existing.dflt_value ?? '').trim();
|
|
389
384
|
const fkMatches = newColDef.foreignKey === undefined || ((!newColDef.foreignKey && !existingFk) ||
|
package/dist/db/export.js
CHANGED
|
@@ -4,25 +4,15 @@ exports.generateSqlDump = generateSqlDump;
|
|
|
4
4
|
exports.generateSchemaDump = generateSchemaDump;
|
|
5
5
|
const generator_1 = require("../sql/generator");
|
|
6
6
|
async function generateSqlDump(db) {
|
|
7
|
-
const out = [];
|
|
8
|
-
if (db.dialect === 'sqlite') {
|
|
9
|
-
out.push('PRAGMA foreign_keys=OFF;');
|
|
10
|
-
out.push('BEGIN TRANSACTION;');
|
|
11
|
-
}
|
|
12
|
-
else {
|
|
13
|
-
out.push('BEGIN;');
|
|
14
|
-
}
|
|
7
|
+
const out = [db.dialect === 'sqlite' ? 'PRAGMA foreign_keys=OFF;\nBEGIN TRANSACTION;' : 'BEGIN;'];
|
|
15
8
|
const tables = await db.listTables();
|
|
16
9
|
if (!tables.success)
|
|
17
10
|
return { success: false, error: tables.error ?? 'Failed to list tables.' };
|
|
18
|
-
for (const
|
|
19
|
-
const name = t.name;
|
|
11
|
+
for (const { name } of tables.data ?? []) {
|
|
20
12
|
const create = await db.getCreateStatement(name);
|
|
21
|
-
if (create.success && create.data)
|
|
13
|
+
if (create.success && create.data)
|
|
22
14
|
out.push(create.data.trim());
|
|
23
|
-
|
|
24
|
-
const info = await db.getTableInfo(name);
|
|
25
|
-
const rows = await db.getAllRows(name);
|
|
15
|
+
const [info, rows] = await Promise.all([db.getTableInfo(name), db.getAllRows(name)]);
|
|
26
16
|
if (info.success && rows.success) {
|
|
27
17
|
const cols = info.data?.columns ?? [];
|
|
28
18
|
for (const row of rows.data ?? []) {
|
|
@@ -39,52 +29,39 @@ async function generateSchemaDump(db) {
|
|
|
39
29
|
const out = [];
|
|
40
30
|
if (db.dialect === 'sqlite') {
|
|
41
31
|
const allSchemas = await db.all(`SELECT sql FROM sqlite_master WHERE sql IS NOT NULL AND name NOT LIKE 'sqlite_%' AND name NOT LIKE '_saved_queries'`);
|
|
42
|
-
if (allSchemas.success
|
|
43
|
-
for (const row of allSchemas.data) {
|
|
44
|
-
let stmt = String(row.sql).trim();
|
|
45
|
-
if (!stmt.endsWith(';'))
|
|
46
|
-
stmt += ';';
|
|
47
|
-
out.push(stmt);
|
|
48
|
-
out.push('');
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
32
|
+
if (!allSchemas.success || !allSchemas.data) {
|
|
52
33
|
return { success: false, error: allSchemas.error ?? 'Failed to read sqlite_master.' };
|
|
53
34
|
}
|
|
35
|
+
for (const row of allSchemas.data) {
|
|
36
|
+
const stmt = String(row.sql).trim();
|
|
37
|
+
out.push(stmt.endsWith(';') ? stmt : `${stmt};`, '');
|
|
38
|
+
}
|
|
39
|
+
return { success: true, data: out.join('\n').trim() };
|
|
54
40
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (fk.on_update && fk.on_update !== 'NO ACTION')
|
|
73
|
-
action += ` ON UPDATE ${fk.on_update}`;
|
|
74
|
-
out.push(`ALTER TABLE "${name}" ADD FOREIGN KEY ("${fk.from}") REFERENCES "${fk.table}" ("${fk.to}")${action};`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if (info.data.indexes && info.data.indexes.length > 0) {
|
|
79
|
-
for (const idx of info.data.indexes) {
|
|
80
|
-
if (idx.sql) {
|
|
81
|
-
out.push(idx.sql.trim() + ';');
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
out.push('');
|
|
41
|
+
const tables = await db.listTables();
|
|
42
|
+
if (!tables.success)
|
|
43
|
+
return { success: false, error: tables.error ?? 'Failed to list tables.' };
|
|
44
|
+
for (const { name } of tables.data ?? []) {
|
|
45
|
+
const info = await db.getTableInfo(name);
|
|
46
|
+
if (!info.success || !info.data)
|
|
47
|
+
continue;
|
|
48
|
+
if (info.data.sql)
|
|
49
|
+
out.push(info.data.sql.trim());
|
|
50
|
+
for (const fk of info.data.foreignKeys ?? []) {
|
|
51
|
+
if (fk.from && fk.to && fk.table) {
|
|
52
|
+
let action = '';
|
|
53
|
+
if (fk.on_delete && fk.on_delete !== 'NO ACTION')
|
|
54
|
+
action += ` ON DELETE ${fk.on_delete}`;
|
|
55
|
+
if (fk.on_update && fk.on_update !== 'NO ACTION')
|
|
56
|
+
action += ` ON UPDATE ${fk.on_update}`;
|
|
57
|
+
out.push(`ALTER TABLE "${name}" ADD FOREIGN KEY ("${fk.from}") REFERENCES "${fk.table}" ("${fk.to}")${action};`);
|
|
86
58
|
}
|
|
87
59
|
}
|
|
60
|
+
for (const idx of info.data.indexes ?? []) {
|
|
61
|
+
if (idx.sql)
|
|
62
|
+
out.push(idx.sql.trim().endsWith(';') ? idx.sql.trim() : `${idx.sql.trim()};`);
|
|
63
|
+
}
|
|
64
|
+
out.push('');
|
|
88
65
|
}
|
|
89
66
|
return { success: true, data: out.join('\n').trim() };
|
|
90
67
|
}
|
package/dist/db/filters.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import type {FilterCondition,RowFilters,SQLInputValue} from './types';export declare
|
|
1
|
+
import type {FilterCondition,RowFilters,SQLInputValue} from './types';export declare const escapeLike:(s:string)=>string;export declare function pushFilterCondition(conds:string[],params:SQLInputValue[],quotedCol:string,c:FilterCondition):void;export declare function buildFilterClause(filters:RowFilters|undefined,availableCols:string[]):{where:string;params:SQLInputValue[];};export declare function convertPlaceholdersToPostgres(sql:string,startIdx?:number):string;
|
package/dist/db/filters.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.escapeLike =
|
|
3
|
+
exports.escapeLike = void 0;
|
|
4
4
|
exports.pushFilterCondition = pushFilterCondition;
|
|
5
5
|
exports.buildFilterClause = buildFilterClause;
|
|
6
6
|
exports.convertPlaceholdersToPostgres = convertPlaceholdersToPostgres;
|
|
7
7
|
const generator_1 = require("../sql/generator");
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
8
|
+
const escapeLike = (s) => String(s).replace(/[\\%_]/g, '\\$&');
|
|
9
|
+
exports.escapeLike = escapeLike;
|
|
11
10
|
function pushFilterCondition(conds, params, quotedCol, c) {
|
|
12
11
|
const value = String(c.value ?? '');
|
|
13
12
|
switch (c.op) {
|
|
@@ -37,17 +36,15 @@ function pushFilterCondition(conds, params, quotedCol, c) {
|
|
|
37
36
|
break;
|
|
38
37
|
case 'like':
|
|
39
38
|
conds.push(`${quotedCol} LIKE ? ESCAPE '\\'`);
|
|
40
|
-
params.push(
|
|
39
|
+
params.push(`%${(0, exports.escapeLike)(value)}%`);
|
|
41
40
|
break;
|
|
42
41
|
case 'prefix':
|
|
43
42
|
conds.push(`${quotedCol} LIKE ? ESCAPE '\\'`);
|
|
44
|
-
params.push(escapeLike(value)
|
|
43
|
+
params.push(`${(0, exports.escapeLike)(value)}%`);
|
|
45
44
|
break;
|
|
46
45
|
case 'between':
|
|
47
|
-
conds.push(`${quotedCol} >= ?`);
|
|
48
|
-
params.push(value);
|
|
49
|
-
conds.push(`${quotedCol} <= ?`);
|
|
50
|
-
params.push(String(c.max ?? ''));
|
|
46
|
+
conds.push(`${quotedCol} >= ?`, `${quotedCol} <= ?`);
|
|
47
|
+
params.push(value, String(c.max ?? ''));
|
|
51
48
|
break;
|
|
52
49
|
case 'null':
|
|
53
50
|
conds.push(`${quotedCol} IS NULL`);
|
|
@@ -55,8 +52,6 @@ function pushFilterCondition(conds, params, quotedCol, c) {
|
|
|
55
52
|
case 'notnull':
|
|
56
53
|
conds.push(`${quotedCol} IS NOT NULL`);
|
|
57
54
|
break;
|
|
58
|
-
default:
|
|
59
|
-
break;
|
|
60
55
|
}
|
|
61
56
|
}
|
|
62
57
|
function buildFilterClause(filters, availableCols) {
|
|
@@ -79,39 +74,21 @@ function buildFilterClause(filters, availableCols) {
|
|
|
79
74
|
continue;
|
|
80
75
|
}
|
|
81
76
|
const value = String(raw ?? '').trim();
|
|
82
|
-
if (value
|
|
77
|
+
if (!value)
|
|
83
78
|
continue;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
conds.push(`${q} <= ?`);
|
|
90
|
-
params.push(value.slice(2).trim());
|
|
91
|
-
}
|
|
92
|
-
else if (value.startsWith('!=')) {
|
|
93
|
-
conds.push(`${q} != ?`);
|
|
94
|
-
params.push(value.slice(2).trim());
|
|
95
|
-
}
|
|
96
|
-
else if (value.startsWith('=')) {
|
|
97
|
-
conds.push(`${q} = ?`);
|
|
98
|
-
params.push(value.slice(1).trim());
|
|
99
|
-
}
|
|
100
|
-
else if (value.startsWith('>')) {
|
|
101
|
-
conds.push(`${q} > ?`);
|
|
102
|
-
params.push(value.slice(1).trim());
|
|
103
|
-
}
|
|
104
|
-
else if (value.startsWith('<')) {
|
|
105
|
-
conds.push(`${q} < ?`);
|
|
106
|
-
params.push(value.slice(1).trim());
|
|
79
|
+
const opMatch = value.match(/^(>=|<=|!=|=|>|<)(.*)$/);
|
|
80
|
+
if (opMatch) {
|
|
81
|
+
const [, op, val] = opMatch;
|
|
82
|
+
conds.push(`${q} ${op} ?`);
|
|
83
|
+
params.push(val.trim());
|
|
107
84
|
}
|
|
108
85
|
else if (value.endsWith('*')) {
|
|
109
86
|
conds.push(`${q} LIKE ? ESCAPE '\\'`);
|
|
110
|
-
params.push(escapeLike(value.slice(0, -1))
|
|
87
|
+
params.push(`${(0, exports.escapeLike)(value.slice(0, -1))}%`);
|
|
111
88
|
}
|
|
112
89
|
else {
|
|
113
90
|
conds.push(`${q} LIKE ? ESCAPE '\\'`);
|
|
114
|
-
params.push(
|
|
91
|
+
params.push(`%${(0, exports.escapeLike)(value)}%`);
|
|
115
92
|
}
|
|
116
93
|
}
|
|
117
94
|
return { where: conds.length ? ` WHERE ${conds.join(' AND ')}` : '', params };
|
package/dist/db/introspection.js
CHANGED
|
@@ -28,8 +28,7 @@ function getIncomingFks(db, targetTable) {
|
|
|
28
28
|
refs.push({ table: t, from: fk.from, to: fk.to ?? '' });
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
catch {
|
|
32
|
-
}
|
|
31
|
+
catch { }
|
|
33
32
|
}
|
|
34
33
|
return refs;
|
|
35
34
|
}
|
|
@@ -52,7 +51,7 @@ function getTableInfoSync(db, table) {
|
|
|
52
51
|
.get(ix.name);
|
|
53
52
|
return {
|
|
54
53
|
name: ix.name,
|
|
55
|
-
unique:
|
|
54
|
+
unique: Boolean(ix.unique),
|
|
56
55
|
partial: ix.partial,
|
|
57
56
|
origin: ix.origin,
|
|
58
57
|
columns: ixCols,
|
|
@@ -90,8 +89,7 @@ function getSchemaSync(db, table) {
|
|
|
90
89
|
const info = getTableInfoSync(db, table);
|
|
91
90
|
if (!info || info.columns.length === 0)
|
|
92
91
|
throw new Error(`Table "${table}" does not exist.`);
|
|
93
|
-
const foreignKeys = info
|
|
94
|
-
const indexes = info.indexes;
|
|
92
|
+
const { foreignKeys, indexes } = info;
|
|
95
93
|
const uniqueCols = new Set();
|
|
96
94
|
const indexedCols = new Set();
|
|
97
95
|
for (const ix of indexes) {
|
|
@@ -158,8 +156,7 @@ function getReferencingTablesSync(db, table) {
|
|
|
158
156
|
const row = db.prepare(`SELECT COUNT(*) AS c FROM ${(0, generator_1.quoteIdentifier)(t)} WHERE ${conds.join(' OR ')}`).get();
|
|
159
157
|
refCount = Number(row.c);
|
|
160
158
|
}
|
|
161
|
-
catch {
|
|
162
|
-
}
|
|
159
|
+
catch { }
|
|
163
160
|
result.push({ table: t, refs: tRefs, refCount });
|
|
164
161
|
}
|
|
165
162
|
result.sort((a, b) => a.table.localeCompare(b.table));
|