relq 1.0.8 → 1.0.10

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/bin/relq.js CHANGED
@@ -1,2 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import '../esm/cli/index.js';
2
+ import { main } from '../esm/cli/index.js';
3
+ main().catch(console.error);
@@ -346,9 +346,10 @@ var require_postgres_interval = __commonJS((exports2, module2) => {
346
346
 
347
347
  // node_modules/postgres-bytea/index.js
348
348
  var require_postgres_bytea = __commonJS((exports2, module2) => {
349
+ var bufferFrom = Buffer.from || Buffer;
349
350
  module2.exports = function parseBytea(input) {
350
351
  if (/^\\x/.test(input)) {
351
- return new Buffer(input.substr(2), "hex");
352
+ return bufferFrom(input.substr(2), "hex");
352
353
  }
353
354
  var output = "";
354
355
  var i = 0;
@@ -372,7 +373,7 @@ var require_postgres_bytea = __commonJS((exports2, module2) => {
372
373
  }
373
374
  }
374
375
  }
375
- return new Buffer(output, "binary");
376
+ return bufferFrom(output, "binary");
376
377
  };
377
378
  });
378
379
 
@@ -346,9 +346,10 @@ var require_postgres_interval = __commonJS((exports2, module2) => {
346
346
 
347
347
  // node_modules/postgres-bytea/index.js
348
348
  var require_postgres_bytea = __commonJS((exports2, module2) => {
349
+ var bufferFrom = Buffer.from || Buffer;
349
350
  module2.exports = function parseBytea(input) {
350
351
  if (/^\\x/.test(input)) {
351
- return new Buffer(input.substr(2), "hex");
352
+ return bufferFrom(input.substr(2), "hex");
352
353
  }
353
354
  var output = "";
354
355
  var i = 0;
@@ -372,7 +373,7 @@ var require_postgres_bytea = __commonJS((exports2, module2) => {
372
373
  }
373
374
  }
374
375
  }
375
- return new Buffer(output, "binary");
376
+ return bufferFrom(output, "binary");
376
377
  };
377
378
  });
378
379
 
@@ -47,6 +47,7 @@ const path = __importStar(require("path"));
47
47
  const repo_manager_1 = require("../utils/repo-manager.cjs");
48
48
  const change_tracker_1 = require("../utils/change-tracker.cjs");
49
49
  const schema_comparator_1 = require("../utils/schema-comparator.cjs");
50
+ const config_loader_1 = require("../utils/config-loader.cjs");
50
51
  function parseSchemaFileForComparison(schemaPath) {
51
52
  if (!fs.existsSync(schemaPath)) {
52
53
  return null;
@@ -623,8 +624,10 @@ async function addCommand(context) {
623
624
  }
624
625
  const ignorePatterns = (0, relqignore_1.loadRelqignore)(projectRoot);
625
626
  const config = await (0, config_1.loadConfig)();
626
- const schemaPathRaw = typeof config?.schema === 'string' ? config.schema : './db/schema.ts';
627
+ const schemaPathRaw = (0, config_loader_1.getSchemaPath)(config);
627
628
  const schemaPath = path.resolve(projectRoot, schemaPathRaw);
629
+ const { requireValidSchema } = await Promise.resolve().then(() => __importStar(require("../utils/config-loader.cjs")));
630
+ await requireValidSchema(schemaPath, context.flags);
628
631
  const injectedCount = injectTrackingIds(schemaPath);
629
632
  if (injectedCount > 0) {
630
633
  console.log(spinner_1.colors.muted(`Injected ${injectedCount} tracking ID(s) into schema.ts`));
@@ -38,6 +38,7 @@ const crypto = __importStar(require("crypto"));
38
38
  const cli_utils_1 = require("../utils/cli-utils.cjs");
39
39
  const repo_manager_1 = require("../utils/repo-manager.cjs");
40
40
  const config_1 = require("../../config/config.cjs");
41
+ const config_loader_1 = require("../utils/config-loader.cjs");
41
42
  const change_tracker_1 = require("../utils/change-tracker.cjs");
42
43
  const fs = __importStar(require("fs"));
43
44
  const path = __importStar(require("path"));
@@ -48,6 +49,9 @@ async function commitCommand(context) {
48
49
  if (!(0, repo_manager_1.isInitialized)(projectRoot)) {
49
50
  (0, cli_utils_1.fatal)('not a relq repository (or any parent directories): .relq', "run 'relq init' to initialize");
50
51
  }
52
+ const schemaPath = path.resolve(projectRoot, (0, config_loader_1.getSchemaPath)(config ?? undefined));
53
+ const { requireValidSchema } = await Promise.resolve().then(() => __importStar(require("../utils/config-loader.cjs")));
54
+ await requireValidSchema(schemaPath, flags);
51
55
  const staged = (0, repo_manager_1.getStagedChanges)(projectRoot);
52
56
  if (staged.length === 0) {
53
57
  console.log('nothing to commit, working tree clean');
@@ -122,7 +126,7 @@ async function commitCommand(context) {
122
126
  }
123
127
  }
124
128
  const commitConfig = await (0, config_1.loadConfig)();
125
- const schemaPathRaw = typeof commitConfig?.schema === 'string' ? commitConfig.schema : './db/schema.ts';
129
+ const schemaPathRaw = (0, config_loader_1.getSchemaPath)(commitConfig);
126
130
  const schemaFilePath = path.resolve(projectRoot, schemaPathRaw);
127
131
  if (fs.existsSync(schemaFilePath)) {
128
132
  const currentContent = fs.readFileSync(schemaFilePath, 'utf-8');
@@ -42,11 +42,13 @@ const ast_transformer_1 = require("../utils/ast-transformer.cjs");
42
42
  const repo_manager_1 = require("../utils/repo-manager.cjs");
43
43
  const schema_comparator_1 = require("../utils/schema-comparator.cjs");
44
44
  const change_tracker_1 = require("../utils/change-tracker.cjs");
45
+ const config_loader_1 = require("../utils/config-loader.cjs");
45
46
  const relqignore_1 = require("../utils/relqignore.cjs");
46
47
  const git_utils_1 = require("../utils/git-utils.cjs");
47
48
  async function importCommand(sqlFilePath, options = {}, projectRoot = process.cwd()) {
48
49
  const { includeFunctions = false, includeTriggers = false, force = false, dryRun = false } = options;
49
50
  const spinner = (0, git_utils_1.createSpinner)();
51
+ const config = await (0, config_loader_1.loadConfigWithEnv)();
50
52
  console.log('');
51
53
  if (!sqlFilePath) {
52
54
  (0, git_utils_1.fatal)('No SQL file specified', 'usage: relq import <sql-file> [options]\n\n' +
@@ -396,7 +398,7 @@ async function importCommand(sqlFilePath, options = {}, projectRoot = process.cw
396
398
  console.log(`${git_utils_1.colors.yellow('Dry run mode')} - no files written`);
397
399
  console.log('');
398
400
  console.log('Would write:');
399
- const dryRunOutputPath = options.output || './db/schema.ts';
401
+ const dryRunOutputPath = options.output || (0, config_loader_1.getSchemaPath)(config);
400
402
  const dryRunAbsPath = path.resolve(projectRoot, dryRunOutputPath);
401
403
  console.log(` ${git_utils_1.colors.cyan(dryRunAbsPath)} ${git_utils_1.colors.gray(`(${(0, git_utils_1.formatBytes)(finalTypescriptContent.length)})`)}`);
402
404
  console.log(` ${git_utils_1.colors.cyan(path.join(projectRoot, '.relq/snapshot.json'))}`);
@@ -406,7 +408,7 @@ async function importCommand(sqlFilePath, options = {}, projectRoot = process.cw
406
408
  console.log('');
407
409
  return;
408
410
  }
409
- const outputPath = options.output || './db/schema.ts';
411
+ const outputPath = options.output || (0, config_loader_1.getSchemaPath)(config);
410
412
  const absoluteOutputPath = path.resolve(projectRoot, outputPath);
411
413
  const outputDir = path.dirname(absoluteOutputPath);
412
414
  if (!fs.existsSync(outputDir)) {
@@ -180,7 +180,7 @@ async function pullCommand(context) {
180
180
  const noCommit = flags['no-commit'] === true;
181
181
  const dryRun = flags['dry-run'] === true;
182
182
  const author = config.author || 'Relq CLI';
183
- const schemaPathRaw = typeof config.schema === 'string' ? config.schema : './db/schema.ts';
183
+ const schemaPathRaw = (0, config_loader_1.getSchemaPath)(config);
184
184
  const schemaPath = path.resolve(projectRoot, schemaPathRaw);
185
185
  const includeFunctions = config.includeFunctions ?? true;
186
186
  const includeTriggers = config.includeTriggers ?? true;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.validateCommand = validateCommand;
37
+ const path = __importStar(require("path"));
38
+ const config_loader_1 = require("../utils/config-loader.cjs");
39
+ const schema_validator_1 = require("../utils/schema-validator.cjs");
40
+ const cli_utils_1 = require("../utils/cli-utils.cjs");
41
+ async function validateCommand(context) {
42
+ const { config, args, projectRoot } = context;
43
+ console.log('');
44
+ let schemaPath;
45
+ if (args.length > 0) {
46
+ schemaPath = path.resolve(projectRoot, args[0]);
47
+ }
48
+ else {
49
+ schemaPath = path.resolve(projectRoot, (0, config_loader_1.getSchemaPath)(config ?? undefined));
50
+ }
51
+ const relativePath = path.relative(process.cwd(), schemaPath);
52
+ console.log(`Validating ${cli_utils_1.colors.cyan(relativePath)}...`);
53
+ console.log('');
54
+ const result = (0, schema_validator_1.validateSchemaFile)(schemaPath);
55
+ if (result.valid) {
56
+ (0, cli_utils_1.success)('Schema is valid');
57
+ console.log('');
58
+ console.log(` No syntax errors found in ${relativePath}`);
59
+ console.log('');
60
+ return;
61
+ }
62
+ console.log(cli_utils_1.colors.red(`error: Schema has ${result.errors.length} syntax error(s)`));
63
+ console.log('');
64
+ console.log((0, schema_validator_1.formatValidationErrors)(result));
65
+ console.log(cli_utils_1.colors.yellow('hint:') + ' Fix the errors above before running other commands');
66
+ console.log('');
67
+ process.exit(1);
68
+ }
69
+ exports.default = validateCommand;
@@ -57,6 +57,7 @@ const merge_1 = require("./commands/merge.cjs");
57
57
  const tag_1 = require("./commands/tag.cjs");
58
58
  const cherry_pick_1 = require("./commands/cherry-pick.cjs");
59
59
  const remote_1 = require("./commands/remote.cjs");
60
+ const validate_1 = require("./commands/validate.cjs");
60
61
  const fs = __importStar(require("fs"));
61
62
  const path = __importStar(require("path"));
62
63
  function loadEnvFile() {
@@ -161,6 +162,7 @@ Sync Commands:
161
162
  sync Pull + push in one command
162
163
 
163
164
  Other Commands:
165
+ validate Check schema for errors
164
166
  diff [--sql] Show schema differences
165
167
  generate Generate TypeScript types
166
168
  introspect Parse database schema
@@ -193,7 +195,7 @@ function printVersion() {
193
195
  console.log(`relq v${VERSION}`);
194
196
  }
195
197
  function requiresConfig(command) {
196
- return !['init', 'introspect', 'import', 'help', 'version'].includes(command);
198
+ return !['init', 'introspect', 'import', 'help', 'version', 'validate'].includes(command);
197
199
  }
198
200
  function requiresDbConnection(command, flags) {
199
201
  const alwaysNeedDb = ['pull', 'push', 'fetch', 'introspect'];
@@ -355,6 +357,9 @@ async function main() {
355
357
  case 'introspect':
356
358
  await (0, introspect_1.introspectCommand)(context);
357
359
  break;
360
+ case 'validate':
361
+ await (0, validate_1.validateCommand)(context);
362
+ break;
358
363
  case 'sync':
359
364
  await (0, sync_1.syncCommand)(context);
360
365
  break;
@@ -38,7 +38,9 @@ exports.findConfigFileRecursive = findConfigFileRecursive;
38
38
  exports.configExists = configExists;
39
39
  exports.loadConfigWithEnv = loadConfigWithEnv;
40
40
  exports.validateConfig = validateConfig;
41
+ exports.getSchemaPath = getSchemaPath;
41
42
  exports.requireValidConfig = requireValidConfig;
43
+ exports.requireValidSchema = requireValidSchema;
42
44
  const config_1 = require("../../config/config.cjs");
43
45
  const env_loader_1 = require("./env-loader.cjs");
44
46
  const fs = __importStar(require("fs"));
@@ -114,6 +116,23 @@ function validateConfig(config) {
114
116
  }
115
117
  return errors;
116
118
  }
119
+ function getSchemaPath(config, defaultPath = './db/schema.ts') {
120
+ if (!config)
121
+ return defaultPath;
122
+ if (typeof config.schema === 'string' && config.schema.length > 0) {
123
+ return config.schema;
124
+ }
125
+ if (typeof config.schema === 'object' && config.schema?.directory) {
126
+ return `${config.schema.directory}/schema.ts`;
127
+ }
128
+ if (config.generate?.outDir) {
129
+ return `${config.generate.outDir}/schema.ts`;
130
+ }
131
+ if (config.typeGeneration?.output) {
132
+ return config.typeGeneration.output;
133
+ }
134
+ return defaultPath;
135
+ }
117
136
  async function requireValidConfig(config, options) {
118
137
  const errors = validateConfig(config);
119
138
  if (errors.length === 0)
@@ -151,3 +170,25 @@ async function requireValidConfig(config, options) {
151
170
  console.error('');
152
171
  process.exit(1);
153
172
  }
173
+ async function requireValidSchema(schemaPath, flags = {}) {
174
+ if (flags['skip-validation'] === true) {
175
+ return true;
176
+ }
177
+ const { validateSchemaFile, formatValidationErrors } = await Promise.resolve().then(() => __importStar(require("./schema-validator.cjs")));
178
+ const result = validateSchemaFile(schemaPath);
179
+ if (result.valid) {
180
+ return true;
181
+ }
182
+ const colors = {
183
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
184
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
185
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
186
+ };
187
+ console.error('');
188
+ console.error(colors.red('error:') + ` Schema has ${result.errors.length} syntax error(s)`);
189
+ console.error('');
190
+ console.error(formatValidationErrors(result));
191
+ console.error(colors.yellow('hint:') + ` Fix the errors above, or use ${colors.cyan('--skip-validation')} to bypass`);
192
+ console.error('');
193
+ process.exit(1);
194
+ }
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.validateSchemaFile = validateSchemaFile;
37
+ exports.formatValidationErrors = formatValidationErrors;
38
+ const ts = __importStar(require("typescript"));
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ function validateSchemaFile(schemaPath) {
42
+ const absolutePath = path.resolve(schemaPath);
43
+ if (!fs.existsSync(absolutePath)) {
44
+ return {
45
+ valid: false,
46
+ errors: [{
47
+ line: 0,
48
+ column: 0,
49
+ message: `Schema file not found: ${absolutePath}`,
50
+ code: -1,
51
+ }],
52
+ filePath: absolutePath,
53
+ };
54
+ }
55
+ const content = fs.readFileSync(absolutePath, 'utf-8');
56
+ const sourceFile = ts.createSourceFile(absolutePath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
57
+ const errors = [];
58
+ const compilerOptions = {
59
+ target: ts.ScriptTarget.ESNext,
60
+ module: ts.ModuleKind.ESNext,
61
+ strict: false,
62
+ skipLibCheck: true,
63
+ noEmit: true,
64
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
65
+ };
66
+ const program = ts.createProgram([absolutePath], compilerOptions, {
67
+ getSourceFile: (fileName) => {
68
+ if (fileName === absolutePath)
69
+ return sourceFile;
70
+ return ts.createSourceFile(fileName, '', ts.ScriptTarget.Latest, true);
71
+ },
72
+ getDefaultLibFileName: () => 'lib.d.ts',
73
+ writeFile: () => { },
74
+ getCurrentDirectory: () => path.dirname(absolutePath),
75
+ getDirectories: () => [],
76
+ fileExists: (fileName) => fileName === absolutePath || fs.existsSync(fileName),
77
+ readFile: (fileName) => {
78
+ if (fileName === absolutePath)
79
+ return content;
80
+ try {
81
+ return fs.readFileSync(fileName, 'utf-8');
82
+ }
83
+ catch {
84
+ return undefined;
85
+ }
86
+ },
87
+ getCanonicalFileName: (fileName) => fileName,
88
+ useCaseSensitiveFileNames: () => true,
89
+ getNewLine: () => '\n',
90
+ });
91
+ const syntacticDiagnostics = program.getSyntacticDiagnostics(sourceFile);
92
+ for (const diagnostic of syntacticDiagnostics) {
93
+ if (diagnostic.file && diagnostic.start !== undefined) {
94
+ const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
95
+ errors.push({
96
+ line: line + 1,
97
+ column: character + 1,
98
+ message: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
99
+ code: diagnostic.code,
100
+ });
101
+ }
102
+ else {
103
+ errors.push({
104
+ line: 0,
105
+ column: 0,
106
+ message: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
107
+ code: diagnostic.code,
108
+ });
109
+ }
110
+ }
111
+ return {
112
+ valid: errors.length === 0,
113
+ errors,
114
+ filePath: absolutePath,
115
+ };
116
+ }
117
+ function formatValidationErrors(result) {
118
+ if (result.valid) {
119
+ return '';
120
+ }
121
+ const lines = [];
122
+ const relativePath = path.relative(process.cwd(), result.filePath);
123
+ for (const error of result.errors) {
124
+ if (error.line > 0) {
125
+ lines.push(` ${relativePath}:${error.line}:${error.column}`);
126
+ lines.push(` │`);
127
+ try {
128
+ const content = fs.readFileSync(result.filePath, 'utf-8');
129
+ const fileLine = content.split('\n')[error.line - 1];
130
+ if (fileLine !== undefined) {
131
+ const trimmedLine = fileLine.substring(0, 60);
132
+ lines.push(`${error.line.toString().padStart(3)} │ ${trimmedLine}${fileLine.length > 60 ? '...' : ''}`);
133
+ lines.push(` │ ${' '.repeat(error.column - 1)}^`);
134
+ }
135
+ }
136
+ catch {
137
+ }
138
+ lines.push(` │`);
139
+ lines.push(` └─ ${error.message}`);
140
+ }
141
+ else {
142
+ lines.push(` ${error.message}`);
143
+ }
144
+ lines.push('');
145
+ }
146
+ return lines.join('\n');
147
+ }
@@ -333,9 +333,10 @@ var require_postgres_interval = __commonJS((exports, module) => {
333
333
 
334
334
  // node_modules/postgres-bytea/index.js
335
335
  var require_postgres_bytea = __commonJS((exports, module) => {
336
+ var bufferFrom = Buffer.from || Buffer;
336
337
  module.exports = function parseBytea(input) {
337
338
  if (/^\\x/.test(input)) {
338
- return new Buffer(input.substr(2), "hex");
339
+ return bufferFrom(input.substr(2), "hex");
339
340
  }
340
341
  var output = "";
341
342
  var i = 0;
@@ -359,7 +360,7 @@ var require_postgres_bytea = __commonJS((exports, module) => {
359
360
  }
360
361
  }
361
362
  }
362
- return new Buffer(output, "binary");
363
+ return bufferFrom(output, "binary");
363
364
  };
364
365
  });
365
366
 
@@ -324,9 +324,10 @@ var require_postgres_interval = __commonJS((exports, module) => {
324
324
 
325
325
  // node_modules/postgres-bytea/index.js
326
326
  var require_postgres_bytea = __commonJS((exports, module) => {
327
+ var bufferFrom = Buffer.from || Buffer;
327
328
  module.exports = function parseBytea(input) {
328
329
  if (/^\\x/.test(input)) {
329
- return new Buffer(input.substr(2), "hex");
330
+ return bufferFrom(input.substr(2), "hex");
330
331
  }
331
332
  var output = "";
332
333
  var i = 0;
@@ -350,7 +351,7 @@ var require_postgres_bytea = __commonJS((exports, module) => {
350
351
  }
351
352
  }
352
353
  }
353
- return new Buffer(output, "binary");
354
+ return bufferFrom(output, "binary");
354
355
  };
355
356
  });
356
357
 
@@ -7,6 +7,7 @@ import * as path from 'path';
7
7
  import { isInitialized, loadSnapshot, getUnstagedChanges, getStagedChanges, stageChanges, detectFileChanges, addUnstagedChanges, clearUnstagedChanges, cleanupStagedChanges, } from "../utils/repo-manager.js";
8
8
  import { getChangeDisplayName } from "../utils/change-tracker.js";
9
9
  import { compareSchemas } from "../utils/schema-comparator.js";
10
+ import { getSchemaPath } from "../utils/config-loader.js";
10
11
  function parseSchemaFileForComparison(schemaPath) {
11
12
  if (!fs.existsSync(schemaPath)) {
12
13
  return null;
@@ -583,8 +584,10 @@ export async function addCommand(context) {
583
584
  }
584
585
  const ignorePatterns = loadRelqignore(projectRoot);
585
586
  const config = await loadConfig();
586
- const schemaPathRaw = typeof config?.schema === 'string' ? config.schema : './db/schema.ts';
587
+ const schemaPathRaw = getSchemaPath(config);
587
588
  const schemaPath = path.resolve(projectRoot, schemaPathRaw);
589
+ const { requireValidSchema } = await import("../utils/config-loader.js");
590
+ await requireValidSchema(schemaPath, context.flags);
588
591
  const injectedCount = injectTrackingIds(schemaPath);
589
592
  if (injectedCount > 0) {
590
593
  console.log(colors.muted(`Injected ${injectedCount} tracking ID(s) into schema.ts`));
@@ -2,6 +2,7 @@ import * as crypto from 'crypto';
2
2
  import { fatal, hint } from "../utils/cli-utils.js";
3
3
  import { isInitialized, getHead, getStagedChanges, getUnstagedChanges, shortHash, hashFileContent, saveFileHash, } from "../utils/repo-manager.js";
4
4
  import { loadConfig } from "../../config/config.js";
5
+ import { getSchemaPath } from "../utils/config-loader.js";
5
6
  import { sortChangesByDependency, generateCombinedSQL, } from "../utils/change-tracker.js";
6
7
  import * as fs from 'fs';
7
8
  import * as path from 'path';
@@ -12,6 +13,9 @@ export async function commitCommand(context) {
12
13
  if (!isInitialized(projectRoot)) {
13
14
  fatal('not a relq repository (or any parent directories): .relq', "run 'relq init' to initialize");
14
15
  }
16
+ const schemaPath = path.resolve(projectRoot, getSchemaPath(config ?? undefined));
17
+ const { requireValidSchema } = await import("../utils/config-loader.js");
18
+ await requireValidSchema(schemaPath, flags);
15
19
  const staged = getStagedChanges(projectRoot);
16
20
  if (staged.length === 0) {
17
21
  console.log('nothing to commit, working tree clean');
@@ -86,7 +90,7 @@ export async function commitCommand(context) {
86
90
  }
87
91
  }
88
92
  const commitConfig = await loadConfig();
89
- const schemaPathRaw = typeof commitConfig?.schema === 'string' ? commitConfig.schema : './db/schema.ts';
93
+ const schemaPathRaw = getSchemaPath(commitConfig);
90
94
  const schemaFilePath = path.resolve(projectRoot, schemaPathRaw);
91
95
  if (fs.existsSync(schemaFilePath)) {
92
96
  const currentContent = fs.readFileSync(schemaFilePath, 'utf-8');
@@ -6,11 +6,13 @@ import { parseSQL, normalizedToParsedSchema } from "../utils/ast-transformer.js"
6
6
  import { saveSnapshot, loadSnapshot, isInitialized, initRepository, stageChanges, addUnstagedChanges } from "../utils/repo-manager.js";
7
7
  import { compareSchemas } from "../utils/schema-comparator.js";
8
8
  import { getChangeDisplayName } from "../utils/change-tracker.js";
9
+ import { getSchemaPath, loadConfigWithEnv } from "../utils/config-loader.js";
9
10
  import { loadRelqignore, validateIgnoreDependencies, isTableIgnored, isColumnIgnored, isIndexIgnored, isConstraintIgnored, isEnumIgnored, isDomainIgnored, isSequenceIgnored, isFunctionIgnored, } from "../utils/relqignore.js";
10
11
  import { colors, fatal, warning, hint, getWorkingTreeStatus, printDirtyWorkingTreeError, printMergeStrategyHelp, readSQLFile, createSpinner, formatBytes, } from "../utils/git-utils.js";
11
12
  export async function importCommand(sqlFilePath, options = {}, projectRoot = process.cwd()) {
12
13
  const { includeFunctions = false, includeTriggers = false, force = false, dryRun = false } = options;
13
14
  const spinner = createSpinner();
15
+ const config = await loadConfigWithEnv();
14
16
  console.log('');
15
17
  if (!sqlFilePath) {
16
18
  fatal('No SQL file specified', 'usage: relq import <sql-file> [options]\n\n' +
@@ -360,7 +362,7 @@ export async function importCommand(sqlFilePath, options = {}, projectRoot = pro
360
362
  console.log(`${colors.yellow('Dry run mode')} - no files written`);
361
363
  console.log('');
362
364
  console.log('Would write:');
363
- const dryRunOutputPath = options.output || './db/schema.ts';
365
+ const dryRunOutputPath = options.output || getSchemaPath(config);
364
366
  const dryRunAbsPath = path.resolve(projectRoot, dryRunOutputPath);
365
367
  console.log(` ${colors.cyan(dryRunAbsPath)} ${colors.gray(`(${formatBytes(finalTypescriptContent.length)})`)}`);
366
368
  console.log(` ${colors.cyan(path.join(projectRoot, '.relq/snapshot.json'))}`);
@@ -370,7 +372,7 @@ export async function importCommand(sqlFilePath, options = {}, projectRoot = pro
370
372
  console.log('');
371
373
  return;
372
374
  }
373
- const outputPath = options.output || './db/schema.ts';
375
+ const outputPath = options.output || getSchemaPath(config);
374
376
  const absoluteOutputPath = path.resolve(projectRoot, outputPath);
375
377
  const outputDir = path.dirname(absoluteOutputPath);
376
378
  if (!fs.existsSync(outputDir)) {
@@ -1,6 +1,6 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import { requireValidConfig } from "../utils/config-loader.js";
3
+ 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 } from "../utils/ast-codegen.js";
@@ -144,7 +144,7 @@ export async function pullCommand(context) {
144
144
  const noCommit = flags['no-commit'] === true;
145
145
  const dryRun = flags['dry-run'] === true;
146
146
  const author = config.author || 'Relq CLI';
147
- const schemaPathRaw = typeof config.schema === 'string' ? config.schema : './db/schema.ts';
147
+ const schemaPathRaw = getSchemaPath(config);
148
148
  const schemaPath = path.resolve(projectRoot, schemaPathRaw);
149
149
  const includeFunctions = config.includeFunctions ?? true;
150
150
  const includeTriggers = config.includeTriggers ?? true;
@@ -0,0 +1,33 @@
1
+ import * as path from 'path';
2
+ import { getSchemaPath } from "../utils/config-loader.js";
3
+ import { validateSchemaFile, formatValidationErrors } from "../utils/schema-validator.js";
4
+ import { colors, success } from "../utils/cli-utils.js";
5
+ export async function validateCommand(context) {
6
+ const { config, args, projectRoot } = context;
7
+ console.log('');
8
+ let schemaPath;
9
+ if (args.length > 0) {
10
+ schemaPath = path.resolve(projectRoot, args[0]);
11
+ }
12
+ else {
13
+ schemaPath = path.resolve(projectRoot, getSchemaPath(config ?? undefined));
14
+ }
15
+ const relativePath = path.relative(process.cwd(), schemaPath);
16
+ console.log(`Validating ${colors.cyan(relativePath)}...`);
17
+ console.log('');
18
+ const result = validateSchemaFile(schemaPath);
19
+ if (result.valid) {
20
+ success('Schema is valid');
21
+ console.log('');
22
+ console.log(` No syntax errors found in ${relativePath}`);
23
+ console.log('');
24
+ return;
25
+ }
26
+ console.log(colors.red(`error: Schema has ${result.errors.length} syntax error(s)`));
27
+ console.log('');
28
+ console.log(formatValidationErrors(result));
29
+ console.log(colors.yellow('hint:') + ' Fix the errors above before running other commands');
30
+ console.log('');
31
+ process.exit(1);
32
+ }
33
+ export default validateCommand;
@@ -21,6 +21,7 @@ import { mergeCommand } from "./commands/merge.js";
21
21
  import { tagCommand } from "./commands/tag.js";
22
22
  import { cherryPickCommand } from "./commands/cherry-pick.js";
23
23
  import { remoteCommand } from "./commands/remote.js";
24
+ import { validateCommand } from "./commands/validate.js";
24
25
  import * as fs from 'fs';
25
26
  import * as path from 'path';
26
27
  function loadEnvFile() {
@@ -125,6 +126,7 @@ Sync Commands:
125
126
  sync Pull + push in one command
126
127
 
127
128
  Other Commands:
129
+ validate Check schema for errors
128
130
  diff [--sql] Show schema differences
129
131
  generate Generate TypeScript types
130
132
  introspect Parse database schema
@@ -157,7 +159,7 @@ function printVersion() {
157
159
  console.log(`relq v${VERSION}`);
158
160
  }
159
161
  function requiresConfig(command) {
160
- return !['init', 'introspect', 'import', 'help', 'version'].includes(command);
162
+ return !['init', 'introspect', 'import', 'help', 'version', 'validate'].includes(command);
161
163
  }
162
164
  function requiresDbConnection(command, flags) {
163
165
  const alwaysNeedDb = ['pull', 'push', 'fetch', 'introspect'];
@@ -319,6 +321,9 @@ async function main() {
319
321
  case 'introspect':
320
322
  await introspectCommand(context);
321
323
  break;
324
+ case 'validate':
325
+ await validateCommand(context);
326
+ break;
322
327
  case 'sync':
323
328
  await syncCommand(context);
324
329
  break;
@@ -73,6 +73,23 @@ export function validateConfig(config) {
73
73
  }
74
74
  return errors;
75
75
  }
76
+ export function getSchemaPath(config, defaultPath = './db/schema.ts') {
77
+ if (!config)
78
+ return defaultPath;
79
+ if (typeof config.schema === 'string' && config.schema.length > 0) {
80
+ return config.schema;
81
+ }
82
+ if (typeof config.schema === 'object' && config.schema?.directory) {
83
+ return `${config.schema.directory}/schema.ts`;
84
+ }
85
+ if (config.generate?.outDir) {
86
+ return `${config.generate.outDir}/schema.ts`;
87
+ }
88
+ if (config.typeGeneration?.output) {
89
+ return config.typeGeneration.output;
90
+ }
91
+ return defaultPath;
92
+ }
76
93
  export async function requireValidConfig(config, options) {
77
94
  const errors = validateConfig(config);
78
95
  if (errors.length === 0)
@@ -110,3 +127,25 @@ export async function requireValidConfig(config, options) {
110
127
  console.error('');
111
128
  process.exit(1);
112
129
  }
130
+ export async function requireValidSchema(schemaPath, flags = {}) {
131
+ if (flags['skip-validation'] === true) {
132
+ return true;
133
+ }
134
+ const { validateSchemaFile, formatValidationErrors } = await import("./schema-validator.js");
135
+ const result = validateSchemaFile(schemaPath);
136
+ if (result.valid) {
137
+ return true;
138
+ }
139
+ const colors = {
140
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
141
+ yellow: (s) => `\x1b[33m${s}\x1b[0m`,
142
+ cyan: (s) => `\x1b[36m${s}\x1b[0m`,
143
+ };
144
+ console.error('');
145
+ console.error(colors.red('error:') + ` Schema has ${result.errors.length} syntax error(s)`);
146
+ console.error('');
147
+ console.error(formatValidationErrors(result));
148
+ console.error(colors.yellow('hint:') + ` Fix the errors above, or use ${colors.cyan('--skip-validation')} to bypass`);
149
+ console.error('');
150
+ process.exit(1);
151
+ }
@@ -0,0 +1,110 @@
1
+ import * as ts from 'typescript';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ export function validateSchemaFile(schemaPath) {
5
+ const absolutePath = path.resolve(schemaPath);
6
+ if (!fs.existsSync(absolutePath)) {
7
+ return {
8
+ valid: false,
9
+ errors: [{
10
+ line: 0,
11
+ column: 0,
12
+ message: `Schema file not found: ${absolutePath}`,
13
+ code: -1,
14
+ }],
15
+ filePath: absolutePath,
16
+ };
17
+ }
18
+ const content = fs.readFileSync(absolutePath, 'utf-8');
19
+ const sourceFile = ts.createSourceFile(absolutePath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
20
+ const errors = [];
21
+ const compilerOptions = {
22
+ target: ts.ScriptTarget.ESNext,
23
+ module: ts.ModuleKind.ESNext,
24
+ strict: false,
25
+ skipLibCheck: true,
26
+ noEmit: true,
27
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
28
+ };
29
+ const program = ts.createProgram([absolutePath], compilerOptions, {
30
+ getSourceFile: (fileName) => {
31
+ if (fileName === absolutePath)
32
+ return sourceFile;
33
+ return ts.createSourceFile(fileName, '', ts.ScriptTarget.Latest, true);
34
+ },
35
+ getDefaultLibFileName: () => 'lib.d.ts',
36
+ writeFile: () => { },
37
+ getCurrentDirectory: () => path.dirname(absolutePath),
38
+ getDirectories: () => [],
39
+ fileExists: (fileName) => fileName === absolutePath || fs.existsSync(fileName),
40
+ readFile: (fileName) => {
41
+ if (fileName === absolutePath)
42
+ return content;
43
+ try {
44
+ return fs.readFileSync(fileName, 'utf-8');
45
+ }
46
+ catch {
47
+ return undefined;
48
+ }
49
+ },
50
+ getCanonicalFileName: (fileName) => fileName,
51
+ useCaseSensitiveFileNames: () => true,
52
+ getNewLine: () => '\n',
53
+ });
54
+ const syntacticDiagnostics = program.getSyntacticDiagnostics(sourceFile);
55
+ for (const diagnostic of syntacticDiagnostics) {
56
+ if (diagnostic.file && diagnostic.start !== undefined) {
57
+ const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
58
+ errors.push({
59
+ line: line + 1,
60
+ column: character + 1,
61
+ message: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
62
+ code: diagnostic.code,
63
+ });
64
+ }
65
+ else {
66
+ errors.push({
67
+ line: 0,
68
+ column: 0,
69
+ message: ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'),
70
+ code: diagnostic.code,
71
+ });
72
+ }
73
+ }
74
+ return {
75
+ valid: errors.length === 0,
76
+ errors,
77
+ filePath: absolutePath,
78
+ };
79
+ }
80
+ export function formatValidationErrors(result) {
81
+ if (result.valid) {
82
+ return '';
83
+ }
84
+ const lines = [];
85
+ const relativePath = path.relative(process.cwd(), result.filePath);
86
+ for (const error of result.errors) {
87
+ if (error.line > 0) {
88
+ lines.push(` ${relativePath}:${error.line}:${error.column}`);
89
+ lines.push(` │`);
90
+ try {
91
+ const content = fs.readFileSync(result.filePath, 'utf-8');
92
+ const fileLine = content.split('\n')[error.line - 1];
93
+ if (fileLine !== undefined) {
94
+ const trimmedLine = fileLine.substring(0, 60);
95
+ lines.push(`${error.line.toString().padStart(3)} │ ${trimmedLine}${fileLine.length > 60 ? '...' : ''}`);
96
+ lines.push(` │ ${' '.repeat(error.column - 1)}^`);
97
+ }
98
+ }
99
+ catch {
100
+ }
101
+ lines.push(` │`);
102
+ lines.push(` └─ ${error.message}`);
103
+ }
104
+ else {
105
+ lines.push(` ${error.message}`);
106
+ }
107
+ lines.push('');
108
+ }
109
+ return lines.join('\n');
110
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relq",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "description": "The Fully-Typed PostgreSQL ORM for TypeScript",
5
5
  "author": "Olajide Mathew O. <olajide.mathew@yuniq.solutions>",
6
6
  "license": "MIT",