@adonisjs/lucid
Version:
SQL ORM built on top of Active Record pattern
122 lines (121 loc) • 4.66 kB
JavaScript
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <virk@adonisjs.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
import merge from 'deepmerge';
import { ImportsBag } from '@poppinss/utils';
import stringHelpers from '@adonisjs/core/helpers/string';
import { DEFAULT_SCHEMA_RULES } from "./rules.js";
import { DATA_TYPES_MAPPING, INTERNAL_TYPES } from "./mappings.js";
/**
* OrmSchemaBuilder handles the core logic of generating TypeScript
* model schemas from database table structures.
*/
export class OrmSchemaBuilder {
client;
/**
* Schema rules for type mapping and customization
*/
schema = DEFAULT_SCHEMA_RULES;
/**
* Mapping from database types to internal type identifiers
*/
dataTypes = DATA_TYPES_MAPPING;
constructor(client) {
this.client = client;
}
/**
* Load user-defined schema rules
*/
loadRules(rules) {
this.schema = merge.all([this.schema, ...rules]);
}
/**
* Generate schema for a single column
*/
generateColumnSchema(columnName, column, tableName) {
const dialectColumnType = `${this.client.dialect.name}.${column.type}`;
// Map database type to internal type identifier
// If no mapping exists, use the database type name itself
const internalType = this.dataTypes[dialectColumnType] ?? this.dataTypes[column.type] ?? column.type;
// For unknown internal types, allow users to define custom rules using the database type name
const typeLookupKey = internalType === INTERNAL_TYPES.UNKNOWN ? column.type : internalType;
// Lookup hierarchy (most specific to least specific):
// 1. Table-specific column
// 2. Table-specific type
// 3. Global column name
// 4. Global type
const ruleDef = this.schema.tables[tableName]?.columns?.[columnName] ??
this.schema.tables[tableName]?.types?.[typeLookupKey] ??
this.schema.columns[columnName] ??
this.schema.types[typeLookupKey];
const rule = typeof ruleDef === 'function' ? ruleDef(typeLookupKey) : ruleDef;
// Default to 'any' type if no rule is defined
const finalRule = rule ?? {
tsType: 'any',
imports: [],
decorator: '@column()',
};
let tsType = finalRule.tsType;
let propertyName = stringHelpers.camelCase(columnName);
if (column.nullable) {
tsType += ' | null';
}
return {
imports: finalRule.imports ?? [],
propertyName,
column: ` ${finalRule.decorator}\n declare ${propertyName}: ${tsType}`,
};
}
/**
* Generate schema for a single table (internal method)
*/
generateTableSchema(tableName, columns, importsBag) {
const schema = Object.keys(columns).map((columnName) => {
const column = columns[columnName];
return this.generateColumnSchema(columnName, column, tableName);
});
// Add column-specific imports
schema.forEach((col) => {
col.imports.forEach((imp) => importsBag.add(imp));
});
const className = stringHelpers.pascalCase(stringHelpers.singular(tableName));
// Return complete class definition as a single string (compact format)
return [
`export class ${className}Schema extends BaseModel {`,
` static $columns = [${schema.map((k) => `'${k.propertyName}'`).join(', ')}] as const`,
` $columns = ${className}Schema.$columns`,
schema.map((k) => k.column).join('\n'),
'}',
].join('\n');
}
/**
* Generate schemas for multiple tables
*/
generateSchemas(tables) {
const importsBag = new ImportsBag();
const classesToCreate = [];
// Add base import
importsBag.add({ source: '@adonisjs/lucid/orm', namedImports: ['BaseModel', 'column'] });
tables.forEach((table) => {
const classDefinition = this.generateTableSchema(table.name, table.columns, importsBag);
classesToCreate.push(classDefinition);
});
return {
imports: importsBag.toArray(),
classes: classesToCreate,
};
}
/**
* Get the final output string from generated schemas
*/
getOutput(schemas) {
const importsBag = new ImportsBag();
schemas.imports.forEach((imp) => importsBag.add(imp));
return `${importsBag.toString()}\n\n${schemas.classes.join('\n\n')}\n`;
}
}