UNPKG

alepha

Version:

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

562 lines (561 loc) 18.7 kB
import { $module, AlephaError, KIND, pageQuerySchema, pageSchema, pageSchema as pageSchema$1, z } from "alepha"; import { AlephaDateTime } from "alepha/datetime"; import { sql } from "drizzle-orm"; //#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/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/DbConnectionError.ts /** * Error thrown when a database connection fails. * * This can happen due to: * - Connection refused (server not running) * - Connection timeout * - Authentication failure * - Network issues * - Database file not found (SQLite) */ var DbConnectionError = class DbConnectionError extends DbError { name = "DbConnectionError"; status = 503; /** * The type of connection error. */ errorType; /** * Whether this error is retryable. * Connection errors are often transient and can be retried. */ retryable; constructor(message, cause, options) { super(message, cause); this.errorType = options?.errorType; this.retryable = options?.retryable ?? true; } /** * Parse a database connection error message and create a DbConnectionError. * Supports both PostgreSQL and SQLite error formats. */ static fromDatabaseError(error) { const message = error.message.toLowerCase(); if (message.includes("connection refused") || message.includes("econnrefused") || message.includes("could not connect")) return new DbConnectionError("Database connection refused. Is the server running?", error, { errorType: "refused", retryable: true }); if (message.includes("timeout") || message.includes("timed out") || message.includes("etimedout")) return new DbConnectionError("Database connection timed out", error, { errorType: "timeout", retryable: true }); if (message.includes("password authentication failed") || message.includes("authentication failed") || message.includes("access denied")) return new DbConnectionError("Database authentication failed. Check credentials.", error, { errorType: "auth", retryable: false }); if (message.includes("unable to open database") || message.includes("no such file") || message.includes("enoent")) return new DbConnectionError("Database file not found", error, { errorType: "not_found", retryable: false }); if (message.includes("database is locked")) return new DbConnectionError("Database is locked by another process", error, { errorType: "locked", retryable: true }); return new DbConnectionError("Failed to connect to database", error, { errorType: "unknown", retryable: true }); } }; //#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/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/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_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/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/schemas/legacyIdSchema.ts /** * @deprecated Use `pg.primaryKey()` instead. */ const legacyIdSchema = pgAttr(pgAttr(pgAttr(z.integer(), PG_PRIMARY_KEY), PG_SERIAL), PG_DEFAULT); //#endregion //#region ../../src/orm/core/index.browser.ts const AlephaOrm = $module({ name: "alepha.orm", primitives: [], services: [AlephaDateTime] }); //#endregion export { $entity, AlephaOrm, DatabaseTypeProvider, DbColumnNotFoundError, DbConnectionError, DbDeadlockError, DbEntityNotFoundError, DbForeignKeyError, DbNotNullError, DbTableNotFoundError, EntityPrimitive, db, getAttrFields, legacyIdSchema, pageQuerySchema, pageSchema, pgAttr, sql }; //# sourceMappingURL=index.browser.js.map