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
@@ -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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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;
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerSyncRoutes = registerSyncRoutes;
4
+ const router_1 = require("../../core/router");
5
+ const tables_service_1 = require("../tables/tables.service");
6
+ const sync_service_1 = require("./sync.service");
7
+ function registerSyncRoutes(router, ctx) {
8
+ const getDb = (dbId, readonly = false) => {
9
+ if (ctx.getNamedContext) {
10
+ return ctx.getNamedContext(dbId, readonly);
11
+ }
12
+ return ctx.getContext({ params: { db: dbId } });
13
+ };
14
+ const renderSyncPage = async (req, res, next) => {
15
+ try {
16
+ const activeDbId = req.params.db || res.locals.dbId || '';
17
+ let currentCtx = null;
18
+ let allNames = [];
19
+ try {
20
+ if (activeDbId) {
21
+ currentCtx = getDb(activeDbId);
22
+ const tablesList = await tables_service_1.TableService.listTables(currentCtx);
23
+ allNames = tablesList.map((t) => t.name);
24
+ (0, router_1.initPageLocals)(res, currentCtx, activeDbId, allNames);
25
+ }
26
+ }
27
+ catch { }
28
+ const databases = sync_service_1.SyncService.listAvailableDatabases(ctx.manager, currentCtx
29
+ ? {
30
+ id: currentCtx.id,
31
+ name: currentCtx.name,
32
+ dialect: currentCtx.dialect.name,
33
+ path: currentCtx.path,
34
+ readonly: currentCtx.isReadOnly,
35
+ }
36
+ : undefined);
37
+ res.render('pages/sync', {
38
+ title: 'Diff & Replication Studio',
39
+ dbId: activeDbId,
40
+ databases,
41
+ tables: allNames,
42
+ isSqlite: currentCtx?.dialect.name === 'sqlite',
43
+ isPostgres: currentCtx?.dialect.name === 'postgres',
44
+ readonly: currentCtx ? currentCtx.isReadOnly : false,
45
+ });
46
+ }
47
+ catch (err) {
48
+ next(err);
49
+ }
50
+ };
51
+ router.get('/diff', renderSyncPage);
52
+ router.get('/diff/:db', renderSyncPage);
53
+ router.get('/sync', renderSyncPage);
54
+ router.get('/sync/:db', renderSyncPage);
55
+ router.get('/api/sync/databases', (0, router_1.wrap)(async (_req, res) => {
56
+ const databases = sync_service_1.SyncService.listAvailableDatabases(ctx.manager);
57
+ (0, router_1.ok)(res, databases);
58
+ }));
59
+ router.post('/api/sync/diff/schema', (0, router_1.wrap)(async (req, res) => {
60
+ const { sourceDbId, targetDbId } = req.body || {};
61
+ if (!sourceDbId || !targetDbId) {
62
+ return (0, router_1.fail)(res, 'Both sourceDbId and targetDbId are required.');
63
+ }
64
+ const srcCtx = getDb(sourceDbId, true);
65
+ const tgtCtx = getDb(targetDbId, true);
66
+ const diff = await sync_service_1.SyncService.diffSchema(srcCtx.db, sourceDbId, tgtCtx.db, targetDbId);
67
+ (0, router_1.ok)(res, diff);
68
+ }));
69
+ router.post('/api/sync/diff/data', (0, router_1.wrap)(async (req, res) => {
70
+ const { sourceDbId, targetDbId, table, limit } = req.body || {};
71
+ if (!sourceDbId || !targetDbId || !table) {
72
+ return (0, router_1.fail)(res, 'sourceDbId, targetDbId, and table are required.');
73
+ }
74
+ const srcCtx = getDb(sourceDbId, true);
75
+ const tgtCtx = getDb(targetDbId, true);
76
+ const rowLimit = typeof limit === 'number' && limit > 0 ? Math.min(limit, 500) : 100;
77
+ const diff = await sync_service_1.SyncService.diffTableData(srcCtx.db, tgtCtx.db, table, rowLimit);
78
+ (0, router_1.ok)(res, diff);
79
+ }));
80
+ router.post('/api/sync/patch', (0, router_1.wrap)(async (req, res) => {
81
+ const { sourceDbId, targetDbId } = req.body || {};
82
+ if (!sourceDbId || !targetDbId) {
83
+ return (0, router_1.fail)(res, 'Both sourceDbId and targetDbId are required.');
84
+ }
85
+ const srcCtx = getDb(sourceDbId, true);
86
+ const tgtCtx = getDb(targetDbId, true);
87
+ const diff = await sync_service_1.SyncService.diffSchema(srcCtx.db, sourceDbId, tgtCtx.db, targetDbId);
88
+ const targetDialect = tgtCtx.dialect.name === 'postgres' ? 'postgres' : 'sqlite';
89
+ const patch = sync_service_1.SyncService.generatePatch(diff, targetDialect);
90
+ (0, router_1.ok)(res, patch);
91
+ }));
92
+ router.post('/api/sync/dry-run', (0, router_1.wrap)(async (req, res) => {
93
+ const { sourceDbId, targetDbId, mode, tables, maskData } = req.body || {};
94
+ if (!sourceDbId || !targetDbId) {
95
+ return (0, router_1.fail)(res, 'Both sourceDbId and targetDbId are required.');
96
+ }
97
+ const srcCtx = getDb(sourceDbId, true);
98
+ const tgtCtx = getDb(targetDbId, true);
99
+ const result = await sync_service_1.SyncService.replicate(srcCtx.db, sourceDbId, tgtCtx.db, targetDbId, {
100
+ mode,
101
+ tables,
102
+ maskData,
103
+ dryRun: true,
104
+ });
105
+ (0, router_1.ok)(res, result);
106
+ }));
107
+ router.post('/api/sync/execute', (0, router_1.wrap)(async (req, res) => {
108
+ const { sourceDbId, targetDbId, mode, tables, maskData, confirmTargetName } = req.body || {};
109
+ if (!sourceDbId || !targetDbId) {
110
+ return (0, router_1.fail)(res, 'Both sourceDbId and targetDbId are required.');
111
+ }
112
+ if (sourceDbId === targetDbId) {
113
+ return (0, router_1.fail)(res, 'Source and target database cannot be the same.');
114
+ }
115
+ const srcCtx = getDb(sourceDbId, true);
116
+ const tgtCtx = getDb(targetDbId, false);
117
+ if (tgtCtx.isReadOnly) {
118
+ return (0, router_1.fail)(res, `Target database "${targetDbId}" is in read-only mode and cannot be modified.`);
119
+ }
120
+ const isTargetProd = targetDbId.toLowerCase().includes('prod') || targetDbId.toLowerCase().includes('production');
121
+ if (isTargetProd && confirmTargetName !== targetDbId) {
122
+ return (0, router_1.fail)(res, `Target database appears to be PRODUCTION (${targetDbId}). You must type the target database name to confirm.`, 400, { requiresConfirmation: true, targetDbId });
123
+ }
124
+ const result = await sync_service_1.SyncService.replicate(srcCtx.db, sourceDbId, tgtCtx.db, targetDbId, {
125
+ mode,
126
+ tables,
127
+ maskData,
128
+ dryRun: false,
129
+ });
130
+ if (!result.success) {
131
+ return (0, router_1.fail)(res, result.error || 'Replication failed.', 500, { result });
132
+ }
133
+ (0, router_1.ok)(res, result);
134
+ }));
135
+ }
@@ -0,0 +1 @@
1
+ import type {IDatabase} from '../../db/types';import type {DbManager,DatabaseEntry} from '../../db/manager';import{type SchemaDiffResult} from '../../core/diff/schema-differ';import{type TableDataDiffResult} from '../../core/diff/data-differ';import{type GeneratedPatch} from '../../core/diff/patch-generator';import{type SyncOptions,type SyncResult} from '../../core/transfer/sync-engine';import type {SupportedDialect} from '../../core/transfer/dialect-mapper';export declare class SyncService{static listAvailableDatabases(manager?:DbManager,defaultDb?:{id:string;name:string;dialect:string;path:string;readonly :boolean;}):DatabaseEntry[];static diffSchema(sourceDb:IDatabase,sourceDbId:string,targetDb:IDatabase,targetDbId:string):Promise<SchemaDiffResult>;static diffTableData(sourceDb:IDatabase,targetDb:IDatabase,table:string,limit?:number):Promise<TableDataDiffResult>;static generatePatch(diff:SchemaDiffResult,targetDialect:SupportedDialect):GeneratedPatch;static replicate(sourceDb:IDatabase,sourceDbId:string,targetDb:IDatabase,targetDbId:string,options:SyncOptions):Promise<SyncResult>;}
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SyncService = void 0;
4
+ const schema_differ_1 = require("../../core/diff/schema-differ");
5
+ const data_differ_1 = require("../../core/diff/data-differ");
6
+ const patch_generator_1 = require("../../core/diff/patch-generator");
7
+ const sync_engine_1 = require("../../core/transfer/sync-engine");
8
+ class SyncService {
9
+ static listAvailableDatabases(manager, defaultDb) {
10
+ if (manager) {
11
+ return manager.list();
12
+ }
13
+ if (defaultDb) {
14
+ return [
15
+ {
16
+ id: defaultDb.id,
17
+ name: defaultDb.name,
18
+ path: defaultDb.path,
19
+ dialect: defaultDb.dialect === 'postgres' ? 'postgres' : 'sqlite',
20
+ size: 0,
21
+ modified: '',
22
+ readonly: defaultDb.readonly,
23
+ },
24
+ ];
25
+ }
26
+ return [];
27
+ }
28
+ static async diffSchema(sourceDb, sourceDbId, targetDb, targetDbId) {
29
+ return (0, schema_differ_1.compareSchemas)(sourceDb, sourceDbId, targetDb, targetDbId);
30
+ }
31
+ static async diffTableData(sourceDb, targetDb, table, limit = 200) {
32
+ return (0, data_differ_1.compareTableData)(sourceDb, targetDb, table, limit);
33
+ }
34
+ static generatePatch(diff, targetDialect) {
35
+ return (0, patch_generator_1.generateSchemaPatch)(diff, targetDialect);
36
+ }
37
+ static async replicate(sourceDb, sourceDbId, targetDb, targetDbId, options) {
38
+ return (0, sync_engine_1.executeSync)(sourceDb, sourceDbId, targetDb, targetDbId, options);
39
+ }
40
+ }
41
+ exports.SyncService = SyncService;