relq 1.0.26 → 1.0.28
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/cjs/cli/commands/export.cjs +1 -0
- package/dist/cjs/cli/commands/import.cjs +58 -5
- package/dist/cjs/cli/commands/pull.cjs +215 -9
- package/dist/cjs/cli/commands/sync.cjs +4 -1
- package/dist/cjs/cli/utils/ast-codegen.cjs +165 -9
- package/dist/cjs/cli/utils/ast-transformer.cjs +16 -2
- package/dist/cjs/cli/utils/fast-introspect.cjs +14 -0
- package/dist/cjs/cli/utils/schema-comparator.cjs +24 -0
- package/dist/cjs/cli/utils/schema-introspect.cjs +25 -4
- package/dist/cjs/cli/utils/sql-parser.cjs +4 -1
- package/dist/cjs/config/config.cjs +3 -0
- package/dist/cjs/schema-definition/pg-function.cjs +4 -0
- package/dist/cjs/schema-definition/pg-trigger.cjs +9 -5
- package/dist/esm/cli/commands/export.js +1 -0
- package/dist/esm/cli/commands/import.js +59 -6
- package/dist/esm/cli/commands/pull.js +216 -10
- package/dist/esm/cli/commands/sync.js +5 -2
- package/dist/esm/cli/utils/ast-codegen.js +163 -9
- package/dist/esm/cli/utils/ast-transformer.js +16 -2
- package/dist/esm/cli/utils/fast-introspect.js +14 -0
- package/dist/esm/cli/utils/schema-comparator.js +24 -0
- package/dist/esm/cli/utils/schema-introspect.js +25 -4
- package/dist/esm/cli/utils/sql-parser.js +4 -1
- package/dist/esm/config/config.js +3 -0
- package/dist/esm/schema-definition/pg-function.js +4 -0
- package/dist/esm/schema-definition/pg-trigger.js +9 -5
- package/dist/schema-builder.d.ts +25 -19
- package/package.json +1 -1
|
@@ -242,6 +242,31 @@ async function importCommand(sqlFilePath, options = {}, projectRoot = process.cw
|
|
|
242
242
|
return;
|
|
243
243
|
}
|
|
244
244
|
spinner.start('Generating TypeScript schema');
|
|
245
|
+
const outputPath = options.output || (0, config_loader_1.getSchemaPath)(config);
|
|
246
|
+
const absoluteOutputPath = path.resolve(projectRoot, outputPath);
|
|
247
|
+
const schemaExists = fs.existsSync(absoluteOutputPath);
|
|
248
|
+
if (schemaExists && (includeFunctions || includeTriggers)) {
|
|
249
|
+
const existingContent = fs.readFileSync(absoluteOutputPath, 'utf-8');
|
|
250
|
+
const hasPgFunction = /\bpgFunction\s*\(/.test(existingContent);
|
|
251
|
+
const hasPgTrigger = /\bpgTrigger\s*\(/.test(existingContent);
|
|
252
|
+
if (hasPgFunction || hasPgTrigger) {
|
|
253
|
+
spinner.stop();
|
|
254
|
+
const items = [];
|
|
255
|
+
if (hasPgFunction)
|
|
256
|
+
items.push('pgFunction');
|
|
257
|
+
if (hasPgTrigger)
|
|
258
|
+
items.push('pgTrigger');
|
|
259
|
+
const schemaBaseName = path.basename(absoluteOutputPath, '.ts');
|
|
260
|
+
(0, git_utils_1.fatal)(`Existing schema contains ${items.join(' and ')} definitions`, `Functions and triggers should be in separate files:\n` +
|
|
261
|
+
` ${git_utils_1.colors.cyan(`${schemaBaseName}.functions.ts`)} - for functions\n` +
|
|
262
|
+
` ${git_utils_1.colors.cyan(`${schemaBaseName}.triggers.ts`)} - for triggers\n\n` +
|
|
263
|
+
`To migrate:\n` +
|
|
264
|
+
` 1. Move pgFunction definitions to ${schemaBaseName}.functions.ts\n` +
|
|
265
|
+
` 2. Move pgTrigger definitions to ${schemaBaseName}.triggers.ts\n` +
|
|
266
|
+
` 3. Run ${git_utils_1.colors.cyan('relq import')} again\n\n` +
|
|
267
|
+
`Or use ${git_utils_1.colors.cyan('relq import --force')} to overwrite and regenerate all files.`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
245
270
|
spinner.succeed('Generated TypeScript schema');
|
|
246
271
|
const incomingSchema = convertToNormalizedSchema(filteredSchema, filteredFunctions, triggers);
|
|
247
272
|
const existingSnapshot = (0, repo_manager_1.loadSnapshot)(projectRoot);
|
|
@@ -389,10 +414,11 @@ async function importCommand(sqlFilePath, options = {}, projectRoot = process.cw
|
|
|
389
414
|
when: t.when,
|
|
390
415
|
})) : [],
|
|
391
416
|
});
|
|
417
|
+
(0, ast_codegen_1.assignTrackingIds)(astSchema);
|
|
392
418
|
const finalTypescriptContent = (0, ast_codegen_1.generateTypeScriptFromAST)(astSchema, {
|
|
393
419
|
camelCase: true,
|
|
394
|
-
includeFunctions,
|
|
395
|
-
includeTriggers,
|
|
420
|
+
includeFunctions: false,
|
|
421
|
+
includeTriggers: false,
|
|
396
422
|
});
|
|
397
423
|
if (dryRun) {
|
|
398
424
|
console.log('');
|
|
@@ -409,14 +435,39 @@ async function importCommand(sqlFilePath, options = {}, projectRoot = process.cw
|
|
|
409
435
|
console.log('');
|
|
410
436
|
return;
|
|
411
437
|
}
|
|
412
|
-
const outputPath = options.output || (0, config_loader_1.getSchemaPath)(config);
|
|
413
|
-
const absoluteOutputPath = path.resolve(projectRoot, outputPath);
|
|
414
438
|
const outputDir = path.dirname(absoluteOutputPath);
|
|
415
439
|
if (!fs.existsSync(outputDir)) {
|
|
416
440
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
417
441
|
}
|
|
418
442
|
fs.writeFileSync(absoluteOutputPath, finalTypescriptContent, 'utf-8');
|
|
419
443
|
console.log(`Written ${git_utils_1.colors.cyan(absoluteOutputPath)} ${git_utils_1.colors.gray(`(${(0, git_utils_1.formatBytes)(finalTypescriptContent.length)})`)}`);
|
|
444
|
+
if (includeFunctions && astSchema.functions.length > 0) {
|
|
445
|
+
const schemaBaseName = path.basename(absoluteOutputPath, '.ts');
|
|
446
|
+
const functionsPath = path.join(outputDir, `${schemaBaseName}.functions.ts`);
|
|
447
|
+
const functionsCode = (0, ast_codegen_1.generateFunctionsFile)(astSchema, {
|
|
448
|
+
camelCase: true,
|
|
449
|
+
importPath: 'relq/schema-builder',
|
|
450
|
+
schemaImportPath: `./${schemaBaseName}`,
|
|
451
|
+
});
|
|
452
|
+
if (functionsCode) {
|
|
453
|
+
fs.writeFileSync(functionsPath, functionsCode, 'utf-8');
|
|
454
|
+
console.log(`Written ${git_utils_1.colors.cyan(functionsPath)} ${git_utils_1.colors.gray(`(${(0, git_utils_1.formatBytes)(functionsCode.length)})`)}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
if (includeTriggers && astSchema.triggers.length > 0) {
|
|
458
|
+
const schemaBaseName = path.basename(absoluteOutputPath, '.ts');
|
|
459
|
+
const triggersPath = path.join(outputDir, `${schemaBaseName}.triggers.ts`);
|
|
460
|
+
const triggersCode = (0, ast_codegen_1.generateTriggersFile)(astSchema, {
|
|
461
|
+
camelCase: true,
|
|
462
|
+
importPath: 'relq/schema-builder',
|
|
463
|
+
schemaImportPath: `./${schemaBaseName}`,
|
|
464
|
+
functionsImportPath: `./${schemaBaseName}.functions`,
|
|
465
|
+
});
|
|
466
|
+
if (triggersCode) {
|
|
467
|
+
fs.writeFileSync(triggersPath, triggersCode, 'utf-8');
|
|
468
|
+
console.log(`Written ${git_utils_1.colors.cyan(triggersPath)} ${git_utils_1.colors.gray(`(${(0, git_utils_1.formatBytes)(triggersCode.length)})`)}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
420
471
|
applyTrackingIdsToSnapshot(finalTypescriptContent, mergedSchema);
|
|
421
472
|
(0, repo_manager_1.saveSnapshot)(mergedSchema, projectRoot);
|
|
422
473
|
if (changes.length > 0) {
|
|
@@ -660,7 +711,7 @@ function convertToNormalizedSchema(parsed, functions = [], triggers = []) {
|
|
|
660
711
|
table: t.tableName,
|
|
661
712
|
events: [t.event],
|
|
662
713
|
timing: t.timing,
|
|
663
|
-
forEach: '
|
|
714
|
+
forEach: (t.forEach || 'ROW'),
|
|
664
715
|
functionName: t.functionName || '',
|
|
665
716
|
})),
|
|
666
717
|
views: regularViews.map(v => ({
|
|
@@ -742,6 +793,7 @@ function snapshotToDbSchema(snapshot) {
|
|
|
742
793
|
tableName: t.table,
|
|
743
794
|
timing: t.timing,
|
|
744
795
|
event: t.events?.[0] || '',
|
|
796
|
+
forEach: (t.forEach || 'ROW'),
|
|
745
797
|
functionName: t.functionName || '',
|
|
746
798
|
definition: '',
|
|
747
799
|
isEnabled: t.isEnabled ?? true,
|
|
@@ -845,6 +897,7 @@ function snapshotToDbSchemaForGeneration(snapshot) {
|
|
|
845
897
|
tableName: t.table,
|
|
846
898
|
timing: t.timing,
|
|
847
899
|
event: t.events?.[0] || '',
|
|
900
|
+
forEach: (t.forEach || 'ROW'),
|
|
848
901
|
functionName: t.functionName || '',
|
|
849
902
|
definition: '',
|
|
850
903
|
isEnabled: t.isEnabled ?? true,
|
|
@@ -375,10 +375,154 @@ async function pullCommand(context) {
|
|
|
375
375
|
(0, cli_utils_1.fatal)('You have unresolved merge conflicts', `Use ${cli_utils_1.colors.cyan('relq resolve')} to see and resolve conflicts\nOr use ${cli_utils_1.colors.cyan('relq pull --force')} to overwrite local`);
|
|
376
376
|
}
|
|
377
377
|
if (schemaExists && localSnapshot && !force) {
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
378
|
+
const localForCompare = {
|
|
379
|
+
extensions: localSnapshot.extensions?.map(e => e.name) || [],
|
|
380
|
+
enums: localSnapshot.enums || [],
|
|
381
|
+
domains: localSnapshot.domains?.map(d => ({
|
|
382
|
+
name: d.name,
|
|
383
|
+
baseType: d.baseType,
|
|
384
|
+
isNotNull: d.notNull,
|
|
385
|
+
defaultValue: d.default,
|
|
386
|
+
checkExpression: d.check,
|
|
387
|
+
})) || [],
|
|
388
|
+
compositeTypes: localSnapshot.compositeTypes || [],
|
|
389
|
+
sequences: localSnapshot.sequences || [],
|
|
390
|
+
tables: localSnapshot.tables.map(t => ({
|
|
391
|
+
name: t.name,
|
|
392
|
+
schema: t.schema,
|
|
393
|
+
columns: t.columns.map(c => ({
|
|
394
|
+
name: c.name,
|
|
395
|
+
dataType: c.type,
|
|
396
|
+
isNullable: c.nullable,
|
|
397
|
+
defaultValue: c.default,
|
|
398
|
+
isPrimaryKey: c.primaryKey,
|
|
399
|
+
isUnique: c.unique,
|
|
400
|
+
comment: c.comment,
|
|
401
|
+
})),
|
|
402
|
+
indexes: t.indexes.map(i => ({
|
|
403
|
+
name: i.name,
|
|
404
|
+
columns: i.columns,
|
|
405
|
+
isUnique: i.unique,
|
|
406
|
+
type: i.type,
|
|
407
|
+
comment: i.comment,
|
|
408
|
+
})),
|
|
409
|
+
constraints: t.constraints || [],
|
|
410
|
+
isPartitioned: t.isPartitioned,
|
|
411
|
+
partitionType: t.partitionType,
|
|
412
|
+
partitionKey: t.partitionKey,
|
|
413
|
+
comment: t.comment,
|
|
414
|
+
})),
|
|
415
|
+
functions: localSnapshot.functions || [],
|
|
416
|
+
triggers: localSnapshot.triggers || [],
|
|
417
|
+
};
|
|
418
|
+
const remoteForCompare = {
|
|
419
|
+
extensions: dbSchema.extensions || [],
|
|
420
|
+
enums: filteredEnums || [],
|
|
421
|
+
domains: filteredDomains?.map(d => ({
|
|
422
|
+
name: d.name,
|
|
423
|
+
baseType: d.baseType,
|
|
424
|
+
isNotNull: d.isNotNull,
|
|
425
|
+
defaultValue: d.defaultValue,
|
|
426
|
+
checkExpression: d.checkExpression,
|
|
427
|
+
})) || [],
|
|
428
|
+
compositeTypes: filteredCompositeTypes || [],
|
|
429
|
+
sequences: [],
|
|
430
|
+
tables: filteredTables.map(t => ({
|
|
431
|
+
name: t.name,
|
|
432
|
+
schema: t.schema,
|
|
433
|
+
columns: t.columns.map(c => ({
|
|
434
|
+
name: c.name,
|
|
435
|
+
dataType: c.dataType,
|
|
436
|
+
isNullable: c.isNullable,
|
|
437
|
+
defaultValue: c.defaultValue,
|
|
438
|
+
isPrimaryKey: c.isPrimaryKey,
|
|
439
|
+
isUnique: c.isUnique,
|
|
440
|
+
comment: c.comment,
|
|
441
|
+
})),
|
|
442
|
+
indexes: t.indexes.map(i => ({
|
|
443
|
+
name: i.name,
|
|
444
|
+
columns: i.columns,
|
|
445
|
+
isUnique: i.isUnique,
|
|
446
|
+
type: i.type,
|
|
447
|
+
comment: i.comment,
|
|
448
|
+
})),
|
|
449
|
+
constraints: t.constraints || [],
|
|
450
|
+
isPartitioned: t.isPartitioned,
|
|
451
|
+
partitionType: t.partitionType,
|
|
452
|
+
partitionKey: t.partitionKey,
|
|
453
|
+
comment: t.comment,
|
|
454
|
+
})),
|
|
455
|
+
functions: filteredFunctions || [],
|
|
456
|
+
triggers: filteredTriggers || [],
|
|
457
|
+
};
|
|
458
|
+
const allChanges = (0, schema_comparator_1.compareSchemas)(localForCompare, remoteForCompare);
|
|
459
|
+
const changeDisplays = [];
|
|
460
|
+
for (const change of allChanges) {
|
|
461
|
+
const objType = change.objectType;
|
|
462
|
+
const changeType = change.type;
|
|
463
|
+
let action;
|
|
464
|
+
if (changeType === 'CREATE')
|
|
465
|
+
action = 'added';
|
|
466
|
+
else if (changeType === 'DROP')
|
|
467
|
+
action = 'removed';
|
|
468
|
+
else
|
|
469
|
+
action = 'modified';
|
|
470
|
+
let type;
|
|
471
|
+
let name;
|
|
472
|
+
if (objType === 'TABLE') {
|
|
473
|
+
type = 'table';
|
|
474
|
+
name = change.objectName;
|
|
475
|
+
}
|
|
476
|
+
else if (objType === 'COLUMN') {
|
|
477
|
+
type = 'column';
|
|
478
|
+
name = change.parentName ? `${change.parentName}.${change.objectName}` : change.objectName;
|
|
479
|
+
}
|
|
480
|
+
else if (objType === 'INDEX') {
|
|
481
|
+
type = 'index';
|
|
482
|
+
name = change.parentName ? `${change.parentName}:${change.objectName}` : change.objectName;
|
|
483
|
+
}
|
|
484
|
+
else if (objType === 'COLUMN_COMMENT') {
|
|
485
|
+
type = 'column comment';
|
|
486
|
+
const colName = change.after?.columnName || change.before?.columnName || change.objectName;
|
|
487
|
+
const tblName = change.after?.tableName || change.before?.tableName || change.parentName;
|
|
488
|
+
name = tblName ? `${tblName}.${colName}` : colName;
|
|
489
|
+
}
|
|
490
|
+
else if (objType === 'TABLE_COMMENT') {
|
|
491
|
+
type = 'table comment';
|
|
492
|
+
name = change.after?.tableName || change.before?.tableName || change.objectName;
|
|
493
|
+
}
|
|
494
|
+
else if (objType === 'INDEX_COMMENT') {
|
|
495
|
+
type = 'index comment';
|
|
496
|
+
const idxName = change.after?.indexName || change.before?.indexName || change.objectName;
|
|
497
|
+
const tblName = change.after?.tableName || change.before?.tableName || change.parentName;
|
|
498
|
+
name = tblName ? `${tblName}:${idxName}` : idxName;
|
|
499
|
+
}
|
|
500
|
+
else if (objType === 'CONSTRAINT' || objType === 'FOREIGN_KEY' || objType === 'PRIMARY_KEY' || objType === 'CHECK') {
|
|
501
|
+
type = objType.toLowerCase().replace(/_/g, ' ');
|
|
502
|
+
name = change.parentName ? `${change.parentName}::${change.objectName}` : change.objectName;
|
|
503
|
+
}
|
|
504
|
+
else if (objType === 'ENUM') {
|
|
505
|
+
type = 'enum';
|
|
506
|
+
name = change.objectName;
|
|
507
|
+
}
|
|
508
|
+
else if (objType === 'DOMAIN') {
|
|
509
|
+
type = 'domain';
|
|
510
|
+
name = change.objectName;
|
|
511
|
+
}
|
|
512
|
+
else if (objType === 'FUNCTION') {
|
|
513
|
+
type = 'function';
|
|
514
|
+
name = change.objectName;
|
|
515
|
+
}
|
|
516
|
+
else if (objType === 'TRIGGER') {
|
|
517
|
+
type = 'trigger';
|
|
518
|
+
name = change.objectName;
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
type = objType.toLowerCase().replace(/_/g, ' ');
|
|
522
|
+
name = change.objectName;
|
|
523
|
+
}
|
|
524
|
+
changeDisplays.push({ action, type, name });
|
|
525
|
+
}
|
|
382
526
|
const conflicts = detectObjectConflicts(localSnapshot, currentSchema);
|
|
383
527
|
if (conflicts.length > 0 && !force) {
|
|
384
528
|
const mergeState = {
|
|
@@ -400,17 +544,27 @@ async function pullCommand(context) {
|
|
|
400
544
|
}
|
|
401
545
|
(0, cli_utils_1.fatal)('Automatic merge failed; fix conflicts and then commit', `${cli_utils_1.colors.cyan('relq resolve --theirs <name>')} Take remote version\n${cli_utils_1.colors.cyan('relq resolve --all-theirs')} Take all remote\n${cli_utils_1.colors.cyan('relq pull --force')} Force overwrite local`);
|
|
402
546
|
}
|
|
403
|
-
if (
|
|
547
|
+
if (allChanges.length === 0) {
|
|
404
548
|
console.log('Already up to date with remote');
|
|
405
549
|
console.log('');
|
|
406
550
|
return;
|
|
407
551
|
}
|
|
408
552
|
console.log(`${cli_utils_1.colors.yellow('Remote has changes:')}`);
|
|
409
|
-
|
|
410
|
-
|
|
553
|
+
for (const chg of changeDisplays.slice(0, 15)) {
|
|
554
|
+
let colorFn = cli_utils_1.colors.cyan;
|
|
555
|
+
let prefix = '~';
|
|
556
|
+
if (chg.action === 'added') {
|
|
557
|
+
colorFn = cli_utils_1.colors.green;
|
|
558
|
+
prefix = '+';
|
|
559
|
+
}
|
|
560
|
+
else if (chg.action === 'removed') {
|
|
561
|
+
colorFn = cli_utils_1.colors.red;
|
|
562
|
+
prefix = '-';
|
|
563
|
+
}
|
|
564
|
+
console.log(` ${colorFn(prefix)} ${chg.type}: ${cli_utils_1.colors.bold(chg.name)}`);
|
|
411
565
|
}
|
|
412
|
-
if (
|
|
413
|
-
console.log(` ${cli_utils_1.colors.
|
|
566
|
+
if (changeDisplays.length > 15) {
|
|
567
|
+
console.log(` ${cli_utils_1.colors.muted(`... and ${changeDisplays.length - 15} more`)}`);
|
|
414
568
|
}
|
|
415
569
|
console.log('');
|
|
416
570
|
const noAutoMerge = flags['no-auto-merge'] === true;
|
|
@@ -484,12 +638,35 @@ async function pullCommand(context) {
|
|
|
484
638
|
console.log('');
|
|
485
639
|
return;
|
|
486
640
|
}
|
|
641
|
+
if (schemaExists && (includeFunctions || includeTriggers)) {
|
|
642
|
+
const existingContent = fs.readFileSync(schemaPath, 'utf-8');
|
|
643
|
+
const hasPgFunction = /\bpgFunction\s*\(/.test(existingContent);
|
|
644
|
+
const hasPgTrigger = /\bpgTrigger\s*\(/.test(existingContent);
|
|
645
|
+
if (hasPgFunction || hasPgTrigger) {
|
|
646
|
+
const items = [];
|
|
647
|
+
if (hasPgFunction)
|
|
648
|
+
items.push('pgFunction');
|
|
649
|
+
if (hasPgTrigger)
|
|
650
|
+
items.push('pgTrigger');
|
|
651
|
+
const schemaBaseName = path.basename(schemaPath, '.ts');
|
|
652
|
+
(0, cli_utils_1.fatal)(`Existing schema contains ${items.join(' and ')} definitions`, `Functions and triggers should be in separate files:\n` +
|
|
653
|
+
` ${cli_utils_1.colors.cyan(`${schemaBaseName}.functions.ts`)} - for functions\n` +
|
|
654
|
+
` ${cli_utils_1.colors.cyan(`${schemaBaseName}.triggers.ts`)} - for triggers\n\n` +
|
|
655
|
+
`To migrate:\n` +
|
|
656
|
+
` 1. Move pgFunction definitions to ${schemaBaseName}.functions.ts\n` +
|
|
657
|
+
` 2. Move pgTrigger definitions to ${schemaBaseName}.triggers.ts\n` +
|
|
658
|
+
` 3. Run ${cli_utils_1.colors.cyan('relq pull')} again\n\n` +
|
|
659
|
+
`Or use ${cli_utils_1.colors.cyan('relq pull --force')} to overwrite and regenerate all files.`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
487
662
|
spinner.start('Generating TypeScript schema...');
|
|
488
663
|
const parsedSchema = await (0, ast_transformer_1.introspectedToParsedSchema)(dbSchema);
|
|
489
664
|
(0, ast_codegen_1.assignTrackingIds)(parsedSchema);
|
|
490
665
|
const typescript = (0, ast_codegen_1.generateTypeScriptFromAST)(parsedSchema, {
|
|
491
666
|
camelCase: config.generate?.camelCase ?? true,
|
|
492
667
|
importPath: 'relq/schema-builder',
|
|
668
|
+
includeFunctions: false,
|
|
669
|
+
includeTriggers: false,
|
|
493
670
|
});
|
|
494
671
|
spinner.succeed('Generated TypeScript schema');
|
|
495
672
|
const schemaDir = path.dirname(schemaPath);
|
|
@@ -502,6 +679,35 @@ async function pullCommand(context) {
|
|
|
502
679
|
spinner.succeed(`Written ${cli_utils_1.colors.cyan(schemaPath)} ${cli_utils_1.colors.muted(`(${(0, cli_utils_1.formatBytes)(fileSize)})`)}`);
|
|
503
680
|
const fileHash = (0, repo_manager_1.hashFileContent)(typescript);
|
|
504
681
|
(0, repo_manager_1.saveFileHash)(fileHash, projectRoot);
|
|
682
|
+
if (includeFunctions && parsedSchema.functions.length > 0) {
|
|
683
|
+
const schemaBaseName = path.basename(schemaPath, '.ts');
|
|
684
|
+
const functionsPath = path.join(schemaDir, `${schemaBaseName}.functions.ts`);
|
|
685
|
+
const functionsCode = (0, ast_codegen_1.generateFunctionsFile)(parsedSchema, {
|
|
686
|
+
camelCase: config.generate?.camelCase ?? true,
|
|
687
|
+
importPath: 'relq/schema-builder',
|
|
688
|
+
schemaImportPath: `./${schemaBaseName}`,
|
|
689
|
+
});
|
|
690
|
+
if (functionsCode) {
|
|
691
|
+
fs.writeFileSync(functionsPath, functionsCode, 'utf-8');
|
|
692
|
+
const funcFileSize = Buffer.byteLength(functionsCode, 'utf8');
|
|
693
|
+
spinner.succeed(`Written ${cli_utils_1.colors.cyan(functionsPath)} ${cli_utils_1.colors.muted(`(${(0, cli_utils_1.formatBytes)(funcFileSize)})`)}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (includeTriggers && parsedSchema.triggers.length > 0) {
|
|
697
|
+
const schemaBaseName = path.basename(schemaPath, '.ts');
|
|
698
|
+
const triggersPath = path.join(schemaDir, `${schemaBaseName}.triggers.ts`);
|
|
699
|
+
const triggersCode = (0, ast_codegen_1.generateTriggersFile)(parsedSchema, {
|
|
700
|
+
camelCase: config.generate?.camelCase ?? true,
|
|
701
|
+
importPath: 'relq/schema-builder',
|
|
702
|
+
schemaImportPath: `./${schemaBaseName}`,
|
|
703
|
+
functionsImportPath: `./${schemaBaseName}.functions`,
|
|
704
|
+
});
|
|
705
|
+
if (triggersCode) {
|
|
706
|
+
fs.writeFileSync(triggersPath, triggersCode, 'utf-8');
|
|
707
|
+
const trigFileSize = Buffer.byteLength(triggersCode, 'utf8');
|
|
708
|
+
spinner.succeed(`Written ${cli_utils_1.colors.cyan(triggersPath)} ${cli_utils_1.colors.muted(`(${(0, cli_utils_1.formatBytes)(trigFileSize)})`)}`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
505
711
|
const oldSnapshot = (0, repo_manager_1.loadSnapshot)(projectRoot);
|
|
506
712
|
const beforeSchema = oldSnapshot ? {
|
|
507
713
|
extensions: oldSnapshot.extensions?.map(e => e.name) || [],
|
|
@@ -16,7 +16,10 @@ async function syncCommand(context) {
|
|
|
16
16
|
const { projectRoot } = context;
|
|
17
17
|
console.log('');
|
|
18
18
|
if (!(0, repo_manager_1.isInitialized)(projectRoot)) {
|
|
19
|
-
|
|
19
|
+
console.log(`${cli_utils_1.colors.yellow('Initializing')} .relq repository...`);
|
|
20
|
+
(0, repo_manager_1.initRepository)(projectRoot);
|
|
21
|
+
console.log(`${cli_utils_1.colors.green('✓')} Repository initialized`);
|
|
22
|
+
console.log('');
|
|
20
23
|
}
|
|
21
24
|
const spinner = (0, cli_utils_1.createSpinner)();
|
|
22
25
|
try {
|
|
@@ -4,7 +4,10 @@ exports.resetTrackingIdCounter = resetTrackingIdCounter;
|
|
|
4
4
|
exports.assignTrackingIds = assignTrackingIds;
|
|
5
5
|
exports.copyTrackingIdsToNormalized = copyTrackingIdsToNormalized;
|
|
6
6
|
exports.generateTypeScriptFromAST = generateTypeScriptFromAST;
|
|
7
|
+
exports.generateFunctionsFile = generateFunctionsFile;
|
|
8
|
+
exports.generateTriggersFile = generateTriggersFile;
|
|
7
9
|
const utils_1 = require("./ast/codegen/utils.cjs");
|
|
10
|
+
const sql_formatter_1 = require("sql-formatter");
|
|
8
11
|
const builder_1 = require("./ast/codegen/builder.cjs");
|
|
9
12
|
const type_map_1 = require("./ast/codegen/type-map.cjs");
|
|
10
13
|
const defaults_1 = require("./ast/codegen/defaults.cjs");
|
|
@@ -552,16 +555,17 @@ function generateSequenceCode(seq, useCamelCase) {
|
|
|
552
555
|
const optsStr = opts.length > 0 ? `, { ${opts.join(', ')} }` : '';
|
|
553
556
|
return `export const ${seqName} = pgSequence('${seq.name}'${optsStr})`;
|
|
554
557
|
}
|
|
555
|
-
function generateFunctionCode(func, useCamelCase) {
|
|
556
|
-
const funcName = useCamelCase ? (0, utils_1.toCamelCase)(func.name) : func.name;
|
|
558
|
+
function generateFunctionCode(func, useCamelCase, varNameOverride) {
|
|
559
|
+
const funcName = varNameOverride || (useCamelCase ? (0, utils_1.toCamelCase)(func.name) : func.name);
|
|
557
560
|
const parts = [];
|
|
558
561
|
parts.push(`export const ${funcName} = pgFunction('${func.name}', {`);
|
|
559
562
|
if (func.args.length > 0) {
|
|
560
|
-
const argsStr = func.args.map(arg => {
|
|
561
|
-
const argParts = [
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
563
|
+
const argsStr = func.args.map((arg, index) => {
|
|
564
|
+
const argParts = [];
|
|
565
|
+
const argName = arg.name || `$${index + 1}`;
|
|
566
|
+
argParts.push(`name: '${argName}'`);
|
|
567
|
+
argParts.push(`type: '${arg.type}'`);
|
|
568
|
+
if (arg.mode && arg.mode !== 'IN')
|
|
565
569
|
argParts.push(`mode: '${arg.mode}'`);
|
|
566
570
|
if (arg.default)
|
|
567
571
|
argParts.push(`default: '${(0, utils_1.escapeString)(arg.default)}'`);
|
|
@@ -571,8 +575,26 @@ function generateFunctionCode(func, useCamelCase) {
|
|
|
571
575
|
}
|
|
572
576
|
parts.push(` returns: '${func.returnType}',`);
|
|
573
577
|
parts.push(` language: '${func.language}',`);
|
|
574
|
-
|
|
575
|
-
|
|
578
|
+
let formattedBody = func.body;
|
|
579
|
+
const lang = func.language?.toLowerCase();
|
|
580
|
+
if (func.body && (lang === 'plpgsql' || lang === 'sql')) {
|
|
581
|
+
try {
|
|
582
|
+
formattedBody = (0, sql_formatter_1.format)(func.body, {
|
|
583
|
+
language: 'postgresql',
|
|
584
|
+
tabWidth: 4,
|
|
585
|
+
keywordCase: 'upper',
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
formattedBody = func.body;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const escapedBody = formattedBody.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
593
|
+
const indentedBody = escapedBody
|
|
594
|
+
.split('\n')
|
|
595
|
+
.map(line => ' ' + line)
|
|
596
|
+
.join('\n');
|
|
597
|
+
parts.push(` body: \`\n${indentedBody}\n \`,`);
|
|
576
598
|
if (func.volatility)
|
|
577
599
|
parts.push(` volatility: '${func.volatility}',`);
|
|
578
600
|
if (func.isStrict)
|
|
@@ -580,6 +602,9 @@ function generateFunctionCode(func, useCamelCase) {
|
|
|
580
602
|
if (func.securityDefiner)
|
|
581
603
|
parts.push(` securityDefiner: true,`);
|
|
582
604
|
parts.push(`})`);
|
|
605
|
+
if (func.trackingId) {
|
|
606
|
+
parts[parts.length - 1] = parts[parts.length - 1].replace('})', `}).$id('${func.trackingId}')`);
|
|
607
|
+
}
|
|
583
608
|
return parts.join('\n');
|
|
584
609
|
}
|
|
585
610
|
function generateViewCode(view, useCamelCase) {
|
|
@@ -1039,3 +1064,134 @@ function generateFKSQLComment(fk, camelCase) {
|
|
|
1039
1064
|
}
|
|
1040
1065
|
return parts.join(' | ');
|
|
1041
1066
|
}
|
|
1067
|
+
function resolveFunctionVarName(func, usedNames, useCamelCase) {
|
|
1068
|
+
const baseName = useCamelCase ? (0, utils_1.toCamelCase)(func.name) : func.name;
|
|
1069
|
+
const count = usedNames.get(baseName) || 0;
|
|
1070
|
+
usedNames.set(baseName, count + 1);
|
|
1071
|
+
if (count === 0) {
|
|
1072
|
+
return baseName;
|
|
1073
|
+
}
|
|
1074
|
+
return `${baseName}_${count + 1}`;
|
|
1075
|
+
}
|
|
1076
|
+
function generateFunctionsFile(schema, options = {}) {
|
|
1077
|
+
const { camelCase = true, importPath = 'relq/schema-builder', schemaImportPath = './schema', } = options;
|
|
1078
|
+
if (schema.functions.length === 0) {
|
|
1079
|
+
return null;
|
|
1080
|
+
}
|
|
1081
|
+
const parts = [];
|
|
1082
|
+
parts.push('/**');
|
|
1083
|
+
parts.push(' * Auto-generated by Relq CLI');
|
|
1084
|
+
parts.push(` * Generated at: ${new Date().toISOString()}`);
|
|
1085
|
+
parts.push(' * DO NOT EDIT - changes will be overwritten');
|
|
1086
|
+
parts.push(' */');
|
|
1087
|
+
parts.push('');
|
|
1088
|
+
parts.push(`import { pgFunction } from '${importPath}';`);
|
|
1089
|
+
void schemaImportPath;
|
|
1090
|
+
parts.push('');
|
|
1091
|
+
const usedNames = new Map();
|
|
1092
|
+
const functionVarNames = [];
|
|
1093
|
+
parts.push('// =============================================================================');
|
|
1094
|
+
parts.push('// FUNCTIONS');
|
|
1095
|
+
parts.push('// =============================================================================');
|
|
1096
|
+
parts.push('');
|
|
1097
|
+
for (const func of schema.functions) {
|
|
1098
|
+
const varName = resolveFunctionVarName(func, usedNames, camelCase);
|
|
1099
|
+
functionVarNames.push(varName);
|
|
1100
|
+
parts.push(generateFunctionCode(func, camelCase, varName));
|
|
1101
|
+
parts.push('');
|
|
1102
|
+
}
|
|
1103
|
+
parts.push('// =============================================================================');
|
|
1104
|
+
parts.push('// EXPORTS');
|
|
1105
|
+
parts.push('// =============================================================================');
|
|
1106
|
+
parts.push('');
|
|
1107
|
+
parts.push('export const functions = {');
|
|
1108
|
+
for (const name of functionVarNames) {
|
|
1109
|
+
parts.push(` ${name},`);
|
|
1110
|
+
}
|
|
1111
|
+
parts.push('} as const;');
|
|
1112
|
+
parts.push('');
|
|
1113
|
+
return parts.join('\n');
|
|
1114
|
+
}
|
|
1115
|
+
function generateTriggersFile(schema, options = {}) {
|
|
1116
|
+
const { camelCase = true, importPath = 'relq/schema-builder', schemaImportPath = './schema', functionsImportPath = './schema.functions', } = options;
|
|
1117
|
+
if (schema.triggers.length === 0) {
|
|
1118
|
+
return null;
|
|
1119
|
+
}
|
|
1120
|
+
const parts = [];
|
|
1121
|
+
parts.push('/**');
|
|
1122
|
+
parts.push(' * Auto-generated by Relq CLI');
|
|
1123
|
+
parts.push(` * Generated at: ${new Date().toISOString()}`);
|
|
1124
|
+
parts.push(' * DO NOT EDIT - changes will be overwritten');
|
|
1125
|
+
parts.push(' */');
|
|
1126
|
+
parts.push('');
|
|
1127
|
+
parts.push(`import { pgTrigger } from '${importPath}';`);
|
|
1128
|
+
parts.push(`import { schema } from '${schemaImportPath}';`);
|
|
1129
|
+
if (schema.functions.length > 0) {
|
|
1130
|
+
parts.push(`import { functions } from '${functionsImportPath}';`);
|
|
1131
|
+
}
|
|
1132
|
+
parts.push('');
|
|
1133
|
+
const functionNames = new Set();
|
|
1134
|
+
const usedFuncNames = new Map();
|
|
1135
|
+
for (const func of schema.functions) {
|
|
1136
|
+
const varName = resolveFunctionVarName(func, usedFuncNames, camelCase);
|
|
1137
|
+
functionNames.add(varName);
|
|
1138
|
+
}
|
|
1139
|
+
parts.push('// =============================================================================');
|
|
1140
|
+
parts.push('// TRIGGERS');
|
|
1141
|
+
parts.push('// =============================================================================');
|
|
1142
|
+
parts.push('');
|
|
1143
|
+
const triggerVarNames = [];
|
|
1144
|
+
for (const trigger of schema.triggers) {
|
|
1145
|
+
const triggerName = camelCase ? (0, utils_1.toCamelCase)(trigger.name) : trigger.name;
|
|
1146
|
+
const tableName = camelCase ? (0, utils_1.toCamelCase)(trigger.table) : trigger.table;
|
|
1147
|
+
triggerVarNames.push(triggerName);
|
|
1148
|
+
parts.push(`export const ${triggerName} = pgTrigger('${trigger.name}', {`);
|
|
1149
|
+
parts.push(` on: schema.${tableName},`);
|
|
1150
|
+
const events = trigger.events.length === 1
|
|
1151
|
+
? `'${trigger.events[0]}'`
|
|
1152
|
+
: `[${trigger.events.map(e => `'${e}'`).join(', ')}]`;
|
|
1153
|
+
if (trigger.timing === 'BEFORE') {
|
|
1154
|
+
parts.push(` before: ${events},`);
|
|
1155
|
+
}
|
|
1156
|
+
else if (trigger.timing === 'AFTER') {
|
|
1157
|
+
parts.push(` after: ${events},`);
|
|
1158
|
+
}
|
|
1159
|
+
else if (trigger.timing === 'INSTEAD OF') {
|
|
1160
|
+
parts.push(` insteadOf: ${events},`);
|
|
1161
|
+
}
|
|
1162
|
+
parts.push(` forEach: '${trigger.forEach || 'ROW'}',`);
|
|
1163
|
+
const funcVarName = camelCase ? (0, utils_1.toCamelCase)(trigger.functionName) : trigger.functionName;
|
|
1164
|
+
if (functionNames.has(funcVarName)) {
|
|
1165
|
+
parts.push(` execute: functions.${funcVarName},`);
|
|
1166
|
+
}
|
|
1167
|
+
else {
|
|
1168
|
+
parts.push(` execute: '${trigger.functionName}',`);
|
|
1169
|
+
}
|
|
1170
|
+
if (trigger.whenClause)
|
|
1171
|
+
parts.push(` when: '${(0, utils_1.escapeString)(trigger.whenClause)}',`);
|
|
1172
|
+
if (trigger.isConstraint)
|
|
1173
|
+
parts.push(` constraint: true,`);
|
|
1174
|
+
if (trigger.deferrable)
|
|
1175
|
+
parts.push(` deferrable: true,`);
|
|
1176
|
+
if (trigger.initiallyDeferred)
|
|
1177
|
+
parts.push(` initially: 'DEFERRED',`);
|
|
1178
|
+
if (trigger.trackingId) {
|
|
1179
|
+
parts.push(`}).$id('${trigger.trackingId}');`);
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
parts.push(`});`);
|
|
1183
|
+
}
|
|
1184
|
+
parts.push('');
|
|
1185
|
+
}
|
|
1186
|
+
parts.push('// =============================================================================');
|
|
1187
|
+
parts.push('// EXPORTS');
|
|
1188
|
+
parts.push('// =============================================================================');
|
|
1189
|
+
parts.push('');
|
|
1190
|
+
parts.push('export const triggers = {');
|
|
1191
|
+
for (const name of triggerVarNames) {
|
|
1192
|
+
parts.push(` ${name},`);
|
|
1193
|
+
}
|
|
1194
|
+
parts.push('} as const;');
|
|
1195
|
+
parts.push('');
|
|
1196
|
+
return parts.join('\n');
|
|
1197
|
+
}
|
|
@@ -708,7 +708,8 @@ async function introspectedToParsedSchema(schema) {
|
|
|
708
708
|
args: parseArgTypes(f.argTypes),
|
|
709
709
|
returnType: f.returnType,
|
|
710
710
|
language: f.language,
|
|
711
|
-
body: f.definition || '',
|
|
711
|
+
body: extractFunctionBody(f.definition || ''),
|
|
712
|
+
volatility: f.volatility,
|
|
712
713
|
isStrict: false,
|
|
713
714
|
securityDefiner: false,
|
|
714
715
|
});
|
|
@@ -719,7 +720,7 @@ async function introspectedToParsedSchema(schema) {
|
|
|
719
720
|
table: t.tableName,
|
|
720
721
|
timing: t.timing,
|
|
721
722
|
events: [t.event],
|
|
722
|
-
forEach: '
|
|
723
|
+
forEach: t.forEach || 'ROW',
|
|
723
724
|
functionName: t.functionName || '',
|
|
724
725
|
isConstraint: false,
|
|
725
726
|
});
|
|
@@ -809,6 +810,19 @@ function parseArgTypes(argTypes) {
|
|
|
809
810
|
type: typeof arg === 'string' ? arg.trim() : String(arg),
|
|
810
811
|
}));
|
|
811
812
|
}
|
|
813
|
+
function extractFunctionBody(definition) {
|
|
814
|
+
if (!definition)
|
|
815
|
+
return '';
|
|
816
|
+
const bodyMatch = definition.match(/AS\s+\$([a-zA-Z_]*)\$\s*([\s\S]*?)\s*\$\1\$/i);
|
|
817
|
+
if (bodyMatch) {
|
|
818
|
+
return bodyMatch[2].trim();
|
|
819
|
+
}
|
|
820
|
+
const singleQuoteMatch = definition.match(/AS\s+'([\s\S]*?)'\s*$/i);
|
|
821
|
+
if (singleQuoteMatch) {
|
|
822
|
+
return singleQuoteMatch[1].replace(/''/g, "'");
|
|
823
|
+
}
|
|
824
|
+
return '';
|
|
825
|
+
}
|
|
812
826
|
function normalizedToParsedSchema(schema) {
|
|
813
827
|
return {
|
|
814
828
|
extensions: (schema.extensions || []).map(e => typeof e === 'string' ? e : e.name),
|