admindb 2.5.0 → 2.6.2

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 (51) hide show
  1. package/LICENSE +20 -20
  2. package/dist/app.js +13 -1
  3. package/dist/core/diff/data-differ.d.ts +1 -0
  4. package/dist/core/diff/data-differ.js +243 -0
  5. package/dist/core/diff/patch-generator.d.ts +1 -0
  6. package/dist/core/diff/patch-generator.js +77 -0
  7. package/dist/core/diff/schema-differ.d.ts +1 -0
  8. package/dist/core/diff/schema-differ.js +229 -0
  9. package/dist/core/router.d.ts +1 -1
  10. package/dist/core/transfer/dialect-mapper.d.ts +1 -0
  11. package/dist/core/transfer/dialect-mapper.js +303 -0
  12. package/dist/core/transfer/masking.d.ts +1 -0
  13. package/dist/core/transfer/masking.js +49 -0
  14. package/dist/core/transfer/sync-engine.d.ts +1 -0
  15. package/dist/core/transfer/sync-engine.js +234 -0
  16. package/dist/core/transfer/topological-sort.d.ts +1 -0
  17. package/dist/core/transfer/topological-sort.js +55 -0
  18. package/dist/db/dialects/postgres/introspector.js +71 -71
  19. package/dist/db/dialects/sqlite/introspector.js +6 -3
  20. package/dist/modules/index.d.ts +1 -1
  21. package/dist/modules/index.js +9 -1
  22. package/dist/modules/search/index.d.ts +1 -0
  23. package/dist/modules/search/index.js +19 -0
  24. package/dist/modules/search/search.routes.d.ts +1 -0
  25. package/dist/modules/search/search.routes.js +38 -0
  26. package/dist/modules/search/search.service.d.ts +1 -0
  27. package/dist/modules/search/search.service.js +458 -0
  28. package/dist/modules/search/search.types.d.ts +1 -0
  29. package/dist/modules/search/search.types.js +2 -0
  30. package/dist/modules/sync/index.d.ts +1 -0
  31. package/dist/modules/sync/index.js +18 -0
  32. package/dist/modules/sync/sync.routes.d.ts +1 -0
  33. package/dist/modules/sync/sync.routes.js +135 -0
  34. package/dist/modules/sync/sync.service.d.ts +1 -0
  35. package/dist/modules/sync/sync.service.js +41 -0
  36. package/dist/public/css/app.css +2 -1
  37. package/dist/public/js/browse.js +1 -1
  38. package/dist/public/js/search.js +1 -0
  39. package/dist/public/js/sync.js +1 -0
  40. package/dist/utils/icons.js +3 -0
  41. package/dist/views/layouts/main.hbs +1 -1
  42. package/dist/views/pages/error.hbs +1 -10
  43. package/dist/views/pages/seed-select.hbs +252 -1
  44. package/dist/views/pages/seed.hbs +307 -1
  45. package/dist/views/pages/sync.hbs +1 -0
  46. package/dist/views/pages/table.hbs +1 -1
  47. package/dist/views/partials/navbar.hbs +1 -1
  48. package/dist/views/partials/sidebar.hbs +1 -1
  49. package/docs/EXAMPLES.md +579 -579
  50. package/docs/SECURITY.md +265 -265
  51. package/package.json +14 -9
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Muhammad Bin Khalid
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Muhammad Bin Khalid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
package/dist/app.js CHANGED
@@ -142,7 +142,19 @@ function createRouter(options = {}) {
142
142
  logger,
143
143
  };
144
144
  };
145
- (0, index_1.registerModules)(app, { getContext, logger });
145
+ const getNamedContext = (dbId, readonlyOverride) => {
146
+ if (manager) {
147
+ if (!manager.has(dbId)) {
148
+ const err = new Error(`Database "${dbId}" does not exist.`);
149
+ err.status = 404;
150
+ throw err;
151
+ }
152
+ const effectiveRo = globalReadonly ? true : manager.isDbReadOnly(dbId);
153
+ return manager.getContext(dbId, readonlyOverride !== undefined ? readonlyOverride : effectiveRo);
154
+ }
155
+ return getContext({ params: { db: defaultDbId } });
156
+ };
157
+ (0, index_1.registerModules)(app, { getContext, getNamedContext, manager, logger });
146
158
  addErrorHandlers(app, logger);
147
159
  return app;
148
160
  }
@@ -0,0 +1 @@
1
+ import type {IDatabase} from '../../db/types';export interface RowValueChange{column:string;sourceValue:unknown;targetValue:unknown;}export interface RowDiffItem{key:string;action:'added'|'removed'|'modified';sourceRow?:Record<string,unknown>;targetRow?:Record<string,unknown>;changes?:RowValueChange[];}export interface TableDataDiffResult{table:string;primaryKeys:string[];sourceRowCount:number;targetRowCount:number;addedCount:number;removedCount:number;modifiedCount:number;identicalCount:number;rows:RowDiffItem[];unifiedDiff:string;}export declare function areValuesEqual(a:unknown,b:unknown):boolean;export declare function compareTableData(sourceDb:IDatabase,targetDb:IDatabase,table:string,limit?:number):Promise<TableDataDiffResult>;
@@ -0,0 +1,243 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.areValuesEqual = areValuesEqual;
4
+ exports.compareTableData = compareTableData;
5
+ function areValuesEqual(a, b) {
6
+ if (a === b)
7
+ return true;
8
+ if ((a === null || a === undefined) && (b === null || b === undefined))
9
+ return true;
10
+ if (a === null || a === undefined || b === null || b === undefined)
11
+ return false;
12
+ const isBoolLikeA = typeof a === 'boolean' || a === 0 || a === 1 || a === '0' || a === '1' || a === 'true' || a === 'false';
13
+ const isBoolLikeB = typeof b === 'boolean' || b === 0 || b === 1 || b === '0' || b === '1' || b === 'true' || b === 'false';
14
+ if ((typeof a === 'boolean' || typeof b === 'boolean') && isBoolLikeA && isBoolLikeB) {
15
+ const boolA = a === true || a === 1 || a === '1' || a === 'true';
16
+ const boolB = b === true || b === 1 || b === '1' || b === 'true';
17
+ return boolA === boolB;
18
+ }
19
+ if (typeof a === 'number' && typeof b === 'number') {
20
+ return Math.abs(a - b) < 0.000001;
21
+ }
22
+ if ((typeof a === 'number' || typeof a === 'bigint') && typeof b === 'string') {
23
+ if (b.trim() !== '' && !isNaN(Number(b)) && Number(a) === Number(b))
24
+ return true;
25
+ }
26
+ if ((typeof b === 'number' || typeof b === 'bigint') && typeof a === 'string') {
27
+ if (a.trim() !== '' && !isNaN(Number(a)) && Number(a) === Number(b))
28
+ return true;
29
+ }
30
+ const isDateA = a instanceof Date;
31
+ const isDateB = b instanceof Date;
32
+ if (isDateA || isDateB) {
33
+ const timeA = isDateA ? a.getTime() : new Date(String(a)).getTime();
34
+ const timeB = isDateB ? b.getTime() : new Date(String(b)).getTime();
35
+ if (!isNaN(timeA) && !isNaN(timeB))
36
+ return timeA === timeB;
37
+ }
38
+ if (typeof a === 'string' && typeof b === 'string') {
39
+ if (a.length >= 10 && b.length >= 10 && /^\d{4}-\d{2}-\d{2}/.test(a) && /^\d{4}-\d{2}-\d{2}/.test(b)) {
40
+ const timeA = new Date(a).getTime();
41
+ const timeB = new Date(b).getTime();
42
+ if (!isNaN(timeA) && !isNaN(timeB))
43
+ return timeA === timeB;
44
+ }
45
+ }
46
+ if (typeof a === 'object' || typeof b === 'object') {
47
+ if (Buffer.isBuffer(a) || a instanceof Uint8Array || Buffer.isBuffer(b) || b instanceof Uint8Array) {
48
+ const bufA = Buffer.isBuffer(a) ? a : Buffer.from(a);
49
+ const bufB = Buffer.isBuffer(b) ? b : Buffer.from(b);
50
+ return bufA.equals(bufB);
51
+ }
52
+ try {
53
+ const strA = typeof a === 'string' ? a : JSON.stringify(a);
54
+ const strB = typeof b === 'string' ? b : JSON.stringify(b);
55
+ return JSON.stringify(JSON.parse(strA)) === JSON.stringify(JSON.parse(strB));
56
+ }
57
+ catch {
58
+ }
59
+ }
60
+ return String(a).trim() === String(b).trim();
61
+ }
62
+ async function compareTableData(sourceDb, targetDb, table, limit = 200) {
63
+ const [srcInfoRes, tgtInfoRes] = await Promise.all([
64
+ sourceDb.getTableInfo(table),
65
+ targetDb.getTableInfo(table),
66
+ ]);
67
+ const srcInfo = srcInfoRes.success && srcInfoRes.data ? srcInfoRes.data : null;
68
+ const tgtInfo = tgtInfoRes.success && tgtInfoRes.data ? tgtInfoRes.data : null;
69
+ if (!srcInfo && !tgtInfo) {
70
+ throw new Error(`Table "${table}" does not exist in either source or target database.`);
71
+ }
72
+ if (srcInfo && (!tgtInfo || tgtInfo.columns.length === 0)) {
73
+ const srcCountRes = await sourceDb.getRowCount(table);
74
+ const srcRowCount = srcCountRes.data || 0;
75
+ const srcRowsRes = await sourceDb.getRows(table, { limit });
76
+ const srcRows = srcRowsRes.data || [];
77
+ const pks = srcInfo.primaryKey.length > 0 ? srcInfo.primaryKey : ['id'];
78
+ const rows = srcRows.map((r, idx) => ({
79
+ key: pks.map((k) => String(r[k] ?? '')).join('::') || `row_${idx + 1}`,
80
+ action: 'added',
81
+ sourceRow: r,
82
+ }));
83
+ const lines = [
84
+ `diff --git a/${table}/data b/${table}/data`,
85
+ `@@ Table: ${table} (MISSING IN TARGET DATABASE) @@`,
86
+ ` Target table "${table}" does not exist. All ${srcRowCount} rows are newly added.`,
87
+ ];
88
+ for (const r of rows.slice(0, 100)) {
89
+ lines.push(`+ [PK: ${r.key}] ${JSON.stringify(r.sourceRow)}`);
90
+ }
91
+ return {
92
+ table,
93
+ primaryKeys: pks,
94
+ sourceRowCount: srcRowCount,
95
+ targetRowCount: 0,
96
+ addedCount: rows.length,
97
+ removedCount: 0,
98
+ modifiedCount: 0,
99
+ identicalCount: 0,
100
+ rows,
101
+ unifiedDiff: lines.join('\n'),
102
+ };
103
+ }
104
+ if (!srcInfo && tgtInfo) {
105
+ const tgtCountRes = await targetDb.getRowCount(table);
106
+ const tgtRowCount = tgtCountRes.data || 0;
107
+ const tgtRowsRes = await targetDb.getRows(table, { limit });
108
+ const tgtRows = tgtRowsRes.data || [];
109
+ const pks = tgtInfo.primaryKey.length > 0 ? tgtInfo.primaryKey : ['id'];
110
+ const rows = tgtRows.map((r, idx) => ({
111
+ key: pks.map((k) => String(r[k] ?? '')).join('::') || `row_${idx + 1}`,
112
+ action: 'removed',
113
+ targetRow: r,
114
+ }));
115
+ const lines = [
116
+ `diff --git a/${table}/data b/${table}/data`,
117
+ `@@ Table: ${table} (MISSING IN SOURCE DATABASE) @@`,
118
+ ` Source table "${table}" does not exist. All ${tgtRowCount} target rows would be removed.`,
119
+ ];
120
+ for (const r of rows.slice(0, 100)) {
121
+ lines.push(`- [PK: ${r.key}] ${JSON.stringify(r.targetRow)}`);
122
+ }
123
+ return {
124
+ table,
125
+ primaryKeys: pks,
126
+ sourceRowCount: 0,
127
+ targetRowCount: tgtRowCount,
128
+ addedCount: 0,
129
+ removedCount: rows.length,
130
+ modifiedCount: 0,
131
+ identicalCount: 0,
132
+ rows,
133
+ unifiedDiff: lines.join('\n'),
134
+ };
135
+ }
136
+ const srcColNames = srcInfo.columns.map((c) => c.name);
137
+ const tgtColNames = tgtInfo.columns.map((c) => c.name);
138
+ let pks = srcInfo.primaryKey.length > 0 ? srcInfo.primaryKey : tgtInfo.primaryKey;
139
+ if (!pks || pks.length === 0) {
140
+ const hasId = srcColNames.some((c) => c.toLowerCase() === 'id');
141
+ if (hasId) {
142
+ pks = ['id'];
143
+ }
144
+ else if (srcColNames.length > 0) {
145
+ pks = srcColNames;
146
+ }
147
+ else {
148
+ pks = ['row_idx'];
149
+ }
150
+ }
151
+ const orderCol = pks[0] && srcColNames.includes(pks[0]) && tgtColNames.includes(pks[0]) ? pks[0] : undefined;
152
+ const [srcCountRes, tgtCountRes, srcRowsRes, tgtRowsRes] = await Promise.all([
153
+ sourceDb.getRowCount(table),
154
+ targetDb.getRowCount(table),
155
+ sourceDb.getRows(table, { limit, orderBy: orderCol, orderDir: 'asc' }),
156
+ targetDb.getRows(table, { limit, orderBy: orderCol, orderDir: 'asc' }),
157
+ ]);
158
+ const srcRows = srcRowsRes.data || [];
159
+ const tgtRows = tgtRowsRes.data || [];
160
+ const makeKey = (row, fallbackIdx) => {
161
+ const keyParts = pks.map((k) => {
162
+ const v = row[k];
163
+ return v === null || v === undefined ? '' : String(v);
164
+ });
165
+ const key = keyParts.join('::');
166
+ return key.trim() ? key : `row_${fallbackIdx}`;
167
+ };
168
+ const srcMap = new Map();
169
+ srcRows.forEach((r, idx) => srcMap.set(makeKey(r, idx + 1), r));
170
+ const tgtMap = new Map();
171
+ tgtRows.forEach((r, idx) => tgtMap.set(makeKey(r, idx + 1), r));
172
+ const allKeys = Array.from(new Set([...srcMap.keys(), ...tgtMap.keys()]));
173
+ const diffItems = [];
174
+ let addedCount = 0;
175
+ let removedCount = 0;
176
+ let modifiedCount = 0;
177
+ let identicalCount = 0;
178
+ for (const key of allKeys) {
179
+ const sr = srcMap.get(key);
180
+ const tr = tgtMap.get(key);
181
+ if (sr && !tr) {
182
+ addedCount++;
183
+ diffItems.push({ key, action: 'added', sourceRow: sr });
184
+ continue;
185
+ }
186
+ if (!sr && tr) {
187
+ removedCount++;
188
+ diffItems.push({ key, action: 'removed', targetRow: tr });
189
+ continue;
190
+ }
191
+ if (sr && tr) {
192
+ const changes = [];
193
+ const cols = Array.from(new Set([...Object.keys(sr), ...Object.keys(tr)]));
194
+ for (const col of cols) {
195
+ const sv = sr[col];
196
+ const tv = tr[col];
197
+ if (!areValuesEqual(sv, tv)) {
198
+ changes.push({ column: col, sourceValue: sv, targetValue: tv });
199
+ }
200
+ }
201
+ if (changes.length > 0) {
202
+ modifiedCount++;
203
+ diffItems.push({ key, action: 'modified', sourceRow: sr, targetRow: tr, changes });
204
+ }
205
+ else {
206
+ identicalCount++;
207
+ }
208
+ }
209
+ }
210
+ const lines = [
211
+ `diff --git a/${table}/data b/${table}/data`,
212
+ `@@ Data Diff for Table: ${table} (Key: ${pks.join(', ')}) @@`,
213
+ ];
214
+ for (const item of diffItems.slice(0, 100)) {
215
+ if (item.action === 'added') {
216
+ lines.push(`+ [PK: ${item.key}] ${JSON.stringify(item.sourceRow)}`);
217
+ }
218
+ else if (item.action === 'removed') {
219
+ lines.push(`- [PK: ${item.key}] ${JSON.stringify(item.targetRow)}`);
220
+ }
221
+ else if (item.action === 'modified') {
222
+ const changesStr = (item.changes || [])
223
+ .map((c) => `${c.column}: ${JSON.stringify(c.targetValue)} -> ${JSON.stringify(c.sourceValue)}`)
224
+ .join(', ');
225
+ lines.push(`~ [PK: ${item.key}] ${changesStr}`);
226
+ }
227
+ }
228
+ if (diffItems.length === 0) {
229
+ lines.push(' Rows are identical (up to preview limit).');
230
+ }
231
+ return {
232
+ table,
233
+ primaryKeys: pks,
234
+ sourceRowCount: srcCountRes.data || 0,
235
+ targetRowCount: tgtCountRes.data || 0,
236
+ addedCount,
237
+ removedCount,
238
+ modifiedCount,
239
+ identicalCount,
240
+ rows: diffItems,
241
+ unifiedDiff: lines.join('\n'),
242
+ };
243
+ }
@@ -0,0 +1 @@
1
+ import type {SchemaDiffResult} from './schema-differ';import{type SupportedDialect} from '../transfer/dialect-mapper';export interface GeneratedPatch{upSql:string;downSql:string;}export declare function generateSchemaPatch(diff:SchemaDiffResult,targetDialect:SupportedDialect):GeneratedPatch;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateSchemaPatch = generateSchemaPatch;
4
+ const dialect_mapper_1 = require("../transfer/dialect-mapper");
5
+ function generateSchemaPatch(diff, targetDialect) {
6
+ const upLines = [`-- UP Migration: Align "${diff.targetDbId}" with "${diff.sourceDbId}" (${targetDialect})`];
7
+ const downLines = [`-- DOWN Rollback: Revert changes on "${diff.targetDbId}"`];
8
+ const q = targetDialect === 'postgres' ? (s) => `"${s}"` : (s) => `\`${s}\``;
9
+ for (const table of diff.tables) {
10
+ if (table.action === 'unchanged')
11
+ continue;
12
+ if (table.action === 'added' && table.sourceTable) {
13
+ const createSql = (0, dialect_mapper_1.generateCreateTableForDialect)(table.sourceTable, targetDialect);
14
+ upLines.push(`\n-- Create table ${table.name}`);
15
+ upLines.push(createSql);
16
+ for (const idx of table.sourceTable.indexes || []) {
17
+ if (idx.name?.startsWith('sqlite_'))
18
+ continue;
19
+ const colsArr = Array.isArray(idx?.columns) ? idx.columns.filter(Boolean) : [];
20
+ if (colsArr.length === 0)
21
+ continue;
22
+ const uq = idx.unique ? 'UNIQUE ' : '';
23
+ const cols = colsArr.map(q).join(', ');
24
+ upLines.push(`CREATE ${uq}INDEX IF NOT EXISTS ${q(idx.name)} ON ${q(table.name)} (${cols});`);
25
+ }
26
+ downLines.push(`\n-- Revert: Drop created table ${table.name}`);
27
+ downLines.push(`DROP TABLE IF EXISTS ${q(table.name)};`);
28
+ continue;
29
+ }
30
+ if (table.action === 'removed' && table.targetTable) {
31
+ upLines.push(`\n-- Drop obsolete table ${table.name}`);
32
+ upLines.push(`DROP TABLE IF EXISTS ${q(table.name)};`);
33
+ const recreateSql = (0, dialect_mapper_1.generateCreateTableForDialect)(table.targetTable, targetDialect);
34
+ downLines.push(`\n-- Revert: Re-create dropped table ${table.name}`);
35
+ downLines.push(recreateSql);
36
+ continue;
37
+ }
38
+ if (table.action === 'modified') {
39
+ upLines.push(`\n-- Modify table ${table.name}`);
40
+ for (const col of table.columnDiffs) {
41
+ if (col.action === 'added' && col.sourceCol) {
42
+ const typeStr = (0, dialect_mapper_1.mapColumnToDialect)(col.sourceCol, targetDialect);
43
+ const notNullStr = col.sourceCol.notNull ? ' NOT NULL' : '';
44
+ const mappedDefault = (0, dialect_mapper_1.mapDefaultValueForDialect)(col.sourceCol.defaultValue, col.sourceCol.genericType, targetDialect);
45
+ const dfltStr = mappedDefault !== null && mappedDefault !== undefined ? ` DEFAULT ${mappedDefault}` : '';
46
+ upLines.push(`ALTER TABLE ${q(table.name)} ADD COLUMN ${q(col.name)} ${typeStr}${notNullStr}${dfltStr};`);
47
+ downLines.push(`ALTER TABLE ${q(table.name)} DROP COLUMN IF EXISTS ${q(col.name)};`);
48
+ }
49
+ else if (col.action === 'removed' && col.targetCol) {
50
+ upLines.push(`ALTER TABLE ${q(table.name)} DROP COLUMN IF EXISTS ${q(col.name)};`);
51
+ const typeStr = (0, dialect_mapper_1.mapColumnToDialect)(col.targetCol, targetDialect);
52
+ downLines.push(`ALTER TABLE ${q(table.name)} ADD COLUMN ${q(col.name)} ${typeStr};`);
53
+ }
54
+ }
55
+ for (const idx of table.sourceTable?.indexes || []) {
56
+ if (idx.name?.startsWith('sqlite_'))
57
+ continue;
58
+ if (table.indexesAdded.includes(idx.name)) {
59
+ const colsArr = Array.isArray(idx?.columns) ? idx.columns.filter(Boolean) : [];
60
+ if (colsArr.length === 0)
61
+ continue;
62
+ const uq = idx.unique ? 'UNIQUE ' : '';
63
+ const cols = colsArr.map(q).join(', ');
64
+ upLines.push(`CREATE ${uq}INDEX IF NOT EXISTS ${q(idx.name)} ON ${q(table.name)} (${cols});`);
65
+ downLines.push(`DROP INDEX IF EXISTS ${q(idx.name)};`);
66
+ }
67
+ }
68
+ for (const idxName of table.indexesRemoved) {
69
+ upLines.push(`DROP INDEX IF EXISTS ${q(idxName)};`);
70
+ }
71
+ }
72
+ }
73
+ return {
74
+ upSql: upLines.join('\n'),
75
+ downSql: downLines.join('\n'),
76
+ };
77
+ }
@@ -0,0 +1 @@
1
+ import type {IDatabase} from '../../db/types';import{type NormalizedColumn,type NormalizedTable,type SupportedDialect} from '../transfer/dialect-mapper';export interface ColumnDiff{name:string;action:'added'|'removed'|'modified';sourceCol?:NormalizedColumn;targetCol?:NormalizedColumn;changes?:string[];}export interface TableDiff{name:string;action:'added'|'removed'|'modified'|'unchanged';sourceTable?:NormalizedTable;targetTable?:NormalizedTable;columnDiffs:ColumnDiff[];pkChanged:boolean;indexesAdded:string[];indexesRemoved:string[];fksAdded:string[];fksRemoved:string[];}export interface SchemaDiffResult{sourceDbId:string;targetDbId:string;sourceDialect:SupportedDialect;targetDialect:SupportedDialect;tables:TableDiff[];stats:{addedTables:number;removedTables:number;modifiedTables:number;identicalTables:number;totalColumnsChanged:number;};unifiedDiff:string;}export declare function compareSchemas(sourceDb:IDatabase,sourceDbId:string,targetDb:IDatabase,targetDbId:string):Promise<SchemaDiffResult>;
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.compareSchemas = compareSchemas;
4
+ const dialect_mapper_1 = require("../transfer/dialect-mapper");
5
+ async function compareSchemas(sourceDb, sourceDbId, targetDb, targetDbId) {
6
+ const [sourceTableListRes, targetTableListRes] = await Promise.all([
7
+ sourceDb.listTables(),
8
+ targetDb.listTables(),
9
+ ]);
10
+ if (!sourceTableListRes.success)
11
+ throw new Error(sourceTableListRes.error || 'Failed to list source tables');
12
+ if (!targetTableListRes.success)
13
+ throw new Error(targetTableListRes.error || 'Failed to list target tables');
14
+ const sourceTableNames = (sourceTableListRes.data || []).map((t) => t.name).filter((n) => !n.startsWith('sqlite_') && !n.startsWith('_admindb_'));
15
+ const targetTableNames = (targetTableListRes.data || []).map((t) => t.name).filter((n) => !n.startsWith('sqlite_') && !n.startsWith('_admindb_'));
16
+ const [sourceInfos, targetInfos] = await Promise.all([
17
+ Promise.all(sourceTableNames.map(async (name) => (await sourceDb.getTableInfo(name)).data)),
18
+ Promise.all(targetTableNames.map(async (name) => (await targetDb.getTableInfo(name)).data)),
19
+ ]);
20
+ const sourceDialect = sourceDb.dialect === 'postgres' ? 'postgres' : 'sqlite';
21
+ const targetDialect = targetDb.dialect === 'postgres' ? 'postgres' : 'sqlite';
22
+ const sourceMap = new Map();
23
+ for (const info of sourceInfos) {
24
+ if (info)
25
+ sourceMap.set(info.table, (0, dialect_mapper_1.normalizeTableInfo)(info, sourceDialect));
26
+ }
27
+ const targetMap = new Map();
28
+ for (const info of targetInfos) {
29
+ if (info)
30
+ targetMap.set(info.table, (0, dialect_mapper_1.normalizeTableInfo)(info, targetDialect));
31
+ }
32
+ const allNames = Array.from(new Set([...sourceTableNames, ...targetTableNames])).sort();
33
+ const tableDiffs = [];
34
+ let addedTables = 0;
35
+ let removedTables = 0;
36
+ let modifiedTables = 0;
37
+ let identicalTables = 0;
38
+ let totalColumnsChanged = 0;
39
+ for (const name of allNames) {
40
+ const src = sourceMap.get(name);
41
+ const tgt = targetMap.get(name);
42
+ if (src && !tgt) {
43
+ addedTables++;
44
+ tableDiffs.push({
45
+ name,
46
+ action: 'added',
47
+ sourceTable: src,
48
+ columnDiffs: src.columns.map((c) => ({ name: c.name, action: 'added', sourceCol: c })),
49
+ pkChanged: true,
50
+ indexesAdded: src.indexes.map((i) => i.name),
51
+ indexesRemoved: [],
52
+ fksAdded: src.foreignKeys.map((f) => `${f.from} -> ${f.table}.${f.to}`),
53
+ fksRemoved: [],
54
+ });
55
+ continue;
56
+ }
57
+ if (!src && tgt) {
58
+ removedTables++;
59
+ tableDiffs.push({
60
+ name,
61
+ action: 'removed',
62
+ targetTable: tgt,
63
+ columnDiffs: tgt.columns.map((c) => ({ name: c.name, action: 'removed', targetCol: c })),
64
+ pkChanged: true,
65
+ indexesAdded: [],
66
+ indexesRemoved: tgt.indexes.map((i) => i.name),
67
+ fksAdded: [],
68
+ fksRemoved: tgt.foreignKeys.map((f) => `${f.from} -> ${f.table}.${f.to}`),
69
+ });
70
+ continue;
71
+ }
72
+ if (src && tgt) {
73
+ const colDiffs = [];
74
+ const srcColMap = new Map(src.columns.map((c) => [c.name, c]));
75
+ const tgtColMap = new Map(tgt.columns.map((c) => [c.name, c]));
76
+ const colNames = Array.from(new Set([...srcColMap.keys(), ...tgtColMap.keys()]));
77
+ for (const colName of colNames) {
78
+ const sc = srcColMap.get(colName);
79
+ const tc = tgtColMap.get(colName);
80
+ if (sc && !tc) {
81
+ colDiffs.push({ name: colName, action: 'added', sourceCol: sc });
82
+ totalColumnsChanged++;
83
+ }
84
+ else if (!sc && tc) {
85
+ colDiffs.push({ name: colName, action: 'removed', targetCol: tc });
86
+ totalColumnsChanged++;
87
+ }
88
+ else if (sc && tc) {
89
+ const changes = [];
90
+ if (sc.genericType !== tc.genericType) {
91
+ changes.push(`Type: target "${tc.type}" (${tc.genericType}) -> source "${sc.type}" (${sc.genericType})`);
92
+ }
93
+ if (sc.notNull !== tc.notNull) {
94
+ changes.push(`NotNull: target ${tc.notNull} -> source ${sc.notNull}`);
95
+ }
96
+ if (sc.primaryKey !== tc.primaryKey) {
97
+ changes.push(`PK: target ${tc.primaryKey} -> source ${sc.primaryKey}`);
98
+ }
99
+ if (sc.defaultValue !== tc.defaultValue) {
100
+ changes.push(`Default: target "${tc.defaultValue}" -> source "${sc.defaultValue}"`);
101
+ }
102
+ if (changes.length > 0) {
103
+ colDiffs.push({ name: colName, action: 'modified', sourceCol: sc, targetCol: tc, changes });
104
+ totalColumnsChanged++;
105
+ }
106
+ }
107
+ }
108
+ const pkChanged = JSON.stringify(src.primaryKeys.sort()) !== JSON.stringify(tgt.primaryKeys.sort());
109
+ const srcIdxNames = new Set(src.indexes.map((i) => i.name));
110
+ const tgtIdxNames = new Set(tgt.indexes.map((i) => i.name));
111
+ const indexesAdded = src.indexes.filter((i) => !tgtIdxNames.has(i.name)).map((i) => i.name);
112
+ const indexesRemoved = tgt.indexes.filter((i) => !srcIdxNames.has(i.name)).map((i) => i.name);
113
+ const srcFkDefs = src.foreignKeys.map((f) => `${f.from} -> ${f.table}.${f.to}`);
114
+ const tgtFkDefs = tgt.foreignKeys.map((f) => `${f.from} -> ${f.table}.${f.to}`);
115
+ const fksAdded = srcFkDefs.filter((f) => !tgtFkDefs.includes(f));
116
+ const fksRemoved = tgtFkDefs.filter((f) => !srcFkDefs.includes(f));
117
+ const isModified = colDiffs.length > 0 ||
118
+ pkChanged ||
119
+ indexesAdded.length > 0 ||
120
+ indexesRemoved.length > 0 ||
121
+ fksAdded.length > 0 ||
122
+ fksRemoved.length > 0;
123
+ if (isModified) {
124
+ modifiedTables++;
125
+ tableDiffs.push({
126
+ name,
127
+ action: 'modified',
128
+ sourceTable: src,
129
+ targetTable: tgt,
130
+ columnDiffs: colDiffs,
131
+ pkChanged,
132
+ indexesAdded,
133
+ indexesRemoved,
134
+ fksAdded,
135
+ fksRemoved,
136
+ });
137
+ }
138
+ else {
139
+ identicalTables++;
140
+ tableDiffs.push({
141
+ name,
142
+ action: 'unchanged',
143
+ sourceTable: src,
144
+ targetTable: tgt,
145
+ columnDiffs: [],
146
+ pkChanged: false,
147
+ indexesAdded: [],
148
+ indexesRemoved: [],
149
+ fksAdded: [],
150
+ fksRemoved: [],
151
+ });
152
+ }
153
+ }
154
+ }
155
+ const unifiedDiff = generateGitUnifiedDiff(sourceDbId, targetDbId, tableDiffs);
156
+ return {
157
+ sourceDbId,
158
+ targetDbId,
159
+ sourceDialect,
160
+ targetDialect,
161
+ tables: tableDiffs,
162
+ stats: {
163
+ addedTables,
164
+ removedTables,
165
+ modifiedTables,
166
+ identicalTables,
167
+ totalColumnsChanged,
168
+ },
169
+ unifiedDiff,
170
+ };
171
+ }
172
+ function generateGitUnifiedDiff(sourceDbId, targetDbId, diffs) {
173
+ const lines = [
174
+ `diff --git a/${targetDbId}/schema.sql b/${sourceDbId}/schema.sql`,
175
+ `--- a/${targetDbId} (target)`,
176
+ `+++ b/${sourceDbId} (source)`,
177
+ ];
178
+ for (const t of diffs) {
179
+ if (t.action === 'unchanged')
180
+ continue;
181
+ lines.push(`@@ table: ${t.name} (${t.action.toUpperCase()}) @@`);
182
+ if (t.action === 'added') {
183
+ lines.push(`+ CREATE TABLE ${t.name}`);
184
+ for (const col of t.sourceTable?.columns || []) {
185
+ lines.push(`+ ${col.name}: ${col.type}${col.primaryKey ? ' PRIMARY KEY' : ''}${col.notNull ? ' NOT NULL' : ''}`);
186
+ }
187
+ for (const idx of t.indexesAdded) {
188
+ lines.push(`+ INDEX ${idx}`);
189
+ }
190
+ for (const fk of t.fksAdded) {
191
+ lines.push(`+ FK ${fk}`);
192
+ }
193
+ continue;
194
+ }
195
+ if (t.action === 'removed') {
196
+ lines.push(`- DROP TABLE ${t.name}`);
197
+ for (const col of t.targetTable?.columns || []) {
198
+ lines.push(`- ${col.name}: ${col.type}`);
199
+ }
200
+ continue;
201
+ }
202
+ for (const c of t.columnDiffs) {
203
+ if (c.action === 'added') {
204
+ lines.push(`+ column: ${c.name} (${c.sourceCol?.type || 'UNKNOWN'})${c.sourceCol?.notNull ? ' NOT NULL' : ''}`);
205
+ }
206
+ else if (c.action === 'removed') {
207
+ lines.push(`- column: ${c.name} (${c.targetCol?.type || 'UNKNOWN'})`);
208
+ }
209
+ else if (c.action === 'modified') {
210
+ lines.push(`~ column: ${c.name} [${c.changes?.join('; ')}]`);
211
+ }
212
+ }
213
+ if (t.pkChanged) {
214
+ lines.push(`~ PK changed: target [${t.targetTable?.primaryKeys.join(', ')}] -> source [${t.sourceTable?.primaryKeys.join(', ')}]`);
215
+ }
216
+ for (const idx of t.indexesAdded)
217
+ lines.push(`+ INDEX ${idx}`);
218
+ for (const idx of t.indexesRemoved)
219
+ lines.push(`- INDEX ${idx}`);
220
+ for (const fk of t.fksAdded)
221
+ lines.push(`+ FK ${fk}`);
222
+ for (const fk of t.fksRemoved)
223
+ lines.push(`- FK ${fk}`);
224
+ }
225
+ if (lines.length === 3) {
226
+ lines.push(' Schemas are identical. No differences found.');
227
+ }
228
+ return lines.join('\n');
229
+ }
@@ -1 +1 @@
1
- import type {Request,Response,NextFunction} from 'express';import type {DatabaseContext} from './context';import type {Logger} from '../utils/logger';export interface RouteContext{getContext:(req:Request)=>DatabaseContext;logger:Logger;}export declare const ok:(res:Response,data:unknown,status?:number)=>Response;export declare const fail:(res:Response,error:string,status?:number,details?:Record<string,unknown>|null)=>Response;export declare const notFound:(res:Response,message:string)=>void;export declare const wrap:(fn:(req:Request,res:Response,next?:NextFunction)=>unknown)=>(req:Request,res:Response,next?:NextFunction)=>Promise<void>;export declare function initPageLocals(res:Response,dbOrCtx:{dialect:string|{name:string;};path:string;isReadOnly:boolean;},dbId:string,allNames:string[]):{tables:string[];};
1
+ import type {Request,Response,NextFunction} from 'express';import type {DatabaseContext} from './context';import type {Logger} from '../utils/logger';import type {DbManager} from '../db/manager';export type RequestParams=Record<string,string>;export interface RouteContext{getContext:(req:Request<any>)=>DatabaseContext;getNamedContext?:(id:string,readonlyOverride?:boolean)=>DatabaseContext;manager?:DbManager;logger:Logger;}export declare const ok:(res:Response,data:unknown,status?:number)=>Response;export declare const fail:(res:Response,error:string,status?:number,details?:Record<string,unknown>|null)=>Response;export declare const notFound:(res:Response,message:string)=>void;export declare const wrap:<P=RequestParams>(fn:(req:Request<P>,res:Response,next?:NextFunction)=>unknown)=>(req:Request<P>,res:Response,next?:NextFunction)=>Promise<void>;export declare function initPageLocals(res:Response,dbOrCtx:{dialect:string|{name:string;};path:string;isReadOnly:boolean;},dbId:string,allNames:string[]):{tables:string[];};
@@ -0,0 +1 @@
1
+ import type {ForeignKeyInfo,TableInfoData} from '../../db/types';export type SupportedDialect='sqlite'|'postgres';export interface NormalizedColumn{name:string;type:string;genericType:'integer'|'text'|'boolean'|'real'|'blob'|'datetime'|'json';primaryKey:boolean;autoIncrement:boolean;notNull:boolean;defaultValue:string|null;unique:boolean;foreignKey:{table:string;column:string;onDelete?:string;onUpdate?:string;}|null;}export interface NormalizedIndex{name:string;unique:boolean;columns:string[];}export interface NormalizedTable{name:string;columns:NormalizedColumn[];primaryKeys:string[];foreignKeys:ForeignKeyInfo[];indexes:NormalizedIndex[];}export declare function categorizeType(rawType:string):NormalizedColumn['genericType'];export declare function mapColumnToDialect(col:NormalizedColumn,targetDialect:SupportedDialect):string;export declare function normalizeTableInfo(info:TableInfoData,dialect:SupportedDialect):NormalizedTable;export declare function convertValueForDialect(value:unknown,genericType:NormalizedColumn['genericType'],targetDialect:SupportedDialect):unknown;export declare function mapDefaultValueForDialect(defaultValue:string|null|undefined,genericType:NormalizedColumn['genericType'],targetDialect:SupportedDialect):string|null;export declare function generateCreateTableForDialect(table:NormalizedTable,targetDialect:SupportedDialect):string;