Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
80 changes: 77 additions & 3 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ const typeDefs = `#graphql
type Query {
hello: String!
artworks(limit: Int!, offset: Int!, search: String): ArtworkPage!
artworks(limit: Int!, offset: Int!, search: String, artist: String): ArtworkPage!
artists(limit: Int!, offset: Int!, search: String): [String!]!
}
type Mutation {
Expand All @@ -81,13 +82,81 @@ const resolvers = {
Query: {
hello: () => "Hello from Artex backend!",

artists: async (
_: unknown,
{
limit,
offset,
search,
}: {
limit: number;
offset: number;
search?: string | null;
},
) => {
if (limit < 1 || limit > 100 || offset < 0) {
throw new Error("Invalid pagination parameters");
}

const collection = db.collection("met_artworks");
const searchTerm = search?.trim() ?? "";
const escapedSearchTerm = searchTerm.replace(
/[.*+?^${}()|[\]\\]/g,
"\\$&",
);

const artists = await collection
.aggregate<{ name: string }>([
{
$match: {
artist_display: { $type: "string" },
},
},
{
$match: {
artist_display: {
$regex: escapedSearchTerm,
$options: "i",
},
},
},
{
$group: {
_id: "$artist_display",
},
},
{
$match: {
_id: { $regex: "\\S" },
},
},
{ $sort: { _id: 1 } },
{ $skip: offset },
{ $limit: limit },
{
$project: {
_id: 0,
name: "$_id",
},
},
])
.toArray();

return artists.map((artist) => artist.name);
},
artworks: async (
_: unknown,
{
limit,
offset,
search,
}: { limit: number; offset: number; search?: string | null },
artist,
}: {
limit: number;
offset: number;
search?: string | null;
artist?: string | null;
},
) => {
if (limit < 1 || limit > 100 || offset < 0) {
throw new Error("Invalid pagination parameters");
Expand All @@ -99,7 +168,7 @@ const resolvers = {
"\\$&",
);

const filter = searchTerm
const searchFilter = searchTerm
? {
$or: [
{ title: { $regex: escapedSearchTerm, $options: "i" } },
Expand All @@ -108,6 +177,11 @@ const resolvers = {
}
: {};

const filter = {
...searchFilter,
...(artist ? { artist_display: artist } : {}),
};

const [artworks, totalCount] = await Promise.all([
collection
.find(filter)
Expand Down
165 changes: 165 additions & 0 deletions backend/tests/artworks-filtering.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import { describe, expect, it } from "vitest";

async function fetchArtworks(
artist?: string,
search?: string,
limit = 10,
offset = 0,
) {
const response = await fetch("http://127.0.0.1:3001/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
query FilterArtworks(
$artist: String
$search: String
$limit: Int!
$offset: Int!
) {
artworks(
artist: $artist
search: $search
limit: $limit
offset: $offset
) {
totalCount
artworks {
id
title
artist_display
}
}
}
`,
variables: { artist, search, limit, offset },
}),
});

expect(response.ok).toBe(true);

const result = await response.json();

expect(result.errors).toBeUndefined();
expect(result.data?.artworks).toBeDefined();

return result.data.artworks;
}

async function fetchArtists(search?: string, limit = 5, offset = 0) {
const response = await fetch("http://127.0.0.1:3001/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
query ArtistOptions(
$search: String
$limit: Int!
$offset: Int!
) {
artists(search: $search, limit: $limit, offset: $offset)
}
`,
variables: { search, limit, offset },
}),
});

expect(response.ok).toBe(true);

const result = await response.json();

expect(result.errors).toBeUndefined();
expect(result.data?.artists).toBeDefined();

return result.data.artists as string[];
}

describe("Artwork filtering API", () => {
it("returns only artworks by the selected artist", async () => {
const artist = "John Singer Sargent";
const result = await fetchArtworks(artist);

expect(result.totalCount).toBeGreaterThan(0);
expect(result.artworks.length).toBeGreaterThan(0);

for (const artwork of result.artworks) {
expect(artwork.artist_display).toBe(artist);
}
});

it("combines artist filtering with artwork search", async () => {
const result = await fetchArtworks("John Singer Sargent", "woman");

expect(result.totalCount).toBe(1);
expect(result.artworks).toEqual([
{
id: 12072,
title: "Egyptian Woman",
artist_display: "John Singer Sargent",
},
]);
});

it("paginates filtered artworks while keeping the total count", async () => {
const artist = "John Singer Sargent";

const combined = await fetchArtworks(artist, undefined, 2, 0);
const firstPage = await fetchArtworks(artist, undefined, 1, 0);
const secondPage = await fetchArtworks(artist, undefined, 1, 1);

expect(combined.totalCount).toBeGreaterThanOrEqual(2);
expect(firstPage.totalCount).toBe(combined.totalCount);
expect(secondPage.totalCount).toBe(combined.totalCount);

expect(firstPage.artworks).toHaveLength(1);
expect(secondPage.artworks).toHaveLength(1);

expect([...firstPage.artworks, ...secondPage.artworks]).toEqual(
combined.artworks,
);

expect(firstPage.artworks[0].id).not.toBe(secondPage.artworks[0].id);
});

it("returns no artworks when the selected artist does not exist", async () => {
const result = await fetchArtworks("nonexistent-artist-987654");

expect(result.totalCount).toBe(0);
expect(result.artworks).toEqual([]);
});

it("returns a limited list of unique, non-empty artist names", async () => {
const artists = await fetchArtists(undefined, 5, 0);

expect(artists).toHaveLength(5);
expect(new Set(artists).size).toBe(artists.length);

for (const artist of artists) {
expect(artist.trim().length).toBeGreaterThan(0);
}
});

it("searches artist options regardless of letter case", async () => {
const lowercase = await fetchArtists("sargent");
const uppercase = await fetchArtists("SARGENT");

expect(lowercase).toContain("John Singer Sargent");
expect(uppercase).toEqual(lowercase);
});

it("paginates artist options without overlap", async () => {
const combined = await fetchArtists(undefined, 10, 0);
const firstPage = await fetchArtists(undefined, 5, 0);
const secondPage = await fetchArtists(undefined, 5, 5);

expect(firstPage).toHaveLength(5);
expect(secondPage).toHaveLength(5);

expect([...firstPage, ...secondPage]).toEqual(combined);
expect(new Set(combined).size).toBe(10);
});
});