alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
562 lines (561 loc) • 18.7 kB
JavaScript
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