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;