@xubylele/schema-forge 1.2.0 → 1.5.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/README.md +128 -13
- package/dist/cli.js +255 -911
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -24,12 +24,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
));
|
|
25
25
|
|
|
26
26
|
// src/cli.ts
|
|
27
|
-
var
|
|
27
|
+
var import_commander6 = require("commander");
|
|
28
28
|
|
|
29
29
|
// package.json
|
|
30
30
|
var package_default = {
|
|
31
31
|
name: "@xubylele/schema-forge",
|
|
32
|
-
version: "1.
|
|
32
|
+
version: "1.5.0",
|
|
33
33
|
description: "Universal migration generator from schema DSL",
|
|
34
34
|
main: "dist/cli.js",
|
|
35
35
|
type: "commonjs",
|
|
@@ -69,6 +69,7 @@ var package_default = {
|
|
|
69
69
|
node: ">=18.0.0"
|
|
70
70
|
},
|
|
71
71
|
dependencies: {
|
|
72
|
+
"@xubylele/schema-forge-core": "^1.0.4",
|
|
72
73
|
boxen: "^8.0.1",
|
|
73
74
|
chalk: "^5.6.2",
|
|
74
75
|
commander: "^14.0.3"
|
|
@@ -85,120 +86,7 @@ var package_default = {
|
|
|
85
86
|
|
|
86
87
|
// src/commands/diff.ts
|
|
87
88
|
var import_commander = require("commander");
|
|
88
|
-
var
|
|
89
|
-
|
|
90
|
-
// src/core/diff.ts
|
|
91
|
-
function getTableNamesFromState(state) {
|
|
92
|
-
return new Set(Object.keys(state.tables));
|
|
93
|
-
}
|
|
94
|
-
function getTableNamesFromSchema(schema) {
|
|
95
|
-
return new Set(Object.keys(schema.tables));
|
|
96
|
-
}
|
|
97
|
-
function getColumnNamesFromState(stateColumns) {
|
|
98
|
-
return new Set(Object.keys(stateColumns));
|
|
99
|
-
}
|
|
100
|
-
function getColumnNamesFromSchema(dbColumns) {
|
|
101
|
-
return new Set(dbColumns.map((column) => column.name));
|
|
102
|
-
}
|
|
103
|
-
function getSortedNames(names) {
|
|
104
|
-
return Array.from(names).sort((a, b) => a.localeCompare(b));
|
|
105
|
-
}
|
|
106
|
-
function normalizeColumnType(type) {
|
|
107
|
-
return type.toLowerCase().trim().replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*,\s*/g, ",").replace(/\s*\)\s*/g, ")");
|
|
108
|
-
}
|
|
109
|
-
function diffSchemas(oldState, newSchema) {
|
|
110
|
-
const operations = [];
|
|
111
|
-
const oldTableNames = getTableNamesFromState(oldState);
|
|
112
|
-
const newTableNames = getTableNamesFromSchema(newSchema);
|
|
113
|
-
const sortedNewTableNames = getSortedNames(newTableNames);
|
|
114
|
-
const sortedOldTableNames = getSortedNames(oldTableNames);
|
|
115
|
-
for (const tableName of sortedNewTableNames) {
|
|
116
|
-
if (!oldTableNames.has(tableName)) {
|
|
117
|
-
operations.push({
|
|
118
|
-
kind: "create_table",
|
|
119
|
-
table: newSchema.tables[tableName]
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
const commonTableNames = sortedNewTableNames.filter(
|
|
124
|
-
(tableName) => oldTableNames.has(tableName)
|
|
125
|
-
);
|
|
126
|
-
for (const tableName of commonTableNames) {
|
|
127
|
-
const newTable = newSchema.tables[tableName];
|
|
128
|
-
const oldTable = oldState.tables[tableName];
|
|
129
|
-
if (!newTable || !oldTable) {
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
for (const column of newTable.columns) {
|
|
133
|
-
const previousColumn = oldTable.columns[column.name];
|
|
134
|
-
if (!previousColumn) {
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
const previousType = normalizeColumnType(previousColumn.type);
|
|
138
|
-
const currentType = normalizeColumnType(column.type);
|
|
139
|
-
if (previousType !== currentType) {
|
|
140
|
-
operations.push({
|
|
141
|
-
kind: "column_type_changed",
|
|
142
|
-
tableName,
|
|
143
|
-
columnName: column.name,
|
|
144
|
-
fromType: previousColumn.type,
|
|
145
|
-
toType: column.type
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
for (const tableName of commonTableNames) {
|
|
151
|
-
const newTable = newSchema.tables[tableName];
|
|
152
|
-
const oldTable = oldState.tables[tableName];
|
|
153
|
-
if (!newTable || !oldTable) {
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
const oldColumnNames = getColumnNamesFromState(oldTable.columns);
|
|
157
|
-
for (const column of newTable.columns) {
|
|
158
|
-
if (!oldColumnNames.has(column.name)) {
|
|
159
|
-
operations.push({
|
|
160
|
-
kind: "add_column",
|
|
161
|
-
tableName,
|
|
162
|
-
column
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
for (const tableName of commonTableNames) {
|
|
168
|
-
const newTable = newSchema.tables[tableName];
|
|
169
|
-
const oldTable = oldState.tables[tableName];
|
|
170
|
-
if (!newTable || !oldTable) {
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
const newColumnNames = getColumnNamesFromSchema(newTable.columns);
|
|
174
|
-
for (const columnName of Object.keys(oldTable.columns)) {
|
|
175
|
-
if (!newColumnNames.has(columnName)) {
|
|
176
|
-
operations.push({
|
|
177
|
-
kind: "drop_column",
|
|
178
|
-
tableName,
|
|
179
|
-
columnName
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
for (const tableName of sortedOldTableNames) {
|
|
185
|
-
if (!newTableNames.has(tableName)) {
|
|
186
|
-
operations.push({
|
|
187
|
-
kind: "drop_table",
|
|
188
|
-
tableName
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return { operations };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// src/core/errors.ts
|
|
196
|
-
var SchemaValidationError = class extends Error {
|
|
197
|
-
constructor(message) {
|
|
198
|
-
super(message);
|
|
199
|
-
this.name = "SchemaValidationError";
|
|
200
|
-
}
|
|
201
|
-
};
|
|
89
|
+
var import_path3 = __toESM(require("path"));
|
|
202
90
|
|
|
203
91
|
// src/core/fs.ts
|
|
204
92
|
var import_fs = require("fs");
|
|
@@ -254,309 +142,6 @@ async function writeJsonFile(filePath, data) {
|
|
|
254
142
|
throw new Error(`Failed to write JSON file ${filePath}: ${error2}`);
|
|
255
143
|
}
|
|
256
144
|
}
|
|
257
|
-
async function findFiles(dirPath, pattern) {
|
|
258
|
-
const results = [];
|
|
259
|
-
try {
|
|
260
|
-
const items = await import_fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
261
|
-
for (const item of items) {
|
|
262
|
-
const fullPath = import_path.default.join(dirPath, item.name);
|
|
263
|
-
if (item.isDirectory()) {
|
|
264
|
-
const subResults = await findFiles(fullPath, pattern);
|
|
265
|
-
results.push(...subResults);
|
|
266
|
-
} else if (item.isFile() && pattern.test(item.name)) {
|
|
267
|
-
results.push(fullPath);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
} catch (error2) {
|
|
271
|
-
throw new Error(`Failed to find files in ${dirPath}: ${error2}`);
|
|
272
|
-
}
|
|
273
|
-
return results;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// src/utils/output.ts
|
|
277
|
-
var import_boxen = __toESM(require("boxen"));
|
|
278
|
-
var import_chalk = require("chalk");
|
|
279
|
-
var isInteractive = Boolean(process.stdout?.isTTY);
|
|
280
|
-
var colorsEnabled = isInteractive && process.env.FORCE_COLOR !== "0" && !("NO_COLOR" in process.env);
|
|
281
|
-
var color = new import_chalk.Chalk({ level: colorsEnabled ? 3 : 0 });
|
|
282
|
-
var theme = {
|
|
283
|
-
primary: color.cyanBright,
|
|
284
|
-
success: color.hex("#00FF88"),
|
|
285
|
-
warning: color.hex("#FFD166"),
|
|
286
|
-
error: color.hex("#EF476F"),
|
|
287
|
-
accent: color.magentaBright
|
|
288
|
-
};
|
|
289
|
-
function success(message) {
|
|
290
|
-
const text = theme.success(`[OK] ${message}`);
|
|
291
|
-
if (!isInteractive) {
|
|
292
|
-
console.log(text);
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
try {
|
|
296
|
-
console.log(
|
|
297
|
-
(0, import_boxen.default)(text, {
|
|
298
|
-
padding: 1,
|
|
299
|
-
borderColor: "cyan",
|
|
300
|
-
borderStyle: "round"
|
|
301
|
-
})
|
|
302
|
-
);
|
|
303
|
-
} catch {
|
|
304
|
-
console.log(text);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
function info(message) {
|
|
308
|
-
console.log(theme.primary(message));
|
|
309
|
-
}
|
|
310
|
-
function warning(message) {
|
|
311
|
-
console.warn(theme.warning(`[WARN] ${message}`));
|
|
312
|
-
}
|
|
313
|
-
function error(message) {
|
|
314
|
-
console.error(theme.error(`[ERROR] ${message}`));
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// src/core/parser.ts
|
|
318
|
-
var SchemaParser = class {
|
|
319
|
-
/**
|
|
320
|
-
* Parse a schema from a JSON file
|
|
321
|
-
*/
|
|
322
|
-
async parseSchemaFile(filePath) {
|
|
323
|
-
try {
|
|
324
|
-
const schema = await readJsonFile(filePath, {});
|
|
325
|
-
return this.normalizeSchema(schema);
|
|
326
|
-
} catch (error2) {
|
|
327
|
-
throw new Error(`Failed to parse schema file ${filePath}: ${error2}`);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
/**
|
|
331
|
-
* Parse multiple schema files from a directory
|
|
332
|
-
*/
|
|
333
|
-
async parseSchemaDirectory(dirPath) {
|
|
334
|
-
const schemaFiles = await findFiles(dirPath, /\.schema\.json$/);
|
|
335
|
-
const schemas = [];
|
|
336
|
-
for (const file of schemaFiles) {
|
|
337
|
-
try {
|
|
338
|
-
const schema = await this.parseSchemaFile(file);
|
|
339
|
-
schemas.push(schema);
|
|
340
|
-
} catch (error2) {
|
|
341
|
-
const reason = error2 instanceof Error ? error2.message : String(error2);
|
|
342
|
-
warning(`Could not parse ${file}: ${reason}`);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
return schemas;
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* Merge multiple schemas into one
|
|
349
|
-
*/
|
|
350
|
-
mergeSchemas(schemas) {
|
|
351
|
-
if (schemas.length === 0) {
|
|
352
|
-
throw new Error("Cannot merge empty schema array");
|
|
353
|
-
}
|
|
354
|
-
const baseSchema = schemas[0];
|
|
355
|
-
const mergedTables = [];
|
|
356
|
-
for (const schema of schemas) {
|
|
357
|
-
for (const table of schema.tables) {
|
|
358
|
-
const existingIndex = mergedTables.findIndex((t) => t.name === table.name);
|
|
359
|
-
if (existingIndex >= 0) {
|
|
360
|
-
warning(`Duplicate table '${table.name}' found, using first occurrence`);
|
|
361
|
-
} else {
|
|
362
|
-
mergedTables.push(table);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
return {
|
|
367
|
-
version: baseSchema.version,
|
|
368
|
-
database: baseSchema.database,
|
|
369
|
-
tables: mergedTables
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
/**
|
|
373
|
-
* Normalize schema to ensure consistent structure
|
|
374
|
-
*/
|
|
375
|
-
normalizeSchema(schema) {
|
|
376
|
-
return {
|
|
377
|
-
version: schema.version || "1.0.0",
|
|
378
|
-
database: schema.database || "postgres",
|
|
379
|
-
tables: schema.tables.map((table) => ({
|
|
380
|
-
...table,
|
|
381
|
-
fields: table.fields.map((field) => ({
|
|
382
|
-
...field,
|
|
383
|
-
required: field.required ?? false,
|
|
384
|
-
unique: field.unique ?? false
|
|
385
|
-
})),
|
|
386
|
-
indexes: table.indexes || [],
|
|
387
|
-
constraints: table.constraints || []
|
|
388
|
-
}))
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Convert schema to JSON string
|
|
393
|
-
*/
|
|
394
|
-
schemaToJson(schema, pretty = true) {
|
|
395
|
-
return pretty ? JSON.stringify(schema, null, 2) : JSON.stringify(schema);
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* Parse schema from JSON string
|
|
399
|
-
*/
|
|
400
|
-
parseSchemaString(jsonString) {
|
|
401
|
-
try {
|
|
402
|
-
const schema = JSON.parse(jsonString);
|
|
403
|
-
return this.normalizeSchema(schema);
|
|
404
|
-
} catch (error2) {
|
|
405
|
-
throw new Error(`Failed to parse schema JSON: ${error2}`);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
var defaultParser = new SchemaParser();
|
|
410
|
-
function parseSchema(source) {
|
|
411
|
-
const lines = source.split("\n");
|
|
412
|
-
const tables = {};
|
|
413
|
-
let currentLine = 0;
|
|
414
|
-
const validBaseColumnTypes = /* @__PURE__ */ new Set([
|
|
415
|
-
"uuid",
|
|
416
|
-
"varchar",
|
|
417
|
-
"text",
|
|
418
|
-
"int",
|
|
419
|
-
"bigint",
|
|
420
|
-
"boolean",
|
|
421
|
-
"timestamptz",
|
|
422
|
-
"date"
|
|
423
|
-
]);
|
|
424
|
-
function normalizeColumnType2(type) {
|
|
425
|
-
return type.toLowerCase().trim().replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*,\s*/g, ",").replace(/\s*\)\s*/g, ")");
|
|
426
|
-
}
|
|
427
|
-
function isValidColumnType2(type) {
|
|
428
|
-
const normalizedType = normalizeColumnType2(type);
|
|
429
|
-
if (validBaseColumnTypes.has(normalizedType)) {
|
|
430
|
-
return true;
|
|
431
|
-
}
|
|
432
|
-
return /^varchar\(\d+\)$/.test(normalizedType) || /^numeric\(\d+,\d+\)$/.test(normalizedType);
|
|
433
|
-
}
|
|
434
|
-
function cleanLine(line) {
|
|
435
|
-
const commentIndex = line.search(/(?:\/\/|#)/);
|
|
436
|
-
if (commentIndex !== -1) {
|
|
437
|
-
line = line.substring(0, commentIndex);
|
|
438
|
-
}
|
|
439
|
-
return line.trim();
|
|
440
|
-
}
|
|
441
|
-
function parseForeignKey(fkRef, lineNum) {
|
|
442
|
-
const parts = fkRef.split(".");
|
|
443
|
-
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
444
|
-
throw new Error(`Line ${lineNum}: Invalid foreign key format '${fkRef}'. Expected format: table.column`);
|
|
445
|
-
}
|
|
446
|
-
return {
|
|
447
|
-
table: parts[0],
|
|
448
|
-
column: parts[1]
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
function parseColumn(line, lineNum) {
|
|
452
|
-
const tokens = line.split(/\s+/).filter((t) => t.length > 0);
|
|
453
|
-
if (tokens.length < 2) {
|
|
454
|
-
throw new Error(`Line ${lineNum}: Invalid column definition. Expected: <name> <type> [modifiers...]`);
|
|
455
|
-
}
|
|
456
|
-
const colName = tokens[0];
|
|
457
|
-
const colType = normalizeColumnType2(tokens[1]);
|
|
458
|
-
if (!isValidColumnType2(colType)) {
|
|
459
|
-
throw new Error(
|
|
460
|
-
`Line ${lineNum}: Invalid column type '${tokens[1]}'. Valid types: ${Array.from(validBaseColumnTypes).join(", ")}, varchar(n), numeric(p,s)`
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
const column = {
|
|
464
|
-
name: colName,
|
|
465
|
-
type: colType
|
|
466
|
-
};
|
|
467
|
-
let i = 2;
|
|
468
|
-
while (i < tokens.length) {
|
|
469
|
-
const modifier = tokens[i];
|
|
470
|
-
switch (modifier) {
|
|
471
|
-
case "pk":
|
|
472
|
-
column.primaryKey = true;
|
|
473
|
-
i++;
|
|
474
|
-
break;
|
|
475
|
-
case "unique":
|
|
476
|
-
column.unique = true;
|
|
477
|
-
i++;
|
|
478
|
-
break;
|
|
479
|
-
case "nullable":
|
|
480
|
-
column.nullable = true;
|
|
481
|
-
i++;
|
|
482
|
-
break;
|
|
483
|
-
case "default":
|
|
484
|
-
i++;
|
|
485
|
-
if (i >= tokens.length) {
|
|
486
|
-
throw new Error(`Line ${lineNum}: 'default' modifier requires a value`);
|
|
487
|
-
}
|
|
488
|
-
column.default = tokens[i];
|
|
489
|
-
i++;
|
|
490
|
-
break;
|
|
491
|
-
case "fk":
|
|
492
|
-
i++;
|
|
493
|
-
if (i >= tokens.length) {
|
|
494
|
-
throw new Error(`Line ${lineNum}: 'fk' modifier requires a table.column reference`);
|
|
495
|
-
}
|
|
496
|
-
column.foreignKey = parseForeignKey(tokens[i], lineNum);
|
|
497
|
-
i++;
|
|
498
|
-
break;
|
|
499
|
-
default:
|
|
500
|
-
throw new Error(`Line ${lineNum}: Unknown modifier '${modifier}'`);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
return column;
|
|
504
|
-
}
|
|
505
|
-
function parseTableBlock(startLine) {
|
|
506
|
-
const firstLine = cleanLine(lines[startLine]);
|
|
507
|
-
const match = firstLine.match(/^table\s+(\w+)\s*\{?\s*$/);
|
|
508
|
-
if (!match) {
|
|
509
|
-
throw new Error(`Line ${startLine + 1}: Invalid table definition. Expected: table <name> {`);
|
|
510
|
-
}
|
|
511
|
-
const tableName = match[1];
|
|
512
|
-
if (tables[tableName]) {
|
|
513
|
-
throw new Error(`Line ${startLine + 1}: Duplicate table definition '${tableName}'`);
|
|
514
|
-
}
|
|
515
|
-
const columns = [];
|
|
516
|
-
let lineIdx = startLine + 1;
|
|
517
|
-
let foundClosingBrace = false;
|
|
518
|
-
while (lineIdx < lines.length) {
|
|
519
|
-
const cleaned = cleanLine(lines[lineIdx]);
|
|
520
|
-
if (!cleaned) {
|
|
521
|
-
lineIdx++;
|
|
522
|
-
continue;
|
|
523
|
-
}
|
|
524
|
-
if (cleaned === "}") {
|
|
525
|
-
foundClosingBrace = true;
|
|
526
|
-
break;
|
|
527
|
-
}
|
|
528
|
-
try {
|
|
529
|
-
const column = parseColumn(cleaned, lineIdx + 1);
|
|
530
|
-
columns.push(column);
|
|
531
|
-
} catch (error2) {
|
|
532
|
-
throw error2;
|
|
533
|
-
}
|
|
534
|
-
lineIdx++;
|
|
535
|
-
}
|
|
536
|
-
if (!foundClosingBrace) {
|
|
537
|
-
throw new Error(`Line ${startLine + 1}: Table '${tableName}' block not closed (missing '}')`);
|
|
538
|
-
}
|
|
539
|
-
tables[tableName] = {
|
|
540
|
-
name: tableName,
|
|
541
|
-
columns
|
|
542
|
-
};
|
|
543
|
-
return lineIdx;
|
|
544
|
-
}
|
|
545
|
-
while (currentLine < lines.length) {
|
|
546
|
-
const cleaned = cleanLine(lines[currentLine]);
|
|
547
|
-
if (!cleaned) {
|
|
548
|
-
currentLine++;
|
|
549
|
-
continue;
|
|
550
|
-
}
|
|
551
|
-
if (cleaned.startsWith("table ")) {
|
|
552
|
-
currentLine = parseTableBlock(currentLine);
|
|
553
|
-
} else {
|
|
554
|
-
throw new Error(`Line ${currentLine + 1}: Unexpected content '${cleaned}'. Expected table definition.`);
|
|
555
|
-
}
|
|
556
|
-
currentLine++;
|
|
557
|
-
}
|
|
558
|
-
return { tables };
|
|
559
|
-
}
|
|
560
145
|
|
|
561
146
|
// src/core/paths.ts
|
|
562
147
|
var import_path2 = __toESM(require("path"));
|
|
@@ -581,491 +166,129 @@ function getStatePath(root, config) {
|
|
|
581
166
|
return import_path2.default.join(schemaForgeDir, fileName);
|
|
582
167
|
}
|
|
583
168
|
|
|
584
|
-
// src/core/
|
|
585
|
-
var
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
this.root = root;
|
|
590
|
-
}
|
|
591
|
-
/**
|
|
592
|
-
* Initialize a new SchemaForge project
|
|
593
|
-
*/
|
|
594
|
-
async initializeProject(directory = ".", force = false) {
|
|
595
|
-
const configPath = import_path3.default.join(directory, "schemaforge.config.json");
|
|
596
|
-
if (await fileExists(configPath) && !force) {
|
|
597
|
-
throw new Error("SchemaForge project already initialized. Use --force to overwrite.");
|
|
598
|
-
}
|
|
599
|
-
const defaultConfig = {
|
|
600
|
-
version: "1.0.0",
|
|
601
|
-
database: "postgres",
|
|
602
|
-
schemaDir: "schemas",
|
|
603
|
-
outputDir: "output",
|
|
604
|
-
migrationDir: "migrations"
|
|
605
|
-
};
|
|
606
|
-
await writeJsonFile(configPath, defaultConfig);
|
|
607
|
-
await ensureDir(import_path3.default.join(directory, defaultConfig.schemaDir));
|
|
608
|
-
await ensureDir(import_path3.default.join(directory, defaultConfig.outputDir));
|
|
609
|
-
await ensureDir(import_path3.default.join(directory, defaultConfig.migrationDir));
|
|
610
|
-
const exampleSchema = {
|
|
611
|
-
version: "1.0.0",
|
|
612
|
-
database: "postgres",
|
|
613
|
-
tables: [
|
|
614
|
-
{
|
|
615
|
-
name: "users",
|
|
616
|
-
fields: [
|
|
617
|
-
{ name: "id", type: "uuid", required: true, unique: true },
|
|
618
|
-
{ name: "email", type: "string", required: true, unique: true, length: 255 },
|
|
619
|
-
{ name: "name", type: "string", required: true, length: 255 },
|
|
620
|
-
{ name: "created_at", type: "datetime", required: true }
|
|
621
|
-
],
|
|
622
|
-
indexes: [
|
|
623
|
-
{ name: "idx_users_email", fields: ["email"], unique: true }
|
|
624
|
-
]
|
|
625
|
-
}
|
|
626
|
-
]
|
|
627
|
-
};
|
|
628
|
-
const exampleSchemaPath = import_path3.default.join(
|
|
629
|
-
directory,
|
|
630
|
-
defaultConfig.schemaDir,
|
|
631
|
-
"example.schema.json"
|
|
632
|
-
);
|
|
633
|
-
await writeJsonFile(exampleSchemaPath, exampleSchema);
|
|
634
|
-
this.config = defaultConfig;
|
|
635
|
-
}
|
|
636
|
-
/**
|
|
637
|
-
* Load configuration from file
|
|
638
|
-
*/
|
|
639
|
-
async loadConfig(directory = ".") {
|
|
640
|
-
const configPath = import_path3.default.join(directory, "schemaforge.config.json");
|
|
641
|
-
if (!await fileExists(configPath)) {
|
|
642
|
-
throw new Error('SchemaForge project not initialized. Run "schemaforge init" first.');
|
|
643
|
-
}
|
|
644
|
-
this.config = await readJsonFile(configPath, {});
|
|
645
|
-
return this.config;
|
|
646
|
-
}
|
|
647
|
-
/**
|
|
648
|
-
* Save configuration to file
|
|
649
|
-
*/
|
|
650
|
-
async saveConfig(config, directory = ".") {
|
|
651
|
-
const configPath = import_path3.default.join(directory, "schemaforge.config.json");
|
|
652
|
-
await writeJsonFile(configPath, config);
|
|
653
|
-
this.config = config;
|
|
654
|
-
}
|
|
655
|
-
/**
|
|
656
|
-
* Get current configuration
|
|
657
|
-
*/
|
|
658
|
-
getConfig() {
|
|
659
|
-
return this.config;
|
|
660
|
-
}
|
|
661
|
-
/**
|
|
662
|
-
* Update configuration
|
|
663
|
-
*/
|
|
664
|
-
updateConfig(updates) {
|
|
665
|
-
if (!this.config) {
|
|
666
|
-
throw new Error("No configuration loaded");
|
|
667
|
-
}
|
|
668
|
-
this.config = { ...this.config, ...updates };
|
|
169
|
+
// src/core/provider.ts
|
|
170
|
+
var DEFAULT_PROVIDER = "postgres";
|
|
171
|
+
function resolveProvider(provider) {
|
|
172
|
+
if (!provider) {
|
|
173
|
+
return { provider: DEFAULT_PROVIDER, usedDefault: true };
|
|
669
174
|
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
* Get schema directory path
|
|
679
|
-
*/
|
|
680
|
-
getSchemaDir() {
|
|
681
|
-
if (!this.config) {
|
|
682
|
-
throw new Error("No configuration loaded");
|
|
683
|
-
}
|
|
684
|
-
return import_path3.default.join(this.root, this.config.schemaDir);
|
|
685
|
-
}
|
|
686
|
-
/**
|
|
687
|
-
* Get output directory path
|
|
688
|
-
*/
|
|
689
|
-
getOutputDir() {
|
|
690
|
-
if (!this.config) {
|
|
691
|
-
throw new Error("No configuration loaded");
|
|
692
|
-
}
|
|
693
|
-
return import_path3.default.join(this.root, this.config.outputDir);
|
|
694
|
-
}
|
|
695
|
-
/**
|
|
696
|
-
* Get migration directory path
|
|
697
|
-
*/
|
|
698
|
-
getMigrationDir() {
|
|
699
|
-
if (!this.config) {
|
|
700
|
-
throw new Error("No configuration loaded");
|
|
701
|
-
}
|
|
702
|
-
return import_path3.default.join(this.root, this.config.migrationDir);
|
|
175
|
+
return { provider, usedDefault: false };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/domain.ts
|
|
179
|
+
var corePromise;
|
|
180
|
+
async function loadCore() {
|
|
181
|
+
if (!corePromise) {
|
|
182
|
+
corePromise = import("@xubylele/schema-forge-core");
|
|
703
183
|
}
|
|
704
|
-
|
|
184
|
+
return corePromise;
|
|
185
|
+
}
|
|
186
|
+
async function parseSchema(source) {
|
|
187
|
+
const core = await loadCore();
|
|
188
|
+
return core.parseSchema(source);
|
|
189
|
+
}
|
|
190
|
+
async function validateSchema(schema) {
|
|
191
|
+
const core = await loadCore();
|
|
192
|
+
core.validateSchema(schema);
|
|
193
|
+
}
|
|
194
|
+
async function diffSchemas(previousState, currentSchema) {
|
|
195
|
+
const core = await loadCore();
|
|
196
|
+
return core.diffSchemas(previousState, currentSchema);
|
|
197
|
+
}
|
|
198
|
+
async function generateSql(diff, provider, config) {
|
|
199
|
+
const core = await loadCore();
|
|
200
|
+
return core.generateSql(diff, provider, config);
|
|
201
|
+
}
|
|
705
202
|
async function schemaToState(schema) {
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
const columns = {};
|
|
709
|
-
for (const column of table.columns) {
|
|
710
|
-
columns[column.name] = {
|
|
711
|
-
type: column.type,
|
|
712
|
-
...column.primaryKey !== void 0 && { primaryKey: column.primaryKey },
|
|
713
|
-
...column.unique !== void 0 && { unique: column.unique },
|
|
714
|
-
...column.nullable !== void 0 && { nullable: column.nullable },
|
|
715
|
-
...column.default !== void 0 && { default: column.default },
|
|
716
|
-
...column.foreignKey !== void 0 && { foreignKey: column.foreignKey }
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
tables[tableName] = { columns };
|
|
720
|
-
}
|
|
721
|
-
return {
|
|
722
|
-
version: 1,
|
|
723
|
-
tables
|
|
724
|
-
};
|
|
203
|
+
const core = await loadCore();
|
|
204
|
+
return core.schemaToState(schema);
|
|
725
205
|
}
|
|
726
206
|
async function loadState(statePath) {
|
|
727
|
-
|
|
207
|
+
const core = await loadCore();
|
|
208
|
+
return core.loadState(statePath);
|
|
728
209
|
}
|
|
729
210
|
async function saveState(statePath, state) {
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
await writeJsonFile(statePath, state);
|
|
211
|
+
const core = await loadCore();
|
|
212
|
+
return core.saveState(statePath, state);
|
|
733
213
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
var SchemaValidator = class {
|
|
738
|
-
/**
|
|
739
|
-
* Validate a complete schema
|
|
740
|
-
*/
|
|
741
|
-
validateSchema(schema) {
|
|
742
|
-
const errors = [];
|
|
743
|
-
if (!schema.version) {
|
|
744
|
-
errors.push({
|
|
745
|
-
path: "schema.version",
|
|
746
|
-
message: "Schema version is required",
|
|
747
|
-
severity: "error"
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
if (!schema.database) {
|
|
751
|
-
errors.push({
|
|
752
|
-
path: "schema.database",
|
|
753
|
-
message: "Database type is required",
|
|
754
|
-
severity: "error"
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
if (!schema.tables || schema.tables.length === 0) {
|
|
758
|
-
errors.push({
|
|
759
|
-
path: "schema.tables",
|
|
760
|
-
message: "Schema must contain at least one table",
|
|
761
|
-
severity: "error"
|
|
762
|
-
});
|
|
763
|
-
}
|
|
764
|
-
if (schema.tables) {
|
|
765
|
-
const tableNames = /* @__PURE__ */ new Set();
|
|
766
|
-
for (let i = 0; i < schema.tables.length; i++) {
|
|
767
|
-
const table = schema.tables[i];
|
|
768
|
-
const tableErrors = this.validateTable(table, i);
|
|
769
|
-
errors.push(...tableErrors);
|
|
770
|
-
if (tableNames.has(table.name)) {
|
|
771
|
-
errors.push({
|
|
772
|
-
path: `schema.tables[${i}].name`,
|
|
773
|
-
message: `Duplicate table name: ${table.name}`,
|
|
774
|
-
severity: "error"
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
tableNames.add(table.name);
|
|
778
|
-
}
|
|
779
|
-
errors.push(...this.validateReferences(schema));
|
|
780
|
-
}
|
|
781
|
-
return {
|
|
782
|
-
valid: errors.filter((e) => e.severity === "error").length === 0,
|
|
783
|
-
errors
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
/**
|
|
787
|
-
* Validate a table
|
|
788
|
-
*/
|
|
789
|
-
validateTable(table, tableIndex) {
|
|
790
|
-
const errors = [];
|
|
791
|
-
const basePath = `schema.tables[${tableIndex}]`;
|
|
792
|
-
if (!table.name || table.name.trim() === "") {
|
|
793
|
-
errors.push({
|
|
794
|
-
path: `${basePath}.name`,
|
|
795
|
-
message: "Table name is required",
|
|
796
|
-
severity: "error"
|
|
797
|
-
});
|
|
798
|
-
}
|
|
799
|
-
if (table.name && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table.name)) {
|
|
800
|
-
errors.push({
|
|
801
|
-
path: `${basePath}.name`,
|
|
802
|
-
message: `Invalid table name '${table.name}': must start with letter or underscore and contain only alphanumeric characters and underscores`,
|
|
803
|
-
severity: "error"
|
|
804
|
-
});
|
|
805
|
-
}
|
|
806
|
-
if (!table.fields || table.fields.length === 0) {
|
|
807
|
-
errors.push({
|
|
808
|
-
path: `${basePath}.fields`,
|
|
809
|
-
message: `Table '${table.name}' must have at least one field`,
|
|
810
|
-
severity: "error"
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
if (table.fields) {
|
|
814
|
-
const fieldNames = /* @__PURE__ */ new Set();
|
|
815
|
-
for (let i = 0; i < table.fields.length; i++) {
|
|
816
|
-
const field = table.fields[i];
|
|
817
|
-
const fieldErrors = this.validateField(field, basePath, i);
|
|
818
|
-
errors.push(...fieldErrors);
|
|
819
|
-
if (fieldNames.has(field.name)) {
|
|
820
|
-
errors.push({
|
|
821
|
-
path: `${basePath}.fields[${i}].name`,
|
|
822
|
-
message: `Duplicate field name: ${field.name}`,
|
|
823
|
-
severity: "error"
|
|
824
|
-
});
|
|
825
|
-
}
|
|
826
|
-
fieldNames.add(field.name);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
return errors;
|
|
830
|
-
}
|
|
831
|
-
/**
|
|
832
|
-
* Validate a field
|
|
833
|
-
*/
|
|
834
|
-
validateField(field, tablePath, fieldIndex) {
|
|
835
|
-
const errors = [];
|
|
836
|
-
const basePath = `${tablePath}.fields[${fieldIndex}]`;
|
|
837
|
-
if (!field.name || field.name.trim() === "") {
|
|
838
|
-
errors.push({
|
|
839
|
-
path: `${basePath}.name`,
|
|
840
|
-
message: "Field name is required",
|
|
841
|
-
severity: "error"
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
if (field.name && !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field.name)) {
|
|
845
|
-
errors.push({
|
|
846
|
-
path: `${basePath}.name`,
|
|
847
|
-
message: `Invalid field name '${field.name}': must start with letter or underscore and contain only alphanumeric characters and underscores`,
|
|
848
|
-
severity: "error"
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
|
-
if (!field.type) {
|
|
852
|
-
errors.push({
|
|
853
|
-
path: `${basePath}.type`,
|
|
854
|
-
message: "Field type is required",
|
|
855
|
-
severity: "error"
|
|
856
|
-
});
|
|
857
|
-
}
|
|
858
|
-
if (field.type === "enum") {
|
|
859
|
-
if (!field.enumValues || field.enumValues.length === 0) {
|
|
860
|
-
errors.push({
|
|
861
|
-
path: `${basePath}.enumValues`,
|
|
862
|
-
message: "Enum type requires enumValues array",
|
|
863
|
-
severity: "error"
|
|
864
|
-
});
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
if (field.type === "string" && field.length && field.length <= 0) {
|
|
868
|
-
errors.push({
|
|
869
|
-
path: `${basePath}.length`,
|
|
870
|
-
message: "String length must be greater than 0",
|
|
871
|
-
severity: "error"
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
|
-
return errors;
|
|
875
|
-
}
|
|
876
|
-
/**
|
|
877
|
-
* Validate foreign key references
|
|
878
|
-
*/
|
|
879
|
-
validateReferences(schema) {
|
|
880
|
-
const errors = [];
|
|
881
|
-
const tableNames = new Set(schema.tables.map((t) => t.name));
|
|
882
|
-
for (let i = 0; i < schema.tables.length; i++) {
|
|
883
|
-
const table = schema.tables[i];
|
|
884
|
-
for (let j = 0; j < table.fields.length; j++) {
|
|
885
|
-
const field = table.fields[j];
|
|
886
|
-
if (field.references) {
|
|
887
|
-
const refTable = field.references.table;
|
|
888
|
-
const refField = field.references.field;
|
|
889
|
-
if (!tableNames.has(refTable)) {
|
|
890
|
-
errors.push({
|
|
891
|
-
path: `schema.tables[${i}].fields[${j}].references.table`,
|
|
892
|
-
message: `Referenced table '${refTable}' does not exist`,
|
|
893
|
-
severity: "error"
|
|
894
|
-
});
|
|
895
|
-
} else {
|
|
896
|
-
const referencedTable = schema.tables.find((t) => t.name === refTable);
|
|
897
|
-
if (referencedTable) {
|
|
898
|
-
const referencedField = referencedTable.fields.find((f) => f.name === refField);
|
|
899
|
-
if (!referencedField) {
|
|
900
|
-
errors.push({
|
|
901
|
-
path: `schema.tables[${i}].fields[${j}].references.field`,
|
|
902
|
-
message: `Referenced field '${refField}' does not exist in table '${refTable}'`,
|
|
903
|
-
severity: "error"
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
return errors;
|
|
912
|
-
}
|
|
913
|
-
};
|
|
914
|
-
var defaultValidator = new SchemaValidator();
|
|
915
|
-
var VALID_BASE_COLUMN_TYPES = [
|
|
916
|
-
"uuid",
|
|
917
|
-
"varchar",
|
|
918
|
-
"text",
|
|
919
|
-
"int",
|
|
920
|
-
"bigint",
|
|
921
|
-
"boolean",
|
|
922
|
-
"timestamptz",
|
|
923
|
-
"date"
|
|
924
|
-
];
|
|
925
|
-
function isValidColumnType(type) {
|
|
926
|
-
const normalizedType = type.toLowerCase().trim().replace(/\s+/g, " ").replace(/\s*\(\s*/g, "(").replace(/\s*,\s*/g, ",").replace(/\s*\)\s*/g, ")");
|
|
927
|
-
if (VALID_BASE_COLUMN_TYPES.includes(normalizedType)) {
|
|
928
|
-
return true;
|
|
929
|
-
}
|
|
930
|
-
return /^varchar\(\d+\)$/.test(normalizedType) || /^numeric\(\d+,\d+\)$/.test(normalizedType);
|
|
214
|
+
async function validateSchemaChanges(previousState, currentSchema) {
|
|
215
|
+
const core = await loadCore();
|
|
216
|
+
return core.validateSchemaChanges(previousState, currentSchema);
|
|
931
217
|
}
|
|
932
|
-
function
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
const table = schema.tables[tableName];
|
|
936
|
-
validateTableColumns(tableName, table, schema.tables);
|
|
937
|
-
}
|
|
218
|
+
async function toValidationReport(findings) {
|
|
219
|
+
const core = await loadCore();
|
|
220
|
+
return core.toValidationReport(findings);
|
|
938
221
|
}
|
|
939
|
-
function
|
|
940
|
-
const
|
|
941
|
-
|
|
942
|
-
for (const tableName of tableNames) {
|
|
943
|
-
if (seen.has(tableName)) {
|
|
944
|
-
throw new Error(`Duplicate table: '${tableName}'`);
|
|
945
|
-
}
|
|
946
|
-
seen.add(tableName);
|
|
947
|
-
}
|
|
222
|
+
async function parseMigrationSql(sql) {
|
|
223
|
+
const core = await loadCore();
|
|
224
|
+
return core.parseMigrationSql(sql);
|
|
948
225
|
}
|
|
949
|
-
function
|
|
950
|
-
const
|
|
951
|
-
|
|
952
|
-
for (const column of table.columns) {
|
|
953
|
-
if (columnNames.has(column.name)) {
|
|
954
|
-
throw new Error(`Table '${tableName}': duplicate column '${column.name}'`);
|
|
955
|
-
}
|
|
956
|
-
columnNames.add(column.name);
|
|
957
|
-
if (column.primaryKey) {
|
|
958
|
-
primaryKeyCount++;
|
|
959
|
-
}
|
|
960
|
-
if (!isValidColumnType(column.type)) {
|
|
961
|
-
throw new Error(
|
|
962
|
-
`Table '${tableName}', column '${column.name}': type '${column.type}' is not valid. Supported types: ${VALID_BASE_COLUMN_TYPES.join(", ")}, varchar(n), numeric(p,s)`
|
|
963
|
-
);
|
|
964
|
-
}
|
|
965
|
-
if (column.foreignKey) {
|
|
966
|
-
const fkTable = column.foreignKey.table;
|
|
967
|
-
const fkColumn = column.foreignKey.column;
|
|
968
|
-
if (!allTables[fkTable]) {
|
|
969
|
-
throw new Error(
|
|
970
|
-
`Table '${tableName}', column '${column.name}': referenced table '${fkTable}' does not exist`
|
|
971
|
-
);
|
|
972
|
-
}
|
|
973
|
-
const referencedTable = allTables[fkTable];
|
|
974
|
-
const columnExists = referencedTable.columns.some((col) => col.name === fkColumn);
|
|
975
|
-
if (!columnExists) {
|
|
976
|
-
throw new Error(
|
|
977
|
-
`Table '${tableName}', column '${column.name}': table '${fkTable}' does not have column '${fkColumn}'`
|
|
978
|
-
);
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
if (primaryKeyCount > 1) {
|
|
983
|
-
throw new Error(`Table '${tableName}': can only have one primary key (found ${primaryKeyCount})`);
|
|
984
|
-
}
|
|
226
|
+
async function applySqlOps(ops) {
|
|
227
|
+
const core = await loadCore();
|
|
228
|
+
return core.applySqlOps(ops);
|
|
985
229
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
const statements = [];
|
|
990
|
-
for (const operation of diff.operations) {
|
|
991
|
-
const sql = generateOperation(operation, provider, sqlConfig);
|
|
992
|
-
if (sql) {
|
|
993
|
-
statements.push(sql);
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
return statements.join("\n\n");
|
|
230
|
+
async function schemaToDsl(schema) {
|
|
231
|
+
const core = await loadCore();
|
|
232
|
+
return core.schemaToDsl(schema);
|
|
997
233
|
}
|
|
998
|
-
function
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
return generateCreateTable(operation.table, provider, sqlConfig);
|
|
1002
|
-
case "drop_table":
|
|
1003
|
-
return generateDropTable(operation.tableName);
|
|
1004
|
-
case "column_type_changed":
|
|
1005
|
-
return generateAlterColumnType(
|
|
1006
|
-
operation.tableName,
|
|
1007
|
-
operation.columnName,
|
|
1008
|
-
operation.toType
|
|
1009
|
-
);
|
|
1010
|
-
case "add_column":
|
|
1011
|
-
return generateAddColumn(operation.tableName, operation.column, provider, sqlConfig);
|
|
1012
|
-
case "drop_column":
|
|
1013
|
-
return generateDropColumn(operation.tableName, operation.columnName);
|
|
1014
|
-
}
|
|
234
|
+
async function loadMigrationSqlInput(inputPath) {
|
|
235
|
+
const core = await loadCore();
|
|
236
|
+
return core.loadMigrationSqlInput(inputPath);
|
|
1015
237
|
}
|
|
1016
|
-
function
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1019
|
-
);
|
|
1020
|
-
const lines = ["CREATE TABLE " + table.name + " ("];
|
|
1021
|
-
columnDefs.forEach((colDef, index) => {
|
|
1022
|
-
const isLast = index === columnDefs.length - 1;
|
|
1023
|
-
lines.push(" " + colDef + (isLast ? "" : ","));
|
|
1024
|
-
});
|
|
1025
|
-
lines.push(");");
|
|
1026
|
-
return lines.join("\n");
|
|
238
|
+
async function createSchemaValidationError(message) {
|
|
239
|
+
const core = await loadCore();
|
|
240
|
+
return new core.SchemaValidationError(message);
|
|
1027
241
|
}
|
|
1028
|
-
function
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
242
|
+
async function isSchemaValidationError(error2) {
|
|
243
|
+
const core = await loadCore();
|
|
244
|
+
return error2 instanceof core.SchemaValidationError;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/utils/output.ts
|
|
248
|
+
var import_boxen = __toESM(require("boxen"));
|
|
249
|
+
var import_chalk = require("chalk");
|
|
250
|
+
var isInteractive = Boolean(process.stdout?.isTTY);
|
|
251
|
+
var colorsEnabled = isInteractive && process.env.FORCE_COLOR !== "0" && !("NO_COLOR" in process.env);
|
|
252
|
+
var color = new import_chalk.Chalk({ level: colorsEnabled ? 3 : 0 });
|
|
253
|
+
var theme = {
|
|
254
|
+
primary: color.cyanBright,
|
|
255
|
+
success: color.hex("#00FF88"),
|
|
256
|
+
warning: color.hex("#FFD166"),
|
|
257
|
+
error: color.hex("#EF476F"),
|
|
258
|
+
accent: color.magentaBright
|
|
259
|
+
};
|
|
260
|
+
function success(message) {
|
|
261
|
+
const text = theme.success(`[OK] ${message}`);
|
|
262
|
+
if (!isInteractive) {
|
|
263
|
+
console.log(text);
|
|
264
|
+
return;
|
|
1043
265
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
266
|
+
try {
|
|
267
|
+
console.log(
|
|
268
|
+
(0, import_boxen.default)(text, {
|
|
269
|
+
padding: 1,
|
|
270
|
+
borderColor: "cyan",
|
|
271
|
+
borderStyle: "round"
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
} catch {
|
|
275
|
+
console.log(text);
|
|
1048
276
|
}
|
|
1049
|
-
return parts.join(" ");
|
|
1050
|
-
}
|
|
1051
|
-
function generateDropTable(tableName) {
|
|
1052
|
-
return `DROP TABLE ${tableName};`;
|
|
1053
277
|
}
|
|
1054
|
-
function
|
|
1055
|
-
|
|
1056
|
-
return `ALTER TABLE ${tableName} ADD COLUMN ${colDef};`;
|
|
278
|
+
function info(message) {
|
|
279
|
+
console.log(theme.primary(message));
|
|
1057
280
|
}
|
|
1058
|
-
function
|
|
1059
|
-
|
|
281
|
+
function warning(message) {
|
|
282
|
+
console.warn(theme.warning(`[WARN] ${message}`));
|
|
1060
283
|
}
|
|
1061
|
-
function
|
|
1062
|
-
|
|
284
|
+
function error(message) {
|
|
285
|
+
console.error(theme.error(`[ERROR] ${message}`));
|
|
1063
286
|
}
|
|
1064
287
|
|
|
1065
288
|
// src/commands/diff.ts
|
|
1066
289
|
var REQUIRED_CONFIG_FIELDS = ["schemaFile", "stateFile"];
|
|
1067
290
|
function resolveConfigPath(root, targetPath) {
|
|
1068
|
-
return
|
|
291
|
+
return import_path3.default.isAbsolute(targetPath) ? targetPath : import_path3.default.join(root, targetPath);
|
|
1069
292
|
}
|
|
1070
293
|
async function runDiff() {
|
|
1071
294
|
const root = getProjectRoot();
|
|
@@ -1082,33 +305,30 @@ async function runDiff() {
|
|
|
1082
305
|
}
|
|
1083
306
|
const schemaPath = resolveConfigPath(root, config.schemaFile);
|
|
1084
307
|
const statePath = resolveConfigPath(root, config.stateFile);
|
|
1085
|
-
|
|
1086
|
-
throw new Error(`Unsupported provider '${config.provider}'.`);
|
|
1087
|
-
}
|
|
1088
|
-
const provider = config.provider ?? "postgres";
|
|
308
|
+
const { provider } = resolveProvider(config.provider);
|
|
1089
309
|
const schemaSource = await readTextFile(schemaPath);
|
|
1090
|
-
const schema = parseSchema(schemaSource);
|
|
310
|
+
const schema = await parseSchema(schemaSource);
|
|
1091
311
|
try {
|
|
1092
|
-
validateSchema(schema);
|
|
312
|
+
await validateSchema(schema);
|
|
1093
313
|
} catch (error2) {
|
|
1094
314
|
if (error2 instanceof Error) {
|
|
1095
|
-
throw
|
|
315
|
+
throw await createSchemaValidationError(error2.message);
|
|
1096
316
|
}
|
|
1097
317
|
throw error2;
|
|
1098
318
|
}
|
|
1099
319
|
const previousState = await loadState(statePath);
|
|
1100
|
-
const diff = diffSchemas(previousState, schema);
|
|
320
|
+
const diff = await diffSchemas(previousState, schema);
|
|
1101
321
|
if (diff.operations.length === 0) {
|
|
1102
322
|
success("No changes detected");
|
|
1103
323
|
return;
|
|
1104
324
|
}
|
|
1105
|
-
const sql = generateSql(diff, provider, config.sql);
|
|
325
|
+
const sql = await generateSql(diff, provider, config.sql);
|
|
1106
326
|
console.log(sql);
|
|
1107
327
|
}
|
|
1108
328
|
|
|
1109
329
|
// src/commands/generate.ts
|
|
1110
330
|
var import_commander2 = require("commander");
|
|
1111
|
-
var
|
|
331
|
+
var import_path4 = __toESM(require("path"));
|
|
1112
332
|
|
|
1113
333
|
// src/core/utils.ts
|
|
1114
334
|
function nowTimestamp() {
|
|
@@ -1127,7 +347,7 @@ var REQUIRED_CONFIG_FIELDS2 = [
|
|
|
1127
347
|
"outputDir"
|
|
1128
348
|
];
|
|
1129
349
|
function resolveConfigPath2(root, targetPath) {
|
|
1130
|
-
return
|
|
350
|
+
return import_path4.default.isAbsolute(targetPath) ? targetPath : import_path4.default.join(root, targetPath);
|
|
1131
351
|
}
|
|
1132
352
|
async function runGenerate(options) {
|
|
1133
353
|
const root = getProjectRoot();
|
|
@@ -1145,44 +365,92 @@ async function runGenerate(options) {
|
|
|
1145
365
|
const schemaPath = resolveConfigPath2(root, config.schemaFile);
|
|
1146
366
|
const statePath = resolveConfigPath2(root, config.stateFile);
|
|
1147
367
|
const outputDir = resolveConfigPath2(root, config.outputDir);
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
}
|
|
1151
|
-
const provider = config.provider ?? "postgres";
|
|
1152
|
-
if (!config.provider) {
|
|
368
|
+
const { provider, usedDefault } = resolveProvider(config.provider);
|
|
369
|
+
if (usedDefault) {
|
|
1153
370
|
info("Provider not set; defaulting to postgres.");
|
|
1154
371
|
}
|
|
1155
372
|
info("Generating SQL...");
|
|
1156
373
|
const schemaSource = await readTextFile(schemaPath);
|
|
1157
|
-
const schema = parseSchema(schemaSource);
|
|
374
|
+
const schema = await parseSchema(schemaSource);
|
|
1158
375
|
try {
|
|
1159
|
-
validateSchema(schema);
|
|
376
|
+
await validateSchema(schema);
|
|
1160
377
|
} catch (error2) {
|
|
1161
378
|
if (error2 instanceof Error) {
|
|
1162
|
-
throw
|
|
379
|
+
throw await createSchemaValidationError(error2.message);
|
|
1163
380
|
}
|
|
1164
381
|
throw error2;
|
|
1165
382
|
}
|
|
1166
383
|
const previousState = await loadState(statePath);
|
|
1167
|
-
const diff = diffSchemas(previousState, schema);
|
|
384
|
+
const diff = await diffSchemas(previousState, schema);
|
|
1168
385
|
if (diff.operations.length === 0) {
|
|
1169
386
|
info("No changes detected");
|
|
1170
387
|
return;
|
|
1171
388
|
}
|
|
1172
|
-
const sql = generateSql(diff, provider, config.sql);
|
|
389
|
+
const sql = await generateSql(diff, provider, config.sql);
|
|
1173
390
|
const timestamp = nowTimestamp();
|
|
1174
391
|
const slug = slugifyName(options.name ?? "migration");
|
|
1175
392
|
const fileName = `${timestamp}-${slug}.sql`;
|
|
1176
393
|
await ensureDir(outputDir);
|
|
1177
|
-
const migrationPath =
|
|
394
|
+
const migrationPath = import_path4.default.join(outputDir, fileName);
|
|
1178
395
|
await writeTextFile(migrationPath, sql + "\n");
|
|
1179
396
|
const nextState = await schemaToState(schema);
|
|
1180
397
|
await saveState(statePath, nextState);
|
|
1181
398
|
success(`SQL generated successfully: ${migrationPath}`);
|
|
1182
399
|
}
|
|
1183
400
|
|
|
1184
|
-
// src/commands/
|
|
401
|
+
// src/commands/import.ts
|
|
1185
402
|
var import_commander3 = require("commander");
|
|
403
|
+
var import_path5 = __toESM(require("path"));
|
|
404
|
+
function resolveConfigPath3(root, targetPath) {
|
|
405
|
+
return import_path5.default.isAbsolute(targetPath) ? targetPath : import_path5.default.join(root, targetPath);
|
|
406
|
+
}
|
|
407
|
+
async function runImport(inputPath, options = {}) {
|
|
408
|
+
const root = getProjectRoot();
|
|
409
|
+
const absoluteInputPath = resolveConfigPath3(root, inputPath);
|
|
410
|
+
const inputs = await loadMigrationSqlInput(absoluteInputPath);
|
|
411
|
+
if (inputs.length === 0) {
|
|
412
|
+
throw new Error(`No .sql migration files found in: ${absoluteInputPath}`);
|
|
413
|
+
}
|
|
414
|
+
const allOps = [];
|
|
415
|
+
const parseWarnings = [];
|
|
416
|
+
for (const input of inputs) {
|
|
417
|
+
const result = await parseMigrationSql(input.sql);
|
|
418
|
+
allOps.push(...result.ops);
|
|
419
|
+
parseWarnings.push(...result.warnings.map((item) => ({
|
|
420
|
+
statement: `[${import_path5.default.basename(input.filePath)}] ${item.statement}`,
|
|
421
|
+
reason: item.reason
|
|
422
|
+
})));
|
|
423
|
+
}
|
|
424
|
+
const applied = await applySqlOps(allOps);
|
|
425
|
+
const dsl = await schemaToDsl(applied.schema);
|
|
426
|
+
let targetPath = options.out;
|
|
427
|
+
if (!targetPath) {
|
|
428
|
+
const configPath = getConfigPath(root);
|
|
429
|
+
if (await fileExists(configPath)) {
|
|
430
|
+
const config = await readJsonFile(configPath, {});
|
|
431
|
+
if (typeof config.schemaFile === "string" && config.schemaFile.length > 0) {
|
|
432
|
+
targetPath = config.schemaFile;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const schemaPath = targetPath ? resolveConfigPath3(root, targetPath) : getSchemaFilePath(root);
|
|
437
|
+
await writeTextFile(schemaPath, dsl);
|
|
438
|
+
success(`Imported ${inputs.length} migration file(s) into ${schemaPath}`);
|
|
439
|
+
info(`Parsed ${allOps.length} supported DDL operation(s)`);
|
|
440
|
+
const warnings = [...parseWarnings, ...applied.warnings];
|
|
441
|
+
if (warnings.length > 0) {
|
|
442
|
+
warning(`Ignored ${warnings.length} unsupported item(s)`);
|
|
443
|
+
for (const item of warnings.slice(0, 10)) {
|
|
444
|
+
warning(`${item.reason}: ${item.statement}`);
|
|
445
|
+
}
|
|
446
|
+
if (warnings.length > 10) {
|
|
447
|
+
warning(`...and ${warnings.length - 10} more`);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/commands/init.ts
|
|
453
|
+
var import_commander4 = require("commander");
|
|
1186
454
|
async function runInit() {
|
|
1187
455
|
const root = getProjectRoot();
|
|
1188
456
|
const schemaForgeDir = getSchemaForgeDir(root);
|
|
@@ -1219,8 +487,8 @@ table users {
|
|
|
1219
487
|
await writeTextFile(schemaFilePath, schemaContent);
|
|
1220
488
|
success(`Created ${schemaFilePath}`);
|
|
1221
489
|
const config = {
|
|
1222
|
-
provider: "
|
|
1223
|
-
outputDir: "
|
|
490
|
+
provider: "postgres",
|
|
491
|
+
outputDir: "migrations",
|
|
1224
492
|
schemaFile: "schemaforge/schema.sf",
|
|
1225
493
|
stateFile: "schemaforge/state.json",
|
|
1226
494
|
sql: {
|
|
@@ -1236,7 +504,7 @@ table users {
|
|
|
1236
504
|
};
|
|
1237
505
|
await writeJsonFile(statePath, state);
|
|
1238
506
|
success(`Created ${statePath}`);
|
|
1239
|
-
const outputDir = "
|
|
507
|
+
const outputDir = "migrations";
|
|
1240
508
|
await ensureDir(outputDir);
|
|
1241
509
|
success(`Created ${outputDir}`);
|
|
1242
510
|
success("Project initialized successfully");
|
|
@@ -1245,11 +513,73 @@ table users {
|
|
|
1245
513
|
info(" 2. Run: schema-forge generate");
|
|
1246
514
|
}
|
|
1247
515
|
|
|
516
|
+
// src/commands/validate.ts
|
|
517
|
+
var import_commander5 = require("commander");
|
|
518
|
+
var import_path6 = __toESM(require("path"));
|
|
519
|
+
var REQUIRED_CONFIG_FIELDS3 = ["schemaFile", "stateFile"];
|
|
520
|
+
function resolveConfigPath4(root, targetPath) {
|
|
521
|
+
return import_path6.default.isAbsolute(targetPath) ? targetPath : import_path6.default.join(root, targetPath);
|
|
522
|
+
}
|
|
523
|
+
async function runValidate(options = {}) {
|
|
524
|
+
const root = getProjectRoot();
|
|
525
|
+
const configPath = getConfigPath(root);
|
|
526
|
+
if (!await fileExists(configPath)) {
|
|
527
|
+
throw new Error('SchemaForge project not initialized. Run "schema-forge init" first.');
|
|
528
|
+
}
|
|
529
|
+
const config = await readJsonFile(configPath, {});
|
|
530
|
+
for (const field of REQUIRED_CONFIG_FIELDS3) {
|
|
531
|
+
const value = config[field];
|
|
532
|
+
if (!value || typeof value !== "string") {
|
|
533
|
+
throw new Error(`Invalid config: '${field}' is required`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const schemaPath = resolveConfigPath4(root, config.schemaFile);
|
|
537
|
+
const statePath = resolveConfigPath4(root, config.stateFile);
|
|
538
|
+
const schemaSource = await readTextFile(schemaPath);
|
|
539
|
+
const schema = await parseSchema(schemaSource);
|
|
540
|
+
try {
|
|
541
|
+
await validateSchema(schema);
|
|
542
|
+
} catch (error2) {
|
|
543
|
+
if (error2 instanceof Error) {
|
|
544
|
+
throw await createSchemaValidationError(error2.message);
|
|
545
|
+
}
|
|
546
|
+
throw error2;
|
|
547
|
+
}
|
|
548
|
+
const previousState = await loadState(statePath);
|
|
549
|
+
const findings = await validateSchemaChanges(previousState, schema);
|
|
550
|
+
const report = await toValidationReport(findings);
|
|
551
|
+
if (options.json) {
|
|
552
|
+
console.log(JSON.stringify(report, null, 2));
|
|
553
|
+
process.exitCode = report.hasErrors ? 1 : 0;
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (findings.length === 0) {
|
|
557
|
+
success("No destructive changes detected");
|
|
558
|
+
process.exitCode = 0;
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
console.log(
|
|
562
|
+
`Validation Summary: ${report.errors.length} error(s), ${report.warnings.length} warning(s)`
|
|
563
|
+
);
|
|
564
|
+
const tableOrder = Array.from(new Set(findings.map((finding) => finding.table)));
|
|
565
|
+
for (const tableName of tableOrder) {
|
|
566
|
+
console.log(tableName);
|
|
567
|
+
for (const finding of findings.filter((entry) => entry.table === tableName)) {
|
|
568
|
+
const target = finding.column ? `${finding.table}.${finding.column}` : finding.table;
|
|
569
|
+
const typeRange = finding.from && finding.to ? ` (${finding.from} -> ${finding.to})` : "";
|
|
570
|
+
console.log(
|
|
571
|
+
`${finding.severity.toUpperCase()}: ${finding.code} ${target}${typeRange} - ${finding.message}`
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
process.exitCode = report.hasErrors ? 1 : 0;
|
|
576
|
+
}
|
|
577
|
+
|
|
1248
578
|
// src/cli.ts
|
|
1249
|
-
var program = new
|
|
579
|
+
var program = new import_commander6.Command();
|
|
1250
580
|
program.name("schema-forge").description("CLI tool for schema management and SQL generation").version(package_default.version);
|
|
1251
|
-
function handleError(error2) {
|
|
1252
|
-
if (error2 instanceof
|
|
581
|
+
async function handleError(error2) {
|
|
582
|
+
if (await isSchemaValidationError(error2) && error2 instanceof Error) {
|
|
1253
583
|
error(error2.message);
|
|
1254
584
|
process.exitCode = 2;
|
|
1255
585
|
return;
|
|
@@ -1265,21 +595,35 @@ program.command("init").description("Initialize a new schema project").action(as
|
|
|
1265
595
|
try {
|
|
1266
596
|
await runInit();
|
|
1267
597
|
} catch (error2) {
|
|
1268
|
-
handleError(error2);
|
|
598
|
+
await handleError(error2);
|
|
1269
599
|
}
|
|
1270
600
|
});
|
|
1271
601
|
program.command("generate").description("Generate SQL from schema files").option("--name <string>", "Schema name to generate").action(async (options) => {
|
|
1272
602
|
try {
|
|
1273
603
|
await runGenerate(options);
|
|
1274
604
|
} catch (error2) {
|
|
1275
|
-
handleError(error2);
|
|
605
|
+
await handleError(error2);
|
|
1276
606
|
}
|
|
1277
607
|
});
|
|
1278
608
|
program.command("diff").description("Compare two schema versions and generate migration SQL").action(async () => {
|
|
1279
609
|
try {
|
|
1280
610
|
await runDiff();
|
|
1281
611
|
} catch (error2) {
|
|
1282
|
-
handleError(error2);
|
|
612
|
+
await handleError(error2);
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
program.command("import").description("Import schema from SQL migrations").argument("<path>", "Path to .sql file or migrations directory").option("--out <path>", "Output schema file path").action(async (targetPath, options) => {
|
|
616
|
+
try {
|
|
617
|
+
await runImport(targetPath, options);
|
|
618
|
+
} catch (error2) {
|
|
619
|
+
await handleError(error2);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
program.command("validate").description("Detect destructive or risky schema changes").option("--json", "Output structured JSON").action(async (options) => {
|
|
623
|
+
try {
|
|
624
|
+
await runValidate(options);
|
|
625
|
+
} catch (error2) {
|
|
626
|
+
await handleError(error2);
|
|
1283
627
|
}
|
|
1284
628
|
});
|
|
1285
629
|
program.parse(process.argv);
|