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
331 changes: 328 additions & 3 deletions backend/src/graphql/resolvers/artworkResolvers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Db } from "mongodb";
import type { Db, Filter } from "mongodb";

type ArtworkSort =
| "TITLE_ASC"
Expand All @@ -9,8 +9,222 @@ type ArtworkSort =
| "YEAR_DESC"
| null;

type ArtworkPeriod =
| "BEFORE_1500"
| "YEARS_1500_1799"
| "CENTURY_19"
| "CENTURY_20_PLUS"
| null;

async function distinctArtworkValues(db: Db, field: "culture" | "period") {
const values = await db.collection("met_artworks").aggregate<{ name: string }>([
{ $match: { [field]: { $type: "string" } } },
{ $project: { name: { $trim: { input: `$${field}` } } } },
{ $match: { name: { $regex: "\\S" } } },
{ $group: { _id: "$name" } },
{ $sort: { _id: 1 } },
{ $project: { _id: 0, name: "$_id" } },
]).toArray();

return values.map(({ name }) => name);
}

type ArtworkFacetFilters = {
search?: string | null;
artists?: string[] | null;
countries?: string[] | null;
cultures?: string[] | null;
artPeriods?: string[] | null;
period?: ArtworkPeriod;
dateFrom?: number | null;
dateTo?: number | null;
};

type ArtworkFacet = "artists" | "countries" | "cultures" | "artPeriods" | "time";

function makeArtworkFilter(filters: ArtworkFacetFilters, omit?: ArtworkFacet): Filter {
const searchTerm = filters.search?.trim() ?? "";
const escapedSearchTerm = searchTerm.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const parts: Filter[] = [];

if (searchTerm) {
parts.push({
$or: [
{ title: { $regex: escapedSearchTerm, $options: "i" } },
{ artist_display: { $regex: escapedSearchTerm, $options: "i" } },
],
});
}

if (omit !== "artists" && filters.artists?.length) {
parts.push({
$expr: {
$in: [
{ $trim: { input: { $ifNull: ["$artist_display", ""] } } },
filters.artists,
],
},
});
}

const addNormalizedFilter = (
facet: Exclude<ArtworkFacet, "artists" | "time">,
field: "country" | "culture" | "period",
values?: string[] | null,
) => {
if (omit === facet || !values?.length) return;
parts.push({
$expr: {
$in: [
{ $trim: { input: { $ifNull: [`$${field}`, ""] } } },
values,
],
},
});
};

addNormalizedFilter("countries", "country", filters.countries);
addNormalizedFilter("cultures", "culture", filters.cultures);
addNormalizedFilter("artPeriods", "period", filters.artPeriods);

if (omit !== "time" && filters.period) {
switch (filters.period) {
case "BEFORE_1500":
parts.push({ date_start: { $lte: 1499 } });
break;
case "YEARS_1500_1799":
parts.push({ $expr: { $and: [
{ $isNumber: "$date_start" },
{ $lte: ["$date_start", 1799] },
{ $gte: [{ $cond: [{ $isNumber: "$date_end" }, "$date_end", "$date_start"] }, 1500] },
] } });
break;
case "CENTURY_19":
parts.push({ $expr: { $and: [
{ $isNumber: "$date_start" },
{ $lte: ["$date_start", 1899] },
{ $gte: [{ $cond: [{ $isNumber: "$date_end" }, "$date_end", "$date_start"] }, 1800] },
] } });
break;
case "CENTURY_20_PLUS":
parts.push({ $expr: { $and: [
{ $isNumber: "$date_start" },
{ $gte: [{ $cond: [{ $isNumber: "$date_end" }, "$date_end", "$date_start"] }, 1900] },
] } });
break;
}
}

if (omit !== "time" && (typeof filters.dateFrom === "number" || typeof filters.dateTo === "number")) {
parts.push({ $expr: { $and: [
{ $isNumber: "$date_start" },
...(typeof filters.dateTo === "number" ? [{ $lte: ["$date_start", filters.dateTo] }] : []),
...(typeof filters.dateFrom === "number"
? [{ $gte: [{ $cond: [{ $isNumber: "$date_end" }, "$date_end", "$date_start"] }, filters.dateFrom] }]
: []),
] } });
}

return parts.length ? { $and: parts } : {};
}

async function countFacetOptions(
db: Db,
field: "artist_display" | "country" | "culture" | "period",
filter: Filter,
search = "",
) {
const escapedSearch = search.trim().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const rows = await db.collection("met_artworks").aggregate<{ value: string; count: number }>([
{ $match: filter },
{ $match: { [field]: { $type: "string" } } },
{ $project: { value: { $trim: { input: `$${field}` } } } },
{ $match: {
value: { $regex: escapedSearch || "\\S", $options: "i" },
} },
{ $group: { _id: "$value", count: { $sum: 1 } } },
{ $sort: { _id: 1 } },
{ $project: { _id: 0, value: "$_id", count: 1 } },
]).toArray();

return rows.filter(({ value }) => value.trim() !== "");
}

export function createArtworkResolvers(db: Db) {
return {
artworkFilterOptions: async (
_: unknown,
{
search,
artistSearch,
artists,
countries,
cultures,
artPeriods,
period,
dateFrom,
dateTo,
}: ArtworkFacetFilters & { artistSearch?: string | null },
) => {
const filters = { search, artists, countries, cultures, artPeriods, period, dateFrom, dateTo };
const [artistOptions, countryOptions, cultureOptions, artPeriodOptions] = await Promise.all([
countFacetOptions(db, "artist_display", makeArtworkFilter(filters, "artists"), artistSearch ?? ""),
countFacetOptions(db, "country", makeArtworkFilter(filters, "countries")),
countFacetOptions(db, "culture", makeArtworkFilter(filters, "cultures")),
countFacetOptions(db, "period", makeArtworkFilter(filters, "artPeriods")),
]);

const timeFilter = makeArtworkFilter(filters, "time");
const periodBuckets: [string, Filter][] = [
["BEFORE_1500", { date_start: { $lte: 1499 } }],
["YEARS_1500_1799", { $expr: { $and: [
{ $isNumber: "$date_start" },
{ $lte: ["$date_start", 1799] },
{ $gte: [{ $cond: [{ $isNumber: "$date_end" }, "$date_end", "$date_start"] }, 1500] },
] } }],
["CENTURY_19", { $expr: { $and: [
{ $isNumber: "$date_start" },
{ $lte: ["$date_start", 1899] },
{ $gte: [{ $cond: [{ $isNumber: "$date_end" }, "$date_end", "$date_start"] }, 1800] },
] } }],
["CENTURY_20_PLUS", { $expr: { $and: [
{ $isNumber: "$date_start" },
{ $gte: [{ $cond: [{ $isNumber: "$date_end" }, "$date_end", "$date_start"] }, 1900] },
] } }],
];
const periodOptions = await Promise.all(periodBuckets.map(async ([value, periodFilter]) => ({
value,
count: await db.collection("met_artworks").countDocuments(
{ $and: [timeFilter, periodFilter] },
{ collation: { locale: "en", strength: 1 } },
),
})));

return {
artists: artistOptions,
countries: countryOptions,
cultures: cultureOptions,
artPeriods: artPeriodOptions,
periods: periodOptions.filter((option) => option.count > 0),
};
},

cultures: () => distinctArtworkValues(db, "culture"),
artPeriods: () => distinctArtworkValues(db, "period"),

countries: async () => {
const countries = await db.collection("met_artworks").aggregate<{ name: string }>([
{ $match: { country: { $type: "string" } } },
{ $project: { name: { $trim: { input: "$country" } } } },
{ $match: { name: { $regex: "\\S" } } },
{ $group: { _id: "$name" } },
{ $sort: { _id: 1 } },
{ $project: { _id: 0, name: "$_id" } },
]).toArray();

return countries.map(({ name }) => name);
},

artists: async (
_: unknown,
{
Expand Down Expand Up @@ -88,13 +302,33 @@ export function createArtworkResolvers(db: Db) {
offset,
search,
artist,
artists,
country,
countries,
culture,
cultures,
artPeriod,
artPeriods,
sort,
period,
dateFrom,
dateTo,
}: {
limit: number;
offset: number;
search?: string | null;
artist?: string | null;
artists?: string[] | null;
country?: string | null;
countries?: string[] | null;
culture?: string | null;
cultures?: string[] | null;
artPeriod?: string | null;
artPeriods?: string[] | null;
sort?: ArtworkSort;
period?: ArtworkPeriod;
dateFrom?: number | null;
dateTo?: number | null;
},
) => {
if (limit < 1 || limit > 100 || offset < 0) {
Expand Down Expand Up @@ -128,9 +362,100 @@ export function createArtworkResolvers(db: Db) {
}
: {};

const periodFilter = (() => {
switch (period) {
case "BEFORE_1500":
return { date_start: { $lte: 1499 } };
case "YEARS_1500_1799":
return { $expr: { $and: [
{ $isNumber: "$date_start" },
{ $lte: ["$date_start", 1799] },
{ $gte: [{ $cond: [{ $isNumber: "$date_end" }, "$date_end", "$date_start"] }, 1500] },
] } };
case "CENTURY_19":
return { $expr: { $and: [
{ $isNumber: "$date_start" },
{ $lte: ["$date_start", 1899] },
{ $gte: [{ $cond: [{ $isNumber: "$date_end" }, "$date_end", "$date_start"] }, 1800] },
] } };
case "CENTURY_20_PLUS":
return { $expr: { $and: [
{ $isNumber: "$date_start" },
{ $gte: [{ $cond: [{ $isNumber: "$date_end" }, "$date_end", "$date_start"] }, 1900] },
] } };
default:
return {};
}
})();

const dateRangeFilter = (() => {
const conditions = [
{ $isNumber: "$date_start" },
...(typeof dateTo === "number" ? [{ $lte: ["$date_start", dateTo] }] : []),
...(typeof dateFrom === "number"
? [{
$gte: [
{
$cond: [
{ $isNumber: "$date_end" },
"$date_end",
"$date_start",
],
},
dateFrom,
],
}]
: []),
];

return conditions.length > 1 ? { $expr: { $and: conditions } } : {};
})();

const filter = {
...searchFilter,
...(artist ? { artist_display: artist } : {}),
$and: [
searchFilter,
...(artist ? [{ artist_display: artist }] : []),
...(artists?.length ? [{
$expr: {
$in: [
{ $trim: { input: { $ifNull: ["$artist_display", ""] } } },
artists,
],
},
}] : []),
...(country || countries?.length
? [{
$expr: {
$in: [
{ $trim: { input: { $ifNull: ["$country", ""] } } },
countries?.length ? countries : [country],
],
},
}]
: []),
...(culture || cultures?.length
? [{
$expr: {
$in: [
{ $trim: { input: { $ifNull: ["$culture", ""] } } },
cultures?.length ? cultures : [culture],
],
},
}]
: []),
...(artPeriod || artPeriods?.length
? [{
$expr: {
$in: [
{ $trim: { input: { $ifNull: ["$period", ""] } } },
artPeriods?.length ? artPeriods : [artPeriod],
],
},
}]
: []),
periodFilter,
dateRangeFilter,
],
};

let sortOrder: Record<string, 1 | -1>;
Expand Down
Loading