relq 1.0.33 → 1.0.35
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/pull.cjs +22 -1
- package/dist/cjs/cli/commands/push.cjs +22 -0
- package/dist/cjs/cli/utils/ast-codegen.cjs +16 -7
- package/dist/cjs/cli/utils/types-manager.cjs +178 -5
- package/dist/esm/cli/commands/pull.js +23 -2
- package/dist/esm/cli/commands/push.js +23 -1
- package/dist/esm/cli/utils/ast-codegen.js +16 -7
- package/dist/esm/cli/utils/types-manager.js +174 -5
- package/package.json +4 -2
|
@@ -255,6 +255,7 @@ async function pullCommand(context) {
|
|
|
255
255
|
{ id: 'extensions', label: 'extensions', status: 'pending', count: 0 },
|
|
256
256
|
...(includeFunctions ? [{ id: 'functions', label: 'functions', status: 'pending', count: 0 }] : []),
|
|
257
257
|
...(includeTriggers ? [{ id: 'triggers', label: 'triggers', status: 'pending', count: 0 }] : []),
|
|
258
|
+
{ id: 'types', label: 'types', status: 'pending', count: 0 },
|
|
258
259
|
{ id: 'collations', label: 'collations', status: 'pending', count: 0 },
|
|
259
260
|
{ id: 'foreign_servers', label: 'foreign servers', status: 'pending', count: 0 },
|
|
260
261
|
{ id: 'foreign_tables', label: 'foreign tables', status: 'pending', count: 0 },
|
|
@@ -268,6 +269,9 @@ async function pullCommand(context) {
|
|
|
268
269
|
progress.updateItem(update.step, { status: update.status, count: update.count });
|
|
269
270
|
},
|
|
270
271
|
});
|
|
272
|
+
progress.updateItem('types', { status: 'pending', count: 0 });
|
|
273
|
+
const typesForProgress = await (0, types_manager_1.getTypesFromDb)(connection);
|
|
274
|
+
progress.updateItem('types', { status: 'done', count: typesForProgress.length });
|
|
271
275
|
progress.complete();
|
|
272
276
|
console.log('');
|
|
273
277
|
const ignorePatterns = (0, relqignore_1.loadRelqignore)(projectRoot);
|
|
@@ -549,13 +553,21 @@ async function pullCommand(context) {
|
|
|
549
553
|
const localCommits = (0, repo_manager_1.getAllCommits)(projectRoot);
|
|
550
554
|
const localHashes = new Set(localCommits.map(c => c.hash));
|
|
551
555
|
const missingCommits = remoteCommits.filter(c => !localHashes.has(c.hash));
|
|
556
|
+
let hasSynced = false;
|
|
552
557
|
if (missingCommits.length > 0) {
|
|
553
558
|
for (const commit of missingCommits.reverse()) {
|
|
554
559
|
(0, repo_manager_1.saveCommit)(commit, projectRoot);
|
|
555
560
|
}
|
|
556
561
|
console.log(`Synced ${missingCommits.length} commit(s) from remote`);
|
|
562
|
+
hasSynced = true;
|
|
557
563
|
}
|
|
558
|
-
|
|
564
|
+
const typesFilePath = (0, types_manager_1.getTypesFilePath)(schemaPath);
|
|
565
|
+
const typesResult = await (0, types_manager_1.syncTypesFromDb)(connection, typesFilePath);
|
|
566
|
+
if (typesResult.generated && typesResult.typesCount > 0) {
|
|
567
|
+
console.log(`Synced ${typesResult.typesCount} type(s) from remote`);
|
|
568
|
+
hasSynced = true;
|
|
569
|
+
}
|
|
570
|
+
if (!hasSynced) {
|
|
559
571
|
console.log('Already up to date with remote');
|
|
560
572
|
}
|
|
561
573
|
console.log('');
|
|
@@ -674,11 +686,20 @@ async function pullCommand(context) {
|
|
|
674
686
|
spinner.start('Generating TypeScript schema...');
|
|
675
687
|
const parsedSchema = await (0, ast_transformer_1.introspectedToParsedSchema)(dbSchema);
|
|
676
688
|
(0, ast_codegen_1.assignTrackingIds)(parsedSchema);
|
|
689
|
+
const columnTypeMap = {};
|
|
690
|
+
for (const typeDef of typesForProgress) {
|
|
691
|
+
if (typeDef.usages) {
|
|
692
|
+
for (const usage of typeDef.usages) {
|
|
693
|
+
columnTypeMap[usage] = typeDef.name;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
677
697
|
const typescript = (0, ast_codegen_1.generateTypeScriptFromAST)(parsedSchema, {
|
|
678
698
|
camelCase: config.generate?.camelCase ?? true,
|
|
679
699
|
importPath: 'relq/schema-builder',
|
|
680
700
|
includeFunctions: false,
|
|
681
701
|
includeTriggers: false,
|
|
702
|
+
columnTypeMap,
|
|
682
703
|
});
|
|
683
704
|
spinner.succeed('Generated TypeScript schema');
|
|
684
705
|
const schemaDir = path.dirname(schemaPath);
|
|
@@ -151,6 +151,28 @@ async function pushCommand(context) {
|
|
|
151
151
|
const schemaPath = (0, config_loader_2.getSchemaPath)(config);
|
|
152
152
|
const typesFilePath = (0, types_manager_1.getTypesFilePath)(schemaPath);
|
|
153
153
|
let typesSynced = false;
|
|
154
|
+
if (fs.existsSync(schemaPath)) {
|
|
155
|
+
spinner.start('Validating types configuration...');
|
|
156
|
+
const validationErrors = await (0, types_manager_1.validateTypesConfiguration)(schemaPath, typesFilePath);
|
|
157
|
+
if (validationErrors.length > 0) {
|
|
158
|
+
spinner.fail('Types validation failed');
|
|
159
|
+
console.log('');
|
|
160
|
+
for (const err of validationErrors) {
|
|
161
|
+
console.log(`${cli_utils_1.colors.red('error:')} ${err.message}`);
|
|
162
|
+
if (err.details && err.details.length > 0) {
|
|
163
|
+
for (const detail of err.details) {
|
|
164
|
+
console.log(` ${cli_utils_1.colors.muted('→')} ${detail}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (err.hint) {
|
|
168
|
+
console.log(` ${cli_utils_1.colors.cyan('hint:')} ${err.hint}`);
|
|
169
|
+
}
|
|
170
|
+
console.log('');
|
|
171
|
+
}
|
|
172
|
+
(0, cli_utils_1.fatal)('Types configuration is invalid', `Fix the issues above and try again.\nTypes must be defined in ${cli_utils_1.colors.cyan(path.basename(typesFilePath))} to enable database syncing.`);
|
|
173
|
+
}
|
|
174
|
+
spinner.succeed('Types configuration valid');
|
|
175
|
+
}
|
|
154
176
|
if (fs.existsSync(typesFilePath) && !dryRun) {
|
|
155
177
|
spinner.start('Syncing TypeScript types...');
|
|
156
178
|
const typesResult = await (0, types_manager_1.syncTypesToDb)(connection, typesFilePath, schemaPath);
|
|
@@ -168,7 +168,7 @@ function getExplicitFKName(constraintName, tableName, columnName) {
|
|
|
168
168
|
}
|
|
169
169
|
return constraintName;
|
|
170
170
|
}
|
|
171
|
-
function generateColumnCode(col, useCamelCase, enumNames, domainNames, checkOverride) {
|
|
171
|
+
function generateColumnCode(col, useCamelCase, enumNames, domainNames, checkOverride, genericTypeName) {
|
|
172
172
|
const colName = useCamelCase ? (0, utils_1.toCamelCase)(col.name) : col.name;
|
|
173
173
|
const commentSuffix = col.comment ? `.comment('${(0, utils_1.escapeString)(col.comment)}')` : '';
|
|
174
174
|
let line;
|
|
@@ -183,15 +183,20 @@ function generateColumnCode(col, useCamelCase, enumNames, domainNames, checkOver
|
|
|
183
183
|
}
|
|
184
184
|
else {
|
|
185
185
|
let typeBuilder;
|
|
186
|
+
const isJsonType = normalizedType === 'json' || normalizedType === 'jsonb';
|
|
187
|
+
const genericSuffix = isJsonType && genericTypeName ? `<${genericTypeName}>` : '';
|
|
186
188
|
if (useCamelCase && colName !== col.name) {
|
|
187
189
|
const builderInfo = (0, type_map_1.getColumnBuilderWithInfo)(col.type, col.typeParams);
|
|
188
|
-
typeBuilder = `${builderInfo.builderName}('${col.name}')`;
|
|
190
|
+
typeBuilder = `${builderInfo.builderName}${genericSuffix}('${col.name}')`;
|
|
189
191
|
if (builderInfo.length != null) {
|
|
190
192
|
typeBuilder += `.length(${builderInfo.length})`;
|
|
191
193
|
}
|
|
192
194
|
}
|
|
193
195
|
else {
|
|
194
196
|
typeBuilder = (0, type_map_1.getColumnBuilder)(col.type, col.typeParams);
|
|
197
|
+
if (genericSuffix) {
|
|
198
|
+
typeBuilder = typeBuilder.replace(/(\w+)\(/, `$1${genericSuffix}(`);
|
|
199
|
+
}
|
|
195
200
|
}
|
|
196
201
|
if (col.isArray) {
|
|
197
202
|
typeBuilder = `${typeBuilder}.array()`;
|
|
@@ -447,7 +452,7 @@ function generatePartitionCode(child, partitionType, useCamelCase) {
|
|
|
447
452
|
needsSqlImport = true;
|
|
448
453
|
return ` partition('${childName}').in([sql\`${(0, utils_1.escapeString)(bound)}\`])`;
|
|
449
454
|
}
|
|
450
|
-
function generateTableCode(table, useCamelCase, enumNames, domainNames) {
|
|
455
|
+
function generateTableCode(table, useCamelCase, enumNames, domainNames, columnTypeMap = {}) {
|
|
451
456
|
const tableName = useCamelCase ? (0, utils_1.toCamelCase)(table.name) : table.name;
|
|
452
457
|
const parts = [];
|
|
453
458
|
const tableComment = table.comment ? ` comment: '${(0, utils_1.escapeString)(table.comment)}'` : null;
|
|
@@ -499,7 +504,11 @@ function generateTableCode(table, useCamelCase, enumNames, domainNames) {
|
|
|
499
504
|
isUnique: col.isUnique || uniqueColumns.has(col.name) || uniqueColumns.has(normalizedColName),
|
|
500
505
|
};
|
|
501
506
|
});
|
|
502
|
-
const columnLines = updatedColumns.map(col =>
|
|
507
|
+
const columnLines = updatedColumns.map(col => {
|
|
508
|
+
const columnKey = `${table.name}.${col.name}`;
|
|
509
|
+
const genericTypeName = columnTypeMap[columnKey];
|
|
510
|
+
return generateColumnCode(col, useCamelCase, enumNames, domainNames, columnChecks.get(col.name), genericTypeName);
|
|
511
|
+
});
|
|
503
512
|
const optionParts = [];
|
|
504
513
|
if (table.isPartitioned && table.partitionType && table.partitionKey?.length) {
|
|
505
514
|
const partCol = useCamelCase ? (0, utils_1.toCamelCase)(table.partitionKey[0]) : table.partitionKey[0];
|
|
@@ -715,7 +724,7 @@ function generateTriggerCode(trigger, useCamelCase, functionNames) {
|
|
|
715
724
|
return parts.join('\n');
|
|
716
725
|
}
|
|
717
726
|
function generateTypeScriptFromAST(schema, options = {}) {
|
|
718
|
-
const { camelCase = true, importPath = 'relq/schema-builder', includeEnums = true, includeDomains = true, includeTables = true, includeFunctions = true, includeTriggers = true, } = options;
|
|
727
|
+
const { camelCase = true, importPath = 'relq/schema-builder', includeEnums = true, includeDomains = true, includeTables = true, includeFunctions = true, includeTriggers = true, columnTypeMap = {}, } = options;
|
|
719
728
|
const parts = [];
|
|
720
729
|
parts.push('/**');
|
|
721
730
|
parts.push(' * Auto-generated by Relq CLI (AST-based)');
|
|
@@ -750,7 +759,7 @@ function generateTypeScriptFromAST(schema, options = {}) {
|
|
|
750
759
|
for (const table of sortedTables) {
|
|
751
760
|
if (table.partitionOf)
|
|
752
761
|
continue;
|
|
753
|
-
const code = generateTableCode(table, camelCase, enumNames, domainNames);
|
|
762
|
+
const code = generateTableCode(table, camelCase, enumNames, domainNames, columnTypeMap);
|
|
754
763
|
tableCodeParts.push(code);
|
|
755
764
|
for (const col of table.columns) {
|
|
756
765
|
if (enumNames.has(col.type.toLowerCase())) {
|
|
@@ -861,7 +870,7 @@ function generateTypeScriptFromAST(schema, options = {}) {
|
|
|
861
870
|
for (const table of sortedTables) {
|
|
862
871
|
if (table.partitionOf)
|
|
863
872
|
continue;
|
|
864
|
-
parts.push(generateTableCode(table, camelCase, enumNames, domainNames));
|
|
873
|
+
parts.push(generateTableCode(table, camelCase, enumNames, domainNames, columnTypeMap));
|
|
865
874
|
parts.push('');
|
|
866
875
|
}
|
|
867
876
|
}
|
|
@@ -43,6 +43,10 @@ exports.storeTypesInDb = storeTypesInDb;
|
|
|
43
43
|
exports.removeStaleTypes = removeStaleTypes;
|
|
44
44
|
exports.generateTypesFile = generateTypesFile;
|
|
45
45
|
exports.validateTypesUsage = validateTypesUsage;
|
|
46
|
+
exports.validateSchemaForInlineTypes = validateSchemaForInlineTypes;
|
|
47
|
+
exports.validateTypeImports = validateTypeImports;
|
|
48
|
+
exports.validateTypesFileSyntax = validateTypesFileSyntax;
|
|
49
|
+
exports.validateTypesConfiguration = validateTypesConfiguration;
|
|
46
50
|
exports.isValidTypesFilePath = isValidTypesFilePath;
|
|
47
51
|
exports.getTypesFilePath = getTypesFilePath;
|
|
48
52
|
exports.syncTypesToDb = syncTypesToDb;
|
|
@@ -207,7 +211,6 @@ function extractTypeUsages(schemaContent) {
|
|
|
207
211
|
const tablePattern = /(?:(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*)?defineTable\s*\(\s*['"]([^'"]+)['"]\s*,\s*\{/g;
|
|
208
212
|
let tableMatch;
|
|
209
213
|
while ((tableMatch = tablePattern.exec(schemaContent)) !== null) {
|
|
210
|
-
const varName = tableMatch[1];
|
|
211
214
|
const tableName = tableMatch[2];
|
|
212
215
|
const tableStartIdx = tableMatch.index + tableMatch[0].length;
|
|
213
216
|
let depth = 1;
|
|
@@ -220,13 +223,15 @@ function extractTypeUsages(schemaContent) {
|
|
|
220
223
|
columnsEndIdx = i;
|
|
221
224
|
}
|
|
222
225
|
const columnsBlock = schemaContent.substring(tableStartIdx, columnsEndIdx);
|
|
223
|
-
const columnPattern = /(\w+)\s*:\s*(?:jsonb|json|varchar|text|char)\s*<\s*([A-Z][a-zA-Z0-9_]*)/g;
|
|
226
|
+
const columnPattern = /(\w+)\s*:\s*(?:jsonb|json|varchar|text|char)\s*<\s*([A-Z][a-zA-Z0-9_]*)\s*>\s*\(([^)]*)\)/g;
|
|
224
227
|
let columnMatch;
|
|
225
228
|
while ((columnMatch = columnPattern.exec(columnsBlock)) !== null) {
|
|
226
|
-
const
|
|
229
|
+
const tsColumnName = columnMatch[1];
|
|
227
230
|
const typeName = columnMatch[2];
|
|
228
|
-
const
|
|
229
|
-
const
|
|
231
|
+
const argsBlock = columnMatch[3];
|
|
232
|
+
const explicitNameMatch = argsBlock.match(/^\s*['"]([^'"]+)['"]/);
|
|
233
|
+
const sqlColumnName = explicitNameMatch ? explicitNameMatch[1] : tsColumnName;
|
|
234
|
+
const usage = `${tableName}.${sqlColumnName}`;
|
|
230
235
|
if (!usages[typeName]) {
|
|
231
236
|
usages[typeName] = [];
|
|
232
237
|
}
|
|
@@ -360,6 +365,174 @@ function validateTypesUsage(usedTypes, definedTypes) {
|
|
|
360
365
|
missingTypes,
|
|
361
366
|
};
|
|
362
367
|
}
|
|
368
|
+
function validateSchemaForInlineTypes(schemaContent) {
|
|
369
|
+
const lines = schemaContent.split('\n');
|
|
370
|
+
const inlineTypes = [];
|
|
371
|
+
const excludePatterns = [
|
|
372
|
+
/RelqDatabaseSchema/,
|
|
373
|
+
/typeof\s+schema/,
|
|
374
|
+
/Infer</,
|
|
375
|
+
];
|
|
376
|
+
for (let i = 0; i < lines.length; i++) {
|
|
377
|
+
const line = lines[i];
|
|
378
|
+
const interfaceMatch = line.match(/^(?:export\s+)?interface\s+(\w+)\s*(?:<[^>]*>)?\s*\{/);
|
|
379
|
+
if (interfaceMatch) {
|
|
380
|
+
let hasBody = false;
|
|
381
|
+
for (let j = i + 1; j < lines.length && j < i + 50; j++) {
|
|
382
|
+
const bodyLine = lines[j].trim();
|
|
383
|
+
if (bodyLine === '}')
|
|
384
|
+
break;
|
|
385
|
+
if (bodyLine && !bodyLine.startsWith('//') && !bodyLine.startsWith('/*')) {
|
|
386
|
+
hasBody = true;
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (hasBody) {
|
|
391
|
+
inlineTypes.push(`interface ${interfaceMatch[1]} (line ${i + 1})`);
|
|
392
|
+
}
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
const typeMatch = line.match(/^(?:export\s+)?type\s+(\w+)(?:<[^>]*>)?\s*=\s*(.+)/);
|
|
396
|
+
if (typeMatch) {
|
|
397
|
+
const typeName = typeMatch[1];
|
|
398
|
+
const typeValue = typeMatch[2];
|
|
399
|
+
const isExcluded = excludePatterns.some(pattern => pattern.test(typeValue));
|
|
400
|
+
if (isExcluded) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const isObjectType = typeValue.trim().startsWith('{');
|
|
404
|
+
const isUnionType = typeValue.includes('|') && !typeValue.includes('<');
|
|
405
|
+
const isLiteralType = /^['"]/.test(typeValue.trim());
|
|
406
|
+
if (isObjectType || isUnionType || isLiteralType) {
|
|
407
|
+
inlineTypes.push(`type ${typeName} (line ${i + 1})`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (inlineTypes.length > 0) {
|
|
412
|
+
return {
|
|
413
|
+
type: 'inline_type',
|
|
414
|
+
message: 'Type definitions found in schema file',
|
|
415
|
+
hint: 'Move type definitions to schema.types.ts to enable database syncing and team collaboration.',
|
|
416
|
+
details: inlineTypes,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
function validateTypeImports(schemaContent, schemaPath) {
|
|
422
|
+
const typesFileName = path.basename(getTypesFilePath(schemaPath));
|
|
423
|
+
const wrongImports = [];
|
|
424
|
+
const importPattern = /import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g;
|
|
425
|
+
let match;
|
|
426
|
+
while ((match = importPattern.exec(schemaContent)) !== null) {
|
|
427
|
+
const imports = match[1];
|
|
428
|
+
const source = match[2];
|
|
429
|
+
if (source.endsWith('.types') || source.includes(typesFileName.replace('.ts', ''))) {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (source.startsWith('relq') || source.startsWith('@') || !source.startsWith('.')) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const importedItems = imports.split(',').map(s => s.trim());
|
|
436
|
+
const typeImports = importedItems.filter(item => {
|
|
437
|
+
const name = item.split(' as ')[0].trim();
|
|
438
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(name);
|
|
439
|
+
});
|
|
440
|
+
if (typeImports.length > 0) {
|
|
441
|
+
wrongImports.push(`${typeImports.join(', ')} from '${source}'`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (wrongImports.length > 0) {
|
|
445
|
+
return {
|
|
446
|
+
type: 'wrong_import',
|
|
447
|
+
message: 'Types imported from incorrect location',
|
|
448
|
+
hint: `Database-synced types must be defined in ${typesFileName}. Move these type definitions there and update the import.`,
|
|
449
|
+
details: wrongImports,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
async function validateTypesFileSyntax(typesFilePath) {
|
|
455
|
+
if (!fs.existsSync(typesFilePath)) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
const content = fs.readFileSync(typesFilePath, 'utf-8');
|
|
459
|
+
const errors = [];
|
|
460
|
+
let braceCount = 0;
|
|
461
|
+
let inInterface = false;
|
|
462
|
+
const lines = content.split('\n');
|
|
463
|
+
for (let i = 0; i < lines.length; i++) {
|
|
464
|
+
const line = lines[i];
|
|
465
|
+
if (/^(?:export\s+)?interface\s+\w+/.test(line)) {
|
|
466
|
+
inInterface = true;
|
|
467
|
+
}
|
|
468
|
+
for (const char of line) {
|
|
469
|
+
if (char === '{')
|
|
470
|
+
braceCount++;
|
|
471
|
+
else if (char === '}')
|
|
472
|
+
braceCount--;
|
|
473
|
+
}
|
|
474
|
+
if (inInterface && braceCount === 0) {
|
|
475
|
+
inInterface = false;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (braceCount !== 0) {
|
|
479
|
+
errors.push('Unbalanced braces detected - check for missing { or }');
|
|
480
|
+
}
|
|
481
|
+
const invalidPatterns = [
|
|
482
|
+
{ pattern: /interface\s+\d/, message: 'Interface name cannot start with a number' },
|
|
483
|
+
{ pattern: /type\s+\d/, message: 'Type name cannot start with a number' },
|
|
484
|
+
{ pattern: /:\s*;/, message: 'Missing type annotation before semicolon' },
|
|
485
|
+
];
|
|
486
|
+
for (const { pattern, message } of invalidPatterns) {
|
|
487
|
+
if (pattern.test(content)) {
|
|
488
|
+
errors.push(message);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (errors.length > 0) {
|
|
492
|
+
return {
|
|
493
|
+
type: 'typescript_error',
|
|
494
|
+
message: 'TypeScript syntax errors in types file',
|
|
495
|
+
hint: 'Fix the syntax errors in your types file before pushing.',
|
|
496
|
+
details: errors,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
async function validateTypesConfiguration(schemaPath, typesFilePath) {
|
|
502
|
+
const errors = [];
|
|
503
|
+
if (!fs.existsSync(schemaPath)) {
|
|
504
|
+
return errors;
|
|
505
|
+
}
|
|
506
|
+
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
|
|
507
|
+
const inlineError = validateSchemaForInlineTypes(schemaContent);
|
|
508
|
+
if (inlineError) {
|
|
509
|
+
errors.push(inlineError);
|
|
510
|
+
}
|
|
511
|
+
const importError = validateTypeImports(schemaContent, schemaPath);
|
|
512
|
+
if (importError) {
|
|
513
|
+
errors.push(importError);
|
|
514
|
+
}
|
|
515
|
+
const syntaxError = await validateTypesFileSyntax(typesFilePath);
|
|
516
|
+
if (syntaxError) {
|
|
517
|
+
errors.push(syntaxError);
|
|
518
|
+
}
|
|
519
|
+
if (fs.existsSync(typesFilePath)) {
|
|
520
|
+
const typesContent = fs.readFileSync(typesFilePath, 'utf-8');
|
|
521
|
+
const usedTypes = extractUsedTypes(schemaContent);
|
|
522
|
+
const parsedTypes = parseTypesFile(typesContent);
|
|
523
|
+
const definedTypeNames = parsedTypes.map(t => t.name);
|
|
524
|
+
const usageValidation = validateTypesUsage(usedTypes, definedTypeNames);
|
|
525
|
+
if (!usageValidation.valid) {
|
|
526
|
+
errors.push({
|
|
527
|
+
type: 'missing_type',
|
|
528
|
+
message: 'Types used in schema but not defined in types file',
|
|
529
|
+
hint: `Add these type definitions to ${path.basename(typesFilePath)}.`,
|
|
530
|
+
details: usageValidation.missingTypes,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return errors;
|
|
535
|
+
}
|
|
363
536
|
function isValidTypesFilePath(filePath) {
|
|
364
537
|
const basename = path.basename(filePath);
|
|
365
538
|
return basename.endsWith('.types.ts');
|
|
@@ -4,7 +4,7 @@ import { requireValidConfig, getSchemaPath } from "../utils/config-loader.js";
|
|
|
4
4
|
import { fastIntrospectDatabase } from "../utils/fast-introspect.js";
|
|
5
5
|
import { introspectedToParsedSchema } from "../utils/ast-transformer.js";
|
|
6
6
|
import { generateTypeScriptFromAST, assignTrackingIds, copyTrackingIdsToNormalized, generateFunctionsFile, generateTriggersFile } from "../utils/ast-codegen.js";
|
|
7
|
-
import { syncTypesFromDb, getTypesFilePath } from "../utils/types-manager.js";
|
|
7
|
+
import { syncTypesFromDb, getTypesFilePath, getTypesFromDb } from "../utils/types-manager.js";
|
|
8
8
|
import { getConnectionDescription } from "../utils/env-loader.js";
|
|
9
9
|
import { createSpinner, colors, formatBytes, formatDuration, fatal, confirm, warning, createMultiProgress } from "../utils/cli-utils.js";
|
|
10
10
|
import { loadRelqignore, isTableIgnored, isColumnIgnored, isIndexIgnored, isConstraintIgnored, isEnumIgnored, isDomainIgnored, isCompositeTypeIgnored, isFunctionIgnored, } from "../utils/relqignore.js";
|
|
@@ -219,6 +219,7 @@ export async function pullCommand(context) {
|
|
|
219
219
|
{ id: 'extensions', label: 'extensions', status: 'pending', count: 0 },
|
|
220
220
|
...(includeFunctions ? [{ id: 'functions', label: 'functions', status: 'pending', count: 0 }] : []),
|
|
221
221
|
...(includeTriggers ? [{ id: 'triggers', label: 'triggers', status: 'pending', count: 0 }] : []),
|
|
222
|
+
{ id: 'types', label: 'types', status: 'pending', count: 0 },
|
|
222
223
|
{ id: 'collations', label: 'collations', status: 'pending', count: 0 },
|
|
223
224
|
{ id: 'foreign_servers', label: 'foreign servers', status: 'pending', count: 0 },
|
|
224
225
|
{ id: 'foreign_tables', label: 'foreign tables', status: 'pending', count: 0 },
|
|
@@ -232,6 +233,9 @@ export async function pullCommand(context) {
|
|
|
232
233
|
progress.updateItem(update.step, { status: update.status, count: update.count });
|
|
233
234
|
},
|
|
234
235
|
});
|
|
236
|
+
progress.updateItem('types', { status: 'pending', count: 0 });
|
|
237
|
+
const typesForProgress = await getTypesFromDb(connection);
|
|
238
|
+
progress.updateItem('types', { status: 'done', count: typesForProgress.length });
|
|
235
239
|
progress.complete();
|
|
236
240
|
console.log('');
|
|
237
241
|
const ignorePatterns = loadRelqignore(projectRoot);
|
|
@@ -513,13 +517,21 @@ export async function pullCommand(context) {
|
|
|
513
517
|
const localCommits = getAllCommits(projectRoot);
|
|
514
518
|
const localHashes = new Set(localCommits.map(c => c.hash));
|
|
515
519
|
const missingCommits = remoteCommits.filter(c => !localHashes.has(c.hash));
|
|
520
|
+
let hasSynced = false;
|
|
516
521
|
if (missingCommits.length > 0) {
|
|
517
522
|
for (const commit of missingCommits.reverse()) {
|
|
518
523
|
saveCommit(commit, projectRoot);
|
|
519
524
|
}
|
|
520
525
|
console.log(`Synced ${missingCommits.length} commit(s) from remote`);
|
|
526
|
+
hasSynced = true;
|
|
521
527
|
}
|
|
522
|
-
|
|
528
|
+
const typesFilePath = getTypesFilePath(schemaPath);
|
|
529
|
+
const typesResult = await syncTypesFromDb(connection, typesFilePath);
|
|
530
|
+
if (typesResult.generated && typesResult.typesCount > 0) {
|
|
531
|
+
console.log(`Synced ${typesResult.typesCount} type(s) from remote`);
|
|
532
|
+
hasSynced = true;
|
|
533
|
+
}
|
|
534
|
+
if (!hasSynced) {
|
|
523
535
|
console.log('Already up to date with remote');
|
|
524
536
|
}
|
|
525
537
|
console.log('');
|
|
@@ -638,11 +650,20 @@ export async function pullCommand(context) {
|
|
|
638
650
|
spinner.start('Generating TypeScript schema...');
|
|
639
651
|
const parsedSchema = await introspectedToParsedSchema(dbSchema);
|
|
640
652
|
assignTrackingIds(parsedSchema);
|
|
653
|
+
const columnTypeMap = {};
|
|
654
|
+
for (const typeDef of typesForProgress) {
|
|
655
|
+
if (typeDef.usages) {
|
|
656
|
+
for (const usage of typeDef.usages) {
|
|
657
|
+
columnTypeMap[usage] = typeDef.name;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
641
661
|
const typescript = generateTypeScriptFromAST(parsedSchema, {
|
|
642
662
|
camelCase: config.generate?.camelCase ?? true,
|
|
643
663
|
importPath: 'relq/schema-builder',
|
|
644
664
|
includeFunctions: false,
|
|
645
665
|
includeTriggers: false,
|
|
666
|
+
columnTypeMap,
|
|
646
667
|
});
|
|
647
668
|
spinner.succeed('Generated TypeScript schema');
|
|
648
669
|
const schemaDir = path.dirname(schemaPath);
|
|
@@ -6,7 +6,7 @@ import { colors, createSpinner, fatal, confirm, warning } from "../utils/cli-uti
|
|
|
6
6
|
import { fastIntrospectDatabase } from "../utils/fast-introspect.js";
|
|
7
7
|
import { loadRelqignore, isTableIgnored, isColumnIgnored, isEnumIgnored, isDomainIgnored, isFunctionIgnored, } from "../utils/relqignore.js";
|
|
8
8
|
import { isInitialized, getHead, shortHash, fetchRemoteCommits, pushCommit, ensureRemoteTable, getAllCommits, loadSnapshot, isCommitSyncedWith, markCommitAsPushed, markCommitAsApplied, getConnectionLabel, } from "../utils/repo-manager.js";
|
|
9
|
-
import { syncTypesToDb, getTypesFilePath } from "../utils/types-manager.js";
|
|
9
|
+
import { syncTypesToDb, getTypesFilePath, validateTypesConfiguration } from "../utils/types-manager.js";
|
|
10
10
|
import { getSchemaPath } from "../utils/config-loader.js";
|
|
11
11
|
export async function pushCommand(context) {
|
|
12
12
|
const { config, flags } = context;
|
|
@@ -115,6 +115,28 @@ export async function pushCommand(context) {
|
|
|
115
115
|
const schemaPath = getSchemaPath(config);
|
|
116
116
|
const typesFilePath = getTypesFilePath(schemaPath);
|
|
117
117
|
let typesSynced = false;
|
|
118
|
+
if (fs.existsSync(schemaPath)) {
|
|
119
|
+
spinner.start('Validating types configuration...');
|
|
120
|
+
const validationErrors = await validateTypesConfiguration(schemaPath, typesFilePath);
|
|
121
|
+
if (validationErrors.length > 0) {
|
|
122
|
+
spinner.fail('Types validation failed');
|
|
123
|
+
console.log('');
|
|
124
|
+
for (const err of validationErrors) {
|
|
125
|
+
console.log(`${colors.red('error:')} ${err.message}`);
|
|
126
|
+
if (err.details && err.details.length > 0) {
|
|
127
|
+
for (const detail of err.details) {
|
|
128
|
+
console.log(` ${colors.muted('→')} ${detail}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (err.hint) {
|
|
132
|
+
console.log(` ${colors.cyan('hint:')} ${err.hint}`);
|
|
133
|
+
}
|
|
134
|
+
console.log('');
|
|
135
|
+
}
|
|
136
|
+
fatal('Types configuration is invalid', `Fix the issues above and try again.\nTypes must be defined in ${colors.cyan(path.basename(typesFilePath))} to enable database syncing.`);
|
|
137
|
+
}
|
|
138
|
+
spinner.succeed('Types configuration valid');
|
|
139
|
+
}
|
|
118
140
|
if (fs.existsSync(typesFilePath) && !dryRun) {
|
|
119
141
|
spinner.start('Syncing TypeScript types...');
|
|
120
142
|
const typesResult = await syncTypesToDb(connection, typesFilePath, schemaPath);
|
|
@@ -160,7 +160,7 @@ function getExplicitFKName(constraintName, tableName, columnName) {
|
|
|
160
160
|
}
|
|
161
161
|
return constraintName;
|
|
162
162
|
}
|
|
163
|
-
function generateColumnCode(col, useCamelCase, enumNames, domainNames, checkOverride) {
|
|
163
|
+
function generateColumnCode(col, useCamelCase, enumNames, domainNames, checkOverride, genericTypeName) {
|
|
164
164
|
const colName = useCamelCase ? toCamelCase(col.name) : col.name;
|
|
165
165
|
const commentSuffix = col.comment ? `.comment('${escapeString(col.comment)}')` : '';
|
|
166
166
|
let line;
|
|
@@ -175,15 +175,20 @@ function generateColumnCode(col, useCamelCase, enumNames, domainNames, checkOver
|
|
|
175
175
|
}
|
|
176
176
|
else {
|
|
177
177
|
let typeBuilder;
|
|
178
|
+
const isJsonType = normalizedType === 'json' || normalizedType === 'jsonb';
|
|
179
|
+
const genericSuffix = isJsonType && genericTypeName ? `<${genericTypeName}>` : '';
|
|
178
180
|
if (useCamelCase && colName !== col.name) {
|
|
179
181
|
const builderInfo = getColumnBuilderWithInfo(col.type, col.typeParams);
|
|
180
|
-
typeBuilder = `${builderInfo.builderName}('${col.name}')`;
|
|
182
|
+
typeBuilder = `${builderInfo.builderName}${genericSuffix}('${col.name}')`;
|
|
181
183
|
if (builderInfo.length != null) {
|
|
182
184
|
typeBuilder += `.length(${builderInfo.length})`;
|
|
183
185
|
}
|
|
184
186
|
}
|
|
185
187
|
else {
|
|
186
188
|
typeBuilder = getColumnBuilder(col.type, col.typeParams);
|
|
189
|
+
if (genericSuffix) {
|
|
190
|
+
typeBuilder = typeBuilder.replace(/(\w+)\(/, `$1${genericSuffix}(`);
|
|
191
|
+
}
|
|
187
192
|
}
|
|
188
193
|
if (col.isArray) {
|
|
189
194
|
typeBuilder = `${typeBuilder}.array()`;
|
|
@@ -439,7 +444,7 @@ function generatePartitionCode(child, partitionType, useCamelCase) {
|
|
|
439
444
|
needsSqlImport = true;
|
|
440
445
|
return ` partition('${childName}').in([sql\`${escapeString(bound)}\`])`;
|
|
441
446
|
}
|
|
442
|
-
function generateTableCode(table, useCamelCase, enumNames, domainNames) {
|
|
447
|
+
function generateTableCode(table, useCamelCase, enumNames, domainNames, columnTypeMap = {}) {
|
|
443
448
|
const tableName = useCamelCase ? toCamelCase(table.name) : table.name;
|
|
444
449
|
const parts = [];
|
|
445
450
|
const tableComment = table.comment ? ` comment: '${escapeString(table.comment)}'` : null;
|
|
@@ -491,7 +496,11 @@ function generateTableCode(table, useCamelCase, enumNames, domainNames) {
|
|
|
491
496
|
isUnique: col.isUnique || uniqueColumns.has(col.name) || uniqueColumns.has(normalizedColName),
|
|
492
497
|
};
|
|
493
498
|
});
|
|
494
|
-
const columnLines = updatedColumns.map(col =>
|
|
499
|
+
const columnLines = updatedColumns.map(col => {
|
|
500
|
+
const columnKey = `${table.name}.${col.name}`;
|
|
501
|
+
const genericTypeName = columnTypeMap[columnKey];
|
|
502
|
+
return generateColumnCode(col, useCamelCase, enumNames, domainNames, columnChecks.get(col.name), genericTypeName);
|
|
503
|
+
});
|
|
495
504
|
const optionParts = [];
|
|
496
505
|
if (table.isPartitioned && table.partitionType && table.partitionKey?.length) {
|
|
497
506
|
const partCol = useCamelCase ? toCamelCase(table.partitionKey[0]) : table.partitionKey[0];
|
|
@@ -707,7 +716,7 @@ function generateTriggerCode(trigger, useCamelCase, functionNames) {
|
|
|
707
716
|
return parts.join('\n');
|
|
708
717
|
}
|
|
709
718
|
export function generateTypeScriptFromAST(schema, options = {}) {
|
|
710
|
-
const { camelCase = true, importPath = 'relq/schema-builder', includeEnums = true, includeDomains = true, includeTables = true, includeFunctions = true, includeTriggers = true, } = options;
|
|
719
|
+
const { camelCase = true, importPath = 'relq/schema-builder', includeEnums = true, includeDomains = true, includeTables = true, includeFunctions = true, includeTriggers = true, columnTypeMap = {}, } = options;
|
|
711
720
|
const parts = [];
|
|
712
721
|
parts.push('/**');
|
|
713
722
|
parts.push(' * Auto-generated by Relq CLI (AST-based)');
|
|
@@ -742,7 +751,7 @@ export function generateTypeScriptFromAST(schema, options = {}) {
|
|
|
742
751
|
for (const table of sortedTables) {
|
|
743
752
|
if (table.partitionOf)
|
|
744
753
|
continue;
|
|
745
|
-
const code = generateTableCode(table, camelCase, enumNames, domainNames);
|
|
754
|
+
const code = generateTableCode(table, camelCase, enumNames, domainNames, columnTypeMap);
|
|
746
755
|
tableCodeParts.push(code);
|
|
747
756
|
for (const col of table.columns) {
|
|
748
757
|
if (enumNames.has(col.type.toLowerCase())) {
|
|
@@ -853,7 +862,7 @@ export function generateTypeScriptFromAST(schema, options = {}) {
|
|
|
853
862
|
for (const table of sortedTables) {
|
|
854
863
|
if (table.partitionOf)
|
|
855
864
|
continue;
|
|
856
|
-
parts.push(generateTableCode(table, camelCase, enumNames, domainNames));
|
|
865
|
+
parts.push(generateTableCode(table, camelCase, enumNames, domainNames, columnTypeMap));
|
|
857
866
|
parts.push('');
|
|
858
867
|
}
|
|
859
868
|
}
|
|
@@ -158,7 +158,6 @@ export function extractTypeUsages(schemaContent) {
|
|
|
158
158
|
const tablePattern = /(?:(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*)?defineTable\s*\(\s*['"]([^'"]+)['"]\s*,\s*\{/g;
|
|
159
159
|
let tableMatch;
|
|
160
160
|
while ((tableMatch = tablePattern.exec(schemaContent)) !== null) {
|
|
161
|
-
const varName = tableMatch[1];
|
|
162
161
|
const tableName = tableMatch[2];
|
|
163
162
|
const tableStartIdx = tableMatch.index + tableMatch[0].length;
|
|
164
163
|
let depth = 1;
|
|
@@ -171,13 +170,15 @@ export function extractTypeUsages(schemaContent) {
|
|
|
171
170
|
columnsEndIdx = i;
|
|
172
171
|
}
|
|
173
172
|
const columnsBlock = schemaContent.substring(tableStartIdx, columnsEndIdx);
|
|
174
|
-
const columnPattern = /(\w+)\s*:\s*(?:jsonb|json|varchar|text|char)\s*<\s*([A-Z][a-zA-Z0-9_]*)/g;
|
|
173
|
+
const columnPattern = /(\w+)\s*:\s*(?:jsonb|json|varchar|text|char)\s*<\s*([A-Z][a-zA-Z0-9_]*)\s*>\s*\(([^)]*)\)/g;
|
|
175
174
|
let columnMatch;
|
|
176
175
|
while ((columnMatch = columnPattern.exec(columnsBlock)) !== null) {
|
|
177
|
-
const
|
|
176
|
+
const tsColumnName = columnMatch[1];
|
|
178
177
|
const typeName = columnMatch[2];
|
|
179
|
-
const
|
|
180
|
-
const
|
|
178
|
+
const argsBlock = columnMatch[3];
|
|
179
|
+
const explicitNameMatch = argsBlock.match(/^\s*['"]([^'"]+)['"]/);
|
|
180
|
+
const sqlColumnName = explicitNameMatch ? explicitNameMatch[1] : tsColumnName;
|
|
181
|
+
const usage = `${tableName}.${sqlColumnName}`;
|
|
181
182
|
if (!usages[typeName]) {
|
|
182
183
|
usages[typeName] = [];
|
|
183
184
|
}
|
|
@@ -311,6 +312,174 @@ export function validateTypesUsage(usedTypes, definedTypes) {
|
|
|
311
312
|
missingTypes,
|
|
312
313
|
};
|
|
313
314
|
}
|
|
315
|
+
export function validateSchemaForInlineTypes(schemaContent) {
|
|
316
|
+
const lines = schemaContent.split('\n');
|
|
317
|
+
const inlineTypes = [];
|
|
318
|
+
const excludePatterns = [
|
|
319
|
+
/RelqDatabaseSchema/,
|
|
320
|
+
/typeof\s+schema/,
|
|
321
|
+
/Infer</,
|
|
322
|
+
];
|
|
323
|
+
for (let i = 0; i < lines.length; i++) {
|
|
324
|
+
const line = lines[i];
|
|
325
|
+
const interfaceMatch = line.match(/^(?:export\s+)?interface\s+(\w+)\s*(?:<[^>]*>)?\s*\{/);
|
|
326
|
+
if (interfaceMatch) {
|
|
327
|
+
let hasBody = false;
|
|
328
|
+
for (let j = i + 1; j < lines.length && j < i + 50; j++) {
|
|
329
|
+
const bodyLine = lines[j].trim();
|
|
330
|
+
if (bodyLine === '}')
|
|
331
|
+
break;
|
|
332
|
+
if (bodyLine && !bodyLine.startsWith('//') && !bodyLine.startsWith('/*')) {
|
|
333
|
+
hasBody = true;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (hasBody) {
|
|
338
|
+
inlineTypes.push(`interface ${interfaceMatch[1]} (line ${i + 1})`);
|
|
339
|
+
}
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const typeMatch = line.match(/^(?:export\s+)?type\s+(\w+)(?:<[^>]*>)?\s*=\s*(.+)/);
|
|
343
|
+
if (typeMatch) {
|
|
344
|
+
const typeName = typeMatch[1];
|
|
345
|
+
const typeValue = typeMatch[2];
|
|
346
|
+
const isExcluded = excludePatterns.some(pattern => pattern.test(typeValue));
|
|
347
|
+
if (isExcluded) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const isObjectType = typeValue.trim().startsWith('{');
|
|
351
|
+
const isUnionType = typeValue.includes('|') && !typeValue.includes('<');
|
|
352
|
+
const isLiteralType = /^['"]/.test(typeValue.trim());
|
|
353
|
+
if (isObjectType || isUnionType || isLiteralType) {
|
|
354
|
+
inlineTypes.push(`type ${typeName} (line ${i + 1})`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (inlineTypes.length > 0) {
|
|
359
|
+
return {
|
|
360
|
+
type: 'inline_type',
|
|
361
|
+
message: 'Type definitions found in schema file',
|
|
362
|
+
hint: 'Move type definitions to schema.types.ts to enable database syncing and team collaboration.',
|
|
363
|
+
details: inlineTypes,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
export function validateTypeImports(schemaContent, schemaPath) {
|
|
369
|
+
const typesFileName = path.basename(getTypesFilePath(schemaPath));
|
|
370
|
+
const wrongImports = [];
|
|
371
|
+
const importPattern = /import\s+(?:type\s+)?\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g;
|
|
372
|
+
let match;
|
|
373
|
+
while ((match = importPattern.exec(schemaContent)) !== null) {
|
|
374
|
+
const imports = match[1];
|
|
375
|
+
const source = match[2];
|
|
376
|
+
if (source.endsWith('.types') || source.includes(typesFileName.replace('.ts', ''))) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (source.startsWith('relq') || source.startsWith('@') || !source.startsWith('.')) {
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const importedItems = imports.split(',').map(s => s.trim());
|
|
383
|
+
const typeImports = importedItems.filter(item => {
|
|
384
|
+
const name = item.split(' as ')[0].trim();
|
|
385
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(name);
|
|
386
|
+
});
|
|
387
|
+
if (typeImports.length > 0) {
|
|
388
|
+
wrongImports.push(`${typeImports.join(', ')} from '${source}'`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (wrongImports.length > 0) {
|
|
392
|
+
return {
|
|
393
|
+
type: 'wrong_import',
|
|
394
|
+
message: 'Types imported from incorrect location',
|
|
395
|
+
hint: `Database-synced types must be defined in ${typesFileName}. Move these type definitions there and update the import.`,
|
|
396
|
+
details: wrongImports,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
export async function validateTypesFileSyntax(typesFilePath) {
|
|
402
|
+
if (!fs.existsSync(typesFilePath)) {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
const content = fs.readFileSync(typesFilePath, 'utf-8');
|
|
406
|
+
const errors = [];
|
|
407
|
+
let braceCount = 0;
|
|
408
|
+
let inInterface = false;
|
|
409
|
+
const lines = content.split('\n');
|
|
410
|
+
for (let i = 0; i < lines.length; i++) {
|
|
411
|
+
const line = lines[i];
|
|
412
|
+
if (/^(?:export\s+)?interface\s+\w+/.test(line)) {
|
|
413
|
+
inInterface = true;
|
|
414
|
+
}
|
|
415
|
+
for (const char of line) {
|
|
416
|
+
if (char === '{')
|
|
417
|
+
braceCount++;
|
|
418
|
+
else if (char === '}')
|
|
419
|
+
braceCount--;
|
|
420
|
+
}
|
|
421
|
+
if (inInterface && braceCount === 0) {
|
|
422
|
+
inInterface = false;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (braceCount !== 0) {
|
|
426
|
+
errors.push('Unbalanced braces detected - check for missing { or }');
|
|
427
|
+
}
|
|
428
|
+
const invalidPatterns = [
|
|
429
|
+
{ pattern: /interface\s+\d/, message: 'Interface name cannot start with a number' },
|
|
430
|
+
{ pattern: /type\s+\d/, message: 'Type name cannot start with a number' },
|
|
431
|
+
{ pattern: /:\s*;/, message: 'Missing type annotation before semicolon' },
|
|
432
|
+
];
|
|
433
|
+
for (const { pattern, message } of invalidPatterns) {
|
|
434
|
+
if (pattern.test(content)) {
|
|
435
|
+
errors.push(message);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (errors.length > 0) {
|
|
439
|
+
return {
|
|
440
|
+
type: 'typescript_error',
|
|
441
|
+
message: 'TypeScript syntax errors in types file',
|
|
442
|
+
hint: 'Fix the syntax errors in your types file before pushing.',
|
|
443
|
+
details: errors,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
export async function validateTypesConfiguration(schemaPath, typesFilePath) {
|
|
449
|
+
const errors = [];
|
|
450
|
+
if (!fs.existsSync(schemaPath)) {
|
|
451
|
+
return errors;
|
|
452
|
+
}
|
|
453
|
+
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
|
|
454
|
+
const inlineError = validateSchemaForInlineTypes(schemaContent);
|
|
455
|
+
if (inlineError) {
|
|
456
|
+
errors.push(inlineError);
|
|
457
|
+
}
|
|
458
|
+
const importError = validateTypeImports(schemaContent, schemaPath);
|
|
459
|
+
if (importError) {
|
|
460
|
+
errors.push(importError);
|
|
461
|
+
}
|
|
462
|
+
const syntaxError = await validateTypesFileSyntax(typesFilePath);
|
|
463
|
+
if (syntaxError) {
|
|
464
|
+
errors.push(syntaxError);
|
|
465
|
+
}
|
|
466
|
+
if (fs.existsSync(typesFilePath)) {
|
|
467
|
+
const typesContent = fs.readFileSync(typesFilePath, 'utf-8');
|
|
468
|
+
const usedTypes = extractUsedTypes(schemaContent);
|
|
469
|
+
const parsedTypes = parseTypesFile(typesContent);
|
|
470
|
+
const definedTypeNames = parsedTypes.map(t => t.name);
|
|
471
|
+
const usageValidation = validateTypesUsage(usedTypes, definedTypeNames);
|
|
472
|
+
if (!usageValidation.valid) {
|
|
473
|
+
errors.push({
|
|
474
|
+
type: 'missing_type',
|
|
475
|
+
message: 'Types used in schema but not defined in types file',
|
|
476
|
+
hint: `Add these type definitions to ${path.basename(typesFilePath)}.`,
|
|
477
|
+
details: usageValidation.missingTypes,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return errors;
|
|
482
|
+
}
|
|
314
483
|
export function isValidTypesFilePath(filePath) {
|
|
315
484
|
const basename = path.basename(filePath);
|
|
316
485
|
return basename.endsWith('.types.ts');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "relq",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.35",
|
|
4
4
|
"description": "The Fully-Typed PostgreSQL ORM for TypeScript",
|
|
5
5
|
"author": "Olajide Mathew O. <olajide.mathew@yuniq.solutions>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,5 +61,7 @@
|
|
|
61
61
|
"sql-formatter": "^15.6.12",
|
|
62
62
|
"strip-comments": "^2.0.1"
|
|
63
63
|
},
|
|
64
|
-
"devDependencies": {
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"typescript": "^5.9.3"
|
|
66
|
+
}
|
|
65
67
|
}
|