@deepbrainspace/nx-surrealdb
Version:
NX plugin for SurrealDB migrations with modular architecture
241 lines ⢠9.63 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = runExecutor;
const tslib_1 = require("tslib");
const devkit_1 = require("@nx/devkit");
const path = tslib_1.__importStar(require("path"));
const fs = tslib_1.__importStar(require("fs"));
const migration_file_processor_1 = require("../../lib/filesystem/migration-file-processor");
async function runExecutor(options, _context) {
devkit_1.logger.info('š Linting SurrealDB migration files...');
const issues = [];
try {
// Get all migration files using the library function
const migrationFiles = await migration_file_processor_1.MigrationFileProcessor.getAllMigrationFiles(options.initPath);
devkit_1.logger.info(`š Found ${migrationFiles.length} migration files`);
// Lint each file
for (const file of migrationFiles) {
const fullPath = path.join(options.initPath, file);
const fileIssues = await lintFile(fullPath, options);
issues.push(...fileIssues);
}
// Lint migration structure
const structureIssues = await lintMigrationStructure(options.initPath, migrationFiles);
issues.push(...structureIssues);
// Lint config file if it exists
const configIssues = await lintConfigFile(options.initPath, options.configPath);
issues.push(...configIssues);
// Report results
const errors = issues.filter(i => i.severity === 'error');
const warnings = issues.filter(i => i.severity === 'warning');
if (issues.length === 0) {
devkit_1.logger.info('ā
No linting issues found');
return { success: true };
}
// Display issues
devkit_1.logger.info(`\nš Linting Results:`);
devkit_1.logger.info(` Errors: ${errors.length}`);
devkit_1.logger.info(` Warnings: ${warnings.length}\n`);
for (const issue of issues) {
const icon = issue.severity === 'error' ? 'ā' : 'ā ļø';
const location = issue.line ? `:${issue.line}` : '';
devkit_1.logger.info(`${icon} ${issue.file}${location}`);
devkit_1.logger.info(` ${issue.rule}: ${issue.message}\n`);
}
return { success: errors.length === 0 };
}
catch (error) {
devkit_1.logger.error(`š„ Linting failed: ${error}`);
return { success: false };
}
}
async function lintFile(filePath, options) {
const issues = [];
const content = fs.readFileSync(filePath, 'utf-8');
const lines = content.split('\n');
const fileName = path.relative(options.initPath, filePath);
// Check for basic SurrealDB syntax patterns
lines.forEach((line, index) => {
const lineNum = index + 1;
const trimmed = line.trim();
// Skip comments and empty lines
if (trimmed.startsWith('--') || trimmed === '')
return;
// Check for dangerous operations without safeguards
if (trimmed.match(/^DROP\s+(TABLE|INDEX|FIELD)/i) && !trimmed.includes('IF EXISTS')) {
issues.push({
file: fileName,
line: lineNum,
severity: 'warning',
message: 'Consider using IF EXISTS for DROP operations to avoid errors',
rule: 'safe-drop',
});
}
// Check for CREATE without IF NOT EXISTS
if (trimmed.match(/^(DEFINE|CREATE)\s+(TABLE|INDEX|FIELD)/i) &&
!trimmed.includes('IF NOT EXISTS')) {
issues.push({
file: fileName,
line: lineNum,
severity: 'warning',
message: 'Consider using IF NOT EXISTS for CREATE/DEFINE operations for idempotency',
rule: 'idempotent-create',
});
}
// Check for basic SQL injection patterns (though less relevant in migrations)
if (trimmed.includes("' +") || trimmed.includes('" +')) {
issues.push({
file: fileName,
line: lineNum,
severity: 'warning',
message: 'Potential string concatenation detected - ensure proper escaping',
rule: 'safe-concatenation',
});
}
});
return issues;
}
async function lintMigrationStructure(initPath, allFiles) {
const issues = [];
try {
// Use the files already discovered
const upFiles = allFiles.filter(f => f.endsWith('_up.surql'));
const downFiles = allFiles.filter(f => f.endsWith('_down.surql'));
// Check for missing down migrations
for (const upFile of upFiles) {
const downFile = upFile.replace('_up.surql', '_down.surql');
if (!downFiles.includes(downFile)) {
issues.push({
file: upFile,
severity: 'error',
message: `Missing corresponding down migration: ${downFile}`,
rule: 'migration-pairs',
});
}
}
// Check for orphaned down migrations
for (const downFile of downFiles) {
const upFile = downFile.replace('_down.surql', '_up.surql');
if (!upFiles.includes(upFile)) {
issues.push({
file: downFile,
severity: 'error',
message: `Orphaned down migration - missing up migration: ${upFile}`,
rule: 'migration-pairs',
});
}
}
// Check module naming convention
const modulePattern = /^(\d{3})_([a-z][a-z0-9_]*)\//;
for (const file of allFiles) {
if (!modulePattern.test(file)) {
issues.push({
file: file,
severity: 'error',
message: 'Module must follow naming convention: XXX_name/ (e.g., 000_admin/, 010_auth/)',
rule: 'module-naming',
});
}
}
}
catch (error) {
issues.push({
file: 'structure',
severity: 'error',
message: `Failed to analyze migration structure: ${error}`,
rule: 'structure-analysis',
});
}
return issues;
}
async function lintConfigFile(initPath, configPath) {
const issues = [];
try {
// Auto-detect config file if not specified
const configFile = configPath || path.join(initPath, 'config.json');
if (!fs.existsSync(configFile)) {
issues.push({
file: 'config.json',
severity: 'warning',
message: 'No config.json found - module dependencies cannot be validated',
rule: 'config-missing',
});
return issues;
}
const configContent = fs.readFileSync(configFile, 'utf-8');
const config = JSON.parse(configContent);
// Validate config structure
if (!config.modules || typeof config.modules !== 'object') {
issues.push({
file: 'config.json',
severity: 'error',
message: 'Config must have a "modules" object',
rule: 'config-structure',
});
return issues;
}
// Check for circular dependencies
const modules = Object.keys(config.modules);
for (const moduleId of modules) {
const visited = new Set();
if (hasCircularDependency(moduleId, config.modules, visited)) {
issues.push({
file: 'config.json',
severity: 'error',
message: `Circular dependency detected involving module: ${moduleId}`,
rule: 'circular-dependency',
});
}
}
// Validate module IDs match directory structure
const discoveredModules = await migration_file_processor_1.MigrationFileProcessor.discoverModules(initPath);
const configModules = Object.keys(config.modules);
for (const module of discoveredModules) {
if (!configModules.includes(module.moduleId)) {
issues.push({
file: 'config.json',
severity: 'warning',
message: `Module directory "${module.moduleId}" not found in config.json`,
rule: 'config-sync',
});
}
}
}
catch (error) {
if (error instanceof SyntaxError) {
issues.push({
file: 'config.json',
severity: 'error',
message: 'Invalid JSON syntax in config file',
rule: 'config-syntax',
});
}
else {
issues.push({
file: 'config.json',
severity: 'error',
message: `Failed to lint config file: ${error}`,
rule: 'config-analysis',
});
}
}
return issues;
}
function hasCircularDependency(moduleId, modules, visited, path = new Set()) {
if (path.has(moduleId))
return true;
if (visited.has(moduleId))
return false;
visited.add(moduleId);
path.add(moduleId);
const module = modules[moduleId];
const dependencies = module?.dependencies || [];
for (const dep of dependencies) {
if (hasCircularDependency(dep, modules, visited, path)) {
return true;
}
}
path.delete(moduleId);
return false;
}
//# sourceMappingURL=executor.js.map