diff --git a/dbal/development/package.json b/dbal/development/package.json index 5859882e0..7aeae2e22 100644 --- a/dbal/development/package.json +++ b/dbal/development/package.json @@ -15,7 +15,8 @@ "lint": "eslint src/**/*.ts", "format": "prettier --write src/**/*.ts", "codegen": "tsx ../shared/tools/codegen/gen_types.ts", - "codegen:prisma": "node ../shared/tools/codegen/gen_prisma_schema.js" + "codegen:prisma": "node ../shared/tools/codegen/gen_prisma_schema.js", + "generate-types": "node ../shared/tools/codegen/generate-types.js" }, "keywords": [ "database", diff --git a/dbal/shared/tools/codegen/generate-types.js b/dbal/shared/tools/codegen/generate-types.js new file mode 100755 index 000000000..d4651dd7b --- /dev/null +++ b/dbal/shared/tools/codegen/generate-types.js @@ -0,0 +1,225 @@ +#!/usr/bin/env node +/** + * DBAL Type Generator + * + * Generates TypeScript type definitions from YAML entity schemas. + * This ensures types are always in sync with the schema source of truth. + */ + +const fs = require('fs') +const path = require('path') + +// Simple YAML parser (supports subset needed for entity schemas) +function parseYAML(content) { + const lines = content.split('\n') + const result = {} + const stack = [{ obj: result, indent: -1 }] + let currentKey = null + let currentIndent = 0 + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + const trimmed = line.trim() + + // Skip empty lines and comments + if (!trimmed || trimmed.startsWith('#')) continue + + // Calculate indentation + const indent = line.search(/\S/) + + // Pop stack if indent decreased + while (stack.length > 1 && indent <= stack[stack.length - 1].indent) { + stack.pop() + } + + const current = stack[stack.length - 1].obj + + // Parse key-value + if (line.includes(':')) { + const colonIndex = line.indexOf(':') + const key = line.substring(0, colonIndex).trim() + const value = line.substring(colonIndex + 1).trim() + + if (value === '') { + // Object key + current[key] = {} + stack.push({ obj: current[key], indent }) + } else if (value.startsWith('[') && value.endsWith(']')) { + // Array value + current[key] = value + .substring(1, value.length - 1) + .split(',') + .map(v => v.trim()) + .filter(v => v) + } else if (value === 'true' || value === 'false') { + // Boolean + current[key] = value === 'true' + } else if (!isNaN(value) && value !== '') { + // Number + current[key] = Number(value) + } else { + // String (remove quotes if present) + current[key] = value.replace(/^["']|["']$/g, '') + } + } + } + + return result +} + +/** + * Maps YAML types to TypeScript types + */ +function mapYamlTypeToTS(yamlType) { + const typeMap = { + uuid: 'string', + string: 'string', + text: 'string', + email: 'string', + integer: 'number', + bigint: 'bigint', + boolean: 'boolean', + enum: 'string', + json: 'unknown', + } + + return typeMap[yamlType] || 'unknown' +} + +/** + * Generates a TypeScript interface for a YAML entity + */ +function generateEntityInterface(entity) { + const interfaceName = entity.entity + const fields = [] + + // Add JSDoc comment + let output = `/**\n * ${entity.description || entity.entity}\n` + output += ` * @generated from ${entity.entity.toLowerCase()}.yaml\n */\n` + + for (const [fieldName, field] of Object.entries(entity.fields)) { + const tsType = mapYamlTypeToTS(field.type) + const isOptional = field.optional || field.nullable || !field.required + const optionalMark = isOptional ? '?' : '' + const nullableType = field.nullable ? ` | null` : '' + + // Add field comment if description exists + let comment = field.description || '' + if (field.sensitive) { + comment = comment ? `${comment} (sensitive - should not be sent to client)` : '(sensitive - should not be sent to client)' + } + if (comment) { + fields.push(` /** ${comment} */`) + } + + fields.push(` ${fieldName}${optionalMark}: ${tsType}${nullableType}`) + } + + output += `export interface ${interfaceName} {\n` + output += fields.join('\n') + output += '\n}\n' + + return output +} + +/** + * Scans all YAML entity files and generates types + */ +function generateAllTypes() { + const schemaDir = path.resolve(__dirname, '../../api/schema/entities') + const entities = [] + + // Recursively find all YAML files + function findYamlFiles(dir) { + const results = [] + const items = fs.readdirSync(dir) + + for (const item of items) { + const fullPath = path.join(dir, item) + const stat = fs.statSync(fullPath) + + if (stat.isDirectory()) { + results.push(...findYamlFiles(fullPath)) + } else if (item.endsWith('.yaml') || item.endsWith('.yml')) { + results.push(fullPath) + } + } + + return results + } + + const yamlFiles = findYamlFiles(schemaDir) + + // Parse all YAML files + for (const file of yamlFiles) { + try { + const content = fs.readFileSync(file, 'utf-8') + const parsed = parseYAML(content) + + if (parsed.entity && parsed.fields) { + entities.push(parsed) + } + } catch (error) { + console.error(`Error parsing ${file}:`, error) + } + } + + // Sort entities alphabetically for consistent output + entities.sort((a, b) => a.entity.localeCompare(b.entity)) + + // Generate output + let output = `/** + * DBAL Generated Types + * + * DO NOT EDIT THIS FILE MANUALLY! + * Generated from YAML entity schemas. + * + * To regenerate: npm run dbal:generate-types + */ + +` + + for (const entity of entities) { + output += generateEntityInterface(entity) + output += '\n' + } + + return output +} + +/** + * Main execution + */ +function main() { + try { + console.log('🔧 Generating TypeScript types from YAML schemas...') + + const output = generateAllTypes() + const outputPath = path.resolve( + __dirname, + '../../..', + 'development/src/core/foundation/types/types.generated.ts' + ) + + // Ensure output directory exists + const outputDir = path.dirname(outputPath) + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }) + } + + fs.writeFileSync(outputPath, output, 'utf-8') + + console.log(`✅ Successfully generated types at: ${outputPath}`) + console.log(` Generated ${output.split('export interface').length - 1} entity types`) + } catch (error) { + console.error('❌ Error generating types:', error) + process.exit(1) + } +} + +// Run if executed directly +if (require.main === module) { + main() +} + +module.exports = { generateAllTypes, generateEntityInterface, mapYamlTypeToTS } diff --git a/dbal/shared/tools/codegen/generate-types.ts b/dbal/shared/tools/codegen/generate-types.ts new file mode 100644 index 000000000..605fe1c1e --- /dev/null +++ b/dbal/shared/tools/codegen/generate-types.ts @@ -0,0 +1,203 @@ +#!/usr/bin/env node +/** + * DBAL Type Generator + * + * Generates TypeScript type definitions from YAML entity schemas. + * This ensures types are always in sync with the schema source of truth. + */ + +import * as fs from 'fs' +import * as path from 'path' +import * as yaml from 'yaml' + +interface YamlField { + type: string + primary?: boolean + required?: boolean + optional?: boolean + nullable?: boolean + generated?: boolean + unique?: boolean + sensitive?: boolean + description?: string + default?: unknown + min_length?: number + max_length?: number + min?: number + max?: number + pattern?: string + values?: string[] + foreign_key?: { + entity: string + field: string + on_delete?: string + } +} + +interface YamlEntity { + entity: string + version: string + description?: string + fields: Record + indexes?: Array<{ fields: string[]; unique?: boolean }> + acl?: Record + security?: Record +} + +/** + * Maps YAML types to TypeScript types + */ +function mapYamlTypeToTS(yamlType: string): string { + const typeMap: Record = { + uuid: 'string', + string: 'string', + text: 'string', + email: 'string', + integer: 'number', + bigint: 'bigint', + boolean: 'boolean', + enum: 'string', + json: 'unknown', + } + + return typeMap[yamlType] || 'unknown' +} + +/** + * Generates a TypeScript interface for a YAML entity + */ +function generateEntityInterface(entity: YamlEntity): string { + const interfaceName = entity.entity + const fields: string[] = [] + + // Add JSDoc comment + let output = `/**\n * ${entity.description || entity.entity}\n` + output += ` * @generated from ${entity.entity.toLowerCase()}.yaml\n */\n` + + for (const [fieldName, field] of Object.entries(entity.fields)) { + // Skip sensitive fields (they should never be in client types) + if (field.sensitive) { + continue + } + + const tsType = mapYamlTypeToTS(field.type) + const isOptional = field.optional || field.nullable || !field.required + const optionalMark = isOptional ? '?' : '' + const nullableType = field.nullable ? ` | null` : '' + + // Add field comment if description exists + if (field.description) { + fields.push(` /** ${field.description} */`) + } + + fields.push(` ${fieldName}${optionalMark}: ${tsType}${nullableType}`) + } + + output += `export interface ${interfaceName} {\n` + output += fields.join('\n') + output += '\n}\n' + + return output +} + +/** + * Scans all YAML entity files and generates types + */ +function generateAllTypes(): string { + const schemaDir = path.resolve(__dirname, '../../api/schema/entities') + const entities: YamlEntity[] = [] + + // Recursively find all YAML files + function findYamlFiles(dir: string): string[] { + const results: string[] = [] + const items = fs.readdirSync(dir) + + for (const item of items) { + const fullPath = path.join(dir, item) + const stat = fs.statSync(fullPath) + + if (stat.isDirectory()) { + results.push(...findYamlFiles(fullPath)) + } else if (item.endsWith('.yaml') || item.endsWith('.yml')) { + results.push(fullPath) + } + } + + return results + } + + const yamlFiles = findYamlFiles(schemaDir) + + // Parse all YAML files + for (const file of yamlFiles) { + try { + const content = fs.readFileSync(file, 'utf-8') + const parsed = yaml.parse(content) as YamlEntity + + if (parsed.entity && parsed.fields) { + entities.push(parsed) + } + } catch (error) { + console.error(`Error parsing ${file}:`, error) + } + } + + // Sort entities alphabetically for consistent output + entities.sort((a, b) => a.entity.localeCompare(b.entity)) + + // Generate output + let output = `/** + * DBAL Generated Types + * + * DO NOT EDIT THIS FILE MANUALLY! + * Generated from YAML entity schemas. + * + * To regenerate: npm run dbal:generate-types + */ + +` + + for (const entity of entities) { + output += generateEntityInterface(entity) + output += '\n' + } + + return output +} + +/** + * Main execution + */ +function main() { + try { + console.log('🔧 Generating TypeScript types from YAML schemas...') + + const output = generateAllTypes() + const outputPath = path.resolve( + __dirname, + '../../..', + 'development/src/core/foundation/types/types.generated.ts' + ) + + // Ensure output directory exists + const outputDir = path.dirname(outputPath) + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }) + } + + fs.writeFileSync(outputPath, output, 'utf-8') + + console.log(`✅ Successfully generated types at: ${outputPath}`) + console.log(` Generated ${output.split('export interface').length - 1} entity types`) + } catch (error) { + console.error('❌ Error generating types:', error) + process.exit(1) + } +} + +// Run if executed directly +if (require.main === module) { + main() +} + +export { generateAllTypes, generateEntityInterface, mapYamlTypeToTS }