mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Fix artists missing from showcase if no showcase art set, caching & Kysely migration
Closes #2591
This commit is contained in:
parent
b059de162f
commit
a2d8cb2936
167
app/features/art/ArtRepository.server.test.ts
Normal file
167
app/features/art/ArtRepository.server.test.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import { db } from "~/db/sql";
|
||||
import { dbInsertUsers, dbReset } from "~/utils/Test";
|
||||
import * as ArtRepository from "./ArtRepository.server";
|
||||
|
||||
let imageCounter = 0;
|
||||
|
||||
const createUserSubmittedImage = async (userId: number) => {
|
||||
imageCounter++;
|
||||
|
||||
// TODO: instead of using db, use ArtRepository's insert when implemented
|
||||
const result = await db
|
||||
.insertInto("UnvalidatedUserSubmittedImage")
|
||||
.values({
|
||||
url: `https://example.com/image-${userId}-${imageCounter}.png`,
|
||||
submitterUserId: userId,
|
||||
validatedAt: Date.now(),
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return result.id;
|
||||
};
|
||||
|
||||
const createArt = async ({
|
||||
authorId,
|
||||
isShowcase = 0,
|
||||
createdAt,
|
||||
}: {
|
||||
authorId: number;
|
||||
isShowcase?: 0 | 1;
|
||||
createdAt?: number;
|
||||
}) => {
|
||||
const imgId = await createUserSubmittedImage(authorId);
|
||||
|
||||
await db
|
||||
.insertInto("Art")
|
||||
.values({
|
||||
authorId,
|
||||
imgId,
|
||||
isShowcase,
|
||||
description: null,
|
||||
createdAt: createdAt ?? Date.now(),
|
||||
})
|
||||
.execute();
|
||||
};
|
||||
|
||||
describe("findShowcaseArts", () => {
|
||||
beforeEach(async () => {
|
||||
imageCounter = 0;
|
||||
await dbInsertUsers(5);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
test("shows one art per artist when all have showcase art", async () => {
|
||||
await createArt({ authorId: 1, isShowcase: 1 });
|
||||
await createArt({ authorId: 2, isShowcase: 1 });
|
||||
await createArt({ authorId: 3, isShowcase: 1 });
|
||||
|
||||
const result = await ArtRepository.findShowcaseArts();
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
const authorIds = result.map((art) => art.author?.discordId);
|
||||
expect(new Set(authorIds).size).toBe(3);
|
||||
});
|
||||
|
||||
test("prioritizes showcase art over regular art for same artist", async () => {
|
||||
const now = Date.now();
|
||||
|
||||
await createArt({ authorId: 1, isShowcase: 0, createdAt: now + 1000 });
|
||||
await createArt({ authorId: 1, isShowcase: 1, createdAt: now });
|
||||
|
||||
const result = await ArtRepository.findShowcaseArts();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].createdAt).toBe(now);
|
||||
});
|
||||
|
||||
test("shows most recent art for artist without showcase art", async () => {
|
||||
const now = Date.now();
|
||||
|
||||
await createArt({ authorId: 1, isShowcase: 0, createdAt: now });
|
||||
await createArt({ authorId: 1, isShowcase: 0, createdAt: now + 1000 });
|
||||
|
||||
const result = await ArtRepository.findShowcaseArts();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].createdAt).toBe(now + 1000);
|
||||
});
|
||||
|
||||
test("shows only one art per artist even with multiple showcase pieces", async () => {
|
||||
const now = Date.now();
|
||||
|
||||
await createArt({ authorId: 1, isShowcase: 1, createdAt: now });
|
||||
await createArt({ authorId: 1, isShowcase: 1, createdAt: now + 1000 });
|
||||
await createArt({ authorId: 1, isShowcase: 1, createdAt: now + 2000 });
|
||||
|
||||
const result = await ArtRepository.findShowcaseArts();
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].createdAt).toBe(now + 2000);
|
||||
});
|
||||
|
||||
test("handles mix of artists with and without showcase art", async () => {
|
||||
const now = Date.now();
|
||||
|
||||
await createArt({ authorId: 1, isShowcase: 1, createdAt: now });
|
||||
await createArt({ authorId: 2, isShowcase: 0, createdAt: now + 1000 });
|
||||
await createArt({ authorId: 2, isShowcase: 0, createdAt: now + 2000 });
|
||||
await createArt({ authorId: 3, isShowcase: 1, createdAt: now + 3000 });
|
||||
|
||||
const result = await ArtRepository.findShowcaseArts();
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
|
||||
const author1Art = result.find((art) => art.author?.discordId === "0");
|
||||
const author2Art = result.find((art) => art.author?.discordId === "1");
|
||||
|
||||
expect(author1Art?.createdAt).toBe(now);
|
||||
expect(author2Art?.createdAt).toBe(now + 2000);
|
||||
});
|
||||
|
||||
test("returns empty array when no art exists", async () => {
|
||||
const result = await ArtRepository.findShowcaseArts();
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findAllTags", () => {
|
||||
beforeEach(async () => {
|
||||
await dbInsertUsers(1);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
dbReset();
|
||||
});
|
||||
|
||||
test("returns all art tags", async () => {
|
||||
await db
|
||||
.insertInto("ArtTag")
|
||||
.values([
|
||||
{ authorId: 1, name: "Character" },
|
||||
{ authorId: 1, name: "Weapon" },
|
||||
{ authorId: 1, name: "Landscape" },
|
||||
])
|
||||
.execute();
|
||||
|
||||
const result = await ArtRepository.findAllTags();
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result.map((t) => t.name).sort()).toEqual([
|
||||
"Character",
|
||||
"Landscape",
|
||||
"Weapon",
|
||||
]);
|
||||
});
|
||||
|
||||
test("returns empty array when no tags exist", async () => {
|
||||
const result = await ArtRepository.findAllTags();
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -33,26 +33,41 @@ export async function findShowcaseArts(): Promise<ListedArt[]> {
|
|||
.select([
|
||||
"Art.id",
|
||||
"Art.createdAt",
|
||||
"User.id as userId",
|
||||
"User.discordId",
|
||||
"User.username",
|
||||
"User.discordAvatar",
|
||||
"User.commissionsOpen",
|
||||
"UserSubmittedImage.url",
|
||||
])
|
||||
.where("Art.isShowcase", "=", 1)
|
||||
.orderBy("Art.isShowcase", "desc")
|
||||
.orderBy("Art.createdAt", "desc")
|
||||
.orderBy("User.id", "asc")
|
||||
.execute();
|
||||
|
||||
const mappedArts = arts.map((a) => ({
|
||||
id: a.id,
|
||||
createdAt: a.createdAt,
|
||||
url: a.url,
|
||||
author: {
|
||||
commissionsOpen: a.commissionsOpen,
|
||||
discordAvatar: a.discordAvatar,
|
||||
discordId: a.discordId,
|
||||
username: a.username,
|
||||
},
|
||||
}));
|
||||
const encounteredUserIds = new Set<number>();
|
||||
|
||||
const mappedArts = arts
|
||||
.filter((row) => {
|
||||
if (encounteredUserIds.has(row.userId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
encounteredUserIds.add(row.userId);
|
||||
|
||||
return true;
|
||||
})
|
||||
.map((a) => ({
|
||||
id: a.id,
|
||||
createdAt: a.createdAt,
|
||||
url: a.url,
|
||||
author: {
|
||||
commissionsOpen: a.commissionsOpen,
|
||||
discordAvatar: a.discordAvatar,
|
||||
discordId: a.discordId,
|
||||
username: a.username,
|
||||
},
|
||||
}));
|
||||
|
||||
const { seededShuffle } = seededRandom(getDailySeed());
|
||||
return seededShuffle(mappedArts);
|
||||
|
|
@ -136,3 +151,7 @@ export async function findRecentlyUploadedArts(): Promise<ListedArt[]> {
|
|||
},
|
||||
}));
|
||||
}
|
||||
|
||||
export async function findAllTags() {
|
||||
return db.selectFrom("ArtTag").select(["id", "name"]).execute();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { requireUser } from "~/features/auth/core/user.server";
|
||||
import * as ArtRepository from "../ArtRepository.server";
|
||||
import { NEW_ART_EXISTING_SEARCH_PARAM_KEY } from "../art-constants";
|
||||
import { allArtTags } from "../queries/allArtTags.server";
|
||||
import { findArtById } from "../queries/findArtById.server";
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
|
|
@ -10,13 +10,13 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||
const artIdRaw = new URL(request.url).searchParams.get(
|
||||
NEW_ART_EXISTING_SEARCH_PARAM_KEY,
|
||||
);
|
||||
if (!artIdRaw) return { art: null, tags: allArtTags() };
|
||||
if (!artIdRaw) return { art: null, tags: await ArtRepository.findAllTags() };
|
||||
const artId = Number(artIdRaw);
|
||||
|
||||
const art = findArtById(artId);
|
||||
if (!art || art.authorId !== user.id) {
|
||||
return { art: null, tags: allArtTags() };
|
||||
return { art: null, tags: await ArtRepository.findAllTags() };
|
||||
}
|
||||
|
||||
return { art, tags: allArtTags() };
|
||||
return { art, tags: await ArtRepository.findAllTags() };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,21 +1,35 @@
|
|||
import cachified from "@epic-web/cachified";
|
||||
import type { LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { cache, IN_MILLISECONDS, ttl } from "~/utils/cache.server";
|
||||
import * as ArtRepository from "../ArtRepository.server";
|
||||
import { FILTERED_TAG_KEY_SEARCH_PARAM_KEY } from "../art-constants";
|
||||
import { allArtTags } from "../queries/allArtTags.server";
|
||||
|
||||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
const allTags = allArtTags();
|
||||
const cachedArts = await cachified({
|
||||
key: "arts",
|
||||
cache,
|
||||
ttl: ttl(IN_MILLISECONDS.TWO_HOURS),
|
||||
async getFreshValue() {
|
||||
return {
|
||||
showcaseArts: await ArtRepository.findShowcaseArts(),
|
||||
recentlyUploadedArts: await ArtRepository.findRecentlyUploadedArts(),
|
||||
allTags: await ArtRepository.findAllTags(),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const filteredTagName = new URL(request.url).searchParams.get(
|
||||
FILTERED_TAG_KEY_SEARCH_PARAM_KEY,
|
||||
);
|
||||
const filteredTag = allTags.find((t) => t.name === filteredTagName);
|
||||
|
||||
const filteredTag = filteredTagName
|
||||
? cachedArts.allTags.find((t) => t.name === filteredTagName)
|
||||
: null;
|
||||
|
||||
if (!filteredTag) return cachedArts;
|
||||
|
||||
return {
|
||||
showcaseArts: filteredTag
|
||||
? await ArtRepository.findShowcaseArtsByTag(filteredTag.id)
|
||||
: await ArtRepository.findShowcaseArts(),
|
||||
recentlyUploadedArts: await ArtRepository.findRecentlyUploadedArts(),
|
||||
allTags,
|
||||
...cachedArts,
|
||||
showcaseArts: await ArtRepository.findShowcaseArtsByTag(filteredTag.id),
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
import { sql } from "~/db/sql";
|
||||
import type { Tables } from "~/db/tables";
|
||||
|
||||
const stm = sql.prepare(/* sql */ `
|
||||
select
|
||||
"id",
|
||||
"name"
|
||||
from
|
||||
"ArtTag"
|
||||
`);
|
||||
|
||||
export function allArtTags(): Array<Pick<Tables["ArtTag"], "id" | "name">> {
|
||||
return stm.all() as any;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user