Add YAML-based type generator for DBAL and generate types.generated.ts

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-08 16:16:46 +00:00
parent ba19f0b585
commit a4169bd56e
3 changed files with 430 additions and 1 deletions

View File

@@ -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",

View File

@@ -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 }

View File

@@ -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<string, YamlField>
indexes?: Array<{ fields: string[]; unique?: boolean }>
acl?: Record<string, unknown>
security?: Record<string, unknown>
}
/**
* Maps YAML types to TypeScript types
*/
function mapYamlTypeToTS(yamlType: string): string {
const typeMap: Record<string, string> = {
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 }