diff --git a/src/server/query-tests/__snapshots__/Pet.test.js.snap b/src/server/query-tests/__snapshots__/Pet.test.js.snap index fdc50c8..45218eb 100644 --- a/src/server/query-tests/__snapshots__/Pet.test.js.snap +++ b/src/server/query-tests/__snapshots__/Pet.test.js.snap @@ -184,157 +184,149 @@ Array [ ], ], Array [ - "INSERT INTO items - ( - id, zones_restrict, thumbnail_url, category, type, rarity_index, - price, weight_lbs, created_at, updated_at - ) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); - ", + "INSERT INTO items (category, created_at, id, price, rarity_index, thumbnail_url, type, updated_at, weight_lbs, zones_restrict) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", Array [ + "Gift", + 2020-01-01T00:00:00.000Z, "37229", - "0000000000000000000000000000000000000000000000", + 0, + 101, "http://images.neopets.com/items/gif_magicball_table.gif", "Gift", - "Gift", - 101, - 0, - 1, 2020-01-01T00:00:00.000Z, + 1, + "0000000000000000000000000000000000000000000000", + "Special", 2020-01-01T00:00:00.000Z, "37375", - "0000000000000000000000000000000000000000000000000000", - "http://images.neopets.com/items/bg_moonstars.gif", - "Special", - "Mystical Surroundings", - 75, 209, - 1, + 75, + "http://images.neopets.com/items/bg_moonstars.gif", + "Mystical Surroundings", 2020-01-01T00:00:00.000Z, + 1, + "0000000000000000000000000000000000000000000000000000", + "Clothes", 2020-01-01T00:00:00.000Z, "38911", - "0000000000000000000000000000000000001100000000000000", + 980, + 92, "http://images.neopets.com/items/clo_zafara_agent_hood.gif", "Clothes", - "Clothes", - 92, - 980, - 1, 2020-01-01T00:00:00.000Z, + 1, + "0000000000000000000000000000000000001100000000000000", + "Clothes", 2020-01-01T00:00:00.000Z, "38912", - "0000000000000000000101000000000000000000000000000000", + 1476, + 90, "http://images.neopets.com/items/clo_zafara_agent_robe.gif", "Clothes", - "Clothes", - 90, - 1476, - 1, 2020-01-01T00:00:00.000Z, + 1, + "0000000000000000000101000000000000000000000000000000", + "Clothes", 2020-01-01T00:00:00.000Z, "38913", - "0000000000000000000000000000000000000000000000000000", + 1177, + 88, "http://images.neopets.com/items/clo_zafara_agent_gloves.gif", "Clothes", - "Clothes", - 88, - 1177, - 1, 2020-01-01T00:00:00.000Z, + 1, + "0000000000000000000000000000000000000000000000000000", + "Toys", 2020-01-01T00:00:00.000Z, "43014", - "0000000000000000000000000000000000000000000000", - "http://images.neopets.com/items/toy_stringlight_illleaf.gif", - "Toys", - "Toy", - 80, 1033, - 1, + 80, + "http://images.neopets.com/items/toy_stringlight_illleaf.gif", + "Toy", 2020-01-01T00:00:00.000Z, + 1, + "0000000000000000000000000000000000000000000000", + "Clothes", 2020-01-01T00:00:00.000Z, "43397", - "0000000000000000000000000000000000000000000000", + 0, + 500, "http://images.neopets.com/items/mall_staff_jewelled.gif", "Clothes", - "Clothes", - 500, - 0, - 1, 2020-01-01T00:00:00.000Z, + 1, + "0000000000000000000000000000000000000000000000", + "Clothes", 2020-01-01T00:00:00.000Z, "48313", - "0000000000000000000000000000000000000000000000000000", + 0, + 101, "http://images.neopets.com/items/clo_altcuplogo_brooch.gif", "Clothes", - "Clothes", - 101, - 0, + 2020-01-01T00:00:00.000Z, 1, - 2020-01-01T00:00:00.000Z, - 2020-01-01T00:00:00.000Z, + "0000000000000000000000000000000000000000000000000000", ], ], Array [ - "INSERT INTO item_translations - (item_id, locale, name, description, rarity, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?);", + "INSERT INTO item_translations (created_at, description, item_id, locale, name, rarity, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?);", Array [ + 2020-01-01T00:00:00.000Z, + "What does this ball actually do?", "37229", "en", "Magic Ball Table", - "What does this ball actually do?", "Special", 2020-01-01T00:00:00.000Z, 2020-01-01T00:00:00.000Z, + "Dont forget to wish upon a star.", "37375", "en", "Moon and Stars Background", - "Dont forget to wish upon a star.", "Uncommon", 2020-01-01T00:00:00.000Z, 2020-01-01T00:00:00.000Z, + "Hide your face and hair so no one can recognise you.", "38911", "en", "Zafara Agent Hood", - "Hide your face and hair so no one can recognise you.", "Very Rare", 2020-01-01T00:00:00.000Z, 2020-01-01T00:00:00.000Z, + "This robe is great for being stealthy.", "38912", "en", "Zafara Agent Robe", - "This robe is great for being stealthy.", "Very Rare", 2020-01-01T00:00:00.000Z, 2020-01-01T00:00:00.000Z, + "Dont leave any trace that you were there with these gloves.", "38913", "en", "Zafara Agent Gloves", - "Dont leave any trace that you were there with these gloves.", "Rare", 2020-01-01T00:00:00.000Z, 2020-01-01T00:00:00.000Z, + "These leaves almost look magical with their gentle glow.", "43014", "en", "Green Leaf String Lights", - "These leaves almost look magical with their gentle glow.", "Uncommon", 2020-01-01T00:00:00.000Z, 2020-01-01T00:00:00.000Z, + "This jewelled staff shines with a magical light.", "43397", "en", "Jewelled Staff", - "This jewelled staff shines with a magical light.", "Artifact", 2020-01-01T00:00:00.000Z, 2020-01-01T00:00:00.000Z, + "Even the announcers of the Altador Cup celebrate. This was given out by the Advent Calendar in Y11.", "48313", "en", "Altador Cup Brooch", - "Even the announcers of the Altador Cup celebrate. This was given out by the Advent Calendar in Y11.", "Special", 2020-01-01T00:00:00.000Z, - 2020-01-01T00:00:00.000Z, ], ], ] diff --git a/src/server/types/Outfit.js b/src/server/types/Outfit.js index 2422f6f..f6eace1 100644 --- a/src/server/types/Outfit.js +++ b/src/server/types/Outfit.js @@ -155,22 +155,12 @@ async function saveModelingData( customPetData, { db, itemLoader, itemTranslationLoader } ) { - const itemIds = Object.keys(customPetData.object_info_registry); - const [items, itemTranslations] = await Promise.all([ - itemLoader.loadMany(itemIds), - itemTranslationLoader.loadMany(itemIds), - ]); + const objectInfos = Object.values(customPetData.object_info_registry); - const rowsToInsert = []; - const rowsToUpdate = []; - for (const index in itemIds) { - const itemId = itemIds[index]; - const item = items[index]; - const itemTranslation = itemTranslations[index]; - - const objectInfo = customPetData.object_info_registry[itemId]; - const objectInfoFields = { - id: itemId, + const incomingItems = objectInfos.map((objectInfo) => [ + String(objectInfo.obj_info_id), + { + id: String(objectInfo.obj_info_id), zonesRestrict: objectInfo.zones_restrict, thumbnailUrl: objectInfo.thumbnail_url, category: objectInfo.category, @@ -178,110 +168,79 @@ async function saveModelingData( rarityIndex: objectInfo.rarity_index, price: objectInfo.price, weightLbs: objectInfo.weight_lbs, + }, + ]); + + const incomingItemTranslations = objectInfos.map((objectInfo) => [ + String(objectInfo.obj_info_id), + { + itemId: String(objectInfo.obj_info_id), + locale: "en", name: objectInfo.name, description: objectInfo.description, rarity: objectInfo.rarity, - }; + }, + ]); - if (item instanceof Error) { - // New item, we'll just insert it! + await Promise.all([ + syncToDb("items", itemLoader, db, incomingItems), + syncToDb( + "item_translations", + itemTranslationLoader, + db, + incomingItemTranslations + ), + ]); +} + +/** + * Syncs the given data to the database: for each incoming row, if there's no + * matching row in the loader, we insert a new row; or, if there's a matching + * row in the loader but its data is different, we update it; or, if there's + * no change, we do nothing. + * + * Automatically sets the `createdAt` and `updatedAt` timestamps for inserted + * or updated rows. + * + * Will perform one call to the loader, and at most one INSERT, and at most one + * UPDATE, regardless of how many rows we're syncing. + */ +async function syncToDb(tableName, loader, db, incomingRows) { + const loaderKeys = incomingRows.map(([key, _]) => key); + const currentRows = await loader.loadMany(loaderKeys); + + const rowsToInsert = []; + for (const index in incomingRows) { + const [_, incomingRow] = incomingRows[index]; + const currentRow = currentRows[index]; + + if (currentRow instanceof Error) { rowsToInsert.push({ - ...objectInfoFields, + ...incomingRow, createdAt: new Date(), updatedAt: new Date(), }); - continue; } - - const itemFields = { - id: item.id, - zonesRestrict: item.zonesRestrict, - thumbnailUrl: item.thumbnailUrl, - category: item.category, - type: item.type, - rarityIndex: item.rarityIndex, - price: item.price, - weightLbs: item.weightLbs, - name: itemTranslation.name, - description: itemTranslation.description, - rarity: itemTranslation.rarity, - }; - - if (objectsShallowEqual(objectInfoFields, itemFields)) { - // Existing item, no change! - continue; - } - - // Updated item, so we'll update it! - rowsToUpdate.push({ - ...objectInfoFields, - updatedAt: new Date(), - }); } if (rowsToInsert.length > 0) { - const itemQs = rowsToInsert - .map((_) => "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") - .join(", "); - const itemTranslationQs = rowsToInsert - .map((_) => "(?, ?, ?, ?, ?, ?, ?)") - .join(", "); - const itemValues = rowsToInsert.map((row) => [ - row.id, - row.zonesRestrict, - row.thumbnailUrl, - row.category, - row.type, - row.rarityIndex, - row.price, - row.weightLbs, - row.createdAt, - row.updatedAt, - ]); - const itemTranslationValues = rowsToInsert.map((row) => [ - row.id, - "en", - row.name, - row.description, - row.rarity, - row.createdAt, - row.updatedAt, - ]); - - // NOTE: Hmm, I tried to use multiple statements to combine these, but I - // guess it doesn't work for prepared statements? - await Promise.all([ - db.execute( - `INSERT INTO items - ( - id, zones_restrict, thumbnail_url, category, type, rarity_index, - price, weight_lbs, created_at, updated_at - ) - VALUES ${itemQs}; - `, - itemValues.flat() - ), - db.execute( - `INSERT INTO item_translations - (item_id, locale, name, description, rarity, created_at, updated_at) - VALUES ${itemTranslationQs};`, - itemTranslationValues.flat() - ), - ]); + // Get the column names from the first row, and convert them to + // underscore-case instead of camel-case. + const rowKeys = Object.keys(rowsToInsert[0]).sort(); + const columnNames = rowKeys.map((key) => + key.replace(/[A-Z]/g, (m) => "_" + m[0].toLowerCase()) + ); + const columnsStr = columnNames.join(", "); + const qs = columnNames.map((_) => "?").join(", "); + const rowQs = rowsToInsert.map((_) => "(" + qs + ")").join(", "); + const rowFields = rowsToInsert.map((row) => rowKeys.map((key) => row[key])); + await db.execute( + `INSERT INTO ${tableName} (${columnsStr}) VALUES ${rowQs};`, + rowFields.flat() + ); } - // TODO: Update the items that need updating! -} - -/** Given two objects with the same keys, return whether their values match. */ -function objectsShallowEqual(a, b) { - for (const key in a) { - if (a[key] !== b[key]) { - return false; - } - } - - return true; + // TODO: Update rows that need updating } module.exports = { typeDefs, resolvers };