add itemByName and itemsByName GQL

This commit is contained in:
Emi Matchu 2020-11-08 00:06:51 -08:00
parent d701f51c15
commit f621391446
4 changed files with 118 additions and 0 deletions

View file

@ -147,6 +147,37 @@ const buildItemTranslationLoader = (db) =>
);
});
const buildItemByNameLoader = (db, loaders) =>
new DataLoader(async (names) => {
const qs = names.map((_) => "?").join(", ");
const [rows, _] = await db.execute(
{
// NOTE: In our MySQL schema, this is a case-insensitive exact search.
sql: `SELECT items.*, item_translations.* FROM item_translations
INNER JOIN items ON items.id = item_translations.item_id
WHERE name IN (${qs}) AND locale = "en"`,
nestTables: true,
},
names
);
const entities = rows.map((row) => {
const item = normalizeRow(row.items);
const itemTranslation = normalizeRow(row.item_translations);
loaders.itemLoader.prime(item.id, item);
loaders.itemTranslationLoader.prime(item.id, itemTranslation);
return { item, itemTranslation };
});
return names.map((name) =>
entities.find(
(e) =>
e.itemTranslation.name.trim().toLowerCase() ===
name.trim().toLowerCase()
)
);
});
const buildItemSearchLoader = (db, loaders) =>
new DataLoader(async (queries) => {
// This isn't actually optimized as a batch query, we're just using a
@ -759,6 +790,7 @@ function buildLoaders(db) {
loaders.colorTranslationLoader = buildColorTranslationLoader(db);
loaders.itemLoader = buildItemLoader(db);
loaders.itemTranslationLoader = buildItemTranslationLoader(db);
loaders.itemByNameLoader = buildItemByNameLoader(db, loaders);
loaders.itemSearchLoader = buildItemSearchLoader(db, loaders);
loaders.itemSearchToFitLoader = buildItemSearchToFitLoader(db, loaders);
loaders.itemsThatNeedModelsLoader = buildItemsThatNeedModelsLoader(db);

View file

@ -104,6 +104,29 @@ describe("Item", () => {
`);
});
it("loads items by name", async () => {
const res = await query({
query: gql`
query {
itemByName(name: "Moon and Stars Background") {
id
name
thumbnailUrl
}
itemsByName(names: ["Zafara Agent Robe", "pile of dung"]) {
id
name
thumbnailUrl
}
}
`,
});
expect(res).toHaveNoErrors();
expect(res.data).toMatchSnapshot("data");
expect(getDbCalls()).toMatchSnapshot("db");
});
it("loads appearance data", async () => {
const res = await query({
query: gql`

View file

@ -724,6 +724,51 @@ Object {
}
`;
exports[`Item loads items by name: data 1`] = `
Object {
"itemByName": Object {
"id": "37375",
"name": "Moon and Stars Background",
"thumbnailUrl": "http://images.neopets.com/items/bg_moonstars.gif",
},
"itemsByName": Array [
Object {
"id": "38912",
"name": "Zafara Agent Robe",
"thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_robe.gif",
},
Object {
"id": "18579",
"name": "Pile of Dung",
"thumbnailUrl": "http://images.neopets.com/items/med_booby_5.gif",
},
],
}
`;
exports[`Item loads items by name: db 1`] = `
Array [
Array [
Object {
"nestTables": true,
"sql": "SELECT items.*, item_translations.* FROM item_translations
INNER JOIN items ON items.id = item_translations.item_id
WHERE name IN (?, ?, ?) AND locale = \\"en\\"",
"values": Array [
"Moon and Stars Background",
"Zafara Agent Robe",
"pile of dung",
],
},
Array [
"Moon and Stars Background",
"Zafara Agent Robe",
"pile of dung",
],
],
]
`;
exports[`Item loads items that need models 1`] = `
Object {
"babyItems": Array [

View file

@ -78,6 +78,16 @@ const typeDefs = gql`
extend type Query {
item(id: ID!): Item
items(ids: [ID!]!): [Item!]!
# Find items by name. Exact match, except for some tweaks, like
# case-insensitivity and trimming extra whitespace. Null if not found.
#
# NOTE: These aren't used in DTI at time of writing; they're a courtesy API
# for the /r/Neopets Discord bot's outfit preview command!
itemByName(name: String!): Item
itemsByName(names: [String!]!): [Item]!
# Search for items with fuzzy matching.
itemSearch(query: String!): ItemSearchResult!
itemSearchToFit(
query: String!
@ -271,6 +281,14 @@ const resolvers = {
items: (_, { ids }) => {
return ids.map((id) => ({ id }));
},
itemByName: async (_, { name }, { itemByNameLoader }) => {
const { item } = await itemByNameLoader.load(name);
return item ? { id: item.id } : null;
},
itemsByName: async (_, { names }, { itemByNameLoader }) => {
const items = await itemByNameLoader.loadMany(names);
return items.map(({ item }) => (item ? { id: item.id } : null));
},
itemSearch: async (_, { query }, { itemSearchLoader }) => {
const items = await itemSearchLoader.load(query.trim());
return { query, items };