UNPKG

alepha

Version:

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

428 lines (354 loc) 12 kB
import { type Alepha, z } from "alepha"; import { sql } from "drizzle-orm"; import { expect } from "vitest"; import { PG_GENERATED } from "../core/constants/PG_SYMBOLS.ts"; import { $entity, $repository, db, pgAttr } from "../core/index.ts"; // ============================================================================ // Shared entity definitions // ============================================================================ const orderEntity = $entity({ name: "orders", schema: z.object({ id: db.primaryKey(), category: z.text(), amount: z.number(), status: z.text(), }), }); // ============================================================================ // Feature 1: Partial Indexes // ============================================================================ export const testPartialIndex = async (alepha: Alepha) => { const entity = $entity({ name: "items_partial_idx", schema: z.object({ id: db.primaryKey(), email: z.text(), active: z.boolean(), }), indexes: [ { column: "email", unique: true, where: sql`active = true`, }, ], }); class App { repo = $repository(entity); } const app = alepha.inject(App); await alepha.start(); // Create an active item — unique constraint applies const a = await app.repo.create({ email: "alice@test.com", active: true }); expect(a.email).toBe("alice@test.com"); // Deactivate it — no longer covered by partial index await app.repo.updateById(a.id, { active: false }); // Insert same email as active — allowed because first one is inactive const b = await app.repo.create({ email: "alice@test.com", active: true }); expect(b.email).toBe("alice@test.com"); expect(b.id).not.toBe(a.id); }; export const testPartialCompositeIndex = async (alepha: Alepha) => { const entity = $entity({ name: "items_partial_comp_idx", schema: z.object({ id: db.primaryKey(), category: z.text(), name: z.text(), active: z.boolean(), }), indexes: [ { columns: ["category", "name"], unique: true, where: sql`active = true`, }, ], }); class App { repo = $repository(entity); } const app = alepha.inject(App); await alepha.start(); await app.repo.create({ category: "A", name: "foo", active: true }); // Same category+name but inactive — allowed by partial index await app.repo.create({ category: "A", name: "foo", active: false }); const all = await app.repo.findMany(); expect(all).toHaveLength(2); }; // ============================================================================ // Feature 2: Subqueries (exists / notExists) // ============================================================================ export const testExistsSubquery = async (alepha: Alepha) => { const parentEntity = $entity({ name: "parents_exist", schema: z.object({ id: db.primaryKey(), name: z.text(), }), }); const childEntity = $entity({ name: "children_exist", schema: z.object({ id: db.primaryKey(), parentId: z.integer(), label: z.text(), }), }); class App { parents = $repository(parentEntity); children = $repository(childEntity); } const app = alepha.inject(App); await alepha.start(); const p1 = await app.parents.create({ name: "P1" }); const p2 = await app.parents.create({ name: "P2" }); await app.children.create({ parentId: p1.id, label: "C1" }); // parents that have at least one child const withChildren = await app.parents.findMany({ where: { exists: sql`(SELECT 1 FROM children_exist WHERE children_exist.parent_id = parents_exist.id)`, }, }); expect(withChildren).toHaveLength(1); expect(withChildren[0].name).toBe("P1"); // parents that have NO children const withoutChildren = await app.parents.findMany({ where: { notExists: sql`(SELECT 1 FROM children_exist WHERE children_exist.parent_id = parents_exist.id)`, }, }); expect(withoutChildren).toHaveLength(1); expect(withoutChildren[0].name).toBe("P2"); }; // ============================================================================ // Feature 4: Aggregate Functions // ============================================================================ export const testAggregateBasic = async (alepha: Alepha) => { class App { orders = $repository(orderEntity); } const app = alepha.inject(App); await alepha.start(); await app.orders.createMany([ { category: "A", amount: 100, status: "paid" }, { category: "A", amount: 200, status: "paid" }, { category: "B", amount: 50, status: "paid" }, { category: "B", amount: 150, status: "pending" }, ]); // Basic grouped aggregation const result = await app.orders.aggregate({ select: { category: true, amount: { sum: true, avg: true } }, groupBy: ["category"], orderBy: "category", }); expect(result).toHaveLength(2); expect(result[0].category).toBe("A"); expect(result[0].amount.sum).toBe(300); expect(result[0].amount.avg).toBe(150); expect(result[1].category).toBe("B"); expect(result[1].amount.sum).toBe(200); }; export const testAggregateMinMaxCount = async (alepha: Alepha) => { class App { orders = $repository(orderEntity); } const app = alepha.inject(App); await alepha.start(); await app.orders.createMany([ { category: "X", amount: 10, status: "paid" }, { category: "X", amount: 30, status: "paid" }, { category: "X", amount: 50, status: "paid" }, ]); const result = await app.orders.aggregate({ select: { category: true, amount: { min: true, max: true, count: true }, }, groupBy: ["category"], }); expect(result).toHaveLength(1); expect(result[0].amount.min).toBe(10); expect(result[0].amount.max).toBe(50); expect(result[0].amount.count).toBe(3); }; export const testAggregateHaving = async (alepha: Alepha) => { class App { orders = $repository(orderEntity); } const app = alepha.inject(App); await alepha.start(); await app.orders.createMany([ { category: "A", amount: 10, status: "paid" }, { category: "A", amount: 20, status: "paid" }, { category: "B", amount: 500, status: "paid" }, { category: "B", amount: 600, status: "paid" }, ]); // Only groups where sum > 100 const result = await app.orders.aggregate({ select: { category: true, amount: { sum: true } }, groupBy: ["category"], having: { amount: { sum: { gt: 100 } } }, }); expect(result).toHaveLength(1); expect(result[0].category).toBe("B"); expect(result[0].amount.sum).toBe(1100); }; export const testAggregateOrderByDotNotation = async (alepha: Alepha) => { class App { orders = $repository(orderEntity); } const app = alepha.inject(App); await alepha.start(); await app.orders.createMany([ { category: "A", amount: 500, status: "paid" }, { category: "B", amount: 100, status: "paid" }, { category: "C", amount: 300, status: "paid" }, ]); const result = await app.orders.aggregate({ select: { category: true, amount: { sum: true } }, groupBy: ["category"], orderBy: { column: "amount.sum", direction: "desc" }, }); expect(result).toHaveLength(3); expect(result[0].category).toBe("A"); expect(result[1].category).toBe("C"); expect(result[2].category).toBe("B"); }; // ============================================================================ // Feature 6: Generated Columns (SQLite only — virtual) // ============================================================================ export const testGeneratedColumnSqlite = async (alepha: Alepha) => { const entity = $entity({ name: "generated_col", schema: z.object({ id: db.primaryKey(), firstName: z.text(), lastName: z.text(), fullName: pgAttr(z.text(), PG_GENERATED, { expression: sql`first_name || ' ' || last_name`, mode: "virtual", }), }), }); class App { repo = $repository(entity); } const app = alepha.inject(App); await alepha.start(); // Insert without fullName — it's computed const created = await app.repo.create({ firstName: "John", lastName: "Doe", } as any); expect(created.fullName).toBe("John Doe"); // Verify findMany also returns the computed value const all = await app.repo.findMany(); expect(all).toHaveLength(1); expect(all[0].fullName).toBe("John Doe"); }; export const testGeneratedColumnPostgres = async (alepha: Alepha) => { const entity = $entity({ name: "generated_col_pg", schema: z.object({ id: db.primaryKey(), firstName: z.text(), lastName: z.text(), fullName: pgAttr(z.text(), PG_GENERATED, { expression: sql`first_name || ' ' || last_name`, mode: "stored", }), }), }); class App { repo = $repository(entity); } const app = alepha.inject(App); await alepha.start(); const created = await app.repo.create({ firstName: "Jane", lastName: "Smith", } as any); expect(created.fullName).toBe("Jane Smith"); const all = await app.repo.findMany(); expect(all).toHaveLength(1); expect(all[0].fullName).toBe("Jane Smith"); }; export const testGeneratedColumnExcludedFromInsertSchema = async ( alepha: Alepha, ) => { const entity = $entity({ name: "generated_schema_test", schema: z.object({ id: db.primaryKey(), a: z.text(), b: z.text(), computed: pgAttr(z.text(), PG_GENERATED, { expression: sql`a || b`, mode: "virtual", }), }), }); // Verify insertSchema does not contain generated column const insertProps = Object.keys(entity.insertSchema.properties); expect(insertProps).toContain("a"); expect(insertProps).toContain("b"); expect(insertProps).not.toContain("computed"); // Verify updateSchema does not contain generated column const updateProps = Object.keys(entity.updateSchema.properties); expect(updateProps).toContain("a"); expect(updateProps).toContain("b"); expect(updateProps).not.toContain("computed"); }; // ============================================================================ // Feature 7: Query Caching // ============================================================================ export const testQueryCache = async (alepha: Alepha) => { class App { orders = $repository(orderEntity); } const app = alepha.inject(App); await alepha.start(); await app.orders.create({ category: "A", amount: 100, status: "paid" }); // First call — hits DB const first = await app.orders.findMany( { where: { category: { eq: "A" } } }, { cache: { ttl: 60_000 } }, ); expect(first).toHaveLength(1); // Insert another — won't be in cache (cache was invalidated by write) await app.orders.create({ category: "A", amount: 200, status: "paid" }); // This should hit DB again (cache was invalidated by the create above) const afterInsert = await app.orders.findMany( { where: { category: { eq: "A" } } }, { cache: { ttl: 60_000 } }, ); expect(afterInsert).toHaveLength(2); // This should return cached result (no writes since last query) const cached = await app.orders.findMany( { where: { category: { eq: "A" } } }, { cache: { ttl: 60_000 } }, ); expect(cached).toHaveLength(2); }; export const testQueryCacheCustomKey = async (alepha: Alepha) => { class App { orders = $repository(orderEntity); } const app = alepha.inject(App); await alepha.start(); await app.orders.create({ category: "A", amount: 100, status: "paid" }); // Use custom cache key const result = await app.orders.findMany( {}, { cache: { ttl: 60_000, key: "all-orders" } }, ); expect(result).toHaveLength(1); // Same custom key returns cached result const cached = await app.orders.findMany( {}, { cache: { ttl: 60_000, key: "all-orders" } }, ); expect(cached).toHaveLength(1); };