impress-2020/src/server/index.js

344 lines
8.6 KiB
JavaScript
Raw Normal View History

const { gql } = require("apollo-server");
2020-04-22 11:51:36 -07:00
const connectToDb = require("./db");
const buildLoaders = require("./loaders");
const neopets = require("./neopets");
2020-04-25 04:33:05 -07:00
const { capitalize } = require("./util");
2020-04-22 11:51:36 -07:00
const typeDefs = gql`
2020-04-23 01:08:00 -07:00
enum LayerImageSize {
SIZE_600
SIZE_300
SIZE_150
}
"""
A pet's gender presentation: masculine or feminine.
Neopets calls these "male" and "female", and I think that's silly and not wise
to propagate further, especially in the context of a strictly visual app like
Dress to Impress! This description isn't altogether correct either, but idk
what's better :/
"""
enum GenderPresentation {
MASCULINE
FEMININE
}
"""
A pet's emotion: happy, sad, or sick.
Note that we don't ever show the angry emotion on Dress to Impress, because
we don't have the data: it's impossible for a pet's passive emotion on the
pet lookup to be angry!
"""
enum Emotion {
HAPPY
SAD
SICK
}
2020-04-22 11:51:36 -07:00
type Item {
id: ID!
name: String!
description: String!
2020-04-22 14:55:12 -07:00
thumbnailUrl: String!
appearanceOn(speciesId: ID!, colorId: ID!): ItemAppearance
2020-04-23 01:08:00 -07:00
}
type PetAppearance {
genderPresentation: GenderPresentation
emotion: Emotion
layers: [AppearanceLayer!]!
}
type ItemAppearance {
layers: [AppearanceLayer!]!
2020-04-23 14:44:06 -07:00
restrictedZones: [Zone!]!
2020-04-23 01:08:00 -07:00
}
type AppearanceLayer {
2020-04-23 01:08:00 -07:00
id: ID!
zone: Zone!
imageUrl(size: LayerImageSize): String
}
type Zone {
id: ID!
depth: Int!
label: String!
2020-04-22 11:51:36 -07:00
}
2020-04-25 01:55:48 -07:00
type ItemSearchResult {
query: String!
items: [Item!]!
}
2020-04-25 03:42:05 -07:00
type Color {
id: ID!
name: String!
}
type Species {
id: ID!
name: String!
}
type SpeciesColorPair {
species: Species!
color: Color!
}
type Outfit {
species: Species!
color: Color!
items: [Item!]!
}
2020-04-22 11:51:36 -07:00
type Query {
2020-04-25 03:42:05 -07:00
allColors: [Color!]!
allSpecies: [Species!]!
allValidSpeciesColorPairs: [SpeciesColorPair!]!
2020-04-22 11:51:36 -07:00
items(ids: [ID!]!): [Item!]!
2020-04-25 01:55:48 -07:00
itemSearch(query: String!): ItemSearchResult!
itemSearchToFit(
query: String!
speciesId: ID!
colorId: ID!
offset: Int
limit: Int
): ItemSearchResult!
petAppearance(speciesId: ID!, colorId: ID!): PetAppearance
petAppearances(speciesId: ID!, colorId: ID!): [PetAppearance!]!
petOnNeopetsDotCom(petName: String!): Outfit
2020-04-22 11:51:36 -07:00
}
`;
const resolvers = {
Item: {
name: async (item, _, { itemTranslationLoader }) => {
// Search queries pre-fill this!
if (item.name) return item.name;
const translation = await itemTranslationLoader.load(item.id);
2020-04-22 11:51:36 -07:00
return translation.name;
},
description: async (item, _, { itemTranslationLoader }) => {
const translation = await itemTranslationLoader.load(item.id);
return translation.description;
},
appearanceOn: async (
item,
{ speciesId, colorId },
{ petTypeLoader, itemSwfAssetLoader }
) => {
2020-04-23 01:08:00 -07:00
const petType = await petTypeLoader.load({
speciesId: speciesId,
colorId: colorId,
2020-04-23 01:08:00 -07:00
});
const swfAssets = await itemSwfAssetLoader.load({
itemId: item.id,
2020-04-23 01:08:00 -07:00
bodyId: petType.bodyId,
});
2020-04-23 14:44:06 -07:00
if (swfAssets.length === 0) {
return null;
}
2020-04-23 14:44:06 -07:00
const restrictedZones = [];
for (const [i, bit] of Array.from(item.zonesRestrict).entries()) {
if (bit === "1") {
const zone = { id: i + 1 };
restrictedZones.push(zone);
}
}
return { layers: swfAssets, restrictedZones };
2020-04-23 01:08:00 -07:00
},
},
PetAppearance: {
genderPresentation: ({ petState }) => {
if (petState.female === 1) {
return "FEMININE";
} else if (petState.female === 0) {
return "MASCULINE";
} else if (petState.female === null) {
return null;
} else {
throw new Error(
`unrecognized gender value ${JSON.stringify(petState.female)}`
);
}
},
emotion: ({ petState }) => {
if (petState.moodId === "1") {
return "HAPPY";
} else if (petState.moodId === "2") {
return "SAD";
} else if (petState.moodId === "4") {
return "SICK";
} else if (petState.moodId === null) {
return null;
} else {
throw new Error(
`unrecognized moodId ${JSON.stringify(petState.moodId)}`
);
}
},
layers: async ({ petState }, _, { petSwfAssetLoader }) => {
const swfAssets = await petSwfAssetLoader.load(petState.id);
return swfAssets;
},
},
AppearanceLayer: {
2020-04-23 01:08:00 -07:00
zone: async (layer, _, { zoneLoader }) => {
const zone = await zoneLoader.load(layer.zoneId);
return zone;
},
imageUrl: (layer, { size }) => {
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}`
);
2020-04-23 01:08:00 -07:00
},
},
Zone: {
label: async (zone, _, { zoneTranslationLoader }) => {
const zoneTranslation = await zoneTranslationLoader.load(zone.id);
return zoneTranslation.label;
},
2020-04-22 11:51:36 -07:00
},
2020-04-25 03:42:05 -07:00
Color: {
name: async (color, _, { colorTranslationLoader }) => {
const colorTranslation = await colorTranslationLoader.load(color.id);
2020-04-25 04:33:05 -07:00
return capitalize(colorTranslation.name);
2020-04-25 03:42:05 -07:00
},
},
Species: {
name: async (species, _, { speciesTranslationLoader }) => {
const speciesTranslation = await speciesTranslationLoader.load(
species.id
);
2020-04-25 04:33:05 -07:00
return capitalize(speciesTranslation.name);
2020-04-25 03:42:05 -07:00
},
},
2020-04-22 11:51:36 -07:00
Query: {
2020-04-25 03:42:05 -07:00
allColors: async (_, { ids }, { loadAllColors }) => {
const allColors = await loadAllColors();
return allColors;
},
allSpecies: async (_, { ids }, { loadAllSpecies }) => {
const allSpecies = await loadAllSpecies();
return allSpecies;
},
allValidSpeciesColorPairs: async (_, __, { loadAllPetTypes }) => {
const allPetTypes = await loadAllPetTypes();
const allPairs = allPetTypes.map((pt) => ({
color: { id: pt.colorId },
species: { id: pt.speciesId },
}));
return allPairs;
},
items: async (_, { ids }, { itemLoader }) => {
const items = await itemLoader.loadMany(ids);
2020-04-23 01:08:00 -07:00
return items;
2020-04-24 21:17:03 -07:00
},
itemSearch: async (_, { query }, { itemSearchLoader }) => {
const items = await itemSearchLoader.load(query);
2020-04-25 01:55:48 -07:00
return { query, items };
},
itemSearchToFit: async (
_,
2020-04-25 01:55:48 -07:00
{ query, speciesId, colorId, offset, limit },
{ petTypeLoader, itemSearchToFitLoader }
) => {
const petType = await petTypeLoader.load({ speciesId, colorId });
const { bodyId } = petType;
2020-04-25 01:55:48 -07:00
const items = await itemSearchToFitLoader.load({
query,
bodyId,
offset,
limit,
});
return { query, items };
2020-04-23 01:08:00 -07:00
},
petAppearance: async (
_,
{ speciesId, colorId },
{ petTypeLoader, petStateLoader, petSwfAssetLoader }
) => {
const petType = await petTypeLoader.load({
speciesId,
colorId,
});
const petStates = await petStateLoader.load(petType.id);
return { petState: petStates[0] };
},
petAppearances: async (
_,
{ speciesId, colorId },
{ petTypeLoader, petStateLoader, petSwfAssetLoader }
) => {
const petType = await petTypeLoader.load({
speciesId,
colorId,
});
const petStates = await petStateLoader.load(petType.id);
return petStates.map((petState) => ({ petState }));
},
petOnNeopetsDotCom: async (_, { petName }) => {
const petData = await neopets.loadPetData(petName);
const outfit = {
species: { id: petData.custom_pet.species_id },
color: { id: petData.custom_pet.color_id },
items: Object.values(petData.object_info_registry).map((o) => ({
id: o.obj_info_id,
})),
};
return outfit;
},
2020-04-22 11:51:36 -07:00
},
};
const config = {
2020-04-22 11:51:36 -07:00
typeDefs,
resolvers,
context: async () => {
const db = await connectToDb();
return {
...buildLoaders(db),
};
2020-04-22 11:51:36 -07:00
},
2020-04-23 01:09:17 -07:00
// Enable Playground in production :)
introspection: true,
playground: {
endpoint: "/api/graphql",
},
};
2020-04-22 11:51:36 -07:00
if (require.main === module) {
const { ApolloServer } = require("apollo-server");
const server = new ApolloServer(config);
2020-04-22 11:51:36 -07:00
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
}
module.exports = { config };