UNPKG

alepha

Version:

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

458 lines (392 loc) 12.8 kB
import type { Alepha } from "alepha"; import { z } from "alepha"; import { expect } from "vitest"; import { $entity, $repository, db } from "../core/index.ts"; // ============================================================================ // Entity definitions — two entities with a foreign key relationship // ============================================================================ const teams = $entity({ name: "teams", schema: z.object({ id: db.primaryKey(), name: z.text(), country: z.text(), }), }); const players = $entity({ name: "players", schema: z.object({ id: db.primaryKey(), teamId: db.ref(z.integer().optional(), () => teams.cols.id), name: z.text(), position: z.text(), goals: db.default(z.integer(), 0), }), }); class App { teams = $repository(teams); players = $repository(players); } // ============================================================================ // Shared setup // ============================================================================ async function seed(app: App) { const barca = await app.teams.create({ name: "FC Barcelona", country: "Spain", }); const psg = await app.teams.create({ name: "Paris Saint-Germain", country: "France", }); const real = await app.teams.create({ name: "Real Madrid", country: "Spain", }); const messi = await app.players.create({ teamId: barca.id, name: "Messi", position: "Forward", goals: 672, }); const pedri = await app.players.create({ teamId: barca.id, name: "Pedri", position: "Midfielder", goals: 18, }); const mbappe = await app.players.create({ teamId: psg.id, name: "Mbappé", position: "Forward", goals: 256, }); const modric = await app.players.create({ teamId: real.id, name: "Modrić", position: "Midfielder", goals: 39, }); const freeAgent = await app.players.create({ name: "Free Agent", position: "Goalkeeper", goals: 0, }); return { teams: { barca, psg, real }, players: { messi, pedri, mbappe, modric, freeAgent }, }; } // ============================================================================ // 1. Basic left join — player with their team // ============================================================================ export const testLeftJoinPlayerWithTeam = async (alepha: Alepha) => { const app = alepha.inject(App); await alepha.start(); const { players: p } = await seed(app); const result = await app.players.getOne({ where: { id: { eq: p.messi.id } }, with: { team: { join: teams, on: ["teamId", teams.cols.id], }, }, }); expect(result.name).toBe("Messi"); expect(result.team).toBeDefined(); expect(result.team.name).toBe("FC Barcelona"); expect(result.team.country).toBe("Spain"); }; // ============================================================================ // 2. Left join returns undefined when FK is null // ============================================================================ export const testLeftJoinReturnsUndefinedWhenFkNull = async ( alepha: Alepha, ) => { const app = alepha.inject(App); await alepha.start(); const { players: p } = await seed(app); const result = await app.players.getOne({ where: { id: { eq: p.freeAgent.id } }, with: { team: { join: teams, on: ["teamId", teams.cols.id], }, }, }); expect(result.name).toBe("Free Agent"); expect(result.team).toBeUndefined(); }; // ============================================================================ // 3. Inner join excludes rows without match // ============================================================================ export const testInnerJoinExcludesNulls = async (alepha: Alepha) => { const app = alepha.inject(App); await alepha.start(); await seed(app); const results = await app.players.findMany({ with: { team: { type: "inner", join: teams, on: ["teamId", teams.cols.id], }, }, }); // Free Agent has no team → excluded by inner join expect(results.length).toBe(4); expect(results.every((r) => r.team !== null && r.team !== undefined)).toBe( true, ); expect(results.find((r) => r.name === "Free Agent")).toBeUndefined(); }; // ============================================================================ // 4. findMany with join + filter on joined table // ============================================================================ export const testFilterOnJoinedTable = async (alepha: Alepha) => { const app = alepha.inject(App); await alepha.start(); await seed(app); const spanishPlayers = await app.players.findMany({ with: { team: { join: teams, on: ["teamId", teams.cols.id], }, }, where: { team: { country: { eq: "Spain" }, }, }, }); expect(spanishPlayers.length).toBe(3); // Messi, Pedri (Barça), Modrić (Real) expect(spanishPlayers.every((p) => p.team.country === "Spain")).toBe(true); }; // ============================================================================ // 5. Combined filters — base table + joined table // ============================================================================ export const testCombinedFilters = async (alepha: Alepha) => { const app = alepha.inject(App); await alepha.start(); await seed(app); const results = await app.players.findMany({ with: { team: { join: teams, on: ["teamId", teams.cols.id], }, }, where: { and: [ { position: { eq: "Forward" } }, { team: { country: { eq: "Spain" } } }, ], }, }); expect(results.length).toBe(1); expect(results[0].name).toBe("Messi"); }; // ============================================================================ // 6. findMany with join + orderBy + limit // ============================================================================ export const testJoinWithOrderByAndLimit = async (alepha: Alepha) => { const app = alepha.inject(App); await alepha.start(); await seed(app); const topScorers = await app.players.findMany({ with: { team: { join: teams, on: ["teamId", teams.cols.id], }, }, orderBy: { column: "goals", direction: "desc" }, limit: 2, }); expect(topScorers.length).toBe(2); expect(topScorers[0].name).toBe("Messi"); expect(topScorers[1].name).toBe("Mbappé"); expect(topScorers[0].team).toBeDefined(); expect(topScorers[1].team).toBeDefined(); }; // ============================================================================ // 7. Pagination with joins // ============================================================================ export const testPaginationWithJoins = async (alepha: Alepha) => { const app = alepha.inject(App); await alepha.start(); await seed(app); const page = await app.players.paginate( { page: 0, size: 2 }, { with: { team: { join: teams, on: ["teamId", teams.cols.id], }, }, orderBy: { column: "name", direction: "asc" }, }, ); expect(page.content.length).toBe(2); expect(page.page.isFirst).toBe(true); expect(page.page.isLast).toBe(false); // Each result should include the joined team for (const player of page.content) { // Some players may have no team (left join) if (player.teamId) { expect(player.team).toBeDefined(); } } }; // ============================================================================ // 8. OR filter across base + joined table // ============================================================================ export const testOrFilterAcrossTables = async (alepha: Alepha) => { const app = alepha.inject(App); await alepha.start(); await seed(app); const results = await app.players.findMany({ with: { team: { join: teams, on: ["teamId", teams.cols.id], }, }, where: { or: [{ goals: { gte: 200 } }, { team: { country: { eq: "France" } } }], }, orderBy: { column: "goals", direction: "desc" }, }); // Messi (672), Mbappé (256 + France) — Mbappé matches both conditions expect(results.length).toBe(2); expect(results.map((r) => r.name)).toEqual(["Messi", "Mbappé"]); }; // ============================================================================ // 9. getOne with join throws when not found // ============================================================================ export const testGetOneWithJoinThrowsWhenNotFound = async (alepha: Alepha) => { const app = alepha.inject(App); await alepha.start(); await seed(app); await expect( app.players.getOne({ where: { name: { eq: "Neymar" } }, with: { team: { join: teams, on: ["teamId", teams.cols.id], }, }, }), ).rejects.toThrow(); }; // ============================================================================ // 10. findOne with join — returns undefined instead of throwing // ============================================================================ export const testFindOneWithJoinReturnsUndefined = async (alepha: Alepha) => { const app = alepha.inject(App); await alepha.start(); await seed(app); const result = await app.players.findOne({ where: { name: { eq: "Neymar" } }, with: { team: { join: teams, on: ["teamId", teams.cols.id], }, }, }); expect(result).toBeUndefined(); }; // ============================================================================ // 11. Empty result set with joins // ============================================================================ export const testEmptyResultWithJoins = async (alepha: Alepha) => { const app = alepha.inject(App); await alepha.start(); await seed(app); const results = await app.players.findMany({ where: { position: { eq: "Defender" } }, with: { team: { join: teams, on: ["teamId", teams.cols.id], }, }, }); expect(results.length).toBe(0); }; // ============================================================================ // 12. Multiple operators on joined table filter // ============================================================================ export const testMultipleOperatorsOnJoinedFilter = async (alepha: Alepha) => { const app = alepha.inject(App); await alepha.start(); await seed(app); const results = await app.players.findMany({ with: { team: { join: teams, on: ["teamId", teams.cols.id], }, }, where: { team: { name: { contains: "paris" }, // case-insensitive }, }, }); expect(results.length).toBe(1); expect(results[0].name).toBe("Mbappé"); expect(results[0].team.name).toBe("Paris Saint-Germain"); }; // ============================================================================ // 13. Join with offset (pagination without paginate) // ============================================================================ export const testJoinWithOffset = async (alepha: Alepha) => { const app = alepha.inject(App); await alepha.start(); await seed(app); const results = await app.players.findMany({ with: { team: { type: "inner", join: teams, on: ["teamId", teams.cols.id], }, }, orderBy: { column: "name", direction: "asc" }, offset: 1, limit: 2, }); // Inner join excludes Free Agent → sorted: Mbappé, Messi, Modrić, Pedri // offset 1 + limit 2 → Messi, Modrić expect(results.length).toBe(2); expect(results[0].name).toBe("Messi"); expect(results[1].name).toBe("Modrić"); }; // ============================================================================ // 14. getById / findById accept a `with` map for relation eager-loading // ============================================================================ export const testGetByIdWithRelations = async (alepha: Alepha) => { const app = alepha.inject(App); await alepha.start(); const { players: p } = await seed(app); // `as const` on the tuple should compile without an explicit cast. const withTeam = { team: { join: teams, on: ["teamId", teams.cols.id] as const }, } as const; const found = await app.players.getById(p.messi.id, { with: withTeam }); expect(found.name).toBe("Messi"); expect(found.team?.name).toBe("FC Barcelona"); // findById returns undefined on miss const missing = await app.players.findById(999_999, { with: withTeam }); expect(missing).toBeUndefined(); // Without `with`, the result is the plain row (no `.team` widening). const plain = await app.players.getById(p.messi.id); expect(plain.name).toBe("Messi"); };