From dd5a7f9242b26959100d726dfc2ad5d1fa47c55c Mon Sep 17 00:00:00 2001 From: Matt Dunn-Rankin Date: Thu, 23 Apr 2020 14:23:46 -0700 Subject: [PATCH] added pet appearance queries, using in frontend --- setup-mysql-user.sql | 1 + src/OutfitPreview.js | 35 ++++++-- src/server/index.js | 66 ++++++++------- src/server/index.test.js | 173 ++++++++++++++++++++++++++++++++------- src/server/loaders.js | 96 ++++++++++++++++------ 5 files changed, 283 insertions(+), 88 deletions(-) diff --git a/setup-mysql-user.sql b/setup-mysql-user.sql index 6e82ff4..d728611 100644 --- a/setup-mysql-user.sql +++ b/setup-mysql-user.sql @@ -2,6 +2,7 @@ GRANT SELECT ON openneo_impress.items TO impress2020; GRANT SELECT ON openneo_impress.item_translations TO impress2020; GRANT SELECT ON openneo_impress.parents_swf_assets TO impress2020; GRANT SELECT ON openneo_impress.pet_types TO impress2020; +GRANT SELECT ON openneo_impress.pet_states TO impress2020; GRANT SELECT ON openneo_impress.swf_assets TO impress2020; GRANT SELECT ON openneo_impress.zones TO impress2020; GRANT SELECT ON openneo_impress.zone_translations TO impress2020; diff --git a/src/OutfitPreview.js b/src/OutfitPreview.js index a8c44bc..b420aa1 100644 --- a/src/OutfitPreview.js +++ b/src/OutfitPreview.js @@ -9,12 +9,25 @@ function OutfitPreview({ itemIds, speciesId, colorId }) { const { loading, error, data } = useQuery( gql` query($itemIds: [ID!]!, $speciesId: ID!, $colorId: ID!) { + petAppearance(speciesId: $speciesId, colorId: $colorId) { + layers { + id + imageUrl(size: SIZE_600) + zone { + depth + } + } + } + items(ids: $itemIds) { id appearanceOn(speciesId: $speciesId, colorId: $colorId) { layers { id imageUrl(size: SIZE_600) + zone { + depth + } } } } @@ -45,14 +58,22 @@ function OutfitPreview({ itemIds, speciesId, colorId }) { ); } + const allLayers = [ + ...data.petAppearance.layers, + ...data.items.map((i) => i.appearanceOn.layers).flat(), + ]; + allLayers.sort((a, b) => a.zone.depth - b.zone.depth); + return ( - - - + + {allLayers.map((layer) => ( + + + + + + ))} + ); } diff --git a/src/server/index.js b/src/server/index.js index f4ab1a3..10081ad 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -1,7 +1,7 @@ const { gql } = require("apollo-server"); const connectToDb = require("./db"); -const loaders = require("./loaders"); +const buildLoaders = require("./loaders"); const typeDefs = gql` enum LayerImageSize { @@ -14,14 +14,14 @@ const typeDefs = gql` id: ID! name: String! thumbnailUrl: String! - appearanceOn(speciesId: ID!, colorId: ID!): ItemAppearance + appearanceOn(speciesId: ID!, colorId: ID!): Appearance } - type ItemAppearance { - layers: [ItemAppearanceLayer!]! + type Appearance { + layers: [AppearanceLayer!]! } - type ItemAppearanceLayer { + type AppearanceLayer { id: ID! zone: Zone! imageUrl(size: LayerImageSize): String @@ -35,6 +35,7 @@ const typeDefs = gql` type Query { items(ids: [ID!]!): [Item!]! + petAppearance(speciesId: ID!, colorId: ID!): Appearance } `; @@ -44,26 +45,23 @@ const resolvers = { const translation = await itemTranslationLoader.load(item.id); return translation.name; }, - appearanceOn: (item, { speciesId, colorId }) => ({ - itemId: item.id, - speciesId, - colorId, - }), - }, - ItemAppearance: { - layers: async (ia, _, { petTypeLoader, swfAssetLoader }) => { + appearanceOn: async ( + item, + { speciesId, colorId }, + { petTypeLoader, itemSwfAssetLoader } + ) => { const petType = await petTypeLoader.load({ - speciesId: ia.speciesId, - colorId: ia.colorId, + speciesId: speciesId, + colorId: colorId, }); - const swfAssets = await swfAssetLoader.load({ - itemId: ia.itemId, + const swfAssets = await itemSwfAssetLoader.load({ + itemId: item.id, bodyId: petType.bodyId, }); - return swfAssets; + return { layers: swfAssets }; }, }, - ItemAppearanceLayer: { + AppearanceLayer: { zone: async (layer, _, { zoneLoader }) => { const zone = await zoneLoader.load(layer.zoneId); return zone; @@ -82,7 +80,10 @@ const resolvers = { const rid3 = paddedId.slice(6, 9); const time = Number(new Date(layer.convertedAt)); - return `https://impress-asset-images.s3.amazonaws.com/object/${rid1}/${rid2}/${rid3}/${rid}/${sizeNum}x${sizeNum}.png?${time}`; + return ( + `https://impress-asset-images.s3.amazonaws.com/${layer.type}` + + `/${rid1}/${rid2}/${rid3}/${rid}/${sizeNum}x${sizeNum}.png?${time}` + ); }, }, Zone: { @@ -92,10 +93,24 @@ const resolvers = { }, }, Query: { - items: async (_, { ids }, { db }) => { - const items = await loaders.loadItems(db, ids); + items: async (_, { ids }, { itemLoader }) => { + const items = await itemLoader.loadMany(ids); return items; }, + petAppearance: async ( + _, + { speciesId, colorId }, + { petTypeLoader, petStateLoader, petSwfAssetLoader } + ) => { + const petType = await petTypeLoader.load({ + speciesId, + colorId, + }); + const petStates = await petStateLoader.load(petType.id); + const petState = petStates[0]; // TODO + const swfAssets = await petSwfAssetLoader.load(petState.id); + return { layers: swfAssets }; + }, }, }; @@ -105,12 +120,7 @@ const config = { context: async () => { const db = await connectToDb(); return { - db, - itemTranslationLoader: loaders.buildItemTranslationLoader(db), - petTypeLoader: loaders.buildPetTypeLoader(db), - swfAssetLoader: loaders.buildSwfAssetLoader(db), - zoneLoader: loaders.buildZoneLoader(db), - zoneTranslationLoader: loaders.buildZoneTranslationLoader(db), + ...buildLoaders(db), }; }, diff --git a/src/server/index.test.js b/src/server/index.test.js index 7e5f11c..b3526a7 100644 --- a/src/server/index.test.js +++ b/src/server/index.test.js @@ -45,6 +45,11 @@ describe("Item", () => { expect(res.data).toMatchInlineSnapshot(` Object { "items": Array [ + Object { + "id": "38913", + "name": "Zafara Agent Gloves", + "thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_gloves.gif", + }, Object { "id": "38911", "name": "Zafara Agent Hood", @@ -55,11 +60,6 @@ describe("Item", () => { "name": "Zafara Agent Robe", "thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_robe.gif", }, - Object { - "id": "38913", - "name": "Zafara Agent Gloves", - "thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_gloves.gif", - }, ], } `); @@ -76,9 +76,9 @@ describe("Item", () => { Array [ "SELECT * FROM item_translations WHERE item_id IN (?,?,?) AND locale = \\"en\\"", Array [ + "38913", "38911", "38912", - "38913", ], ], ] @@ -117,18 +117,18 @@ describe("Item", () => { "appearanceOn": Object { "layers": Array [ Object { - "id": "30203", - "imageUrl": "https://impress-asset-images.s3.amazonaws.com/object/000/000/006/6829/600x600.png?0", + "id": "37128", + "imageUrl": "https://impress-asset-images.s3.amazonaws.com/object/000/000/014/14856/600x600.png?1587653266000", "zone": Object { - "depth": 3, - "id": "3", - "label": "Background", + "depth": 30, + "id": "26", + "label": "Jacket", }, }, ], }, - "id": "37375", - "name": "Moon and Stars Background", + "id": "38912", + "name": "Zafara Agent Robe", }, Object { "appearanceOn": Object { @@ -151,18 +151,18 @@ describe("Item", () => { "appearanceOn": Object { "layers": Array [ Object { - "id": "37128", - "imageUrl": "https://impress-asset-images.s3.amazonaws.com/object/000/000/014/14856/600x600.png?1587653266000", + "id": "30203", + "imageUrl": "https://impress-asset-images.s3.amazonaws.com/object/000/000/006/6829/600x600.png?0", "zone": Object { - "depth": 30, - "id": "26", - "label": "Jacket", + "depth": 3, + "id": "3", + "label": "Background", }, }, ], }, - "id": "38912", - "name": "Zafara Agent Robe", + "id": "37375", + "name": "Moon and Stars Background", }, ], } @@ -180,9 +180,9 @@ describe("Item", () => { Array [ "SELECT * FROM item_translations WHERE item_id IN (?,?,?) AND locale = \\"en\\"", Array [ - "37375", - "38911", "38912", + "38911", + "37375", ], ], Array [ @@ -203,28 +203,145 @@ describe("Item", () => { rel.swf_asset_id = sa.id WHERE (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0)) OR (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0)) OR (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0))", Array [ - "37375", + "38912", "180", "38911", "180", - "38912", + "37375", "180", ], ], Array [ "SELECT * FROM zones WHERE id IN (?,?,?)", Array [ - "3", - "40", "26", + "40", + "3", ], ], Array [ "SELECT * FROM zone_translations WHERE zone_id IN (?,?,?) AND locale = \\"en\\"", Array [ - "3", - "40", "26", + "40", + "3", + ], + ], + ] + `); + }); +}); + +describe("PetAppearance", () => { + it("loads for species and color", async () => { + const res = await query({ + query: gql` + query { + petAppearance(speciesId: "54", colorId: "75") { + layers { + id + imageUrl(size: SIZE_600) + zone { + depth + } + } + } + } + `, + }); + + expect(res).toHaveNoErrors(); + expect(res.data).toMatchInlineSnapshot(` + Object { + "petAppearance": Object { + "layers": Array [ + Object { + "id": "5995", + "imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7941/600x600.png?0", + "zone": Object { + "depth": 18, + }, + }, + Object { + "id": "5996", + "imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7942/600x600.png?0", + "zone": Object { + "depth": 7, + }, + }, + Object { + "id": "6000", + "imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7946/600x600.png?0", + "zone": Object { + "depth": 40, + }, + }, + Object { + "id": "16467", + "imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/024/24008/600x600.png?0", + "zone": Object { + "depth": 34, + }, + }, + Object { + "id": "19549", + "imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/028/28548/600x600.png?1345719457000", + "zone": Object { + "depth": 37, + }, + }, + Object { + "id": "19550", + "imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/028/28549/600x600.png?0", + "zone": Object { + "depth": 38, + }, + }, + Object { + "id": "163528", + "imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/028/28549/600x600.png?1326455337000", + "zone": Object { + "depth": 38, + }, + }, + ], + }, + } + `); + expect(queryFn.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?)", + Array [ + "54", + "75", + ], + ], + Array [ + "SELECT * FROM pet_states WHERE pet_type_id IN (?)", + Array [ + "2", + ], + ], + Array [ + "SELECT sa.*, rel.parent_id FROM swf_assets sa + INNER JOIN parents_swf_assets rel ON + rel.parent_type = \\"PetState\\" AND + rel.swf_asset_id = sa.id + WHERE rel.parent_id IN (?)", + Array [ + "2", + ], + ], + Array [ + "SELECT * FROM zones WHERE id IN (?,?,?,?,?,?)", + Array [ + "15", + "5", + "37", + "30", + "33", + "34", ], ], ] diff --git a/src/server/loaders.js b/src/server/loaders.js index f34e38f..55c83fe 100644 --- a/src/server/loaders.js +++ b/src/server/loaders.js @@ -1,14 +1,21 @@ const DataLoader = require("dataloader"); -async function loadItems(db, ids) { - const qs = ids.map((_) => "?").join(","); - const [rows, _] = await db.execute( - `SELECT * FROM items WHERE id IN (${qs})`, - ids - ); - const entities = rows.map(normalizeRow); - return entities; -} +const buildItemsLoader = (db) => + new DataLoader(async (ids) => { + const qs = ids.map((_) => "?").join(","); + const [rows, _] = await db.execute( + `SELECT * FROM items WHERE id IN (${qs})`, + ids + ); + + const entities = rows.map(normalizeRow); + const entitiesById = new Map(entities.map((e) => [e.id, e])); + + return ids.map( + (id) => + entitiesById.get(id) || new Error(`could not find item with ID: ${id}`) + ); + }); const buildItemTranslationLoader = (db) => new DataLoader(async (itemIds) => { @@ -52,7 +59,7 @@ const buildPetTypeLoader = (db) => ); }); -const buildSwfAssetLoader = (db) => +const buildItemSwfAssetLoader = (db) => new DataLoader(async (itemAndBodyPairs) => { const conditions = []; const values = []; @@ -82,21 +89,54 @@ const buildSwfAssetLoader = (db) => ); }); +const buildPetSwfAssetLoader = (db) => + new DataLoader(async (petStateIds) => { + const qs = petStateIds.map((_) => "?").join(","); + const [rows, _] = await db.execute( + `SELECT sa.*, rel.parent_id FROM swf_assets sa + INNER JOIN parents_swf_assets rel ON + rel.parent_type = "PetState" AND + rel.swf_asset_id = sa.id + WHERE rel.parent_id IN (${qs})`, + petStateIds + ); + + const entities = rows.map(normalizeRow); + + return petStateIds.map((petStateId) => + entities.filter((e) => e.parentId === petStateId) + ); + }); + +const buildPetStateLoader = (db) => + new DataLoader(async (petTypeIds) => { + const qs = petTypeIds.map((_) => "?").join(","); + const [rows, _] = await db.execute( + `SELECT * FROM pet_states WHERE pet_type_id IN (${qs})`, + petTypeIds + ); + + const entities = rows.map(normalizeRow); + + return petTypeIds.map((petTypeId) => + entities.filter((e) => e.petTypeId === petTypeId) + ); + }); + const buildZoneLoader = (db) => - new DataLoader(async (zoneIds) => { - const qs = zoneIds.map((_) => "?").join(","); + new DataLoader(async (ids) => { + const qs = ids.map((_) => "?").join(","); const [rows, _] = await db.execute( `SELECT * FROM zones WHERE id IN (${qs})`, - zoneIds + ids ); const entities = rows.map(normalizeRow); const entitiesById = new Map(entities.map((e) => [e.id, e])); - return zoneIds.map( - (zoneId) => - entitiesById.get(zoneId) || - new Error(`could not find zone with ID: ${zoneId}`) + return ids.map( + (id) => + entitiesById.get(id) || new Error(`could not find zone with ID: ${id}`) ); }); @@ -130,11 +170,17 @@ function normalizeRow(row) { return normalizedRow; } -module.exports = { - loadItems, - buildItemTranslationLoader, - buildPetTypeLoader, - buildSwfAssetLoader, - buildZoneLoader, - buildZoneTranslationLoader, -}; +function buildLoaders(db) { + return { + itemLoader: buildItemsLoader(db), + itemTranslationLoader: buildItemTranslationLoader(db), + petTypeLoader: buildPetTypeLoader(db), + itemSwfAssetLoader: buildItemSwfAssetLoader(db), + petSwfAssetLoader: buildPetSwfAssetLoader(db), + petStateLoader: buildPetStateLoader(db), + zoneLoader: buildZoneLoader(db), + zoneTranslationLoader: buildZoneTranslationLoader(db), + }; +} + +module.exports = buildLoaders;