adba
Version:
Any DataBase to API
215 lines (188 loc) • 7.46 kB
text/typescript
import { Model, RelationMappings, JSONSchema } from 'objection';
import { Knex } from 'knex';
import { deepMerge } from 'dbl-utils';
import { className } from './model-utilities';
import { IGenerateModelsOptions } from './types';
/**
* Generates MSSQL models dynamically based on database structures.
* @param knexInstance - The Knex instance connected to the database.
* @param opts - Options including parse and format functions.
* @returns A promise that resolves to an object containing all generated models.
*/
export async function generateMSSQLModels(
knexInstance: Knex,
opts: IGenerateModelsOptions = {}
): Promise<Record<string, typeof Model>> {
const models: Record<string, typeof Model> = {};
const { relationsFunc, squemaFixings, parseFunc, formatFunc } = opts;
try {
// Query table and view structures from the database
const structures = await knexInstance.raw(`
SELECT TABLE_NAME as name, TABLE_TYPE as type
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_TYPE IN ('BASE TABLE', 'VIEW')
`).then(res => res.recordset);
for (const { name: structureName, type } of structures) {
const columns = await knexInstance.raw(`
SELECT COLUMN_NAME as column_name, DATA_TYPE as data_type, IS_NULLABLE as is_nullable, COLUMN_DEFAULT as column_default
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = ?
`, [structureName]).then(res => res.recordset);
const foreignKeys = await knexInstance.raw(`
SELECT
fk.name AS fk_name,
tp.name AS table,
rcp.name AS to,
cp.name AS from
FROM sys.foreign_keys AS fk
INNER JOIN sys.foreign_key_columns AS fkc ON fk.object_id = fkc.constraint_object_id
INNER JOIN sys.tables AS tp ON fkc.parent_object_id = tp.object_id
INNER JOIN sys.columns AS cp ON fkc.parent_object_id = cp.object_id AND fkc.parent_column_id = cp.column_id
INNER JOIN sys.tables AS tr ON fkc.referenced_object_id = tr.object_id
INNER JOIN sys.columns AS rcp ON fkc.referenced_object_id = rcp.object_id AND fkc.referenced_column_id = rcp.column_id
WHERE tp.name = ?
`, [structureName]).then(res => res.recordset);
// Define a dynamic model class
const DynamicModel = class extends Model {
/**
* Returns the table name for the model.
*/
static get tableName(): string {
return structureName;
}
/**
* Constructs the JSON schema based on the table structure.
* @returns The JSON schema object for the table.
*/
static get jsonSchema(): JSONSchema {
const requiredFields: string[] = [];
const schemaProperties: Record<string, JSONSchema> = {};
for (const column of columns) {
const format = mapMssqlTypeToJsonFormat(column.data_type, column.column_name);
const type = mapMssqlTypeToJsonType(column.data_type);
const property: JSONSchema = {
type: type !== 'buffer' ? type : undefined
};
if (column.is_nullable === 'NO' && !column.column_default) {
requiredFields.push(column.column_name);
}
property.$comment = [type, format].filter(Boolean).join('.');
const prefix = type === 'buffer' ? 'x-' : '';
schemaProperties[prefix + column.column_name] = property;
}
if (typeof squemaFixings === 'function') {
const r = squemaFixings(structureName, schemaProperties);
if (r) deepMerge(schemaProperties, r);
}
return {
type: 'object',
properties: schemaProperties,
required: requiredFields.length ? requiredFields : undefined,
};
}
/**
* Constructs relation mappings for the model.
* @returns An object containing relation mappings.
*/
static get relationMappings(): RelationMappings {
const relations: RelationMappings = {};
for (const fk of foreignKeys) {
const relatedModel = Object.values(models).find((Model) => Model.tableName === fk.table);
if (!relatedModel) {
throw new Error(`${structureName}: Model for table ${fk.table} not found`);
}
relations[`${fk.table}`] = {
relation: Model.BelongsToOneRelation,
modelClass: relatedModel,
join: {
from: `${structureName}.${fk.from}`,
to: `${fk.table}.${fk.to}`,
},
};
}
if (typeof relationsFunc === 'function') {
const r = relationsFunc(structureName, relations);
if (r) Object.assign(relations, r);
}
return relations;
}
/**
* Parses the database JSON.
* @param json The JSON object from the database.
* @returns The parsed JSON object.
*/
$parseDatabaseJson(json: any) {
json = super.$parseDatabaseJson(json);
return typeof parseFunc === 'function' ? parseFunc(structureName, json) : json;
}
/**
* Formats the JSON object for the database.
* @param json The JSON object to be formatted.
* @returns The formatted JSON object.
*/
$formatDatabaseJson(json: any) {
json = super.$formatDatabaseJson(json);
return typeof formatFunc === 'function' ? formatFunc(structureName, json) : json;
}
};
const pascalCaseName = className(structureName);
const suffix = type === 'BASE TABLE' ? 'Table' : 'View';
const modelName = `${pascalCaseName}${suffix}Model`;
Object.defineProperty(DynamicModel, 'name', { value: modelName });
DynamicModel.knex(knexInstance);
models[modelName] = DynamicModel;
}
} catch (err) {
console.error('Error generating models:', err);
}
return models;
}
/**
* Maps MSSQL data types to corresponding JSON Schema types.
* @param mssqlType - The MSSQL data type.
* @returns The JSON Schema type.
*/
export function mapMssqlTypeToJsonType(mssqlType: string): string {
const baseType = mssqlType.toUpperCase();
const typeMap: Record<string, string> = {
BIT: 'boolean',
VARBINARY: 'buffer',
BINARY: 'buffer',
BIGINT: 'integer',
INT: 'integer',
SMALLINT: 'integer',
TINYINT: 'integer',
DECIMAL: 'number',
FLOAT: 'number',
NUMERIC: 'number',
REAL: 'number',
CHAR: 'string',
VARCHAR: 'string',
NVARCHAR: 'string',
TEXT: 'string',
NTEXT: 'string',
DATE: 'string',
DATETIME: 'string',
DATETIME2: 'string',
SMALLDATETIME: 'string',
TIME: 'string',
};
return typeMap[baseType] || 'string';
}
/**
* Maps MSSQL data types to corresponding JSON Schema formats.
* @param mssqlType - The MSSQL data type.
* @param colName - The column name for potential additional format inference.
* @returns The JSON Schema format if applicable.
*/
export function mapMssqlTypeToJsonFormat(mssqlType: string, colName: string): string | undefined {
const baseType = mssqlType.toUpperCase();
const typeMap: Record<string, string> = {
DATE: 'date',
DATETIME: 'datetime',
DATETIME2: 'datetime',
SMALLDATETIME: 'datetime',
TIME: 'time',
};
return typeMap[baseType] || undefined;
}