From 1b59b9631b868d43ca93f7a9f5d48ddb57268bc4 Mon Sep 17 00:00:00 2001 From: Matchu Date: Sun, 20 Sep 2020 22:21:23 -0700 Subject: [PATCH] GQL for canonical appearance for body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gonna use this for item page! I walked back my supported species idea 😅 --- src/app/ItemPage.js | 33 +- src/server/loaders.js | 65 ++- src/server/query-tests/Item.test.js | 126 +++- .../__snapshots__/Item.test.js.snap | 551 ++++++++++++------ src/server/types/Item.js | 33 +- src/server/types/PetAppearance.js | 31 + 6 files changed, 613 insertions(+), 226 deletions(-) diff --git a/src/app/ItemPage.js b/src/app/ItemPage.js index c8b64eb..13305ad 100644 --- a/src/app/ItemPage.js +++ b/src/app/ItemPage.js @@ -33,6 +33,10 @@ import { NpBadge, } from "./components/ItemCard"; import { Delay, Heading1, usePageTitle } from "./util"; +import { + itemAppearanceFragment, + petAppearanceFragment, +} from "./components/useOutfitAppearance"; import OutfitPreview from "./components/OutfitPreview"; import SpeciesColorPicker from "./components/SpeciesColorPicker"; @@ -439,16 +443,41 @@ function ItemPageOutfitPreview({ itemId }) { [] ); const [petState, setPetState] = React.useState({ + // Start by looking up Acara appearance data. speciesId: "1", colorId: "8", pose: idealPose, }); + // Start by loading the "canonical" pet and item appearance for the outfit + // preview. We'll use this to initialize both the preview and the picker. + const { loading, error, data } = useQuery(gql` + query ItemPageOutfitPreview($itemId: ID!) { + item(id: $itemId) { + id + canonicalAppearance { + id + ...ItemAppearanceFragment + body { + id + canonicalAppearance { + id + ...PetAppearanceFragment + } + } + } + } + } + + ${itemAppearanceFragment} + ${petAppearanceFragment} + `); + // To check whether the item is compatible with this pet, query for the // appearance, but only against the cache. That way, we don't send a // redundant network request just for this (the OutfitPreview component will // handle it!), but we'll get an update once it arrives in the cache. - const { data } = useQuery( + const { cachedData } = useQuery( gql` query ItemPageOutfitPreview_CacheOnly( $itemId: ID! @@ -477,7 +506,7 @@ function ItemPageOutfitPreview({ itemId }) { // If the layers are null-y, then we're still loading. Otherwise, if the // layers are an empty array, then we're incomaptible. Or, if they're a // non-empty array, then we're compatible! - const layers = data?.item?.appearanceOn?.layers; + const layers = cachedData?.item?.appearanceOn?.layers; const isIncompatible = Array.isArray(layers) && layers.length === 0; const borderColor = useColorModeValue("green.700", "green.400"); diff --git a/src/server/loaders.js b/src/server/loaders.js index 679e39e..5d713e0 100644 --- a/src/server/loaders.js +++ b/src/server/loaders.js @@ -294,11 +294,13 @@ const buildItemsThatNeedModelsLoader = (db) => return [lastResult]; }); -const buildItemSpeciesWithAppearanceDataLoader = (db) => +const buildItemBodiesWithAppearanceDataLoader = (db) => new DataLoader(async (itemIds) => { const qs = itemIds.map((_) => "?").join(","); const [rows, _] = await db.execute( - `SELECT DISTINCT pet_types.species_id AS id, items.id AS item_id + // TODO: I'm not sure this ORDER BY clause will reliably get standard + // bodies to the top, it seems like it depends how DISTINCT works? + `SELECT pet_types.body_id, pet_types.species_id, items.id AS item_id FROM items INNER JOIN parents_swf_assets ON items.id = parents_swf_assets.parent_id AND @@ -307,8 +309,13 @@ const buildItemSpeciesWithAppearanceDataLoader = (db) => parents_swf_assets.swf_asset_id = swf_assets.id INNER JOIN pet_types ON pet_types.body_id = swf_assets.body_id OR swf_assets.body_id = 0 - WHERE items.id = ${qs} - ORDER BY id`, + INNER JOIN colors ON + pet_types.color_id = colors.id + WHERE items.id IN (${qs}) + GROUP BY pet_types.body_id + ORDER BY + pet_types.species_id, + colors.standard DESC`, itemIds ); @@ -503,6 +510,50 @@ const buildPetStatesForPetTypeLoader = (db, loaders) => ); }); +/** Given a bodyId, loads the canonical PetState to show as an example. */ +const buildCanonicalPetStateForBodyLoader = (db, loaders) => + new DataLoader(async (bodyIds) => { + // I don't know how to do this query in bulk, so we'll just do it in + // parallel! + return await Promise.all( + bodyIds.map(async (bodyId) => { + // Randomly-ish choose which gender presentation to prefer, based on + // body ID. This makes the outcome stable, which is nice for caching + // and testing and just generally not being surprised, but sitll + // creates an even distribution. + const gender = bodyId % 2 === 0 ? "masc" : "fem"; + + const [rows, _] = await db.execute( + { + sql: ` + SELECT pet_states.*, pet_types.* FROM pet_states + INNER JOIN pet_types ON pet_types.id = pet_states.pet_type_id + WHERE pet_types.body_id = ? + ORDER BY + pet_types.color_id = 8 DESC, -- Prefer Blue + pet_states.mood_id = 1 DESC, -- Prefer Happy + pet_states.female = ? DESC, -- Prefer given gender + pet_states.id DESC, -- Prefer recent models (like in the app) + pet_states.glitched ASC -- Prefer not glitched (like in the app) + LIMIT 1`, + nestTables: true, + }, + [bodyId, gender === "fem"] + ); + const petState = normalizeRow(rows[0].pet_states); + const petType = normalizeRow(rows[0].pet_types); + if (!petState || !petType) { + return null; + } + + loaders.petStateLoader.prime(petState.id, petState); + loaders.petTypeLoader.prime(petType.id, petType); + + return petState; + }) + ); + }); + const buildUserLoader = (db) => new DataLoader(async (ids) => { const qs = ids.map((_) => "?").join(","); @@ -617,7 +668,7 @@ function buildLoaders(db) { loaders.itemSearchLoader = buildItemSearchLoader(db, loaders); loaders.itemSearchToFitLoader = buildItemSearchToFitLoader(db, loaders); loaders.itemsThatNeedModelsLoader = buildItemsThatNeedModelsLoader(db); - loaders.itemSpeciesWithAppearanceDataLoader = buildItemSpeciesWithAppearanceDataLoader( + loaders.itemBodiesWithAppearanceDataLoader = buildItemBodiesWithAppearanceDataLoader( db ); loaders.petTypeLoader = buildPetTypeLoader(db); @@ -637,6 +688,10 @@ function buildLoaders(db) { db, loaders ); + loaders.canonicalPetStateForBodyLoader = buildCanonicalPetStateForBodyLoader( + db, + loaders + ); loaders.speciesLoader = buildSpeciesLoader(db); loaders.speciesTranslationLoader = buildSpeciesTranslationLoader(db); loaders.userLoader = buildUserLoader(db); diff --git a/src/server/query-tests/Item.test.js b/src/server/query-tests/Item.test.js index b8df9cf..8ef513b 100644 --- a/src/server/query-tests/Item.test.js +++ b/src/server/query-tests/Item.test.js @@ -431,15 +431,26 @@ describe("Item", () => { expect(getDbCalls()).toMatchSnapshot(); }); - it("loads species with appearance data for single-species item", async () => { + it("loads canonical appearance for single-species item", async () => { const res = await query({ query: gql` query { item( id: "38911" # Zafara Agent Hood ) { - speciesWithAppearanceDataForThisItem { - name + canonicalAppearance { + id + layers { + id + } + body { + species { + name + } + canonicalAppearance { + id + } + } } } } @@ -447,22 +458,36 @@ describe("Item", () => { }); expect(res).toHaveNoErrors(); - expect(res.data.item.speciesWithAppearanceDataForThisItem).toHaveLength(1); - expect(res.data.item.speciesWithAppearanceDataForThisItem[0].name).toEqual( - "Zafara" + const body = res.data.item.canonicalAppearance.body; + expect(body.species.name).toEqual("Zafara"); + expect(res.data.item.canonicalAppearance.layers).toMatchSnapshot( + "item layers" ); - expect(getDbCalls()).toMatchSnapshot(); + expect(body.canonicalAppearance).toBeTruthy(); + expect(body.canonicalAppearance).toMatchSnapshot("pet layers"); + expect(getDbCalls()).toMatchSnapshot("db"); }); - it("loads species with appearance data for all-species item", async () => { + it("loads canonical appearance for all-species item", async () => { const res = await query({ query: gql` query { item( id: "74967" # 17th Birthday Party Hat ) { - speciesWithAppearanceDataForThisItem { - name + canonicalAppearance { + id + layers { + id + } + body { + species { + name + } + canonicalAppearance { + id + } + } } } } @@ -470,19 +495,80 @@ describe("Item", () => { }); expect(res).toHaveNoErrors(); - expect(res.data.item.speciesWithAppearanceDataForThisItem).toHaveLength(55); - expect(getDbCalls()).toMatchSnapshot(); + const body = res.data.item.canonicalAppearance.body; + expect(body.species.name).toEqual("Acara"); + expect(res.data.item.canonicalAppearance.layers).toMatchSnapshot( + "item layers" + ); + expect(body.canonicalAppearance).toBeTruthy(); + expect(body.canonicalAppearance).toMatchSnapshot("pet layers"); + expect(getDbCalls()).toMatchSnapshot("db"); }); - it("loads species with appearance data for bodyId=0 item", async () => { + it("loads canonical appearance for all-species Maraquan item", async () => { + const res = await query({ + query: gql` + query { + item( + id: "77530" # Maraquan Sea Blue Gown + ) { + canonicalAppearance { + id + layers { + id + } + body { + canonicalAppearance { + color { + name + } + species { + name + } + layers { + id + } + } + } + } + } + } + `, + }); + + expect(res).toHaveNoErrors(); + const body = res.data.item.canonicalAppearance.body; + expect(res.data.item.canonicalAppearance).toBeTruthy(); + expect(res.data.item.canonicalAppearance.layers).toMatchSnapshot( + "item layers" + ); + expect(body.canonicalAppearance).toBeTruthy(); + expect(body.canonicalAppearance.species.name).toEqual("Acara"); + expect(body.canonicalAppearance.color.name).toEqual("Maraquan"); + expect(body.canonicalAppearance.layers).toMatchSnapshot("pet layers"); + expect(getDbCalls()).toMatchSnapshot("db"); + }); + + it("loads canonical appearance for bodyId=0 item", async () => { const res = await query({ query: gql` query { item( id: "37375" # Moon and Stars Background ) { - speciesWithAppearanceDataForThisItem { - name + canonicalAppearance { + id + layers { + id + } + body { + species { + name + } + canonicalAppearance { + id + } + } } } } @@ -490,7 +576,13 @@ describe("Item", () => { }); expect(res).toHaveNoErrors(); - expect(res.data.item.speciesWithAppearanceDataForThisItem).toHaveLength(55); - expect(getDbCalls()).toMatchSnapshot(); + const body = res.data.item.canonicalAppearance.body; + expect(body.species.name).toEqual("Acara"); + expect(res.data.item.canonicalAppearance.layers).toMatchSnapshot( + "item layers" + ); + expect(body.canonicalAppearance).toBeTruthy(); + expect(body.canonicalAppearance).toMatchSnapshot("pet layers"); + expect(getDbCalls()).toMatchSnapshot("db"); }); }); diff --git a/src/server/query-tests/__snapshots__/Item.test.js.snap b/src/server/query-tests/__snapshots__/Item.test.js.snap index 7e4817b..5b343f1 100644 --- a/src/server/query-tests/__snapshots__/Item.test.js.snap +++ b/src/server/query-tests/__snapshots__/Item.test.js.snap @@ -81,6 +81,365 @@ Object { } `; +exports[`Item loads canonical appearance for all-species Maraquan item: db 1`] = ` +Array [ + Array [ + "SELECT pet_types.body_id, pet_types.species_id, items.id AS item_id + FROM items + INNER JOIN parents_swf_assets ON + items.id = parents_swf_assets.parent_id AND + parents_swf_assets.parent_type = \\"Item\\" + INNER JOIN swf_assets ON + parents_swf_assets.swf_asset_id = swf_assets.id + INNER JOIN pet_types ON + pet_types.body_id = swf_assets.body_id OR swf_assets.body_id = 0 + INNER JOIN colors ON + pet_types.color_id = colors.id + WHERE items.id IN (?) + GROUP BY pet_types.body_id + ORDER BY + pet_types.species_id, + colors.standard DESC", + Array [ + "77530", + ], + ], + Array [ + "SELECT sa.*, rel.parent_id FROM swf_assets sa + INNER JOIN parents_swf_assets rel ON + rel.parent_type = \\"Item\\" AND + rel.swf_asset_id = sa.id + WHERE (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0))", + Array [ + "77530", + "112", + ], + ], + Array [ + Object { + "nestTables": true, + "sql": " + SELECT pet_states.*, pet_types.* FROM pet_states + INNER JOIN pet_types ON pet_types.id = pet_states.pet_type_id + WHERE pet_types.body_id = ? + ORDER BY + pet_types.color_id = 8 DESC, -- Prefer Blue + pet_states.mood_id = 1 DESC, -- Prefer Happy + pet_states.female = ? DESC, -- Prefer given gender + pet_states.id DESC, -- Prefer recent models (like in the app) + pet_states.glitched ASC -- Prefer not glitched (like in the app) + LIMIT 1", + "values": Array [ + "112", + false, + ], + }, + Array [ + "112", + false, + ], + ], + 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 [ + "5233", + ], + ], + Array [ + "SELECT * FROM color_translations + WHERE color_id IN (?) AND locale = \\"en\\"", + Array [ + "44", + ], + ], + Array [ + "SELECT * FROM species_translations + WHERE species_id IN (?) AND locale = \\"en\\"", + Array [ + "1", + ], + ], +] +`; + +exports[`Item loads canonical appearance for all-species Maraquan item: item layers 1`] = ` +Array [ + Object { + "id": "442864", + }, +] +`; + +exports[`Item loads canonical appearance for all-species Maraquan item: pet layers 1`] = ` +Array [ + Object { + "id": "2652", + }, + Object { + "id": "2653", + }, + Object { + "id": "2654", + }, + Object { + "id": "2656", + }, + Object { + "id": "2663", + }, +] +`; + +exports[`Item loads canonical appearance for all-species item: db 1`] = ` +Array [ + Array [ + "SELECT pet_types.body_id, pet_types.species_id, items.id AS item_id + FROM items + INNER JOIN parents_swf_assets ON + items.id = parents_swf_assets.parent_id AND + parents_swf_assets.parent_type = \\"Item\\" + INNER JOIN swf_assets ON + parents_swf_assets.swf_asset_id = swf_assets.id + INNER JOIN pet_types ON + pet_types.body_id = swf_assets.body_id OR swf_assets.body_id = 0 + INNER JOIN colors ON + pet_types.color_id = colors.id + WHERE items.id IN (?) + GROUP BY pet_types.body_id + ORDER BY + pet_types.species_id, + colors.standard DESC", + Array [ + "74967", + ], + ], + Array [ + "SELECT sa.*, rel.parent_id FROM swf_assets sa + INNER JOIN parents_swf_assets rel ON + rel.parent_type = \\"Item\\" AND + rel.swf_asset_id = sa.id + WHERE (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0))", + Array [ + "74967", + "93", + ], + ], + Array [ + "SELECT * FROM species_translations + WHERE species_id IN (?) AND locale = \\"en\\"", + Array [ + "1", + ], + ], + Array [ + Object { + "nestTables": true, + "sql": " + SELECT pet_states.*, pet_types.* FROM pet_states + INNER JOIN pet_types ON pet_types.id = pet_states.pet_type_id + WHERE pet_types.body_id = ? + ORDER BY + pet_types.color_id = 8 DESC, -- Prefer Blue + pet_states.mood_id = 1 DESC, -- Prefer Happy + pet_states.female = ? DESC, -- Prefer given gender + pet_states.id DESC, -- Prefer recent models (like in the app) + pet_states.glitched ASC -- Prefer not glitched (like in the app) + LIMIT 1", + "values": Array [ + "93", + true, + ], + }, + Array [ + "93", + true, + ], + ], +] +`; + +exports[`Item loads canonical appearance for all-species item: item layers 1`] = ` +Array [ + Object { + "id": "395679", + }, +] +`; + +exports[`Item loads canonical appearance for all-species item: pet layers 1`] = ` +Object { + "id": "5161", +} +`; + +exports[`Item loads canonical appearance for bodyId=0 item: db 1`] = ` +Array [ + Array [ + "SELECT pet_types.body_id, pet_types.species_id, items.id AS item_id + FROM items + INNER JOIN parents_swf_assets ON + items.id = parents_swf_assets.parent_id AND + parents_swf_assets.parent_type = \\"Item\\" + INNER JOIN swf_assets ON + parents_swf_assets.swf_asset_id = swf_assets.id + INNER JOIN pet_types ON + pet_types.body_id = swf_assets.body_id OR swf_assets.body_id = 0 + INNER JOIN colors ON + pet_types.color_id = colors.id + WHERE items.id IN (?) + GROUP BY pet_types.body_id + ORDER BY + pet_types.species_id, + colors.standard DESC", + Array [ + "37375", + ], + ], + Array [ + "SELECT sa.*, rel.parent_id FROM swf_assets sa + INNER JOIN parents_swf_assets rel ON + rel.parent_type = \\"Item\\" AND + rel.swf_asset_id = sa.id + WHERE (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0))", + Array [ + "37375", + "93", + ], + ], + Array [ + "SELECT * FROM species_translations + WHERE species_id IN (?) AND locale = \\"en\\"", + Array [ + "1", + ], + ], + Array [ + Object { + "nestTables": true, + "sql": " + SELECT pet_states.*, pet_types.* FROM pet_states + INNER JOIN pet_types ON pet_types.id = pet_states.pet_type_id + WHERE pet_types.body_id = ? + ORDER BY + pet_types.color_id = 8 DESC, -- Prefer Blue + pet_states.mood_id = 1 DESC, -- Prefer Happy + pet_states.female = ? DESC, -- Prefer given gender + pet_states.id DESC, -- Prefer recent models (like in the app) + pet_states.glitched ASC -- Prefer not glitched (like in the app) + LIMIT 1", + "values": Array [ + "93", + true, + ], + }, + Array [ + "93", + true, + ], + ], +] +`; + +exports[`Item loads canonical appearance for bodyId=0 item: item layers 1`] = ` +Array [ + Object { + "id": "30203", + }, +] +`; + +exports[`Item loads canonical appearance for bodyId=0 item: pet layers 1`] = ` +Object { + "id": "5161", +} +`; + +exports[`Item loads canonical appearance for single-species item: db 1`] = ` +Array [ + Array [ + "SELECT pet_types.body_id, pet_types.species_id, items.id AS item_id + FROM items + INNER JOIN parents_swf_assets ON + items.id = parents_swf_assets.parent_id AND + parents_swf_assets.parent_type = \\"Item\\" + INNER JOIN swf_assets ON + parents_swf_assets.swf_asset_id = swf_assets.id + INNER JOIN pet_types ON + pet_types.body_id = swf_assets.body_id OR swf_assets.body_id = 0 + INNER JOIN colors ON + pet_types.color_id = colors.id + WHERE items.id IN (?) + GROUP BY pet_types.body_id + ORDER BY + pet_types.species_id, + colors.standard DESC", + Array [ + "38911", + ], + ], + Array [ + "SELECT sa.*, rel.parent_id FROM swf_assets sa + INNER JOIN parents_swf_assets rel ON + rel.parent_type = \\"Item\\" AND + rel.swf_asset_id = sa.id + WHERE (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0))", + Array [ + "38911", + "180", + ], + ], + Array [ + "SELECT * FROM species_translations + WHERE species_id IN (?) AND locale = \\"en\\"", + Array [ + "54", + ], + ], + Array [ + Object { + "nestTables": true, + "sql": " + SELECT pet_states.*, pet_types.* FROM pet_states + INNER JOIN pet_types ON pet_types.id = pet_states.pet_type_id + WHERE pet_types.body_id = ? + ORDER BY + pet_types.color_id = 8 DESC, -- Prefer Blue + pet_states.mood_id = 1 DESC, -- Prefer Happy + pet_states.female = ? DESC, -- Prefer given gender + pet_states.id DESC, -- Prefer recent models (like in the app) + pet_states.glitched ASC -- Prefer not glitched (like in the app) + LIMIT 1", + "values": Array [ + "180", + false, + ], + }, + Array [ + "180", + false, + ], + ], +] +`; + +exports[`Item loads canonical appearance for single-species item: item layers 1`] = ` +Array [ + Object { + "id": "37129", + }, +] +`; + +exports[`Item loads canonical appearance for single-species item: pet layers 1`] = ` +Object { + "id": "17861", +} +`; + exports[`Item loads items that need models 1`] = ` Object { "babyItems": Array [ @@ -11998,198 +12357,6 @@ Object { } `; -exports[`Item loads species with appearance data for all-species item 1`] = ` -Array [ - Array [ - "SELECT DISTINCT pet_types.species_id AS id, items.id AS item_id - FROM items - INNER JOIN parents_swf_assets ON - items.id = parents_swf_assets.parent_id AND - parents_swf_assets.parent_type = \\"Item\\" - INNER JOIN swf_assets ON - parents_swf_assets.swf_asset_id = swf_assets.id - INNER JOIN pet_types ON - pet_types.body_id = swf_assets.body_id OR swf_assets.body_id = 0 - WHERE items.id = ? - ORDER BY id", - Array [ - "74967", - ], - ], - Array [ - "SELECT * FROM species_translations - WHERE species_id IN (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) AND locale = \\"en\\"", - Array [ - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "10", - "11", - "12", - "13", - "14", - "15", - "16", - "17", - "18", - "19", - "20", - "21", - "22", - "23", - "24", - "25", - "26", - "27", - "28", - "29", - "30", - "31", - "32", - "33", - "34", - "35", - "36", - "37", - "38", - "39", - "40", - "41", - "42", - "43", - "44", - "45", - "46", - "47", - "48", - "49", - "50", - "51", - "52", - "53", - "54", - "55", - ], - ], -] -`; - -exports[`Item loads species with appearance data for bodyId=0 item 1`] = ` -Array [ - Array [ - "SELECT DISTINCT pet_types.species_id AS id, items.id AS item_id - FROM items - INNER JOIN parents_swf_assets ON - items.id = parents_swf_assets.parent_id AND - parents_swf_assets.parent_type = \\"Item\\" - INNER JOIN swf_assets ON - parents_swf_assets.swf_asset_id = swf_assets.id - INNER JOIN pet_types ON - pet_types.body_id = swf_assets.body_id OR swf_assets.body_id = 0 - WHERE items.id = ? - ORDER BY id", - Array [ - "37375", - ], - ], - Array [ - "SELECT * FROM species_translations - WHERE species_id IN (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) AND locale = \\"en\\"", - Array [ - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "10", - "11", - "12", - "13", - "14", - "15", - "16", - "17", - "18", - "19", - "20", - "21", - "22", - "23", - "24", - "25", - "26", - "27", - "28", - "29", - "30", - "31", - "32", - "33", - "34", - "35", - "36", - "37", - "38", - "39", - "40", - "41", - "42", - "43", - "44", - "45", - "46", - "47", - "48", - "49", - "50", - "51", - "52", - "53", - "54", - "55", - ], - ], -] -`; - -exports[`Item loads species with appearance data for single-species item 1`] = ` -Array [ - Array [ - "SELECT DISTINCT pet_types.species_id AS id, items.id AS item_id - FROM items - INNER JOIN parents_swf_assets ON - items.id = parents_swf_assets.parent_id AND - parents_swf_assets.parent_type = \\"Item\\" - INNER JOIN swf_assets ON - parents_swf_assets.swf_asset_id = swf_assets.id - INNER JOIN pet_types ON - pet_types.body_id = swf_assets.body_id OR swf_assets.body_id = 0 - WHERE items.id = ? - ORDER BY id", - Array [ - "38911", - ], - ], - Array [ - "SELECT * FROM species_translations - WHERE species_id IN (?) AND locale = \\"en\\"", - Array [ - "54", - ], - ], -] -`; - exports[`Item returns empty appearance for incompatible items 1`] = ` Object { "items": Array [ diff --git a/src/server/types/Item.js b/src/server/types/Item.js index e6c2bcc..d9c3cce 100644 --- a/src/server/types/Item.js +++ b/src/server/types/Item.js @@ -42,17 +42,19 @@ const typeDefs = gql` # bodies like Blue, Green, Red, etc. speciesThatNeedModels(colorId: ID): [Species!]! - # Species that we know how they look wearing this item. Used to initialize - # the preview on the item page with a compatible species. - # TODO: This would probably make more sense as like, compatible bodies, so - # we could also encode special-color stuff in here too. - speciesWithAppearanceDataForThisItem: [Species!]! + # Return a single ItemAppearance for this item. It'll be for the species + # with the smallest ID for which we have item appearance data. We use this + # on the item page, to initialize the preview section. (You can find out + # which species this is for by going through the body field on + # ItemAppearance!) + canonicalAppearance: ItemAppearance } type ItemAppearance { id: ID! item: Item! - bodyId: ID! + bodyId: ID! # Deprecated, use body->id. + body: Body! layers: [AppearanceLayer!] restrictedZones: [Zone!]! } @@ -182,18 +184,29 @@ const resolvers = { ); return unmodeledSpeciesIds.map((id) => ({ id })); }, - speciesWithAppearanceDataForThisItem: async ( + canonicalAppearance: async ( { id }, _, - { itemSpeciesWithAppearanceDataLoader } + { itemBodiesWithAppearanceDataLoader } ) => { - const rows = await itemSpeciesWithAppearanceDataLoader.load(id); - return rows.map((row) => ({ id: row.id })); + const rows = await itemBodiesWithAppearanceDataLoader.load(id); + const canonicalBodyId = rows[0].bodyId; + return { + item: { id }, + bodyId: canonicalBodyId, + // An optimization: we know the species already, so fill it in here + // without requiring an extra query if we want it. + // TODO: Maybe this would be cleaner if we make the body -> species + // loader, and prime it in the item bodies loader, rather than + // setting it here? + body: { id: canonicalBodyId, species: { id: rows[0].speciesId } }, + }; }, }, ItemAppearance: { id: ({ item, bodyId }) => `item-${item.id}-body-${bodyId}`, + body: ({ body, bodyId }) => body || { id: bodyId }, layers: async ({ item, bodyId }, _, { itemSwfAssetLoader }) => { const allSwfAssets = await itemSwfAssetLoader.load({ itemId: item.id, diff --git a/src/server/types/PetAppearance.js b/src/server/types/PetAppearance.js index 27a775e..7bc9233 100644 --- a/src/server/types/PetAppearance.js +++ b/src/server/types/PetAppearance.js @@ -38,6 +38,14 @@ const typeDefs = gql` UNKNOWN # for when we have the data, but we don't know what it is } + type Body { + id: ID! + species: Species! + + # A PetAppearance that has this body. Prefers Blue and happy poses. + canonicalAppearance: PetAppearance + } + # Cache for 1 week (unlikely to change) type PetAppearance @cacheControl(maxAge: 604800) { id: ID! @@ -102,6 +110,29 @@ const resolvers = { }, }, + Body: { + species: ({ species }) => { + if (species) { + return species; + } + throw new Error( + "HACK: We populate this when you look up a canonicalAppearance, but " + + "don't have a direct query for it yet, oops!" + ); + }, + canonicalAppearance: async ( + { id }, + _, + { canonicalPetStateForBodyLoader } + ) => { + const petState = await canonicalPetStateForBodyLoader.load(id); + if (!petState) { + return null; + } + return { id: petState.id }; + }, + }, + PetAppearance: { color: async ({ id }, _, { petStateLoader, petTypeLoader }) => { const petState = await petStateLoader.load(id);