UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

263 lines (236 loc) 8.96 kB
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; }; } }