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.
Files changed (89) hide show
  1. package/README.md +368 -364
  2. package/dist/app.js +25 -16
  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 +20 -29
  10. package/dist/cli/config.d.ts +1 -1
  11. package/dist/cli/config.js +15 -18
  12. package/dist/cli/runner.js +13 -13
  13. package/dist/data/chain.d.ts +1 -1
  14. package/dist/data/chain.js +40 -617
  15. package/dist/data/datasets.d.ts +1 -1
  16. package/dist/data/datasets.js +43 -1
  17. package/dist/data/detector.js +45 -21
  18. package/dist/data/engine.d.ts +1 -1
  19. package/dist/data/engine.js +483 -142
  20. package/dist/data/index.d.ts +1 -1
  21. package/dist/data/index.js +8 -0
  22. package/dist/data/prng.d.ts +1 -0
  23. package/dist/data/prng.js +73 -0
  24. package/dist/data/registry.d.ts +1 -0
  25. package/dist/data/registry.js +805 -0
  26. package/dist/data/schema-graph.d.ts +1 -0
  27. package/dist/data/schema-graph.js +210 -0
  28. package/dist/data/strategies.d.ts +1 -1
  29. package/dist/data/strategies.js +44 -283
  30. package/dist/data/templates.d.ts +1 -0
  31. package/dist/data/templates.js +134 -0
  32. package/dist/data/types.d.ts +1 -1
  33. package/dist/data/validator.d.ts +1 -0
  34. package/dist/data/validator.js +160 -0
  35. package/dist/db/database.js +9 -14
  36. package/dist/db/export.js +32 -55
  37. package/dist/db/filters.d.ts +1 -1
  38. package/dist/db/filters.js +15 -38
  39. package/dist/db/introspection.js +4 -7
  40. package/dist/db/manager.d.ts +1 -1
  41. package/dist/db/manager.js +48 -53
  42. package/dist/db/migrations.js +7 -14
  43. package/dist/db/postgres.js +30 -78
  44. package/dist/index.d.ts +1 -1
  45. package/dist/public/css/app.css +1 -1
  46. package/dist/public/js/browse.js +1 -1
  47. package/dist/public/js/inspector.js +1 -1
  48. package/dist/public/js/query.js +1 -1
  49. package/dist/public/js/seed.js +1 -1
  50. package/dist/routes/api/erd.js +3 -8
  51. package/dist/routes/api/helpers.d.ts +1 -1
  52. package/dist/routes/api/helpers.js +75 -25
  53. package/dist/routes/api/import-export.js +1 -2
  54. package/dist/routes/api/query.js +2 -6
  55. package/dist/routes/api/rows.js +4 -8
  56. package/dist/routes/api/seed.js +35 -21
  57. package/dist/routes/api/tables.js +15 -30
  58. package/dist/routes/databases.js +8 -26
  59. package/dist/routes/pages.js +67 -18
  60. package/dist/serverless.js +31 -44
  61. package/dist/sql/generator.d.ts +1 -1
  62. package/dist/sql/generator.js +35 -109
  63. package/dist/types/api.d.ts +1 -1
  64. package/dist/utils/common.d.ts +1 -1
  65. package/dist/utils/common.js +45 -85
  66. package/dist/utils/csv.d.ts +1 -1
  67. package/dist/utils/csv.js +11 -17
  68. package/dist/utils/datatype.js +46 -60
  69. package/dist/utils/icons.js +8 -8
  70. package/dist/views/layouts/main.hbs +1 -87
  71. package/dist/views/pages/databases.hbs +1 -233
  72. package/dist/views/pages/designer.hbs +1 -89
  73. package/dist/views/pages/erd.hbs +1 -1241
  74. package/dist/views/pages/error.hbs +1 -10
  75. package/dist/views/pages/form.hbs +1 -59
  76. package/dist/views/pages/home.hbs +1 -220
  77. package/dist/views/pages/info.hbs +1 -107
  78. package/dist/views/pages/login.hbs +1 -98
  79. package/dist/views/pages/query.hbs +1 -129
  80. package/dist/views/pages/schema.hbs +1 -298
  81. package/dist/views/pages/seed-select.hbs +251 -0
  82. package/dist/views/pages/seed.hbs +256 -414
  83. package/dist/views/pages/table.hbs +1 -327
  84. package/dist/views/partials/navbar.hbs +1 -83
  85. package/dist/views/partials/sidebar.hbs +1 -129
  86. package/docs/API.md +149 -149
  87. package/docs/EXAMPLES.md +579 -579
  88. package/docs/SECURITY.md +265 -265
  89. package/package.json +80 -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
+ }
@@ -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:string;scope:ErChainScope;executionOrder:string[];tableResults:Record<string,{count:number;rows:WhereClause[][];previewRows:Record<string,unknown>[];warnings:string[];}>;totalRows:number;sql:string;warnings:string[];}export declare const SKIP:unique symbol;
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
+ }
@@ -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 = !!options.readonly;
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
- const data = await Promise.resolve(fn());
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 === undefined || info.lastInsertRowid === null ? null : Number(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 stmt = this.db.prepare(trimmed);
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 += 1;
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
- const sql = (0, generator_1.generateAddColumn)(table, colCopy);
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 === (!!existing.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 t of tables.data ?? []) {
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 && allSchemas.data) {
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
- else {
56
- const tables = await db.listTables();
57
- if (!tables.success)
58
- return { success: false, error: tables.error ?? 'Failed to list tables.' };
59
- for (const t of tables.data ?? []) {
60
- const name = t.name;
61
- const info = await db.getTableInfo(name);
62
- if (info.success && info.data) {
63
- if (info.data.sql) {
64
- out.push(info.data.sql.trim());
65
- }
66
- if (info.data.foreignKeys && info.data.foreignKeys.length > 0) {
67
- for (const fk of info.data.foreignKeys) {
68
- if (fk.from && fk.to && fk.table) {
69
- let action = '';
70
- if (fk.on_delete && fk.on_delete !== 'NO ACTION')
71
- action += ` ON DELETE ${fk.on_delete}`;
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
  }
@@ -1 +1 @@
1
- import type {FilterCondition,RowFilters,SQLInputValue} from './types';export declare function 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;
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;
@@ -1,13 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.escapeLike = 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
- function escapeLike(s) {
9
- return String(s).replace(/[\\%_]/g, (m) => '\\' + m);
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('%' + escapeLike(value) + '%');
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
- if (value.startsWith('>=')) {
85
- conds.push(`${q} >= ?`);
86
- params.push(value.slice(2).trim());
87
- }
88
- else if (value.startsWith('<=')) {
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('%' + escapeLike(value) + '%');
91
+ params.push(`%${(0, exports.escapeLike)(value)}%`);
115
92
  }
116
93
  }
117
94
  return { where: conds.length ? ` WHERE ${conds.join(' AND ')}` : '', params };
@@ -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: !!ix.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.foreignKeys;
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));
@@ -1 +1 @@
1
- import type {IDatabase} from './types';import{type Logger} from '../utils/logger';export interface DbManagerOptions{dir?:string;files?:string[];connections?:Record<string,string>;readonly ?:boolean;}export interface DatabaseEntry{id:string;name:string;path:string;dialect:'sqlite'|'postgres';size:number;modified:string;readonly :boolean;}export declare class DbManager{private logger;private dir?;private createDir;private readonly readonlyMode;private files;private idByPath;private pathById;private nameById;private readonlyById;private openDbs;constructor(options:DbManagerOptions|string,logger?:Logger);get directory():string;get isReadOnly():boolean;isDbFile(name:string):boolean;private registerConnections;private registerDirFiles;private registerExplicitFiles;private addNamedTarget;private addFile;list():DatabaseEntry[];has(id:string):boolean;isDbReadOnly(id:string):boolean;setReadonly(id:string,readonly :boolean):void;open(id:string,readonlyOverride?:boolean):IDatabase;openFile(absPath:string,readonly ?:boolean):string;get(id:string):IDatabase|undefined;countTables(id:string):Promise<number>;create(name:string):string;addConnection(name:string,connectionString:string,readonly ?:boolean):string;remove(id:string):void;closeAll():void;}
1
+ import type {IDatabase} from './types';import{type Logger} from '../utils/logger';export interface DbManagerOptions{dir?:string;files?:string[];connections?:Record<string,string>;readonly ?:boolean;serverless?:boolean;}export interface DatabaseEntry{id:string;name:string;path:string;dialect:'sqlite'|'postgres';size:number;modified:string;readonly :boolean;}export declare class DbManager{private logger;private dir?;private createDir;private readonly readonlyMode;private readonly isServerless;private files;private idByPath;private pathById;private nameById;private readonlyById;private openDbs;constructor(options:DbManagerOptions|string,logger?:Logger);get directory():string;get isReadOnly():boolean;isDbFile(name:string):boolean;private registerConnections;private registerDirFiles;private registerExplicitFiles;private addNamedTarget;private addFile;list():DatabaseEntry[];has(id:string):boolean;isDbReadOnly(id:string):boolean;setReadonly(id:string,readonly :boolean):void;open(id:string,readonlyOverride?:boolean):IDatabase;openFile(absPath:string,readonly ?:boolean):string;get(id:string):IDatabase|undefined;countTables(id:string):Promise<number>;create(name:string):string;addConnection(name:string,connectionString:string,readonly ?:boolean):string;remove(id:string):void;closeAll():void;}