admindb 2.5.0 → 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/dist/app.js +13 -1
- 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/router.d.ts +1 -1
- 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/db/dialects/postgres/introspector.js +1 -1
- package/dist/db/dialects/sqlite/introspector.js +6 -3
- package/dist/modules/index.d.ts +1 -1
- package/dist/modules/index.js +9 -1
- 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/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/public/css/app.css +1 -1
- package/dist/public/js/browse.js +1 -1
- package/dist/public/js/search.js +1 -0
- package/dist/public/js/sync.js +1 -0
- package/dist/utils/icons.js +3 -0
- package/dist/views/layouts/main.hbs +1 -1
- 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/package.json +7 -8
|
@@ -55,7 +55,9 @@ class SqliteIntrospector {
|
|
|
55
55
|
.prepare(`PRAGMA index_list(${(0, generator_1.quoteIdentifier)(table)})`)
|
|
56
56
|
.all();
|
|
57
57
|
const indexes = indexRows.map((ix) => {
|
|
58
|
-
const ixCols = db.prepare(`PRAGMA index_info(${(0, generator_1.quoteIdentifier)(ix.name)})`).all()
|
|
58
|
+
const ixCols = db.prepare(`PRAGMA index_info(${(0, generator_1.quoteIdentifier)(ix.name)})`).all()
|
|
59
|
+
.map((c) => c.name)
|
|
60
|
+
.filter((n) => Boolean(n));
|
|
59
61
|
const sqlRow = db
|
|
60
62
|
.prepare("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = ?")
|
|
61
63
|
.get(ix.name);
|
|
@@ -114,10 +116,11 @@ class SqliteIntrospector {
|
|
|
114
116
|
const uniqueCols = new Set();
|
|
115
117
|
const indexedCols = new Set();
|
|
116
118
|
for (const ix of indexes) {
|
|
119
|
+
const cols = Array.isArray(ix.columns) ? ix.columns.filter(Boolean) : [];
|
|
117
120
|
if (ix.unique)
|
|
118
|
-
|
|
121
|
+
cols.forEach((c) => uniqueCols.add(c));
|
|
119
122
|
if (ix.origin === 'c')
|
|
120
|
-
|
|
123
|
+
cols.forEach((c) => indexedCols.add(c));
|
|
121
124
|
}
|
|
122
125
|
const refs = this.getIncomingFks(db, table);
|
|
123
126
|
const refsByTo = new Map();
|
package/dist/modules/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import type {Router} from 'express';import type {RouteContext} from '../core/router';export * from './databases/index';export * from './tables/index';export * from './rows/index';export * from './query/index';export * from './schema/index';export * from './seed/index';export * from './erd/index';export declare function registerModules(router:Router,ctx:RouteContext):void;
|
|
1
|
+
import type {Router} from 'express';import type {RouteContext} from '../core/router';export * from './databases/index';export * from './tables/index';export * from './rows/index';export * from './query/index';export * from './schema/index';export * from './seed/index';export * from './erd/index';export * from './sync/index';export * from './search/index';export declare function registerModules(router:Router,ctx:RouteContext):void;
|
package/dist/modules/index.js
CHANGED
|
@@ -21,6 +21,8 @@ const schema_routes_1 = require("./schema/schema.routes");
|
|
|
21
21
|
const query_routes_1 = require("./query/query.routes");
|
|
22
22
|
const seed_routes_1 = require("./seed/seed.routes");
|
|
23
23
|
const erd_routes_1 = require("./erd/erd.routes");
|
|
24
|
+
const sync_routes_1 = require("./sync/sync.routes");
|
|
25
|
+
const search_routes_1 = require("./search/search.routes");
|
|
24
26
|
__exportStar(require("./databases/index"), exports);
|
|
25
27
|
__exportStar(require("./tables/index"), exports);
|
|
26
28
|
__exportStar(require("./rows/index"), exports);
|
|
@@ -28,6 +30,8 @@ __exportStar(require("./query/index"), exports);
|
|
|
28
30
|
__exportStar(require("./schema/index"), exports);
|
|
29
31
|
__exportStar(require("./seed/index"), exports);
|
|
30
32
|
__exportStar(require("./erd/index"), exports);
|
|
33
|
+
__exportStar(require("./sync/index"), exports);
|
|
34
|
+
__exportStar(require("./search/index"), exports);
|
|
31
35
|
function registerModules(router, ctx) {
|
|
32
36
|
router.use((req, res, next) => {
|
|
33
37
|
if (req.method === 'GET')
|
|
@@ -37,7 +41,9 @@ function registerModules(router, ctx) {
|
|
|
37
41
|
p.endsWith('/preview') ||
|
|
38
42
|
p.includes('/api/query/') ||
|
|
39
43
|
p.endsWith('/rows/bulk-impact') ||
|
|
40
|
-
p.endsWith('/rows/bulk-export')
|
|
44
|
+
p.endsWith('/rows/bulk-export') ||
|
|
45
|
+
p.startsWith('/api/sync/') ||
|
|
46
|
+
p.startsWith('/api/search/')) {
|
|
41
47
|
return next();
|
|
42
48
|
}
|
|
43
49
|
try {
|
|
@@ -59,4 +65,6 @@ function registerModules(router, ctx) {
|
|
|
59
65
|
(0, query_routes_1.registerQueryRoutes)(router, ctx);
|
|
60
66
|
(0, seed_routes_1.registerSeedRoutes)(router, ctx);
|
|
61
67
|
(0, erd_routes_1.registerErdRoutes)(router, ctx);
|
|
68
|
+
(0, sync_routes_1.registerSyncRoutes)(router, ctx);
|
|
69
|
+
(0, search_routes_1.registerSearchRoutes)(router, ctx);
|
|
62
70
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './search.types';export * from './search.service';export * from './search.routes';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./search.types"), exports);
|
|
18
|
+
__exportStar(require("./search.service"), exports);
|
|
19
|
+
__exportStar(require("./search.routes"), exports);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import type {Router} from 'express';import{type RouteContext} from '../../core/router';export declare function registerSearchRoutes(router:Router,ctx:RouteContext):void;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerSearchRoutes = registerSearchRoutes;
|
|
4
|
+
const router_1 = require("../../core/router");
|
|
5
|
+
const search_service_1 = require("./search.service");
|
|
6
|
+
function registerSearchRoutes(router, ctx) {
|
|
7
|
+
router.get('/api/search/:db', (0, router_1.wrap)(async (req, res) => {
|
|
8
|
+
const db = ctx.getContext(req);
|
|
9
|
+
const query = String(req.query.q ?? '');
|
|
10
|
+
const type = req.query.type ? String(req.query.type) : undefined;
|
|
11
|
+
const limit = req.query.limit ? parseInt(String(req.query.limit), 10) : 50;
|
|
12
|
+
const includeRows = req.query.includeRows !== 'false';
|
|
13
|
+
const basePath = String(res.locals.basePath ?? '');
|
|
14
|
+
try {
|
|
15
|
+
const results = await search_service_1.SearchService.search(db, query, {
|
|
16
|
+
type,
|
|
17
|
+
limit,
|
|
18
|
+
includeRows,
|
|
19
|
+
basePath,
|
|
20
|
+
});
|
|
21
|
+
(0, router_1.ok)(res, results);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
(0, router_1.fail)(res, err.message ?? 'Failed to execute global search.', 500);
|
|
25
|
+
}
|
|
26
|
+
}));
|
|
27
|
+
router.get('/api/search/:db/metadata', (0, router_1.wrap)(async (req, res) => {
|
|
28
|
+
const db = ctx.getContext(req);
|
|
29
|
+
const basePath = String(res.locals.basePath ?? '');
|
|
30
|
+
try {
|
|
31
|
+
const index = await search_service_1.SearchService.getMetadataIndex(db, basePath);
|
|
32
|
+
(0, router_1.ok)(res, index);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
(0, router_1.fail)(res, err.message ?? 'Failed to fetch search metadata.', 500);
|
|
36
|
+
}
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import type {DatabaseContext} from '../../core/context';import type {SearchOptions,SearchResponse,SearchMetadataIndex,CommandItem} from './search.types';export declare class SearchService{static getCommands(dbId:string,basePath?:string,isReadOnly?:boolean):CommandItem[];static getMetadataIndex(ctx:DatabaseContext,basePath?:string):Promise<SearchMetadataIndex>;private static calculateScore;static search(ctx:DatabaseContext,rawQuery:string,options?:SearchOptions):Promise<SearchResponse>;private static searchRowData;}
|
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SearchService = void 0;
|
|
4
|
+
const generator_1 = require("../../sql/generator");
|
|
5
|
+
class SearchService {
|
|
6
|
+
static getCommands(dbId, basePath = '', isReadOnly = false) {
|
|
7
|
+
const encDb = encodeURIComponent(dbId);
|
|
8
|
+
const commands = [
|
|
9
|
+
{
|
|
10
|
+
id: 'cmd-query',
|
|
11
|
+
title: 'New SQL Query',
|
|
12
|
+
description: 'Open the SQL query editor to run queries and scripts',
|
|
13
|
+
icon: 'query',
|
|
14
|
+
url: `${basePath}/query/${encDb}`,
|
|
15
|
+
keywords: ['sql', 'query', 'editor', 'execute', 'run', 'select', 'terminal'],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'cmd-erd',
|
|
19
|
+
title: 'Entity Relationship Diagram',
|
|
20
|
+
description: 'Visualize database tables, relationships, and schema graph',
|
|
21
|
+
icon: 'diagram',
|
|
22
|
+
url: `${basePath}/erd/${encDb}`,
|
|
23
|
+
keywords: ['erd', 'diagram', 'graph', 'schema', 'relations', 'visualize', 'foreign keys'],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'cmd-seed',
|
|
27
|
+
title: 'Seed Data Generator',
|
|
28
|
+
description: 'Generate relational mock and seed data with intelligent heuristics',
|
|
29
|
+
icon: 'seed',
|
|
30
|
+
url: `${basePath}/seed/${encDb}`,
|
|
31
|
+
keywords: ['seed', 'mock', 'fake data', 'generator', 'faker', 'synthetic data'],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'cmd-diff',
|
|
35
|
+
title: 'Diff & Replicate Database',
|
|
36
|
+
description: 'Compare schemas, generate migration patches, or clone data',
|
|
37
|
+
icon: 'diff',
|
|
38
|
+
url: `${basePath}/diff/${encDb}`,
|
|
39
|
+
keywords: ['diff', 'sync', 'replicate', 'migrate', 'migration', 'patch', 'clone', 'compare'],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'cmd-info',
|
|
43
|
+
title: 'Database Info & Settings',
|
|
44
|
+
description: 'View database pragma, engine configuration, and metadata',
|
|
45
|
+
icon: 'settings',
|
|
46
|
+
url: `${basePath}/info/${encDb}`,
|
|
47
|
+
keywords: ['info', 'settings', 'config', 'pragma', 'driver', 'version', 'database info'],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'cmd-export',
|
|
51
|
+
title: 'Export Database (SQL Dump)',
|
|
52
|
+
description: 'Download full database schema and data as an SQL script',
|
|
53
|
+
icon: 'export',
|
|
54
|
+
url: `${basePath}/export/${encDb}`,
|
|
55
|
+
keywords: ['export', 'dump', 'backup', 'download', 'sql dump', 'save'],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'cmd-home',
|
|
59
|
+
title: 'Database Dashboard',
|
|
60
|
+
description: 'Overview of all tables, total rows, and statistics',
|
|
61
|
+
icon: 'home',
|
|
62
|
+
url: `${basePath}/home/${encDb}`,
|
|
63
|
+
keywords: ['home', 'dashboard', 'overview', 'tables', 'stats'],
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
if (!isReadOnly) {
|
|
67
|
+
commands.push({
|
|
68
|
+
id: 'cmd-designer',
|
|
69
|
+
title: 'Create New Table',
|
|
70
|
+
description: 'Design and create a new database table with custom columns and keys',
|
|
71
|
+
icon: 'plus',
|
|
72
|
+
url: `${basePath}/designer/${encDb}`,
|
|
73
|
+
keywords: ['create table', 'designer', 'new table', 'add table', 'schema designer'],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return commands;
|
|
77
|
+
}
|
|
78
|
+
static async getMetadataIndex(ctx, basePath = '') {
|
|
79
|
+
const listRes = await ctx.db.listTables();
|
|
80
|
+
const tableItems = listRes.success && listRes.data ? listRes.data : [];
|
|
81
|
+
const tables = [];
|
|
82
|
+
const columns = [];
|
|
83
|
+
const indexes = [];
|
|
84
|
+
const foreignKeys = [];
|
|
85
|
+
await Promise.all(tableItems.map(async (t) => {
|
|
86
|
+
const tableName = t.name;
|
|
87
|
+
try {
|
|
88
|
+
const infoRes = await ctx.db.getTableInfo(tableName);
|
|
89
|
+
if (!infoRes.success || !infoRes.data)
|
|
90
|
+
return;
|
|
91
|
+
const info = infoRes.data;
|
|
92
|
+
tables.push({
|
|
93
|
+
name: tableName,
|
|
94
|
+
columnsCount: info.columns.length,
|
|
95
|
+
primaryKey: info.primaryKey || [],
|
|
96
|
+
});
|
|
97
|
+
for (const col of info.columns) {
|
|
98
|
+
const fk = (info.foreignKeys || []).find((f) => f.from === col.name);
|
|
99
|
+
columns.push({
|
|
100
|
+
table: tableName,
|
|
101
|
+
name: col.name,
|
|
102
|
+
type: col.type || 'TEXT',
|
|
103
|
+
isPk: Boolean(col.pk),
|
|
104
|
+
isFk: Boolean(fk),
|
|
105
|
+
fkTarget: fk ? `${fk.table}.${fk.to || 'id'}` : null,
|
|
106
|
+
notnull: Boolean(col.notnull),
|
|
107
|
+
dflt_value: col.dflt_value,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
for (const idx of info.indexes || []) {
|
|
111
|
+
indexes.push({
|
|
112
|
+
table: tableName,
|
|
113
|
+
name: idx.name,
|
|
114
|
+
unique: Boolean(idx.unique),
|
|
115
|
+
columns: idx.columns || [],
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
for (const fk of info.foreignKeys || []) {
|
|
119
|
+
foreignKeys.push({
|
|
120
|
+
fromTable: tableName,
|
|
121
|
+
fromColumn: fk.from,
|
|
122
|
+
toTable: fk.table,
|
|
123
|
+
toColumn: fk.to || null,
|
|
124
|
+
onUpdate: fk.on_update || 'NO ACTION',
|
|
125
|
+
onDelete: fk.on_delete || 'NO ACTION',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
}
|
|
131
|
+
}));
|
|
132
|
+
const commands = this.getCommands(ctx.name, basePath, ctx.isReadOnly || ctx.db.isReadOnly);
|
|
133
|
+
return {
|
|
134
|
+
tables,
|
|
135
|
+
columns,
|
|
136
|
+
indexes,
|
|
137
|
+
foreignKeys,
|
|
138
|
+
commands,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
static calculateScore(target, query) {
|
|
142
|
+
if (!target || !query)
|
|
143
|
+
return 0;
|
|
144
|
+
const t = target.toLowerCase();
|
|
145
|
+
const q = query.toLowerCase();
|
|
146
|
+
if (t === q)
|
|
147
|
+
return 100;
|
|
148
|
+
if (t.startsWith(q))
|
|
149
|
+
return 85;
|
|
150
|
+
if (t.includes(`_${q}`) || t.includes(`-${q}`) || t.includes(`.${q}`) || t.includes(` ${q}`))
|
|
151
|
+
return 75;
|
|
152
|
+
if (t.includes(q))
|
|
153
|
+
return 50;
|
|
154
|
+
let qIdx = 0;
|
|
155
|
+
for (let i = 0; i < t.length && qIdx < q.length; i++) {
|
|
156
|
+
if (t[i] === q[qIdx])
|
|
157
|
+
qIdx++;
|
|
158
|
+
}
|
|
159
|
+
if (qIdx === q.length)
|
|
160
|
+
return 25;
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
static async search(ctx, rawQuery, options = {}) {
|
|
164
|
+
const q = String(rawQuery ?? '').trim();
|
|
165
|
+
const basePath = options.basePath ?? '';
|
|
166
|
+
const type = options.type ? options.type.toLowerCase() : 'all';
|
|
167
|
+
const limit = options.limit && options.limit > 0 ? options.limit : 50;
|
|
168
|
+
const includeRows = options.includeRows !== false;
|
|
169
|
+
const encDb = encodeURIComponent(ctx.name);
|
|
170
|
+
const metadata = await this.getMetadataIndex(ctx, basePath);
|
|
171
|
+
const results = [];
|
|
172
|
+
const shouldSearch = (category) => {
|
|
173
|
+
if (type === 'all')
|
|
174
|
+
return true;
|
|
175
|
+
if (type === 'tables' || type === 'table')
|
|
176
|
+
return category === 'table';
|
|
177
|
+
if (type === 'columns' || type === 'column')
|
|
178
|
+
return category === 'column';
|
|
179
|
+
if (type === 'indexes' || type === 'index')
|
|
180
|
+
return category === 'index';
|
|
181
|
+
if (type === 'relations' || type === 'relation' || type === 'foreignkeys')
|
|
182
|
+
return category === 'relation';
|
|
183
|
+
if (type === 'commands' || type === 'command')
|
|
184
|
+
return category === 'command';
|
|
185
|
+
if (type === 'rows' || type === 'row' || type === 'data')
|
|
186
|
+
return category === 'row';
|
|
187
|
+
return false;
|
|
188
|
+
};
|
|
189
|
+
if (shouldSearch('table')) {
|
|
190
|
+
for (const table of metadata.tables) {
|
|
191
|
+
const score = q ? this.calculateScore(table.name, q) : 50;
|
|
192
|
+
if (score > 0 || !q) {
|
|
193
|
+
const encTable = encodeURIComponent(table.name);
|
|
194
|
+
results.push({
|
|
195
|
+
id: `tbl-${table.name}`,
|
|
196
|
+
category: 'table',
|
|
197
|
+
title: table.name,
|
|
198
|
+
subtitle: `Table · ${table.columnsCount} columns${table.primaryKey.length ? ` · PK (${table.primaryKey.join(', ')})` : ''}`,
|
|
199
|
+
icon: 'table',
|
|
200
|
+
url: `${basePath}/tables/${encDb}/${encTable}`,
|
|
201
|
+
score: score + 15,
|
|
202
|
+
badge: 'Table',
|
|
203
|
+
badgeType: 'primary',
|
|
204
|
+
actions: [
|
|
205
|
+
{ label: 'Browse', url: `${basePath}/tables/${encDb}/${encTable}`, primary: true },
|
|
206
|
+
{ label: 'Schema', url: `${basePath}/schema/${encDb}/${encTable}` },
|
|
207
|
+
{ label: 'Query', url: `${basePath}/query/${encDb}?sql=${encodeURIComponent(`SELECT * FROM ${table.name} LIMIT 100;`)}` },
|
|
208
|
+
],
|
|
209
|
+
metadata: {
|
|
210
|
+
table: table.name,
|
|
211
|
+
columnsCount: table.columnsCount,
|
|
212
|
+
primaryKey: table.primaryKey,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (shouldSearch('column')) {
|
|
219
|
+
for (const col of metadata.columns) {
|
|
220
|
+
let score = 0;
|
|
221
|
+
if (q) {
|
|
222
|
+
const nameScore = this.calculateScore(col.name, q);
|
|
223
|
+
const fullScore = this.calculateScore(`${col.table}.${col.name}`, q);
|
|
224
|
+
const typeScore = this.calculateScore(col.type, q) > 70 ? 40 : 0;
|
|
225
|
+
score = Math.max(nameScore, fullScore, typeScore);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
score = 30;
|
|
229
|
+
}
|
|
230
|
+
if (score > 0 || !q) {
|
|
231
|
+
const encTable = encodeURIComponent(col.table);
|
|
232
|
+
const pkTag = col.isPk ? ' · Primary Key' : '';
|
|
233
|
+
const fkTag = col.isFk ? ` · FK → ${col.fkTarget}` : '';
|
|
234
|
+
const nullTag = col.notnull ? ' · NOT NULL' : '';
|
|
235
|
+
const dfltTag = col.dflt_value ? ` · default ${col.dflt_value}` : '';
|
|
236
|
+
results.push({
|
|
237
|
+
id: `col-${col.table}-${col.name}`,
|
|
238
|
+
category: 'column',
|
|
239
|
+
title: col.name,
|
|
240
|
+
subtitle: `${col.table} · ${col.type}${pkTag}${fkTag}${nullTag}${dfltTag}`,
|
|
241
|
+
icon: 'columns',
|
|
242
|
+
url: `${basePath}/tables/${encDb}/${encTable}`,
|
|
243
|
+
score: score + 10,
|
|
244
|
+
badge: col.isPk ? 'PK' : col.isFk ? 'FK' : col.type,
|
|
245
|
+
badgeType: col.isPk ? 'accent' : col.isFk ? 'info' : 'neutral',
|
|
246
|
+
actions: [
|
|
247
|
+
{ label: 'Browse Table', url: `${basePath}/tables/${encDb}/${encTable}`, primary: true },
|
|
248
|
+
{ label: 'View Schema', url: `${basePath}/schema/${encDb}/${encTable}` },
|
|
249
|
+
],
|
|
250
|
+
metadata: {
|
|
251
|
+
table: col.table,
|
|
252
|
+
column: col.name,
|
|
253
|
+
type: col.type,
|
|
254
|
+
isPk: col.isPk,
|
|
255
|
+
isFk: col.isFk,
|
|
256
|
+
fkTarget: col.fkTarget,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (shouldSearch('index')) {
|
|
263
|
+
for (const idx of metadata.indexes) {
|
|
264
|
+
let score = 0;
|
|
265
|
+
if (q) {
|
|
266
|
+
const nameScore = this.calculateScore(idx.name, q);
|
|
267
|
+
const colsScore = idx.columns.some((c) => this.calculateScore(c, q) > 60) ? 60 : 0;
|
|
268
|
+
score = Math.max(nameScore, colsScore);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
score = 25;
|
|
272
|
+
}
|
|
273
|
+
if (score > 0 || !q) {
|
|
274
|
+
const encTable = encodeURIComponent(idx.table);
|
|
275
|
+
results.push({
|
|
276
|
+
id: `idx-${idx.table}-${idx.name}`,
|
|
277
|
+
category: 'index',
|
|
278
|
+
title: idx.name,
|
|
279
|
+
subtitle: `${idx.table} (${idx.columns.join(', ')}) · ${idx.unique ? 'UNIQUE ' : ''}Index`,
|
|
280
|
+
icon: 'key',
|
|
281
|
+
url: `${basePath}/schema/${encDb}/${encTable}`,
|
|
282
|
+
score,
|
|
283
|
+
badge: idx.unique ? 'Unique Index' : 'Index',
|
|
284
|
+
badgeType: idx.unique ? 'warning' : 'neutral',
|
|
285
|
+
actions: [
|
|
286
|
+
{ label: 'View in Schema', url: `${basePath}/schema/${encDb}/${encTable}`, primary: true },
|
|
287
|
+
{ label: 'Browse Table', url: `${basePath}/tables/${encDb}/${encTable}` },
|
|
288
|
+
],
|
|
289
|
+
metadata: {
|
|
290
|
+
table: idx.table,
|
|
291
|
+
index: idx.name,
|
|
292
|
+
unique: idx.unique,
|
|
293
|
+
columns: idx.columns,
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (shouldSearch('relation')) {
|
|
300
|
+
for (const fk of metadata.foreignKeys) {
|
|
301
|
+
let score = 0;
|
|
302
|
+
if (q) {
|
|
303
|
+
const fromScore = this.calculateScore(`${fk.fromTable}.${fk.fromColumn}`, q);
|
|
304
|
+
const toScore = this.calculateScore(`${fk.toTable}.${fk.toColumn || 'id'}`, q);
|
|
305
|
+
score = Math.max(fromScore, toScore);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
score = 25;
|
|
309
|
+
}
|
|
310
|
+
if (score > 0 || !q) {
|
|
311
|
+
const fromEnc = encodeURIComponent(fk.fromTable);
|
|
312
|
+
results.push({
|
|
313
|
+
id: `fk-${fk.fromTable}-${fk.fromColumn}-${fk.toTable}`,
|
|
314
|
+
category: 'relation',
|
|
315
|
+
title: `${fk.fromTable}.${fk.fromColumn} → ${fk.toTable}.${fk.toColumn || 'id'}`,
|
|
316
|
+
subtitle: `Foreign Key · ON UPDATE ${fk.onUpdate}, ON DELETE ${fk.onDelete}`,
|
|
317
|
+
icon: 'diagram',
|
|
318
|
+
url: `${basePath}/erd/${encDb}`,
|
|
319
|
+
score,
|
|
320
|
+
badge: 'Relation',
|
|
321
|
+
badgeType: 'info',
|
|
322
|
+
actions: [
|
|
323
|
+
{ label: 'View ERD', url: `${basePath}/erd/${encDb}`, primary: true },
|
|
324
|
+
{ label: `Browse ${fk.fromTable}`, url: `${basePath}/tables/${encDb}/${fromEnc}` },
|
|
325
|
+
],
|
|
326
|
+
metadata: {
|
|
327
|
+
fromTable: fk.fromTable,
|
|
328
|
+
fromColumn: fk.fromColumn,
|
|
329
|
+
toTable: fk.toTable,
|
|
330
|
+
toColumn: fk.toColumn,
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (shouldSearch('command')) {
|
|
337
|
+
for (const cmd of metadata.commands) {
|
|
338
|
+
let score = 0;
|
|
339
|
+
if (q) {
|
|
340
|
+
const titleScore = this.calculateScore(cmd.title, q);
|
|
341
|
+
const descScore = this.calculateScore(cmd.description, q) > 70 ? 40 : 0;
|
|
342
|
+
const kwScore = cmd.keywords.some((k) => this.calculateScore(k, q) > 70) ? 70 : 0;
|
|
343
|
+
score = Math.max(titleScore, descScore, kwScore);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
score = 45;
|
|
347
|
+
}
|
|
348
|
+
if (score > 0 || !q) {
|
|
349
|
+
results.push({
|
|
350
|
+
id: cmd.id,
|
|
351
|
+
category: 'command',
|
|
352
|
+
title: cmd.title,
|
|
353
|
+
subtitle: cmd.description,
|
|
354
|
+
icon: cmd.icon,
|
|
355
|
+
url: cmd.url,
|
|
356
|
+
score: score + 5,
|
|
357
|
+
badge: 'Command',
|
|
358
|
+
badgeType: 'secondary',
|
|
359
|
+
actions: [{ label: 'Execute', url: cmd.url, primary: true }],
|
|
360
|
+
metadata: {
|
|
361
|
+
keywords: cmd.keywords,
|
|
362
|
+
},
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (q.length >= 2 && includeRows && shouldSearch('row')) {
|
|
368
|
+
try {
|
|
369
|
+
const rowResults = await this.searchRowData(ctx, q, metadata.tables, metadata.columns, basePath, limit);
|
|
370
|
+
results.push(...rowResults);
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
results.sort((a, b) => b.score - a.score);
|
|
376
|
+
const categoryCounts = {
|
|
377
|
+
table: 0,
|
|
378
|
+
column: 0,
|
|
379
|
+
index: 0,
|
|
380
|
+
relation: 0,
|
|
381
|
+
command: 0,
|
|
382
|
+
row: 0,
|
|
383
|
+
};
|
|
384
|
+
for (const r of results) {
|
|
385
|
+
categoryCounts[r.category] = (categoryCounts[r.category] || 0) + 1;
|
|
386
|
+
}
|
|
387
|
+
const slicedResults = results.slice(0, limit);
|
|
388
|
+
return {
|
|
389
|
+
query: q,
|
|
390
|
+
total: results.length,
|
|
391
|
+
categories: categoryCounts,
|
|
392
|
+
results: slicedResults,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
static async searchRowData(ctx, query, tables, columns, basePath, limit) {
|
|
396
|
+
const rowResults = [];
|
|
397
|
+
const encDb = encodeURIComponent(ctx.name);
|
|
398
|
+
const maxTablesToSearch = 12;
|
|
399
|
+
const maxRowsPerTable = 3;
|
|
400
|
+
const searchableTables = tables.slice(0, maxTablesToSearch);
|
|
401
|
+
for (const table of searchableTables) {
|
|
402
|
+
if (rowResults.length >= limit)
|
|
403
|
+
break;
|
|
404
|
+
const textCols = columns.filter((c) => c.table === table.name &&
|
|
405
|
+
/(text|char|varchar|nvarchar|string|clob|json|blob)/i.test(c.type || 'TEXT'));
|
|
406
|
+
if (textCols.length === 0)
|
|
407
|
+
continue;
|
|
408
|
+
const colNames = textCols.map((c) => c.name);
|
|
409
|
+
const whereClauses = colNames.map((c) => `${(0, generator_1.quoteIdentifier)(c)} LIKE ?`).join(' OR ');
|
|
410
|
+
const params = colNames.map(() => `%${query}%`);
|
|
411
|
+
const selectSql = `SELECT rowid AS _rowid_, * FROM ${(0, generator_1.quoteIdentifier)(table.name)} WHERE ${whereClauses} LIMIT ${maxRowsPerTable}`;
|
|
412
|
+
try {
|
|
413
|
+
const res = await ctx.db.all(selectSql, params);
|
|
414
|
+
if (res.success && Array.isArray(res.data)) {
|
|
415
|
+
for (const row of res.data) {
|
|
416
|
+
let matchedCol = colNames[0];
|
|
417
|
+
let matchedVal = '';
|
|
418
|
+
for (const colName of colNames) {
|
|
419
|
+
const val = String(row[colName] ?? '');
|
|
420
|
+
if (val.toLowerCase().includes(query.toLowerCase())) {
|
|
421
|
+
matchedCol = colName;
|
|
422
|
+
matchedVal = val;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const snippet = matchedVal.length > 80 ? `${matchedVal.slice(0, 80)}…` : matchedVal;
|
|
427
|
+
const rowId = row._rowid_ ?? row.id ?? row[table.primaryKey[0]] ?? '';
|
|
428
|
+
const encTable = encodeURIComponent(table.name);
|
|
429
|
+
rowResults.push({
|
|
430
|
+
id: `row-${table.name}-${rowId}-${matchedCol}`,
|
|
431
|
+
category: 'row',
|
|
432
|
+
title: `${table.name}${rowId ? ` #${rowId}` : ''}: ${matchedCol} = "${snippet}"`,
|
|
433
|
+
subtitle: `Row data match in ${table.name}.${matchedCol}`,
|
|
434
|
+
icon: 'inspect',
|
|
435
|
+
url: `${basePath}/tables/${encDb}/${encTable}`,
|
|
436
|
+
score: 35,
|
|
437
|
+
badge: 'Row Data',
|
|
438
|
+
badgeType: 'neutral',
|
|
439
|
+
actions: [
|
|
440
|
+
{ label: 'Browse Table', url: `${basePath}/tables/${encDb}/${encTable}`, primary: true },
|
|
441
|
+
],
|
|
442
|
+
metadata: {
|
|
443
|
+
table: table.name,
|
|
444
|
+
column: matchedCol,
|
|
445
|
+
value: snippet,
|
|
446
|
+
row,
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return rowResults;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
exports.SearchService = SearchService;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type SearchCategory='table'|'column'|'index'|'relation'|'command'|'row';export interface SearchAction{label:string;url:string;icon?:string;primary?:boolean;}export interface SearchItem{id:string;category:SearchCategory;title:string;subtitle:string;icon:string;url:string;score:number;badge?:string;badgeType?:'primary'|'secondary'|'accent'|'info'|'success'|'warning'|'error'|'neutral';actions?:SearchAction[];metadata?:Record<string,unknown>;}export interface SearchResponse{query:string;total:number;categories:Record<SearchCategory,number>;results:SearchItem[];}export interface SearchOptions{query?:string;type?:string;limit?:number;includeRows?:boolean;basePath?:string;}export interface TableSummary{name:string;columnsCount:number;rowCount?:number;primaryKey:string[];}export interface ColumnSummary{table:string;name:string;type:string;isPk:boolean;isFk:boolean;fkTarget?:string|null;notnull:boolean;dflt_value:string|null;}export interface IndexSummary{table:string;name:string;unique:boolean;columns:string[];}export interface ForeignKeySummary{fromTable:string;fromColumn:string;toTable:string;toColumn:string|null;onUpdate:string;onDelete:string;}export interface CommandItem{id:string;title:string;description:string;icon:string;url:string;keywords:string[];shortcut?:string;}export interface SearchMetadataIndex{tables:TableSummary[];columns:ColumnSummary[];indexes:IndexSummary[];foreignKeys:ForeignKeySummary[];commands:CommandItem[];}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './sync.service';export * from './sync.routes';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./sync.service"), exports);
|
|
18
|
+
__exportStar(require("./sync.routes"), exports);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import type {Router} from 'express';import{type RouteContext} from '../../core/router';export declare function registerSyncRoutes(router:Router,ctx:RouteContext):void;
|