alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
263 lines (236 loc) • 8.96 kB
text/typescript
import { AlephaError } from "alepha";
import type { SQL } from "drizzle-orm";
import type { EntityPrimitive } from "../primitives/$entity.ts";
import type { SequencePrimitive } from "../primitives/$sequence.ts";
/**
* Database-specific table configuration functions
*/
export interface TableConfigBuilders<TConfig> {
index: (name: string) => {
on: (...columns: any[]) => TConfig;
};
uniqueIndex: (name: string) => {
on: (...columns: any[]) => TConfig;
};
unique: (name: string) => {
on: (...columns: any[]) => TConfig;
};
check: (name: string, sql: SQL) => TConfig;
foreignKey: (config: {
name: string;
columns: any[];
foreignColumns: any[];
}) => TConfig;
}
/**
* Abstract base class for transforming Alepha Primitives (Entity, Sequence, etc...)
* into drizzle models (tables, enums, sequences, etc...).
*/
export abstract class ModelBuilder {
/**
* Build a table from an entity primitive.
*/
abstract buildTable(
entity: EntityPrimitive,
options: {
tables: Map<string, unknown>;
enums: Map<string, unknown>;
schemas: Map<string, unknown>;
schema: string;
},
): void;
/**
* Build a sequence from a sequence primitive.
*/
abstract buildSequence(
sequence: SequencePrimitive,
options: {
sequences: Map<string, unknown>;
schema: string;
},
): void;
/**
* Convert camelCase to snake_case for column names.
*/
protected toColumnName(str: string): string {
return (
str[0].toLowerCase() +
str.slice(1).replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
);
}
/**
* Build the table configuration function for any database.
* This includes indexes, foreign keys, constraints, and custom config.
*
* @param entity - The entity primitive
* @param builders - Database-specific builder functions
* @param tableResolver - Function to resolve entity references to table columns
* @param customConfigHandler - Optional handler for custom config
*/
protected buildTableConfig<TConfig, TSelf>(
entity: EntityPrimitive,
builders: TableConfigBuilders<TConfig>,
tableResolver?: (entityName: string) => any,
customConfigHandler?: (config: any, self: TSelf) => TConfig[],
): ((self: TSelf) => TConfig[]) | undefined {
// If no extra config is needed, return undefined
if (
!entity.options.indexes &&
!entity.options.foreignKeys &&
!entity.options.constraints &&
!entity.options.config
) {
return undefined;
}
return (self: TSelf) => {
const configs: TConfig[] = [];
// Build indexes
if (entity.options.indexes) {
for (const indexDef of entity.options.indexes) {
if (typeof indexDef === "string") {
const columnName = this.toColumnName(indexDef);
const indexName = `${entity.name}_${columnName}_idx`;
// Use original camelCase property name for lookup
if ((self as any)[indexDef]) {
configs.push(
builders.index(indexName).on((self as any)[indexDef]),
);
}
} else if (typeof indexDef === "object" && indexDef !== null) {
if ("column" in indexDef) {
const columnName = this.toColumnName(indexDef.column as string);
const indexName =
indexDef.name || `${entity.name}_${columnName}_idx`;
// Use original camelCase property name for lookup
if ((self as any)[indexDef.column]) {
let idx = indexDef.unique
? builders
.uniqueIndex(indexName)
.on((self as any)[indexDef.column])
: builders
.index(indexName)
.on((self as any)[indexDef.column]);
if ("where" in indexDef && indexDef.where) {
idx = (idx as any).where(indexDef.where);
}
configs.push(idx);
}
} else if ("expressions" in indexDef) {
const parts = indexDef.expressions(self as any);
if (parts.length > 0) {
let idx = indexDef.unique
? builders.uniqueIndex(indexDef.name).on(...parts)
: builders.index(indexDef.name).on(...parts);
if ("where" in indexDef && indexDef.where) {
idx = (idx as any).where(indexDef.where);
}
configs.push(idx);
}
} else if ("columns" in indexDef) {
const columnNames = indexDef.columns.map((col: any) =>
this.toColumnName(col as string),
);
const indexName =
indexDef.name || `${entity.name}_${columnNames.join("_")}_idx`;
// Use original camelCase property names for lookup
const cols = indexDef.columns
.map((col: any) => (self as any)[col])
.filter(Boolean);
if (cols.length === indexDef.columns.length) {
let idx = indexDef.unique
? builders.uniqueIndex(indexName).on(...cols)
: builders.index(indexName).on(...cols);
if ("where" in indexDef && indexDef.where) {
idx = (idx as any).where(indexDef.where);
}
configs.push(idx);
}
}
}
}
}
// Build foreign keys
if (entity.options.foreignKeys) {
for (const fkDef of entity.options.foreignKeys) {
const columnNames = fkDef.columns.map((col) =>
this.toColumnName(col as string),
);
// Use original camelCase property names for lookup
const cols = fkDef.columns
.map((col) => (self as any)[col])
.filter(Boolean);
if (cols.length === fkDef.columns.length) {
const fkName =
fkDef.name || `${entity.name}_${columnNames.join("_")}_fk`;
// Resolve foreign column references
const foreignColumns = fkDef.foreignColumns.map((colRef) => {
const entityCol = colRef();
if (!entityCol?.entity || !entityCol.name) {
throw new AlephaError(
`Invalid foreign column reference in ${entity.name}`,
);
}
// If we have a table resolver, use it to get the actual table column
if (tableResolver) {
const foreignTable = tableResolver(entityCol.entity.name);
if (!foreignTable) {
throw new AlephaError(
`Foreign table ${entityCol.entity.name} not found for ${entity.name}`,
);
}
// Use original camelCase property name for lookup in foreign table
return foreignTable[entityCol.name];
}
// Fallback: return the entity column reference (will be resolved later)
return entityCol;
});
configs.push(
builders.foreignKey({
name: fkName,
columns: cols,
foreignColumns,
}),
);
}
}
}
// Build constraints
if (entity.options.constraints) {
for (const constraintDef of entity.options.constraints) {
const columnNames = constraintDef.columns.map((col) =>
this.toColumnName(col as string),
);
// Use original camelCase property names for lookup
const cols = constraintDef.columns
.map((col) => (self as any)[col])
.filter(Boolean);
if (cols.length === constraintDef.columns.length) {
if (constraintDef.unique) {
const constraintName =
constraintDef.name ||
`${entity.name}_${columnNames.join("_")}_unique`;
configs.push(builders.unique(constraintName).on(...cols));
}
if (constraintDef.check) {
const constraintName =
constraintDef.name ||
`${entity.name}_${columnNames.join("_")}_check`;
configs.push(builders.check(constraintName, constraintDef.check));
}
}
}
}
// Add custom config if provided
if (entity.options.config && customConfigHandler) {
configs.push(...customConfigHandler(entity.options.config, self));
} else if (entity.options.config) {
// Default behavior: call the config function directly
const customConfigs = entity.options.config(self as any);
if (Array.isArray(customConfigs)) {
configs.push(...(customConfigs as any));
}
}
return configs;
};
}
}