UNPKG

alepha

Version:

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

287 lines (262 loc) 7.63 kB
import { KIND, type Static, type TObject } from "alepha"; import type { BuildExtraConfigColumns, SQL } from "drizzle-orm"; import type { PgColumn, PgColumnBuilderBase, PgTableExtraConfigValue, } from "drizzle-orm/pg-core"; import { insertSchema, type TObjectInsert } from "../schemas/insertSchema.ts"; import { type TObjectUpdate, updateSchema } from "../schemas/updateSchema.ts"; /** * Creates a database entity primitive that defines table structure using TypeBox schemas. * * @example * ```ts * import { z } from "alepha"; * import { $entity } from "alepha/orm"; * * const userEntity = $entity({ * name: "users", * schema: z.object({ * id: pg.primaryKey(), * name: z.text(), * email: z.email(), * }), * }); * ``` */ export const $entity = <TSchema extends TObject>( options: EntityPrimitiveOptions<TSchema>, ): EntityPrimitive<TSchema> => { return new EntityPrimitive<TSchema>(options); }; // --------------------------------------------------------------------------------------------------------------------- export interface EntityPrimitiveOptions< T extends TObject, Keys = keyof Static<T>, > { /** * The database table name that will be created for this entity. * If not provided, name will be inferred from the $repository variable name. */ name: string; /** * TypeBox schema defining the table structure and column types. */ schema: T; /** * Database indexes to create for query optimization. */ indexes?: ( | Keys | { /** * Single column to index. */ column: Keys; /** * Whether this should be a unique index (enforces uniqueness constraint). */ unique?: boolean; /** * Custom name for the index. If not provided, generates name automatically. */ name?: string; /** * Partial index condition. Only rows matching this SQL expression are indexed. */ where?: SQL; } | { /** * Multiple columns for composite index (order matters for query optimization). */ columns: Keys[]; /** * Whether this should be a unique index (enforces uniqueness constraint). */ unique?: boolean; /** * Custom name for the index. If not provided, generates name automatically. */ name?: string; /** * Partial index condition. Only rows matching this SQL expression are indexed. */ where?: SQL; } | { /** * SQL expressions for expression-based indexes. * * Can include column references and SQL functions like `LOWER()`, `UPPER()`, etc. * Columns and expressions can be mixed together. * * @example * ```ts * // Case-insensitive unique username per realm * indexes: [{ * expressions: (self) => [self.realm, sql`LOWER(${self.username})`], * unique: true, * name: "users_realm_username_lower_idx", * }] * ``` */ expressions: (self: Record<Keys & string, any>) => (SQL | any)[]; /** * Whether this should be a unique index (enforces uniqueness constraint). */ unique?: boolean; /** * Custom name for the index. If not provided, generates name automatically. */ name: string; /** * Partial index condition. Only rows matching this SQL expression are indexed. */ where?: SQL; } )[]; /** * Foreign key constraints to maintain referential integrity. */ foreignKeys?: Array<{ /** * Optional name for the foreign key constraint. */ name?: string; /** * Local columns that reference the foreign table. */ columns: Array<keyof Static<T>>; /** * Referenced columns in the foreign table. * Must be EntityColumn references from other entities. */ foreignColumns: Array<() => EntityColumn<any>>; }>; /** * Additional table constraints for data validation. * * Constraints enforce business rules at the database level, providing * an additional layer of data integrity beyond application validation. * * **Constraint Types**: * - **Unique constraints**: Prevent duplicate values across columns * - **Check constraints**: Enforce custom validation rules with SQL expressions * * @example * ```ts * constraints: [ * { * name: "unique_user_email", * columns: ["email"], * unique: true * }, * { * name: "valid_age_range", * columns: ["age"], * check: sql`age >= 0 AND age <= 150` * }, * { * name: "unique_user_username_per_tenant", * columns: ["tenantId", "username"], * unique: true * } * ] * ``` */ constraints?: Array<{ /** * Columns involved in this constraint. */ columns: Array<keyof Static<T>>; /** * Optional name for the constraint. */ name?: string; /** * Whether this is a unique constraint. */ unique?: boolean | {} /* options */; /** * SQL expression for check constraint validation. */ check?: SQL; }>; /** * Advanced Drizzle ORM configuration for complex table setups. */ config?: ( self: BuildExtraConfigColumns<string, FromSchema<T>, "pg">, ) => PgTableExtraConfigValue[]; } // --------------------------------------------------------------------------------------------------------------------- export class EntityPrimitive<T extends TObject = TObject> { public readonly options: EntityPrimitiveOptions<T>; constructor(options: EntityPrimitiveOptions<T>) { this.options = options; } alias(alias: string): this { const aliased = new EntityPrimitive<T>(this.options); return new Proxy(aliased, { get(target, prop, receiver) { if (prop === "$alias") { return alias; } return Reflect.get(target, prop, receiver); }, }) as this; } get cols(): EntityColumns<T> { const cols: Partial<EntityColumns<T>> = {}; for (const key of Object.keys(this.schema.properties) as Array< keyof T["properties"] >) { cols[key] = { name: key as string, entity: this, }; } return cols as EntityColumns<T>; } get name(): string { return this.options.name; } get schema(): T { return this.options.schema; } protected _insertSchema?: TObjectInsert<T>; get insertSchema(): TObjectInsert<T> { this._insertSchema ??= insertSchema(this.options.schema); return this._insertSchema; } protected _updateSchema?: TObjectUpdate<T>; get updateSchema(): TObjectUpdate<T> { this._updateSchema ??= updateSchema(this.options.schema); return this._updateSchema; } } $entity[KIND] = EntityPrimitive; // --------------------------------------------------------------------------------------------------------------------- /** * Convert a schema to columns. */ export type FromSchema<T extends TObject> = { [key in keyof T["properties"]]: PgColumnBuilderBase; }; export type SchemaToTableConfig<T extends TObject> = { name: string; schema: string | undefined; columns: { [key in keyof T["properties"]]: PgColumn; }; dialect: string; }; export type EntityColumn<T extends TObject> = { name: string; entity: EntityPrimitive<T>; }; export type EntityColumns<T extends TObject> = { [key in keyof T["properties"]]: EntityColumn<T>; };