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.
@@ -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
- else {
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 => generateColumnCode(col, useCamelCase, enumNames, domainNames, columnChecks.get(col.name)));
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 columnName = columnMatch[1];
229
+ const tsColumnName = columnMatch[1];
227
230
  const typeName = columnMatch[2];
228
- const tableRef = varName || tableName;
229
- const usage = `${tableRef}.${columnName}`;
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
- else {
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 => generateColumnCode(col, useCamelCase, enumNames, domainNames, columnChecks.get(col.name)));
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 columnName = columnMatch[1];
176
+ const tsColumnName = columnMatch[1];
178
177
  const typeName = columnMatch[2];
179
- const tableRef = varName || tableName;
180
- const usage = `${tableRef}.${columnName}`;
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.33",
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
  }