relq 1.0.34 → 1.0.36
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 +48 -0
- package/dist/cjs/cli/commands/push.cjs +22 -0
- package/dist/cjs/cli/utils/ast-codegen.cjs +21 -7
- package/dist/cjs/cli/utils/types-manager.cjs +178 -5
- package/dist/esm/cli/commands/pull.js +49 -1
- package/dist/esm/cli/commands/push.js +23 -1
- package/dist/esm/cli/utils/ast-codegen.js +21 -7
- package/dist/esm/cli/utils/types-manager.js +174 -5
- package/package.json +4 -2
|
@@ -258,6 +258,7 @@ async function pullCommand(context) {
|
|
|
258
258
|
{ id: 'collations', label: 'collations', status: 'pending', count: 0 },
|
|
259
259
|
{ id: 'foreign_servers', label: 'foreign servers', status: 'pending', count: 0 },
|
|
260
260
|
{ id: 'foreign_tables', label: 'foreign tables', status: 'pending', count: 0 },
|
|
261
|
+
{ id: 'types', label: 'types', status: 'pending', count: 0 },
|
|
261
262
|
];
|
|
262
263
|
progress.setItems(progressItems);
|
|
263
264
|
progress.start();
|
|
@@ -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: 'fetching', 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);
|
|
@@ -563,6 +567,38 @@ async function pullCommand(context) {
|
|
|
563
567
|
console.log(`Synced ${typesResult.typesCount} type(s) from remote`);
|
|
564
568
|
hasSynced = true;
|
|
565
569
|
}
|
|
570
|
+
if (typesForProgress.length > 0 && fs.existsSync(schemaPath)) {
|
|
571
|
+
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
|
|
572
|
+
const schemaBaseName = path.basename(schemaPath, '.ts');
|
|
573
|
+
const typesImportPattern = new RegExp(`from\\s+['"]\\./${schemaBaseName}\\.types['"]`);
|
|
574
|
+
const hasTypesImport = typesImportPattern.test(schemaContent);
|
|
575
|
+
const typesWithUsages = typesForProgress.filter(t => t.usages && t.usages.length > 0);
|
|
576
|
+
if (typesWithUsages.length > 0 && !hasTypesImport) {
|
|
577
|
+
console.log(`Applying ${typesWithUsages.length} type(s) to schema...`);
|
|
578
|
+
const columnTypeMap = {};
|
|
579
|
+
for (const typeDef of typesForProgress) {
|
|
580
|
+
if (typeDef.usages) {
|
|
581
|
+
for (const usage of typeDef.usages) {
|
|
582
|
+
columnTypeMap[usage] = typeDef.name;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const parsedSchema = await (0, ast_transformer_1.introspectedToParsedSchema)(dbSchema);
|
|
587
|
+
(0, ast_codegen_1.assignTrackingIds)(parsedSchema);
|
|
588
|
+
const typesImportPath = `./${schemaBaseName}.types`;
|
|
589
|
+
const typescript = (0, ast_codegen_1.generateTypeScriptFromAST)(parsedSchema, {
|
|
590
|
+
camelCase: config.generate?.camelCase ?? true,
|
|
591
|
+
importPath: 'relq/schema-builder',
|
|
592
|
+
includeFunctions: false,
|
|
593
|
+
includeTriggers: false,
|
|
594
|
+
columnTypeMap,
|
|
595
|
+
typesImportPath,
|
|
596
|
+
});
|
|
597
|
+
fs.writeFileSync(schemaPath, typescript, 'utf-8');
|
|
598
|
+
console.log(`Updated ${cli_utils_1.colors.cyan(schemaPath)} with type annotations`);
|
|
599
|
+
hasSynced = true;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
566
602
|
if (!hasSynced) {
|
|
567
603
|
console.log('Already up to date with remote');
|
|
568
604
|
}
|
|
@@ -682,11 +718,23 @@ async function pullCommand(context) {
|
|
|
682
718
|
spinner.start('Generating TypeScript schema...');
|
|
683
719
|
const parsedSchema = await (0, ast_transformer_1.introspectedToParsedSchema)(dbSchema);
|
|
684
720
|
(0, ast_codegen_1.assignTrackingIds)(parsedSchema);
|
|
721
|
+
const columnTypeMap = {};
|
|
722
|
+
for (const typeDef of typesForProgress) {
|
|
723
|
+
if (typeDef.usages) {
|
|
724
|
+
for (const usage of typeDef.usages) {
|
|
725
|
+
columnTypeMap[usage] = typeDef.name;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
const schemaBaseName = path.basename(schemaPath, '.ts');
|
|
730
|
+
const typesImportPath = `./${schemaBaseName}.types`;
|
|
685
731
|
const typescript = (0, ast_codegen_1.generateTypeScriptFromAST)(parsedSchema, {
|
|
686
732
|
camelCase: config.generate?.camelCase ?? true,
|
|
687
733
|
importPath: 'relq/schema-builder',
|
|
688
734
|
includeFunctions: false,
|
|
689
735
|
includeTriggers: false,
|
|
736
|
+
columnTypeMap,
|
|
737
|
+
typesImportPath,
|
|
690
738
|
});
|
|
691
739
|
spinner.succeed('Generated TypeScript schema');
|
|
692
740
|
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 = {}, typesImportPath, } = 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())) {
|
|
@@ -807,6 +816,11 @@ function generateTypeScriptFromAST(schema, options = {}) {
|
|
|
807
816
|
parts.push(` ${imports.join(',\n ')},`);
|
|
808
817
|
parts.push(` type RelqDatabaseSchema,`);
|
|
809
818
|
parts.push(`} from '${importPath}';`);
|
|
819
|
+
const usedTypeNames = new Set(Object.values(columnTypeMap));
|
|
820
|
+
if (usedTypeNames.size > 0 && typesImportPath) {
|
|
821
|
+
const typeNames = Array.from(usedTypeNames).sort();
|
|
822
|
+
parts.push(`import { ${typeNames.join(', ')} } from '${typesImportPath}';`);
|
|
823
|
+
}
|
|
810
824
|
parts.push('');
|
|
811
825
|
if (needsPgExtensions) {
|
|
812
826
|
parts.push('// =============================================================================');
|
|
@@ -861,7 +875,7 @@ function generateTypeScriptFromAST(schema, options = {}) {
|
|
|
861
875
|
for (const table of sortedTables) {
|
|
862
876
|
if (table.partitionOf)
|
|
863
877
|
continue;
|
|
864
|
-
parts.push(generateTableCode(table, camelCase, enumNames, domainNames));
|
|
878
|
+
parts.push(generateTableCode(table, camelCase, enumNames, domainNames, columnTypeMap));
|
|
865
879
|
parts.push('');
|
|
866
880
|
}
|
|
867
881
|
}
|
|
@@ -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";
|
|
@@ -222,6 +222,7 @@ export async function pullCommand(context) {
|
|
|
222
222
|
{ id: 'collations', label: 'collations', status: 'pending', count: 0 },
|
|
223
223
|
{ id: 'foreign_servers', label: 'foreign servers', status: 'pending', count: 0 },
|
|
224
224
|
{ id: 'foreign_tables', label: 'foreign tables', status: 'pending', count: 0 },
|
|
225
|
+
{ id: 'types', label: 'types', status: 'pending', count: 0 },
|
|
225
226
|
];
|
|
226
227
|
progress.setItems(progressItems);
|
|
227
228
|
progress.start();
|
|
@@ -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: 'fetching', 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);
|
|
@@ -527,6 +531,38 @@ export async function pullCommand(context) {
|
|
|
527
531
|
console.log(`Synced ${typesResult.typesCount} type(s) from remote`);
|
|
528
532
|
hasSynced = true;
|
|
529
533
|
}
|
|
534
|
+
if (typesForProgress.length > 0 && fs.existsSync(schemaPath)) {
|
|
535
|
+
const schemaContent = fs.readFileSync(schemaPath, 'utf-8');
|
|
536
|
+
const schemaBaseName = path.basename(schemaPath, '.ts');
|
|
537
|
+
const typesImportPattern = new RegExp(`from\\s+['"]\\./${schemaBaseName}\\.types['"]`);
|
|
538
|
+
const hasTypesImport = typesImportPattern.test(schemaContent);
|
|
539
|
+
const typesWithUsages = typesForProgress.filter(t => t.usages && t.usages.length > 0);
|
|
540
|
+
if (typesWithUsages.length > 0 && !hasTypesImport) {
|
|
541
|
+
console.log(`Applying ${typesWithUsages.length} type(s) to schema...`);
|
|
542
|
+
const columnTypeMap = {};
|
|
543
|
+
for (const typeDef of typesForProgress) {
|
|
544
|
+
if (typeDef.usages) {
|
|
545
|
+
for (const usage of typeDef.usages) {
|
|
546
|
+
columnTypeMap[usage] = typeDef.name;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const parsedSchema = await introspectedToParsedSchema(dbSchema);
|
|
551
|
+
assignTrackingIds(parsedSchema);
|
|
552
|
+
const typesImportPath = `./${schemaBaseName}.types`;
|
|
553
|
+
const typescript = generateTypeScriptFromAST(parsedSchema, {
|
|
554
|
+
camelCase: config.generate?.camelCase ?? true,
|
|
555
|
+
importPath: 'relq/schema-builder',
|
|
556
|
+
includeFunctions: false,
|
|
557
|
+
includeTriggers: false,
|
|
558
|
+
columnTypeMap,
|
|
559
|
+
typesImportPath,
|
|
560
|
+
});
|
|
561
|
+
fs.writeFileSync(schemaPath, typescript, 'utf-8');
|
|
562
|
+
console.log(`Updated ${colors.cyan(schemaPath)} with type annotations`);
|
|
563
|
+
hasSynced = true;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
530
566
|
if (!hasSynced) {
|
|
531
567
|
console.log('Already up to date with remote');
|
|
532
568
|
}
|
|
@@ -646,11 +682,23 @@ export async function pullCommand(context) {
|
|
|
646
682
|
spinner.start('Generating TypeScript schema...');
|
|
647
683
|
const parsedSchema = await introspectedToParsedSchema(dbSchema);
|
|
648
684
|
assignTrackingIds(parsedSchema);
|
|
685
|
+
const columnTypeMap = {};
|
|
686
|
+
for (const typeDef of typesForProgress) {
|
|
687
|
+
if (typeDef.usages) {
|
|
688
|
+
for (const usage of typeDef.usages) {
|
|
689
|
+
columnTypeMap[usage] = typeDef.name;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
const schemaBaseName = path.basename(schemaPath, '.ts');
|
|
694
|
+
const typesImportPath = `./${schemaBaseName}.types`;
|
|
649
695
|
const typescript = generateTypeScriptFromAST(parsedSchema, {
|
|
650
696
|
camelCase: config.generate?.camelCase ?? true,
|
|
651
697
|
importPath: 'relq/schema-builder',
|
|
652
698
|
includeFunctions: false,
|
|
653
699
|
includeTriggers: false,
|
|
700
|
+
columnTypeMap,
|
|
701
|
+
typesImportPath,
|
|
654
702
|
});
|
|
655
703
|
spinner.succeed('Generated TypeScript schema');
|
|
656
704
|
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 = {}, typesImportPath, } = 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())) {
|
|
@@ -799,6 +808,11 @@ export function generateTypeScriptFromAST(schema, options = {}) {
|
|
|
799
808
|
parts.push(` ${imports.join(',\n ')},`);
|
|
800
809
|
parts.push(` type RelqDatabaseSchema,`);
|
|
801
810
|
parts.push(`} from '${importPath}';`);
|
|
811
|
+
const usedTypeNames = new Set(Object.values(columnTypeMap));
|
|
812
|
+
if (usedTypeNames.size > 0 && typesImportPath) {
|
|
813
|
+
const typeNames = Array.from(usedTypeNames).sort();
|
|
814
|
+
parts.push(`import { ${typeNames.join(', ')} } from '${typesImportPath}';`);
|
|
815
|
+
}
|
|
802
816
|
parts.push('');
|
|
803
817
|
if (needsPgExtensions) {
|
|
804
818
|
parts.push('// =============================================================================');
|
|
@@ -853,7 +867,7 @@ export function generateTypeScriptFromAST(schema, options = {}) {
|
|
|
853
867
|
for (const table of sortedTables) {
|
|
854
868
|
if (table.partitionOf)
|
|
855
869
|
continue;
|
|
856
|
-
parts.push(generateTableCode(table, camelCase, enumNames, domainNames));
|
|
870
|
+
parts.push(generateTableCode(table, camelCase, enumNames, domainNames, columnTypeMap));
|
|
857
871
|
parts.push('');
|
|
858
872
|
}
|
|
859
873
|
}
|
|
@@ -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.36",
|
|
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
|
}
|