const beeline = require("honeycomb-beeline")({ writeKey: process.env["HONEYCOMB_WRITE_KEY"], dataset: process.env["NODE_ENV"] === "production" ? "Dress to Impress (2020)" : "Dress to Impress (2020, dev)", serviceName: "impress-2020-gql-server", }); import fetch from "node-fetch"; import connectToDb from "../src/server/db"; async function handle(req, res) { const allNcItemNamesAndIdsPromise = loadAllNcItemNamesAndIds(); let itemValuesByIdOrName; try { itemValuesByIdOrName = await loadWakaValuesByIdOrName(); } catch (e) { console.error(e); res.setHeader("Content-Type", "text/plain"); res.status(500).send("Error loading Waka data from Google Sheets API"); return; } // Restructure the value data to use IDs as keys, instead of names. const allNcItemNamesAndIds = await allNcItemNamesAndIdsPromise; const itemValues = {}; for (const { name, id } of allNcItemNamesAndIds) { if (id in itemValuesByIdOrName) { itemValues[id] = itemValuesByIdOrName[id]; } else if (name in itemValuesByIdOrName) { itemValues[id] = itemValuesByIdOrName[name]; } } // Cache for 1 minute, and immediately serve stale data for a day after. // This should keep it fast and responsive, and stay well within our API key // limits. (This will cause the client to send more requests than necessary, // but the CDN cache should generally respond quickly with a small 304 Not // Modified, unless the data really did change.) res.setHeader( "Cache-Control", "public, max-age=3600, stale-while-revalidate=86400" ); return res.send(itemValues); } async function loadAllNcItemNamesAndIds() { const db = await connectToDb(); const [rows] = await db.query(` SELECT items.id, item_translations.name FROM items INNER JOIN item_translations ON item_translations.item_id = items.id WHERE (items.rarity_index IN (0, 500) OR is_manually_nc = 1) AND item_translations.locale = "en" `); return rows.map(({ id, name }) => ({ id, name: normalizeItemName(name) })); } /** * Load all Waka values from the spreadsheet. Returns an object keyed by ID or * name - that is, if the item ID is provided in the sheet, we use that as the * key; or if not, we use the name as the key. */ async function loadWakaValuesByIdOrName() { if (!process.env["GOOGLE_API_KEY"]) { throw new Error(`GOOGLE_API_KEY environment variable must be provided`); } const res = await fetch( `https://sheets.googleapis.com/v4/spreadsheets/` + `1DRMrniTSZP0sgZK6OAFFYqpmbT6xY_Ve_i480zghOX0/values/NC%20Values` + `?fields=values&key=${encodeURIComponent(process.env["GOOGLE_API_KEY"])}` ); const json = await res.json(); if (!res.ok) { if (json.error) { const { code, status, message } = json.error; throw new Error( `Google Sheets API returned error ${code} ${status}: ${message}` ); } else { throw new Error( `Google Sheets API returned unexpected error: ${res.status} ${res.statusText}` ); } } // Get the rows from the JSON response - skipping the first-row headers. const rows = json.values.slice(1); // Reformat the rows as a map from item name to value. We offer the item data // as an object with a single field `value` for extensibility, but we omit // the spreadsheet columns that we don't use on DTI, like Notes. // // NOTE: The Sheets API only returns the first non-empty cells of the row. // That's why we set `""` as the defaults, in case the value/notes/etc // aren't provided. const itemValuesByIdOrName = {}; for (const [ itemName, value = "", notes = "", marks = "", itemId = "", ] of rows) { const normalizedItemName = normalizeItemName(itemName); itemValuesByIdOrName[itemId || normalizedItemName] = { value }; } return itemValuesByIdOrName; } function normalizeItemName(name) { return ( name // Remove all spaces, they're a common source of inconsistency .replace(/\s+/g, "") // Lower case, because capitalization is another common source .toLowerCase() // Remove diacritics: https://stackoverflow.com/a/37511463/107415 // Waka has some stray ones in item names, not sure why! .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") ); } export default async (req, res) => { beeline.withTrace( { name: "api/allWakaValues", operation_name: "api/allWakaValues" }, () => handle(req, res) ); };