alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
1,076 lines (924 loc) • 27.5 kB
text/typescript
import type { Alepha } from "alepha";
import { z } from "alepha";
import { sql } from "drizzle-orm";
import { expect } from "vitest";
import { $entity, $repository, db } from "../core/index.ts";
// ============================================================================
// Schema definitions
// ============================================================================
const countries = $entity({
name: "countries",
schema: z.object({
id: db.primaryKey(),
name: z.text(),
code: z.text(),
}),
});
const cities = $entity({
name: "cities",
schema: z.object({
id: db.primaryKey(),
countryId: db.ref(z.integer(), () => countries.cols.id),
name: z.text(),
population: z.integer().optional(),
}),
});
const users = $entity({
name: "users",
schema: z.object({
id: db.primaryKey(),
name: z.text(),
email: z.text(),
cityId: db.ref(z.integer().optional(), () => cities.cols.id),
managerId: db.ref(z.integer().optional(), () => users.cols.id),
}),
});
const profiles = $entity({
name: "profiles",
schema: z.object({
id: db.primaryKey(),
userId: db.ref(z.integer(), () => users.cols.id),
bio: z.text(),
website: z.text().optional(),
}),
});
const posts = $entity({
name: "posts",
schema: z.object({
id: db.primaryKey(),
authorId: db.ref(z.integer(), () => users.cols.id),
title: z.text(),
content: z.text(),
publishedAt: db.createdAt(),
}),
});
const comments = $entity({
name: "comments",
schema: z.object({
id: db.primaryKey(),
postId: db.ref(z.integer(), () => posts.cols.id),
authorId: db.ref(z.integer(), () => users.cols.id),
content: z.text(),
createdAt: db.createdAt(),
}),
});
const tags = $entity({
name: "tags",
schema: z.object({
id: db.primaryKey(),
name: z.text(),
}),
});
const postTags = $entity({
name: "post_tags",
schema: z.object({
id: db.primaryKey(),
postId: db.ref(z.integer(), () => posts.cols.id),
tagId: db.ref(z.integer(), () => tags.cols.id),
}),
});
class App {
countries = $repository(countries);
cities = $repository(cities);
users = $repository(users);
profiles = $repository(profiles);
posts = $repository(posts);
comments = $repository(comments);
tags = $repository(tags);
postTags = $repository(postTags);
}
// ============================================================================
// Test setup helpers
// ============================================================================
async function setupTestData(app: App) {
// Create countries
const usa = await app.countries.create({
name: "United States",
code: "US",
});
const canada = await app.countries.create({ name: "Canada", code: "CA" });
// Create cities
const nyc = await app.cities.create({
countryId: usa.id,
name: "New York",
population: 8_000_000,
});
const toronto = await app.cities.create({
countryId: canada.id,
name: "Toronto",
population: 3_000_000,
});
const vancouver = await app.cities.create({
countryId: canada.id,
name: "Vancouver",
population: 700_000,
});
// Create users
const alice = await app.users.create({
name: "Alice",
email: "alice@example.com",
cityId: nyc.id,
});
const bob = await app.users.create({
name: "Bob",
email: "bob@example.com",
cityId: toronto.id,
managerId: alice.id,
});
const charlie = await app.users.create({
name: "Charlie",
email: "charlie@example.com",
cityId: vancouver.id,
managerId: alice.id,
});
const diana = await app.users.create({
name: "Diana",
email: "diana@example.com",
cityId: toronto.id,
managerId: bob.id,
});
// Create profiles
await app.profiles.create({
userId: alice.id,
bio: "Tech lead and mentor",
website: "https://alice.dev",
});
await app.profiles.create({
userId: bob.id,
bio: "Senior developer",
});
await app.profiles.create({
userId: charlie.id,
bio: "Frontend specialist",
website: "https://charlie.io",
});
// Create posts
const post1 = await app.posts.create({
authorId: alice.id,
title: "Introduction to TypeScript",
content: "TypeScript is great...",
});
const post2 = await app.posts.create({
authorId: alice.id,
title: "Advanced PostgreSQL",
content: "Let's dive deep into Postgres...",
});
const post3 = await app.posts.create({
authorId: bob.id,
title: "React Best Practices",
content: "Here are some tips...",
});
// Create tags
const typescript = await app.tags.create({ name: "TypeScript" });
const postgres = await app.tags.create({ name: "PostgreSQL" });
const react = await app.tags.create({ name: "React" });
// Create post-tag associations
await app.postTags.create({ postId: post1.id, tagId: typescript.id });
await app.postTags.create({ postId: post2.id, tagId: postgres.id });
await app.postTags.create({ postId: post3.id, tagId: react.id });
await app.postTags.create({ postId: post3.id, tagId: typescript.id });
// Create comments
await app.comments.create({
postId: post1.id,
authorId: bob.id,
content: "Great article!",
});
await app.comments.create({
postId: post1.id,
authorId: charlie.id,
content: "Very helpful, thanks!",
});
await app.comments.create({
postId: post2.id,
authorId: diana.id,
content: "Could you explain more about indexes?",
});
return {
countries: { usa, canada },
cities: { nyc, toronto, vancouver },
users: { alice, bob, charlie, diana },
posts: { post1, post2, post3 },
tags: { typescript, postgres, react },
};
}
// ============================================================================
// Basic joins
// ============================================================================
export const testSimpleLeftJoin = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
const { users: testUsers } = await setupTestData(app);
const result = await app.users.getOne({
where: { id: { eq: testUsers.alice.id } },
with: {
profile: {
join: profiles,
on: ["id", profiles.cols.userId],
},
},
});
expect(result.id).toBe(testUsers.alice.id);
expect(result.name).toBe("Alice");
expect(result.profile).toBeDefined();
expect(result.profile.bio).toBe("Tech lead and mentor");
expect(result.profile.website).toBe("https://alice.dev");
};
export const testLeftJoinTupleSyntax = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
const { cities: testCities } = await setupTestData(app);
const result = await app.cities.getOne({
where: { id: { eq: testCities.nyc.id } },
with: {
country: {
join: countries,
on: ["countryId", countries.cols.id],
},
},
});
expect(result.name).toBe("New York");
expect(result.country).toBeDefined();
expect(result.country.name).toBe("United States");
expect(result.country.code).toBe("US");
};
export const testLeftJoinSqlWrapper = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
const { users: testUsers } = await setupTestData(app);
const result = await app.users.getOne({
where: { id: { eq: testUsers.alice.id } },
with: {
profile: {
join: profiles,
on: sql`${users.cols.id} = ${profiles.cols.userId}`,
},
},
});
expect(result.name).toBe("Alice");
expect(result.profile).toBeDefined();
expect(result.profile.bio).toBe("Tech lead and mentor");
};
export const testInnerJoin = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
await setupTestData(app);
// Diana has no profile, so should be excluded with inner join
const results = await app.users.findMany({
with: {
profile: {
type: "inner",
join: profiles,
on: ["id", profiles.cols.userId],
},
},
});
expect(results.length).toBe(3); // Alice, Bob, Charlie have profiles
expect(results.every((u) => u.profile !== null)).toBe(true);
expect(results.find((u) => u.name === "Diana")).toBeUndefined();
};
// ============================================================================
// Self-referencing joins
// ============================================================================
export const testSelfReferencingJoinWithManager = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
const { users: testUsers } = await setupTestData(app);
const manager = users.alias("manager");
const result = await app.users.getOne({
where: { id: { eq: testUsers.bob.id } },
with: {
manager: {
join: manager,
on: ["managerId", manager.cols.id],
},
},
});
expect(result.name).toBe("Bob");
expect(result.manager).toBeDefined();
expect(result.manager.name).toBe("Alice");
expect(result.manager.email).toBe("alice@example.com");
};
export const testSelfReferencingJoinWithoutManager = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
await setupTestData(app);
const results = await app.users.findMany({
where: {
managerId: { isNull: true },
},
});
expect(results.length).toBe(1);
expect(results[0].name).toBe("Alice");
};
// ============================================================================
// Multiple joins
// ============================================================================
export const testMultipleJoins = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
const { users: testUsers } = await setupTestData(app);
const result = await app.users.getOne({
where: { id: { eq: testUsers.bob.id } },
with: {
profile: {
join: profiles,
on: ["id", profiles.cols.userId],
},
city: {
join: cities,
on: ["cityId", cities.cols.id],
},
},
});
expect(result.name).toBe("Bob");
expect(result.profile).toBeDefined();
expect(result.profile.bio).toBe("Senior developer");
expect(result.city).toBeDefined();
expect(result.city.name).toBe("Toronto");
expect(result.city.population).toBe(3_000_000);
};
export const testMultipleJoinsWithDifferentTypes = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
const { users: testUsers } = await setupTestData(app);
const result = await app.users.getOne({
where: { id: { eq: testUsers.alice.id } },
with: {
profile: {
type: "inner", // Must have profile
join: profiles,
on: ["id", profiles.cols.userId],
},
city: {
type: "left", // May not have city
join: cities,
on: ["cityId", cities.cols.id],
},
},
});
expect(result.name).toBe("Alice");
expect(result.profile).toBeDefined();
expect(result.city).toBeDefined();
};
// ============================================================================
// Nested joins (2 levels)
// ============================================================================
export const testNestedJoin2LevelsUserCityCountry = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
const { users: testUsers } = await setupTestData(app);
const result = await app.users.getOne({
where: { id: { eq: testUsers.bob.id } },
with: {
city: {
join: cities,
on: ["cityId", cities.cols.id],
with: {
country: {
join: countries,
on: ["countryId", countries.cols.id],
},
},
},
},
});
expect(result.name).toBe("Bob");
expect(result.city).toBeDefined();
expect(result.city.name).toBe("Toronto");
expect(result.city.country).toBeDefined();
expect(result.city.country.name).toBe("Canada");
expect(result.city.country.code).toBe("CA");
};
export const testNestedJoin2LevelsPostAuthorCity = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
const { posts: testPosts } = await setupTestData(app);
const result = await app.posts.getOne({
where: { id: { eq: testPosts.post1.id } },
with: {
author: {
join: users,
on: ["authorId", users.cols.id],
with: {
city: {
join: cities,
on: ["cityId", cities.cols.id],
},
},
},
},
});
expect(result.title).toBe("Introduction to TypeScript");
expect(result.author).toBeDefined();
expect(result.author.name).toBe("Alice");
expect(result.author.city).toBeDefined();
expect(result.author.city.name).toBe("New York");
};
export const testMultipleNestedJoinsWithManagerAndCities = async (
alepha: Alepha,
) => {
const app = alepha.inject(App);
await alepha.start();
const { users: testUsers } = await setupTestData(app);
const manager = users.alias("manager");
const managerCity = cities.alias("manager_city");
const result = await app.users.getOne({
where: { id: { eq: testUsers.bob.id } },
with: {
city: {
join: cities,
on: ["cityId", cities.cols.id],
with: {
country: {
join: countries,
on: ["countryId", countries.cols.id],
},
},
},
manager: {
join: manager,
on: ["managerId", manager.cols.id],
with: {
city: {
join: managerCity,
on: ["cityId", managerCity.cols.id],
},
},
},
},
});
expect(result.name).toBe("Bob");
expect(result.city).toBeDefined();
expect(result.city.name).toBe("Toronto");
expect(result.city.country).toBeDefined();
expect(result.city.country.name).toBe("Canada");
expect(result.manager).toBeDefined();
expect(result.manager.name).toBe("Alice");
expect(result.manager.city).toBeDefined();
expect(result.manager.city.name).toBe("New York");
};
// ============================================================================
// Nested joins (3 levels)
// ============================================================================
export const testNestedJoin3LevelsPostAuthorCityCountry = async (
alepha: Alepha,
) => {
const app = alepha.inject(App);
await alepha.start();
const { posts: testPosts } = await setupTestData(app);
const result = await app.posts.getOne({
where: { id: { eq: testPosts.post3.id } },
with: {
author: {
join: users,
on: ["authorId", users.cols.id],
with: {
city: {
join: cities,
on: ["cityId", cities.cols.id],
with: {
country: {
join: countries,
on: ["countryId", countries.cols.id],
},
},
},
},
},
},
});
expect(result.title).toBe("React Best Practices");
expect(result.author).toBeDefined();
expect(result.author.name).toBe("Bob");
expect(result.author.city).toBeDefined();
expect(result.author.city.name).toBe("Toronto");
expect(result.author.city.country).toBeDefined();
expect(result.author.city.country.name).toBe("Canada");
expect(result.author.city.country.code).toBe("CA");
};
export const testDeeplyNestedSelfReference = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
const { users: testUsers } = await setupTestData(app);
const manager = users.alias("manager");
const managerManager = users.alias("manager_manager");
const result = await app.users.getOne({
where: { id: { eq: testUsers.diana.id } },
with: {
manager: {
join: manager,
on: ["managerId", manager.cols.id],
with: {
manager: {
join: managerManager,
on: ["managerId", managerManager.cols.id],
},
},
},
},
});
expect(result.name).toBe("Diana");
expect(result.manager).toBeDefined();
expect(result.manager.name).toBe("Bob");
expect(result.manager.manager).toBeDefined();
expect(result.manager.manager.name).toBe("Alice");
};
// ============================================================================
// Where clause on joined tables
// ============================================================================
export const testFilterByJoinedTableColumn = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
await setupTestData(app);
const results = await app.users.findMany({
with: {
city: {
join: cities,
on: ["cityId", cities.cols.id],
},
},
where: {
city: {
name: { eq: "Toronto" },
},
},
});
expect(results.length).toBe(2); // Bob and Diana
expect(results.every((u) => u.city.name === "Toronto")).toBe(true);
expect(results.map((u) => u.name).sort()).toEqual(["Bob", "Diana"]);
};
export const testFilterByNestedJoinedTableColumn = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
await setupTestData(app);
const results = await app.users.findMany({
with: {
city: {
join: cities,
on: ["cityId", cities.cols.id],
with: {
country: {
join: countries,
on: ["countryId", countries.cols.id],
},
},
},
},
where: {
city: {
country: {
code: { eq: "CA" },
},
},
},
});
expect(results.length).toBe(3); // Bob, Charlie, Diana in Canadian cities
expect(results.every((u) => u.city.country.code === "CA")).toBe(true);
};
export const testCombineBaseAndJoinedTableFilters = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
await setupTestData(app);
const results = await app.users.findMany({
with: {
city: {
join: cities,
on: ["cityId", cities.cols.id],
},
},
where: {
and: [
{ name: { contains: "ar" } }, // Charlie
{
city: {
population: { gt: 500_000 },
},
},
],
},
});
expect(results.length).toBe(1);
expect(results[0].name).toBe("Charlie");
expect(results[0].city.name).toBe("Vancouver");
};
export const testFilterBySelfReferencingJoin = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
await setupTestData(app);
const manager = users.alias("manager");
const results = await app.users.findMany({
with: {
manager: {
join: manager,
on: ["managerId", manager.cols.id],
},
},
where: {
manager: {
name: { eq: "Alice" },
},
},
});
expect(results.length).toBe(2); // Bob and Charlie report to Alice
expect(results.map((u) => u.name).sort()).toEqual(["Bob", "Charlie"]);
expect(results.every((u) => u.manager?.name === "Alice")).toBe(true);
};
// ============================================================================
// Find multiple with joins
// ============================================================================
export const testFindMultipleWithJoins = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
await setupTestData(app);
const results = await app.users.findMany({
with: {
profile: {
join: profiles,
on: ["id", profiles.cols.userId],
},
city: {
join: cities,
on: ["cityId", cities.cols.id],
},
},
orderBy: { column: "name", direction: "asc" },
});
expect(results.length).toBeGreaterThan(0);
expect(results[0].name).toBe("Alice");
expect(results[0].profile).toBeDefined();
expect(results[0].city).toBeDefined();
};
export const testFindWithJoinsAndLimit = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
await setupTestData(app);
const results = await app.users.findMany({
with: {
city: {
join: cities,
on: ["cityId", cities.cols.id],
},
},
limit: 2,
orderBy: { column: "name", direction: "asc" },
});
expect(results.length).toBe(2);
expect(results[0].name).toBe("Alice");
expect(results[1].name).toBe("Bob");
};
export const testFindWithJoinsAndOffset = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
await setupTestData(app);
const results = await app.users.findMany({
with: {
profile: {
join: profiles,
on: ["id", profiles.cols.userId],
},
},
offset: 1,
limit: 2,
orderBy: { column: "name", direction: "asc" },
});
expect(results.length).toBe(2);
expect(results[0].name).toBe("Bob");
expect(results[1].name).toBe("Charlie");
};
// ============================================================================
// Complex scenarios
// ============================================================================
export const testPostWithAuthorCommentsAndCommentAuthors = async (
alepha: Alepha,
) => {
const app = alepha.inject(App);
await alepha.start();
const { posts: testPosts } = await setupTestData(app);
// This requires multiple separate queries since we don't support one-to-many yet
const post = await app.posts.getOne({
where: { id: { eq: testPosts.post1.id } },
with: {
author: {
join: users,
on: ["authorId", users.cols.id],
},
},
});
const postComments = await app.comments.findMany({
where: { postId: { eq: testPosts.post1.id } },
with: {
author: {
join: users,
on: ["authorId", users.cols.id],
},
},
});
expect(post.title).toBe("Introduction to TypeScript");
expect(post.author.name).toBe("Alice");
expect(postComments.length).toBe(2);
expect(postComments[0].author).toBeDefined();
expect(postComments[1].author).toBeDefined();
};
export const testJoinWithNullForeignKey = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
await setupTestData(app);
// Create user without city
const userWithoutCity = await app.users.create({
name: "Eve",
email: "eve@example.com",
});
const result = await app.users.getOne({
where: { id: { eq: userWithoutCity.id } },
with: {
city: {
join: cities,
on: ["cityId", cities.cols.id],
},
},
});
expect(result.name).toBe("Eve");
expect(result.city).toBeUndefined();
};
export const testComplexWhereWithMultipleJoinFilters = async (
alepha: Alepha,
) => {
const app = alepha.inject(App);
await alepha.start();
await setupTestData(app);
const manager = users.alias("manager");
const results = await app.users.findMany({
with: {
city: {
join: cities,
on: ["cityId", cities.cols.id],
with: {
country: {
join: countries,
on: ["countryId", countries.cols.id],
},
},
},
manager: {
join: manager,
on: ["managerId", manager.cols.id],
},
},
where: {
and: [
{ email: { endsWith: "example.com" } },
{
or: [
{
city: {
population: { gte: 1_000_000 },
},
},
{
manager: {
name: { eq: "Alice" },
},
},
],
},
],
},
});
expect(results.length).toBeGreaterThan(0);
expect(results.every((u) => u.email.endsWith("example.com"))).toBe(true);
};
// ============================================================================
// Pagination with joins
// ============================================================================
export const testPaginateWithJoins = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
await setupTestData(app);
const page = await app.users.paginate(
{ page: 0, size: 2 },
{
with: {
city: {
join: cities,
on: ["cityId", cities.cols.id],
},
},
orderBy: { column: "name", direction: "asc" },
},
);
expect(page.content.length).toBe(2);
expect(page.content[0].name).toBe("Alice");
expect(page.content[0].city).toBeDefined();
expect(page.page.isFirst).toBe(true);
expect(page.page.isLast).toBe(false);
};
export const testPaginateWithNestedJoins = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
await setupTestData(app);
const page = await app.users.paginate(
{ page: 0, size: 3 },
{
with: {
city: {
join: cities,
on: ["cityId", cities.cols.id],
with: {
country: {
join: countries,
on: ["countryId", countries.cols.id],
},
},
},
},
},
);
expect(page.content.length).toBe(3);
expect(page.content[0].city).toBeDefined();
expect(page.content[0].city.country).toBeDefined();
};
// ============================================================================
// Edge cases
// ============================================================================
export const testInnerJoinNoMatchingRecords = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
await setupTestData(app);
const results = await app.users.findMany({
where: { name: { eq: "Diana" } }, // Diana has no profile
with: {
profile: {
type: "inner",
join: profiles,
on: ["id", profiles.cols.userId],
},
},
});
expect(results.length).toBe(0);
};
export const testSameTableJoinedMultipleTimesWithAliases = async (
alepha: Alepha,
) => {
const app = alepha.inject(App);
await alepha.start();
const { posts: testPosts } = await setupTestData(app);
const author = users.alias("author");
const authorCity = cities.alias("author_city");
// Get first comment
const comment = await app.comments.getOne({
where: { postId: { eq: testPosts.post1.id } },
});
const result = await app.comments.getOne({
where: { id: { eq: comment.id } },
with: {
post: {
join: posts,
on: ["postId", posts.cols.id],
with: {
author: {
join: author,
on: ["authorId", author.cols.id],
with: {
city: {
join: authorCity,
on: ["cityId", authorCity.cols.id],
},
},
},
},
},
author: {
join: users,
on: ["authorId", users.cols.id],
with: {
city: {
join: cities,
on: ["cityId", cities.cols.id],
},
},
},
},
});
expect(result.content).toBeDefined();
expect(result.post).toBeDefined();
expect(result.post.author).toBeDefined();
expect(result.author).toBeDefined();
// Both should have cities
expect(result.post.author.city).toBeDefined();
expect(result.author.city).toBeDefined();
};
export const testEmptyResultSetWithJoins = async (alepha: Alepha) => {
const app = alepha.inject(App);
await alepha.start();
await setupTestData(app);
const results = await app.users.findMany({
where: { name: { eq: "NonExistentUser" } },
with: {
profile: {
join: profiles,
on: ["id", profiles.cols.userId],
},
},
});
expect(results.length).toBe(0);
};