Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 34 additions & 62 deletions apps/api/src/routes/content/post.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,12 @@ describe("Post Routes Tests", () => {
test("returns a post with its authors and a chapter list sorted by collectionOrder", async () => {
vi.mocked(db.query.posts.findFirst).mockResolvedValue({
slug: "chapter-two",
data: [
{
title: "Chapter Two",
description: "The second chapter",
bannerImage: "content/banner.png",
socialImage: "content/social.png",
wordCount: 500,
publishedAt: new Date("2024-01-15T00:00:00Z"),
},
],
title: "Chapter Two",
description: "The second chapter",
bannerImage: "content/banner.png",
socialImage: "content/social.png",
wordCount: 500,
publishedAt: new Date("2024-01-15T00:00:00Z"),
authors: [
{
slug: "crutchcorn",
Expand All @@ -41,12 +37,12 @@ describe("Post Routes Tests", () => {
{
slug: "chapter-two",
collectionOrder: 1,
data: [{ title: "Chapter Two" }],
title: "Chapter Two",
},
{
slug: "chapter-one",
collectionOrder: 0,
data: [{ title: "Chapter One" }],
title: "Chapter One",
},
],
},
Expand Down Expand Up @@ -96,16 +92,12 @@ describe("Post Routes Tests", () => {
test("omits collection when the post is not part of a collection", async () => {
vi.mocked(db.query.posts.findFirst).mockResolvedValue({
slug: "standalone-post",
data: [
{
title: "Standalone Post",
description: "A post with no collection",
bannerImage: null,
socialImage: null,
wordCount: 200,
publishedAt: new Date("2024-01-15T00:00:00Z"),
},
],
title: "Standalone Post",
description: "A post with no collection",
bannerImage: null,
socialImage: null,
wordCount: 200,
publishedAt: new Date("2024-01-15T00:00:00Z"),
authors: [
{ slug: "crutchcorn", name: "Corbin Crutchley", profileImage: null },
],
Expand Down Expand Up @@ -139,16 +131,12 @@ describe("Post Routes Tests", () => {
test("returns 404 when the requested post is unpublished for the locale", async () => {
vi.mocked(db.query.posts.findFirst).mockResolvedValue({
slug: "draft-post",
data: [
{
title: "Draft Post",
description: "Not yet published",
bannerImage: null,
socialImage: null,
wordCount: 100,
publishedAt: null,
},
],
title: "Draft Post",
description: "Not yet published",
bannerImage: null,
socialImage: null,
wordCount: 100,
publishedAt: null,
authors: [],
collection: null,
} as never);
Expand All @@ -170,16 +158,12 @@ describe("Post Routes Tests", () => {
test("excludes unpublished sibling chapters from the chapter list", async () => {
vi.mocked(db.query.posts.findFirst).mockResolvedValue({
slug: "chapter-one",
data: [
{
title: "Chapter One",
description: "The first chapter",
bannerImage: null,
socialImage: null,
wordCount: 300,
publishedAt: new Date("2024-01-15T00:00:00Z"),
},
],
title: "Chapter One",
description: "The first chapter",
bannerImage: null,
socialImage: null,
wordCount: 300,
publishedAt: new Date("2024-01-15T00:00:00Z"),
authors: [],
collection: {
slug: "example-collection",
Expand All @@ -188,27 +172,20 @@ describe("Post Routes Tests", () => {
{
slug: "chapter-one",
collectionOrder: 0,
data: [
{
title: "Chapter One",
publishedAt: new Date("2024-01-15T00:00:00Z"),
},
],
title: "Chapter One",
publishedAt: new Date("2024-01-15T00:00:00Z"),
},
{
slug: "chapter-two-draft",
collectionOrder: 1,
data: [{ title: "Chapter Two (Draft)", publishedAt: null }],
title: "Chapter Two (Draft)",
publishedAt: null,
},
{
slug: "chapter-three",
collectionOrder: 2,
data: [
{
title: "Chapter Three",
publishedAt: new Date("2024-01-20T00:00:00Z"),
},
],
title: "Chapter Three",
publishedAt: new Date("2024-01-20T00:00:00Z"),
},
],
},
Expand Down Expand Up @@ -264,13 +241,8 @@ describe("Post Routes Tests", () => {
`);
});

test("returns 404 when the post has no post_data row for the requested locale", async () => {
vi.mocked(db.query.posts.findFirst).mockResolvedValue({
slug: "spanish-only-post",
data: [],
authors: [],
collection: null,
} as never);
test("returns 404 when the post does not exist", async () => {
vi.mocked(db.query.posts.findFirst).mockResolvedValue(undefined as never);
Comment thread
fennifith marked this conversation as resolved.
Outdated

const response = await app.inject({
method: "GET",
Expand Down
66 changes: 30 additions & 36 deletions apps/api/src/routes/content/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const PostParamsSchema = Type.Object({

const PostQueryParamsSchema = Type.Object({
locale: Type.String({ default: "en" }),
branch: Type.String({ default: "main" }),
});

const PostResponseSchema = Type.Object(
Expand Down Expand Up @@ -107,22 +108,20 @@ const postRoutes: FastifyPluginAsync = async (fastify) => {
},
async (request, reply) => {
const { slug } = request.params;
const { locale } = request.query;
const { locale, branch } = request.query;

const post = await db.query.posts.findFirst({
where: { slug },
where: { slug, locale, branch },
columns: {
slug: true,
title: true,
description: true,
bannerImage: true,
socialImage: true,
wordCount: true,
publishedAt: true,
},
with: {
data: {
columns: {
title: true,
description: true,
bannerImage: true,
socialImage: true,
wordCount: true,
publishedAt: true,
},
where: { locale },
},
authors: { columns: { slug: true, name: true, profileImage: true } },
collection: {
with: {
Expand All @@ -131,22 +130,20 @@ const postRoutes: FastifyPluginAsync = async (fastify) => {
where: { locale },
},
posts: {
columns: { slug: true, collectionOrder: true },
with: {
data: {
columns: { title: true, publishedAt: true },
where: { locale },
},
columns: {
slug: true,
collectionOrder: true,
title: true,
publishedAt: true,
},
where: { locale, branch },
},
},
},
},
});

const postData = post?.data[0];

if (!post || !postData || postData.publishedAt === null) {
if (!post || post.publishedAt === null) {
reply.code(404);
reply.send({ error: "Post not found" });
return;
Expand All @@ -160,31 +157,28 @@ const postRoutes: FastifyPluginAsync = async (fastify) => {
slug: post.collection.slug,
title: collectionData.title,
chapters: post.collection.posts
.filter(
(chapter) =>
chapter.data[0] && chapter.data[0].publishedAt !== null,
)
.filter((chapter) => chapter.publishedAt !== null)
.sort((a, b) => a.collectionOrder - b.collectionOrder)
.map((chapter) => ({
slug: chapter.slug,
title: chapter.data[0].title,
title: chapter.title,
})),
}
: undefined;

const response: PostResponse = {
slug: post.slug,
title: postData.title,
description: postData.description,
bannerUrl: postData.bannerImage
? createImageUrl(postData.bannerImage)
title: post.title,
description: post.description,
bannerUrl: post.bannerImage
? createImageUrl(post.bannerImage)
: undefined,
socialImageUrl: postData.socialImage
? createImageUrl(postData.socialImage)
socialImageUrl: post.socialImage
? createImageUrl(post.socialImage)
: undefined,
wordCount: postData.wordCount,
publishedAt: postData.publishedAt
? postData.publishedAt.toISOString()
wordCount: post.wordCount,
publishedAt: post.publishedAt
? post.publishedAt.toISOString()
: undefined,
authors: post.authors.map((author) => ({
id: author.slug,
Expand Down
32 changes: 17 additions & 15 deletions apps/api/src/routes/content/profiles.test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
import fastify, { type FastifyInstance } from "fastify";
import profilesRoutes from "./profiles.ts";
import { db, profiles, postAuthors, postData } from "@playfulprogramming/db";
import { db, profiles, postAuthors, posts } from "@playfulprogramming/db";
import { and, asc, countDistinct, desc, eq, isNotNull } from "drizzle-orm";

function mockSelectChain(rows: unknown[]) {
const offset = vi.fn().mockResolvedValue(rows);
const limit = vi.fn().mockReturnValue({ offset });
const orderBy = vi.fn().mockReturnValue({ limit });
const groupBy = vi.fn().mockReturnValue({ orderBy });
const leftJoinPostData = vi.fn().mockReturnValue({ groupBy });
const leftJoinPosts = vi
.fn()
.mockReturnValue({ groupBy });
const leftJoinPostAuthors = vi
.fn()
.mockReturnValue({ leftJoin: leftJoinPostData });
.mockReturnValue({ leftJoin: leftJoinPosts });
const from = vi.fn().mockReturnValue({ leftJoin: leftJoinPostAuthors });

vi.mocked(db.select).mockReturnValue({ from } as never);

return {
from,
leftJoinPostAuthors,
leftJoinPostData,
leftJoinPosts,
groupBy,
orderBy,
limit,
Expand Down Expand Up @@ -106,8 +108,8 @@ describe("Profiles Routes Tests", () => {
});

expect(response.statusCode).toBe(200);
expect(chain.limit).toBeCalledWith(10);
expect(chain.offset).toBeCalledWith(20);
expect(chain.limit).toHaveBeenCalledWith(10);
expect(chain.offset).toHaveBeenCalledWith(20);
});

test("defaults to sortBy=id, ordering by profile slug ascending", async () => {
Expand All @@ -120,7 +122,7 @@ describe("Profiles Routes Tests", () => {
});

expect(response.statusCode).toBe(200);
expect(chain.orderBy).toBeCalledWith(asc(profiles.slug));
expect(chain.orderBy).toHaveBeenCalledWith(asc(profiles.slug));
});

test("sortBy=posts orders authors by descending post count", async () => {
Expand Down Expand Up @@ -148,8 +150,8 @@ describe("Profiles Routes Tests", () => {
});

expect(response.statusCode).toBe(200);
expect(chain.orderBy).toBeCalledWith(
desc(countDistinct(postData.slug)),
expect(chain.orderBy).toHaveBeenCalledWith(
desc(countDistinct(posts.slug)),
asc(profiles.slug),
);
expect(
Expand All @@ -167,7 +169,7 @@ describe("Profiles Routes Tests", () => {
});

expect(response.statusCode).toBe(200);
expect(chain.leftJoinPostAuthors).toBeCalledWith(
expect(chain.leftJoinPostAuthors).toHaveBeenCalledWith(
postAuthors,
eq(postAuthors.authorSlug, profiles.slug),
);
Expand All @@ -183,12 +185,12 @@ describe("Profiles Routes Tests", () => {
});

expect(response.statusCode).toBe(200);
expect(chain.leftJoinPostData).toBeCalledWith(
postData,
expect(chain.leftJoinPosts).toHaveBeenCalledWith(
posts,
and(
eq(postData.slug, postAuthors.postSlug),
isNotNull(postData.publishedAt),
eq(postData.noindex, false),
eq(posts.id, postAuthors.postId),
isNotNull(posts.publishedAt),
eq(posts.noindex, false),
),
);
});
Expand Down
14 changes: 7 additions & 7 deletions apps/api/src/routes/content/profiles.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FastifyPluginAsync } from "fastify";
import { db, profiles, postAuthors, postData } from "@playfulprogramming/db";
import { db, profiles, postAuthors, posts } from "@playfulprogramming/db";
import { and, asc, countDistinct, desc, eq, isNotNull } from "drizzle-orm";
import { Type, type Static } from "typebox";
import { createImageUrl } from "../../utils.ts";
Expand Down Expand Up @@ -76,16 +76,16 @@ const profilesRoutes: FastifyPluginAsync = async (fastify) => {
name: profiles.name,
description: profiles.description,
profileImage: profiles.profileImage,
postsCount: countDistinct(postData.slug),
postsCount: countDistinct(posts.slug),
})
.from(profiles)
.leftJoin(postAuthors, eq(postAuthors.authorSlug, profiles.slug))
.leftJoin(
postData,
posts,
and(
eq(postData.slug, postAuthors.postSlug),
isNotNull(postData.publishedAt),
eq(postData.noindex, false),
eq(posts.id, postAuthors.postId),
isNotNull(posts.publishedAt),
eq(posts.noindex, false),
),
)
.groupBy(
Expand All @@ -96,7 +96,7 @@ const profilesRoutes: FastifyPluginAsync = async (fastify) => {
)
.orderBy(
...(sortBy === "posts"
? [desc(countDistinct(postData.slug)), asc(profiles.slug)]
? [desc(countDistinct(posts.slug)), asc(profiles.slug)]
: [asc(profiles.slug)]),
)
.limit(queryParams.limit)
Expand Down
Loading
Loading