@msugiura/rawsql-prisma
Version:
Prisma integration for rawsql-ts - Dynamic SQL generation with type safety and hierarchical JSON serialization
335 lines • 13.7 kB
JavaScript
;
/**
* Automatic Type Compatibility Validator
*
* Uses TypeScript compiler API to automatically validate JsonMapping compatibility
* with target TypeScript interfaces. Only requires interface name and import path!
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.AutoTypeCompatibilityValidator = void 0;
const ts = __importStar(require("typescript"));
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
/**
* Supported TypeScript file extensions for interface resolution
*/
const TYPESCRIPT_EXTENSIONS = ['.ts', '.tsx', '.d.ts'];
/**
* Automatic type compatibility validator that reads TypeScript interfaces
* and validates JsonMapping structure compatibility
*/
class AutoTypeCompatibilityValidator {
constructor(options = {}) {
// Use project current directory for proper path resolution
const defaultBaseDir = process.cwd();
this.options = {
baseDir: defaultBaseDir,
compilerOptions: {
target: ts.ScriptTarget.ES2020,
module: ts.ModuleKind.CommonJS,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
esModuleInterop: true,
allowSyntheticDefaultImports: true,
strict: true,
skipLibCheck: true
},
debug: false,
...options
};
}
/**
* Validate JsonMapping compatibility with TypeScript interface
*
* @param jsonMapping - Enhanced JsonMapping with typeInfo
* @returns Validation result with detailed compatibility information
*/
async validateMapping(jsonMapping) {
if (!jsonMapping.typeInfo) {
return {
isValid: false,
errors: ['No type information provided in JsonMapping'],
missingProperties: [],
extraProperties: [],
typeConflicts: []
};
}
try {
// Resolve the interface file path
const interfaceFilePath = this.resolveInterfacePath(jsonMapping.typeInfo.importPath);
if (this.options.debug) {
console.log(`🔍 Import path: ${jsonMapping.typeInfo.importPath}`);
console.log(`📂 Base directory: ${this.options.baseDir}`);
console.log(`📄 Resolved to: ${interfaceFilePath}`);
console.log(`✅ File exists: ${fs.existsSync(interfaceFilePath)}`);
}
// Parse TypeScript interface
const interfaceStructure = await this.parseInterface(interfaceFilePath, jsonMapping.typeInfo.interface);
if (this.options.debug) {
console.log(`📋 Parsed interface structure:`, JSON.stringify(interfaceStructure, null, 2));
}
// Generate expected structure from JsonMapping
const mappingStructure = this.generateStructureFromMapping(jsonMapping);
if (this.options.debug) {
console.log(`🏗️ Generated mapping structure:`, JSON.stringify(mappingStructure, null, 2));
}
// Compare structures
const validationResult = this.compareStructures(interfaceStructure, mappingStructure);
if (this.options.debug) {
console.log(`✅ Validation result:`, validationResult);
}
return validationResult;
}
catch (error) {
return {
isValid: false,
errors: [`Failed to validate interface: ${error instanceof Error ? error.message : String(error)}`],
missingProperties: [],
extraProperties: [],
typeConflicts: []
};
}
}
/**
* Resolve interface file path relative to base directory
*/
resolveInterfacePath(importPath) {
if (path.isAbsolute(importPath)) {
return importPath;
}
// Try different resolution strategies
const candidatePaths = this.generateCandidatePaths(importPath);
// Find the first existing file
for (const candidatePath of candidatePaths) {
const resolvedPath = this.resolveWithExtensions(candidatePath);
if (resolvedPath) {
return resolvedPath;
}
}
// Fallback: return original resolution with .ts extension
return path.resolve(this.options.baseDir, importPath) + '.ts';
}
/**
* Generate candidate paths for resolution, handling redundant directory prefixes
*
* This method addresses the issue where import paths may contain redundant directory names
* that match the base directory name. For example:
* - baseDir: "/project/static-analysis"
* - importPath: "static-analysis/src/types.ts"
* - Result: First tries "/project/static-analysis/src/types.ts", then "/project/static-analysis/static-analysis/src/types.ts"
*
* @param importPath - The relative import path from the JSON mapping file
* @returns Array of candidate absolute paths to try, ordered by preference
*/
generateCandidatePaths(importPath) {
const baseDir = this.options.baseDir;
const baseDirName = path.basename(baseDir);
const candidates = [];
// Strategy 1: Check for redundant directory prefix
if (importPath.startsWith(baseDirName + path.sep)) {
const withoutPrefix = importPath.substring(baseDirName.length + 1);
candidates.push(path.resolve(baseDir, withoutPrefix));
}
// Strategy 2: Standard resolution
candidates.push(path.resolve(baseDir, importPath));
return candidates;
}
/**
* Try to resolve a path with common TypeScript extensions
*
* @param basePath - The base path to resolve (with or without extension)
* @returns The resolved path if found, null otherwise
*/
resolveWithExtensions(basePath) {
// If path already has an extension, check if it exists
if (path.extname(basePath)) {
return fs.existsSync(basePath) ? basePath : null;
}
// Try common TypeScript file extensions
for (const ext of TYPESCRIPT_EXTENSIONS) {
const withExt = basePath + ext;
if (fs.existsSync(withExt)) {
return withExt;
}
}
return null;
}
/**
* Parse TypeScript interface and extract structure
*/
async parseInterface(filePath, interfaceName) {
const sourceCode = fs.readFileSync(filePath, 'utf8');
const sourceFile = ts.createSourceFile(filePath, sourceCode, ts.ScriptTarget.Latest, true);
let interfaceDeclaration;
// Find the target interface
ts.forEachChild(sourceFile, (node) => {
if (ts.isInterfaceDeclaration(node) && node.name.text === interfaceName) {
interfaceDeclaration = node;
}
});
if (!interfaceDeclaration) {
throw new Error(`Interface ${interfaceName} not found in ${filePath}`);
}
// Extract interface structure
return this.extractInterfaceStructure(interfaceDeclaration);
}
/**
* Extract structure from TypeScript interface declaration
*/
extractInterfaceStructure(interfaceDecl) {
const structure = {};
for (const member of interfaceDecl.members) {
if (ts.isPropertySignature(member) && member.name && ts.isIdentifier(member.name)) {
const propertyName = member.name.text;
const isOptional = !!member.questionToken;
const typeInfo = this.extractTypeInfo(member.type);
structure[propertyName] = {
required: !isOptional,
type: typeInfo
};
}
}
return structure;
}
/**
* Extract type information from TypeScript type node
*/
extractTypeInfo(typeNode) {
if (!typeNode)
return 'unknown';
switch (typeNode.kind) {
case ts.SyntaxKind.StringKeyword:
return 'string';
case ts.SyntaxKind.NumberKeyword:
return 'number';
case ts.SyntaxKind.BooleanKeyword:
return 'boolean';
case ts.SyntaxKind.ArrayType:
const arrayType = typeNode;
const elementType = this.extractTypeInfo(arrayType.elementType);
return `${elementType}[]`;
case ts.SyntaxKind.TypeReference:
const typeRef = typeNode;
if (ts.isIdentifier(typeRef.typeName)) {
const typeName = typeRef.typeName.text;
if (typeName === 'Date')
return 'Date';
return 'object'; // Assume other type references are objects
}
return 'object';
default:
return 'unknown';
}
}
/**
* Generate expected structure from JsonMapping
*/
generateStructureFromMapping(mapping) {
const structure = {}; // Add root entity properties
for (const [jsonKey, sqlColumn] of Object.entries(mapping.rootEntity.columns)) {
structure[jsonKey] = {
required: true, // Assume required unless specified otherwise
type: this.inferTypeFromColumnName(String(sqlColumn))
};
}
// Add nested entities
for (const nestedEntity of mapping.nestedEntities) {
if (nestedEntity.relationshipType === 'array') {
structure[nestedEntity.propertyName] = {
required: true,
type: 'object[]'
};
}
else {
structure[nestedEntity.propertyName] = {
required: true,
type: 'object'
};
}
}
return structure;
}
/**
* Infer TypeScript type from SQL column name (basic heuristics)
*/
inferTypeFromColumnName(columnName) {
const name = columnName.toLowerCase();
if (name.includes('id') || name.includes('count'))
return 'number';
if (name.includes('created_at') || name.includes('updated_at') || name.includes('date'))
return 'Date';
if (name.includes('completed') || name.includes('is_') || name.includes('has_'))
return 'boolean';
return 'string'; // Default to string
} /**
* Compare interface structure with mapping structure (structure-only validation)
*/
compareStructures(interfaceStructure, mappingStructure) {
const errors = [];
const missingProperties = [];
const extraProperties = [];
const typeConflicts = [];
// Check for missing required properties (property names only)
for (const [prop, info] of Object.entries(interfaceStructure)) {
const propInfo = info;
if (propInfo.required && !mappingStructure[prop]) {
missingProperties.push(prop);
}
}
// Check for extra properties (ignore type conflicts for structure-only validation)
for (const [prop, info] of Object.entries(mappingStructure)) {
if (!interfaceStructure[prop]) {
extraProperties.push(prop);
}
// Skip type comparison - structure-only validation
}
// Generate error messages (only for missing/extra properties)
if (missingProperties.length > 0) {
errors.push(`Missing required properties: ${missingProperties.join(', ')}`);
}
if (extraProperties.length > 0) {
errors.push(`Extra properties not in interface: ${extraProperties.join(', ')}`);
}
return {
isValid: errors.length === 0,
errors,
missingProperties,
extraProperties,
typeConflicts: [] // Always empty for structure-only validation
};
}
}
exports.AutoTypeCompatibilityValidator = AutoTypeCompatibilityValidator;
//# sourceMappingURL=AutoTypeCompatibilityValidator.js.map