alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
1,419 lines (1,418 loc) • 111 kB
JavaScript
import { createRequire } from "node:module";
import { $atom, $context, $env, $hook, $inject, $mode, $module, $state, Alepha, AlephaError, KIND, Primitive, SchemaValidator, createMiddleware, createPagination, createPrimitive, pageQuerySchema, pageSchema, pageSchema as pageSchema$1, z } from "alepha";
import { AlephaDateTime, DateTimeProvider } from "alepha/datetime";
import { mkdir, stat } from "node:fs/promises";
import { $logger } from "alepha/logger";
import * as drizzle from "drizzle-orm";
import { and, arrayContained, arrayContains, arrayOverlaps, asc, avg, between, count, desc, eq, exists, getTableName, gt, gte, ilike, inArray, isNotNull, isNull, isSQLWrapper, like, lt, lte, max, min, ne, not, notBetween, notExists, notIlike, notInArray, notLike, or, sql, sql as sql$1, sum } from "drizzle-orm";
import { alias, customType } from "drizzle-orm/pg-core";
import { CryptoProvider } from "alepha/crypto";
import { currentTenantAtom, currentUserAtom } from "alepha/security";
import { isSQLWrapper as isSQLWrapper$1 } from "drizzle-orm/sql/sql";
import { dirname } from "node:path";
import { randomUUID } from "node:crypto";
import * as pg from "drizzle-orm/sqlite-core";
import { check, foreignKey, index, sqliteTable, unique, uniqueIndex } from "drizzle-orm/sqlite-core";
export * from "drizzle-orm/pg-core";
//#endregion
//#region ../../src/orm/core/errors/DbError.ts
var DbError = class extends AlephaError {
name = "DbError";
constructor(message, cause) {
super(message, { cause });
}
};
//#endregion
//#region ../../src/orm/core/schemas/databaseEnvSchema.ts
/**
* Base database environment schema.
*
* Defines the `DATABASE_URL` connection string used by all ORM providers
* to determine the database driver and connection target.
*
* Supported URL formats:
* - `sqlite://:memory:` or `sqlite://./path/to/db` — SQLite (Node.js or Bun)
* - `postgres://user:password@host:port/database` — PostgreSQL (Node.js or Bun)
* - `pglite://:memory:` or `pglite://./path` — PGlite (embedded Postgres)
* - `d1://BINDING_NAME` — Cloudflare D1
* - `hyperdrive://BINDING_NAME` — Cloudflare Hyperdrive
*/
const databaseEnvSchema = z.object({
DATABASE_URL: z.text().optional(),
/**
* Enable or disable push-based schema synchronization (drizzle-kit push).
*
* Defaults to `true` in development and test, `false` in production.
* Set to `false` in development to skip automatic schema sync
* (e.g. when managing migrations manually).
*/
DATABASE_SYNC: z.boolean().optional()
});
//#endregion
//#region ../../src/orm/core/providers/DrizzleKitProvider.ts
var DrizzleKitProvider = class {
log = $logger();
dateTime = $inject(DateTimeProvider);
alepha = $inject(Alepha);
/**
* Push-based synchronization using Drizzle Kit's introspection API.
*
* Reads the actual database state, diffs against current entity definitions,
* and applies changes. No stored snapshots — no drift, no corruption.
*
* - SQLite: uses `pushSQLiteSchema` (requires sync driver — node:sqlite shim or bun-sqlite)
* - PostgreSQL: uses `pushSchema` with schema filters
*
* Does nothing in production mode — use file-based migrations instead.
*/
async synchronize(provider) {
if (this.alepha.isProduction()) {
this.log.warn("Synchronization skipped in production mode.");
return;
}
if (this.alepha.isTest()) {
const { statements } = await this.generateMigration(provider);
await this.executeStatements(statements.map((s) => s.replace(/^CREATE SCHEMA /i, "CREATE SCHEMA IF NOT EXISTS ")), provider);
return;
}
const now = this.dateTime.nowMillis();
const kit = this.importDrizzleKit();
const models = this.getModels(provider);
if (Object.keys(models).length === 0) {
this.log.info(`No models to synchronize for '${provider.name}'`);
return;
}
try {
if (provider.dialect === "sqlite") await this.pushSqlite(kit, models, provider);
else await this.pushPostgres(kit, models, provider);
} catch (error) {
this.log.debug("Push sync not available, falling back to migration generation", { error });
const { statements } = await this.generateMigration(provider);
await this.executeStatementsLenient(statements, provider);
}
this.log.info(`Synchronization of '${provider.name}' OK [${this.dateTime.nowMillis() - now}ms]`);
}
/**
* Generate SQL migration statements by diffing two schema states.
*
* Used by tests (schema validation) and CLI (`alepha db migrations generate`).
* Not part of the push sync flow.
*
* When `withoutSchema` is true, models are rebuilt without schema qualifiers
* so the generated SQL is portable across different PostgreSQL schemas.
*/
async generateMigration(provider, prevSnapshot, options) {
const kit = this.importDrizzleKit();
const models = options?.withoutSchema ? this.getModelsWithoutSchema(provider) : this.getModels(provider);
if (Object.keys(models).length > 0) {
if (provider.dialect === "sqlite") {
const prev = prevSnapshot ?? await kit.generateSQLiteDrizzleJson({});
const curr = await kit.generateSQLiteDrizzleJson(models);
return {
models,
statements: await kit.generateSQLiteMigration(prev, curr),
snapshot: curr
};
}
const prev = prevSnapshot ?? kit.generateDrizzleJson({});
const curr = kit.generateDrizzleJson(models);
return {
models,
statements: await kit.generateMigration(prev, curr),
snapshot: curr
};
}
return {
models,
statements: [],
snapshot: {}
};
}
/**
* Load all tables, enums, sequences, etc. from the provider's repositories.
*/
getModels(provider) {
const models = {};
for (const [key, value] of provider.schemas.entries()) models[`__schema_${key}`] = value;
for (const [key, value] of provider.tables.entries()) {
if (models[key]) throw new AlephaError(`Model name conflict: '${key}' is already defined.`);
models[key] = value;
}
for (const [key, value] of provider.enums.entries()) {
if (models[key]) throw new AlephaError(`Model name conflict: '${key}' is already defined.`);
models[key] = value;
}
for (const [key, value] of provider.sequences.entries()) {
if (models[key]) throw new AlephaError(`Model name conflict: '${key}' is already defined.`);
models[key] = value;
}
return models;
}
/**
* Build schema-free models for migration generation.
*
* Rebuilds all entities with `schema = "public"` so Drizzle produces
* SQL without schema qualifiers (e.g. `CREATE TABLE "users"` instead
* of `CREATE TABLE "myschema"."users"`).
*
* The actual schema is applied at migration execution time via `search_path`.
*/
getModelsWithoutSchema(provider) {
const maps = provider.rebuildModels("public");
const models = {};
for (const [key, value] of maps.tables.entries()) {
if (models[key]) throw new AlephaError(`Model name conflict: '${key}' is already defined.`);
models[key] = value;
}
for (const [key, value] of maps.enums.entries()) {
if (models[key]) throw new AlephaError(`Model name conflict: '${key}' is already defined.`);
models[key] = value;
}
for (const [key, value] of maps.sequences.entries()) {
if (models[key]) throw new AlephaError(`Model name conflict: '${key}' is already defined.`);
models[key] = value;
}
return models;
}
/**
* Preview schema push without executing any statements.
*
* Returns the SQL statements that would be executed, warnings, and
* whether data loss would occur. Does NOT execute any SQL.
*/
async dryRunPush(provider) {
const kit = this.importDrizzleKit();
const models = this.getModels(provider);
if (Object.keys(models).length === 0) return {
statements: [],
warnings: [],
hasDataLoss: false
};
let result;
if (provider.dialect === "sqlite") result = await kit.pushSQLiteSchema(models, provider.db);
else {
const wrappedDb = this.wrapDbForDrizzleKit(provider.db);
result = await kit.pushSchema(models, wrappedDb, [provider.schema]);
}
return {
statements: result.statementsToExecute,
warnings: result.warnings,
hasDataLoss: result.hasDataLoss
};
}
async pushSqlite(kit, models, provider) {
const { statementsToExecute } = await kit.pushSQLiteSchema(models, provider.db);
await this.executeStatements(statementsToExecute, provider);
}
/**
* Push schema changes to PostgreSQL using Drizzle Kit's pushSchema with schema filters.
*/
async pushPostgres(kit, models, provider) {
if (provider.schema !== "public") await this.createSchemaIfNotExists(provider, provider.schema);
const wrappedDb = this.wrapDbForDrizzleKit(provider.db);
const { statementsToExecute } = await kit.pushSchema(models, wrappedDb, [provider.schema]);
await this.executeStatements(statementsToExecute, provider);
}
/**
* Execute a list of SQL statements against the provider.
*/
async executeStatements(statements, provider) {
if (statements.length > 0) this.log.debug(`Executing ${statements.length} statements ...`, { statements });
for (const statement of statements) await provider.execute(sql$1.raw(statement));
}
/**
* Execute SQL statements, ignoring "already exists" errors.
*
* Used by the fallback migration path where push may have partially
* applied changes before erroring, leaving some objects already created.
*/
async executeStatementsLenient(statements, provider) {
if (statements.length > 0) this.log.debug(`Executing ${statements.length} statements (lenient) ...`, { statements });
for (const statement of statements) try {
await provider.execute(sql$1.raw(statement));
} catch (error) {
if ((error?.message ?? "").includes("already exists")) {
this.log.debug(`Skipped (already exists): ${statement.slice(0, 80)}`);
continue;
}
throw error;
}
}
async createSchemaIfNotExists(provider, schemaName) {
if (!/^[a-z0-9_]+$/i.test(schemaName)) throw new AlephaError(`Invalid schema name: ${schemaName}. Must only contain alphanumeric characters and underscores.`);
const sqlSchema = sql$1.raw(schemaName);
if (schemaName.startsWith("test_")) {
this.log.info(`Drop test schema '${schemaName}' ...`, schemaName);
await provider.execute(sql$1`DROP SCHEMA IF EXISTS ${sqlSchema} CASCADE`);
}
this.log.debug(`Ensuring schema '${schemaName}' exists`);
await provider.execute(sql$1`CREATE SCHEMA IF NOT EXISTS ${sqlSchema}`);
}
/**
* Wrap a Drizzle PgDatabase instance for compatibility with Drizzle Kit.
*
* Drizzle Kit's pushSchema expects execute() to return { rows: T[] }
* (node-postgres/pg format), but postgres.js returns a Result that
* extends Array directly — no .rows property.
*/
wrapDbForDrizzleKit(db) {
return new Proxy(db, { get(target, prop, receiver) {
if (prop === "execute") return async (...args) => {
const res = await target.execute(...args);
if (Array.isArray(res) && !("rows" in res)) return Object.assign(res, { rows: [...res] });
return res;
};
return Reflect.get(target, prop, receiver);
} });
}
/**
* Try to load the official Drizzle Kit API.
*/
importDrizzleKit() {
try {
return createRequire(import.meta.url)("drizzle-kit/api");
} catch (_) {
throw new AlephaError("Drizzle Kit is not installed. Please install it with `npm install -D drizzle-kit`.");
}
}
};
//#endregion
//#region ../../src/orm/core/providers/drivers/DatabaseProvider.ts
var DatabaseProvider = class {
alepha = $inject(Alepha);
dateTime = $inject(DateTimeProvider);
log = $logger();
kit = $inject(DrizzleKitProvider);
enums = /* @__PURE__ */ new Map();
tables = /* @__PURE__ */ new Map();
sequences = /* @__PURE__ */ new Map();
schemas = /* @__PURE__ */ new Map();
entityPrimitives = [];
sequencePrimitives = [];
get name() {
return "default";
}
get driver() {
return this.dialect;
}
/**
* Whether this driver supports SQL-level transactions (BEGIN/COMMIT/ROLLBACK).
*
* Drivers that do not (e.g. PGlite, Cloudflare D1) should override to `false`.
*/
get supportsTransactions() {
return true;
}
/**
* Raw database connection handle (e.g. DatabaseSync, bun:sqlite Database).
* Override in providers that expose native connections for introspection.
*/
get nativeConnection() {}
get schema() {
return "public";
}
/**
* Migration tracking table name, scoped by schema.
*
* Returns `migrations_{schema}` so that multiple schemas sharing the same
* database each get their own migration history (e.g. `migrations_lore`,
* `migrations_public`).
*/
get migrationsTable() {
return `migrations_${this.schema}`;
}
/**
* Log a database query with structured metadata for devtools inspection.
*/
logQuery(sql, params, duration, rowCount, error) {
const operation = this.parseOperation(sql);
const data = {
type: "db:query",
sql,
params,
operation,
duration: Math.round(duration * 100) / 100,
rowCount,
success: !error,
error
};
if (error) this.log.warn(`Query failed (${operation})`, data);
else this.log.debug(`Query OK (${operation}, ${Math.round(duration)}ms)`, data);
}
parseOperation(sql) {
const trimmed = sql.trimStart().toUpperCase();
if (trimmed.startsWith("SELECT")) return "SELECT";
if (trimmed.startsWith("INSERT")) return "INSERT";
if (trimmed.startsWith("UPDATE")) return "UPDATE";
if (trimmed.startsWith("DELETE")) return "DELETE";
if (trimmed.startsWith("CREATE")) return "CREATE";
if (trimmed.startsWith("ALTER")) return "ALTER";
if (trimmed.startsWith("DROP")) return "DROP";
return "OTHER";
}
table(entity) {
const table = this.tables.get(entity.name);
if (!table) throw new AlephaError(`Table '${entity.name}' is not registered`);
const hasAlias = entity.$alias;
if (hasAlias) return alias(table, hasAlias);
return table;
}
registerEntity(entity) {
this.entityPrimitives.push(entity);
this.builder.buildTable(entity, this);
}
registerSequence(sequence) {
this.sequencePrimitives.push(sequence);
this.builder.buildSequence(sequence, this);
}
/**
* Rebuild all models into fresh maps using a different schema.
*
* When called with `"public"`, produces schema-free models suitable for
* migration generation (no schema qualifiers in the SQL output).
*/
rebuildModels(targetSchema) {
const result = {
tables: /* @__PURE__ */ new Map(),
enums: /* @__PURE__ */ new Map(),
sequences: /* @__PURE__ */ new Map(),
schemas: /* @__PURE__ */ new Map(),
schema: targetSchema
};
for (const entity of this.entityPrimitives) this.builder.buildTable(entity, result);
for (const seq of this.sequencePrimitives) this.builder.buildSequence(seq, result);
return result;
}
/**
* Run a function inside a database transaction with implicit tx propagation.
*
* The transaction object is stored in `alepha.store` so that all Repository
* operations within `fn` automatically participate in the transaction without
* explicit `{ tx }` drilling.
*
* Nesting is safe — if already inside a `transactional()` block, the inner
* call reuses the outer transaction (no nested PG transactions / savepoints).
*/
async transactional(fn, config) {
if (this.alepha.get("alepha.orm.tx")) return fn();
if (!this.supportsTransactions) return fn();
return this.db.transaction(async (tx) => {
this.alepha.store.set("alepha.orm.tx", tx, { skipEvents: true });
try {
return await fn();
} finally {
this.alepha.store.set("alepha.orm.tx", void 0, { skipEvents: true });
}
}, config);
}
async run(statement, schema) {
const result = await this.execute(statement);
if (result == null) return [];
if (!Array.isArray(result)) {
this.log.error("Unexpected query result format", { result });
throw new DbError("Unexpected query result format, expected array of rows");
}
return result.map((row) => this.alepha.codec.decode(schema, row));
}
/**
* Get migrations folder path - can be overridden
*/
getMigrationsFolder() {
return `migrations/${this.name}`;
}
/**
* Migration orchestration.
*
* - Production: file-based migrations from the migrations folder
* - Dev / Test: push-based sync (introspects actual DB, no snapshots)
* - Serverless: skipped (migrations should be applied during deployment)
*/
async migrate() {
if (this.alepha.isServerless()) return;
if (this.alepha.isProduction()) {
const migrationsFolder = this.getMigrationsFolder();
if (!await stat(migrationsFolder).catch(() => false)) {
this.log.warn("Migration SKIPPED - no migrations found");
return;
}
if (this.dialect === "postgresql" && this.schema !== "public") {
this.log.debug(`Ensuring schema '${this.schema}' exists ...`);
await this.execute(sql$1`CREATE SCHEMA IF NOT EXISTS ${sql$1.raw(this.schema)}`);
}
this.log.debug(`Migrate from '${migrationsFolder}' directory ...`);
await this.executeMigrations(migrationsFolder);
this.log.info("Migration OK");
} else {
const { DATABASE_SYNC } = this.alepha.parseEnv(databaseEnvSchema);
if (DATABASE_SYNC === false) {
this.log.info("Database synchronization disabled (DATABASE_SYNC=false)");
return;
}
try {
await this.kit.synchronize(this);
} catch (error) {
throw new DbError(`Failed to synchronize ${this.dialect} database schema`, error);
}
}
}
/**
* For testing purposes, generate a unique schema name.
*
* Format: `test_alepha_{epoch_seconds}_{random8}`
* Example: `test_alepha_1739871618_k3m9x2p1`
*/
generateTestSchemaName() {
return `test_alepha_${Math.floor(this.dateTime.nowMillis() / 1e3)}_${Math.random().toString(36).slice(2, 10).padEnd(8, "0")}`;
}
};
//#endregion
//#region ../../src/orm/core/modes/DbMigrationMode.ts
/**
* Built-in migration mode.
*
* When `MIGRATE=true` (or `MODE=MIGRATE`) is set, runs database migrations
* via `DatabaseProvider.migrate()`, then stops the process.
*
* Auto-registered by `AlephaOrm` — any app using the ORM module gets
* `MIGRATE=true node app.js` for free.
*
* @example
* ```bash
* MIGRATE=true node app.js # runs migrations, then exits
* MODE=MIGRATE node app.js # same effect
* ```
*/
var DbMigrationMode = class {
db = $inject(DatabaseProvider);
mode = $mode({
env: "MIGRATE",
ready: async () => {
await this.db.migrate();
}
});
};
//#endregion
//#region ../../src/orm/core/constants/PG_SYMBOLS.ts
const PG_DEFAULT = Symbol.for("Alepha.Postgres.Default");
const PG_PRIMARY_KEY = Symbol.for("Alepha.Postgres.PrimaryKey");
const PG_CREATED_AT = Symbol.for("Alepha.Postgres.CreatedAt");
const PG_UPDATED_AT = Symbol.for("Alepha.Postgres.UpdatedAt");
const PG_DELETED_AT = Symbol.for("Alepha.Postgres.DeletedAt");
const PG_VERSION = Symbol.for("Alepha.Postgres.Version");
const PG_IDENTITY = Symbol.for("Alepha.Postgres.Identity");
const PG_ENUM = Symbol.for("Alepha.Postgres.Enum");
const PG_REF = Symbol.for("Alepha.Postgres.Ref");
const PG_GENERATED = Symbol.for("Alepha.Postgres.Generated");
const PG_ORGANIZATION = Symbol.for("Alepha.Postgres.Organization");
/**
* @deprecated Use `PG_IDENTITY` instead.
*/
const PG_SERIAL = Symbol.for("Alepha.Postgres.Serial");
//#endregion
//#region ../../src/orm/core/schemas/insertSchema.ts
const insertSchema = (obj) => {
const newProperties = {};
for (const key in obj.shape) {
const prop = obj.shape[key];
if (PG_GENERATED in prop) continue;
if (PG_DEFAULT in prop || PG_ORGANIZATION in prop) newProperties[key] = prop.optional();
else if (z.schema.isOptional(prop)) newProperties[key] = z.schema.unwrap(prop).nullable().optional();
else newProperties[key] = prop;
}
return z.object(newProperties);
};
//#endregion
//#region ../../src/orm/core/schemas/updateSchema.ts
const updateSchema = (schema) => {
const newProperties = {};
for (const key in schema.shape) {
const prop = schema.shape[key];
if (PG_GENERATED in prop) continue;
if (z.schema.isOptional(prop)) newProperties[key] = z.union([prop, z.null()]).optional();
else newProperties[key] = prop;
}
return z.object(newProperties);
};
//#endregion
//#region ../../src/orm/core/primitives/$entity.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(),
* }),
* });
* ```
*/
const $entity = (options) => {
return new EntityPrimitive(options);
};
var EntityPrimitive = class EntityPrimitive {
options;
constructor(options) {
this.options = options;
}
alias(alias) {
const aliased = new EntityPrimitive(this.options);
return new Proxy(aliased, { get(target, prop, receiver) {
if (prop === "$alias") return alias;
return Reflect.get(target, prop, receiver);
} });
}
get cols() {
const cols = {};
for (const key of Object.keys(this.schema.properties)) cols[key] = {
name: key,
entity: this
};
return cols;
}
get name() {
return this.options.name;
}
get schema() {
return this.options.schema;
}
_insertSchema;
get insertSchema() {
this._insertSchema ??= insertSchema(this.options.schema);
return this._insertSchema;
}
_updateSchema;
get updateSchema() {
this._updateSchema ??= updateSchema(this.options.schema);
return this._updateSchema;
}
};
$entity[KIND] = EntityPrimitive;
//#endregion
//#region ../../src/orm/core/helpers/pgAttr.ts
/**
* Decorates a typebox schema with a Postgres attribute.
*
* > It's just a fancy way to add Symbols to a field.
*
* @example
* ```ts
* import { z } from "alepha";
* import { PG_UPDATED_AT } from "../constants/PG_SYMBOLS";
*
* export const updatedAtSchema = pgAttr(
* z.datetime(), PG_UPDATED_AT,
* );
* ```
*/
const pgAttr = (type, attr, value) => {
Object.assign(type, { [attr]: value ?? {} });
return type;
};
/**
* Retrieves the fields of a schema that have a specific attribute.
*/
const getAttrFields = (schema, name) => {
const fields = [];
for (const key of Object.keys(schema.shape)) {
const value = schema.shape[key];
if (name in value) fields.push({
type: value,
key,
data: value[name]
});
}
return fields;
};
//#endregion
//#region ../../src/orm/core/providers/DatabaseTypeProvider.ts
var DatabaseTypeProvider = class {
attr = pgAttr;
/**
* Creates a primary key with an identity column.
*/
identityPrimaryKey = (identity) => pgAttr(pgAttr(pgAttr(z.integer(), PG_PRIMARY_KEY), PG_IDENTITY, identity), PG_DEFAULT);
/**
* Creates a primary key with a big identity column. (default)
*/
bigIdentityPrimaryKey = (identity) => pgAttr(pgAttr(pgAttr(z.int64(), PG_PRIMARY_KEY), PG_IDENTITY, identity), PG_DEFAULT);
/**
* Creates a primary key with a UUID column.
*/
uuidPrimaryKey = () => pgAttr(pgAttr(z.uuid(), PG_PRIMARY_KEY), PG_DEFAULT);
primaryKey(type, _options, identity) {
if (!type || z.schema.isInteger(type)) return pgAttr(pgAttr(pgAttr(z.integer(), PG_PRIMARY_KEY), PG_IDENTITY, identity), PG_DEFAULT);
if (z.schema.isString(type) && z.schema.format(type) === "uuid") return pgAttr(pgAttr(z.uuid(), PG_PRIMARY_KEY), PG_DEFAULT);
if (z.schema.isNumber(type) && z.schema.format(type) === "int64") return pgAttr(pgAttr(pgAttr(z.number(), PG_PRIMARY_KEY), PG_IDENTITY, identity), PG_DEFAULT);
if (z.schema.isBigInt(type)) return pgAttr(pgAttr(pgAttr(z.bigint(), PG_PRIMARY_KEY), PG_IDENTITY, identity), PG_DEFAULT);
throw new AlephaError(`Unsupported type for primary key: ${type}`);
}
/**
* Wrap a schema with "default" attribute.
* This is used to set a default value for a column in the database.
*/
default = (type, value) => {
if (value != null) Object.assign(type, { default: value });
return this.attr(type, PG_DEFAULT);
};
/**
* Creates a column 'version'.
*
* This is used to track the version of a row in the database.
*
* You can use it for optimistic concurrency control (OCC) with {@link RepositoryPrimitive#save}.
*
* @see {@link RepositoryPrimitive#save}
* @see {@link PgVersionMismatchError}
*/
version = () => this.default(pgAttr(z.integer(), PG_VERSION), 0);
/**
* Creates a column Created At. So just a datetime column with a default value of the current timestamp.
*/
createdAt = () => pgAttr(pgAttr(z.datetime(), PG_CREATED_AT), PG_DEFAULT);
/**
* Creates a column Updated At. Like createdAt, but it is updated on every update of the row.
*/
updatedAt = () => pgAttr(pgAttr(z.datetime(), PG_UPDATED_AT), PG_DEFAULT);
/**
* Creates a column Deleted At for soft delete functionality.
* This is used to mark rows as deleted without actually removing them from the database.
* The column is nullable - NULL means not deleted, timestamp means deleted.
*/
deletedAt = () => pgAttr(z.datetime().optional(), PG_DELETED_AT);
/**
* Creates an organization column for multi-tenant row scoping.
*
* When present, queries are automatically filtered by the current user's organization.
* On create, the column is auto-stamped with the current user's organization.
*
* @param options.nullable - When `false`, the column is NOT NULL in the database and
* the ORM rejects inserts that arrive without an organization context.
* Defaults to `true` (nullable) — unless `strict` is set, which flips the
* default to non-nullable. NULL rows are visible to every tenant (the
* historic "global row" semantics) only when the column is nullable AND
* not strict.
* @param options.strict - Fail-closed tenant scoping for security-sensitive
* tables. Refuses reads/writes with no resolved tenant (instead of a
* fail-open "see/write everything") and drops the `OR org IS NULL` escape
* so a scoped tenant never sees global rows. Defaults to `false`.
*/
organization = (options) => {
const strict = options?.strict ?? false;
return pgAttr(options?.nullable ?? !strict ? z.uuid().optional() : z.uuid(), PG_ORGANIZATION, { strict });
};
/**
* Creates a reference to another table or schema. Basically a foreign key.
*/
ref = (type, ref, actions) => {
const finalActions = actions ?? { onDelete: z.schema.isOptional(type) ? "set null" : "cascade" };
return this.attr(type, PG_REF, {
ref,
actions: finalActions
});
};
/**
* Creates a page schema for a given object schema.
* It's used by {@link Repository#paginate} method.
*/
page = (resource) => {
return pageSchema$1(resource);
};
};
/**
* Wrapper of TypeProvider (`t`) for database types.
*
* Use `db` for improve TypeBox schema definitions with database-specific attributes.
*
* @example
* ```ts
* import { t } from "alepha";
* import { db } from "alepha/orm";
*
* const userSchema = t.object({
* id: db.primaryKey(t.uuid()),
* email: t.email(),
* createdAt: db.createdAt(),
* });
* ```
*/
const db = new DatabaseTypeProvider();
//#endregion
//#region ../../src/orm/core/entities/alephaSequences.ts
/**
* Storage table for the {@link SequenceProvider}.
*
* Each row represents one counter, identified by `(name, scope)`:
* - `name` is the sequence primitive name (defaulted from its property key).
* - `scope` is an arbitrary string passed by the caller, defaulted to "default"
* for the global, unscoped form. Use this column to namespace per-tenant /
* per-campaign / per-anything counters off the same primitive.
* - `value` is the current counter value. Incremented atomically by
* `SequenceProvider.advance()` through an `INSERT ... ON CONFLICT DO UPDATE`
* on `(name, scope)` — works identically on Postgres, SQLite, and D1.
*/
const alephaSequences = $entity({
name: "alepha_sequences",
schema: z.object({
id: db.primaryKey(z.uuid()),
createdAt: db.createdAt(),
updatedAt: db.updatedAt(),
name: z.text({ description: "Sequence primitive name (from $sequence's property key)." }),
scope: z.text({
description: "Caller-provided sub-scope. Defaults to 'default' for the unscoped form.",
default: "default"
}),
value: z.integer().describe("Current counter value, advanced atomically on each call.")
}),
indexes: [{
columns: ["name", "scope"],
unique: true
}]
});
//#endregion
//#region ../../src/orm/core/errors/DbColumnNotFoundError.ts
/**
* Error thrown when a column does not exist.
*
* This typically happens when:
* - Column name is misspelled
* - Migrations haven't been run
* - Using wrong schema version
*/
var DbColumnNotFoundError = class DbColumnNotFoundError extends DbError {
name = "DbColumnNotFoundError";
status = 500;
/**
* The column that was not found.
*/
column;
/**
* The table where the column was expected.
*/
table;
constructor(message, cause, options) {
super(message, cause);
this.column = options?.column;
this.table = options?.table;
}
/**
* Parse a database column not found error and create a DbColumnNotFoundError.
* Supports both PostgreSQL and SQLite error formats.
*/
static fromDatabaseError(error) {
const message = error.message;
const pgMatch = message.match(/column "([^"]+)"(?: of relation "([^"]+)")? does not exist/);
if (pgMatch) {
const column = pgMatch[1];
const table = pgMatch[2];
const msg = table ? `Column '${column}' does not exist in table '${table}'` : `Column '${column}' does not exist`;
return new DbColumnNotFoundError(msg, error, {
column,
table
});
}
const sqliteMatch = message.match(/no such column: (?:(\S+)\.)?(\S+)/);
if (sqliteMatch) {
const table = sqliteMatch[1];
const column = sqliteMatch[2];
const msg = table ? `Column '${column}' does not exist in table '${table}'` : `Column '${column}' does not exist`;
return new DbColumnNotFoundError(msg, error, {
column,
table
});
}
return new DbColumnNotFoundError("Column does not exist", error);
}
};
//#endregion
//#region ../../src/orm/core/errors/DbConflictError.ts
var DbConflictError = class extends DbError {
name = "DbConflictError";
status = 409;
};
//#endregion
//#region ../../src/orm/core/errors/DbDeadlockError.ts
/**
* Error thrown when a deadlock is detected.
*
* This happens when two or more transactions are waiting for each other
* to release locks. The database automatically aborts one transaction
* to resolve the deadlock.
*
* This error is useful for implementing retry logic.
*/
var DbDeadlockError = class DbDeadlockError extends DbError {
name = "DbDeadlockError";
status = 409;
/**
* Whether this error is retryable.
* Deadlocks are typically safe to retry.
*/
retryable = true;
/**
* Parse a database deadlock error message and create a DbDeadlockError.
* Supports PostgreSQL. SQLite doesn't have true deadlocks (uses SQLITE_BUSY instead).
*/
static fromDatabaseError(error) {
return new DbDeadlockError("Transaction deadlock detected. Please retry the operation.", error);
}
};
//#endregion
//#region ../../src/orm/core/errors/DbEntityNotFoundError.ts
var DbEntityNotFoundError = class extends DbError {
name = "DbEntityNotFoundError";
status = 404;
constructor(entityName) {
super(`Entity from '${entityName}' was not found`);
}
};
//#endregion
//#region ../../src/orm/core/errors/DbForeignKeyError.ts
/**
* Error thrown when a foreign key constraint is violated.
*
* This typically happens when trying to delete an entity that is
* referenced by another entity.
*/
var DbForeignKeyError = class DbForeignKeyError extends DbError {
name = "DbForeignKeyError";
status = 409;
/**
* The table that references the entity being deleted.
*/
referencingTable;
/**
* The constraint name that was violated.
*/
constraintName;
constructor(message, cause, options) {
super(message, cause);
this.referencingTable = options?.referencingTable;
this.constraintName = options?.constraintName;
}
/**
* Parse a database foreign key error message and create a DbForeignKeyError.
* Supports both PostgreSQL and SQLite error formats.
*/
static fromDatabaseError(error, tableName) {
const pgMatch = error.message.match(/violates foreign key constraint "([^"]+)" on table "([^"]+)"/);
if (pgMatch) {
const constraintName = pgMatch[1];
const referencingTable = pgMatch[2];
return new DbForeignKeyError(`Cannot delete ${tableName}: it is referenced by ${referencingTable}`, error, {
referencingTable,
constraintName
});
}
return new DbForeignKeyError(`Cannot delete ${tableName}: it is referenced by another entity`, error);
}
};
//#endregion
//#region ../../src/orm/core/errors/DbNotNullError.ts
/**
* Error thrown when a NOT NULL constraint is violated.
*
* This happens when inserting or updating a NULL value into a column
* that has a NOT NULL constraint.
*/
var DbNotNullError = class DbNotNullError extends DbError {
name = "DbNotNullError";
status = 400;
/**
* The column that violated the NOT NULL constraint.
*/
column;
/**
* The table containing the column.
*/
table;
constructor(message, cause, options) {
super(message, cause);
this.column = options?.column;
this.table = options?.table;
}
/**
* Parse a database NOT NULL error message and create a DbNotNullError.
* Supports both PostgreSQL and SQLite error formats.
*/
static fromDatabaseError(error, tableName) {
const message = error.message;
const pgMatch = message.match(/null value in column "([^"]+)" of relation "([^"]+)" violates not-null constraint/);
if (pgMatch) {
const column = pgMatch[1];
const table = pgMatch[2];
return new DbNotNullError(`Column '${column}' in '${table}' cannot be null`, error, {
column,
table
});
}
const sqliteMatch = message.match(/NOT NULL constraint failed: ([^.]+)\.(\S+)/);
if (sqliteMatch) {
const table = sqliteMatch[1];
const column = sqliteMatch[2];
return new DbNotNullError(`Column '${column}' in '${table}' cannot be null`, error, {
column,
table
});
}
return new DbNotNullError(`A required field in '${tableName}' cannot be null`, error);
}
};
//#endregion
//#region ../../src/orm/core/errors/DbTableNotFoundError.ts
/**
* Error thrown when a table does not exist.
*
* This typically happens when:
* - Migrations haven't been run
* - Table name is misspelled
* - Wrong database/schema is being used
*/
var DbTableNotFoundError = class DbTableNotFoundError extends DbError {
name = "DbTableNotFoundError";
status = 500;
/**
* The table that was not found.
*/
table;
constructor(message, cause, options) {
super(message, cause);
this.table = options?.table;
}
/**
* Parse a database table not found error and create a DbTableNotFoundError.
* Supports both PostgreSQL and SQLite error formats.
*/
static fromDatabaseError(error) {
const message = error.message;
const pgMatch = message.match(/relation "([^"]+)" does not exist/);
if (pgMatch) {
const table = pgMatch[1];
return new DbTableNotFoundError(`Table '${table}' does not exist. Have you run migrations?`, error, { table });
}
const sqliteMatch = message.match(/no such table: (\S+)/);
if (sqliteMatch) {
const table = sqliteMatch[1];
return new DbTableNotFoundError(`Table '${table}' does not exist. Have you run migrations?`, error, { table });
}
return new DbTableNotFoundError("Table does not exist. Have you run migrations?", error);
}
};
//#endregion
//#region ../../src/orm/core/errors/DbVersionMismatchError.ts
/**
* Error thrown when there is a version mismatch.
* It's thrown by {@link Repository#save} when the updated entity version does not match the one in the database.
* This is used for optimistic concurrency control.
*/
var DbVersionMismatchError = class extends DbError {
name = "DbVersionMismatchError";
constructor(table, id) {
super(`Version mismatch for table '${table}' and id '${id}'`);
}
};
//#endregion
//#region ../../src/orm/core/providers/DbCacheProvider.ts
/**
* Database query cache using a simple in-memory Map.
*
* Uses `{tableName}:{cacheKey}` as the storage key.
* Provides per-table invalidation for write-through cache busting.
*
* This is intentionally self-contained (no external cache dependencies)
* so the ORM module does not force `AlephaCache` on all consumers.
*/
var DbCacheProvider = class {
dateTime = $inject(DateTimeProvider);
store = /* @__PURE__ */ new Map();
storeKey(tableName, cacheKey) {
return `${tableName}:${cacheKey}`;
}
/**
* Get a cached query result.
*/
async get(tableName, cacheKey) {
const key = this.storeKey(tableName, cacheKey);
const entry = this.store.get(key);
if (!entry) return void 0;
if (entry.expiresAt && this.dateTime.nowMillis() > entry.expiresAt) {
this.store.delete(key);
return;
}
return entry.value;
}
/**
* Store a query result in the cache.
*/
async set(tableName, cacheKey, value, ttl) {
const key = this.storeKey(tableName, cacheKey);
this.store.set(key, {
value,
expiresAt: ttl ? this.dateTime.nowMillis() + ttl : void 0
});
}
/**
* Invalidate all cached queries for a table.
*/
async invalidateTable(tableName) {
const prefix = `${tableName}:`;
for (const key of this.store.keys()) if (key.startsWith(prefix)) this.store.delete(key);
}
};
//#endregion
//#region ../../src/orm/core/services/PgRelationManager.ts
var PgRelationManager = class {
schemaValidator = $inject(SchemaValidator);
/**
* Recursively build joins for the query builder based on the relations map
*/
buildJoins(provider, builder, joins, withRelations, table, parentKey) {
for (const [key, join] of Object.entries(withRelations)) {
const from = provider.table(join.join);
const on = isSQLWrapper$1(join.on) ? join.on : sql$1`${table[join.on[0]]} = ${from[join.on[1].name]}`;
if (join.type === "right") builder.rightJoin(from, on);
else if (join.type === "inner") builder.innerJoin(from, on);
else builder.leftJoin(from, on);
joins.push({
key,
table: getTableName(from),
schema: join.join.schema,
col: (name) => from[name],
parent: parentKey
});
if (join.with) this.buildJoins(provider, builder, joins, join.with, from, parentKey ? `${parentKey}.${key}` : key);
}
}
/**
* Map a row with its joined relations based on the joins definition
*/
mapRowWithJoins(record, row, schema, joins, parentKey) {
for (const join of joins) if (join.parent === parentKey) {
const joinedData = row[join.table];
if (this.isAllNull(joinedData)) record[join.key] = void 0;
else {
record[join.key] = joinedData;
this.mapRowWithJoins(record[join.key], row, schema, joins, parentKey ? `${parentKey}.${join.key}` : join.key);
}
}
return record;
}
/**
* Check if all values in an object are null (indicates a left join with no match)
*/
isAllNull(obj) {
if (obj === null || obj === void 0) return true;
if (typeof obj !== "object") return false;
return Object.values(obj).every((val) => val === null);
}
/**
* Build a schema that includes all join properties recursively
*/
buildSchemaWithJoins(baseSchema, joins, parentPath) {
const schema = this.schemaValidator.clone(baseSchema);
const joinsAtThisLevel = joins.filter((j) => j.parent === parentPath);
for (const join of joinsAtThisLevel) {
const joinPath = parentPath ? `${parentPath}.${join.key}` : join.key;
const childJoins = joins.filter((j) => j.parent === joinPath);
let joinSchema = join.schema;
if (childJoins.length > 0) joinSchema = this.buildSchemaWithJoins(join.schema, joins, joinPath);
schema.properties[join.key] = joinSchema.optional();
}
return schema;
}
};
//#endregion
//#region ../../src/orm/core/services/QueryManager.ts
var QueryManager = class {
alepha = $inject(Alepha);
/**
* Convert a query object to a SQL query.
*/
toSQL(query, options) {
const { schema, col, joins } = options;
const conditions = [];
if (isSQLWrapper(query)) conditions.push(query);
else {
const keys = Object.keys(query);
for (const key of keys) {
const operator = query[key];
if (typeof query[key] === "object" && query[key] != null && !Array.isArray(query[key]) && joins?.length) {
const matchingJoins = joins.filter((j) => j.key === key);
if (matchingJoins.length > 0) {
const join = matchingJoins[0];
const joinPath = join.parent ? `${join.parent}.${key}` : key;
const recursiveJoins = joins.filter((j) => {
if (!j.parent) return false;
return j.parent === joinPath || j.parent.startsWith(`${joinPath}.`);
}).map((j) => {
const newParent = j.parent === joinPath ? void 0 : j.parent.substring(joinPath.length + 1);
return {
...j,
parent: newParent
};
});
const sql = this.toSQL(query[key], {
schema: join.schema,
col: join.col,
joins: recursiveJoins.length > 0 ? recursiveJoins : void 0,
dialect: options.dialect
});
if (sql) conditions.push(sql);
continue;
}
}
if (Array.isArray(operator)) {
const operations = operator.map((it) => {
if (isSQLWrapper(it)) return it;
return this.toSQL(it, {
schema,
col,
joins,
dialect: options.dialect
});
}).filter((it) => it != null);
if (key === "and") {
const combined = and(...operations);
if (combined) conditions.push(combined);
continue;
}
if (key === "or") {
const combined = or(...operations);
if (combined) conditions.push(combined);
continue;
}
}
if (key === "not") {
const where = this.toSQL(operator, {
schema,
col,
joins,
dialect: options.dialect
});
if (where) conditions.push(not(where));
continue;
}
if (key === "exists") {
conditions.push(exists(operator));
continue;
}
if (key === "notExists") {
conditions.push(notExists(operator));
continue;
}
if (operator != null) {
const column = col(key);
const sql = this.mapOperatorToSql(operator, column, schema, key, options.dialect);
if (sql) conditions.push(sql);
}
}
}
if (conditions.length === 1) return conditions[0];
return and(...conditions);
}
/**
* Check if an object has any filter operator properties.
*/
hasFilterOperatorProperties(obj) {
if (!obj || typeof obj !== "object") return false;
return [
"eq",
"ne",
"gt",
"gte",
"lt",
"lte",
"inArray",
"notInArray",
"isNull",
"isNotNull",
"like",
"notLike",
"ilike",
"notIlike",
"contains",
"startsWith",
"endsWith",
"between",
"notBetween",
"arrayContains",
"arrayContained",
"arrayOverlaps"
].some((key) => key in obj);
}
/**
* Map a filter operator to a SQL query.
*/
mapOperatorToSql(operator, column, columnSchema, columnName, dialect = "postgresql") {
const encodeValue = (value) => {
if (value == null) return value;
if (columnSchema && columnName) try {
const fieldSchema = columnSchema.properties[columnName];
if (fieldSchema) return this.alepha.codec.encode(fieldSchema, value, { encoder: "drizzle" });
} catch (error) {}
return value;
};
const encodeArray = (values) => {
return values.map((v) => encodeValue(v));
};
if (typeof operator !== "object" || operator == null || !this.hasFilterOperatorProperties(operator)) return eq(column, encodeValue(operator));
const conditions = [];
if (operator?.eq != null) conditions.push(eq(column, encodeValue(operator.eq)));
if (operator?.ne != null) conditions.push(ne(column, encodeValue(operator.ne)));
if (operator?.gt != null) conditions.push(gt(column, encodeValue(operator.gt)));
if (operator?.gte != null) conditions.push(gte(column, encodeValue(operator.gte)));
if (operator?.lt != null) conditions.push(lt(column, encodeValue(operator.lt)));
if (operator?.lte != null) conditions.push(lte(column, encodeValue(operator.lte)));
if (operator?.inArray != null) {
if (!Array.isArray(operator.inArray) || operator.inArray.length === 0) throw new AlephaError("inArray operator requires at least one value");
conditions.push(inArray(column, encodeArray(operator.inArray)));
}
if (operator?.notInArray != null) {
if (!Array.isArray(operator.notInArray) || operator.notInArray.length === 0) throw new AlephaError("notInArray operator requires at least one value");
conditions.push(notInArray(column, encodeArray(operator.notInArray)));
}
if (operator?.isNull != null) conditions.push(isNull(column));
if (operator?.isNotNull != null) conditions.push(isNotNull(column));
if (operator?.like != null) conditions.push(like(column, encodeValue(operator.like)));
if (operator?.notLike != null) conditions.push(notLike(column, encodeValue(operator.notLike)));
if (operator?.ilike != null) if (dialect === "sqlite") conditions.push(sql$1`LOWER(${column}) LIKE LOWER(${encodeValue(operator.ilike)})`);
else conditions.push(ilike(column, encodeValue(operator.ilike)));
if (operator?.notIlike != null) if (dialect === "sqlite") conditions.push(sql$1`LOWER(${column}) NOT LIKE LOWER(${encodeValue(operator.notIlike)})`);
else conditions.push(notIlike(column, encodeValue(operator.notIlike)));
if (operator?.contains != null) {
const escapedValue = String(operator.contains).replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
if (dialect === "sqlite") conditions.push(sql$1`LOWER(${column}) LIKE LOWER(${encodeValue(`%${escapedValue}%`)}) ESCAPE '\\'`);
else conditions.push(ilike(column, encodeValue(`%${escapedValue}%`)));
}
if (operator?.startsWith != null) {
const escapedValue = String(operator.startsWith).replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
if (dialect === "sqlite") conditions.push(sql$1`LOWER(${column}) LIKE LOWER(${encodeValue(`${escapedValue}%`)}) ESCAPE '\\'`);
else conditions.push(ilike(column, encodeValue(`${escapedValue}%`)));
}
if (operator?.endsWith != null) {
const escapedValue = String(operator.endsWith).replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
if (dialect === "sqlite") conditions.push(sql$1`LOWER(${column}) LIKE LOWER(${encodeValue(`%${escapedValue}`)}) ESCAPE '\\'`);
else conditions.push(ilike(column, encodeValue(`%${escapedValue}`)));
}
if (operator?.between != null) {
if (!Array.isArray(operator.between) || operator.between.length !== 2) throw new AlephaError("between operator requires exactly 2 values [min, max]");
conditions.push(between(column, encodeValue(operator.between[0]), encodeValue(operator.between[1])));
}
if (operator?.notBetween != null) {
if (!Array.isArray(operator.notBetween) || operator.notBetween.length !== 2) throw new AlephaError("notBetween operator requires exactly 2 values [min, max]");
conditions.push(notBetween(column, encodeValue(operator.notBetween[0]), encodeValue(operator.notBetween[1])));
}
if (operator?.arrayContains != null) conditions.push(arrayContains(column, encodeValue(operator.arrayContains)));
if (operator?.arrayContained != null) conditions.push(arrayContained(column, encodeValue(operator.arrayContained)));
if (operator?.arrayOverlaps != null) conditions.push(arrayOverlaps(column, encodeValue(operator.arrayOverlaps)));
if (conditions.length === 0) return;
if (conditions.length === 1) return conditions[0];
return and(...conditions);
}
/**
* Parse pagination sort string to orderBy format.
* Format: "firstName,-lastName" -> [{ column: "firstName", direction: "asc" }, { column: "lastName", direction: "desc" }]
* - Columns separated by comma
* - Prefix with '-' for DESC direction
*
* @param sort Pagination sort string
* @returns OrderBy array or single object
*/
parsePaginationSort(sort) {
const orderByClauses = sort.split(",").map((field) => field.trim()).map((field) => {
if (field.startsWith("-")) return {
column: field.substring(1),
direction: "desc"
};
return {
column: field,
direction: "asc"
};
});
return orderByClauses.length === 1 ? orderByClauses[0] : orderByClauses;
}
/**
* Normalize orderBy parameter to array format.
* Supports 3 modes:
* 1. String: "name" -> [{ column: "name", direction: "asc" }]
* 2. Object: { column: "name", direction: "desc" } -> [{ column: "name", direction: "desc" }]
* 3. Array: [{ column: "name" }, { column: "age", direction: "desc" }] -> normalized array
*
* @param orderBy The orderBy parameter
* @returns Normalized array of order by clauses
*/
normalizeOrderBy(orderBy) {
if (typeof orderBy === "string") return [{
column: orderBy,
direction: "asc"
}];
if (!Array.isArray(orderBy) && typeof orderBy === "object") return [{
column: orderBy.column,
direction: orderBy.direction ?? "asc"
}];
if (Array.isArray(orderBy)) return orderBy.map((item) => ({
column: item.column,
direction: item.direction ?? "asc"
}));
return [];
}
/**
* Create a pagination object.
*
* @deprecated Use `createPagination` from alepha instead.
* This method now delegates to the framework-level helper.
*
* @param entities The entities to paginate.
* @param limit The limit of the pagination.
* @param offset The offset of the pagination.
* @param sort Optional sort metadata to include in response.
*/
createPagination(entities, limit = 10, offset = 0, sort) {
return createPagination(entities, limit, offset, sort);
}
};
//#endregion
//#region ../../src/orm/core/services/Repository.ts
var Repository = class Repository {
entity;
provider;
log = $logger();
relationManager = $inject(PgRelationManager);
queryManager = $inject(QueryManager);
dateTimeProvider = $inject(DateTimeProvider);
dbCache = new DbCacheProvider();
alepha = $inject(Alepha);
static of(entity, provider = DatabaseProvider) {
return class InlineRepository extends Repository {
constructor() {
super(entity, provider);
}
};
}
constructor(entity, provider = DatabaseProvider) {
this.entity = entity;
this.provider = this.alepha.inject(provider);
this.provider.registerEntity(entity);
}
/**
* Represents the primary key of the table.
* - Key is the name of the primary key column.
* - Type is the type (TypeBox) of the primary key column.
*
* ID is mandatory. If the table does not have a primary key, it will throw an error.
*/
get id() {
return this.getPrimaryKey(this.entity.schema);
}
/**
* Get Drizzle table object.
*/
get table() {
return this.provider.table(this.entity);
}