admindb 2.4.1 → 2.6.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/LICENSE +20 -20
- package/dist/app.js +48 -25
- package/dist/core/context.d.ts +1 -0
- package/dist/core/context.js +2 -0
- package/dist/core/diff/data-differ.d.ts +1 -0
- package/dist/core/diff/data-differ.js +243 -0
- package/dist/core/diff/patch-generator.d.ts +1 -0
- package/dist/core/diff/patch-generator.js +77 -0
- package/dist/core/diff/schema-differ.d.ts +1 -0
- package/dist/core/diff/schema-differ.js +229 -0
- package/dist/core/errors.d.ts +1 -0
- package/dist/core/errors.js +39 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +20 -0
- package/dist/core/result.d.ts +1 -0
- package/dist/core/result.js +10 -0
- package/dist/core/router.d.ts +1 -0
- package/dist/core/router.js +37 -0
- package/dist/core/transfer/dialect-mapper.d.ts +1 -0
- package/dist/core/transfer/dialect-mapper.js +303 -0
- package/dist/core/transfer/masking.d.ts +1 -0
- package/dist/core/transfer/masking.js +49 -0
- package/dist/core/transfer/sync-engine.d.ts +1 -0
- package/dist/core/transfer/sync-engine.js +234 -0
- package/dist/core/transfer/topological-sort.d.ts +1 -0
- package/dist/core/transfer/topological-sort.js +55 -0
- package/dist/data/chain.js +1 -1
- package/dist/data/engine.js +1 -1
- package/dist/db/database.d.ts +1 -1
- package/dist/db/database.js +9 -31
- package/dist/db/dialects/index.d.ts +1 -0
- package/dist/db/dialects/index.js +58 -0
- package/dist/db/dialects/postgres/ddl.d.ts +1 -0
- package/dist/db/dialects/postgres/ddl.js +107 -0
- package/dist/db/dialects/postgres/dialect.d.ts +1 -0
- package/dist/db/dialects/postgres/dialect.js +28 -0
- package/dist/db/dialects/postgres/introspector.d.ts +1 -0
- package/dist/db/dialects/postgres/introspector.js +265 -0
- package/dist/db/dialects/postgres/types.d.ts +1 -0
- package/dist/db/dialects/postgres/types.js +83 -0
- package/dist/db/dialects/sqlite/ddl.d.ts +1 -0
- package/dist/db/dialects/sqlite/ddl.js +244 -0
- package/dist/db/dialects/sqlite/dialect.d.ts +1 -0
- package/dist/db/dialects/sqlite/dialect.js +25 -0
- package/dist/db/dialects/sqlite/introspector.d.ts +1 -0
- package/dist/db/dialects/sqlite/introspector.js +217 -0
- package/dist/db/dialects/sqlite/types.d.ts +1 -0
- package/dist/db/dialects/sqlite/types.js +69 -0
- package/dist/db/dialects/types.d.ts +1 -0
- package/dist/db/dialects/types.js +15 -0
- package/dist/db/export.js +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/manager.d.ts +1 -1
- package/dist/db/manager.js +101 -21
- package/dist/db/postgres.d.ts +1 -1
- package/dist/db/postgres.js +20 -284
- package/dist/db/types.d.ts +1 -1
- package/dist/db/types.js +0 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -1
- package/dist/modules/databases/databases.routes.d.ts +1 -0
- package/dist/modules/databases/databases.routes.js +150 -0
- package/dist/modules/databases/databases.service.d.ts +1 -0
- package/dist/modules/databases/databases.service.js +134 -0
- package/dist/modules/databases/index.d.ts +1 -0
- package/dist/{routes → modules/databases}/index.js +2 -3
- package/dist/modules/erd/erd.routes.d.ts +1 -0
- package/dist/modules/erd/erd.routes.js +32 -0
- package/dist/modules/erd/erd.service.d.ts +1 -0
- package/dist/modules/erd/erd.service.js +39 -0
- package/dist/modules/erd/index.d.ts +1 -0
- package/dist/modules/erd/index.js +18 -0
- package/dist/modules/index.d.ts +1 -0
- package/dist/modules/index.js +70 -0
- package/dist/modules/query/index.d.ts +1 -0
- package/dist/modules/query/index.js +18 -0
- package/dist/modules/query/query.routes.d.ts +1 -0
- package/dist/modules/query/query.routes.js +54 -0
- package/dist/modules/query/query.service.d.ts +1 -0
- package/dist/modules/query/query.service.js +95 -0
- package/dist/modules/rows/blob.helper.d.ts +1 -0
- package/dist/modules/rows/blob.helper.js +40 -0
- package/dist/modules/rows/display.helper.d.ts +1 -0
- package/dist/modules/rows/display.helper.js +73 -0
- package/dist/modules/rows/index.d.ts +1 -0
- package/dist/modules/rows/index.js +20 -0
- package/dist/modules/rows/rows.routes.d.ts +1 -0
- package/dist/modules/rows/rows.routes.js +555 -0
- package/dist/modules/rows/rows.service.d.ts +1 -0
- package/dist/modules/rows/rows.service.js +433 -0
- package/dist/modules/schema/index.d.ts +1 -0
- package/dist/modules/schema/index.js +18 -0
- package/dist/modules/schema/schema.routes.d.ts +1 -0
- package/dist/modules/schema/schema.routes.js +171 -0
- package/dist/modules/schema/schema.service.d.ts +1 -0
- package/dist/modules/schema/schema.service.js +58 -0
- package/dist/modules/search/index.d.ts +1 -0
- package/dist/modules/search/index.js +19 -0
- package/dist/modules/search/search.routes.d.ts +1 -0
- package/dist/modules/search/search.routes.js +38 -0
- package/dist/modules/search/search.service.d.ts +1 -0
- package/dist/modules/search/search.service.js +458 -0
- package/dist/modules/search/search.types.d.ts +1 -0
- package/dist/modules/search/search.types.js +2 -0
- package/dist/modules/seed/index.d.ts +1 -0
- package/dist/modules/seed/index.js +18 -0
- package/dist/modules/seed/seed.routes.d.ts +1 -0
- package/dist/modules/seed/seed.routes.js +212 -0
- package/dist/modules/seed/seed.service.d.ts +1 -0
- package/dist/{routes/api/helpers.js → modules/seed/seed.service.js} +86 -109
- package/dist/modules/sync/index.d.ts +1 -0
- package/dist/modules/sync/index.js +18 -0
- package/dist/modules/sync/sync.routes.d.ts +1 -0
- package/dist/modules/sync/sync.routes.js +135 -0
- package/dist/modules/sync/sync.service.d.ts +1 -0
- package/dist/modules/sync/sync.service.js +41 -0
- package/dist/modules/tables/index.d.ts +1 -0
- package/dist/modules/tables/index.js +18 -0
- package/dist/modules/tables/tables.routes.d.ts +1 -0
- package/dist/modules/tables/tables.routes.js +194 -0
- package/dist/modules/tables/tables.service.d.ts +1 -0
- package/dist/modules/tables/tables.service.js +218 -0
- package/dist/public/css/app.css +1 -1
- package/dist/public/js/browse.js +1 -1
- package/dist/public/js/query.js +1 -1
- package/dist/public/js/search.js +1 -0
- package/dist/public/js/sync.js +1 -0
- package/dist/types/api.d.ts +1 -1
- package/dist/utils/common.d.ts +1 -1
- package/dist/utils/common.js +1 -3
- package/dist/utils/icons.js +3 -0
- package/dist/utils/stream.d.ts +1 -0
- package/dist/utils/stream.js +131 -0
- package/dist/views/layouts/main.hbs +1 -1
- package/dist/views/pages/error.hbs +10 -1
- package/dist/views/pages/home.hbs +1 -1
- package/dist/views/pages/query.hbs +1 -1
- package/dist/views/pages/seed-select.hbs +1 -252
- package/dist/views/pages/seed.hbs +1 -307
- package/dist/views/pages/sync.hbs +409 -0
- package/dist/views/pages/table.hbs +1 -1
- package/dist/views/partials/navbar.hbs +1 -1
- package/dist/views/partials/sidebar.hbs +1 -1
- package/docs/API.md +4 -17
- package/docs/EXAMPLES.md +579 -579
- package/docs/SECURITY.md +265 -265
- package/package.json +10 -9
- package/dist/db/introspection.d.ts +0 -1
- package/dist/db/introspection.js +0 -164
- package/dist/routes/api/erd.d.ts +0 -1
- package/dist/routes/api/erd.js +0 -45
- package/dist/routes/api/helpers.d.ts +0 -1
- package/dist/routes/api/import-export.d.ts +0 -1
- package/dist/routes/api/import-export.js +0 -84
- package/dist/routes/api/index.d.ts +0 -1
- package/dist/routes/api/index.js +0 -55
- package/dist/routes/api/query.d.ts +0 -1
- package/dist/routes/api/query.js +0 -77
- package/dist/routes/api/rows.d.ts +0 -1
- package/dist/routes/api/rows.js +0 -373
- package/dist/routes/api/seed.d.ts +0 -1
- package/dist/routes/api/seed.js +0 -180
- package/dist/routes/api/tables.d.ts +0 -1
- package/dist/routes/api/tables.js +0 -230
- package/dist/routes/databases.d.ts +0 -1
- package/dist/routes/databases.js +0 -212
- package/dist/routes/index.d.ts +0 -1
- package/dist/routes/pages.d.ts +0 -1
- package/dist/routes/pages.js +0 -540
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
|
@@ -10,14 +10,13 @@ const express_handlebars_1 = require("express-handlebars");
|
|
|
10
10
|
const database_1 = require("./db/database");
|
|
11
11
|
const postgres_1 = require("./db/postgres");
|
|
12
12
|
const logger_1 = require("./utils/logger");
|
|
13
|
-
const
|
|
14
|
-
const index_1 = require("./routes/api/index");
|
|
15
|
-
const databases_1 = require("./routes/databases");
|
|
13
|
+
const index_1 = require("./modules/index");
|
|
16
14
|
const args_1 = require("./cli/args");
|
|
17
15
|
const common_1 = require("./utils/common");
|
|
18
16
|
const icons_1 = require("./utils/icons");
|
|
19
17
|
const serverless_1 = require("./serverless");
|
|
20
18
|
const auth_1 = require("./auth");
|
|
19
|
+
const index_2 = require("./db/dialects/index");
|
|
21
20
|
const APP_VERSION = (0, args_1.getPackageVersion)();
|
|
22
21
|
function createRouter(options = {}) {
|
|
23
22
|
const isServerless = options.serverless !== undefined ? options.serverless : (0, serverless_1.isServerlessEnvironment)();
|
|
@@ -60,10 +59,15 @@ function createRouter(options = {}) {
|
|
|
60
59
|
else {
|
|
61
60
|
singleDb = new database_1.SqliteDatabase(options.dbPath ?? 'admindb.db', logger, { readonly: globalReadonly });
|
|
62
61
|
}
|
|
63
|
-
if (!defaultDbId) {
|
|
62
|
+
if (!defaultDbId && singleDb) {
|
|
64
63
|
defaultDbId = singleDb.dialect === 'postgres' ? 'PostgreSQL' : (node_path_1.default.basename(singleDb.path) || 'SQLite');
|
|
65
64
|
}
|
|
66
65
|
}
|
|
66
|
+
let singleDialect;
|
|
67
|
+
if (singleDb) {
|
|
68
|
+
const pgSchema = singleDb.schema || 'public';
|
|
69
|
+
singleDialect = (0, index_2.getDialect)(singleDb.dialect, singleDb.dialect === 'postgres' ? pgSchema : undefined);
|
|
70
|
+
}
|
|
67
71
|
const app = (0, express_1.default)();
|
|
68
72
|
app.disable('x-powered-by');
|
|
69
73
|
app.use(express_1.default.static(node_path_1.default.join(__dirname, 'public')));
|
|
@@ -88,14 +92,28 @@ function createRouter(options = {}) {
|
|
|
88
92
|
res.locals.isDefaultPassword = authConfig.isDefaultPassword;
|
|
89
93
|
res.locals.icons = icons_1.ICONS;
|
|
90
94
|
res.locals.tables = [];
|
|
91
|
-
res.locals.internalTables = [];
|
|
92
95
|
next();
|
|
93
96
|
}
|
|
94
97
|
catch (err) {
|
|
95
98
|
next(err);
|
|
96
99
|
}
|
|
97
100
|
});
|
|
98
|
-
|
|
101
|
+
if (manager) {
|
|
102
|
+
(0, index_1.registerDatabasesRoutes)(app, {
|
|
103
|
+
manager,
|
|
104
|
+
logger,
|
|
105
|
+
basePath,
|
|
106
|
+
readonly: globalReadonly,
|
|
107
|
+
allowBrowse: isServerless ? false : options.allowBrowse,
|
|
108
|
+
browseRoot: options.browseRoot,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
app.get('/', (_req, res) => {
|
|
113
|
+
res.redirect(302, `${basePath}/home/${encodeURIComponent(defaultDbId)}`);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const getContext = (req) => {
|
|
99
117
|
if (manager) {
|
|
100
118
|
const dbId = String(req.params.db || req.params.dbId || req.params.id || '');
|
|
101
119
|
if (!manager.has(dbId)) {
|
|
@@ -111,27 +129,32 @@ function createRouter(options = {}) {
|
|
|
111
129
|
}
|
|
112
130
|
}
|
|
113
131
|
const effectiveRo = globalReadonly ? true : manager.isDbReadOnly(dbId);
|
|
114
|
-
return manager.
|
|
132
|
+
return manager.getContext(dbId, effectiveRo);
|
|
115
133
|
}
|
|
116
|
-
return
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
134
|
+
return {
|
|
135
|
+
id: defaultDbId,
|
|
136
|
+
name: defaultDbId,
|
|
137
|
+
path: singleDb.path,
|
|
138
|
+
db: singleDb,
|
|
139
|
+
dialect: singleDialect,
|
|
140
|
+
capabilities: singleDialect.capabilities,
|
|
141
|
+
isReadOnly: singleDb.isReadOnly,
|
|
121
142
|
logger,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
143
|
+
};
|
|
144
|
+
};
|
|
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 });
|
|
135
158
|
addErrorHandlers(app, logger);
|
|
136
159
|
return app;
|
|
137
160
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import type {Logger} from '../utils/logger';import type {IDatabase} from '../db/types';import type {IDialect,DialectCapabilities} from '../db/dialects/types';export interface DatabaseContext{readonly id:string;readonly name:string;readonly path:string;readonly db:IDatabase;readonly dialect:IDialect;readonly capabilities:DialectCapabilities;readonly isReadOnly:boolean;readonly logger:Logger;}
|
|
@@ -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>;
|