refactor GQL typedefs/resolvers into separate files
get that giant index file broken up a bit!
This commit is contained in:
parent
453953dded
commit
3512418a66
16 changed files with 2154 additions and 30157 deletions
1325
src/server/index.js
1325
src/server/index.js
File diff suppressed because it is too large
Load diff
|
@ -1,74 +0,0 @@
|
|||
const fetch = require("node-fetch");
|
||||
|
||||
async function loadPetMetaData(petName) {
|
||||
const url =
|
||||
`http://www.neopets.com/amfphp/json.php/PetService.getPet` + `/${petName}`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`for pet meta data, neopets.com returned: ` +
|
||||
`${res.status} ${res.statusText}. (${url})`
|
||||
);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json;
|
||||
}
|
||||
|
||||
async function loadCustomPetData(petName) {
|
||||
const url =
|
||||
`http://www.neopets.com/amfphp/json.php/CustomPetService.getViewerData` +
|
||||
`/${petName}`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`for custom pet data, neopets.com returned: ` +
|
||||
`${res.status} ${res.statusText}. (${url})`
|
||||
);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
if (!json.custom_pet) {
|
||||
throw new Error(`missing custom_pet data`);
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
async function loadAssetManifest(swfUrl) {
|
||||
const manifestUrl = convertSwfUrlToManifestUrl(swfUrl);
|
||||
const res = await fetch(manifestUrl);
|
||||
if (res.status === 404) {
|
||||
return null;
|
||||
} else if (!res.ok) {
|
||||
throw new Error(
|
||||
`for asset manifest, images.neopets.com returned: ` +
|
||||
`${res.status} ${res.statusText}. (${manifestUrl})`
|
||||
);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return {
|
||||
assets: json["cpmanifest"]["assets"].map((asset) => ({
|
||||
format: asset["format"],
|
||||
assetData: asset["asset_data"].map((assetDatum) => ({
|
||||
path: assetDatum["url"],
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
const SWF_URL_PATTERN = /^http:\/\/images\.neopets\.com\/cp\/(.+?)\/swf\/(.+?)\.swf$/;
|
||||
|
||||
function convertSwfUrlToManifestUrl(swfUrl) {
|
||||
const match = swfUrl.match(SWF_URL_PATTERN);
|
||||
if (!match) {
|
||||
throw new Error(`unexpected SWF URL format: ${JSON.stringify(swfUrl)}`);
|
||||
}
|
||||
|
||||
const [_, type, folders] = match;
|
||||
|
||||
return `http://images.neopets.com/cp/${type}/data/${folders}/manifest.json`;
|
||||
}
|
||||
|
||||
module.exports = { loadPetMetaData, loadCustomPetData, loadAssetManifest };
|
|
@ -1,31 +0,0 @@
|
|||
const gql = require("graphql-tag");
|
||||
const { query, getDbCalls } = require("./setup.js");
|
||||
|
||||
describe("SpeciesColorPair", () => {
|
||||
it("gets them all", async () => {
|
||||
const res = await query({
|
||||
query: gql`
|
||||
query {
|
||||
allValidSpeciesColorPairs {
|
||||
color {
|
||||
id
|
||||
}
|
||||
species {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(res).toHaveNoErrors();
|
||||
expect(res.data).toMatchSnapshot();
|
||||
expect(getDbCalls()).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"SELECT species_id, color_id FROM pet_types",
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -170,7 +170,7 @@ describe("User", () => {
|
|||
INNER JOIN items ON items.id = closet_hangers.item_id
|
||||
INNER JOIN item_translations ON
|
||||
item_translations.item_id = items.id AND locale = \\"en\\"
|
||||
WHERE user_id IN (44743) AND owned = 1
|
||||
WHERE user_id IN (?) AND owned = 1
|
||||
ORDER BY item_name",
|
||||
Array [
|
||||
"44743",
|
||||
|
|
|
@ -165,3 +165,18 @@ Object {
|
|||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Item skips appearance data for audio assets 5`] = `
|
||||
Object {
|
||||
"items": Array [
|
||||
Object {
|
||||
"appearanceOn": Object {
|
||||
"layers": Array [],
|
||||
"restrictedZones": Array [],
|
||||
},
|
||||
"id": "42829",
|
||||
"name": "Time Tunnel Music Track",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -96,6 +96,44 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`ItemSearch loads Neopian Times items that fit the Starry Zafara 5`] = `
|
||||
Object {
|
||||
"itemSearchToFit": Object {
|
||||
"items": Array [
|
||||
Object {
|
||||
"id": "40431",
|
||||
"name": "Neopian Times Background",
|
||||
},
|
||||
Object {
|
||||
"id": "51098",
|
||||
"name": "Neopian Times Writing Quill",
|
||||
},
|
||||
Object {
|
||||
"id": "61101",
|
||||
"name": "Neopian Times Zafara Handkerchief",
|
||||
},
|
||||
Object {
|
||||
"id": "61100",
|
||||
"name": "Neopian Times Zafara Hat",
|
||||
},
|
||||
Object {
|
||||
"id": "61102",
|
||||
"name": "Neopian Times Zafara Shirt and Vest",
|
||||
},
|
||||
Object {
|
||||
"id": "61104",
|
||||
"name": "Neopian Times Zafara Shoes",
|
||||
},
|
||||
Object {
|
||||
"id": "61103",
|
||||
"name": "Neopian Times Zafara Trousers",
|
||||
},
|
||||
],
|
||||
"query": "Neopian Times",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`ItemSearch loads the first 10 hats that fit the Starry Zafara 1`] = `
|
||||
Object {
|
||||
"itemSearchToFit": Object {
|
||||
|
|
|
@ -701,3 +701,637 @@ Object {
|
|||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`PetAppearance loads unconverted appearance 3`] = `
|
||||
Object {
|
||||
"petAppearances": Array [
|
||||
Object {
|
||||
"bodyId": "180",
|
||||
"color": Object {
|
||||
"id": "75",
|
||||
"name": "Starry",
|
||||
},
|
||||
"id": "17723",
|
||||
"layers": Array [
|
||||
Object {
|
||||
"id": "5995",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7941/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 18,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "5996",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7942/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 7,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "6000",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7946/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 40,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "16467",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/024/24008/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 34,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "19784",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/028/28892/600x600.png?v2-1313418652000",
|
||||
"zone": Object {
|
||||
"depth": 37,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "178150",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/036/36887/600x600.png?v2-1354240708000",
|
||||
"zone": Object {
|
||||
"depth": 38,
|
||||
},
|
||||
},
|
||||
],
|
||||
"petStateId": "17723",
|
||||
"pose": "HAPPY_FEM",
|
||||
"species": Object {
|
||||
"id": "54",
|
||||
"name": "Zafara",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bodyId": "180",
|
||||
"color": Object {
|
||||
"id": "75",
|
||||
"name": "Starry",
|
||||
},
|
||||
"id": "17742",
|
||||
"layers": Array [
|
||||
Object {
|
||||
"id": "5995",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7941/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 18,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "5996",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7942/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 7,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "6000",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7946/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 40,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "16467",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/024/24008/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 34,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "19549",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/028/28548/600x600.png?v2-1345719457000",
|
||||
"zone": Object {
|
||||
"depth": 37,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "178150",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/036/36887/600x600.png?v2-1354240708000",
|
||||
"zone": Object {
|
||||
"depth": 38,
|
||||
},
|
||||
},
|
||||
],
|
||||
"petStateId": "17742",
|
||||
"pose": "HAPPY_MASC",
|
||||
"species": Object {
|
||||
"id": "54",
|
||||
"name": "Zafara",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bodyId": "180",
|
||||
"color": Object {
|
||||
"id": "75",
|
||||
"name": "Starry",
|
||||
},
|
||||
"id": "5991",
|
||||
"layers": Array [
|
||||
Object {
|
||||
"id": "5995",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7941/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 18,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "5996",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7942/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 7,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "6000",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7946/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 40,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "14790",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/021/21057/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 38,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "14793",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/021/21061/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 37,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "16467",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/024/24008/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 34,
|
||||
},
|
||||
},
|
||||
],
|
||||
"petStateId": "5991",
|
||||
"pose": "SAD_FEM",
|
||||
"species": Object {
|
||||
"id": "54",
|
||||
"name": "Zafara",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bodyId": "180",
|
||||
"color": Object {
|
||||
"id": "75",
|
||||
"name": "Starry",
|
||||
},
|
||||
"id": "436",
|
||||
"layers": Array [
|
||||
Object {
|
||||
"id": "5995",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7941/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 18,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "5996",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7942/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 7,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "6000",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7946/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 40,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "14790",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/021/21057/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 38,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "14792",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/021/21060/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 37,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "16467",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/024/24008/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 34,
|
||||
},
|
||||
},
|
||||
],
|
||||
"petStateId": "436",
|
||||
"pose": "SAD_MASC",
|
||||
"species": Object {
|
||||
"id": "54",
|
||||
"name": "Zafara",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bodyId": "180",
|
||||
"color": Object {
|
||||
"id": "75",
|
||||
"name": "Starry",
|
||||
},
|
||||
"id": "10014",
|
||||
"layers": Array [
|
||||
Object {
|
||||
"id": "5995",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7941/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 18,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "5996",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7942/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 7,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "6000",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7946/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 40,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "14791",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/021/21059/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 38,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "14795",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/021/21066/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 37,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "16467",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/024/24008/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 34,
|
||||
},
|
||||
},
|
||||
],
|
||||
"petStateId": "10014",
|
||||
"pose": "SICK_FEM",
|
||||
"species": Object {
|
||||
"id": "54",
|
||||
"name": "Zafara",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bodyId": "180",
|
||||
"color": Object {
|
||||
"id": "75",
|
||||
"name": "Starry",
|
||||
},
|
||||
"id": "11089",
|
||||
"layers": Array [
|
||||
Object {
|
||||
"id": "5995",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7941/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 18,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "5996",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7942/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 7,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "6000",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7946/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 40,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "14791",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/021/21059/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 38,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "14794",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/021/21064/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 37,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "16467",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/024/24008/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 34,
|
||||
},
|
||||
},
|
||||
],
|
||||
"petStateId": "11089",
|
||||
"pose": "SICK_MASC",
|
||||
"species": Object {
|
||||
"id": "54",
|
||||
"name": "Zafara",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bodyId": "180",
|
||||
"color": Object {
|
||||
"id": "75",
|
||||
"name": "Starry",
|
||||
},
|
||||
"id": "4751",
|
||||
"layers": Array [
|
||||
Object {
|
||||
"id": "5995",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7941/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 18,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "5996",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7942/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 7,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "6000",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7946/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 40,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "16467",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/024/24008/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 34,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "19550",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/028/28549/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 38,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "19784",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/028/28892/600x600.png?v2-1313418652000",
|
||||
"zone": Object {
|
||||
"depth": 37,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "163528",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/028/28549/600x600.png?v2-1326455337000",
|
||||
"zone": Object {
|
||||
"depth": 38,
|
||||
},
|
||||
},
|
||||
],
|
||||
"petStateId": "4751",
|
||||
"pose": "UNKNOWN",
|
||||
"species": Object {
|
||||
"id": "54",
|
||||
"name": "Zafara",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bodyId": "180",
|
||||
"color": Object {
|
||||
"id": "75",
|
||||
"name": "Starry",
|
||||
},
|
||||
"id": "2",
|
||||
"layers": Array [
|
||||
Object {
|
||||
"id": "5995",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7941/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 18,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "5996",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7942/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 7,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "6000",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/007/7946/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 40,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "16467",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/024/24008/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 34,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "19549",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/028/28548/600x600.png?v2-1345719457000",
|
||||
"zone": Object {
|
||||
"depth": 37,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "19550",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/028/28549/600x600.png?v2-0",
|
||||
"zone": Object {
|
||||
"depth": 38,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"id": "163528",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/028/28549/600x600.png?v2-1326455337000",
|
||||
"zone": Object {
|
||||
"depth": 38,
|
||||
},
|
||||
},
|
||||
],
|
||||
"petStateId": "2",
|
||||
"pose": "UNKNOWN",
|
||||
"species": Object {
|
||||
"id": "54",
|
||||
"name": "Zafara",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`PetAppearance loads unconverted appearance 5`] = `
|
||||
Object {
|
||||
"petAppearance": Object {
|
||||
"color": Object {
|
||||
"id": "63",
|
||||
"isStandard": true,
|
||||
"name": "Royalboy",
|
||||
},
|
||||
"id": "2571",
|
||||
"layers": Array [
|
||||
Object {
|
||||
"id": "9941",
|
||||
"imageUrl": "https://impress-asset-images.s3.amazonaws.com/biology/000/000/013/13024/600x600.png?v2-0",
|
||||
"svgUrl": "http://images.neopets.com/cp/bio/data/000/000/013/13024_18911a85d1/13024.svg",
|
||||
"zone": Object {
|
||||
"depth": 48,
|
||||
},
|
||||
},
|
||||
],
|
||||
"restrictedZones": Array [
|
||||
Object {
|
||||
"id": "4",
|
||||
},
|
||||
Object {
|
||||
"id": "5",
|
||||
},
|
||||
Object {
|
||||
"id": "6",
|
||||
},
|
||||
Object {
|
||||
"id": "7",
|
||||
},
|
||||
Object {
|
||||
"id": "8",
|
||||
},
|
||||
Object {
|
||||
"id": "9",
|
||||
},
|
||||
Object {
|
||||
"id": "10",
|
||||
},
|
||||
Object {
|
||||
"id": "11",
|
||||
},
|
||||
Object {
|
||||
"id": "12",
|
||||
},
|
||||
Object {
|
||||
"id": "13",
|
||||
},
|
||||
Object {
|
||||
"id": "14",
|
||||
},
|
||||
Object {
|
||||
"id": "15",
|
||||
},
|
||||
Object {
|
||||
"id": "16",
|
||||
},
|
||||
Object {
|
||||
"id": "17",
|
||||
},
|
||||
Object {
|
||||
"id": "18",
|
||||
},
|
||||
Object {
|
||||
"id": "19",
|
||||
},
|
||||
Object {
|
||||
"id": "20",
|
||||
},
|
||||
Object {
|
||||
"id": "21",
|
||||
},
|
||||
Object {
|
||||
"id": "22",
|
||||
},
|
||||
Object {
|
||||
"id": "23",
|
||||
},
|
||||
Object {
|
||||
"id": "24",
|
||||
},
|
||||
Object {
|
||||
"id": "25",
|
||||
},
|
||||
Object {
|
||||
"id": "26",
|
||||
},
|
||||
Object {
|
||||
"id": "27",
|
||||
},
|
||||
Object {
|
||||
"id": "28",
|
||||
},
|
||||
Object {
|
||||
"id": "29",
|
||||
},
|
||||
Object {
|
||||
"id": "30",
|
||||
},
|
||||
Object {
|
||||
"id": "31",
|
||||
},
|
||||
Object {
|
||||
"id": "32",
|
||||
},
|
||||
Object {
|
||||
"id": "33",
|
||||
},
|
||||
Object {
|
||||
"id": "34",
|
||||
},
|
||||
Object {
|
||||
"id": "35",
|
||||
},
|
||||
Object {
|
||||
"id": "36",
|
||||
},
|
||||
Object {
|
||||
"id": "37",
|
||||
},
|
||||
Object {
|
||||
"id": "38",
|
||||
},
|
||||
Object {
|
||||
"id": "39",
|
||||
},
|
||||
Object {
|
||||
"id": "40",
|
||||
},
|
||||
Object {
|
||||
"id": "41",
|
||||
},
|
||||
Object {
|
||||
"id": "42",
|
||||
},
|
||||
Object {
|
||||
"id": "43",
|
||||
},
|
||||
],
|
||||
"species": Object {
|
||||
"id": "1",
|
||||
"name": "Acara",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
|
File diff suppressed because it is too large
Load diff
262
src/server/types/AppearanceLayer.js
Normal file
262
src/server/types/AppearanceLayer.js
Normal file
|
@ -0,0 +1,262 @@
|
|||
const fetch = require("node-fetch");
|
||||
const { gql } = require("apollo-server");
|
||||
|
||||
const typeDefs = gql`
|
||||
enum LayerImageSize {
|
||||
SIZE_600
|
||||
SIZE_300
|
||||
SIZE_150
|
||||
}
|
||||
|
||||
# Cache for 1 week (unlikely to change)
|
||||
type AppearanceLayer @cacheControl(maxAge: 604800) {
|
||||
# The DTI ID. Guaranteed unique across all layers of all types.
|
||||
id: ID!
|
||||
|
||||
# The Neopets ID. Guaranteed unique across layers of the _same_ type, but
|
||||
# not of different types. That is, it's allowed and common for an item
|
||||
# layer and a pet layer to have the same remoteId.
|
||||
remoteId: ID!
|
||||
|
||||
zone: Zone!
|
||||
imageUrl(size: LayerImageSize): String
|
||||
|
||||
"""
|
||||
This layer as a single SVG, if available.
|
||||
|
||||
This might not be available if the asset isn't converted yet by Neopets,
|
||||
or if it's not as simple as a single SVG (e.g. animated).
|
||||
"""
|
||||
svgUrl: String
|
||||
|
||||
"""
|
||||
This layer as a single SWF, if available.
|
||||
|
||||
At time of writing, all layers have SWFs. But I've marked this nullable
|
||||
because I'm not sure this will continue to be true after the HTML5
|
||||
migration, and I'd like clients to guard against it.
|
||||
"""
|
||||
swfUrl: String
|
||||
|
||||
"""
|
||||
This layer can fit on PetAppearances with the same bodyId. "0" is a
|
||||
special body ID that indicates it fits all PetAppearances.
|
||||
"""
|
||||
bodyId: ID!
|
||||
|
||||
"""
|
||||
The item this layer is for, if any. (For pet layers, this is null.)
|
||||
"""
|
||||
item: Item
|
||||
|
||||
"""
|
||||
The zones that this layer restricts, if any. Note that, for item layers,
|
||||
this is generally empty and the restriction is on the ItemAppearance, not
|
||||
the individual layers. For pet layers, this is generally used for
|
||||
Unconverted pets.
|
||||
|
||||
Deprecated, aggregated into PetAppearance for a simpler API.
|
||||
"""
|
||||
restrictedZones: [Zone!]!
|
||||
}
|
||||
`;
|
||||
|
||||
const resolvers = {
|
||||
AppearanceLayer: {
|
||||
bodyId: async ({ id }, _, { swfAssetLoader }) => {
|
||||
const layer = await swfAssetLoader.load(id);
|
||||
return layer.remoteId;
|
||||
},
|
||||
bodyId: async ({ id }, _, { swfAssetLoader }) => {
|
||||
const layer = await swfAssetLoader.load(id);
|
||||
return layer.bodyId;
|
||||
},
|
||||
zone: async ({ id }, _, { swfAssetLoader, zoneLoader }) => {
|
||||
const layer = await swfAssetLoader.load(id);
|
||||
return { id: layer.zoneId };
|
||||
},
|
||||
restrictedZones: async ({ id }, _, { swfAssetLoader }) => {
|
||||
const layer = await swfAssetLoader.load(id);
|
||||
return getRestrictedZoneIds(layer.zonesRestrict).map((id) => ({ id }));
|
||||
},
|
||||
swfUrl: async ({ id }, _, { swfAssetLoader }) => {
|
||||
const layer = await swfAssetLoader.load(id);
|
||||
return layer.url;
|
||||
},
|
||||
imageUrl: async ({ id }, { size }, { swfAssetLoader }) => {
|
||||
const layer = await swfAssetLoader.load(id);
|
||||
|
||||
if (!layer.hasImage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sizeNum = size.split("_")[1];
|
||||
|
||||
const rid = layer.remoteId;
|
||||
const paddedId = rid.padStart(12, "0");
|
||||
const rid1 = paddedId.slice(0, 3);
|
||||
const rid2 = paddedId.slice(3, 6);
|
||||
const rid3 = paddedId.slice(6, 9);
|
||||
const time = Number(new Date(layer.convertedAt));
|
||||
|
||||
return (
|
||||
`https://impress-asset-images.s3.amazonaws.com/${layer.type}` +
|
||||
`/${rid1}/${rid2}/${rid3}/${rid}/${sizeNum}x${sizeNum}.png?v2-${time}`
|
||||
);
|
||||
},
|
||||
svgUrl: async ({ id }, _, { db, swfAssetLoader, svgLogger }) => {
|
||||
const layer = await swfAssetLoader.load(id);
|
||||
let manifest = layer.manifest && JSON.parse(layer.manifest);
|
||||
|
||||
// When the manifest is specifically null, that means we don't know if
|
||||
// it exists yet. Load it to find out!
|
||||
if (manifest === null) {
|
||||
manifest = await loadAssetManifest(layer.url);
|
||||
|
||||
// Then, write the new manifest. We make sure to write an empty string
|
||||
// if there was no manifest, to signify that it doesn't exist, so we
|
||||
// don't need to bother looking it up again.
|
||||
//
|
||||
// TODO: Someday the manifests will all exist, right? So we'll want to
|
||||
// reload all the missing ones at that time.
|
||||
manifest = manifest || "";
|
||||
const [
|
||||
result,
|
||||
] = await db.execute(
|
||||
`UPDATE swf_assets SET manifest = ? WHERE id = ? LIMIT 1;`,
|
||||
[manifest, layer.id]
|
||||
);
|
||||
if (result.affectedRows !== 1) {
|
||||
throw new Error(
|
||||
`Expected to affect 1 asset, but affected ${result.affectedRows}`
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`Loaded and saved manifest for ${layer.type} ${layer.remoteId}. ` +
|
||||
`DTI ID: ${layer.id}. Exists?: ${Boolean(manifest)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!manifest) {
|
||||
svgLogger.log("no-manifest");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (manifest.assets.length !== 1) {
|
||||
svgLogger.log(`wrong-asset-count:${manifest.assets.length}!=1`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const asset = manifest.assets[0];
|
||||
if (asset.format !== "vector") {
|
||||
svgLogger.log(`wrong-asset-format:${asset.format}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (asset.assetData.length !== 1) {
|
||||
svgLogger.log(`wrong-assetData-length:${asset.assetData.length}!=1`);
|
||||
return null;
|
||||
}
|
||||
|
||||
svgLogger.log("success");
|
||||
const assetDatum = asset.assetData[0];
|
||||
const url = new URL(assetDatum.path, "http://images.neopets.com");
|
||||
return url.toString();
|
||||
},
|
||||
item: async ({ id }, _, { db }) => {
|
||||
// TODO: If this becomes a popular request, we'll definitely need to
|
||||
// loaderize this! I'm cheating for now because it's just Support, one at
|
||||
// a time.
|
||||
const [rows] = await db.query(
|
||||
`
|
||||
SELECT parent_id FROM parents_swf_assets
|
||||
WHERE swf_asset_id = ? AND parent_type = "Item" LIMIT 1;
|
||||
`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { id: String(rows[0].parent_id) };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async function loadAssetManifest(swfUrl) {
|
||||
const manifestUrl = convertSwfUrlToManifestUrl(swfUrl);
|
||||
const res = await fetch(manifestUrl);
|
||||
if (res.status === 404) {
|
||||
return null;
|
||||
} else if (!res.ok) {
|
||||
throw new Error(
|
||||
`for asset manifest, images.neopets.com returned: ` +
|
||||
`${res.status} ${res.statusText}. (${manifestUrl})`
|
||||
);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return {
|
||||
assets: json["cpmanifest"]["assets"].map((asset) => ({
|
||||
format: asset["format"],
|
||||
assetData: asset["asset_data"].map((assetDatum) => ({
|
||||
path: assetDatum["url"],
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
const SWF_URL_PATTERN = /^http:\/\/images\.neopets\.com\/cp\/(.+?)\/swf\/(.+?)\.swf$/;
|
||||
|
||||
function convertSwfUrlToManifestUrl(swfUrl) {
|
||||
const match = swfUrl.match(SWF_URL_PATTERN);
|
||||
if (!match) {
|
||||
throw new Error(`unexpected SWF URL format: ${JSON.stringify(swfUrl)}`);
|
||||
}
|
||||
|
||||
const [_, type, folders] = match;
|
||||
|
||||
return `http://images.neopets.com/cp/${type}/data/${folders}/manifest.json`;
|
||||
}
|
||||
|
||||
let lastSvgLogger = null;
|
||||
const svgLoggingPlugin = {
|
||||
requestDidStart() {
|
||||
return {
|
||||
willSendResponse({ operationName }) {
|
||||
const logEntries = lastSvgLogger.entries;
|
||||
if (logEntries.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[svgLogger] Operation: ${operationName}`);
|
||||
|
||||
const logEntryCounts = {};
|
||||
for (const logEntry of logEntries) {
|
||||
logEntryCounts[logEntry] = (logEntryCounts[logEntry] || 0) + 1;
|
||||
}
|
||||
|
||||
const logEntriesSortedByCount = Object.entries(logEntryCounts).sort(
|
||||
(a, b) => b[1] - a[1]
|
||||
);
|
||||
for (const [logEntry, count] of logEntriesSortedByCount) {
|
||||
console.log(`[svgLogger] - ${logEntry}: ${count}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
buildSvgLogger() {
|
||||
const svgLogger = {
|
||||
entries: [],
|
||||
log(entry) {
|
||||
this.entries.push(entry);
|
||||
},
|
||||
};
|
||||
lastSvgLogger = svgLogger;
|
||||
return svgLogger;
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { typeDefs, resolvers, svgLoggingPlugin };
|
169
src/server/types/Item.js
Normal file
169
src/server/types/Item.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
const { gql } = require("apollo-server");
|
||||
const { getRestrictedZoneIds } = require("../util");
|
||||
|
||||
const typeDefs = gql`
|
||||
type Item {
|
||||
id: ID!
|
||||
name: String!
|
||||
description: String!
|
||||
thumbnailUrl: String!
|
||||
rarityIndex: Int!
|
||||
isNc: Boolean!
|
||||
|
||||
# How this item appears on the given species/color combo. If it does not
|
||||
# fit the pet, we'll return an empty ItemAppearance with no layers.
|
||||
appearanceOn(speciesId: ID!, colorId: ID!): ItemAppearance!
|
||||
|
||||
# This is set manually by Support users, when the pet is only for e.g.
|
||||
# Maraquan pets, and our usual auto-detection isn't working. We provide
|
||||
# this for the Support UI; it's not very helpful for most users, because it
|
||||
# can be empty even if the item _has_ an auto-detected special color.
|
||||
manualSpecialColor: Color
|
||||
|
||||
# This is set manually by Support users, when the item _seems_ to fit all
|
||||
# pets the same because of its zones, but it actually doesn't - e.g.,
|
||||
# the Dug Up Dirt Foreground actually looks different for each body. We
|
||||
# provide this for the Support UI; it's not very helpful for most users,
|
||||
# because it's only used at modeling time. This value does not change how
|
||||
# layer data from this API should be interpreted!
|
||||
explicitlyBodySpecific: Boolean!
|
||||
}
|
||||
|
||||
type ItemAppearance {
|
||||
id: ID!
|
||||
item: Item!
|
||||
bodyId: ID!
|
||||
layers: [AppearanceLayer!]
|
||||
restrictedZones: [Zone!]!
|
||||
}
|
||||
|
||||
type ItemSearchResult {
|
||||
query: String!
|
||||
zones: [Zone!]!
|
||||
items: [Item!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
item(id: ID!): Item
|
||||
items(ids: [ID!]!): [Item!]!
|
||||
itemSearch(query: String!): ItemSearchResult!
|
||||
itemSearchToFit(
|
||||
query: String!
|
||||
speciesId: ID!
|
||||
colorId: ID!
|
||||
zoneIds: [ID!]
|
||||
offset: Int
|
||||
limit: Int
|
||||
): ItemSearchResult!
|
||||
}
|
||||
`;
|
||||
|
||||
const resolvers = {
|
||||
Item: {
|
||||
name: async ({ id, name }, _, { itemTranslationLoader }) => {
|
||||
if (name) return name;
|
||||
const translation = await itemTranslationLoader.load(id);
|
||||
return translation.name;
|
||||
},
|
||||
description: async ({ id, description }, _, { itemTranslationLoader }) => {
|
||||
if (description) return description;
|
||||
const translation = await itemTranslationLoader.load(id);
|
||||
return translation.description;
|
||||
},
|
||||
thumbnailUrl: async ({ id, thumbnailUrl }, _, { itemLoader }) => {
|
||||
if (thumbnailUrl) return thumbnailUrl;
|
||||
const item = await itemLoader.load(id);
|
||||
return item.thumbnailUrl;
|
||||
},
|
||||
rarityIndex: async ({ id, rarityIndex }, _, { itemLoader }) => {
|
||||
if (rarityIndex) return rarityIndex;
|
||||
const item = await itemLoader.load(id);
|
||||
return item.rarityIndex;
|
||||
},
|
||||
isNc: async ({ id, rarityIndex }, _, { itemLoader }) => {
|
||||
if (rarityIndex != null) return rarityIndex === 500 || rarityIndex === 0;
|
||||
const item = await itemLoader.load(id);
|
||||
return item.rarityIndex === 500 || item.rarityIndex === 0;
|
||||
},
|
||||
appearanceOn: async (
|
||||
{ id },
|
||||
{ speciesId, colorId },
|
||||
{ petTypeBySpeciesAndColorLoader }
|
||||
) => {
|
||||
const petType = await petTypeBySpeciesAndColorLoader.load({
|
||||
speciesId,
|
||||
colorId,
|
||||
});
|
||||
return { item: { id }, bodyId: petType.bodyId };
|
||||
},
|
||||
manualSpecialColor: async ({ id }, _, { itemLoader }) => {
|
||||
const item = await itemLoader.load(id);
|
||||
return item.manualSpecialColorId != null
|
||||
? { id: item.manualSpecialColorId }
|
||||
: null;
|
||||
},
|
||||
explicitlyBodySpecific: async ({ id }, _, { itemLoader }) => {
|
||||
const item = await itemLoader.load(id);
|
||||
return item.explicitlyBodySpecific;
|
||||
},
|
||||
},
|
||||
|
||||
ItemAppearance: {
|
||||
id: ({ item, bodyId }) => `item-${item.id}-body-${bodyId}`,
|
||||
layers: async ({ item, bodyId }, _, { itemSwfAssetLoader }) => {
|
||||
const allSwfAssets = await itemSwfAssetLoader.load({
|
||||
itemId: item.id,
|
||||
bodyId,
|
||||
});
|
||||
|
||||
return allSwfAssets.filter((sa) => sa.url.endsWith(".swf"));
|
||||
},
|
||||
restrictedZones: async (
|
||||
{ item: { id: itemId }, bodyId },
|
||||
_,
|
||||
{ itemSwfAssetLoader, itemLoader }
|
||||
) => {
|
||||
// Check whether this appearance is empty. If so, restrict no zones.
|
||||
const allSwfAssets = await itemSwfAssetLoader.load({ itemId, bodyId });
|
||||
if (allSwfAssets.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const item = await itemLoader.load(itemId);
|
||||
return getRestrictedZoneIds(item.zonesRestrict).map((id) => ({ id }));
|
||||
},
|
||||
},
|
||||
|
||||
Query: {
|
||||
item: (_, { id }) => ({ id }),
|
||||
items: (_, { ids }) => {
|
||||
return ids.map((id) => ({ id }));
|
||||
},
|
||||
itemSearch: async (_, { query }, { itemSearchLoader }) => {
|
||||
const items = await itemSearchLoader.load(query.trim());
|
||||
return { query, items };
|
||||
},
|
||||
itemSearchToFit: async (
|
||||
_,
|
||||
{ query, speciesId, colorId, zoneIds = [], offset, limit },
|
||||
{ petTypeBySpeciesAndColorLoader, itemSearchToFitLoader }
|
||||
) => {
|
||||
const petType = await petTypeBySpeciesAndColorLoader.load({
|
||||
speciesId,
|
||||
colorId,
|
||||
});
|
||||
const { bodyId } = petType;
|
||||
const items = await itemSearchToFitLoader.load({
|
||||
query: query.trim(),
|
||||
bodyId,
|
||||
zoneIds,
|
||||
offset,
|
||||
limit,
|
||||
});
|
||||
const zones = zoneIds.map((id) => ({ id }));
|
||||
return { query, zones, items };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { typeDefs, resolvers };
|
531
src/server/types/MutationsForSupport.js
Normal file
531
src/server/types/MutationsForSupport.js
Normal file
|
@ -0,0 +1,531 @@
|
|||
const { gql } = require("apollo-server");
|
||||
const {
|
||||
capitalize,
|
||||
getPoseFromPetState,
|
||||
getPetStateFieldsFromPose,
|
||||
getPoseName,
|
||||
loadBodyName,
|
||||
logToDiscord,
|
||||
normalizeRow,
|
||||
} = require("../util");
|
||||
|
||||
const typeDefs = gql`
|
||||
type RemoveLayerFromItemMutationResult {
|
||||
layer: AppearanceLayer!
|
||||
item: Item!
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
setManualSpecialColor(
|
||||
itemId: ID!
|
||||
colorId: ID
|
||||
supportSecret: String!
|
||||
): Item!
|
||||
|
||||
setItemExplicitlyBodySpecific(
|
||||
itemId: ID!
|
||||
explicitlyBodySpecific: Boolean!
|
||||
supportSecret: String!
|
||||
): Item!
|
||||
|
||||
setLayerBodyId(
|
||||
layerId: ID!
|
||||
bodyId: ID!
|
||||
supportSecret: String!
|
||||
): AppearanceLayer!
|
||||
|
||||
removeLayerFromItem(
|
||||
layerId: ID!
|
||||
itemId: ID!
|
||||
supportSecret: String!
|
||||
): RemoveLayerFromItemMutationResult!
|
||||
|
||||
setPetAppearancePose(
|
||||
appearanceId: ID!
|
||||
pose: Pose!
|
||||
supportSecret: String!
|
||||
): PetAppearance!
|
||||
|
||||
setPetAppearanceIsGlitched(
|
||||
appearanceId: ID!
|
||||
isGlitched: Boolean!
|
||||
supportSecret: String!
|
||||
): PetAppearance!
|
||||
}
|
||||
`;
|
||||
|
||||
const resolvers = {
|
||||
Mutation: {
|
||||
setManualSpecialColor: async (
|
||||
_,
|
||||
{ itemId, colorId, supportSecret },
|
||||
{ itemLoader, itemTranslationLoader, colorTranslationLoader, db }
|
||||
) => {
|
||||
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
|
||||
throw new Error(`Support secret is incorrect. Try setting up again?`);
|
||||
}
|
||||
|
||||
const oldItem = await itemLoader.load(itemId);
|
||||
|
||||
const [
|
||||
result,
|
||||
] = await db.execute(
|
||||
`UPDATE items SET manual_special_color_id = ? WHERE id = ? LIMIT 1`,
|
||||
[colorId, itemId]
|
||||
);
|
||||
|
||||
if (result.affectedRows !== 1) {
|
||||
throw new Error(
|
||||
`Expected to affect 1 item, but affected ${result.affectedRows}`
|
||||
);
|
||||
}
|
||||
|
||||
itemLoader.clear(itemId); // we changed the item, so clear it from cache
|
||||
|
||||
if (process.env["SUPPORT_TOOLS_DISCORD_WEBHOOK_URL"]) {
|
||||
try {
|
||||
const [
|
||||
itemTranslation,
|
||||
oldColorTranslation,
|
||||
newColorTranslation,
|
||||
] = await Promise.all([
|
||||
itemTranslationLoader.load(itemId),
|
||||
oldItem.manualSpecialColorId
|
||||
? colorTranslationLoader.load(oldItem.manualSpecialColorId)
|
||||
: Promise.resolve(null),
|
||||
colorId
|
||||
? colorTranslationLoader.load(colorId)
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
|
||||
const oldColorName = oldColorTranslation
|
||||
? capitalize(oldColorTranslation.name)
|
||||
: "Auto-detect";
|
||||
const newColorName = newColorTranslation
|
||||
? capitalize(newColorTranslation.name)
|
||||
: "Auto-detect";
|
||||
await logToDiscord({
|
||||
embeds: [
|
||||
{
|
||||
title: `🛠 ${itemTranslation.name}`,
|
||||
thumbnail: {
|
||||
url: oldItem.thumbnailUrl,
|
||||
height: 80,
|
||||
width: 80,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "Special color",
|
||||
value: `${oldColorName} → **${newColorName}**`,
|
||||
},
|
||||
],
|
||||
timestamp: new Date().toISOString(),
|
||||
url: `https://impress.openneo.net/items/${oldItem.id}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error sending Discord support log", e);
|
||||
}
|
||||
} else {
|
||||
console.warn("No Discord support webhook provided, skipping");
|
||||
}
|
||||
|
||||
return { id: itemId };
|
||||
},
|
||||
|
||||
setItemExplicitlyBodySpecific: async (
|
||||
_,
|
||||
{ itemId, explicitlyBodySpecific, supportSecret },
|
||||
{ itemLoader, itemTranslationLoader, db }
|
||||
) => {
|
||||
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
|
||||
throw new Error(`Support secret is incorrect. Try setting up again?`);
|
||||
}
|
||||
|
||||
const oldItem = await itemLoader.load(itemId);
|
||||
|
||||
const [
|
||||
result,
|
||||
] = await db.execute(
|
||||
`UPDATE items SET explicitly_body_specific = ? WHERE id = ? LIMIT 1`,
|
||||
[explicitlyBodySpecific ? 1 : 0, itemId]
|
||||
);
|
||||
|
||||
if (result.affectedRows !== 1) {
|
||||
throw new Error(
|
||||
`Expected to affect 1 item, but affected ${result.affectedRows}`
|
||||
);
|
||||
}
|
||||
|
||||
itemLoader.clear(itemId); // we changed the item, so clear it from cache
|
||||
|
||||
if (process.env["SUPPORT_TOOLS_DISCORD_WEBHOOK_URL"]) {
|
||||
try {
|
||||
const itemTranslation = await itemTranslationLoader.load(itemId);
|
||||
const oldRuleName = oldItem.explicitlyBodySpecific
|
||||
? "Body specific"
|
||||
: "Auto-detect";
|
||||
const newRuleName = explicitlyBodySpecific
|
||||
? "Body specific"
|
||||
: "Auto-detect";
|
||||
await logToDiscord({
|
||||
embeds: [
|
||||
{
|
||||
title: `🛠 ${itemTranslation.name}`,
|
||||
thumbnail: {
|
||||
url: oldItem.thumbnailUrl,
|
||||
height: 80,
|
||||
width: 80,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "Pet compatibility rule",
|
||||
value: `${oldRuleName} → **${newRuleName}**`,
|
||||
},
|
||||
],
|
||||
timestamp: new Date().toISOString(),
|
||||
url: `https://impress.openneo.net/items/${oldItem.id}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error sending Discord support log", e);
|
||||
}
|
||||
} else {
|
||||
console.warn("No Discord support webhook provided, skipping");
|
||||
}
|
||||
|
||||
return { id: itemId };
|
||||
},
|
||||
|
||||
setLayerBodyId: async (
|
||||
_,
|
||||
{ layerId, bodyId, supportSecret },
|
||||
{
|
||||
itemLoader,
|
||||
itemTranslationLoader,
|
||||
swfAssetLoader,
|
||||
zoneTranslationLoader,
|
||||
db,
|
||||
}
|
||||
) => {
|
||||
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
|
||||
throw new Error(`Support secret is incorrect. Try setting up again?`);
|
||||
}
|
||||
|
||||
const oldSwfAsset = await swfAssetLoader.load(layerId);
|
||||
|
||||
const [
|
||||
result,
|
||||
] = await db.execute(
|
||||
`UPDATE swf_assets SET body_id = ? WHERE id = ? LIMIT 1`,
|
||||
[bodyId, layerId]
|
||||
);
|
||||
|
||||
if (result.affectedRows !== 1) {
|
||||
throw new Error(
|
||||
`Expected to affect 1 layer, but affected ${result.affectedRows}`
|
||||
);
|
||||
}
|
||||
|
||||
swfAssetLoader.clear(layerId); // we changed it, so clear it from cache
|
||||
|
||||
if (process.env["SUPPORT_TOOLS_DISCORD_WEBHOOK_URL"]) {
|
||||
try {
|
||||
const itemId = await db
|
||||
.execute(
|
||||
`SELECT parent_id FROM parents_swf_assets
|
||||
WHERE swf_asset_id = ? AND parent_type = "Item" LIMIT 1;`,
|
||||
[layerId]
|
||||
)
|
||||
.then(([rows]) => normalizeRow(rows[0]).parentId);
|
||||
|
||||
const [
|
||||
item,
|
||||
itemTranslation,
|
||||
zoneTranslation,
|
||||
oldBodyName,
|
||||
newBodyName,
|
||||
] = await Promise.all([
|
||||
itemLoader.load(itemId),
|
||||
itemTranslationLoader.load(itemId),
|
||||
zoneTranslationLoader.load(oldSwfAsset.zoneId),
|
||||
loadBodyName(oldSwfAsset.bodyId, db),
|
||||
loadBodyName(bodyId, db),
|
||||
]);
|
||||
|
||||
await logToDiscord({
|
||||
embeds: [
|
||||
{
|
||||
title: `🛠 ${itemTranslation.name}`,
|
||||
thumbnail: {
|
||||
url: item.thumbnailUrl,
|
||||
height: 80,
|
||||
width: 80,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name:
|
||||
`Layer ${layerId} (${zoneTranslation.label}): ` +
|
||||
`Pet compatibility`,
|
||||
value: `${oldBodyName} → **${newBodyName}**`,
|
||||
},
|
||||
],
|
||||
timestamp: new Date().toISOString(),
|
||||
url: `https://impress.openneo.net/items/${itemId}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error sending Discord support log", e);
|
||||
}
|
||||
} else {
|
||||
console.warn("No Discord support webhook provided, skipping");
|
||||
}
|
||||
|
||||
return { id: layerId };
|
||||
},
|
||||
|
||||
removeLayerFromItem: async (
|
||||
_,
|
||||
{ layerId, itemId, supportSecret },
|
||||
{
|
||||
itemLoader,
|
||||
itemTranslationLoader,
|
||||
swfAssetLoader,
|
||||
zoneTranslationLoader,
|
||||
db,
|
||||
}
|
||||
) => {
|
||||
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
|
||||
throw new Error(`Support secret is incorrect. Try setting up again?`);
|
||||
}
|
||||
|
||||
const oldSwfAsset = await swfAssetLoader.load(layerId);
|
||||
|
||||
const [result] = await db.execute(
|
||||
`DELETE FROM parents_swf_assets ` +
|
||||
`WHERE swf_asset_id = ? AND parent_type = "Item" AND parent_id = ? ` +
|
||||
`LIMIT 1`,
|
||||
[layerId, itemId]
|
||||
);
|
||||
|
||||
if (result.affectedRows !== 1) {
|
||||
throw new Error(
|
||||
`Expected to affect 1 layer, but affected ${result.affectedRows}`
|
||||
);
|
||||
}
|
||||
|
||||
swfAssetLoader.clear(layerId); // we changed it, so clear it from cache
|
||||
|
||||
if (process.env["SUPPORT_TOOLS_DISCORD_WEBHOOK_URL"]) {
|
||||
try {
|
||||
const [
|
||||
item,
|
||||
itemTranslation,
|
||||
zoneTranslation,
|
||||
bodyName,
|
||||
] = await Promise.all([
|
||||
itemLoader.load(itemId),
|
||||
itemTranslationLoader.load(itemId),
|
||||
zoneTranslationLoader.load(oldSwfAsset.zoneId),
|
||||
loadBodyName(oldSwfAsset.bodyId, db),
|
||||
]);
|
||||
|
||||
await logToDiscord({
|
||||
embeds: [
|
||||
{
|
||||
title: `🛠 ${itemTranslation.name}`,
|
||||
thumbnail: {
|
||||
url: item.thumbnailUrl,
|
||||
height: 80,
|
||||
width: 80,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: `Layer ${layerId} (${zoneTranslation.label})`,
|
||||
value: `❌ Removed from ${bodyName}`,
|
||||
},
|
||||
],
|
||||
timestamp: new Date().toISOString(),
|
||||
url: `https://impress.openneo.net/items/${itemId}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error sending Discord support log", e);
|
||||
}
|
||||
} else {
|
||||
console.warn("No Discord support webhook provided, skipping");
|
||||
}
|
||||
|
||||
return { layer: { id: layerId }, item: { id: itemId } };
|
||||
},
|
||||
|
||||
setPetAppearancePose: async (
|
||||
_,
|
||||
{ appearanceId, pose, supportSecret },
|
||||
{
|
||||
colorTranslationLoader,
|
||||
speciesTranslationLoader,
|
||||
petStateLoader,
|
||||
petTypeLoader,
|
||||
db,
|
||||
}
|
||||
) => {
|
||||
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
|
||||
throw new Error(`Support secret is incorrect. Try setting up again?`);
|
||||
}
|
||||
|
||||
const oldPetState = await petStateLoader.load(appearanceId);
|
||||
|
||||
const { moodId, female, unconverted } = getPetStateFieldsFromPose(pose);
|
||||
|
||||
const [result] = await db.execute(
|
||||
`UPDATE pet_states SET mood_id = ?, female = ?, unconverted = ?
|
||||
WHERE id = ? LIMIT 1`,
|
||||
[moodId, female, unconverted, appearanceId]
|
||||
);
|
||||
|
||||
if (result.affectedRows !== 1) {
|
||||
throw new Error(
|
||||
`Expected to affect 1 layer, but affected ${result.affectedRows}`
|
||||
);
|
||||
}
|
||||
|
||||
// we changed it, so clear it from cache
|
||||
petStateLoader.clear(appearanceId);
|
||||
|
||||
if (process.env["SUPPORT_TOOLS_DISCORD_WEBHOOK_URL"]) {
|
||||
try {
|
||||
const petType = await petTypeLoader.load(oldPetState.petTypeId);
|
||||
const [colorTranslation, speciesTranslation] = await Promise.all([
|
||||
colorTranslationLoader.load(petType.colorId),
|
||||
speciesTranslationLoader.load(petType.speciesId),
|
||||
]);
|
||||
|
||||
const oldPose = getPoseFromPetState(oldPetState);
|
||||
const colorName = capitalize(colorTranslation.name);
|
||||
const speciesName = capitalize(speciesTranslation.name);
|
||||
|
||||
await logToDiscord({
|
||||
embeds: [
|
||||
{
|
||||
title: `🛠 ${colorName} ${speciesName}`,
|
||||
thumbnail: {
|
||||
url: `http://pets.neopets.com/cp/${
|
||||
petType.basicImageHash || petType.imageHash
|
||||
}/1/6.png`,
|
||||
height: 150,
|
||||
width: 150,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: `Appearance ${appearanceId}: Pose`,
|
||||
value: `${getPoseName(oldPose)} → **${getPoseName(pose)}**`,
|
||||
},
|
||||
{
|
||||
name: "As a reminder…",
|
||||
value: "…the thumbnail might not match!",
|
||||
},
|
||||
],
|
||||
timestamp: new Date().toISOString(),
|
||||
url: `https://impress-2020.openneo.net/outfits/new?species=${petType.speciesId}&color=${petType.colorId}&pose=${pose}&state=${appearanceId}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error sending Discord support log", e);
|
||||
}
|
||||
} else {
|
||||
console.warn("No Discord support webhook provided, skipping");
|
||||
}
|
||||
|
||||
return { id: appearanceId };
|
||||
},
|
||||
|
||||
setPetAppearanceIsGlitched: async (
|
||||
_,
|
||||
{ appearanceId, isGlitched, supportSecret },
|
||||
{
|
||||
colorTranslationLoader,
|
||||
speciesTranslationLoader,
|
||||
petStateLoader,
|
||||
petTypeLoader,
|
||||
db,
|
||||
}
|
||||
) => {
|
||||
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
|
||||
throw new Error(`Support secret is incorrect. Try setting up again?`);
|
||||
}
|
||||
|
||||
const oldPetState = await petStateLoader.load(appearanceId);
|
||||
|
||||
const [
|
||||
result,
|
||||
] = await db.execute(
|
||||
`UPDATE pet_states SET glitched = ? WHERE id = ? LIMIT 1`,
|
||||
[isGlitched, appearanceId]
|
||||
);
|
||||
|
||||
if (result.affectedRows !== 1) {
|
||||
throw new Error(
|
||||
`Expected to affect 1 layer, but affected ${result.affectedRows}`
|
||||
);
|
||||
}
|
||||
|
||||
// we changed it, so clear it from cache
|
||||
petStateLoader.clear(appearanceId);
|
||||
|
||||
if (process.env["SUPPORT_TOOLS_DISCORD_WEBHOOK_URL"]) {
|
||||
try {
|
||||
const petType = await petTypeLoader.load(oldPetState.petTypeId);
|
||||
const [colorTranslation, speciesTranslation] = await Promise.all([
|
||||
colorTranslationLoader.load(petType.colorId),
|
||||
speciesTranslationLoader.load(petType.speciesId),
|
||||
]);
|
||||
|
||||
const colorName = capitalize(colorTranslation.name);
|
||||
const speciesName = capitalize(speciesTranslation.name);
|
||||
|
||||
const pose = getPoseFromPetState(oldPetState);
|
||||
const oldGlitchinessState =
|
||||
String(oldPetState.glitched) === "1" ? "Glitched" : "Valid";
|
||||
const newGlitchinessState = isGlitched ? "Glitched" : "Valid";
|
||||
|
||||
await logToDiscord({
|
||||
embeds: [
|
||||
{
|
||||
title: `🛠 ${colorName} ${speciesName}`,
|
||||
thumbnail: {
|
||||
url: `http://pets.neopets.com/cp/${
|
||||
petType.basicImageHash || petType.imageHash
|
||||
}/1/6.png`,
|
||||
height: 150,
|
||||
width: 150,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: `Appearance ${appearanceId}`,
|
||||
value: `${oldGlitchinessState} → **${newGlitchinessState}**`,
|
||||
},
|
||||
],
|
||||
timestamp: new Date().toISOString(),
|
||||
url: `https://impress-2020.openneo.net/outfits/new?species=${petType.speciesId}&color=${petType.colorId}&pose=${pose}&state=${appearanceId}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error sending Discord support log", e);
|
||||
}
|
||||
} else {
|
||||
console.warn("No Discord support webhook provided, skipping");
|
||||
}
|
||||
|
||||
return { id: appearanceId };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { typeDefs, resolvers };
|
134
src/server/types/Outfit.js
Normal file
134
src/server/types/Outfit.js
Normal file
|
@ -0,0 +1,134 @@
|
|||
const fetch = require("node-fetch");
|
||||
const { gql } = require("apollo-server");
|
||||
|
||||
const typeDefs = gql`
|
||||
type Outfit {
|
||||
id: ID!
|
||||
name: String!
|
||||
petAppearance: PetAppearance!
|
||||
wornItems: [Item!]!
|
||||
closetedItems: [Item!]!
|
||||
|
||||
species: Species! # to be deprecated? can use petAppearance? 🤔
|
||||
color: Color! # to be deprecated? can use petAppearance? 🤔
|
||||
pose: Pose! # to be deprecated? can use petAppearance? 🤔
|
||||
items: [Item!]! # deprecated alias for wornItems
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
outfit(id: ID!): Outfit
|
||||
petOnNeopetsDotCom(petName: String!): Outfit
|
||||
}
|
||||
`;
|
||||
|
||||
const resolvers = {
|
||||
Outfit: {
|
||||
name: async ({ id }, _, { outfitLoader }) => {
|
||||
const outfit = await outfitLoader.load(id);
|
||||
return outfit.name;
|
||||
},
|
||||
petAppearance: async ({ id }, _, { outfitLoader }) => {
|
||||
const outfit = await outfitLoader.load(id);
|
||||
return { id: outfit.petStateId };
|
||||
},
|
||||
wornItems: async ({ id }, _, { itemOutfitRelationshipsLoader }) => {
|
||||
const relationships = await itemOutfitRelationshipsLoader.load(id);
|
||||
return relationships
|
||||
.filter((oir) => oir.isWorn)
|
||||
.map((oir) => ({ id: oir.itemId }));
|
||||
},
|
||||
closetedItems: async ({ id }, _, { itemOutfitRelationshipsLoader }) => {
|
||||
const relationships = await itemOutfitRelationshipsLoader.load(id);
|
||||
return relationships
|
||||
.filter((oir) => !oir.isWorn)
|
||||
.map((oir) => ({ id: oir.itemId }));
|
||||
},
|
||||
},
|
||||
Query: {
|
||||
outfit: (_, { id }) => ({ id }),
|
||||
petOnNeopetsDotCom: async (_, { petName }) => {
|
||||
const [petMetaData, customPetData] = await Promise.all([
|
||||
loadPetMetaData(petName),
|
||||
loadCustomPetData(petName),
|
||||
]);
|
||||
const outfit = {
|
||||
// TODO: This isn't a fully-working Outfit object. It works for the
|
||||
// client as currently implemented, but we'll probably want to
|
||||
// move the client and this onto our more generic fields!
|
||||
species: { id: customPetData.custom_pet.species_id },
|
||||
color: { id: customPetData.custom_pet.color_id },
|
||||
pose: getPoseFromPetData(petMetaData, customPetData),
|
||||
items: Object.values(customPetData.object_info_registry).map((o) => ({
|
||||
id: o.obj_info_id,
|
||||
name: o.name,
|
||||
description: o.description,
|
||||
thumbnailUrl: o.thumbnail_url,
|
||||
rarityIndex: o.rarity_index,
|
||||
})),
|
||||
};
|
||||
return outfit;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async function loadPetMetaData(petName) {
|
||||
const url =
|
||||
`http://www.neopets.com/amfphp/json.php/PetService.getPet` + `/${petName}`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`for pet meta data, neopets.com returned: ` +
|
||||
`${res.status} ${res.statusText}. (${url})`
|
||||
);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json;
|
||||
}
|
||||
|
||||
async function loadCustomPetData(petName) {
|
||||
const url =
|
||||
`http://www.neopets.com/amfphp/json.php/CustomPetService.getViewerData` +
|
||||
`/${petName}`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`for custom pet data, neopets.com returned: ` +
|
||||
`${res.status} ${res.statusText}. (${url})`
|
||||
);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
if (!json.custom_pet) {
|
||||
throw new Error(`missing custom_pet data`);
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
function getPoseFromPetData(petMetaData, petCustomData) {
|
||||
// TODO: Use custom data to decide if Unconverted.
|
||||
const moodId = petMetaData.mood;
|
||||
const genderId = petMetaData.gender;
|
||||
if (String(moodId) === "1" && String(genderId) === "1") {
|
||||
return "HAPPY_MASC";
|
||||
} else if (String(moodId) === "1" && String(genderId) === "2") {
|
||||
return "HAPPY_FEM";
|
||||
} else if (String(moodId) === "2" && String(genderId) === "1") {
|
||||
return "SAD_MASC";
|
||||
} else if (String(moodId) === "2" && String(genderId) === "2") {
|
||||
return "SAD_FEM";
|
||||
} else if (String(moodId) === "4" && String(genderId) === "1") {
|
||||
return "SICK_MASC";
|
||||
} else if (String(moodId) === "4" && String(genderId) === "2") {
|
||||
return "SICK_FEM";
|
||||
} else {
|
||||
throw new Error(
|
||||
`could not identify pose: ` +
|
||||
`moodId=${moodId}, ` +
|
||||
`genderId=${genderId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { typeDefs, resolvers };
|
206
src/server/types/PetAppearance.js
Normal file
206
src/server/types/PetAppearance.js
Normal file
|
@ -0,0 +1,206 @@
|
|||
const { gql } = require("apollo-server");
|
||||
const {
|
||||
capitalize,
|
||||
getPoseFromPetState,
|
||||
getRestrictedZoneIds,
|
||||
} = require("../util");
|
||||
|
||||
const typeDefs = gql`
|
||||
# Cache for 1 week (unlikely to change)
|
||||
type Color @cacheControl(maxAge: 604800) {
|
||||
id: ID!
|
||||
name: String!
|
||||
isStandard: Boolean!
|
||||
}
|
||||
|
||||
# Cache for 1 week (unlikely to change)
|
||||
type Species @cacheControl(maxAge: 604800) {
|
||||
id: ID!
|
||||
name: String!
|
||||
|
||||
# The bodyId for PetAppearances that use this species and a standard color.
|
||||
# We use this to preload the standard body IDs, so that items stay when
|
||||
# switching between standard colors.
|
||||
standardBodyId: ID!
|
||||
}
|
||||
|
||||
"""
|
||||
The poses a PetAppearance can take!
|
||||
"""
|
||||
enum Pose {
|
||||
HAPPY_MASC
|
||||
SAD_MASC
|
||||
SICK_MASC
|
||||
HAPPY_FEM
|
||||
SAD_FEM
|
||||
SICK_FEM
|
||||
UNCONVERTED
|
||||
UNKNOWN # for when we have the data, but we don't know what it is
|
||||
}
|
||||
|
||||
# Cache for 1 week (unlikely to change)
|
||||
type PetAppearance @cacheControl(maxAge: 604800) {
|
||||
id: ID!
|
||||
species: Species!
|
||||
color: Color!
|
||||
pose: Pose!
|
||||
bodyId: ID!
|
||||
|
||||
layers: [AppearanceLayer!]!
|
||||
restrictedZones: [Zone!]!
|
||||
|
||||
petStateId: ID! # Deprecated, an alias for id
|
||||
# Whether this PetAppearance is known to look incorrect. This is a manual
|
||||
# flag that we set, in the case where this glitchy PetAppearance really did
|
||||
# appear on Neopets.com, and has since been fixed.
|
||||
isGlitched: Boolean!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
color(id: ID!): Color
|
||||
allColors: [Color!]! @cacheControl(maxAge: 10800) # Cache for 3 hours (we might add more!)
|
||||
species(id: ID!): Species
|
||||
allSpecies: [Species!]! @cacheControl(maxAge: 10800) # Cache for 3 hours (we might add more!)
|
||||
petAppearanceById(id: ID!): PetAppearance @cacheControl(maxAge: 10800) # Cache for 3 hours (Support might edit!)
|
||||
# The canonical pet appearance for the given species, color, and pose.
|
||||
# Null if we don't have any data for this combination.
|
||||
petAppearance(speciesId: ID!, colorId: ID!, pose: Pose!): PetAppearance
|
||||
@cacheControl(maxAge: 10800) # Cache for 3 hours (we might model more!)
|
||||
# All pet appearances we've ever seen for the given species and color. Note
|
||||
# that this might include multiple copies for the same pose, and they might
|
||||
# even be glitched data. We use this for Support tools, and we don't cache
|
||||
# it to make sure that Support users are always seeing the most up-to-date
|
||||
# version here (even if the standard pose picker is still showing outdated
|
||||
# cached canonical poses).
|
||||
petAppearances(speciesId: ID!, colorId: ID!): [PetAppearance!]!
|
||||
}
|
||||
`;
|
||||
|
||||
const resolvers = {
|
||||
Color: {
|
||||
name: async ({ id }, _, { colorTranslationLoader }) => {
|
||||
const colorTranslation = await colorTranslationLoader.load(id);
|
||||
return capitalize(colorTranslation.name);
|
||||
},
|
||||
isStandard: async ({ id }, _, { colorLoader }) => {
|
||||
const color = await colorLoader.load(id);
|
||||
return color.standard ? true : false;
|
||||
},
|
||||
},
|
||||
|
||||
Species: {
|
||||
name: async ({ id }, _, { speciesTranslationLoader }) => {
|
||||
const speciesTranslation = await speciesTranslationLoader.load(id);
|
||||
return capitalize(speciesTranslation.name);
|
||||
},
|
||||
standardBodyId: async ({ id }, _, { petTypeBySpeciesAndColorLoader }) => {
|
||||
const petType = await petTypeBySpeciesAndColorLoader.load({
|
||||
speciesId: id,
|
||||
colorId: "8", // Blue
|
||||
});
|
||||
return petType.bodyId;
|
||||
},
|
||||
},
|
||||
|
||||
PetAppearance: {
|
||||
color: async ({ id }, _, { petStateLoader, petTypeLoader }) => {
|
||||
const petState = await petStateLoader.load(id);
|
||||
const petType = await petTypeLoader.load(petState.petTypeId);
|
||||
return { id: petType.colorId };
|
||||
},
|
||||
species: async ({ id }, _, { petStateLoader, petTypeLoader }) => {
|
||||
const petState = await petStateLoader.load(id);
|
||||
const petType = await petTypeLoader.load(petState.petTypeId);
|
||||
return { id: petType.speciesId };
|
||||
},
|
||||
bodyId: async ({ id }, _, { petStateLoader, petTypeLoader }) => {
|
||||
const petState = await petStateLoader.load(id);
|
||||
const petType = await petTypeLoader.load(petState.petTypeId);
|
||||
return petType.bodyId;
|
||||
},
|
||||
pose: async ({ id }, _, { petStateLoader }) => {
|
||||
const petState = await petStateLoader.load(id);
|
||||
return getPoseFromPetState(petState);
|
||||
},
|
||||
layers: async ({ id }, _, { petSwfAssetLoader }) => {
|
||||
const swfAssets = await petSwfAssetLoader.load(id);
|
||||
return swfAssets;
|
||||
},
|
||||
restrictedZones: async ({ id }, _, { petSwfAssetLoader }) => {
|
||||
// The restricted zones are defined on the layers. Load them and aggegate
|
||||
// the zones, then uniquify and sort them for ease of use.
|
||||
const swfAssets = await petSwfAssetLoader.load(id);
|
||||
let restrictedZoneIds = swfAssets
|
||||
.map((sa) => getRestrictedZoneIds(sa.zonesRestrict))
|
||||
.flat();
|
||||
restrictedZoneIds = [...new Set(restrictedZoneIds)];
|
||||
restrictedZoneIds.sort((a, b) => parseInt(a) - parseInt(b));
|
||||
return restrictedZoneIds.map((id) => ({ id }));
|
||||
},
|
||||
petStateId: ({ id }) => id,
|
||||
isGlitched: async ({ id }, _, { petStateLoader }) => {
|
||||
const petState = await petStateLoader.load(id);
|
||||
return petState.glitched;
|
||||
},
|
||||
},
|
||||
|
||||
Query: {
|
||||
color: async (_, { id }, { colorLoader }) => {
|
||||
const color = await colorLoader.load(id);
|
||||
if (!color) {
|
||||
return null;
|
||||
}
|
||||
return { id };
|
||||
},
|
||||
allColors: async (_, __, { colorLoader }) => {
|
||||
const allColors = await colorLoader.loadAll();
|
||||
return allColors;
|
||||
},
|
||||
species: async (_, { id }, { speciesLoader }) => {
|
||||
const species = await speciesLoader.load(id);
|
||||
if (!species) {
|
||||
return null;
|
||||
}
|
||||
return { id };
|
||||
},
|
||||
allSpecies: async (_, __, { speciesLoader }) => {
|
||||
const allSpecies = await speciesLoader.loadAll();
|
||||
return allSpecies;
|
||||
},
|
||||
petAppearanceById: (_, { id }) => ({ id }),
|
||||
petAppearance: async (
|
||||
_,
|
||||
{ speciesId, colorId, pose },
|
||||
{ petTypeBySpeciesAndColorLoader, petStatesForPetTypeLoader }
|
||||
) => {
|
||||
const petType = await petTypeBySpeciesAndColorLoader.load({
|
||||
speciesId,
|
||||
colorId,
|
||||
});
|
||||
|
||||
// TODO: We could query for this more directly, instead of loading all
|
||||
// appearances 🤔
|
||||
const petStates = await petStatesForPetTypeLoader.load(petType.id);
|
||||
const petState = petStates.find((ps) => getPoseFromPetState(ps) === pose);
|
||||
if (!petState) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { id: petState.id };
|
||||
},
|
||||
petAppearances: async (
|
||||
_,
|
||||
{ speciesId, colorId },
|
||||
{ petTypeBySpeciesAndColorLoader, petStatesForPetTypeLoader }
|
||||
) => {
|
||||
const petType = await petTypeBySpeciesAndColorLoader.load({
|
||||
speciesId,
|
||||
colorId,
|
||||
});
|
||||
const petStates = await petStatesForPetTypeLoader.load(petType.id);
|
||||
return petStates.map((petState) => ({ id: petState.id }));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { typeDefs, resolvers };
|
83
src/server/types/User.js
Normal file
83
src/server/types/User.js
Normal file
|
@ -0,0 +1,83 @@
|
|||
const { gql } = require("apollo-server");
|
||||
|
||||
const typeDefs = gql`
|
||||
type User {
|
||||
id: ID!
|
||||
username: String!
|
||||
itemsTheyOwn: [Item!]!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
user(id: ID!): User
|
||||
currentUser: User
|
||||
}
|
||||
`;
|
||||
|
||||
const resolvers = {
|
||||
User: {
|
||||
username: async ({ id }, _, { userLoader }) => {
|
||||
const user = await userLoader.load(id);
|
||||
return user.name;
|
||||
},
|
||||
itemsTheyOwn: async (
|
||||
{ id },
|
||||
_,
|
||||
{ currentUserId, userLoader, userOwnedClosetHangersLoader }
|
||||
) => {
|
||||
const user = await userLoader.load(id);
|
||||
const hangersAreVisible =
|
||||
user.ownedClosetHangersVisibility >= 2 || user.id === currentUserId;
|
||||
if (!hangersAreVisible) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allClosetHangers = await userOwnedClosetHangersLoader.load(id);
|
||||
const closetHangersWithNoList = allClosetHangers.filter(
|
||||
(h) => h.listId == null
|
||||
);
|
||||
|
||||
const items = closetHangersWithNoList.map((h) => ({
|
||||
id: h.itemId,
|
||||
// We get this for the ORDER BY clause anyway - may as well include it
|
||||
// here to avoid an extra lookup!
|
||||
name: h.itemName,
|
||||
}));
|
||||
return items;
|
||||
},
|
||||
},
|
||||
|
||||
Query: {
|
||||
user: async (_, { id }, { userLoader }) => {
|
||||
try {
|
||||
await userLoader.load(id);
|
||||
} catch (e) {
|
||||
if (e.message.includes("could not find user")) {
|
||||
return null;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return { id };
|
||||
},
|
||||
currentUser: async (_, __, { currentUserId, userLoader }) => {
|
||||
if (currentUserId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
await userLoader.load(currentUserId);
|
||||
} catch (e) {
|
||||
if (e.message.includes("could not find user")) {
|
||||
return null;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
return { id: currentUserId };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { typeDefs, resolvers };
|
49
src/server/types/Zone.js
Normal file
49
src/server/types/Zone.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
const { gql } = require("apollo-server");
|
||||
|
||||
const typeDefs = gql`
|
||||
# Cache for 1 week (unlikely to change)
|
||||
type Zone @cacheControl(maxAge: 604800) {
|
||||
id: ID!
|
||||
depth: Int!
|
||||
label: String!
|
||||
isCommonlyUsedByItems: Boolean!
|
||||
}
|
||||
|
||||
extend type Query {
|
||||
allZones: [Zone!]!
|
||||
}
|
||||
`;
|
||||
|
||||
const resolvers = {
|
||||
Zone: {
|
||||
depth: async ({ id }, _, { zoneLoader }) => {
|
||||
// TODO: Should we extend this loader-in-field pattern elsewhere? I like
|
||||
// that we avoid the fetch in cases where we only want the zone ID,
|
||||
// but it adds complexity 🤔
|
||||
const zone = await zoneLoader.load(id);
|
||||
return zone.depth;
|
||||
},
|
||||
label: async ({ id }, _, { zoneTranslationLoader }) => {
|
||||
const zoneTranslation = await zoneTranslationLoader.load(id);
|
||||
return zoneTranslation.label;
|
||||
},
|
||||
isCommonlyUsedByItems: async ({ id }, _, { zoneLoader }) => {
|
||||
// Zone metadata marks item zones with types 2, 3, and 4. But also, in
|
||||
// practice, the Biology Effects zone (type 1) has been used for a few
|
||||
// items too. So, that's what we return true for!
|
||||
const zone = await zoneLoader.load(id);
|
||||
const isMarkedForItems = ["2", "3", "4"].includes(zone.typeId);
|
||||
const isBiologyEffects = zone.id === "4";
|
||||
return isMarkedForItems || isBiologyEffects;
|
||||
},
|
||||
},
|
||||
|
||||
Query: {
|
||||
allZones: async (_, __, { zoneLoader }) => {
|
||||
const zones = await zoneLoader.loadAll();
|
||||
return zones.map(({ id }) => ({ id }));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = { typeDefs, resolvers };
|
|
@ -5,32 +5,6 @@ function capitalize(str) {
|
|||
return str[0].toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
function getEmotion(pose) {
|
||||
if (["HAPPY_MASC", "HAPPY_FEM"].includes(pose)) {
|
||||
return "HAPPY";
|
||||
} else if (["SAD_MASC", "SAD_FEM"].includes(pose)) {
|
||||
return "SAD";
|
||||
} else if (["SICK_MASC", "SICK_FEM"].includes(pose)) {
|
||||
return "SICK";
|
||||
} else if (["UNCONVERTED", "UNKNOWN"].includes(pose)) {
|
||||
return null;
|
||||
} else {
|
||||
throw new Error(`unrecognized pose ${JSON.stringify(pose)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getGenderPresentation(pose) {
|
||||
if (["HAPPY_MASC", "SAD_MASC", "SICK_MASC"].includes(pose)) {
|
||||
return "MASC";
|
||||
} else if (["HAPPY_FEM", "SAD_FEM", "SICK_FEM"].includes(pose)) {
|
||||
return "FEM";
|
||||
} else if (["UNCONVERTED", "UNKNOWN"].includes(pose)) {
|
||||
return null;
|
||||
} else {
|
||||
throw new Error(`unrecognized pose ${JSON.stringify(pose)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getPoseFromPetState(petState) {
|
||||
const { moodId, female, unconverted } = petState;
|
||||
|
||||
|
@ -82,31 +56,6 @@ function getPetStateFieldsFromPose(pose) {
|
|||
}
|
||||
}
|
||||
|
||||
function getPoseFromPetData(petMetaData, petCustomData) {
|
||||
// TODO: Use custom data to decide if Unconverted.
|
||||
const moodId = petMetaData.mood;
|
||||
const genderId = petMetaData.gender;
|
||||
if (String(moodId) === "1" && String(genderId) === "1") {
|
||||
return "HAPPY_MASC";
|
||||
} else if (String(moodId) === "1" && String(genderId) === "2") {
|
||||
return "HAPPY_FEM";
|
||||
} else if (String(moodId) === "2" && String(genderId) === "1") {
|
||||
return "SAD_MASC";
|
||||
} else if (String(moodId) === "2" && String(genderId) === "2") {
|
||||
return "SAD_FEM";
|
||||
} else if (String(moodId) === "4" && String(genderId) === "1") {
|
||||
return "SICK_MASC";
|
||||
} else if (String(moodId) === "4" && String(genderId) === "2") {
|
||||
return "SICK_FEM";
|
||||
} else {
|
||||
throw new Error(
|
||||
`could not identify pose: ` +
|
||||
`moodId=${moodId}, ` +
|
||||
`genderId=${genderId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const POSE_NAMES = {
|
||||
HAPPY_MASC: "Happy Masc",
|
||||
SAD_MASC: "Sad Masc",
|
||||
|
@ -192,11 +141,8 @@ function normalizeRow(row) {
|
|||
|
||||
module.exports = {
|
||||
capitalize,
|
||||
getEmotion,
|
||||
getGenderPresentation,
|
||||
getPoseFromPetState,
|
||||
getPetStateFieldsFromPose,
|
||||
getPoseFromPetData,
|
||||
getPoseName,
|
||||
getRestrictedZoneIds,
|
||||
loadBodyName,
|
||||
|
|
Loading…
Reference in a new issue