@hed-hog/cli 0.0.94 → 0.0.96
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/package.json +1 -1
- package/dist/src/app.module.js +2 -0
- package/dist/src/app.module.js.map +1 -1
- package/dist/src/commands/new.command.d.ts +6 -0
- package/dist/src/commands/new.command.js +198 -93
- package/dist/src/commands/new.command.js.map +1 -1
- package/dist/src/commands/update.command.d.ts +14 -0
- package/dist/src/commands/update.command.js +58 -0
- package/dist/src/commands/update.command.js.map +1 -0
- package/dist/src/modules/database/database.service.d.ts +8 -0
- package/dist/src/modules/database/database.service.js +120 -0
- package/dist/src/modules/database/database.service.js.map +1 -1
- package/dist/src/modules/hedhog/hedhog.module.js +5 -2
- package/dist/src/modules/hedhog/hedhog.module.js.map +1 -1
- package/dist/src/modules/hedhog/hedhog.service.d.ts +12 -0
- package/dist/src/modules/hedhog/hedhog.service.js +178 -9
- package/dist/src/modules/hedhog/hedhog.service.js.map +1 -1
- package/dist/src/modules/hedhog/services/diff.service.d.ts +107 -0
- package/dist/src/modules/hedhog/services/diff.service.js +573 -0
- package/dist/src/modules/hedhog/services/diff.service.js.map +1 -0
- package/dist/src/modules/hedhog/services/migration.service.d.ts +36 -1
- package/dist/src/modules/hedhog/services/migration.service.js +619 -11
- package/dist/src/modules/hedhog/services/migration.service.js.map +1 -1
- package/dist/src/modules/hedhog/services/table.service.d.ts +7 -0
- package/dist/src/modules/hedhog/services/table.service.js +93 -13
- package/dist/src/modules/hedhog/services/table.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { FileSystemService } from './file-system.service';
|
|
2
|
+
import { TableColumn, TableDefinition } from './table.service';
|
|
3
|
+
export interface ColumnChange {
|
|
4
|
+
column: TableColumn;
|
|
5
|
+
oldType: string;
|
|
6
|
+
newType: string;
|
|
7
|
+
oldNullable: boolean;
|
|
8
|
+
newNullable: boolean;
|
|
9
|
+
oldDefault: any;
|
|
10
|
+
newDefault: any;
|
|
11
|
+
oldLength: number | undefined;
|
|
12
|
+
newLength: number | undefined;
|
|
13
|
+
}
|
|
14
|
+
export interface RenameCandidate {
|
|
15
|
+
oldColumn: TableColumn;
|
|
16
|
+
newColumn: TableColumn;
|
|
17
|
+
}
|
|
18
|
+
export interface RenameDecision {
|
|
19
|
+
oldName: string;
|
|
20
|
+
newName: string;
|
|
21
|
+
oldColumn: TableColumn;
|
|
22
|
+
newColumn: TableColumn;
|
|
23
|
+
isRename: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface EnumValueChanges {
|
|
26
|
+
columnName: string;
|
|
27
|
+
addedValues: string[];
|
|
28
|
+
removedValues: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface TableChanges {
|
|
31
|
+
tableName: string;
|
|
32
|
+
addedColumns: TableColumn[];
|
|
33
|
+
/** Columns dropped AFTER rename resolution (confirmed drops only) */
|
|
34
|
+
removedColumns: TableColumn[];
|
|
35
|
+
/** Confirmed renames (isRename === true) */
|
|
36
|
+
renamedColumns: RenameDecision[];
|
|
37
|
+
changedColumns: ColumnChange[];
|
|
38
|
+
addedUniqueConstraints: string[];
|
|
39
|
+
removedUniqueConstraints: string[];
|
|
40
|
+
addedForeignKeys: TableColumn[];
|
|
41
|
+
removedForeignKeys: TableColumn[];
|
|
42
|
+
enumValueChanges: EnumValueChanges[];
|
|
43
|
+
}
|
|
44
|
+
export interface SchemaDiff {
|
|
45
|
+
newTables: TableDefinition[];
|
|
46
|
+
removedTables: TableDefinition[];
|
|
47
|
+
changedTables: TableChanges[];
|
|
48
|
+
}
|
|
49
|
+
export interface RelationChange {
|
|
50
|
+
parentKeyValue: string;
|
|
51
|
+
targetTable: string;
|
|
52
|
+
addedRelations: any[];
|
|
53
|
+
removedRelations: any[];
|
|
54
|
+
}
|
|
55
|
+
export interface DataRowChange {
|
|
56
|
+
tableName: string;
|
|
57
|
+
keyColumn: string | string[];
|
|
58
|
+
insertedRows: any[];
|
|
59
|
+
updatedRows: any[];
|
|
60
|
+
removedRows: any[];
|
|
61
|
+
relationChanges: RelationChange[];
|
|
62
|
+
}
|
|
63
|
+
export interface DataFileChange {
|
|
64
|
+
tableName: string;
|
|
65
|
+
/** New data file — all rows are inserts */
|
|
66
|
+
isNew: boolean;
|
|
67
|
+
/** Data file removed — all rows may be deleted (after prompt) */
|
|
68
|
+
isRemoved: boolean;
|
|
69
|
+
allRows: any[];
|
|
70
|
+
}
|
|
71
|
+
export interface DataDiff {
|
|
72
|
+
changes: DataRowChange[];
|
|
73
|
+
fileChanges: DataFileChange[];
|
|
74
|
+
}
|
|
75
|
+
export declare class DiffService {
|
|
76
|
+
private readonly fileSystem;
|
|
77
|
+
constructor(fileSystem: FileSystemService);
|
|
78
|
+
diffSchema(oldTables: TableDefinition[], newTables: TableDefinition[]): SchemaDiff;
|
|
79
|
+
private diffTableColumns;
|
|
80
|
+
private detectColumnChange;
|
|
81
|
+
/** Normalize a processed TableColumn to its effective SQL type string for comparison */
|
|
82
|
+
normalizeColType(col: TableColumn): string;
|
|
83
|
+
private hasSchemaChanges;
|
|
84
|
+
diffData(oldLibraryPath: string, newLibraryPath: string, fallbackTableDefs?: Map<string, TableDefinition>): Promise<DataDiff>;
|
|
85
|
+
private loadAllRawData;
|
|
86
|
+
private loadRawTableDef;
|
|
87
|
+
/** Returns the identity key column name(s) for a table, or null if none found.
|
|
88
|
+
* Priority: slug > column-level isUnique > table-level uniqueIndices > FK columns (junction fallback)
|
|
89
|
+
*/
|
|
90
|
+
findIdentityKey(table: TableDefinition): string | string[] | null;
|
|
91
|
+
private getRowKeyValue;
|
|
92
|
+
private rowScalarsChanged;
|
|
93
|
+
private diffRelations;
|
|
94
|
+
detectRenameCandidates(droppedColumns: TableColumn[], addedColumns: TableColumn[]): RenameCandidate[];
|
|
95
|
+
private prompt;
|
|
96
|
+
promptRenames(tableName: string, candidates: RenameCandidate[]): Promise<RenameDecision[]>;
|
|
97
|
+
promptDropColumns(tableName: string, columns: TableColumn[]): Promise<TableColumn[]>;
|
|
98
|
+
promptDeleteRows(tableName: string, rows: any[], keyColumn: string | string[]): Promise<any[]>;
|
|
99
|
+
promptDeleteEntireDataFile(tableName: string, rows: any[]): Promise<'delete' | 'ignore'>;
|
|
100
|
+
promptDeleteRelations(parentTable: string, targetTable: string, pairs: {
|
|
101
|
+
parentKeyValue: string;
|
|
102
|
+
relation: any;
|
|
103
|
+
}[]): Promise<{
|
|
104
|
+
parentKeyValue: string;
|
|
105
|
+
relation: any;
|
|
106
|
+
}[]>;
|
|
107
|
+
}
|
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.DiffService = void 0;
|
|
13
|
+
const common_1 = require("@nestjs/common");
|
|
14
|
+
const chalk = require("chalk");
|
|
15
|
+
const path_1 = require("path");
|
|
16
|
+
const file_system_service_1 = require("./file-system.service");
|
|
17
|
+
// ── Service ───────────────────────────────────────────────────────────────────
|
|
18
|
+
let DiffService = class DiffService {
|
|
19
|
+
fileSystem;
|
|
20
|
+
constructor(fileSystem) {
|
|
21
|
+
this.fileSystem = fileSystem;
|
|
22
|
+
}
|
|
23
|
+
// ── Schema diff ─────────────────────────────────────────────────────────────
|
|
24
|
+
diffSchema(oldTables, newTables) {
|
|
25
|
+
const oldTableMap = new Map(oldTables.map((t) => [t.name, t]));
|
|
26
|
+
const newTableMap = new Map(newTables.map((t) => [t.name, t]));
|
|
27
|
+
const newTableDefs = [];
|
|
28
|
+
const removedTables = [];
|
|
29
|
+
const changedTables = [];
|
|
30
|
+
for (const [name, table] of newTableMap) {
|
|
31
|
+
if (!oldTableMap.has(name))
|
|
32
|
+
newTableDefs.push(table);
|
|
33
|
+
}
|
|
34
|
+
for (const [name, table] of oldTableMap) {
|
|
35
|
+
if (!newTableMap.has(name))
|
|
36
|
+
removedTables.push(table);
|
|
37
|
+
}
|
|
38
|
+
for (const [name, newTable] of newTableMap) {
|
|
39
|
+
const oldTable = oldTableMap.get(name);
|
|
40
|
+
if (!oldTable)
|
|
41
|
+
continue;
|
|
42
|
+
const changes = this.diffTableColumns(oldTable, newTable);
|
|
43
|
+
if (this.hasSchemaChanges(changes))
|
|
44
|
+
changedTables.push(changes);
|
|
45
|
+
}
|
|
46
|
+
return { newTables: newTableDefs, removedTables, changedTables };
|
|
47
|
+
}
|
|
48
|
+
diffTableColumns(oldTable, newTable) {
|
|
49
|
+
const oldColMap = new Map(oldTable.columns.map((c) => [c.name, c]));
|
|
50
|
+
const newColMap = new Map(newTable.columns.map((c) => [c.name, c]));
|
|
51
|
+
const addedColumns = [];
|
|
52
|
+
const removedColumns = [];
|
|
53
|
+
const changedColumns = [];
|
|
54
|
+
for (const [name, col] of newColMap) {
|
|
55
|
+
if (!oldColMap.has(name))
|
|
56
|
+
addedColumns.push(col);
|
|
57
|
+
}
|
|
58
|
+
for (const [name, col] of oldColMap) {
|
|
59
|
+
if (!newColMap.has(name))
|
|
60
|
+
removedColumns.push(col);
|
|
61
|
+
}
|
|
62
|
+
for (const [name, newCol] of newColMap) {
|
|
63
|
+
const oldCol = oldColMap.get(name);
|
|
64
|
+
if (!oldCol)
|
|
65
|
+
continue;
|
|
66
|
+
const change = this.detectColumnChange(oldCol, newCol);
|
|
67
|
+
if (change)
|
|
68
|
+
changedColumns.push(change);
|
|
69
|
+
}
|
|
70
|
+
const oldUniqueSet = new Set(oldTable.columns.filter((c) => c.isUnique).map((c) => c.name));
|
|
71
|
+
const newUniqueSet = new Set(newTable.columns.filter((c) => c.isUnique).map((c) => c.name));
|
|
72
|
+
const addedUniqueConstraints = [...newUniqueSet].filter((n) => !oldUniqueSet.has(n));
|
|
73
|
+
const removedUniqueConstraints = [...oldUniqueSet].filter((n) => !newUniqueSet.has(n));
|
|
74
|
+
const oldFkMap = new Map(oldTable.columns.filter((c) => c.type === 'fk').map((c) => [c.name, c]));
|
|
75
|
+
const newFkMap = new Map(newTable.columns.filter((c) => c.type === 'fk').map((c) => [c.name, c]));
|
|
76
|
+
const addedForeignKeys = [...newFkMap.values()].filter((c) => !oldFkMap.has(c.name));
|
|
77
|
+
const removedForeignKeys = [...oldFkMap.values()].filter((c) => !newFkMap.has(c.name));
|
|
78
|
+
const enumValueChanges = [];
|
|
79
|
+
for (const [name, newCol] of newColMap) {
|
|
80
|
+
const oldCol = oldColMap.get(name);
|
|
81
|
+
if (!oldCol || newCol.type !== 'enum')
|
|
82
|
+
continue;
|
|
83
|
+
const oldEnum = [...(oldCol.enum || oldCol.values || [])];
|
|
84
|
+
const newEnum = [...(newCol.enum || newCol.values || [])];
|
|
85
|
+
const addedValues = newEnum.filter((v) => !oldEnum.includes(v));
|
|
86
|
+
const removedValues = oldEnum.filter((v) => !newEnum.includes(v));
|
|
87
|
+
if (addedValues.length || removedValues.length) {
|
|
88
|
+
enumValueChanges.push({
|
|
89
|
+
columnName: name,
|
|
90
|
+
addedValues,
|
|
91
|
+
removedValues,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
tableName: newTable.name,
|
|
97
|
+
addedColumns,
|
|
98
|
+
removedColumns,
|
|
99
|
+
renamedColumns: [],
|
|
100
|
+
changedColumns,
|
|
101
|
+
addedUniqueConstraints,
|
|
102
|
+
removedUniqueConstraints,
|
|
103
|
+
addedForeignKeys,
|
|
104
|
+
removedForeignKeys,
|
|
105
|
+
enumValueChanges,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
detectColumnChange(oldCol, newCol) {
|
|
109
|
+
const oldType = this.normalizeColType(oldCol);
|
|
110
|
+
const newType = this.normalizeColType(newCol);
|
|
111
|
+
const oldNullable = oldCol.isNullable ?? false;
|
|
112
|
+
const newNullable = newCol.isNullable ?? false;
|
|
113
|
+
const oldDefault = String(oldCol.default ?? '');
|
|
114
|
+
const newDefault = String(newCol.default ?? '');
|
|
115
|
+
const oldLength = oldCol.length;
|
|
116
|
+
const newLength = newCol.length;
|
|
117
|
+
if (oldType === newType &&
|
|
118
|
+
oldNullable === newNullable &&
|
|
119
|
+
oldDefault === newDefault &&
|
|
120
|
+
oldLength === newLength) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
column: newCol,
|
|
125
|
+
oldType,
|
|
126
|
+
newType,
|
|
127
|
+
oldNullable,
|
|
128
|
+
newNullable,
|
|
129
|
+
oldDefault: oldCol.default,
|
|
130
|
+
newDefault: newCol.default,
|
|
131
|
+
oldLength,
|
|
132
|
+
newLength,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** Normalize a processed TableColumn to its effective SQL type string for comparison */
|
|
136
|
+
normalizeColType(col) {
|
|
137
|
+
switch (col.type.toLowerCase()) {
|
|
138
|
+
case 'pk':
|
|
139
|
+
return 'SERIAL';
|
|
140
|
+
case 'fk':
|
|
141
|
+
return 'INTEGER';
|
|
142
|
+
case 'slug':
|
|
143
|
+
case 'varchar':
|
|
144
|
+
return `VARCHAR(${col.length || 255})`;
|
|
145
|
+
case 'order':
|
|
146
|
+
return 'INTEGER';
|
|
147
|
+
case 'created_at':
|
|
148
|
+
case 'updated_at':
|
|
149
|
+
return 'TIMESTAMPTZ';
|
|
150
|
+
case 'enum':
|
|
151
|
+
return 'enum';
|
|
152
|
+
case 'locale_varchar':
|
|
153
|
+
return `VARCHAR(${col.length || 255})`;
|
|
154
|
+
case 'locale_text':
|
|
155
|
+
return 'TEXT';
|
|
156
|
+
case 'char':
|
|
157
|
+
return `CHAR(${col.length || 1})`;
|
|
158
|
+
case 'int':
|
|
159
|
+
return 'INTEGER';
|
|
160
|
+
default:
|
|
161
|
+
return col.type.toUpperCase();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
hasSchemaChanges(changes) {
|
|
165
|
+
return (changes.addedColumns.length > 0 ||
|
|
166
|
+
changes.removedColumns.length > 0 ||
|
|
167
|
+
changes.changedColumns.length > 0 ||
|
|
168
|
+
changes.addedUniqueConstraints.length > 0 ||
|
|
169
|
+
changes.removedUniqueConstraints.length > 0 ||
|
|
170
|
+
changes.addedForeignKeys.length > 0 ||
|
|
171
|
+
changes.removedForeignKeys.length > 0 ||
|
|
172
|
+
changes.enumValueChanges.length > 0);
|
|
173
|
+
}
|
|
174
|
+
// ── Data diff ───────────────────────────────────────────────────────────────
|
|
175
|
+
async diffData(oldLibraryPath, newLibraryPath, fallbackTableDefs) {
|
|
176
|
+
const changes = [];
|
|
177
|
+
const fileChanges = [];
|
|
178
|
+
const oldDataMap = await this.loadAllRawData(oldLibraryPath);
|
|
179
|
+
const newDataMap = await this.loadAllRawData(newLibraryPath);
|
|
180
|
+
const allTableNames = new Set([...oldDataMap.keys(), ...newDataMap.keys()]);
|
|
181
|
+
for (const tableName of allTableNames) {
|
|
182
|
+
const oldRows = oldDataMap.get(tableName) ?? [];
|
|
183
|
+
const newRows = newDataMap.get(tableName) ?? [];
|
|
184
|
+
if (oldRows.length === 0 && newRows.length > 0) {
|
|
185
|
+
fileChanges.push({
|
|
186
|
+
tableName,
|
|
187
|
+
isNew: true,
|
|
188
|
+
isRemoved: false,
|
|
189
|
+
allRows: newRows,
|
|
190
|
+
});
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (oldRows.length > 0 && newRows.length === 0) {
|
|
194
|
+
fileChanges.push({
|
|
195
|
+
tableName,
|
|
196
|
+
isNew: false,
|
|
197
|
+
isRemoved: true,
|
|
198
|
+
allRows: oldRows,
|
|
199
|
+
});
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
// Find identity key from the new YAML table definition, then old, then fallback
|
|
203
|
+
const tableDef = (await this.loadRawTableDef((0, path_1.join)(newLibraryPath, 'hedhog', 'table'), tableName)) ??
|
|
204
|
+
(await this.loadRawTableDef((0, path_1.join)(oldLibraryPath, 'hedhog', 'table'), tableName)) ??
|
|
205
|
+
fallbackTableDefs?.get(tableName) ??
|
|
206
|
+
null;
|
|
207
|
+
if (!tableDef) {
|
|
208
|
+
console.warn(chalk.yellow(`[update] Cannot diff data for table "${tableName}": table definition not found. Treating all rows as inserts.`));
|
|
209
|
+
fileChanges.push({
|
|
210
|
+
tableName,
|
|
211
|
+
isNew: true,
|
|
212
|
+
isRemoved: false,
|
|
213
|
+
allRows: newRows,
|
|
214
|
+
});
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const keyColumn = this.findIdentityKey(tableDef);
|
|
218
|
+
if (!keyColumn) {
|
|
219
|
+
console.warn(chalk.yellow(`[update] Cannot diff data for table "${tableName}": no identity key (slug or unique column) found. Treating all rows as inserts.`));
|
|
220
|
+
fileChanges.push({
|
|
221
|
+
tableName,
|
|
222
|
+
isNew: true,
|
|
223
|
+
isRemoved: false,
|
|
224
|
+
allRows: newRows,
|
|
225
|
+
});
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const oldRowIndex = new Map();
|
|
229
|
+
for (const row of oldRows) {
|
|
230
|
+
const keyVal = this.getRowKeyValue(row, keyColumn);
|
|
231
|
+
if (keyVal !== null)
|
|
232
|
+
oldRowIndex.set(keyVal, row);
|
|
233
|
+
}
|
|
234
|
+
const insertedRows = [];
|
|
235
|
+
const updatedRows = [];
|
|
236
|
+
const removedRows = [];
|
|
237
|
+
const relationChanges = [];
|
|
238
|
+
for (const newRow of newRows) {
|
|
239
|
+
const keyVal = this.getRowKeyValue(newRow, keyColumn);
|
|
240
|
+
if (keyVal === null) {
|
|
241
|
+
insertedRows.push(newRow);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const oldRow = oldRowIndex.get(keyVal);
|
|
245
|
+
if (!oldRow) {
|
|
246
|
+
insertedRows.push(newRow);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
if (this.rowScalarsChanged(oldRow, newRow))
|
|
250
|
+
updatedRows.push(newRow);
|
|
251
|
+
relationChanges.push(...this.diffRelations(oldRow, newRow, keyVal));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const newKeySet = new Set(newRows
|
|
255
|
+
.map((r) => this.getRowKeyValue(r, keyColumn))
|
|
256
|
+
.filter((v) => v !== null));
|
|
257
|
+
for (const oldRow of oldRows) {
|
|
258
|
+
const keyVal = this.getRowKeyValue(oldRow, keyColumn);
|
|
259
|
+
if (keyVal !== null && !newKeySet.has(keyVal))
|
|
260
|
+
removedRows.push(oldRow);
|
|
261
|
+
}
|
|
262
|
+
if (insertedRows.length ||
|
|
263
|
+
updatedRows.length ||
|
|
264
|
+
removedRows.length ||
|
|
265
|
+
relationChanges.length) {
|
|
266
|
+
changes.push({
|
|
267
|
+
tableName,
|
|
268
|
+
keyColumn,
|
|
269
|
+
insertedRows,
|
|
270
|
+
updatedRows,
|
|
271
|
+
removedRows,
|
|
272
|
+
relationChanges,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return { changes, fileChanges };
|
|
277
|
+
}
|
|
278
|
+
async loadAllRawData(libraryPath) {
|
|
279
|
+
const result = new Map();
|
|
280
|
+
const dataDir = (0, path_1.join)(libraryPath, 'hedhog', 'data');
|
|
281
|
+
try {
|
|
282
|
+
const files = await this.fileSystem.getYamlFiles(dataDir);
|
|
283
|
+
for (const file of files) {
|
|
284
|
+
const name = this.fileSystem.getFileNameWithoutExtension(file);
|
|
285
|
+
const rows = await this.fileSystem.readYamlFile((0, path_1.join)(dataDir, file));
|
|
286
|
+
if (Array.isArray(rows))
|
|
287
|
+
result.set(name, rows);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// directory may not exist for this library version
|
|
292
|
+
}
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
async loadRawTableDef(tablesPath, tableName) {
|
|
296
|
+
try {
|
|
297
|
+
const yamlPath = (0, path_1.join)(tablesPath, `${tableName}.yaml`);
|
|
298
|
+
const ymlPath = (0, path_1.join)(tablesPath, `${tableName}.yml`);
|
|
299
|
+
let filePath = null;
|
|
300
|
+
if (await this.fileSystem.exists(yamlPath))
|
|
301
|
+
filePath = yamlPath;
|
|
302
|
+
else if (await this.fileSystem.exists(ymlPath))
|
|
303
|
+
filePath = ymlPath;
|
|
304
|
+
if (!filePath)
|
|
305
|
+
return null;
|
|
306
|
+
const parsed = await this.fileSystem.readYamlFile(filePath);
|
|
307
|
+
if (!parsed?.columns)
|
|
308
|
+
return null;
|
|
309
|
+
// Minimal processing: only what we need for identity key detection
|
|
310
|
+
const mainCols = parsed.columns.filter((c) => !['locale_varchar', 'locale_text'].includes(c.type));
|
|
311
|
+
// Apply basic type normalizations so slug/unique detection works
|
|
312
|
+
for (const col of mainCols) {
|
|
313
|
+
if (col.type === 'pk') {
|
|
314
|
+
col.name = 'id';
|
|
315
|
+
col.isPrimaryKey = true;
|
|
316
|
+
}
|
|
317
|
+
else if (col.type === 'slug') {
|
|
318
|
+
col.name = col.name ?? 'slug';
|
|
319
|
+
col.isUnique = true;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Parse table-level unique indices from indices: section
|
|
323
|
+
const uniqueIndices = [];
|
|
324
|
+
if (Array.isArray(parsed.indices)) {
|
|
325
|
+
for (const idx of parsed.indices) {
|
|
326
|
+
if (idx.isUnique &&
|
|
327
|
+
Array.isArray(idx.columns) &&
|
|
328
|
+
idx.columns.length > 0) {
|
|
329
|
+
uniqueIndices.push(idx.columns);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
name: tableName,
|
|
335
|
+
columns: mainCols,
|
|
336
|
+
data: [],
|
|
337
|
+
pk: mainCols.find((c) => c.isPrimaryKey || c.type === 'pk')?.name ||
|
|
338
|
+
'id',
|
|
339
|
+
uniqueIndices: uniqueIndices.length > 0 ? uniqueIndices : undefined,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/** Returns the identity key column name(s) for a table, or null if none found.
|
|
347
|
+
* Priority: slug > column-level isUnique > table-level uniqueIndices > FK columns (junction fallback)
|
|
348
|
+
*/
|
|
349
|
+
findIdentityKey(table) {
|
|
350
|
+
if (table.columns.some((c) => c.name === 'slug'))
|
|
351
|
+
return 'slug';
|
|
352
|
+
const uniqueCols = table.columns.filter((c) => c.isUnique);
|
|
353
|
+
if (uniqueCols.length === 1)
|
|
354
|
+
return uniqueCols[0].name;
|
|
355
|
+
if (uniqueCols.length > 1)
|
|
356
|
+
return uniqueCols.map((c) => c.name);
|
|
357
|
+
// Table-level composite unique index (e.g. from YAML indices: section)
|
|
358
|
+
if (table.uniqueIndices?.length)
|
|
359
|
+
return table.uniqueIndices[0];
|
|
360
|
+
// Junction-table fallback: use all FK columns as composite key
|
|
361
|
+
const fkCols = table.columns.filter((c) => c.type === 'fk');
|
|
362
|
+
if (fkCols.length > 0)
|
|
363
|
+
return fkCols.map((c) => c.name);
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
getRowKeyValue(row, keyColumn) {
|
|
367
|
+
if (Array.isArray(keyColumn)) {
|
|
368
|
+
const parts = keyColumn.map((k) => {
|
|
369
|
+
const val = row[k];
|
|
370
|
+
if (val === undefined || val === null)
|
|
371
|
+
return null;
|
|
372
|
+
if (typeof val === 'object' && 'where' in val)
|
|
373
|
+
return JSON.stringify(val.where);
|
|
374
|
+
return String(val);
|
|
375
|
+
});
|
|
376
|
+
if (parts.some((p) => p === null))
|
|
377
|
+
return null;
|
|
378
|
+
return parts.join('::');
|
|
379
|
+
}
|
|
380
|
+
const val = row[keyColumn];
|
|
381
|
+
if (val === undefined || val === null)
|
|
382
|
+
return null;
|
|
383
|
+
if (typeof val === 'object' && 'where' in val) {
|
|
384
|
+
return JSON.stringify(val.where);
|
|
385
|
+
}
|
|
386
|
+
return String(val);
|
|
387
|
+
}
|
|
388
|
+
rowScalarsChanged(oldRow, newRow) {
|
|
389
|
+
const extractScalars = (row) => {
|
|
390
|
+
const result = {};
|
|
391
|
+
for (const [key, value] of Object.entries(row)) {
|
|
392
|
+
if (key !== 'relations')
|
|
393
|
+
result[key] = value;
|
|
394
|
+
}
|
|
395
|
+
return result;
|
|
396
|
+
};
|
|
397
|
+
return (JSON.stringify(extractScalars(oldRow)) !==
|
|
398
|
+
JSON.stringify(extractScalars(newRow)));
|
|
399
|
+
}
|
|
400
|
+
diffRelations(oldRow, newRow, parentKeyValue) {
|
|
401
|
+
const changes = [];
|
|
402
|
+
const oldRels = oldRow.relations || {};
|
|
403
|
+
const newRels = newRow.relations || {};
|
|
404
|
+
const allTargets = new Set([
|
|
405
|
+
...Object.keys(oldRels),
|
|
406
|
+
...Object.keys(newRels),
|
|
407
|
+
]);
|
|
408
|
+
for (const targetTable of allTargets) {
|
|
409
|
+
const oldRelList = (oldRels[targetTable] ?? []);
|
|
410
|
+
const newRelList = (newRels[targetTable] ?? []);
|
|
411
|
+
const oldRelSet = new Set(oldRelList.map((r) => JSON.stringify(r)));
|
|
412
|
+
const newRelSet = new Set(newRelList.map((r) => JSON.stringify(r)));
|
|
413
|
+
const addedRelations = newRelList.filter((r) => !oldRelSet.has(JSON.stringify(r)));
|
|
414
|
+
const removedRelations = oldRelList.filter((r) => !newRelSet.has(JSON.stringify(r)));
|
|
415
|
+
if (addedRelations.length || removedRelations.length) {
|
|
416
|
+
changes.push({
|
|
417
|
+
parentKeyValue,
|
|
418
|
+
targetTable,
|
|
419
|
+
addedRelations,
|
|
420
|
+
removedRelations,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return changes;
|
|
425
|
+
}
|
|
426
|
+
// ── Rename detection ────────────────────────────────────────────────────────
|
|
427
|
+
detectRenameCandidates(droppedColumns, addedColumns) {
|
|
428
|
+
const candidates = [];
|
|
429
|
+
for (const dropped of droppedColumns) {
|
|
430
|
+
const normalizedDropped = this.normalizeColType(dropped);
|
|
431
|
+
for (const added of addedColumns) {
|
|
432
|
+
if (normalizedDropped === this.normalizeColType(added)) {
|
|
433
|
+
candidates.push({ oldColumn: dropped, newColumn: added });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return candidates;
|
|
438
|
+
}
|
|
439
|
+
// ── Interactive prompts (inquirer v12 via dynamic import) ───────────────────
|
|
440
|
+
async prompt(questions) {
|
|
441
|
+
const { default: inquirer } = await Promise.resolve().then(() => require('inquirer'));
|
|
442
|
+
return inquirer.prompt(questions);
|
|
443
|
+
}
|
|
444
|
+
async promptRenames(tableName, candidates) {
|
|
445
|
+
const decisions = [];
|
|
446
|
+
for (const candidate of candidates) {
|
|
447
|
+
const type = this.normalizeColType(candidate.oldColumn);
|
|
448
|
+
const answers = await this.prompt([
|
|
449
|
+
{
|
|
450
|
+
type: 'confirm',
|
|
451
|
+
name: 'isRename',
|
|
452
|
+
message: chalk.yellow(`[Table "${tableName}"] Column "${candidate.oldColumn.name}" was removed and "${candidate.newColumn.name}" was added (same type: ${type}). Is this a RENAME?`) + chalk.gray(' (No = DROP + ADD)'),
|
|
453
|
+
default: false,
|
|
454
|
+
},
|
|
455
|
+
]);
|
|
456
|
+
decisions.push({
|
|
457
|
+
oldName: candidate.oldColumn.name,
|
|
458
|
+
newName: candidate.newColumn.name,
|
|
459
|
+
oldColumn: candidate.oldColumn,
|
|
460
|
+
newColumn: candidate.newColumn,
|
|
461
|
+
isRename: answers.isRename,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
return decisions;
|
|
465
|
+
}
|
|
466
|
+
async promptDropColumns(tableName, columns) {
|
|
467
|
+
const approved = [];
|
|
468
|
+
for (const col of columns) {
|
|
469
|
+
console.warn(chalk.red(`\n⚠️ WARNING: Dropping column "${col.name}" from table "${tableName}" will cause permanent DATA LOSS.\n`));
|
|
470
|
+
const answers = await this.prompt([
|
|
471
|
+
{
|
|
472
|
+
type: 'list',
|
|
473
|
+
name: 'action',
|
|
474
|
+
message: `Column "${col.name}" (${this.normalizeColType(col)}) was removed from the YAML. What should be done?`,
|
|
475
|
+
choices: [
|
|
476
|
+
{
|
|
477
|
+
name: `DROP COLUMN "${col.name}" (recommended — removes column and all its data)`,
|
|
478
|
+
value: 'drop',
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
name: `IGNORE (keep the column in the database as-is)`,
|
|
482
|
+
value: 'ignore',
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
default: 'drop',
|
|
486
|
+
},
|
|
487
|
+
]);
|
|
488
|
+
if (answers.action === 'drop')
|
|
489
|
+
approved.push(col);
|
|
490
|
+
}
|
|
491
|
+
return approved;
|
|
492
|
+
}
|
|
493
|
+
async promptDeleteRows(tableName, rows, keyColumn) {
|
|
494
|
+
const approved = [];
|
|
495
|
+
for (const row of rows) {
|
|
496
|
+
const keyVal = Array.isArray(keyColumn)
|
|
497
|
+
? keyColumn.map((k) => row[k]).join('::')
|
|
498
|
+
: row[keyColumn];
|
|
499
|
+
const answers = await this.prompt([
|
|
500
|
+
{
|
|
501
|
+
type: 'list',
|
|
502
|
+
name: 'action',
|
|
503
|
+
message: `Row "${keyVal}" was removed from the data file for table "${tableName}". What should be done?`,
|
|
504
|
+
choices: [
|
|
505
|
+
{
|
|
506
|
+
name: `IGNORE (keep the row in the database)`,
|
|
507
|
+
value: 'ignore',
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
name: `DELETE "${keyVal}" from "${tableName}"`,
|
|
511
|
+
value: 'delete',
|
|
512
|
+
},
|
|
513
|
+
],
|
|
514
|
+
default: 'ignore',
|
|
515
|
+
},
|
|
516
|
+
]);
|
|
517
|
+
if (answers.action === 'delete')
|
|
518
|
+
approved.push(row);
|
|
519
|
+
}
|
|
520
|
+
return approved;
|
|
521
|
+
}
|
|
522
|
+
async promptDeleteEntireDataFile(tableName, rows) {
|
|
523
|
+
console.warn(chalk.red(`\n⚠️ The entire data file for table "${tableName}" was removed in the new version (${rows.length} rows).\n`));
|
|
524
|
+
const answers = await this.prompt([
|
|
525
|
+
{
|
|
526
|
+
type: 'list',
|
|
527
|
+
name: 'action',
|
|
528
|
+
message: `Generate DELETE for all ${rows.length} rows in "${tableName}", or ignore?`,
|
|
529
|
+
choices: [
|
|
530
|
+
{
|
|
531
|
+
name: `IGNORE (keep all existing rows in the database)`,
|
|
532
|
+
value: 'ignore',
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
name: `DELETE all ${rows.length} rows from "${tableName}"`,
|
|
536
|
+
value: 'delete',
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
default: 'ignore',
|
|
540
|
+
},
|
|
541
|
+
]);
|
|
542
|
+
return answers.action;
|
|
543
|
+
}
|
|
544
|
+
async promptDeleteRelations(parentTable, targetTable, pairs) {
|
|
545
|
+
const approved = [];
|
|
546
|
+
for (const pair of pairs) {
|
|
547
|
+
const answers = await this.prompt([
|
|
548
|
+
{
|
|
549
|
+
type: 'list',
|
|
550
|
+
name: 'action',
|
|
551
|
+
message: `Relation between "${parentTable}:${pair.parentKeyValue}" and "${targetTable}:${JSON.stringify(pair.relation)}" was removed. DELETE or IGNORE?`,
|
|
552
|
+
choices: [
|
|
553
|
+
{
|
|
554
|
+
name: `IGNORE (keep the relation in the database)`,
|
|
555
|
+
value: 'ignore',
|
|
556
|
+
},
|
|
557
|
+
{ name: `DELETE the relation`, value: 'delete' },
|
|
558
|
+
],
|
|
559
|
+
default: 'ignore',
|
|
560
|
+
},
|
|
561
|
+
]);
|
|
562
|
+
if (answers.action === 'delete')
|
|
563
|
+
approved.push(pair);
|
|
564
|
+
}
|
|
565
|
+
return approved;
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
exports.DiffService = DiffService;
|
|
569
|
+
exports.DiffService = DiffService = __decorate([
|
|
570
|
+
(0, common_1.Injectable)(),
|
|
571
|
+
__metadata("design:paramtypes", [file_system_service_1.FileSystemService])
|
|
572
|
+
], DiffService);
|
|
573
|
+
//# sourceMappingURL=diff.service.js.map
|