GQL for canonical appearance for body

gonna use this for item page! I walked back my supported species idea 😅
This commit is contained in:
Emi Matchu 2020-09-20 22:21:23 -07:00
parent b53d95cda8
commit 1b59b9631b
6 changed files with 613 additions and 226 deletions

View file

@ -33,6 +33,10 @@ import {
NpBadge, NpBadge,
} from "./components/ItemCard"; } from "./components/ItemCard";
import { Delay, Heading1, usePageTitle } from "./util"; import { Delay, Heading1, usePageTitle } from "./util";
import {
itemAppearanceFragment,
petAppearanceFragment,
} from "./components/useOutfitAppearance";
import OutfitPreview from "./components/OutfitPreview"; import OutfitPreview from "./components/OutfitPreview";
import SpeciesColorPicker from "./components/SpeciesColorPicker"; import SpeciesColorPicker from "./components/SpeciesColorPicker";
@ -439,16 +443,41 @@ function ItemPageOutfitPreview({ itemId }) {
[] []
); );
const [petState, setPetState] = React.useState({ const [petState, setPetState] = React.useState({
// Start by looking up Acara appearance data.
speciesId: "1", speciesId: "1",
colorId: "8", colorId: "8",
pose: idealPose, 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 // 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 // appearance, but only against the cache. That way, we don't send a
// redundant network request just for this (the OutfitPreview component will // redundant network request just for this (the OutfitPreview component will
// handle it!), but we'll get an update once it arrives in the cache. // handle it!), but we'll get an update once it arrives in the cache.
const { data } = useQuery( const { cachedData } = useQuery(
gql` gql`
query ItemPageOutfitPreview_CacheOnly( query ItemPageOutfitPreview_CacheOnly(
$itemId: ID! $itemId: ID!
@ -477,7 +506,7 @@ function ItemPageOutfitPreview({ itemId }) {
// If the layers are null-y, then we're still loading. Otherwise, if the // 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 // layers are an empty array, then we're incomaptible. Or, if they're a
// non-empty array, then we're compatible! // 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 isIncompatible = Array.isArray(layers) && layers.length === 0;
const borderColor = useColorModeValue("green.700", "green.400"); const borderColor = useColorModeValue("green.700", "green.400");

View file

@ -294,11 +294,13 @@ const buildItemsThatNeedModelsLoader = (db) =>
return [lastResult]; return [lastResult];
}); });
const buildItemSpeciesWithAppearanceDataLoader = (db) => const buildItemBodiesWithAppearanceDataLoader = (db) =>
new DataLoader(async (itemIds) => { new DataLoader(async (itemIds) => {
const qs = itemIds.map((_) => "?").join(","); const qs = itemIds.map((_) => "?").join(",");
const [rows, _] = await db.execute( 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 FROM items
INNER JOIN parents_swf_assets ON INNER JOIN parents_swf_assets ON
items.id = parents_swf_assets.parent_id AND items.id = parents_swf_assets.parent_id AND
@ -307,8 +309,13 @@ const buildItemSpeciesWithAppearanceDataLoader = (db) =>
parents_swf_assets.swf_asset_id = swf_assets.id parents_swf_assets.swf_asset_id = swf_assets.id
INNER JOIN pet_types ON INNER JOIN pet_types ON
pet_types.body_id = swf_assets.body_id OR swf_assets.body_id = 0 pet_types.body_id = swf_assets.body_id OR swf_assets.body_id = 0
WHERE items.id = ${qs} INNER JOIN colors ON
ORDER BY id`, 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 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) => const buildUserLoader = (db) =>
new DataLoader(async (ids) => { new DataLoader(async (ids) => {
const qs = ids.map((_) => "?").join(","); const qs = ids.map((_) => "?").join(",");
@ -617,7 +668,7 @@ function buildLoaders(db) {
loaders.itemSearchLoader = buildItemSearchLoader(db, loaders); loaders.itemSearchLoader = buildItemSearchLoader(db, loaders);
loaders.itemSearchToFitLoader = buildItemSearchToFitLoader(db, loaders); loaders.itemSearchToFitLoader = buildItemSearchToFitLoader(db, loaders);
loaders.itemsThatNeedModelsLoader = buildItemsThatNeedModelsLoader(db); loaders.itemsThatNeedModelsLoader = buildItemsThatNeedModelsLoader(db);
loaders.itemSpeciesWithAppearanceDataLoader = buildItemSpeciesWithAppearanceDataLoader( loaders.itemBodiesWithAppearanceDataLoader = buildItemBodiesWithAppearanceDataLoader(
db db
); );
loaders.petTypeLoader = buildPetTypeLoader(db); loaders.petTypeLoader = buildPetTypeLoader(db);
@ -637,6 +688,10 @@ function buildLoaders(db) {
db, db,
loaders loaders
); );
loaders.canonicalPetStateForBodyLoader = buildCanonicalPetStateForBodyLoader(
db,
loaders
);
loaders.speciesLoader = buildSpeciesLoader(db); loaders.speciesLoader = buildSpeciesLoader(db);
loaders.speciesTranslationLoader = buildSpeciesTranslationLoader(db); loaders.speciesTranslationLoader = buildSpeciesTranslationLoader(db);
loaders.userLoader = buildUserLoader(db); loaders.userLoader = buildUserLoader(db);

View file

@ -431,66 +431,158 @@ describe("Item", () => {
expect(getDbCalls()).toMatchSnapshot(); 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({ const res = await query({
query: gql` query: gql`
query { query {
item( item(
id: "38911" # Zafara Agent Hood id: "38911" # Zafara Agent Hood
) { ) {
speciesWithAppearanceDataForThisItem { canonicalAppearance {
id
layers {
id
}
body {
species {
name name
} }
canonicalAppearance {
id
}
}
}
} }
} }
`, `,
}); });
expect(res).toHaveNoErrors(); expect(res).toHaveNoErrors();
expect(res.data.item.speciesWithAppearanceDataForThisItem).toHaveLength(1); const body = res.data.item.canonicalAppearance.body;
expect(res.data.item.speciesWithAppearanceDataForThisItem[0].name).toEqual( expect(body.species.name).toEqual("Zafara");
"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({ const res = await query({
query: gql` query: gql`
query { query {
item( item(
id: "74967" # 17th Birthday Party Hat id: "74967" # 17th Birthday Party Hat
) { ) {
speciesWithAppearanceDataForThisItem { canonicalAppearance {
id
layers {
id
}
body {
species {
name name
} }
canonicalAppearance {
id
}
}
}
} }
} }
`, `,
}); });
expect(res).toHaveNoErrors(); expect(res).toHaveNoErrors();
expect(res.data.item.speciesWithAppearanceDataForThisItem).toHaveLength(55); const body = res.data.item.canonicalAppearance.body;
expect(getDbCalls()).toMatchSnapshot(); 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({ const res = await query({
query: gql` query: gql`
query { query {
item( item(
id: "37375" # Moon and Stars Background id: "37375" # Moon and Stars Background
) { ) {
speciesWithAppearanceDataForThisItem { canonicalAppearance {
id
layers {
id
}
body {
species {
name name
} }
canonicalAppearance {
id
}
}
}
} }
} }
`, `,
}); });
expect(res).toHaveNoErrors(); expect(res).toHaveNoErrors();
expect(res.data.item.speciesWithAppearanceDataForThisItem).toHaveLength(55); const body = res.data.item.canonicalAppearance.body;
expect(getDbCalls()).toMatchSnapshot(); 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");
}); });
}); });

View file

@ -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`] = ` exports[`Item loads items that need models 1`] = `
Object { Object {
"babyItems": Array [ "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`] = ` exports[`Item returns empty appearance for incompatible items 1`] = `
Object { Object {
"items": Array [ "items": Array [

View file

@ -42,17 +42,19 @@ const typeDefs = gql`
# bodies like Blue, Green, Red, etc. # bodies like Blue, Green, Red, etc.
speciesThatNeedModels(colorId: ID): [Species!]! speciesThatNeedModels(colorId: ID): [Species!]!
# Species that we know how they look wearing this item. Used to initialize # Return a single ItemAppearance for this item. It'll be for the species
# the preview on the item page with a compatible species. # with the smallest ID for which we have item appearance data. We use this
# TODO: This would probably make more sense as like, compatible bodies, so # on the item page, to initialize the preview section. (You can find out
# we could also encode special-color stuff in here too. # which species this is for by going through the body field on
speciesWithAppearanceDataForThisItem: [Species!]! # ItemAppearance!)
canonicalAppearance: ItemAppearance
} }
type ItemAppearance { type ItemAppearance {
id: ID! id: ID!
item: Item! item: Item!
bodyId: ID! bodyId: ID! # Deprecated, use body->id.
body: Body!
layers: [AppearanceLayer!] layers: [AppearanceLayer!]
restrictedZones: [Zone!]! restrictedZones: [Zone!]!
} }
@ -182,18 +184,29 @@ const resolvers = {
); );
return unmodeledSpeciesIds.map((id) => ({ id })); return unmodeledSpeciesIds.map((id) => ({ id }));
}, },
speciesWithAppearanceDataForThisItem: async ( canonicalAppearance: async (
{ id }, { id },
_, _,
{ itemSpeciesWithAppearanceDataLoader } { itemBodiesWithAppearanceDataLoader }
) => { ) => {
const rows = await itemSpeciesWithAppearanceDataLoader.load(id); const rows = await itemBodiesWithAppearanceDataLoader.load(id);
return rows.map((row) => ({ id: row.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: { ItemAppearance: {
id: ({ item, bodyId }) => `item-${item.id}-body-${bodyId}`, id: ({ item, bodyId }) => `item-${item.id}-body-${bodyId}`,
body: ({ body, bodyId }) => body || { id: bodyId },
layers: async ({ item, bodyId }, _, { itemSwfAssetLoader }) => { layers: async ({ item, bodyId }, _, { itemSwfAssetLoader }) => {
const allSwfAssets = await itemSwfAssetLoader.load({ const allSwfAssets = await itemSwfAssetLoader.load({
itemId: item.id, itemId: item.id,

View file

@ -38,6 +38,14 @@ const typeDefs = gql`
UNKNOWN # for when we have the data, but we don't know what it is 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) # Cache for 1 week (unlikely to change)
type PetAppearance @cacheControl(maxAge: 604800) { type PetAppearance @cacheControl(maxAge: 604800) {
id: ID! 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: { PetAppearance: {
color: async ({ id }, _, { petStateLoader, petTypeLoader }) => { color: async ({ id }, _, { petStateLoader, petTypeLoader }) => {
const petState = await petStateLoader.load(id); const petState = await petStateLoader.load(id);