impress-2020/src/server/index.js

752 lines
23 KiB
JavaScript
Raw Normal View History

const { gql, makeExecutableSchema } = require("apollo-server");
const { addBeelineToSchema, beelinePlugin } = require("./lib/beeline-graphql");
2020-04-22 11:51:36 -07:00
const connectToDb = require("./db");
const buildLoaders = require("./loaders");
const neopets = require("./neopets");
2020-05-23 12:47:06 -07:00
const {
capitalize,
getPoseFromPetState,
getPoseFromPetData,
2020-05-23 12:47:06 -07:00
getEmotion,
getGenderPresentation,
} = require("./util");
2020-04-22 11:51:36 -07:00
// These are caches of stable database tables. They're built in the
// `build-cached-data` script, at build time and dev-start time.
const zoneRows = require("../../build/cached-data/zones.json");
const zones = new Map(zoneRows.map((z) => [z.id, z]));
const zoneTranslationRows = require("../../build/cached-data/zone_translations.json");
const zoneTranslations = new Map(
zoneTranslationRows.map((zt) => [`${zt.zoneId}-${zt.locale}`, zt])
);
2020-04-22 11:51:36 -07:00
const typeDefs = gql`
directive @cacheControl(maxAge: Int!) on FIELD_DEFINITION | OBJECT
2020-04-23 01:08:00 -07:00
enum LayerImageSize {
SIZE_600
SIZE_300
SIZE_150
}
2020-05-23 12:47:06 -07:00
"""
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
}
"""
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!
2020-05-31 15:56:40 -07:00
rarityIndex: Int!
isNc: Boolean!
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!
2020-04-23 01:08:00 -07:00
}
# Cache for 1 week (unlikely to change)
type PetAppearance @cacheControl(maxAge: 604800) {
2020-05-02 20:48:32 -07:00
id: ID!
2020-06-24 19:05:07 -07:00
species: Species!
color: Color!
2020-05-23 12:47:06 -07:00
pose: Pose!
2020-06-24 19:05:07 -07:00
bodyId: ID!
layers: [AppearanceLayer!]!
2020-06-24 19:05:07 -07:00
petStateId: ID! # Convenience field for developers
}
type ItemAppearance {
layers: [AppearanceLayer!]!
2020-04-23 14:44:06 -07:00
restrictedZones: [Zone!]!
2020-04-23 01:08:00 -07:00
}
# Cache for 1 week (unlikely to change)
type AppearanceLayer @cacheControl(maxAge: 604800) {
2020-08-14 22:09:52 -07:00
# The DTI ID. Guaranteed unique across all layers of all types.
2020-04-23 01:08:00 -07:00
id: ID!
2020-08-14 22:09:52 -07:00
# 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!
2020-04-23 01:08:00 -07:00
zone: Zone!
imageUrl(size: LayerImageSize): String
2020-05-11 21:19:34 -07:00
"""
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
2020-04-23 01:08:00 -07:00
}
# Cache for 1 week (unlikely to change)
type Zone @cacheControl(maxAge: 604800) {
2020-04-23 01:08:00 -07:00
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!]!
}
# Cache for 1 week (unlikely to change)
type Color @cacheControl(maxAge: 604800) {
2020-04-25 03:42:05 -07:00
id: ID!
name: String!
2020-07-31 22:11:32 -07:00
isStandard: Boolean!
2020-04-25 03:42:05 -07:00
}
# Cache for 1 week (unlikely to change)
type Species @cacheControl(maxAge: 604800) {
2020-04-25 03:42:05 -07:00
id: ID!
name: String!
}
type SpeciesColorPair {
species: Species!
color: Color!
}
type Outfit {
2020-06-24 19:05:07 -07:00
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
}
2020-04-22 11:51:36 -07:00
type Query {
allColors: [Color!]! @cacheControl(maxAge: 10800) # Cache for 3 hours (we might add more!)
allSpecies: [Species!]! @cacheControl(maxAge: 10800) # Cache for 3 hours (we might add more!)
allValidSpeciesColorPairs: [SpeciesColorPair!]! # deprecated
item(id: ID!): Item
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!
2020-05-23 12:47:06 -07:00
petAppearance(speciesId: ID!, colorId: ID!, pose: Pose!): PetAppearance
@cacheControl(maxAge: 604800) # Cache for 1 week (unlikely to change)
petAppearances(speciesId: ID!, colorId: ID!): [PetAppearance!]!
@cacheControl(maxAge: 10800) # Cache for 3 hours (we might add more!)
2020-06-24 19:05:07 -07:00
outfit(id: ID!): Outfit
petOnNeopetsDotCom(petName: String!): Outfit
2020-04-22 11:51:36 -07:00
}
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!
}
2020-04-22 11:51:36 -07:00
`;
const resolvers = {
Item: {
name: async ({ id, name }, _, { itemTranslationLoader }) => {
if (name) return name;
const translation = await itemTranslationLoader.load(id);
2020-04-22 11:51:36 -07:00
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, itemSwfAssetLoader, itemLoader }
) => {
const itemPromise = itemLoader.load(id);
2020-06-24 19:05:07 -07:00
const petType = await petTypeBySpeciesAndColorLoader.load({
speciesId: speciesId,
colorId: colorId,
2020-04-23 01:08:00 -07:00
});
2020-05-27 00:46:55 -07:00
const allSwfAssets = await itemSwfAssetLoader.load({
itemId: id,
2020-04-23 01:08:00 -07:00
bodyId: petType.bodyId,
});
2020-05-27 00:46:55 -07:00
if (allSwfAssets.length === 0) {
// If there's no assets at all, treat it as non-fitting: no appearance.
// (If there are assets but they're non-SWF, we'll treat this as
// fitting, but with an *empty* appearance.)
return null;
}
2020-05-27 00:46:55 -07:00
const swfAssets = allSwfAssets.filter((sa) => sa.url.endsWith(".swf"));
2020-04-23 14:44:06 -07:00
const restrictedZones = [];
const item = await itemPromise;
2020-04-23 14:44:06 -07:00
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
},
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;
},
2020-04-23 01:08:00 -07:00
},
PetAppearance: {
2020-06-24 19:05:07 -07:00
id: async ({ petStateId }, _, { petStateLoader, petTypeLoader }) => {
const petState = await petStateLoader.load(petStateId);
const petType = await petTypeLoader.load(petState.petTypeId);
2020-05-23 12:47:06 -07:00
const pose = getPoseFromPetState(petState);
2020-06-24 19:05:07 -07:00
return `${petType.speciesId}-${petType.colorId}-${pose}`;
},
2020-06-24 19:05:07 -07:00
color: async ({ petStateId }, _, { petStateLoader, petTypeLoader }) => {
const petState = await petStateLoader.load(petStateId);
const petType = await petTypeLoader.load(petState.petTypeId);
return { id: petType.colorId };
2020-05-02 20:48:32 -07:00
},
2020-06-24 19:05:07 -07:00
species: async ({ petStateId }, _, { petStateLoader, petTypeLoader }) => {
const petState = await petStateLoader.load(petStateId);
const petType = await petTypeLoader.load(petState.petTypeId);
return { id: petType.speciesId };
},
bodyId: async ({ petStateId }, _, { petStateLoader, petTypeLoader }) => {
const petState = await petStateLoader.load(petStateId);
const petType = await petTypeLoader.load(petState.petTypeId);
return petType.bodyId;
},
pose: async ({ petStateId }, _, { petStateLoader }) => {
const petState = await petStateLoader.load(petStateId);
return getPoseFromPetState(petState);
},
layers: async ({ petStateId }, _, { petSwfAssetLoader }) => {
const swfAssets = await petSwfAssetLoader.load(petStateId);
return swfAssets;
},
},
AppearanceLayer: {
bodyId: async ({ id }, _, { swfAssetLoader }) => {
2020-08-14 22:09:52 -07:00
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 }) => {
const layer = await swfAssetLoader.load(id);
return { id: layer.zoneId };
2020-04-23 01:08:00 -07:00
},
swfUrl: async ({ id }, _, { swfAssetLoader }) => {
const layer = await swfAssetLoader.load(id);
return layer.url;
},
imageUrl: async ({ id }, { size }, { swfAssetLoader }) => {
const layer = await swfAssetLoader.load(id);
2020-04-23 01:08:00 -07:00
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
},
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 neopets.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)}`
);
}
2020-05-11 21:19:34 -07:00
if (!manifest) {
2020-05-23 11:32:05 -07:00
svgLogger.log("no-manifest");
2020-05-11 21:19:34 -07:00
return null;
}
if (manifest.assets.length !== 1) {
2020-05-23 11:32:05 -07:00
svgLogger.log(`wrong-asset-count:${manifest.assets.length}!=1`);
2020-05-11 21:19:34 -07:00
return null;
}
const asset = manifest.assets[0];
if (asset.format !== "vector") {
2020-05-23 11:32:05 -07:00
svgLogger.log(`wrong-asset-format:${asset.format}`);
2020-05-11 21:19:34 -07:00
return null;
}
if (asset.assetData.length !== 1) {
2020-05-23 11:32:05 -07:00
svgLogger.log(`wrong-assetData-length:${asset.assetData.length}!=1`);
2020-05-11 21:19:34 -07:00
return null;
}
2020-05-23 11:32:05 -07:00
svgLogger.log("success");
2020-05-11 21:19:34 -07:00
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) };
},
2020-04-23 01:08:00 -07:00
},
Zone: {
depth: ({ id }) => zones.get(id).depth,
label: ({ id }) => zoneTranslations.get(`${id}-en`).label,
2020-04-22 11:51:36 -07:00
},
2020-04-25 03:42:05 -07:00
Color: {
2020-06-24 19:05:07 -07:00
name: async ({ id }, _, { colorTranslationLoader }) => {
const colorTranslation = await colorTranslationLoader.load(id);
2020-04-25 04:33:05 -07:00
return capitalize(colorTranslation.name);
2020-04-25 03:42:05 -07:00
},
2020-07-31 22:11:32 -07:00
isStandard: async ({ id }, _, { colorLoader }) => {
const color = await colorLoader.load(id);
return color.standard ? true : false;
},
2020-04-25 03:42:05 -07:00
},
Species: {
2020-06-24 19:05:07 -07:00
name: async ({ id }, _, { speciesTranslationLoader }) => {
const speciesTranslation = await speciesTranslationLoader.load(id);
2020-04-25 04:33:05 -07:00
return capitalize(speciesTranslation.name);
2020-04-25 03:42:05 -07:00
},
},
2020-06-24 19:05:07 -07:00
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 { petStateId: 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 }));
},
},
2020-04-22 11:51:36 -07:00
Query: {
2020-07-31 22:11:32 -07:00
allColors: async (_, { ids }, { colorLoader }) => {
const allColors = await colorLoader.loadAll();
2020-04-25 03:42:05 -07:00
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;
},
item: (_, { id }) => ({ id }),
items: (_, { ids }) => {
return ids.map((id) => ({ id }));
2020-04-24 21:17:03 -07:00
},
itemSearch: async (_, { query }, { itemSearchLoader }) => {
2020-05-31 15:52:54 -07:00
const items = await itemSearchLoader.load(query.trim());
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 },
2020-06-24 19:05:07 -07:00
{ petTypeBySpeciesAndColorLoader, itemSearchToFitLoader }
) => {
2020-06-24 19:05:07 -07:00
const petType = await petTypeBySpeciesAndColorLoader.load({
speciesId,
colorId,
});
const { bodyId } = petType;
2020-04-25 01:55:48 -07:00
const items = await itemSearchToFitLoader.load({
2020-05-31 15:52:54 -07:00
query: query.trim(),
2020-04-25 01:55:48 -07:00
bodyId,
offset,
limit,
});
return { query, items };
2020-04-23 01:08:00 -07:00
},
petAppearance: async (
_,
2020-05-23 12:47:06 -07:00
{ speciesId, colorId, pose },
2020-06-24 19:05:07 -07:00
{ petTypeBySpeciesAndColorLoader, petStatesForPetTypeLoader }
) => {
2020-06-24 19:05:07 -07:00
const petType = await petTypeBySpeciesAndColorLoader.load({
speciesId,
colorId,
});
2020-06-24 19:05:07 -07:00
const petStates = await petStatesForPetTypeLoader.load(petType.id);
// TODO: This could be optimized into the query condition 🤔
2020-05-23 12:47:06 -07:00
const petState = petStates.find((ps) => getPoseFromPetState(ps) === pose);
if (!petState) {
return null;
}
2020-06-24 19:05:07 -07:00
return { petStateId: petState.id };
},
petAppearances: async (
_,
{ speciesId, colorId },
2020-06-24 19:05:07 -07:00
{ petTypeBySpeciesAndColorLoader, petStatesForPetTypeLoader }
) => {
2020-06-24 19:05:07 -07:00
const petType = await petTypeBySpeciesAndColorLoader.load({
speciesId,
colorId,
});
2020-06-24 19:05:07 -07:00
const petStates = await petStatesForPetTypeLoader.load(petType.id);
2020-07-31 22:11:32 -07:00
petStates.sort((a, b) => a.id - b.id);
2020-06-24 19:05:07 -07:00
return petStates.map((petState) => ({ petStateId: petState.id }));
},
2020-06-24 19:05:07 -07:00
outfit: (_, { id }) => ({ id }),
petOnNeopetsDotCom: async (_, { petName }) => {
const [petMetaData, customPetData] = await Promise.all([
neopets.loadPetMetaData(petName),
neopets.loadCustomPetData(petName),
]);
const outfit = {
2020-06-24 19:05:07 -07:00
// 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;
},
2020-04-22 11:51:36 -07:00
},
Mutation: {
setManualSpecialColor: async (
_,
{ itemId, colorId, supportSecret },
{ db }
) => {
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
throw new Error(`Support secret is incorrect. Try setting up again?`);
}
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}`
);
}
return { id: itemId };
},
setItemExplicitlyBodySpecific: async (
_,
{ itemId, explicitlyBodySpecific, supportSecret },
{ db }
) => {
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
throw new Error(`Support secret is incorrect. Try setting up again?`);
}
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}`
);
}
return { id: itemId };
},
setLayerBodyId: async (_, { layerId, bodyId, supportSecret }, { db }) => {
if (supportSecret !== process.env["SUPPORT_SECRET"]) {
throw new Error(`Support secret is incorrect. Try setting up again?`);
}
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}`
);
}
return { id: layerId };
},
},
2020-04-22 11:51:36 -07:00
};
2020-05-23 11:32:05 -07:00
let lastSvgLogger = null;
const svgLogging = {
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}`);
}
},
};
},
};
const schema = makeExecutableSchema({ typeDefs, resolvers });
2020-08-17 01:27:05 -07:00
const plugins = [svgLogging];
if (process.env["NODE_ENV"] !== "test") {
addBeelineToSchema(schema);
plugins.push(beelinePlugin);
}
const config = {
schema,
2020-04-22 11:51:36 -07:00
context: async () => {
const db = await connectToDb();
2020-05-23 11:32:05 -07:00
const svgLogger = {
entries: [],
log(entry) {
this.entries.push(entry);
},
};
lastSvgLogger = svgLogger;
return {
2020-05-23 11:32:05 -07:00
svgLogger,
db,
...buildLoaders(db),
};
2020-04-22 11:51:36 -07:00
},
2020-04-23 01:09:17 -07:00
2020-08-17 01:27:05 -07:00
plugins,
2020-05-23 11:32:05 -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 };