UNPKG

alepha

Version:

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

764 lines (660 loc) 25 kB
import { Alepha, z } from "alepha"; import { describe, expect, it } from "vitest"; import { $entity, $repository, DatabaseProvider, DbColumnNotFoundError, DbConflictError, DbConnectionError, DbDeadlockError, DbForeignKeyError, DbNotNullError, DbTableNotFoundError, db, sql, } from "../core/index.ts"; import { NodeSqliteProvider, nodeSqliteOptions, } from "../core/providers/drivers/NodeSqliteProvider.ts"; import { AlephaOrmPostgres } from "../postgres/index.ts"; /** * Database Error Tests * * This test suite validates that the ORM produces user-friendly error messages * for common database constraint violations: * - DbConflictError: unique constraint violations (duplicate key) * - DbForeignKeyError: foreign key constraint violations (referenced entity) * * Both PostgreSQL and SQLite are tested. */ describe("Database Error Tests", () => { // Parent entity (referenced by child) const parents = $entity({ name: "error_test_parents", schema: z.object({ id: db.primaryKey(), name: z.text(), email: z.text(), }), indexes: [{ column: "email", unique: true }], }); // Child entity (references parent) - using RESTRICT (no cascade) to test FK errors const children = $entity({ name: "error_test_children", schema: z.object({ id: db.primaryKey(), parentId: z.integer(), // Plain integer, no db.ref() to avoid cascade name: z.text(), }), foreignKeys: [ { columns: ["parentId"], foreignColumns: [() => parents.cols.id], }, ], }); // Entity with required fields for NOT NULL tests const requiredFields = $entity({ name: "error_test_required", schema: z.object({ id: db.primaryKey(), requiredName: z.text(), // NOT NULL by default optionalNote: z.text().optional(), // Nullable }), }); class App { parents = $repository(parents); children = $repository(children); required = $repository(requiredFields); } describe("PostgreSQL", () => { describe("DbConflictError", () => { it("should throw DbConflictError on unique constraint violation", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); const app = alepha.inject(App); await alepha.start(); // Create first entity await app.parents.create({ name: "Alice", email: "alice@example.com", }); // Try to create duplicate try { await app.parents.create({ name: "Alice Clone", email: "alice@example.com", // Same email - should conflict }); expect.fail("Should have thrown DbConflictError"); } catch (error) { expect(error).toBeInstanceOf(DbConflictError); expect((error as DbConflictError).status).toBe(409); expect((error as DbConflictError).name).toBe("DbConflictError"); } }); it("should throw DbConflictError on update causing unique violation", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); const app = alepha.inject(App); await alepha.start(); // Create two entities const alice = await app.parents.create({ name: "Alice", email: "alice@example.com", }); await app.parents.create({ name: "Bob", email: "bob@example.com", }); // Try to update Alice's email to Bob's email try { await app.parents.updateById(alice.id, { email: "bob@example.com", }); expect.fail("Should have thrown DbConflictError"); } catch (error) { expect(error).toBeInstanceOf(DbConflictError); expect((error as DbConflictError).status).toBe(409); } }); }); describe("DbForeignKeyError", () => { it("should throw DbForeignKeyError when deleting referenced entity", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); const app = alepha.inject(App); await alepha.start(); // Create parent const parent = await app.parents.create({ name: "Parent", email: "parent@example.com", }); // Create child referencing parent await app.children.create({ parentId: parent.id, name: "Child", }); // Try to delete parent (should fail due to FK constraint) try { await app.parents.deleteById(parent.id); expect.fail("Should have thrown DbForeignKeyError"); } catch (error) { expect(error).toBeInstanceOf(DbForeignKeyError); expect((error as DbForeignKeyError).status).toBe(409); expect((error as DbForeignKeyError).name).toBe("DbForeignKeyError"); expect((error as DbForeignKeyError).message).toContain( "Cannot delete", ); expect((error as DbForeignKeyError).message).toContain( "error_test_parents", ); // PostgreSQL provides the referencing table name expect((error as DbForeignKeyError).referencingTable).toBe( "error_test_children", ); } }); it("should include constraint name in DbForeignKeyError", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); const app = alepha.inject(App); await alepha.start(); const parent = await app.parents.create({ name: "Parent2", email: "parent2@example.com", }); await app.children.create({ parentId: parent.id, name: "Child2", }); try { await app.parents.deleteById(parent.id); expect.fail("Should have thrown DbForeignKeyError"); } catch (error) { expect(error).toBeInstanceOf(DbForeignKeyError); // PostgreSQL provides the constraint name expect((error as DbForeignKeyError).constraintName).toBeDefined(); expect((error as DbForeignKeyError).constraintName).toContain( "parent_id", ); } }); it("should allow delete after removing referencing entities", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); const app = alepha.inject(App); await alepha.start(); const parent = await app.parents.create({ name: "Parent3", email: "parent3@example.com", }); const child = await app.children.create({ parentId: parent.id, name: "Child3", }); // Delete child first await app.children.deleteById(child.id); // Now parent can be deleted const deleted = await app.parents.deleteById(parent.id); expect(deleted).toContain(parent.id); }); }); describe("DbNotNullError", () => { it("should throw DbNotNullError when inserting null into required field via raw SQL", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); const app = alepha.inject(App); await alepha.start(); // Use raw SQL to bypass ORM validation and trigger actual NOT NULL constraint try { await app.required.query( (t) => sql`INSERT INTO ${t} (required_name) VALUES (${sql.raw("NULL")})`, ); expect.fail("Should have thrown DbNotNullError"); } catch (error) { expect(error).toBeInstanceOf(DbNotNullError); expect((error as DbNotNullError).status).toBe(400); expect((error as DbNotNullError).name).toBe("DbNotNullError"); expect((error as DbNotNullError).column).toBe("required_name"); expect((error as DbNotNullError).message).toContain("cannot be null"); } }); }); describe("DbTableNotFoundError", () => { it("should throw DbTableNotFoundError for non-existent table", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); const app = alepha.inject(App); await alepha.start(); try { await app.parents.query( () => sql`SELECT * FROM non_existent_table_xyz`, ); expect.fail("Should have thrown DbTableNotFoundError"); } catch (error) { expect(error).toBeInstanceOf(DbTableNotFoundError); expect((error as DbTableNotFoundError).status).toBe(500); expect((error as DbTableNotFoundError).table).toBe( "non_existent_table_xyz", ); expect((error as DbTableNotFoundError).message).toContain( "does not exist", ); } }); }); describe("DbColumnNotFoundError", () => { it("should throw DbColumnNotFoundError for non-existent column", async () => { const alepha = Alepha.create().with(AlephaOrmPostgres); const app = alepha.inject(App); await alepha.start(); try { await app.parents.query( (t) => sql`SELECT ${sql.raw("non_existent_column_xyz")} FROM ${t}`, ); expect.fail("Should have thrown DbColumnNotFoundError"); } catch (error) { expect(error).toBeInstanceOf(DbColumnNotFoundError); expect((error as DbColumnNotFoundError).status).toBe(500); expect((error as DbColumnNotFoundError).column).toBe( "non_existent_column_xyz", ); } }); }); }); describe("SQLite", () => { describe("DbConflictError", () => { it("should throw DbConflictError on unique constraint violation", async () => { const alepha = Alepha.create().with({ provide: DatabaseProvider, use: NodeSqliteProvider, }); alepha.store.mut(nodeSqliteOptions, (old) => ({ ...old, path: "sqlite://:memory:", })); const app = alepha.inject(App); await alepha.start(); // Create first entity await app.parents.create({ name: "SQLite Alice", email: "sqlite_alice@example.com", }); // Try to create duplicate try { await app.parents.create({ name: "SQLite Alice Clone", email: "sqlite_alice@example.com", }); expect.fail("Should have thrown DbConflictError"); } catch (error) { expect(error).toBeInstanceOf(DbConflictError); expect((error as DbConflictError).status).toBe(409); } }); }); describe("DbForeignKeyError", () => { it("should throw DbForeignKeyError when deleting referenced entity", async () => { const alepha = Alepha.create().with({ provide: DatabaseProvider, use: NodeSqliteProvider, }); alepha.store.mut(nodeSqliteOptions, (old) => ({ ...old, path: "sqlite://:memory:", })); const app = alepha.inject(App); await alepha.start(); // Create parent const parent = await app.parents.create({ name: "SQLite Parent", email: "sqlite_parent@example.com", }); // Create child referencing parent await app.children.create({ parentId: parent.id, name: "SQLite Child", }); // Try to delete parent (should fail due to FK constraint) try { await app.parents.deleteById(parent.id); expect.fail("Should have thrown DbForeignKeyError"); } catch (error) { expect(error).toBeInstanceOf(DbForeignKeyError); expect((error as DbForeignKeyError).status).toBe(409); expect((error as DbForeignKeyError).name).toBe("DbForeignKeyError"); expect((error as DbForeignKeyError).message).toContain( "Cannot delete", ); // SQLite doesn't provide the referencing table name in error expect((error as DbForeignKeyError).message).toContain( "error_test_parents", ); } }); it("should have generic message for SQLite (no table name available)", async () => { const alepha = Alepha.create().with({ provide: DatabaseProvider, use: NodeSqliteProvider, }); alepha.store.mut(nodeSqliteOptions, (old) => ({ ...old, path: "sqlite://:memory:", })); const app = alepha.inject(App); await alepha.start(); const parent = await app.parents.create({ name: "SQLite Parent2", email: "sqlite_parent2@example.com", }); await app.children.create({ parentId: parent.id, name: "SQLite Child2", }); try { await app.parents.deleteById(parent.id); expect.fail("Should have thrown DbForeignKeyError"); } catch (error) { expect(error).toBeInstanceOf(DbForeignKeyError); // SQLite doesn't provide constraint name or referencing table expect((error as DbForeignKeyError).referencingTable).toBeUndefined(); expect((error as DbForeignKeyError).constraintName).toBeUndefined(); // Message should mention "another entity" since table name is unknown expect((error as DbForeignKeyError).message).toContain( "another entity", ); } }); }); describe("DbNotNullError", () => { it("should throw DbNotNullError when inserting null into required field via raw SQL", async () => { const alepha = Alepha.create().with({ provide: DatabaseProvider, use: NodeSqliteProvider, }); alepha.store.mut(nodeSqliteOptions, (old) => ({ ...old, path: "sqlite://:memory:", })); const app = alepha.inject(App); await alepha.start(); // Use raw SQL to bypass ORM validation and trigger actual NOT NULL constraint try { await app.required.query( (t) => sql`INSERT INTO ${t} (required_name) VALUES (${sql.raw("NULL")})`, ); expect.fail("Should have thrown DbNotNullError"); } catch (error) { expect(error).toBeInstanceOf(DbNotNullError); expect((error as DbNotNullError).status).toBe(400); // SQLite includes table.column format expect((error as DbNotNullError).column).toBe("required_name"); expect((error as DbNotNullError).message).toContain("cannot be null"); } }); }); describe("DbTableNotFoundError", () => { it("should throw DbTableNotFoundError for non-existent table", async () => { const alepha = Alepha.create().with({ provide: DatabaseProvider, use: NodeSqliteProvider, }); alepha.store.mut(nodeSqliteOptions, (old) => ({ ...old, path: "sqlite://:memory:", })); const app = alepha.inject(App); await alepha.start(); try { await app.parents.query( () => sql`SELECT * FROM non_existent_table_xyz`, ); expect.fail("Should have thrown DbTableNotFoundError"); } catch (error) { expect(error).toBeInstanceOf(DbTableNotFoundError); expect((error as DbTableNotFoundError).table).toBe( "non_existent_table_xyz", ); } }); }); describe("DbColumnNotFoundError", () => { it("should throw DbColumnNotFoundError for non-existent column", async () => { const alepha = Alepha.create().with({ provide: DatabaseProvider, use: NodeSqliteProvider, }); alepha.store.mut(nodeSqliteOptions, (old) => ({ ...old, path: "sqlite://:memory:", })); const app = alepha.inject(App); await alepha.start(); try { await app.parents.query( (t) => sql`SELECT ${sql.raw("non_existent_column_xyz")} FROM ${t}`, ); expect.fail("Should have thrown DbColumnNotFoundError"); } catch (error) { expect(error).toBeInstanceOf(DbColumnNotFoundError); expect((error as DbColumnNotFoundError).column).toBe( "non_existent_column_xyz", ); } }); }); }); describe("Error Properties", () => { it("DbConflictError should have correct properties", async () => { const error = new DbConflictError("Test conflict"); expect(error.name).toBe("DbConflictError"); expect(error.status).toBe(409); expect(error.message).toBe("Test conflict"); }); it("DbForeignKeyError should have correct properties", async ({ expect, }) => { const error = new DbForeignKeyError("Test FK error", undefined, { referencingTable: "child_table", constraintName: "child_parent_id_fk", }); expect(error.name).toBe("DbForeignKeyError"); expect(error.status).toBe(409); expect(error.message).toBe("Test FK error"); expect(error.referencingTable).toBe("child_table"); expect(error.constraintName).toBe("child_parent_id_fk"); }); it("DbForeignKeyError.fromDatabaseError should parse PostgreSQL error", async ({ expect, }) => { const dbError = new Error( 'update or delete on table "query_item" violates foreign key constraint "reporting_alert_query_id_fk" on table "reporting_alert"', ); const error = DbForeignKeyError.fromDatabaseError(dbError, "query_item"); expect(error.message).toBe( "Cannot delete query_item: it is referenced by reporting_alert", ); expect(error.referencingTable).toBe("reporting_alert"); expect(error.constraintName).toBe("reporting_alert_query_id_fk"); }); it("DbForeignKeyError.fromDatabaseError should handle SQLite error", async ({ expect, }) => { const sqliteError = new Error("FOREIGN KEY constraint failed"); const error = DbForeignKeyError.fromDatabaseError( sqliteError, "parent_table", ); expect(error.message).toBe( "Cannot delete parent_table: it is referenced by another entity", ); expect(error.referencingTable).toBeUndefined(); expect(error.constraintName).toBeUndefined(); }); // DbNotNullError tests it("DbNotNullError should have correct properties", async () => { const error = new DbNotNullError("Test not null", undefined, { column: "email", table: "users", }); expect(error.name).toBe("DbNotNullError"); expect(error.status).toBe(400); expect(error.column).toBe("email"); expect(error.table).toBe("users"); }); it("DbNotNullError.fromDatabaseError should parse PostgreSQL error", async ({ expect, }) => { const dbError = new Error( 'null value in column "email" of relation "users" violates not-null constraint', ); const error = DbNotNullError.fromDatabaseError(dbError, "users"); expect(error.message).toBe("Column 'email' in 'users' cannot be null"); expect(error.column).toBe("email"); expect(error.table).toBe("users"); }); it("DbNotNullError.fromDatabaseError should parse SQLite error", async ({ expect, }) => { const sqliteError = new Error("NOT NULL constraint failed: users.email"); const error = DbNotNullError.fromDatabaseError(sqliteError, "users"); expect(error.message).toBe("Column 'email' in 'users' cannot be null"); expect(error.column).toBe("email"); expect(error.table).toBe("users"); }); // DbDeadlockError tests it("DbDeadlockError should have correct properties", async () => { const error = new DbDeadlockError("Deadlock detected"); expect(error.name).toBe("DbDeadlockError"); expect(error.status).toBe(409); expect(error.retryable).toBe(true); }); it("DbDeadlockError.fromDatabaseError should create error", async ({ expect, }) => { const dbError = new Error("deadlock detected"); const error = DbDeadlockError.fromDatabaseError(dbError); expect(error.message).toContain("deadlock"); expect(error.retryable).toBe(true); }); // DbConnectionError tests it("DbConnectionError should have correct properties", async ({ expect, }) => { const error = new DbConnectionError("Connection failed", undefined, { errorType: "refused", retryable: true, }); expect(error.name).toBe("DbConnectionError"); expect(error.status).toBe(503); expect(error.errorType).toBe("refused"); expect(error.retryable).toBe(true); }); it("DbConnectionError.fromDatabaseError should detect connection refused", async ({ expect, }) => { const error = DbConnectionError.fromDatabaseError( new Error("connection refused"), ); expect(error.errorType).toBe("refused"); expect(error.retryable).toBe(true); }); it("DbConnectionError.fromDatabaseError should detect timeout", async ({ expect, }) => { const error = DbConnectionError.fromDatabaseError( new Error("connection timed out"), ); expect(error.errorType).toBe("timeout"); expect(error.retryable).toBe(true); }); it("DbConnectionError.fromDatabaseError should detect auth failure", async ({ expect, }) => { const error = DbConnectionError.fromDatabaseError( new Error("password authentication failed for user"), ); expect(error.errorType).toBe("auth"); expect(error.retryable).toBe(false); }); it("DbConnectionError.fromDatabaseError should detect SQLite locked", async ({ expect, }) => { const error = DbConnectionError.fromDatabaseError( new Error("database is locked"), ); expect(error.errorType).toBe("locked"); expect(error.retryable).toBe(true); }); // DbTableNotFoundError tests it("DbTableNotFoundError should have correct properties", async ({ expect, }) => { const error = new DbTableNotFoundError("Table not found", undefined, { table: "users", }); expect(error.name).toBe("DbTableNotFoundError"); expect(error.status).toBe(500); expect(error.table).toBe("users"); }); it("DbTableNotFoundError.fromDatabaseError should parse PostgreSQL error", async ({ expect, }) => { const dbError = new Error('relation "users" does not exist'); const error = DbTableNotFoundError.fromDatabaseError(dbError); expect(error.table).toBe("users"); expect(error.message).toContain("does not exist"); }); it("DbTableNotFoundError.fromDatabaseError should parse SQLite error", async ({ expect, }) => { const sqliteError = new Error("no such table: users"); const error = DbTableNotFoundError.fromDatabaseError(sqliteError); expect(error.table).toBe("users"); expect(error.message).toContain("does not exist"); }); // DbColumnNotFoundError tests it("DbColumnNotFoundError should have correct properties", async ({ expect, }) => { const error = new DbColumnNotFoundError("Column not found", undefined, { column: "email", table: "users", }); expect(error.name).toBe("DbColumnNotFoundError"); expect(error.status).toBe(500); expect(error.column).toBe("email"); expect(error.table).toBe("users"); }); it("DbColumnNotFoundError.fromDatabaseError should parse PostgreSQL error", async ({ expect, }) => { const dbError = new Error('column "email" does not exist'); const error = DbColumnNotFoundError.fromDatabaseError(dbError); expect(error.column).toBe("email"); }); it("DbColumnNotFoundError.fromDatabaseError should parse PostgreSQL error with table", async ({ expect, }) => { const dbError = new Error( 'column "email" of relation "users" does not exist', ); const error = DbColumnNotFoundError.fromDatabaseError(dbError); expect(error.column).toBe("email"); expect(error.table).toBe("users"); }); it("DbColumnNotFoundError.fromDatabaseError should parse SQLite error", async ({ expect, }) => { const sqliteError = new Error("no such column: email"); const error = DbColumnNotFoundError.fromDatabaseError(sqliteError); expect(error.column).toBe("email"); }); it("DbColumnNotFoundError.fromDatabaseError should parse SQLite error with table", async ({ expect, }) => { const sqliteError = new Error("no such column: users.email"); const error = DbColumnNotFoundError.fromDatabaseError(sqliteError); expect(error.column).toBe("email"); expect(error.table).toBe("users"); }); }); });