adba
Version:
Any DataBase to API
267 lines (238 loc) • 8.65 kB
text/typescript
import { Model, RelationMappings, JSONSchema } from "objection";
import { Knex } from "knex";
import { deepMerge } from "dbl-utils";
import {
className,
jsonSchemaToColumns,
ITableColumn,
} from "./model-utilities";
import { IGenerateModelsOptions } from "./types";
/**
* Generates MySQL 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 generateMySQLModels(
knexInstance: Knex,
opts: IGenerateModelsOptions = {}
): Promise<Record<string, typeof Model>> {
const models: Record<string, typeof Model> = {};
const { relationsFunc, squemaFixings, columnsFunc, parseFunc, formatFunc } =
opts;
try {
// Query table and view structures from the database
const [tables, views] = await Promise.all([
knexInstance("information_schema.tables")
.where("table_schema", knexInstance.client.config.connection.database)
.select("table_name AS name", knexInstance.raw(`'table' as type`)),
knexInstance("information_schema.views")
.where("table_schema", knexInstance.client.config.connection.database)
.select("table_name AS name", knexInstance.raw(`'view' as type`)),
]);
const structures = [...tables, ...views];
for (const { name: structureName, type } of structures) {
const columns = await knexInstance("information_schema.columns")
.where("table_schema", knexInstance.client.config.connection.database)
.andWhere("table_name", structureName)
.select(
"column_name",
"data_type",
"is_nullable",
"column_comment",
"column_key",
"column_default",
"extra"
);
const foreignKeys = await knexInstance(
"information_schema.key_column_usage"
)
.where("table_schema", knexInstance.client.config.connection.database)
.andWhere("table_name", structureName)
.andWhereNot("referenced_table_name", null)
.select(
"column_name AS from",
"referenced_table_name AS table",
"referenced_column_name AS to"
);
// 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 = mapMySqlTypeToJsonFormat(
column.data_type,
column.column_name
);
const type = mapMySqlTypeToJsonType(column.data_type);
const property: JSONSchema = {
type: type !== "buffer" ? type : undefined,
description: column.column_comment || undefined,
};
const isNotNullable = column.is_nullable === "NO";
const isAutoIncrement = column.extra
?.toLowerCase()
.includes("auto_increment");
const hasDefault = column.column_default !== null;
if (isNotNullable && !isAutoIncrement && !hasDefault) {
requiredFields.push(column.column_name);
}
if (
/^(INT|INTEGER|REAL)$/i.test(column.data_type) &&
column.column_key !== "PRI"
) {
property.minimum = 0;
}
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;
}
/**
* Columns information derived from the JSON schema.
*/
static get columns(): Record<string, ITableColumn> {
const { properties = {}, required = [] } = this.jsonSchema as any;
const cols = jsonSchemaToColumns(properties, required as string[]);
if (typeof columnsFunc === "function") {
const r = columnsFunc(structureName, cols);
if (r) deepMerge(cols, r);
}
return cols;
}
/**
* 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 === "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 MySQL data types to corresponding JSON Schema types.
* @param mysqlType - The MySQL data type.
* @returns The JSON Schema type.
*/
export function mapMySqlTypeToJsonType(mysqlType: string): string {
const baseType = mysqlType.toUpperCase();
const typeMap: Record<string, string> = {
BOOLEAN: "boolean",
BINARY: "buffer",
BLOB: "buffer",
BIGINT: "integer",
INT: "integer",
INTEGER: "integer",
MEDIUMINT: "integer",
SMALLINT: "integer",
TINYINT: "integer",
DECIMAL: "number",
DOUBLE: "number",
FLOAT: "number",
NUMERIC: "number",
REAL: "number",
CHAR: "string",
VARCHAR: "string",
TEXT: "string",
DATE: "string",
DATETIME: "string",
TIMESTAMP: "string",
TIME: "string",
};
return typeMap[baseType] || "string";
}
/**
* Maps MySQL data types to corresponding JSON Schema formats.
* @param mysqlType - The MySQL data type.
* @param colName - The column name for potential additional format inference.
* @returns The JSON Schema format if applicable.
*/
export function mapMySqlTypeToJsonFormat(
mysqlType: string,
colName: string
): string | undefined {
const baseType = mysqlType.toUpperCase();
const typeMap: Record<string, string> = {
DATE: "date",
DATETIME: "datetime",
TIMESTAMP: "datetime",
TIME: "time",
};
return typeMap[baseType] || undefined;
}