mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
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:
@@ -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",
|
||||
|
||||
225
dbal/shared/tools/codegen/generate-types.js
Executable file
225
dbal/shared/tools/codegen/generate-types.js
Executable 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 }
|
||||
203
dbal/shared/tools/codegen/generate-types.ts
Normal file
203
dbal/shared/tools/codegen/generate-types.ts
Normal 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 }
|
||||
Reference in New Issue
Block a user