From f621391446a48dd6293559659797171b8a00279d Mon Sep 17 00:00:00 2001 From: Matchu Date: Sun, 8 Nov 2020 00:06:51 -0800 Subject: [PATCH] add itemByName and itemsByName GQL --- src/server/loaders.js | 32 +++++++++++++ src/server/query-tests/Item.test.js | 23 ++++++++++ .../__snapshots__/Item.test.js.snap | 45 +++++++++++++++++++ src/server/types/Item.js | 18 ++++++++ 4 files changed, 118 insertions(+) diff --git a/src/server/loaders.js b/src/server/loaders.js index 4636cc0..210d97a 100644 --- a/src/server/loaders.js +++ b/src/server/loaders.js @@ -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); diff --git a/src/server/query-tests/Item.test.js b/src/server/query-tests/Item.test.js index 2b4aa5f..9a11d15 100644 --- a/src/server/query-tests/Item.test.js +++ b/src/server/query-tests/Item.test.js @@ -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` diff --git a/src/server/query-tests/__snapshots__/Item.test.js.snap b/src/server/query-tests/__snapshots__/Item.test.js.snap index f102ba6..1dbdce7 100644 --- a/src/server/query-tests/__snapshots__/Item.test.js.snap +++ b/src/server/query-tests/__snapshots__/Item.test.js.snap @@ -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 [ diff --git a/src/server/types/Item.js b/src/server/types/Item.js index 01593b2..a15098c 100644 --- a/src/server/types/Item.js +++ b/src/server/types/Item.js @@ -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 };