Files
admin/apps/backend/app/services/module_generator/module_context.ts
T

136 lines
4.1 KiB
TypeScript

import { getColumnsForTable, getForeignKeysReferencing } from '#services/database_inspector'
import {
mapDBTypeToFrontend,
mapDBTypeToTs,
mapDBTypeToValidator,
} from '#services/module_generator/db_types'
import { camelCase, capitalize } from '#services/module_generator/naming'
export interface EnhancedColumn {
name: string
camelCase: string
type: string
isPrimary: boolean
isPassword: boolean
isEmail: boolean
isDateTime: boolean
isSpecial: boolean
isRequired: boolean
isNullable: boolean
validatorType: string
needsTrim: boolean
}
export interface TestField {
camelCase: string
sampleValue: string | number | boolean
column_name: string
}
export interface ModuleContext {
hasPassword: boolean
enhancedColumns: EnhancedColumn[]
createFields: TestField[]
updateFields: TestField[]
insertJson: string
frontendFields: Array<{ name: string; type: string }>
relationships: { imports: string[]; code: string[] }
}
/** Derives everything the templates and builders need from the table schema. */
export async function buildModuleContext(table: string): Promise<ModuleContext> {
const columns = await getColumnsForTable(table)
const enhancedColumns = columns.map((col): EnhancedColumn => {
const colName = col.column_name
const type = mapDBTypeToTs(col.data_type) + (col.is_nullable === 'YES' ? ' | null' : '')
return {
name: colName,
camelCase: camelCase(colName),
type,
isPrimary: colName === 'id',
isPassword: colName === 'password',
isEmail: colName === 'email',
isDateTime: ['created_at', 'updated_at'].includes(colName),
isSpecial: ['created_at', 'updated_at', 'deleted_at'].includes(colName),
isRequired: col.is_nullable === 'NO',
isNullable: col.is_nullable === 'YES',
validatorType: mapDBTypeToValidator(col.data_type),
needsTrim:
mapDBTypeToValidator(col.data_type) === 'string' &&
colName !== 'email' &&
colName !== 'password',
}
})
const sampleFor = (col: EnhancedColumn) => {
const t = col.validatorType
if (t === 'number') return 1
if (t === 'boolean') return false
if (t === 'date') return `'${new Date().toISOString()}'`
return `'sample_${col.camelCase}'`
}
const toTestField = (col: EnhancedColumn): TestField => ({
camelCase: col.camelCase,
sampleValue: sampleFor(col),
column_name: col.name,
})
const createFields = enhancedColumns.filter((c) => !c.isPrimary && !c.isSpecial).map(toTestField)
const updateFields = enhancedColumns.filter((c) => !c.isPrimary && !c.isDateTime).map(toTestField)
// minimal insert json for show/update/destroy fixtures
const insertJson = `{
${createFields.map((f) => ` ${f.column_name}: ${f.sampleValue}`).join(',\n')}
}`
const frontendFields = columns.map((col) => ({
name: col.column_name,
type: mapDBTypeToFrontend(col.data_type) + (col.is_nullable === 'YES' ? ' | null' : ''),
}))
return {
hasPassword: columns.some((col) => col.column_name === 'password'),
enhancedColumns,
createFields,
updateFields,
insertJson,
frontendFields,
relationships: await buildRelationships(table),
}
}
/** Lucid relationship decorators inferred from the `<table>_id` column convention. */
async function buildRelationships(table: string) {
const lines: string[] = []
const imports: Set<string> = new Set()
const columns = await getColumnsForTable(table)
const references = await getForeignKeysReferencing(table)
for (const col of columns) {
if (col.column_name.endsWith('_id')) {
const refTable = col.column_name.replace('_id', '')
const refModel = capitalize(refTable)
imports.add('belongsTo')
lines.push(
` @belongsTo(() => ${refModel})\n declare ${refTable}: BelongsTo<typeof ${refModel}>`
)
}
}
for (const ref of references) {
const relModel = capitalize(ref.table_name)
imports.add('hasMany')
lines.push(
` @hasMany(() => ${relModel})\n declare ${ref.table_name}: HasMany<typeof ${relModel}>`
)
}
return {
imports: Array.from(imports),
code: lines,
}
}