From 8793d8b5706cdd317770cff1bdd466bce44c747c Mon Sep 17 00:00:00 2001 From: Matchu Date: Fri, 18 Sep 2020 08:04:46 -0700 Subject: [PATCH 01/22] refactor modeling code a bit to use `syncToDb` fn --- .../__snapshots__/Pet.test.js.snap | 116 ++++++------ src/server/types/Outfit.js | 169 +++++++----------- 2 files changed, 118 insertions(+), 167 deletions(-) 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 }; From f7d9faa2657fd1ac7d93eb4c4e163ac86c62fc2a Mon Sep 17 00:00:00 2001 From: Matchu Date: Sat, 19 Sep 2020 02:42:37 -0700 Subject: [PATCH 02/22] update modeled item data --- src/server/query-tests/Pet.test.js | 123 +++++++--- .../__snapshots__/Pet.test.js.snap | 227 ++++++++++++++++++ src/server/query-tests/setup.js | 3 +- src/server/types/Outfit.js | 124 +++++++--- 4 files changed, 405 insertions(+), 72 deletions(-) diff --git a/src/server/query-tests/Pet.test.js b/src/server/query-tests/Pet.test.js index f33aac3..eced112 100644 --- a/src/server/query-tests/Pet.test.js +++ b/src/server/query-tests/Pet.test.js @@ -1,5 +1,11 @@ const gql = require("graphql-tag"); -const { query, getDbCalls, clearDbCalls, useTestDb } = require("./setup.js"); +const { + query, + getDbCalls, + clearDbCalls, + useTestDb, + connectToDb, +} = require("./setup.js"); describe("Pet", () => { it("looks up a pet", async () => { @@ -106,35 +112,90 @@ describe("Pet", () => { expect(res2).toHaveNoErrors(); expect(res2.data).toMatchSnapshot(); - expect(getDbCalls()).toMatchInlineSnapshot(` - Array [ - Array [ - "SELECT * FROM item_translations WHERE item_id IN (?,?,?,?,?,?,?,?) AND locale = \\"en\\"", - Array [ - "37229", - "37375", - "38911", - "38912", - "38913", - "43014", - "43397", - "48313", - ], - ], - Array [ - "SELECT * FROM items WHERE id IN (?,?,?,?,?,?,?,?)", - Array [ - "37229", - "37375", - "38911", - "38912", - "38913", - "43014", - "43397", - "48313", - ], - ], - ] - `); + expect(getDbCalls()).toMatchSnapshot(); + }); + + it("models updated item data", async () => { + useTestDb(); + + // First, write a fake version of the Jewelled Staff to the database. + // It's mostly the real data, except we changed rarity_index, + // thumbnail_url, translated name, and translated description. + const db = await connectToDb(); + await Promise.all([ + db.query( + `INSERT INTO items (id, zones_restrict, thumbnail_url, category, + type, rarity_index, price, weight_lbs) + VALUES (43397, "00000000000000000000000000000000000000000000000", + "http://example.com/favicon.ico", "Clothes", "Clothes", 101, + 0, 1);` + ), + db.query( + `INSERT INTO item_translations (item_id, locale, name, description, + rarity) + VALUES (43397, "en", "Bejewelled Staffo", + "This staff is really neat and good!", "Artifact")` + ), + ]); + + clearDbCalls(); + + // Then, load a pet wearing this. It should trigger an UPDATE for the item + // and its translation, and return the new names in the query. + const res = await query({ + query: gql` + query { + petOnNeopetsDotCom(petName: "roopal27") { + items { + id + name + description + thumbnailUrl + rarityIndex + } + } + } + `, + }); + + expect(res).toHaveNoErrors(); + const itemData = res.data.petOnNeopetsDotCom.items.find( + (item) => item.id === "43397" + ); + expect(itemData).toEqual({ + id: "43397", + name: "Jewelled Staff", + description: "This jewelled staff shines with a magical light.", + thumbnailUrl: "http://images.neopets.com/items/mall_staff_jewelled.gif", + rarityIndex: 500, + }); + expect(getDbCalls()).toMatchSnapshot(); + + clearDbCalls(); + + // Finally, load the item. It should have the updated values. + const res2 = await query({ + query: gql` + query { + item(id: "43397") { + id + name + description + thumbnailUrl + rarityIndex + } + } + `, + }); + + expect(res2).toHaveNoErrors(); + expect(res2.data.item).toEqual({ + id: "43397", + name: "Jewelled Staff", + description: "This jewelled staff shines with a magical light.", + thumbnailUrl: "http://images.neopets.com/items/mall_staff_jewelled.gif", + rarityIndex: 500, + }); + expect(getDbCalls()).toMatchSnapshot(); }); }); diff --git a/src/server/query-tests/__snapshots__/Pet.test.js.snap b/src/server/query-tests/__snapshots__/Pet.test.js.snap index 45218eb..7a30925 100644 --- a/src/server/query-tests/__snapshots__/Pet.test.js.snap +++ b/src/server/query-tests/__snapshots__/Pet.test.js.snap @@ -410,3 +410,230 @@ Object { ], } `; + +exports[`Pet models new item data 4`] = ` +Array [ + Array [ + "SELECT * FROM item_translations WHERE item_id IN (?,?,?,?,?,?,?,?) AND locale = \\"en\\"", + Array [ + "37229", + "37375", + "38911", + "38912", + "38913", + "43014", + "43397", + "48313", + ], + ], + Array [ + "SELECT * FROM items WHERE id IN (?,?,?,?,?,?,?,?)", + Array [ + "37229", + "37375", + "38911", + "38912", + "38913", + "43014", + "43397", + "48313", + ], + ], +] +`; + +exports[`Pet models updated item data 1`] = ` +Array [ + Array [ + "SELECT * FROM items WHERE id IN (?,?,?,?,?,?,?,?)", + Array [ + "37229", + "37375", + "38911", + "38912", + "38913", + "43014", + "43397", + "48313", + ], + ], + Array [ + "SELECT * FROM item_translations WHERE item_id IN (?,?,?,?,?,?,?,?) AND locale = \\"en\\"", + Array [ + "37229", + "37375", + "38911", + "38912", + "38913", + "43014", + "43397", + "48313", + ], + ], + Array [ + "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", + 0, + 101, + "http://images.neopets.com/items/gif_magicball_table.gif", + "Gift", + 2020-01-01T00:00:00.000Z, + 1, + "0000000000000000000000000000000000000000000000", + "Special", + 2020-01-01T00:00:00.000Z, + "37375", + 209, + 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", + 980, + 92, + "http://images.neopets.com/items/clo_zafara_agent_hood.gif", + "Clothes", + 2020-01-01T00:00:00.000Z, + 1, + "0000000000000000000000000000000000001100000000000000", + "Clothes", + 2020-01-01T00:00:00.000Z, + "38912", + 1476, + 90, + "http://images.neopets.com/items/clo_zafara_agent_robe.gif", + "Clothes", + 2020-01-01T00:00:00.000Z, + 1, + "0000000000000000000101000000000000000000000000000000", + "Clothes", + 2020-01-01T00:00:00.000Z, + "38913", + 1177, + 88, + "http://images.neopets.com/items/clo_zafara_agent_gloves.gif", + "Clothes", + 2020-01-01T00:00:00.000Z, + 1, + "0000000000000000000000000000000000000000000000000000", + "Toys", + 2020-01-01T00:00:00.000Z, + "43014", + 1033, + 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, + "48313", + 0, + 101, + "http://images.neopets.com/items/clo_altcuplogo_brooch.gif", + "Clothes", + 2020-01-01T00:00:00.000Z, + 1, + "0000000000000000000000000000000000000000000000000000", + ], + ], + Array [ + "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", + "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", + "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", + "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", + "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", + "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", + "Uncommon", + 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", + "Special", + 2020-01-01T00:00:00.000Z, + ], + ], + Array [ + "UPDATE items SET rarity_index = ?, thumbnail_url = ?, updated_at = ?, zones_restrict = ? WHERE id = ?;", + Array [ + 500, + "http://images.neopets.com/items/mall_staff_jewelled.gif", + 2020-01-01T00:00:00.000Z, + "0000000000000000000000000000000000000000000000", + "43397", + ], + ], + Array [ + "UPDATE item_translations SET description = ?, name = ?, updated_at = ? WHERE item_id = ? AND locale = \\"en\\";", + Array [ + "This jewelled staff shines with a magical light.", + "Jewelled Staff", + 2020-01-01T00:00:00.000Z, + "43397", + ], + ], +] +`; + +exports[`Pet models updated item data 2`] = ` +Array [ + Array [ + "SELECT * FROM item_translations WHERE item_id IN (?) AND locale = \\"en\\"", + Array [ + "43397", + ], + ], + Array [ + "SELECT * FROM items WHERE id IN (?)", + Array [ + "43397", + ], + ], +] +`; diff --git a/src/server/query-tests/setup.js b/src/server/query-tests/setup.js index 82548e3..608cb2d 100644 --- a/src/server/query-tests/setup.js +++ b/src/server/query-tests/setup.js @@ -83,6 +83,7 @@ beforeEach(() => { } dbEnvironment = "production"; dbSetupDone = false; + db = null; }); afterAll(() => { if (db) { @@ -138,7 +139,7 @@ module.exports = { query, getDbCalls, clearDbCalls, - getDb: () => db, + connectToDb, useTestDb, logInAsTestUser, }; diff --git a/src/server/types/Outfit.js b/src/server/types/Outfit.js index f6eace1..dbbf08e 100644 --- a/src/server/types/Outfit.js +++ b/src/server/types/Outfit.js @@ -157,39 +157,41 @@ async function saveModelingData( ) { const objectInfos = Object.values(customPetData.object_info_registry); - 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, - type: objectInfo.type, - rarityIndex: objectInfo.rarity_index, - price: objectInfo.price, - weightLbs: objectInfo.weight_lbs, - }, - ]); + const incomingItems = objectInfos.map((objectInfo) => ({ + id: String(objectInfo.obj_info_id), + zonesRestrict: objectInfo.zones_restrict, + thumbnailUrl: objectInfo.thumbnail_url, + category: objectInfo.category, + type: objectInfo.type, + 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, - }, - ]); + const incomingItemTranslations = objectInfos.map((objectInfo) => ({ + itemId: String(objectInfo.obj_info_id), + locale: "en", + name: objectInfo.name, + description: objectInfo.description, + rarity: objectInfo.rarity, + })); await Promise.all([ - syncToDb("items", itemLoader, db, incomingItems), - syncToDb( - "item_translations", - itemTranslationLoader, - db, - incomingItemTranslations - ), + syncToDb(db, incomingItems, { + loader: itemLoader, + tableName: "items", + buildLoaderKey: (row) => row.id, + buildUpdateCondition: (row) => [`id = ?`, row.id], + }), + syncToDb(db, incomingItemTranslations, { + loader: itemTranslationLoader, + tableName: "item_translations", + buildLoaderKey: (row) => row.itemId, + buildUpdateCondition: (row) => [ + `item_id = ? AND locale = "en"`, + row.itemId, + ], + }), ]); } @@ -205,42 +207,84 @@ async function saveModelingData( * 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); +async function syncToDb( + db, + incomingRows, + { loader, tableName, buildLoaderKey, buildUpdateCondition } +) { + const loaderKeys = incomingRows.map(buildLoaderKey); const currentRows = await loader.loadMany(loaderKeys); - const rowsToInsert = []; + const inserts = []; + const updates = []; for (const index in incomingRows) { - const [_, incomingRow] = incomingRows[index]; + const incomingRow = incomingRows[index]; const currentRow = currentRows[index]; + // If there is no corresponding row in the database, prepare an insert. if (currentRow instanceof Error) { - rowsToInsert.push({ + inserts.push({ ...incomingRow, createdAt: new Date(), updatedAt: new Date(), }); + continue; + } + + // If there's a row in the database, and some of the values don't match, + // prepare an update with the updated fields only. + const updatedKeys = Object.keys(incomingRow).filter( + (k) => incomingRow[k] !== currentRow[k] + ); + if (updatedKeys.length > 0) { + const update = {}; + for (const key of updatedKeys) { + update[key] = incomingRow[key]; + } + update.updatedAt = new Date(); + updates.push({ incomingRow, update }); } } - if (rowsToInsert.length > 0) { + // Do a bulk insert of anything that needs added. + if (inserts.length > 0) { // 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 rowKeys = Object.keys(inserts[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])); + const rowQs = inserts.map((_) => "(" + qs + ")").join(", "); + const rowFields = inserts.map((row) => rowKeys.map((key) => row[key])); await db.execute( `INSERT INTO ${tableName} (${columnsStr}) VALUES ${rowQs};`, rowFields.flat() ); } - // TODO: Update rows that need updating + // Do parallel updates of anything that needs updated. + // NOTE: I feel like it's not possible to do bulk updates, even in a + // multi-statement mysql2 request? I might be wrong, but whatever; it's + // very uncommon, and any perf hit would be nbd. + const updatePromises = []; + for (const { incomingRow, update } of updates) { + const rowKeys = Object.keys(update).sort(); + const rowValues = rowKeys.map((k) => update[k]); + const columnNames = rowKeys.map((key) => + key.replace(/[A-Z]/g, (m) => "_" + m[0].toLowerCase()) + ); + const qs = columnNames.map((c) => `${c} = ?`).join(", "); + const [conditionQs, ...conditionValues] = buildUpdateCondition(incomingRow); + updatePromises.push( + db.execute(`UPDATE ${tableName} SET ${qs} WHERE ${conditionQs};`, [ + ...rowValues, + ...conditionValues, + ]) + ); + } + await Promise.all(updatePromises); } module.exports = { typeDefs, resolvers }; From 9111dfddd3cd8f71d275ed67943ca7de223e5968 Mon Sep 17 00:00:00 2001 From: Matchu Date: Sat, 19 Sep 2020 03:04:19 -0700 Subject: [PATCH 03/22] save item swf assets during modeling --- package.json | 2 +- scripts/setup-mysql-dev-constants.sql | 2 +- scripts/setup-mysql-dev-schema.sql | 33 +++- src/server/loaders.js | 24 +++ .../__snapshots__/Pet.test.js.snap | 164 ++++++++++++++++++ src/server/types/Outfit.js | 55 +++++- 6 files changed, 267 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 17565f3..9fb07c4 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "mysql-dev": "mysql --host=localhost --user=impress_2020_dev --password=impress_2020_dev --database=impress_2020_dev", "mysql-admin": "mysql --host=impress.openneo.net --user=matchu --password --database=openneo_impress", "mysqldump": "mysqldump --host=impress.openneo.net --user=$(dotenv -p IMPRESS_MYSQL_USER) --password=$(dotenv -p IMPRESS_MYSQL_PASSWORD) --column-statistics=0", - "download-mysql-schema": "yarn --silent mysqldump openneo_impress species species_translations colors color_translations > scripts/setup-mysql-dev-constants.sql && yarn --silent mysqldump --no-data openneo_impress items item_translations > scripts/setup-mysql-dev-schema.sql", + "download-mysql-schema": "yarn --silent mysqldump openneo_impress species species_translations colors color_translations > scripts/setup-mysql-dev-constants.sql && yarn --silent mysqldump --no-data openneo_impress items item_translations swf_assets > scripts/setup-mysql-dev-schema.sql", "setup-mysql": "yarn mysql-admin < scripts/setup-mysql.sql", "setup-mysql-dev": "yarn mysql-dev < scripts/setup-mysql-dev-constants.sql && yarn mysql-dev < scripts/setup-mysql-dev-schema.sql", "build-cached-data": "node -r dotenv/config scripts/build-cached-data.js", diff --git a/scripts/setup-mysql-dev-constants.sql b/scripts/setup-mysql-dev-constants.sql index ba7d6f8..b31399b 100644 --- a/scripts/setup-mysql-dev-constants.sql +++ b/scripts/setup-mysql-dev-constants.sql @@ -133,4 +133,4 @@ UNLOCK TABLES; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2020-09-18 6:27:12 +-- Dump completed on 2020-09-19 2:56:30 diff --git a/scripts/setup-mysql-dev-schema.sql b/scripts/setup-mysql-dev-schema.sql index ea3fb0f..458c378 100644 --- a/scripts/setup-mysql-dev-schema.sql +++ b/scripts/setup-mysql-dev-schema.sql @@ -65,7 +65,36 @@ CREATE TABLE `item_translations` ( KEY `index_item_translations_on_locale` (`locale`), KEY `index_item_translations_name` (`name`), KEY `index_item_translations_on_item_id_and_locale` (`item_id`,`locale`) -) ENGINE=InnoDB AUTO_INCREMENT=215758 DEFAULT CHARSET=latin1; +) ENGINE=InnoDB AUTO_INCREMENT=215780 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Table structure for table `swf_assets` +-- + +DROP TABLE IF EXISTS `swf_assets`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `swf_assets` ( + `type` varchar(7) COLLATE utf8_unicode_ci NOT NULL, + `remote_id` mediumint(9) NOT NULL, + `url` mediumtext COLLATE utf8_unicode_ci NOT NULL, + `zone_id` tinyint(4) NOT NULL, + `zones_restrict` text COLLATE utf8_unicode_ci NOT NULL, + `created_at` datetime NOT NULL, + `body_id` smallint(6) NOT NULL, + `has_image` tinyint(1) NOT NULL DEFAULT '0', + `image_requested` tinyint(1) NOT NULL DEFAULT '0', + `reported_broken_at` datetime DEFAULT NULL, + `converted_at` datetime DEFAULT NULL, + `id` int(11) NOT NULL AUTO_INCREMENT, + `image_manual` tinyint(1) NOT NULL DEFAULT '0', + `manifest` text COLLATE utf8_unicode_ci, + PRIMARY KEY (`id`), + KEY `swf_assets_body_id_and_object_id` (`body_id`), + KEY `idx_swf_assets_zone_id` (`zone_id`), + KEY `swf_assets_type_and_id` (`type`,`remote_id`) +) ENGINE=InnoDB AUTO_INCREMENT=521790 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; @@ -77,4 +106,4 @@ CREATE TABLE `item_translations` ( /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2020-09-18 6:27:15 +-- Dump completed on 2020-09-19 2:56:34 diff --git a/src/server/loaders.js b/src/server/loaders.js index 5d713e0..2837bd0 100644 --- a/src/server/loaders.js +++ b/src/server/loaders.js @@ -385,6 +385,29 @@ const buildSwfAssetLoader = (db) => ); }); +const buildSwfAssetByRemoteIdLoader = (db) => + new DataLoader( + async (typeAndRemoteIdPairs) => { + const qs = typeAndRemoteIdPairs + .map((_) => "(type = ? AND remote_id = ?)") + .join(" OR "); + const values = typeAndRemoteIdPairs + .map(({ type, remoteId }) => [type, remoteId]) + .flat(); + const [rows, _] = await db.execute( + `SELECT * FROM swf_assets WHERE ${qs}`, + values + ); + + const entities = rows.map(normalizeRow); + + return swfAssetIds.map((remoteId) => + entities.find((e) => e.remoteId === remoteId) + ); + }, + { cacheKeyFn: ({ type, remoteId }) => `${type},${remoteId}` } + ); + const buildItemSwfAssetLoader = (db, loaders) => new DataLoader( async (itemAndBodyPairs) => { @@ -677,6 +700,7 @@ function buildLoaders(db) { loaders ); loaders.swfAssetLoader = buildSwfAssetLoader(db); + loaders.swfAssetByRemoteIdLoader = buildSwfAssetByRemoteIdLoader(db); loaders.itemSwfAssetLoader = buildItemSwfAssetLoader(db, loaders); loaders.petSwfAssetLoader = buildPetSwfAssetLoader(db, loaders); loaders.outfitLoader = buildOutfitLoader(db); diff --git a/src/server/query-tests/__snapshots__/Pet.test.js.snap b/src/server/query-tests/__snapshots__/Pet.test.js.snap index 7a30925..6040dc3 100644 --- a/src/server/query-tests/__snapshots__/Pet.test.js.snap +++ b/src/server/query-tests/__snapshots__/Pet.test.js.snap @@ -183,6 +183,27 @@ Array [ "48313", ], ], + Array [ + "SELECT * FROM swf_assets WHERE (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?)", + Array [ + "object", + "6829", + "object", + "14855", + "object", + "14856", + "object", + "14857", + "object", + "36414", + "object", + "39646", + "object", + "51959", + "object", + "56478", + ], + ], Array [ "INSERT INTO items (category, created_at, id, price, rarity_index, thumbnail_url, type, updated_at, weight_lbs, zones_restrict) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", Array [ @@ -329,6 +350,67 @@ Array [ 2020-01-01T00:00:00.000Z, ], ], + Array [ + "INSERT INTO swf_assets (body_id, created_at, remote_id, type, url, zone_id, zones_restrict) VALUES (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?);", + Array [ + 180, + 2020-01-01T00:00:00.000Z, + "6829", + "object", + "http://images.neopets.com/cp/items/swf/000/000/006/6829_1707e50385.swf", + 3, + "", + 180, + 2020-01-01T00:00:00.000Z, + "14855", + "object", + "http://images.neopets.com/cp/items/swf/000/000/014/14855_215f367070.swf", + 25, + "", + 180, + 2020-01-01T00:00:00.000Z, + "14856", + "object", + "http://images.neopets.com/cp/items/swf/000/000/014/14856_46c1b32797.swf", + 26, + "", + 180, + 2020-01-01T00:00:00.000Z, + "14857", + "object", + "http://images.neopets.com/cp/items/swf/000/000/014/14857_d43380ef66.swf", + 40, + "", + 180, + 2020-01-01T00:00:00.000Z, + "36414", + "object", + "http://images.neopets.com/cp/items/swf/000/000/036/36414_1e2aaab4ad.swf", + 48, + "", + 180, + 2020-01-01T00:00:00.000Z, + "39646", + "object", + "http://images.neopets.com/cp/items/swf/000/000/039/39646_e129e22ada.swf", + 42, + "", + 180, + 2020-01-01T00:00:00.000Z, + "51959", + "object", + "http://images.neopets.com/cp/items/swf/000/000/051/51959_4439727c48.swf", + 45, + "", + 180, + 2020-01-01T00:00:00.000Z, + "56478", + "object", + "http://images.neopets.com/cp/items/swf/000/000/056/56478_eabc28e7c7.swf", + 27, + "", + ], + ], ] `; @@ -470,6 +552,27 @@ Array [ "48313", ], ], + Array [ + "SELECT * FROM swf_assets WHERE (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?)", + Array [ + "object", + "6829", + "object", + "14855", + "object", + "14856", + "object", + "14857", + "object", + "36414", + "object", + "39646", + "object", + "51959", + "object", + "56478", + ], + ], Array [ "INSERT INTO items (category, created_at, id, price, rarity_index, thumbnail_url, type, updated_at, weight_lbs, zones_restrict) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", Array [ @@ -599,6 +702,67 @@ Array [ 2020-01-01T00:00:00.000Z, ], ], + Array [ + "INSERT INTO swf_assets (body_id, created_at, remote_id, type, url, zone_id, zones_restrict) VALUES (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?);", + Array [ + 180, + 2020-01-01T00:00:00.000Z, + "6829", + "object", + "http://images.neopets.com/cp/items/swf/000/000/006/6829_1707e50385.swf", + 3, + "", + 180, + 2020-01-01T00:00:00.000Z, + "14855", + "object", + "http://images.neopets.com/cp/items/swf/000/000/014/14855_215f367070.swf", + 25, + "", + 180, + 2020-01-01T00:00:00.000Z, + "14856", + "object", + "http://images.neopets.com/cp/items/swf/000/000/014/14856_46c1b32797.swf", + 26, + "", + 180, + 2020-01-01T00:00:00.000Z, + "14857", + "object", + "http://images.neopets.com/cp/items/swf/000/000/014/14857_d43380ef66.swf", + 40, + "", + 180, + 2020-01-01T00:00:00.000Z, + "36414", + "object", + "http://images.neopets.com/cp/items/swf/000/000/036/36414_1e2aaab4ad.swf", + 48, + "", + 180, + 2020-01-01T00:00:00.000Z, + "39646", + "object", + "http://images.neopets.com/cp/items/swf/000/000/039/39646_e129e22ada.swf", + 42, + "", + 180, + 2020-01-01T00:00:00.000Z, + "51959", + "object", + "http://images.neopets.com/cp/items/swf/000/000/051/51959_4439727c48.swf", + 45, + "", + 180, + 2020-01-01T00:00:00.000Z, + "56478", + "object", + "http://images.neopets.com/cp/items/swf/000/000/056/56478_eabc28e7c7.swf", + 27, + "", + ], + ], Array [ "UPDATE items SET rarity_index = ?, thumbnail_url = ?, updated_at = ?, zones_restrict = ? WHERE id = ?;", Array [ diff --git a/src/server/types/Outfit.js b/src/server/types/Outfit.js index dbbf08e..769ce35 100644 --- a/src/server/types/Outfit.js +++ b/src/server/types/Outfit.js @@ -49,7 +49,7 @@ const resolvers = { petOnNeopetsDotCom: async ( _, { petName }, - { db, itemLoader, itemTranslationLoader } + { db, itemLoader, itemTranslationLoader, swfAssetByRemoteIdLoader } ) => { // Start all these requests as soon as possible... const petMetaDataPromise = loadPetMetaData(petName); @@ -59,6 +59,7 @@ const resolvers = { db, itemLoader, itemTranslationLoader, + swfAssetByRemoteIdLoader, }) ); @@ -153,9 +154,10 @@ function getPoseFromPetData(petMetaData, petCustomData) { async function saveModelingData( customPetData, - { db, itemLoader, itemTranslationLoader } + { db, itemLoader, itemTranslationLoader, swfAssetByRemoteIdLoader } ) { const objectInfos = Object.values(customPetData.object_info_registry); + const objectAssets = Object.values(customPetData.object_asset_registry); const incomingItems = objectInfos.map((objectInfo) => ({ id: String(objectInfo.obj_info_id), @@ -176,6 +178,18 @@ async function saveModelingData( rarity: objectInfo.rarity, })); + const incomingItemSwfAssets = objectAssets.map((objectAsset) => ({ + type: "object", + remoteId: String(objectAsset.asset_id), + url: objectAsset.asset_url, + zoneId: objectAsset.zone_id, + zonesRestrict: "", + // TODO: This doesn't actually work... sometimes it needs to be 0, yeah? + // So we actually have to do asset writing after we load the current + // row and compare... maybe a cutesy fn syntax here? + bodyId: customPetData.custom_pet.body_id, + })); + await Promise.all([ syncToDb(db, incomingItems, { loader: itemLoader, @@ -192,6 +206,17 @@ async function saveModelingData( row.itemId, ], }), + syncToDb(db, incomingItemSwfAssets, { + loader: swfAssetByRemoteIdLoader, + tableName: "swf_assets", + buildLoaderKey: (row) => ({ type: row.type, remoteId: row.remoteId }), + buildUpdateCondition: (row) => [ + `type = ? AND remote_id = ?`, + row.type, + row.remoteId, + ], + includeUpdatedAt: false, + }), ]); } @@ -210,7 +235,14 @@ async function saveModelingData( async function syncToDb( db, incomingRows, - { loader, tableName, buildLoaderKey, buildUpdateCondition } + { + loader, + tableName, + buildLoaderKey, + buildUpdateCondition, + includeCreatedAt = true, + includeUpdatedAt = true, + } ) { const loaderKeys = incomingRows.map(buildLoaderKey); const currentRows = await loader.loadMany(loaderKeys); @@ -223,11 +255,14 @@ async function syncToDb( // If there is no corresponding row in the database, prepare an insert. if (currentRow instanceof Error) { - inserts.push({ - ...incomingRow, - createdAt: new Date(), - updatedAt: new Date(), - }); + const insert = { ...incomingRow }; + if (includeCreatedAt) { + insert.createdAt = new Date(); + } + if (includeUpdatedAt) { + insert.updatedAt = new Date(); + } + inserts.push(insert); continue; } @@ -241,7 +276,9 @@ async function syncToDb( for (const key of updatedKeys) { update[key] = incomingRow[key]; } - update.updatedAt = new Date(); + if (includeUpdatedAt) { + update.updatedAt = new Date(); + } updates.push({ incomingRow, update }); } } From ff3fc943d756748c2e7ae286694377cedd9e1017 Mon Sep 17 00:00:00 2001 From: Matchu Date: Sat, 19 Sep 2020 03:28:53 -0700 Subject: [PATCH 04/22] modeling saves pet type --- package.json | 2 +- scripts/setup-mysql-dev-constants.sql | 2 +- scripts/setup-mysql-dev-schema.sql | 22 +++++- src/server/query-tests/Pet.test.js | 14 +++- .../__snapshots__/Pet.test.js.snap | 48 ++++++++++- src/server/types/Outfit.js | 79 +++++++++++++------ src/server/types/PetAppearance.js | 3 + 7 files changed, 140 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 9fb07c4..a6144d8 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "mysql-dev": "mysql --host=localhost --user=impress_2020_dev --password=impress_2020_dev --database=impress_2020_dev", "mysql-admin": "mysql --host=impress.openneo.net --user=matchu --password --database=openneo_impress", "mysqldump": "mysqldump --host=impress.openneo.net --user=$(dotenv -p IMPRESS_MYSQL_USER) --password=$(dotenv -p IMPRESS_MYSQL_PASSWORD) --column-statistics=0", - "download-mysql-schema": "yarn --silent mysqldump openneo_impress species species_translations colors color_translations > scripts/setup-mysql-dev-constants.sql && yarn --silent mysqldump --no-data openneo_impress items item_translations swf_assets > scripts/setup-mysql-dev-schema.sql", + "download-mysql-schema": "yarn --silent mysqldump --no-data openneo_impress items item_translations pet_types pet_states swf_assets > scripts/setup-mysql-dev-schema.sql && yarn --silent mysqldump openneo_impress species species_translations colors color_translations > scripts/setup-mysql-dev-constants.sql", "setup-mysql": "yarn mysql-admin < scripts/setup-mysql.sql", "setup-mysql-dev": "yarn mysql-dev < scripts/setup-mysql-dev-constants.sql && yarn mysql-dev < scripts/setup-mysql-dev-schema.sql", "build-cached-data": "node -r dotenv/config scripts/build-cached-data.js", diff --git a/scripts/setup-mysql-dev-constants.sql b/scripts/setup-mysql-dev-constants.sql index b31399b..edf4a25 100644 --- a/scripts/setup-mysql-dev-constants.sql +++ b/scripts/setup-mysql-dev-constants.sql @@ -133,4 +133,4 @@ UNLOCK TABLES; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2020-09-19 2:56:30 +-- Dump completed on 2020-09-19 3:12:59 diff --git a/scripts/setup-mysql-dev-schema.sql b/scripts/setup-mysql-dev-schema.sql index 458c378..ee50b72 100644 --- a/scripts/setup-mysql-dev-schema.sql +++ b/scripts/setup-mysql-dev-schema.sql @@ -68,6 +68,26 @@ CREATE TABLE `item_translations` ( ) ENGINE=InnoDB AUTO_INCREMENT=215780 DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `pet_types` +-- + +DROP TABLE IF EXISTS `pet_types`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `pet_types` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `color_id` tinyint(4) NOT NULL, + `species_id` tinyint(4) NOT NULL, + `created_at` datetime NOT NULL, + `body_id` smallint(6) NOT NULL, + `image_hash` varchar(8) COLLATE utf8_unicode_ci DEFAULT NULL, + `basic_image_hash` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `pet_types_species_color` (`species_id`,`color_id`) +) ENGINE=InnoDB AUTO_INCREMENT=4795 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `swf_assets` -- @@ -106,4 +126,4 @@ CREATE TABLE `swf_assets` ( /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2020-09-19 2:56:34 +-- Dump completed on 2020-09-19 3:12:53 diff --git a/src/server/query-tests/Pet.test.js b/src/server/query-tests/Pet.test.js index eced112..9233a12 100644 --- a/src/server/query-tests/Pet.test.js +++ b/src/server/query-tests/Pet.test.js @@ -57,7 +57,7 @@ describe("Pet", () => { `); }); - it("models new item data", async () => { + it("models new pet and item data", async () => { useTestDb(); const res = await query({ @@ -86,6 +86,18 @@ describe("Pet", () => { const res2 = await query({ query: gql` query { + petAppearance(colorId: "75", speciesId: "54", pose: SAD_MASC) { + id + pose + layers { + id + swfUrl + } + restrictedZones { + id + } + } + items( ids: [ "37229" diff --git a/src/server/query-tests/__snapshots__/Pet.test.js.snap b/src/server/query-tests/__snapshots__/Pet.test.js.snap index 6040dc3..71143a8 100644 --- a/src/server/query-tests/__snapshots__/Pet.test.js.snap +++ b/src/server/query-tests/__snapshots__/Pet.test.js.snap @@ -82,7 +82,7 @@ Object { } `; -exports[`Pet models new item data 1`] = ` +exports[`Pet models new pet and item data 1`] = ` Object { "petOnNeopetsDotCom": Object { "items": Array [ @@ -155,8 +155,15 @@ Object { } `; -exports[`Pet models new item data 2`] = ` +exports[`Pet models new pet and item data 2`] = ` Array [ + Array [ + "SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?)", + Array [ + "54", + "75", + ], + ], Array [ "SELECT * FROM items WHERE id IN (?,?,?,?,?,?,?,?)", Array [ @@ -204,6 +211,15 @@ Array [ "56478", ], ], + Array [ + "INSERT INTO pet_types (body_id, color_id, created_at, species_id) VALUES (?, ?, ?, ?);", + Array [ + "180", + "75", + 2020-01-01T00:00:00.000Z, + "54", + ], + ], Array [ "INSERT INTO items (category, created_at, id, price, rarity_index, thumbnail_url, type, updated_at, weight_lbs, zones_restrict) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", Array [ @@ -414,7 +430,7 @@ Array [ ] `; -exports[`Pet models new item data 3`] = ` +exports[`Pet models new pet and item data 3`] = ` Object { "items": Array [ Object { @@ -490,11 +506,19 @@ Object { "thumbnailUrl": "http://images.neopets.com/items/clo_altcuplogo_brooch.gif", }, ], + "petAppearance": null, } `; -exports[`Pet models new item data 4`] = ` +exports[`Pet models new pet and item data 4`] = ` Array [ + Array [ + "SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?)", + Array [ + "54", + "75", + ], + ], Array [ "SELECT * FROM item_translations WHERE item_id IN (?,?,?,?,?,?,?,?) AND locale = \\"en\\"", Array [ @@ -526,6 +550,13 @@ Array [ exports[`Pet models updated item data 1`] = ` Array [ + Array [ + "SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?)", + Array [ + "54", + "75", + ], + ], Array [ "SELECT * FROM items WHERE id IN (?,?,?,?,?,?,?,?)", Array [ @@ -573,6 +604,15 @@ Array [ "56478", ], ], + Array [ + "INSERT INTO pet_types (body_id, color_id, created_at, species_id) VALUES (?, ?, ?, ?);", + Array [ + "180", + "75", + 2020-01-01T00:00:00.000Z, + "54", + ], + ], Array [ "INSERT INTO items (category, created_at, id, price, rarity_index, thumbnail_url, type, updated_at, weight_lbs, zones_restrict) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", Array [ diff --git a/src/server/types/Outfit.js b/src/server/types/Outfit.js index 769ce35..d533598 100644 --- a/src/server/types/Outfit.js +++ b/src/server/types/Outfit.js @@ -49,28 +49,27 @@ const resolvers = { petOnNeopetsDotCom: async ( _, { petName }, - { db, itemLoader, itemTranslationLoader, swfAssetByRemoteIdLoader } + { + db, + petTypeBySpeciesAndColorLoader, + itemLoader, + itemTranslationLoader, + swfAssetByRemoteIdLoader, + } ) => { - // Start all these requests as soon as possible... - const petMetaDataPromise = loadPetMetaData(petName); - const customPetDataPromise = loadCustomPetData(petName); - const modelingPromise = customPetDataPromise.then((customPetData) => - saveModelingData(customPetData, { - db, - itemLoader, - itemTranslationLoader, - swfAssetByRemoteIdLoader, - }) - ); - - // ...then wait on all of them before finishing. It's important to wait - // on modeling, so that it doesn't get cut off when the request ends! - const [petMetaData, customPetData, __] = await Promise.all([ - petMetaDataPromise, - customPetDataPromise, - modelingPromise, + const [customPetData, petMetaData, __] = await Promise.all([ + loadCustomPetData(petName), + loadPetMetaData(petName), ]); + await saveModelingData(customPetData, petMetaData, { + db, + petTypeBySpeciesAndColorLoader, + itemLoader, + itemTranslationLoader, + swfAssetByRemoteIdLoader, + }); + 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 @@ -154,8 +153,16 @@ function getPoseFromPetData(petMetaData, petCustomData) { async function saveModelingData( customPetData, - { db, itemLoader, itemTranslationLoader, swfAssetByRemoteIdLoader } + petMetaData, + { + db, + petTypeBySpeciesAndColorLoader, + itemLoader, + itemTranslationLoader, + swfAssetByRemoteIdLoader, + } ) { + const customPet = customPetData.custom_pet; const objectInfos = Object.values(customPetData.object_info_registry); const objectAssets = Object.values(customPetData.object_asset_registry); @@ -187,10 +194,37 @@ async function saveModelingData( // TODO: This doesn't actually work... sometimes it needs to be 0, yeah? // So we actually have to do asset writing after we load the current // row and compare... maybe a cutesy fn syntax here? - bodyId: customPetData.custom_pet.body_id, + bodyId: customPet.body_id, })); + const incomingPetTypes = [ + { + colorId: String(customPet.color_id), + speciesId: String(customPet.species_id), + bodyId: String(customPet.body_id), + // NOTE: I skip the image_hash stuff here... on Rails, we set a hash on + // creation, and may or may not bother to update it, I forget? But + // here I don't want to bother with an update. We could maybe do + // a merge function to make it on create only, but eh, I don't + // care enough ^_^` + }, + ]; + await Promise.all([ + syncToDb(db, incomingPetTypes, { + loader: petTypeBySpeciesAndColorLoader, + tableName: "pet_types", + buildLoaderKey: (row) => ({ + speciesId: row.speciesId, + colorId: row.colorId, + }), + buildUpdateCondition: (row) => [ + `species_id = ? AND color_id = ?`, + row.speciesId, + row.colorId, + ], + includeUpdatedAt: false, + }), syncToDb(db, incomingItems, { loader: itemLoader, tableName: "items", @@ -254,7 +288,8 @@ async function syncToDb( const currentRow = currentRows[index]; // If there is no corresponding row in the database, prepare an insert. - if (currentRow instanceof Error) { + // TODO: Should probably converge on whether not-found is null or an error + if (currentRow == null || currentRow instanceof Error) { const insert = { ...incomingRow }; if (includeCreatedAt) { insert.createdAt = new Date(); diff --git a/src/server/types/PetAppearance.js b/src/server/types/PetAppearance.js index 7bc9233..b804b05 100644 --- a/src/server/types/PetAppearance.js +++ b/src/server/types/PetAppearance.js @@ -208,6 +208,9 @@ const resolvers = { speciesId, colorId, }); + if (!petType) { + return null; + } // TODO: We could query for this more directly, instead of loading all // appearances 🤔 From 5332c9e265ac0e68f6c898ff5bcc4e81d7bd1f50 Mon Sep 17 00:00:00 2001 From: Matchu Date: Sat, 19 Sep 2020 03:46:03 -0700 Subject: [PATCH 05/22] save biology assets on model and start in comments on pet states :) --- scripts/setup-mysql-dev-constants.sql | 2 +- scripts/setup-mysql-dev-schema.sql | 24 +++- .../__snapshots__/Pet.test.js.snap | 125 +++++++++++++++++- src/server/types/Outfit.js | 45 ++++++- 4 files changed, 186 insertions(+), 10 deletions(-) diff --git a/scripts/setup-mysql-dev-constants.sql b/scripts/setup-mysql-dev-constants.sql index edf4a25..b1911cb 100644 --- a/scripts/setup-mysql-dev-constants.sql +++ b/scripts/setup-mysql-dev-constants.sql @@ -133,4 +133,4 @@ UNLOCK TABLES; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2020-09-19 3:12:59 +-- Dump completed on 2020-09-19 3:34:44 diff --git a/scripts/setup-mysql-dev-schema.sql b/scripts/setup-mysql-dev-schema.sql index ee50b72..315d516 100644 --- a/scripts/setup-mysql-dev-schema.sql +++ b/scripts/setup-mysql-dev-schema.sql @@ -88,6 +88,28 @@ CREATE TABLE `pet_types` ( ) ENGINE=InnoDB AUTO_INCREMENT=4795 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `pet_states` +-- + +DROP TABLE IF EXISTS `pet_states`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `pet_states` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `pet_type_id` mediumint(9) NOT NULL, + `swf_asset_ids` text COLLATE utf8_unicode_ci NOT NULL, + `female` tinyint(1) DEFAULT NULL, + `mood_id` int(11) DEFAULT NULL, + `unconverted` tinyint(1) DEFAULT NULL, + `labeled` tinyint(1) NOT NULL DEFAULT '0', + `glitched` tinyint(1) NOT NULL DEFAULT '0', + `artist_neopets_username` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `pet_states_pet_type_id` (`pet_type_id`) +) ENGINE=InnoDB AUTO_INCREMENT=28561 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `swf_assets` -- @@ -126,4 +148,4 @@ CREATE TABLE `swf_assets` ( /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2020-09-19 3:12:53 +-- Dump completed on 2020-09-19 3:34:36 diff --git a/src/server/query-tests/__snapshots__/Pet.test.js.snap b/src/server/query-tests/__snapshots__/Pet.test.js.snap index 71143a8..2e13d2b 100644 --- a/src/server/query-tests/__snapshots__/Pet.test.js.snap +++ b/src/server/query-tests/__snapshots__/Pet.test.js.snap @@ -191,7 +191,7 @@ Array [ ], ], Array [ - "SELECT * FROM swf_assets WHERE (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?)", + "SELECT * FROM swf_assets WHERE (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?)", Array [ "object", "6829", @@ -209,6 +209,18 @@ Array [ "51959", "object", "56478", + "biology", + "7942", + "biology", + "7941", + "biology", + "24008", + "biology", + "21060", + "biology", + "21057", + "biology", + "7946", ], ], Array [ @@ -367,7 +379,7 @@ Array [ ], ], Array [ - "INSERT INTO swf_assets (body_id, created_at, remote_id, type, url, zone_id, zones_restrict) VALUES (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?);", + "INSERT INTO swf_assets (body_id, created_at, remote_id, type, url, zone_id, zones_restrict) VALUES (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?);", Array [ 180, 2020-01-01T00:00:00.000Z, @@ -425,6 +437,48 @@ Array [ "http://images.neopets.com/cp/items/swf/000/000/056/56478_eabc28e7c7.swf", 27, "", + 0, + 2020-01-01T00:00:00.000Z, + "7942", + "biology", + "http://images.neopets.com/cp/bio/swf/000/000/007/7942_2eab06fd7b.swf", + 5, + "0000000000000000000000000000000000000000000000000000", + 0, + 2020-01-01T00:00:00.000Z, + "7941", + "biology", + "http://images.neopets.com/cp/bio/swf/000/000/007/7941_2c4cc4b846.swf", + 15, + "0000000000000000000000000000000000000000000000000000", + 0, + 2020-01-01T00:00:00.000Z, + "24008", + "biology", + "http://images.neopets.com/cp/bio/swf/000/000/024/24008_a05fe9876a.swf", + 30, + "0000000000000000000000000000000000000000000000000000", + 0, + 2020-01-01T00:00:00.000Z, + "21060", + "biology", + "http://images.neopets.com/cp/bio/swf/000/000/021/21060_d77ba93b7b.swf", + 33, + "0000000000000000000000000000000000000000000000000000", + 0, + 2020-01-01T00:00:00.000Z, + "21057", + "biology", + "http://images.neopets.com/cp/bio/swf/000/000/021/21057_4550efbb2f.swf", + 34, + "0000000000000000000000000000000000000000000000000000", + 0, + 2020-01-01T00:00:00.000Z, + "7946", + "biology", + "http://images.neopets.com/cp/bio/swf/000/000/007/7946_0348dad587.swf", + 37, + "0000000000000000000000000000000000000000000000000000", ], ], ] @@ -545,6 +599,15 @@ Array [ "48313", ], ], + Array [ + "SELECT * FROM pet_states + WHERE pet_type_id IN (?) + ORDER BY (mood_id IS NULL) ASC, mood_id ASC, female DESC, + unconverted DESC, glitched ASC, id DESC", + Array [ + "4795", + ], + ], ] `; @@ -584,7 +647,7 @@ Array [ ], ], Array [ - "SELECT * FROM swf_assets WHERE (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?)", + "SELECT * FROM swf_assets WHERE (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?)", Array [ "object", "6829", @@ -602,6 +665,18 @@ Array [ "51959", "object", "56478", + "biology", + "7942", + "biology", + "7941", + "biology", + "24008", + "biology", + "21060", + "biology", + "21057", + "biology", + "7946", ], ], Array [ @@ -743,7 +818,7 @@ Array [ ], ], Array [ - "INSERT INTO swf_assets (body_id, created_at, remote_id, type, url, zone_id, zones_restrict) VALUES (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?);", + "INSERT INTO swf_assets (body_id, created_at, remote_id, type, url, zone_id, zones_restrict) VALUES (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?);", Array [ 180, 2020-01-01T00:00:00.000Z, @@ -801,6 +876,48 @@ Array [ "http://images.neopets.com/cp/items/swf/000/000/056/56478_eabc28e7c7.swf", 27, "", + 0, + 2020-01-01T00:00:00.000Z, + "7942", + "biology", + "http://images.neopets.com/cp/bio/swf/000/000/007/7942_2eab06fd7b.swf", + 5, + "0000000000000000000000000000000000000000000000000000", + 0, + 2020-01-01T00:00:00.000Z, + "7941", + "biology", + "http://images.neopets.com/cp/bio/swf/000/000/007/7941_2c4cc4b846.swf", + 15, + "0000000000000000000000000000000000000000000000000000", + 0, + 2020-01-01T00:00:00.000Z, + "24008", + "biology", + "http://images.neopets.com/cp/bio/swf/000/000/024/24008_a05fe9876a.swf", + 30, + "0000000000000000000000000000000000000000000000000000", + 0, + 2020-01-01T00:00:00.000Z, + "21060", + "biology", + "http://images.neopets.com/cp/bio/swf/000/000/021/21060_d77ba93b7b.swf", + 33, + "0000000000000000000000000000000000000000000000000000", + 0, + 2020-01-01T00:00:00.000Z, + "21057", + "biology", + "http://images.neopets.com/cp/bio/swf/000/000/021/21057_4550efbb2f.swf", + 34, + "0000000000000000000000000000000000000000000000000000", + 0, + 2020-01-01T00:00:00.000Z, + "7946", + "biology", + "http://images.neopets.com/cp/bio/swf/000/000/007/7946_0348dad587.swf", + 37, + "0000000000000000000000000000000000000000000000000000", ], ], Array [ diff --git a/src/server/types/Outfit.js b/src/server/types/Outfit.js index d533598..438946c 100644 --- a/src/server/types/Outfit.js +++ b/src/server/types/Outfit.js @@ -163,9 +163,8 @@ async function saveModelingData( } ) { const customPet = customPetData.custom_pet; - const objectInfos = Object.values(customPetData.object_info_registry); - const objectAssets = Object.values(customPetData.object_asset_registry); + const objectInfos = Object.values(customPetData.object_info_registry); const incomingItems = objectInfos.map((objectInfo) => ({ id: String(objectInfo.obj_info_id), zonesRestrict: objectInfo.zones_restrict, @@ -176,7 +175,6 @@ async function saveModelingData( price: objectInfo.price, weightLbs: objectInfo.weight_lbs, })); - const incomingItemTranslations = objectInfos.map((objectInfo) => ({ itemId: String(objectInfo.obj_info_id), locale: "en", @@ -185,6 +183,7 @@ async function saveModelingData( rarity: objectInfo.rarity, })); + const objectAssets = Object.values(customPetData.object_asset_registry); const incomingItemSwfAssets = objectAssets.map((objectAsset) => ({ type: "object", remoteId: String(objectAsset.asset_id), @@ -197,6 +196,18 @@ async function saveModelingData( bodyId: customPet.body_id, })); + const biologyAssets = Object.values(customPet.biology_by_zone); + const incomingPetSwfAssets = biologyAssets.map((biologyAsset) => ({ + type: "biology", + remoteId: String(biologyAsset.part_id), + url: biologyAsset.asset_url, + zoneId: biologyAsset.zone_id, + zonesRestrict: biologyAsset.zones_restrict, + bodyId: 0, + })); + + const incomingSwfAssets = [...incomingItemSwfAssets, ...incomingPetSwfAssets]; + const incomingPetTypes = [ { colorId: String(customPet.color_id), @@ -240,7 +251,7 @@ async function saveModelingData( row.itemId, ], }), - syncToDb(db, incomingItemSwfAssets, { + syncToDb(db, incomingSwfAssets, { loader: swfAssetByRemoteIdLoader, tableName: "swf_assets", buildLoaderKey: (row) => ({ type: row.type, remoteId: row.remoteId }), @@ -252,6 +263,23 @@ async function saveModelingData( includeUpdatedAt: false, }), ]); + + // TODO: If we look up the potentially existing pet state earlier, then I + // think we can prime the cache and avoid creating a waterfall of + // queries here, even though it looks waterfall-y! + // NOTE: This pet type should have been looked up when syncing pet type, so + // this should be cached. + // const petType = await petTypeBySpeciesAndColorLoader.load({ + // colorId: String(customPet.color_id), + // speciesId: String(customPet.species_id), + // }); + // const incomingPetStates = [ + // { + // petTypeId: petType.id, + // swfAssetIds: incomingPetSwfAssets.map(a => a.remoteId).sort().join(","), + // female: + // }, + // ]; } /** @@ -298,6 +326,11 @@ async function syncToDb( insert.updatedAt = new Date(); } inserts.push(insert); + + // Remove this from the loader cache, so that loading again will fetch + // the inserted row. + loader.clear(buildLoaderKey(incomingRow)); + continue; } @@ -315,6 +348,10 @@ async function syncToDb( update.updatedAt = new Date(); } updates.push({ incomingRow, update }); + + // Remove this from the loader cache, so that loading again will fetch + // the updated row. + loader.clear(buildLoaderKey(incomingRow)); } } From 71f491ce650c58e3fc958bdf85d9adfdab652eb1 Mon Sep 17 00:00:00 2001 From: Matchu Date: Sat, 19 Sep 2020 03:59:02 -0700 Subject: [PATCH 06/22] modeling saves pet state --- src/server/loaders.js | 30 ++++++++++ src/server/query-tests/Pet.test.js | 8 +-- .../__snapshots__/Pet.test.js.snap | 60 ++++++++++++++++++- src/server/types/Outfit.js | 49 +++++++++++---- 4 files changed, 127 insertions(+), 20 deletions(-) diff --git a/src/server/loaders.js b/src/server/loaders.js index 2837bd0..9e5e944 100644 --- a/src/server/loaders.js +++ b/src/server/loaders.js @@ -577,6 +577,33 @@ const buildCanonicalPetStateForBodyLoader = (db, loaders) => ); }); +const buildPetStateByPetTypeAndAssetsLoader = (db) => + new DataLoader( + async (petTypeIdAndAssetIdsPairs) => { + const qs = petTypeIdAndAssetIdsPairs + .map((_) => "(pet_type_id = ? AND swf_asset_ids = ?)") + .join(" OR "); + const values = petTypeIdAndAssetIdsPairs.map( + ({ petTypeId, swfAssetIds }) => [petTypeId, swfAssetIds] + ); + const [rows, _] = await db.execute( + `SELECT * FROM pet_states WHERE ${qs}`, + values + ); + + const entities = rows.map(normalizeRow); + + return petTypeIdAndAssetIdsPairs.map(({ petTypeId, swfAssetIds }) => + entities.find( + (e) => e.petTypeId === petTypeId && e.swfAssetIds === swfAssetIds + ) + ); + }, + { + cacheKeyFn: ({ petTypeId, swfAssetIds }) => `${petTypeId}-${swfAssetIds}`, + } + ); + const buildUserLoader = (db) => new DataLoader(async (ids) => { const qs = ids.map((_) => "?").join(","); @@ -716,6 +743,9 @@ function buildLoaders(db) { db, loaders ); + loaders.petStateByPetTypeAndAssetsLoader = buildPetStateByPetTypeAndAssetsLoader( + db + ); loaders.speciesLoader = buildSpeciesLoader(db); loaders.speciesTranslationLoader = buildSpeciesTranslationLoader(db); loaders.userLoader = buildUserLoader(db); diff --git a/src/server/query-tests/Pet.test.js b/src/server/query-tests/Pet.test.js index 9233a12..3ca5544 100644 --- a/src/server/query-tests/Pet.test.js +++ b/src/server/query-tests/Pet.test.js @@ -89,13 +89,7 @@ describe("Pet", () => { petAppearance(colorId: "75", speciesId: "54", pose: SAD_MASC) { id pose - layers { - id - swfUrl - } - restrictedZones { - id - } + bodyId } items( diff --git a/src/server/query-tests/__snapshots__/Pet.test.js.snap b/src/server/query-tests/__snapshots__/Pet.test.js.snap index 2e13d2b..3f6a1af 100644 --- a/src/server/query-tests/__snapshots__/Pet.test.js.snap +++ b/src/server/query-tests/__snapshots__/Pet.test.js.snap @@ -481,6 +481,33 @@ Array [ "0000000000000000000000000000000000000000000000000000", ], ], + Array [ + "SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?)", + Array [ + "54", + "75", + ], + ], + Array [ + "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", + Array [ + Array [ + "4795", + "21057,21060,24008,7941,7942,7946", + ], + ], + ], + Array [ + "INSERT INTO pet_states (female, labeled, mood_id, pet_type_id, swf_asset_ids, unconverted) VALUES (?, ?, ?, ?, ?, ?);", + Array [ + false, + true, + 2, + "4795", + "21057,21060,24008,7941,7942,7946", + false, + ], + ], ] `; @@ -560,7 +587,11 @@ Object { "thumbnailUrl": "http://images.neopets.com/items/clo_altcuplogo_brooch.gif", }, ], - "petAppearance": null, + "petAppearance": Object { + "bodyId": "180", + "id": "28561", + "pose": "SAD_MASC", + }, } `; @@ -939,6 +970,33 @@ Array [ "43397", ], ], + Array [ + "SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?)", + Array [ + "54", + "75", + ], + ], + Array [ + "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", + Array [ + Array [ + "4795", + "21057,21060,24008,7941,7942,7946", + ], + ], + ], + Array [ + "INSERT INTO pet_states (female, labeled, mood_id, pet_type_id, swf_asset_ids, unconverted) VALUES (?, ?, ?, ?, ?, ?);", + Array [ + false, + true, + 2, + "4795", + "21057,21060,24008,7941,7942,7946", + false, + ], + ], ] `; diff --git a/src/server/types/Outfit.js b/src/server/types/Outfit.js index 438946c..88ba1f5 100644 --- a/src/server/types/Outfit.js +++ b/src/server/types/Outfit.js @@ -52,6 +52,7 @@ const resolvers = { { db, petTypeBySpeciesAndColorLoader, + petStateByPetTypeAndAssetsLoader, itemLoader, itemTranslationLoader, swfAssetByRemoteIdLoader, @@ -65,6 +66,7 @@ const resolvers = { await saveModelingData(customPetData, petMetaData, { db, petTypeBySpeciesAndColorLoader, + petStateByPetTypeAndAssetsLoader, itemLoader, itemTranslationLoader, swfAssetByRemoteIdLoader, @@ -157,6 +159,7 @@ async function saveModelingData( { db, petTypeBySpeciesAndColorLoader, + petStateByPetTypeAndAssetsLoader, itemLoader, itemTranslationLoader, swfAssetByRemoteIdLoader, @@ -266,20 +269,42 @@ async function saveModelingData( // TODO: If we look up the potentially existing pet state earlier, then I // think we can prime the cache and avoid creating a waterfall of - // queries here, even though it looks waterfall-y! + // queries here in the happy case, even though it'll look waterfall-y! // NOTE: This pet type should have been looked up when syncing pet type, so // this should be cached. - // const petType = await petTypeBySpeciesAndColorLoader.load({ - // colorId: String(customPet.color_id), - // speciesId: String(customPet.species_id), - // }); - // const incomingPetStates = [ - // { - // petTypeId: petType.id, - // swfAssetIds: incomingPetSwfAssets.map(a => a.remoteId).sort().join(","), - // female: - // }, - // ]; + const petType = await petTypeBySpeciesAndColorLoader.load({ + colorId: String(customPet.color_id), + speciesId: String(customPet.species_id), + }); + const incomingPetStates = [ + { + petTypeId: petType.id, + swfAssetIds: incomingPetSwfAssets + .map((a) => a.remoteId) + .sort() + .join(","), + female: petMetaData.gender === 2, // sorry for this column name :/ + moodId: petMetaData.mood, + unconverted: incomingPetSwfAssets.length === 1, + labeled: true, + }, + ]; + + await syncToDb(db, incomingPetStates, { + loader: petStateByPetTypeAndAssetsLoader, + tableName: "pet_states", + buildLoaderKey: (row) => ({ + petTypeId: row.petTypeId, + swfAssetIds: row.swfAssetIds, + }), + buildUpdateCondition: (row) => [ + `pet_type_id = ? AND swf_asset_ids = ?`, + row.petTypeId, + row.swfAssetIds, + ], + includeCreatedAt: false, + includeUpdatedAt: false, + }); } /** From 50537758c5bb89f8fa902054930d282fd32c108b Mon Sep 17 00:00:00 2001 From: Matchu Date: Sat, 19 Sep 2020 04:01:29 -0700 Subject: [PATCH 07/22] start test/dev db IDs at 1, not wherever prod is We download the schema from prod, and omit real data, but I didn't notice that we were still pulling the metadata of the auto increment counter for IDs! Now, we scrub that from the schema file we save. --- package.json | 2 +- scripts/setup-mysql-dev-constants.sql | 2 +- scripts/setup-mysql-dev-schema.sql | 12 ++++++------ .../query-tests/__snapshots__/Pet.test.js.snap | 12 ++++++------ 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index a6144d8..02c8668 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "mysql-dev": "mysql --host=localhost --user=impress_2020_dev --password=impress_2020_dev --database=impress_2020_dev", "mysql-admin": "mysql --host=impress.openneo.net --user=matchu --password --database=openneo_impress", "mysqldump": "mysqldump --host=impress.openneo.net --user=$(dotenv -p IMPRESS_MYSQL_USER) --password=$(dotenv -p IMPRESS_MYSQL_PASSWORD) --column-statistics=0", - "download-mysql-schema": "yarn --silent mysqldump --no-data openneo_impress items item_translations pet_types pet_states swf_assets > scripts/setup-mysql-dev-schema.sql && yarn --silent mysqldump openneo_impress species species_translations colors color_translations > scripts/setup-mysql-dev-constants.sql", + "download-mysql-schema": "yarn --silent mysqldump --no-data openneo_impress items item_translations pet_types pet_states swf_assets | sed 's/ AUTO_INCREMENT=[0-9]*//g' > scripts/setup-mysql-dev-schema.sql && yarn --silent mysqldump openneo_impress species species_translations colors color_translations > scripts/setup-mysql-dev-constants.sql", "setup-mysql": "yarn mysql-admin < scripts/setup-mysql.sql", "setup-mysql-dev": "yarn mysql-dev < scripts/setup-mysql-dev-constants.sql && yarn mysql-dev < scripts/setup-mysql-dev-schema.sql", "build-cached-data": "node -r dotenv/config scripts/build-cached-data.js", diff --git a/scripts/setup-mysql-dev-constants.sql b/scripts/setup-mysql-dev-constants.sql index b1911cb..943c270 100644 --- a/scripts/setup-mysql-dev-constants.sql +++ b/scripts/setup-mysql-dev-constants.sql @@ -133,4 +133,4 @@ UNLOCK TABLES; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2020-09-19 3:34:44 +-- Dump completed on 2020-09-19 4:00:01 diff --git a/scripts/setup-mysql-dev-schema.sql b/scripts/setup-mysql-dev-schema.sql index 315d516..63c252b 100644 --- a/scripts/setup-mysql-dev-schema.sql +++ b/scripts/setup-mysql-dev-schema.sql @@ -41,7 +41,7 @@ CREATE TABLE `items` ( `modeling_status_hint` enum('done','glitchy') COLLATE utf8_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), KEY `objects_last_spidered` (`last_spidered`) -) ENGINE=InnoDB AUTO_INCREMENT=81718 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -65,7 +65,7 @@ CREATE TABLE `item_translations` ( KEY `index_item_translations_on_locale` (`locale`), KEY `index_item_translations_name` (`name`), KEY `index_item_translations_on_item_id_and_locale` (`item_id`,`locale`) -) ENGINE=InnoDB AUTO_INCREMENT=215780 DEFAULT CHARSET=latin1; +) ENGINE=InnoDB DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -85,7 +85,7 @@ CREATE TABLE `pet_types` ( `basic_image_hash` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `pet_types_species_color` (`species_id`,`color_id`) -) ENGINE=InnoDB AUTO_INCREMENT=4795 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -107,7 +107,7 @@ CREATE TABLE `pet_states` ( `artist_neopets_username` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), KEY `pet_states_pet_type_id` (`pet_type_id`) -) ENGINE=InnoDB AUTO_INCREMENT=28561 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -- @@ -136,7 +136,7 @@ CREATE TABLE `swf_assets` ( KEY `swf_assets_body_id_and_object_id` (`body_id`), KEY `idx_swf_assets_zone_id` (`zone_id`), KEY `swf_assets_type_and_id` (`type`,`remote_id`) -) ENGINE=InnoDB AUTO_INCREMENT=521790 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; @@ -148,4 +148,4 @@ CREATE TABLE `swf_assets` ( /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2020-09-19 3:34:36 +-- Dump completed on 2020-09-19 3:59:55 diff --git a/src/server/query-tests/__snapshots__/Pet.test.js.snap b/src/server/query-tests/__snapshots__/Pet.test.js.snap index 3f6a1af..b6c1a55 100644 --- a/src/server/query-tests/__snapshots__/Pet.test.js.snap +++ b/src/server/query-tests/__snapshots__/Pet.test.js.snap @@ -492,7 +492,7 @@ Array [ "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", Array [ Array [ - "4795", + "1", "21057,21060,24008,7941,7942,7946", ], ], @@ -503,7 +503,7 @@ Array [ false, true, 2, - "4795", + "1", "21057,21060,24008,7941,7942,7946", false, ], @@ -589,7 +589,7 @@ Object { ], "petAppearance": Object { "bodyId": "180", - "id": "28561", + "id": "1", "pose": "SAD_MASC", }, } @@ -636,7 +636,7 @@ Array [ ORDER BY (mood_id IS NULL) ASC, mood_id ASC, female DESC, unconverted DESC, glitched ASC, id DESC", Array [ - "4795", + "1", ], ], ] @@ -981,7 +981,7 @@ Array [ "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", Array [ Array [ - "4795", + "1", "21057,21060,24008,7941,7942,7946", ], ], @@ -992,7 +992,7 @@ Array [ false, true, 2, - "4795", + "1", "21057,21060,24008,7941,7942,7946", false, ], From 41e70ba8d09ffa5f91fc53796b45a76b73fbd197 Mon Sep 17 00:00:00 2001 From: Matchu Date: Sat, 19 Sep 2020 04:22:39 -0700 Subject: [PATCH 08/22] finish modeling full pet appearance --- package.json | 2 +- scripts/setup-mysql-dev-constants.sql | 2 +- scripts/setup-mysql-dev-schema.sql | 21 ++- src/server/loaders.js | 10 +- src/server/query-tests/Pet.test.js | 8 + .../__snapshots__/Pet.test.js.snap | 145 +++++++++++++++++- src/server/types/Outfit.js | 83 ++++++---- 7 files changed, 228 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index 02c8668..480f3f5 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "mysql-dev": "mysql --host=localhost --user=impress_2020_dev --password=impress_2020_dev --database=impress_2020_dev", "mysql-admin": "mysql --host=impress.openneo.net --user=matchu --password --database=openneo_impress", "mysqldump": "mysqldump --host=impress.openneo.net --user=$(dotenv -p IMPRESS_MYSQL_USER) --password=$(dotenv -p IMPRESS_MYSQL_PASSWORD) --column-statistics=0", - "download-mysql-schema": "yarn --silent mysqldump --no-data openneo_impress items item_translations pet_types pet_states swf_assets | sed 's/ AUTO_INCREMENT=[0-9]*//g' > scripts/setup-mysql-dev-schema.sql && yarn --silent mysqldump openneo_impress species species_translations colors color_translations > scripts/setup-mysql-dev-constants.sql", + "download-mysql-schema": "yarn --silent mysqldump --no-data openneo_impress items item_translations parents_swf_assets pet_types pet_states swf_assets | sed 's/ AUTO_INCREMENT=[0-9]*//g' > scripts/setup-mysql-dev-schema.sql && yarn --silent mysqldump openneo_impress species species_translations colors color_translations > scripts/setup-mysql-dev-constants.sql", "setup-mysql": "yarn mysql-admin < scripts/setup-mysql.sql", "setup-mysql-dev": "yarn mysql-dev < scripts/setup-mysql-dev-constants.sql && yarn mysql-dev < scripts/setup-mysql-dev-schema.sql", "build-cached-data": "node -r dotenv/config scripts/build-cached-data.js", diff --git a/scripts/setup-mysql-dev-constants.sql b/scripts/setup-mysql-dev-constants.sql index 943c270..0180c57 100644 --- a/scripts/setup-mysql-dev-constants.sql +++ b/scripts/setup-mysql-dev-constants.sql @@ -133,4 +133,4 @@ UNLOCK TABLES; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2020-09-19 4:00:01 +-- Dump completed on 2020-09-19 4:19:07 diff --git a/scripts/setup-mysql-dev-schema.sql b/scripts/setup-mysql-dev-schema.sql index 63c252b..2623c24 100644 --- a/scripts/setup-mysql-dev-schema.sql +++ b/scripts/setup-mysql-dev-schema.sql @@ -68,6 +68,25 @@ CREATE TABLE `item_translations` ( ) ENGINE=InnoDB DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `parents_swf_assets` +-- + +DROP TABLE IF EXISTS `parents_swf_assets`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `parents_swf_assets` ( + `parent_id` mediumint(9) NOT NULL, + `swf_asset_id` mediumint(9) NOT NULL, + `id` int(11) NOT NULL AUTO_INCREMENT, + `parent_type` varchar(8) COLLATE utf8_unicode_ci NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `unique_parents_swf_assets` (`parent_id`,`swf_asset_id`), + KEY `parents_swf_assets_swf_asset_id` (`swf_asset_id`), + KEY `index_parents_swf_assets_on_parent_id_and_parent_type` (`parent_id`,`parent_type`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `pet_types` -- @@ -148,4 +167,4 @@ CREATE TABLE `swf_assets` ( /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2020-09-19 3:59:55 +-- Dump completed on 2020-09-19 4:19:01 diff --git a/src/server/loaders.js b/src/server/loaders.js index 9e5e944..39168be 100644 --- a/src/server/loaders.js +++ b/src/server/loaders.js @@ -401,8 +401,8 @@ const buildSwfAssetByRemoteIdLoader = (db) => const entities = rows.map(normalizeRow); - return swfAssetIds.map((remoteId) => - entities.find((e) => e.remoteId === remoteId) + return typeAndRemoteIdPairs.map(({ type, remoteId }) => + entities.find((e) => e.type === type && e.remoteId === remoteId) ); }, { cacheKeyFn: ({ type, remoteId }) => `${type},${remoteId}` } @@ -583,9 +583,9 @@ const buildPetStateByPetTypeAndAssetsLoader = (db) => const qs = petTypeIdAndAssetIdsPairs .map((_) => "(pet_type_id = ? AND swf_asset_ids = ?)") .join(" OR "); - const values = petTypeIdAndAssetIdsPairs.map( - ({ petTypeId, swfAssetIds }) => [petTypeId, swfAssetIds] - ); + const values = petTypeIdAndAssetIdsPairs + .map(({ petTypeId, swfAssetIds }) => [petTypeId, swfAssetIds]) + .flat(); const [rows, _] = await db.execute( `SELECT * FROM pet_states WHERE ${qs}`, values diff --git a/src/server/query-tests/Pet.test.js b/src/server/query-tests/Pet.test.js index 3ca5544..5f9a51d 100644 --- a/src/server/query-tests/Pet.test.js +++ b/src/server/query-tests/Pet.test.js @@ -90,6 +90,14 @@ describe("Pet", () => { id pose bodyId + + restrictedZones { + id + } + layers { + id + swfUrl + } } items( diff --git a/src/server/query-tests/__snapshots__/Pet.test.js.snap b/src/server/query-tests/__snapshots__/Pet.test.js.snap index b6c1a55..bd6831b 100644 --- a/src/server/query-tests/__snapshots__/Pet.test.js.snap +++ b/src/server/query-tests/__snapshots__/Pet.test.js.snap @@ -491,10 +491,8 @@ Array [ Array [ "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", Array [ - Array [ - "1", - "21057,21060,24008,7941,7942,7946", - ], + "1", + "21057,21060,24008,7941,7942,7946", ], ], Array [ @@ -508,6 +506,54 @@ Array [ false, ], ], + Array [ + "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", + Array [ + "1", + "21057,21060,24008,7941,7942,7946", + ], + ], + Array [ + "SELECT * FROM swf_assets WHERE (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?)", + Array [ + "biology", + "7942", + "biology", + "7941", + "biology", + "24008", + "biology", + "21060", + "biology", + "21057", + "biology", + "7946", + ], + ], + Array [ + "INSERT INTO parents_swf_assets (parent_type, parent_id, swf_asset_id) + VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?), (?, ?, ?), (?, ?, ?), (?, ?, ?);", + Array [ + "PetState", + "1", + "9", + "PetState", + "1", + "10", + "PetState", + "1", + "11", + "PetState", + "1", + "12", + "PetState", + "1", + "13", + "PetState", + "1", + "14", + ], + ], ] `; @@ -590,7 +636,34 @@ Object { "petAppearance": Object { "bodyId": "180", "id": "1", + "layers": Array [ + Object { + "id": "9", + "swfUrl": "http://images.neopets.com/cp/bio/swf/000/000/007/7942_2eab06fd7b.swf", + }, + Object { + "id": "10", + "swfUrl": "http://images.neopets.com/cp/bio/swf/000/000/007/7941_2c4cc4b846.swf", + }, + Object { + "id": "11", + "swfUrl": "http://images.neopets.com/cp/bio/swf/000/000/024/24008_a05fe9876a.swf", + }, + Object { + "id": "12", + "swfUrl": "http://images.neopets.com/cp/bio/swf/000/000/021/21060_d77ba93b7b.swf", + }, + Object { + "id": "13", + "swfUrl": "http://images.neopets.com/cp/bio/swf/000/000/021/21057_4550efbb2f.swf", + }, + Object { + "id": "14", + "swfUrl": "http://images.neopets.com/cp/bio/swf/000/000/007/7946_0348dad587.swf", + }, + ], "pose": "SAD_MASC", + "restrictedZones": Array [], }, } `; @@ -639,6 +712,16 @@ Array [ "1", ], ], + Array [ + "SELECT sa.*, rel.parent_id FROM swf_assets sa + INNER JOIN parents_swf_assets rel ON + rel.parent_type = \\"PetState\\" AND + rel.swf_asset_id = sa.id + WHERE rel.parent_id IN (?)", + Array [ + "1", + ], + ], ] `; @@ -980,10 +1063,8 @@ Array [ Array [ "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", Array [ - Array [ - "1", - "21057,21060,24008,7941,7942,7946", - ], + "1", + "21057,21060,24008,7941,7942,7946", ], ], Array [ @@ -997,6 +1078,54 @@ Array [ false, ], ], + Array [ + "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", + Array [ + "1", + "21057,21060,24008,7941,7942,7946", + ], + ], + Array [ + "SELECT * FROM swf_assets WHERE (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?)", + Array [ + "biology", + "7942", + "biology", + "7941", + "biology", + "24008", + "biology", + "21060", + "biology", + "21057", + "biology", + "7946", + ], + ], + Array [ + "INSERT INTO parents_swf_assets (parent_type, parent_id, swf_asset_id) + VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?), (?, ?, ?), (?, ?, ?), (?, ?, ?);", + Array [ + "PetState", + "1", + "9", + "PetState", + "1", + "10", + "PetState", + "1", + "11", + "PetState", + "1", + "12", + "PetState", + "1", + "13", + "PetState", + "1", + "14", + ], + ], ] `; diff --git a/src/server/types/Outfit.js b/src/server/types/Outfit.js index 88ba1f5..4122c61 100644 --- a/src/server/types/Outfit.js +++ b/src/server/types/Outfit.js @@ -211,21 +211,19 @@ async function saveModelingData( const incomingSwfAssets = [...incomingItemSwfAssets, ...incomingPetSwfAssets]; - const incomingPetTypes = [ - { - colorId: String(customPet.color_id), - speciesId: String(customPet.species_id), - bodyId: String(customPet.body_id), - // NOTE: I skip the image_hash stuff here... on Rails, we set a hash on - // creation, and may or may not bother to update it, I forget? But - // here I don't want to bother with an update. We could maybe do - // a merge function to make it on create only, but eh, I don't - // care enough ^_^` - }, - ]; + const incomingPetType = { + colorId: String(customPet.color_id), + speciesId: String(customPet.species_id), + bodyId: String(customPet.body_id), + // NOTE: I skip the image_hash stuff here... on Rails, we set a hash on + // creation, and may or may not bother to update it, I forget? But + // here I don't want to bother with an update. We could maybe do + // a merge function to make it on create only, but eh, I don't + // care enough ^_^` + }; await Promise.all([ - syncToDb(db, incomingPetTypes, { + syncToDb(db, [incomingPetType], { loader: petTypeBySpeciesAndColorLoader, tableName: "pet_types", buildLoaderKey: (row) => ({ @@ -276,21 +274,19 @@ async function saveModelingData( colorId: String(customPet.color_id), speciesId: String(customPet.species_id), }); - const incomingPetStates = [ - { - petTypeId: petType.id, - swfAssetIds: incomingPetSwfAssets - .map((a) => a.remoteId) - .sort() - .join(","), - female: petMetaData.gender === 2, // sorry for this column name :/ - moodId: petMetaData.mood, - unconverted: incomingPetSwfAssets.length === 1, - labeled: true, - }, - ]; + const incomingPetState = { + petTypeId: petType.id, + swfAssetIds: incomingPetSwfAssets + .map((a) => a.remoteId) + .sort() + .join(","), + female: petMetaData.gender === 2, // sorry for this column name :/ + moodId: petMetaData.mood, + unconverted: incomingPetSwfAssets.length === 1, + labeled: true, + }; - await syncToDb(db, incomingPetStates, { + await syncToDb(db, [incomingPetState], { loader: petStateByPetTypeAndAssetsLoader, tableName: "pet_states", buildLoaderKey: (row) => ({ @@ -304,6 +300,35 @@ async function saveModelingData( ], includeCreatedAt: false, includeUpdatedAt: false, + // For pet states, syncing assets is easy: a new set of assets counts as a + // new state, so, whatever! Just insert the relationships when inserting + // the pet state, and ignore them any other time. + afterInsert: async () => { + // We need to load from the db to get the actual inserted IDs. Not lovely + // for perf, but this is a real new-data model, so that's fine! + let [petState, swfAssets] = await Promise.all([ + petStateByPetTypeAndAssetsLoader.load({ + petTypeId: incomingPetState.petTypeId, + swfAssetIds: incomingPetState.swfAssetIds, + }), + swfAssetByRemoteIdLoader.loadMany( + incomingPetSwfAssets.map((row) => ({ + type: row.type, + remoteId: row.remoteId, + })) + ), + ]); + swfAssets = swfAssets.filter((sa) => sa != null); + const qs = swfAssets.map((_) => `(?, ?, ?)`).join(", "); + const values = swfAssets + .map((sa) => ["PetState", petState.id, sa.id]) + .flat(); + await db.execute( + `INSERT INTO parents_swf_assets (parent_type, parent_id, swf_asset_id) + VALUES ${qs};`, + values + ); + }, }); } @@ -329,6 +354,7 @@ async function syncToDb( buildUpdateCondition, includeCreatedAt = true, includeUpdatedAt = true, + afterInsert = null, } ) { const loaderKeys = incomingRows.map(buildLoaderKey); @@ -396,6 +422,9 @@ async function syncToDb( `INSERT INTO ${tableName} (${columnsStr}) VALUES ${rowQs};`, rowFields.flat() ); + if (afterInsert) { + await afterInsert(); + } } // Do parallel updates of anything that needs updated. From 96a126ebbaf4ebef80a26d6357648fe4891f74d6 Mon Sep 17 00:00:00 2001 From: Matchu Date: Sat, 19 Sep 2020 04:39:08 -0700 Subject: [PATCH 09/22] oops, stop sending unnecessary inserts/updates got the types wrong on some stuff, and got pet state sorting wrong! --- src/server/db.js | 12 +- src/server/query-tests/Pet.test.js | 96 ++++++++++++ .../__snapshots__/Pet.test.js.snap | 140 +++++++++--------- src/server/types/Outfit.js | 20 +-- 4 files changed, 183 insertions(+), 85 deletions(-) diff --git a/src/server/db.js b/src/server/db.js index a981f87..f06b9d9 100644 --- a/src/server/db.js +++ b/src/server/db.js @@ -1,6 +1,6 @@ const mysql = require("mysql2"); -let globalDb; +let globalDbs = new Map(); async function connectToDb({ host = "impress.openneo.net", @@ -8,11 +8,11 @@ async function connectToDb({ password = process.env["IMPRESS_MYSQL_PASSWORD"], database = "openneo_impress", } = {}) { - if (globalDb) { - return globalDb; + if (globalDbs.has(host)) { + return globalDbs.get(host); } - globalDb = mysql + const db = mysql .createConnection({ host, user, @@ -24,7 +24,9 @@ async function connectToDb({ // for compatibility with Honeycomb's automatic tracing. .promise(); - return globalDb; + globalDbs.set(host, db); + + return db; } module.exports = connectToDb; diff --git a/src/server/query-tests/Pet.test.js b/src/server/query-tests/Pet.test.js index 5f9a51d..3a5f18f 100644 --- a/src/server/query-tests/Pet.test.js +++ b/src/server/query-tests/Pet.test.js @@ -39,6 +39,79 @@ describe("Pet", () => { expect(res.data).toMatchSnapshot(); expect(getDbCalls()).toMatchInlineSnapshot(` Array [ + Array [ + "SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?)", + Array [ + "54", + "75", + ], + ], + Array [ + "SELECT * FROM items WHERE id IN (?,?,?,?,?,?,?,?)", + Array [ + "37229", + "37375", + "38911", + "38912", + "38913", + "43014", + "43397", + "48313", + ], + ], + Array [ + "SELECT * FROM item_translations WHERE item_id IN (?,?,?,?,?,?,?,?) AND locale = \\"en\\"", + Array [ + "37229", + "37375", + "38911", + "38912", + "38913", + "43014", + "43397", + "48313", + ], + ], + Array [ + "SELECT * FROM swf_assets WHERE (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?)", + Array [ + "object", + "6829", + "object", + "14855", + "object", + "14856", + "object", + "14857", + "object", + "36414", + "object", + "39646", + "object", + "51959", + "object", + "56478", + "biology", + "7942", + "biology", + "7941", + "biology", + "24008", + "biology", + "21060", + "biology", + "21057", + "biology", + "7946", + ], + ], + Array [ + "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", + Array [ + "2", + "7941,7942,7946,21057,21060,24008", + ], + ], Array [ "SELECT * FROM species_translations WHERE species_id IN (?) AND locale = \\"en\\"", @@ -127,6 +200,29 @@ describe("Pet", () => { expect(res2).toHaveNoErrors(); expect(res2.data).toMatchSnapshot(); expect(getDbCalls()).toMatchSnapshot(); + + clearDbCalls(); + + // If we load the pet again, it should only make SELECT queries, not + // INSERT or UPDATE. + await query({ + query: gql` + query { + petOnNeopetsDotCom(petName: "roopal27") { + items { + id + } + } + } + `, + }); + + const dbCalls = getDbCalls(); + for (const [query, _] of dbCalls) { + expect(query).toMatch(/SELECT/); + expect(query).not.toMatch(/INSERT/); + expect(query).not.toMatch(/UPDATE/); + } }); it("models updated item data", async () => { diff --git a/src/server/query-tests/__snapshots__/Pet.test.js.snap b/src/server/query-tests/__snapshots__/Pet.test.js.snap index bd6831b..ab56c0d 100644 --- a/src/server/query-tests/__snapshots__/Pet.test.js.snap +++ b/src/server/query-tests/__snapshots__/Pet.test.js.snap @@ -381,103 +381,103 @@ Array [ Array [ "INSERT INTO swf_assets (body_id, created_at, remote_id, type, url, zone_id, zones_restrict) VALUES (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?);", Array [ - 180, + "180", 2020-01-01T00:00:00.000Z, "6829", "object", "http://images.neopets.com/cp/items/swf/000/000/006/6829_1707e50385.swf", - 3, + "3", "", - 180, + "180", 2020-01-01T00:00:00.000Z, "14855", "object", "http://images.neopets.com/cp/items/swf/000/000/014/14855_215f367070.swf", - 25, + "25", "", - 180, + "180", 2020-01-01T00:00:00.000Z, "14856", "object", "http://images.neopets.com/cp/items/swf/000/000/014/14856_46c1b32797.swf", - 26, + "26", "", - 180, + "180", 2020-01-01T00:00:00.000Z, "14857", "object", "http://images.neopets.com/cp/items/swf/000/000/014/14857_d43380ef66.swf", - 40, + "40", "", - 180, + "180", 2020-01-01T00:00:00.000Z, "36414", "object", "http://images.neopets.com/cp/items/swf/000/000/036/36414_1e2aaab4ad.swf", - 48, + "48", "", - 180, + "180", 2020-01-01T00:00:00.000Z, "39646", "object", "http://images.neopets.com/cp/items/swf/000/000/039/39646_e129e22ada.swf", - 42, + "42", "", - 180, + "180", 2020-01-01T00:00:00.000Z, "51959", "object", "http://images.neopets.com/cp/items/swf/000/000/051/51959_4439727c48.swf", - 45, + "45", "", - 180, + "180", 2020-01-01T00:00:00.000Z, "56478", "object", "http://images.neopets.com/cp/items/swf/000/000/056/56478_eabc28e7c7.swf", - 27, + "27", "", - 0, + "0", 2020-01-01T00:00:00.000Z, "7942", "biology", "http://images.neopets.com/cp/bio/swf/000/000/007/7942_2eab06fd7b.swf", - 5, + "5", "0000000000000000000000000000000000000000000000000000", - 0, + "0", 2020-01-01T00:00:00.000Z, "7941", "biology", "http://images.neopets.com/cp/bio/swf/000/000/007/7941_2c4cc4b846.swf", - 15, + "15", "0000000000000000000000000000000000000000000000000000", - 0, + "0", 2020-01-01T00:00:00.000Z, "24008", "biology", "http://images.neopets.com/cp/bio/swf/000/000/024/24008_a05fe9876a.swf", - 30, + "30", "0000000000000000000000000000000000000000000000000000", - 0, + "0", 2020-01-01T00:00:00.000Z, "21060", "biology", "http://images.neopets.com/cp/bio/swf/000/000/021/21060_d77ba93b7b.swf", - 33, + "33", "0000000000000000000000000000000000000000000000000000", - 0, + "0", 2020-01-01T00:00:00.000Z, "21057", "biology", "http://images.neopets.com/cp/bio/swf/000/000/021/21057_4550efbb2f.swf", - 34, + "34", "0000000000000000000000000000000000000000000000000000", - 0, + "0", 2020-01-01T00:00:00.000Z, "7946", "biology", "http://images.neopets.com/cp/bio/swf/000/000/007/7946_0348dad587.swf", - 37, + "37", "0000000000000000000000000000000000000000000000000000", ], ], @@ -492,25 +492,25 @@ Array [ "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", Array [ "1", - "21057,21060,24008,7941,7942,7946", + "7941,7942,7946,21057,21060,24008", ], ], Array [ "INSERT INTO pet_states (female, labeled, mood_id, pet_type_id, swf_asset_ids, unconverted) VALUES (?, ?, ?, ?, ?, ?);", Array [ - false, - true, - 2, + 0, + 1, + "2", "1", - "21057,21060,24008,7941,7942,7946", - false, + "7941,7942,7946,21057,21060,24008", + 0, ], ], Array [ "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", Array [ "1", - "21057,21060,24008,7941,7942,7946", + "7941,7942,7946,21057,21060,24008", ], ], Array [ @@ -934,103 +934,103 @@ Array [ Array [ "INSERT INTO swf_assets (body_id, created_at, remote_id, type, url, zone_id, zones_restrict) VALUES (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?);", Array [ - 180, + "180", 2020-01-01T00:00:00.000Z, "6829", "object", "http://images.neopets.com/cp/items/swf/000/000/006/6829_1707e50385.swf", - 3, + "3", "", - 180, + "180", 2020-01-01T00:00:00.000Z, "14855", "object", "http://images.neopets.com/cp/items/swf/000/000/014/14855_215f367070.swf", - 25, + "25", "", - 180, + "180", 2020-01-01T00:00:00.000Z, "14856", "object", "http://images.neopets.com/cp/items/swf/000/000/014/14856_46c1b32797.swf", - 26, + "26", "", - 180, + "180", 2020-01-01T00:00:00.000Z, "14857", "object", "http://images.neopets.com/cp/items/swf/000/000/014/14857_d43380ef66.swf", - 40, + "40", "", - 180, + "180", 2020-01-01T00:00:00.000Z, "36414", "object", "http://images.neopets.com/cp/items/swf/000/000/036/36414_1e2aaab4ad.swf", - 48, + "48", "", - 180, + "180", 2020-01-01T00:00:00.000Z, "39646", "object", "http://images.neopets.com/cp/items/swf/000/000/039/39646_e129e22ada.swf", - 42, + "42", "", - 180, + "180", 2020-01-01T00:00:00.000Z, "51959", "object", "http://images.neopets.com/cp/items/swf/000/000/051/51959_4439727c48.swf", - 45, + "45", "", - 180, + "180", 2020-01-01T00:00:00.000Z, "56478", "object", "http://images.neopets.com/cp/items/swf/000/000/056/56478_eabc28e7c7.swf", - 27, + "27", "", - 0, + "0", 2020-01-01T00:00:00.000Z, "7942", "biology", "http://images.neopets.com/cp/bio/swf/000/000/007/7942_2eab06fd7b.swf", - 5, + "5", "0000000000000000000000000000000000000000000000000000", - 0, + "0", 2020-01-01T00:00:00.000Z, "7941", "biology", "http://images.neopets.com/cp/bio/swf/000/000/007/7941_2c4cc4b846.swf", - 15, + "15", "0000000000000000000000000000000000000000000000000000", - 0, + "0", 2020-01-01T00:00:00.000Z, "24008", "biology", "http://images.neopets.com/cp/bio/swf/000/000/024/24008_a05fe9876a.swf", - 30, + "30", "0000000000000000000000000000000000000000000000000000", - 0, + "0", 2020-01-01T00:00:00.000Z, "21060", "biology", "http://images.neopets.com/cp/bio/swf/000/000/021/21060_d77ba93b7b.swf", - 33, + "33", "0000000000000000000000000000000000000000000000000000", - 0, + "0", 2020-01-01T00:00:00.000Z, "21057", "biology", "http://images.neopets.com/cp/bio/swf/000/000/021/21057_4550efbb2f.swf", - 34, + "34", "0000000000000000000000000000000000000000000000000000", - 0, + "0", 2020-01-01T00:00:00.000Z, "7946", "biology", "http://images.neopets.com/cp/bio/swf/000/000/007/7946_0348dad587.swf", - 37, + "37", "0000000000000000000000000000000000000000000000000000", ], ], @@ -1064,25 +1064,25 @@ Array [ "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", Array [ "1", - "21057,21060,24008,7941,7942,7946", + "7941,7942,7946,21057,21060,24008", ], ], Array [ "INSERT INTO pet_states (female, labeled, mood_id, pet_type_id, swf_asset_ids, unconverted) VALUES (?, ?, ?, ?, ?, ?);", Array [ - false, - true, - 2, + 0, + 1, + "2", "1", - "21057,21060,24008,7941,7942,7946", - false, + "7941,7942,7946,21057,21060,24008", + 0, ], ], Array [ "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", Array [ "1", - "21057,21060,24008,7941,7942,7946", + "7941,7942,7946,21057,21060,24008", ], ], Array [ diff --git a/src/server/types/Outfit.js b/src/server/types/Outfit.js index 4122c61..0d6f38f 100644 --- a/src/server/types/Outfit.js +++ b/src/server/types/Outfit.js @@ -191,12 +191,12 @@ async function saveModelingData( type: "object", remoteId: String(objectAsset.asset_id), url: objectAsset.asset_url, - zoneId: objectAsset.zone_id, + zoneId: String(objectAsset.zone_id), zonesRestrict: "", // TODO: This doesn't actually work... sometimes it needs to be 0, yeah? // So we actually have to do asset writing after we load the current // row and compare... maybe a cutesy fn syntax here? - bodyId: customPet.body_id, + bodyId: String(customPet.body_id), })); const biologyAssets = Object.values(customPet.biology_by_zone); @@ -204,9 +204,9 @@ async function saveModelingData( type: "biology", remoteId: String(biologyAsset.part_id), url: biologyAsset.asset_url, - zoneId: biologyAsset.zone_id, + zoneId: String(biologyAsset.zone_id), zonesRestrict: biologyAsset.zones_restrict, - bodyId: 0, + bodyId: "0", })); const incomingSwfAssets = [...incomingItemSwfAssets, ...incomingPetSwfAssets]; @@ -277,13 +277,13 @@ async function saveModelingData( const incomingPetState = { petTypeId: petType.id, swfAssetIds: incomingPetSwfAssets - .map((a) => a.remoteId) - .sort() + .map((row) => row.remoteId) + .sort((a, b) => Number(a) - Number(b)) .join(","), - female: petMetaData.gender === 2, // sorry for this column name :/ - moodId: petMetaData.mood, - unconverted: incomingPetSwfAssets.length === 1, - labeled: true, + female: petMetaData.gender === 2 ? 1 : 0, // sorry for this column name :/ + moodId: String(petMetaData.mood), + unconverted: incomingPetSwfAssets.length === 1 ? 1 : 0, + labeled: 1, }; await syncToDb(db, [incomingPetState], { From cc5a8a6fabad69a7d2f355976181f19df9dba25a Mon Sep 17 00:00:00 2001 From: Matchu Date: Fri, 25 Sep 2020 05:04:12 -0700 Subject: [PATCH 10/22] set bodyId=0 when item's body id changes that is, if we see an item modeled on a second body, then treat it as body 0 --- src/server/query-tests/Pet.test.js | 36 ++ .../__snapshots__/Pet.test.js.snap | 403 ++++++++++++++++++ src/server/types/AppearanceLayer.js | 4 +- src/server/types/Outfit.js | 75 +++- 4 files changed, 502 insertions(+), 16 deletions(-) diff --git a/src/server/query-tests/Pet.test.js b/src/server/query-tests/Pet.test.js index 3a5f18f..9e2296e 100644 --- a/src/server/query-tests/Pet.test.js +++ b/src/server/query-tests/Pet.test.js @@ -308,4 +308,40 @@ describe("Pet", () => { }); expect(getDbCalls()).toMatchSnapshot(); }); + + it("sets bodyId=0 after seeing it on two body types", async () => { + useTestDb(); + + // First, write the Moon and Stars Background SWF to the database, but with + // the Standard Acara body ID set. + const db = await connectToDb(); + await db.query( + `INSERT INTO swf_assets (type, remote_id, url, zone_id, zones_restrict, + created_at, body_id) + VALUES ("object", 6829, "http://images.neopets.com/cp/items/swf/000/000/006/6829_1707e50385.swf", + 3, "", CURRENT_TIMESTAMP(), 93);` + ); + + clearDbCalls(); + + // Then, model a Zafara wearing it. + await query({ + query: gql` + query { + petOnNeopetsDotCom(petName: "roopal27") { + id + } + } + `, + }); + + expect(getDbCalls()).toMatchSnapshot("db"); + + // The body ID should be 0 now. + const [rows, _] = await db.query( + `SELECT body_id FROM swf_assets + WHERE type = "object" AND remote_id = 6829;` + ); + expect(rows[0].body_id).toEqual(0); + }); }); diff --git a/src/server/query-tests/__snapshots__/Pet.test.js.snap b/src/server/query-tests/__snapshots__/Pet.test.js.snap index ab56c0d..6e144a2 100644 --- a/src/server/query-tests/__snapshots__/Pet.test.js.snap +++ b/src/server/query-tests/__snapshots__/Pet.test.js.snap @@ -1145,3 +1145,406 @@ Array [ ], ] `; + +exports[`Pet sets bodyId=0 after seeing it on two body types: db 1`] = ` +Array [ + Array [ + "SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?)", + Array [ + "54", + "75", + ], + ], + Array [ + "SELECT * FROM items WHERE id IN (?,?,?,?,?,?,?,?)", + Array [ + "37229", + "37375", + "38911", + "38912", + "38913", + "43014", + "43397", + "48313", + ], + ], + Array [ + "SELECT * FROM item_translations WHERE item_id IN (?,?,?,?,?,?,?,?) AND locale = \\"en\\"", + Array [ + "37229", + "37375", + "38911", + "38912", + "38913", + "43014", + "43397", + "48313", + ], + ], + Array [ + "SELECT * FROM swf_assets WHERE (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?)", + Array [ + "object", + "6829", + "object", + "14855", + "object", + "14856", + "object", + "14857", + "object", + "36414", + "object", + "39646", + "object", + "51959", + "object", + "56478", + "biology", + "7942", + "biology", + "7941", + "biology", + "24008", + "biology", + "21060", + "biology", + "21057", + "biology", + "7946", + ], + ], + Array [ + "INSERT INTO pet_types (body_id, color_id, created_at, species_id) VALUES (?, ?, ?, ?);", + Array [ + "180", + "75", + 2020-01-01T00:00:00.000Z, + "54", + ], + ], + Array [ + "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", + 0, + 101, + "http://images.neopets.com/items/gif_magicball_table.gif", + "Gift", + 2020-01-01T00:00:00.000Z, + 1, + "0000000000000000000000000000000000000000000000", + "Special", + 2020-01-01T00:00:00.000Z, + "37375", + 209, + 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", + 980, + 92, + "http://images.neopets.com/items/clo_zafara_agent_hood.gif", + "Clothes", + 2020-01-01T00:00:00.000Z, + 1, + "0000000000000000000000000000000000001100000000000000", + "Clothes", + 2020-01-01T00:00:00.000Z, + "38912", + 1476, + 90, + "http://images.neopets.com/items/clo_zafara_agent_robe.gif", + "Clothes", + 2020-01-01T00:00:00.000Z, + 1, + "0000000000000000000101000000000000000000000000000000", + "Clothes", + 2020-01-01T00:00:00.000Z, + "38913", + 1177, + 88, + "http://images.neopets.com/items/clo_zafara_agent_gloves.gif", + "Clothes", + 2020-01-01T00:00:00.000Z, + 1, + "0000000000000000000000000000000000000000000000000000", + "Toys", + 2020-01-01T00:00:00.000Z, + "43014", + 1033, + 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", + 0, + 500, + "http://images.neopets.com/items/mall_staff_jewelled.gif", + "Clothes", + 2020-01-01T00:00:00.000Z, + 1, + "0000000000000000000000000000000000000000000000", + "Clothes", + 2020-01-01T00:00:00.000Z, + "48313", + 0, + 101, + "http://images.neopets.com/items/clo_altcuplogo_brooch.gif", + "Clothes", + 2020-01-01T00:00:00.000Z, + 1, + "0000000000000000000000000000000000000000000000000000", + ], + ], + Array [ + "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", + "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", + "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", + "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", + "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", + "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", + "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", + "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", + "Special", + 2020-01-01T00:00:00.000Z, + ], + ], + Array [ + "INSERT INTO swf_assets (body_id, created_at, remote_id, type, url, zone_id, zones_restrict) VALUES (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?);", + Array [ + "180", + 2020-01-01T00:00:00.000Z, + "14855", + "object", + "http://images.neopets.com/cp/items/swf/000/000/014/14855_215f367070.swf", + "25", + "", + "180", + 2020-01-01T00:00:00.000Z, + "14856", + "object", + "http://images.neopets.com/cp/items/swf/000/000/014/14856_46c1b32797.swf", + "26", + "", + "180", + 2020-01-01T00:00:00.000Z, + "14857", + "object", + "http://images.neopets.com/cp/items/swf/000/000/014/14857_d43380ef66.swf", + "40", + "", + "180", + 2020-01-01T00:00:00.000Z, + "36414", + "object", + "http://images.neopets.com/cp/items/swf/000/000/036/36414_1e2aaab4ad.swf", + "48", + "", + "180", + 2020-01-01T00:00:00.000Z, + "39646", + "object", + "http://images.neopets.com/cp/items/swf/000/000/039/39646_e129e22ada.swf", + "42", + "", + "180", + 2020-01-01T00:00:00.000Z, + "51959", + "object", + "http://images.neopets.com/cp/items/swf/000/000/051/51959_4439727c48.swf", + "45", + "", + "180", + 2020-01-01T00:00:00.000Z, + "56478", + "object", + "http://images.neopets.com/cp/items/swf/000/000/056/56478_eabc28e7c7.swf", + "27", + "", + "0", + 2020-01-01T00:00:00.000Z, + "7942", + "biology", + "http://images.neopets.com/cp/bio/swf/000/000/007/7942_2eab06fd7b.swf", + "5", + "0000000000000000000000000000000000000000000000000000", + "0", + 2020-01-01T00:00:00.000Z, + "7941", + "biology", + "http://images.neopets.com/cp/bio/swf/000/000/007/7941_2c4cc4b846.swf", + "15", + "0000000000000000000000000000000000000000000000000000", + "0", + 2020-01-01T00:00:00.000Z, + "24008", + "biology", + "http://images.neopets.com/cp/bio/swf/000/000/024/24008_a05fe9876a.swf", + "30", + "0000000000000000000000000000000000000000000000000000", + "0", + 2020-01-01T00:00:00.000Z, + "21060", + "biology", + "http://images.neopets.com/cp/bio/swf/000/000/021/21060_d77ba93b7b.swf", + "33", + "0000000000000000000000000000000000000000000000000000", + "0", + 2020-01-01T00:00:00.000Z, + "21057", + "biology", + "http://images.neopets.com/cp/bio/swf/000/000/021/21057_4550efbb2f.swf", + "34", + "0000000000000000000000000000000000000000000000000000", + "0", + 2020-01-01T00:00:00.000Z, + "7946", + "biology", + "http://images.neopets.com/cp/bio/swf/000/000/007/7946_0348dad587.swf", + "37", + "0000000000000000000000000000000000000000000000000000", + ], + ], + Array [ + "UPDATE swf_assets SET body_id = ? WHERE type = ? AND remote_id = ?;", + Array [ + "0", + "object", + "6829", + ], + ], + Array [ + "SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?)", + Array [ + "54", + "75", + ], + ], + Array [ + "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", + Array [ + "1", + "7941,7942,7946,21057,21060,24008", + ], + ], + Array [ + "INSERT INTO pet_states (female, labeled, mood_id, pet_type_id, swf_asset_ids, unconverted) VALUES (?, ?, ?, ?, ?, ?);", + Array [ + 0, + 1, + "2", + "1", + "7941,7942,7946,21057,21060,24008", + 0, + ], + ], + Array [ + "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", + Array [ + "1", + "7941,7942,7946,21057,21060,24008", + ], + ], + Array [ + "SELECT * FROM swf_assets WHERE (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?) OR (type = ? AND remote_id = ?)", + Array [ + "biology", + "7942", + "biology", + "7941", + "biology", + "24008", + "biology", + "21060", + "biology", + "21057", + "biology", + "7946", + ], + ], + Array [ + "INSERT INTO parents_swf_assets (parent_type, parent_id, swf_asset_id) + VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?), (?, ?, ?), (?, ?, ?), (?, ?, ?);", + Array [ + "PetState", + "1", + "9", + "PetState", + "1", + "10", + "PetState", + "1", + "11", + "PetState", + "1", + "12", + "PetState", + "1", + "13", + "PetState", + "1", + "14", + ], + ], +] +`; diff --git a/src/server/types/AppearanceLayer.js b/src/server/types/AppearanceLayer.js index 59b40ac..386f84e 100644 --- a/src/server/types/AppearanceLayer.js +++ b/src/server/types/AppearanceLayer.js @@ -71,7 +71,7 @@ const typeDefs = gql` const resolvers = { AppearanceLayer: { - bodyId: async ({ id }, _, { swfAssetLoader }) => { + remoteId: async ({ id }, _, { swfAssetLoader }) => { const layer = await swfAssetLoader.load(id); return layer.remoteId; }, @@ -79,7 +79,7 @@ const resolvers = { const layer = await swfAssetLoader.load(id); return layer.bodyId; }, - zone: async ({ id }, _, { swfAssetLoader, zoneLoader }) => { + zone: async ({ id }, _, { swfAssetLoader }) => { const layer = await swfAssetLoader.load(id); return { id: layer.zoneId }; }, diff --git a/src/server/types/Outfit.js b/src/server/types/Outfit.js index 0d6f38f..7e2ab00 100644 --- a/src/server/types/Outfit.js +++ b/src/server/types/Outfit.js @@ -193,10 +193,33 @@ async function saveModelingData( url: objectAsset.asset_url, zoneId: String(objectAsset.zone_id), zonesRestrict: "", - // TODO: This doesn't actually work... sometimes it needs to be 0, yeah? - // So we actually have to do asset writing after we load the current - // row and compare... maybe a cutesy fn syntax here? - bodyId: String(customPet.body_id), + bodyId: (currentBodyId) => { + const incomingBodyId = String(customPet.body_id); + + if (currentBodyId == null) { + // If this is a new asset, use the incoming body ID. This might not be + // totally true, the real ID might be 0, but we're conservative to + // start and will update it to 0 if we see a contradiction later! + // + // NOTE: There's an explicitly_body_specific column on Item. We don't + // need to consider it here, because it's specifically to + // override the heuristics in the old app that sometimes set + // bodyId=0 for incoming items depending on their zone. We don't + // do that here! + return incomingBodyId; + } else if (currentBodyId === "0") { + // If this is already an all-bodies asset, keep it that way. + return "0"; + } else if (currentBodyId !== incomingBodyId) { + // If this isn't an all-bodies asset yet, but we've now seen it on two + // different items, then make it an all-bodies asset! + return "0"; + } else { + // Okay, the row already exists, and its body ID matches this one. + // No change! + return currentBodyId; + } + }, })); const biologyAssets = Object.values(customPet.biology_by_zone); @@ -369,7 +392,21 @@ async function syncToDb( // If there is no corresponding row in the database, prepare an insert. // TODO: Should probably converge on whether not-found is null or an error if (currentRow == null || currentRow instanceof Error) { - const insert = { ...incomingRow }; + const insert = {}; + for (const key in incomingRow) { + let incomingValue = incomingRow[key]; + + // If you pass a function as a value, we treat it as a merge function: + // we'll pass it the current value, and you'll use it to determine and + // return the incoming value. In this case, the row doesn't exist yet, + // so the current value is `null`. + if (typeof incomingValue === "function") { + incomingValue = incomingValue(null); + } + + insert[key] = incomingValue; + } + if (includeCreatedAt) { insert.createdAt = new Date(); } @@ -387,14 +424,24 @@ async function syncToDb( // If there's a row in the database, and some of the values don't match, // prepare an update with the updated fields only. - const updatedKeys = Object.keys(incomingRow).filter( - (k) => incomingRow[k] !== currentRow[k] - ); - if (updatedKeys.length > 0) { - const update = {}; - for (const key of updatedKeys) { - update[key] = incomingRow[key]; + const update = {}; + for (const key in incomingRow) { + const currentValue = currentRow[key]; + let incomingValue = incomingRow[key]; + + // If you pass a function as a value, we treat it as a merge function: + // we'll pass it the current value, and you'll use it to determine and + // return the incoming value. + if (typeof incomingValue === "function") { + incomingValue = incomingValue(currentValue); } + + if (currentValue !== incomingValue) { + update[key] = incomingValue; + } + } + + if (Object.keys(update).length > 0) { if (includeUpdatedAt) { update.updatedAt = new Date(); } @@ -417,10 +464,10 @@ async function syncToDb( const columnsStr = columnNames.join(", "); const qs = columnNames.map((_) => "?").join(", "); const rowQs = inserts.map((_) => "(" + qs + ")").join(", "); - const rowFields = inserts.map((row) => rowKeys.map((key) => row[key])); + const rowValues = inserts.map((row) => rowKeys.map((key) => row[key])); await db.execute( `INSERT INTO ${tableName} (${columnsStr}) VALUES ${rowQs};`, - rowFields.flat() + rowValues.flat() ); if (afterInsert) { await afterInsert(); From 94f6363251772a28d61909d1342198c6d06babf6 Mon Sep 17 00:00:00 2001 From: Matchu Date: Fri, 25 Sep 2020 05:15:58 -0700 Subject: [PATCH 11/22] add LIMIT 1 to our updates, out of healthy fear This is just me thinking about what could go wrong in the modeling rollout, this at least makes it so that, if something breaks, it will break small --- src/server/query-tests/__snapshots__/Pet.test.js.snap | 6 +++--- src/server/types/Outfit.js | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/server/query-tests/__snapshots__/Pet.test.js.snap b/src/server/query-tests/__snapshots__/Pet.test.js.snap index 6e144a2..a3141b6 100644 --- a/src/server/query-tests/__snapshots__/Pet.test.js.snap +++ b/src/server/query-tests/__snapshots__/Pet.test.js.snap @@ -1035,7 +1035,7 @@ Array [ ], ], Array [ - "UPDATE items SET rarity_index = ?, thumbnail_url = ?, updated_at = ?, zones_restrict = ? WHERE id = ?;", + "UPDATE items SET rarity_index = ?, thumbnail_url = ?, updated_at = ?, zones_restrict = ? WHERE id = ? LIMIT 1;", Array [ 500, "http://images.neopets.com/items/mall_staff_jewelled.gif", @@ -1045,7 +1045,7 @@ Array [ ], ], Array [ - "UPDATE item_translations SET description = ?, name = ?, updated_at = ? WHERE item_id = ? AND locale = \\"en\\";", + "UPDATE item_translations SET description = ?, name = ?, updated_at = ? WHERE item_id = ? AND locale = \\"en\\" LIMIT 1;", Array [ "This jewelled staff shines with a magical light.", "Jewelled Staff", @@ -1466,7 +1466,7 @@ Array [ ], ], Array [ - "UPDATE swf_assets SET body_id = ? WHERE type = ? AND remote_id = ?;", + "UPDATE swf_assets SET body_id = ? WHERE type = ? AND remote_id = ? LIMIT 1;", Array [ "0", "object", diff --git a/src/server/types/Outfit.js b/src/server/types/Outfit.js index 7e2ab00..11f0200 100644 --- a/src/server/types/Outfit.js +++ b/src/server/types/Outfit.js @@ -488,10 +488,10 @@ async function syncToDb( const qs = columnNames.map((c) => `${c} = ?`).join(", "); const [conditionQs, ...conditionValues] = buildUpdateCondition(incomingRow); updatePromises.push( - db.execute(`UPDATE ${tableName} SET ${qs} WHERE ${conditionQs};`, [ - ...rowValues, - ...conditionValues, - ]) + db.execute( + `UPDATE ${tableName} SET ${qs} WHERE ${conditionQs} LIMIT 1;`, + [...rowValues, ...conditionValues] + ) ); } await Promise.all(updatePromises); From e57af14625e8bd86794349f5681964132ff156d6 Mon Sep 17 00:00:00 2001 From: Matchu Date: Sun, 27 Sep 2020 03:18:46 -0700 Subject: [PATCH 12/22] refactor Outfit.js to split Pet to a separate type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit yeah, I had unified Pet into Outfit, but now I think that was overly clever… 😅 Here, I define a new Pet type, and it has some of the fields of Outfit and the deprecated fields still. I did this because I want petAppearance to work, for UC testing! --- src/server/loaders.js | 20 +++++++++--- src/server/types/Outfit.js | 65 ++++++++++++++++++++++++++------------ 2 files changed, 61 insertions(+), 24 deletions(-) diff --git a/src/server/loaders.js b/src/server/loaders.js index 39168be..bada992 100644 --- a/src/server/loaders.js +++ b/src/server/loaders.js @@ -324,7 +324,7 @@ const buildItemBodiesWithAppearanceDataLoader = (db) => return itemIds.map((itemId) => entities.filter((e) => e.itemId === itemId)); }); -const buildPetTypeLoader = (db) => +const buildPetTypeLoader = (db, loaders) => new DataLoader(async (petTypeIds) => { const qs = petTypeIds.map((_) => "?").join(","); const [rows, _] = await db.execute( @@ -334,6 +334,13 @@ const buildPetTypeLoader = (db) => const entities = rows.map(normalizeRow); + for (const petType of entities) { + loaders.petTypeBySpeciesAndColorLoader.prime( + { speciesId: petType.speciesId, colorId: petType.colorId }, + petType + ); + } + return petTypeIds.map((petTypeId) => entities.find((e) => e.id === petTypeId) ); @@ -577,7 +584,7 @@ const buildCanonicalPetStateForBodyLoader = (db, loaders) => ); }); -const buildPetStateByPetTypeAndAssetsLoader = (db) => +const buildPetStateByPetTypeAndAssetsLoader = (db, loaders) => new DataLoader( async (petTypeIdAndAssetIdsPairs) => { const qs = petTypeIdAndAssetIdsPairs @@ -593,6 +600,10 @@ const buildPetStateByPetTypeAndAssetsLoader = (db) => const entities = rows.map(normalizeRow); + for (const petState of entities) { + loaders.petStateLoader.prime(petState.id, petState); + } + return petTypeIdAndAssetIdsPairs.map(({ petTypeId, swfAssetIds }) => entities.find( (e) => e.petTypeId === petTypeId && e.swfAssetIds === swfAssetIds @@ -721,7 +732,7 @@ function buildLoaders(db) { loaders.itemBodiesWithAppearanceDataLoader = buildItemBodiesWithAppearanceDataLoader( db ); - loaders.petTypeLoader = buildPetTypeLoader(db); + loaders.petTypeLoader = buildPetTypeLoader(db, loaders); loaders.petTypeBySpeciesAndColorLoader = buildPetTypeBySpeciesAndColorLoader( db, loaders @@ -744,7 +755,8 @@ function buildLoaders(db) { loaders ); loaders.petStateByPetTypeAndAssetsLoader = buildPetStateByPetTypeAndAssetsLoader( - db + db, + loaders ); loaders.speciesLoader = buildSpeciesLoader(db); loaders.speciesTranslationLoader = buildSpeciesTranslationLoader(db); diff --git a/src/server/types/Outfit.js b/src/server/types/Outfit.js index 11f0200..d83492d 100644 --- a/src/server/types/Outfit.js +++ b/src/server/types/Outfit.js @@ -1,5 +1,6 @@ const fetch = require("node-fetch"); const { gql } = require("apollo-server"); +const { getPoseFromPetState } = require("../util"); const typeDefs = gql` type Outfit { @@ -8,6 +9,14 @@ const typeDefs = gql` petAppearance: PetAppearance! wornItems: [Item!]! closetedItems: [Item!]! + } + + # TODO: This maybe should move to a separate file? + type Pet { + id: ID! + name: String! + petAppearance: PetAppearance! + wornItems: [Item!]! species: Species! # to be deprecated? can use petAppearance? 🤔 color: Color! # to be deprecated? can use petAppearance? 🤔 @@ -17,7 +26,7 @@ const typeDefs = gql` extend type Query { outfit(id: ID!): Outfit - petOnNeopetsDotCom(petName: String!): Outfit + petOnNeopetsDotCom(petName: String!): Pet } `; @@ -44,6 +53,37 @@ const resolvers = { .map((oir) => ({ id: oir.itemId })); }, }, + Pet: { + species: ({ customPetData }) => ({ + id: customPetData.custom_pet.species_id, + }), + color: ({ customPetData }) => ({ id: customPetData.custom_pet.color_id }), + pose: ({ customPetData, petMetaData }) => + getPoseFromPetData(petMetaData, customPetData), + petAppearance: async ( + { customPetData, petMetaData }, + _, + { petTypeBySpeciesAndColorLoader, petStatesForPetTypeLoader } + ) => { + const petType = await petTypeBySpeciesAndColorLoader.load({ + speciesId: customPetData.custom_pet.species_id, + colorId: customPetData.custom_pet.color_id, + }); + const petStates = await petStatesForPetTypeLoader.load(petType.id); + const pose = getPoseFromPetData(petMetaData, customPetData); + const petState = petStates.find((ps) => getPoseFromPetState(ps) === pose); + return { id: petState.id }; + }, + wornItems: ({ customPetData }) => + 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, + })), + items: (...args) => resolvers.Pet.wornItems(...args), + }, Query: { outfit: (_, { id }) => ({ id }), petOnNeopetsDotCom: async ( @@ -72,23 +112,7 @@ const resolvers = { swfAssetByRemoteIdLoader, }); - 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; + return { name, customPetData, petMetaData }; }, }, }; @@ -129,10 +153,11 @@ async function loadCustomPetData(petName) { } 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") { + if (Object.keys(petCustomData.custom_pet.biology_by_zone).length === 1) { + return "UNCONVERTED"; + } else if (String(moodId) === "1" && String(genderId) === "1") { return "HAPPY_MASC"; } else if (String(moodId) === "1" && String(genderId) === "2") { return "HAPPY_FEM"; From 914a06f8c7fac57f777cc0299c613bc64f85489a Mon Sep 17 00:00:00 2001 From: Matchu Date: Sun, 27 Sep 2020 03:19:10 -0700 Subject: [PATCH 13/22] add test for UC modeling This got fixed in the refactor last commit, where we added the petAppearance field! --- src/server/query-tests/Pet.test.js | 50 ++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/server/query-tests/Pet.test.js b/src/server/query-tests/Pet.test.js index 9e2296e..f0c1f8c 100644 --- a/src/server/query-tests/Pet.test.js +++ b/src/server/query-tests/Pet.test.js @@ -344,4 +344,54 @@ describe("Pet", () => { ); expect(rows[0].body_id).toEqual(0); }); + + it("models unconverted pets", async () => { + useTestDb(); + + // First, model an unconverted pet, and check its pose and layers. + const res = await query({ + query: gql` + query { + petOnNeopetsDotCom(petName: "Marishka82") { + pose + petAppearance { + id + pose + layers { + id + } + } + } + } + `, + }); + expect(res).toHaveNoErrors(); + + const modeledPet = res.data.petOnNeopetsDotCom; + expect(modeledPet.pose).toEqual("UNCONVERTED"); + expect(modeledPet.petAppearance.pose).toEqual("UNCONVERTED"); + expect(modeledPet.petAppearance.layers).toHaveLength(1); + + // Then, request the corresponding appearance fresh from the db, and + // confirm we get the same back as when we modeled the pet. + const res2 = await query({ + query: gql` + query { + petAppearance(speciesId: "31", colorId: "36", pose: UNCONVERTED) { + id + layers { + id + } + } + } + `, + }); + expect(res2).toHaveNoErrors(); + + const petAppearance = res2.data.petAppearance; + expect(petAppearance.id).toEqual(modeledPet.petAppearance.id); + expect(petAppearance.layers.map((l) => l.id)).toEqual( + modeledPet.petAppearance.layers.map((l) => l.id) + ); + }); }); From a8c351b1023388f4ef4f3a0e30125616adabdb10 Mon Sep 17 00:00:00 2001 From: Matchu Date: Sun, 27 Sep 2020 03:21:07 -0700 Subject: [PATCH 14/22] oops, fix a bug with Pet name GQL --- src/server/types/Outfit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/types/Outfit.js b/src/server/types/Outfit.js index d83492d..6879cc1 100644 --- a/src/server/types/Outfit.js +++ b/src/server/types/Outfit.js @@ -112,7 +112,7 @@ const resolvers = { swfAssetByRemoteIdLoader, }); - return { name, customPetData, petMetaData }; + return { name: petName, customPetData, petMetaData }; }, }, }; From 0f5c437ffd0d3b06d0d84ffef745063215eed554 Mon Sep 17 00:00:00 2001 From: Matchu Date: Sun, 27 Sep 2020 03:57:07 -0700 Subject: [PATCH 15/22] split up modeling code into smaller functions This is mostly because I want to chain the rels after both items and assets save, and I want to be able to specify that stuff a bit more precisely, rather than the like, layers-of-awaits we were building up. --- .../__snapshots__/Pet.test.js.snap | 28 +- src/server/types/Outfit.js | 276 +++++++++--------- 2 files changed, 160 insertions(+), 144 deletions(-) diff --git a/src/server/query-tests/__snapshots__/Pet.test.js.snap b/src/server/query-tests/__snapshots__/Pet.test.js.snap index a3141b6..1111b37 100644 --- a/src/server/query-tests/__snapshots__/Pet.test.js.snap +++ b/src/server/query-tests/__snapshots__/Pet.test.js.snap @@ -1034,6 +1034,13 @@ Array [ "0000000000000000000000000000000000000000000000000000", ], ], + Array [ + "SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?)", + Array [ + "54", + "75", + ], + ], Array [ "UPDATE items SET rarity_index = ?, thumbnail_url = ?, updated_at = ?, zones_restrict = ? WHERE id = ? LIMIT 1;", Array [ @@ -1053,13 +1060,6 @@ Array [ "43397", ], ], - Array [ - "SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?)", - Array [ - "54", - "75", - ], - ], Array [ "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", Array [ @@ -1465,6 +1465,13 @@ Array [ "0000000000000000000000000000000000000000000000000000", ], ], + Array [ + "SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?)", + Array [ + "54", + "75", + ], + ], Array [ "UPDATE swf_assets SET body_id = ? WHERE type = ? AND remote_id = ? LIMIT 1;", Array [ @@ -1473,13 +1480,6 @@ Array [ "6829", ], ], - Array [ - "SELECT * FROM pet_types WHERE (species_id = ? AND color_id = ?)", - Array [ - "54", - "75", - ], - ], Array [ "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", Array [ diff --git a/src/server/types/Outfit.js b/src/server/types/Outfit.js index 6879cc1..9a1dede 100644 --- a/src/server/types/Outfit.js +++ b/src/server/types/Outfit.js @@ -178,91 +178,29 @@ function getPoseFromPetData(petMetaData, petCustomData) { } } -async function saveModelingData( +async function saveModelingData(customPetData, petMetaData, context) { + await Promise.all([ + savePetTypeAndStateModelingData(customPetData, petMetaData, context), + saveItemModelingData(customPetData, context), + saveSwfAssetModelingData(customPetData, context), + ]); +} + +async function savePetTypeAndStateModelingData( customPetData, petMetaData, - { + context +) { + const { db, petTypeBySpeciesAndColorLoader, petStateByPetTypeAndAssetsLoader, - itemLoader, - itemTranslationLoader, swfAssetByRemoteIdLoader, - } -) { - const customPet = customPetData.custom_pet; - - const objectInfos = Object.values(customPetData.object_info_registry); - const incomingItems = objectInfos.map((objectInfo) => ({ - id: String(objectInfo.obj_info_id), - zonesRestrict: objectInfo.zones_restrict, - thumbnailUrl: objectInfo.thumbnail_url, - category: objectInfo.category, - type: objectInfo.type, - rarityIndex: objectInfo.rarity_index, - price: objectInfo.price, - weightLbs: objectInfo.weight_lbs, - })); - const incomingItemTranslations = objectInfos.map((objectInfo) => ({ - itemId: String(objectInfo.obj_info_id), - locale: "en", - name: objectInfo.name, - description: objectInfo.description, - rarity: objectInfo.rarity, - })); - - const objectAssets = Object.values(customPetData.object_asset_registry); - const incomingItemSwfAssets = objectAssets.map((objectAsset) => ({ - type: "object", - remoteId: String(objectAsset.asset_id), - url: objectAsset.asset_url, - zoneId: String(objectAsset.zone_id), - zonesRestrict: "", - bodyId: (currentBodyId) => { - const incomingBodyId = String(customPet.body_id); - - if (currentBodyId == null) { - // If this is a new asset, use the incoming body ID. This might not be - // totally true, the real ID might be 0, but we're conservative to - // start and will update it to 0 if we see a contradiction later! - // - // NOTE: There's an explicitly_body_specific column on Item. We don't - // need to consider it here, because it's specifically to - // override the heuristics in the old app that sometimes set - // bodyId=0 for incoming items depending on their zone. We don't - // do that here! - return incomingBodyId; - } else if (currentBodyId === "0") { - // If this is already an all-bodies asset, keep it that way. - return "0"; - } else if (currentBodyId !== incomingBodyId) { - // If this isn't an all-bodies asset yet, but we've now seen it on two - // different items, then make it an all-bodies asset! - return "0"; - } else { - // Okay, the row already exists, and its body ID matches this one. - // No change! - return currentBodyId; - } - }, - })); - - const biologyAssets = Object.values(customPet.biology_by_zone); - const incomingPetSwfAssets = biologyAssets.map((biologyAsset) => ({ - type: "biology", - remoteId: String(biologyAsset.part_id), - url: biologyAsset.asset_url, - zoneId: String(biologyAsset.zone_id), - zonesRestrict: biologyAsset.zones_restrict, - bodyId: "0", - })); - - const incomingSwfAssets = [...incomingItemSwfAssets, ...incomingPetSwfAssets]; - + } = context; const incomingPetType = { - colorId: String(customPet.color_id), - speciesId: String(customPet.species_id), - bodyId: String(customPet.body_id), + colorId: String(customPetData.custom_pet.color_id), + speciesId: String(customPetData.custom_pet.species_id), + bodyId: String(customPetData.custom_pet.body_id), // NOTE: I skip the image_hash stuff here... on Rails, we set a hash on // creation, and may or may not bother to update it, I forget? But // here I don't want to bother with an update. We could maybe do @@ -270,67 +208,37 @@ async function saveModelingData( // care enough ^_^` }; - await Promise.all([ - syncToDb(db, [incomingPetType], { - loader: petTypeBySpeciesAndColorLoader, - tableName: "pet_types", - buildLoaderKey: (row) => ({ - speciesId: row.speciesId, - colorId: row.colorId, - }), - buildUpdateCondition: (row) => [ - `species_id = ? AND color_id = ?`, - row.speciesId, - row.colorId, - ], - includeUpdatedAt: false, + await syncToDb(db, [incomingPetType], { + loader: petTypeBySpeciesAndColorLoader, + tableName: "pet_types", + buildLoaderKey: (row) => ({ + speciesId: row.speciesId, + colorId: row.colorId, }), - syncToDb(db, incomingItems, { - loader: itemLoader, - tableName: "items", - buildLoaderKey: (row) => row.id, - buildUpdateCondition: (row) => [`id = ?`, row.id], - }), - syncToDb(db, incomingItemTranslations, { - loader: itemTranslationLoader, - tableName: "item_translations", - buildLoaderKey: (row) => row.itemId, - buildUpdateCondition: (row) => [ - `item_id = ? AND locale = "en"`, - row.itemId, - ], - }), - syncToDb(db, incomingSwfAssets, { - loader: swfAssetByRemoteIdLoader, - tableName: "swf_assets", - buildLoaderKey: (row) => ({ type: row.type, remoteId: row.remoteId }), - buildUpdateCondition: (row) => [ - `type = ? AND remote_id = ?`, - row.type, - row.remoteId, - ], - includeUpdatedAt: false, - }), - ]); + buildUpdateCondition: (row) => [ + `species_id = ? AND color_id = ?`, + row.speciesId, + row.colorId, + ], + includeUpdatedAt: false, + }); - // TODO: If we look up the potentially existing pet state earlier, then I - // think we can prime the cache and avoid creating a waterfall of - // queries here in the happy case, even though it'll look waterfall-y! // NOTE: This pet type should have been looked up when syncing pet type, so // this should be cached. const petType = await petTypeBySpeciesAndColorLoader.load({ - colorId: String(customPet.color_id), - speciesId: String(customPet.species_id), + colorId: String(customPetData.custom_pet.color_id), + speciesId: String(customPetData.custom_pet.species_id), }); + const biologyAssets = Object.values(customPetData.custom_pet.biology_by_zone); const incomingPetState = { petTypeId: petType.id, - swfAssetIds: incomingPetSwfAssets - .map((row) => row.remoteId) + swfAssetIds: biologyAssets + .map((row) => row.part_id) .sort((a, b) => Number(a) - Number(b)) .join(","), female: petMetaData.gender === 2 ? 1 : 0, // sorry for this column name :/ moodId: String(petMetaData.mood), - unconverted: incomingPetSwfAssets.length === 1 ? 1 : 0, + unconverted: biologyAssets.length === 1 ? 1 : 0, labeled: 1, }; @@ -360,13 +268,16 @@ async function saveModelingData( swfAssetIds: incomingPetState.swfAssetIds, }), swfAssetByRemoteIdLoader.loadMany( - incomingPetSwfAssets.map((row) => ({ - type: row.type, - remoteId: row.remoteId, + biologyAssets.map((asset) => ({ + type: "biology", + remoteId: String(asset.part_id), })) ), ]); swfAssets = swfAssets.filter((sa) => sa != null); + if (swfAssets.length === 0) { + throw new Error(`pet state ${petState.id} has no saved assets?`); + } const qs = swfAssets.map((_) => `(?, ?, ?)`).join(", "); const values = swfAssets .map((sa) => ["PetState", petState.id, sa.id]) @@ -380,6 +291,111 @@ async function saveModelingData( }); } +async function saveItemModelingData(customPetData, context) { + const { db, itemLoader, itemTranslationLoader } = context; + + const objectInfos = Object.values(customPetData.object_info_registry); + const incomingItems = objectInfos.map((objectInfo) => ({ + id: String(objectInfo.obj_info_id), + zonesRestrict: objectInfo.zones_restrict, + thumbnailUrl: objectInfo.thumbnail_url, + category: objectInfo.category, + type: objectInfo.type, + rarityIndex: objectInfo.rarity_index, + price: objectInfo.price, + weightLbs: objectInfo.weight_lbs, + })); + const incomingItemTranslations = objectInfos.map((objectInfo) => ({ + itemId: String(objectInfo.obj_info_id), + locale: "en", + name: objectInfo.name, + description: objectInfo.description, + rarity: objectInfo.rarity, + })); + + await Promise.all([ + syncToDb(db, incomingItems, { + loader: itemLoader, + tableName: "items", + buildLoaderKey: (row) => row.id, + buildUpdateCondition: (row) => [`id = ?`, row.id], + }), + syncToDb(db, incomingItemTranslations, { + loader: itemTranslationLoader, + tableName: "item_translations", + buildLoaderKey: (row) => row.itemId, + buildUpdateCondition: (row) => [ + `item_id = ? AND locale = "en"`, + row.itemId, + ], + }), + ]); +} + +async function saveSwfAssetModelingData(customPetData, context) { + const { db, swfAssetByRemoteIdLoader } = context; + + const objectAssets = Object.values(customPetData.object_asset_registry); + const incomingItemSwfAssets = objectAssets.map((objectAsset) => ({ + type: "object", + remoteId: String(objectAsset.asset_id), + url: objectAsset.asset_url, + zoneId: String(objectAsset.zone_id), + zonesRestrict: "", + bodyId: (currentBodyId) => { + const incomingBodyId = String(customPetData.custom_pet.body_id); + + if (currentBodyId == null) { + // If this is a new asset, use the incoming body ID. This might not be + // totally true, the real ID might be 0, but we're conservative to + // start and will update it to 0 if we see a contradiction later! + // + // NOTE: There's an explicitly_body_specific column on Item. We don't + // need to consider it here, because it's specifically to + // override the heuristics in the old app that sometimes set + // bodyId=0 for incoming items depending on their zone. We don't + // do that here! + return incomingBodyId; + } else if (currentBodyId === "0") { + // If this is already an all-bodies asset, keep it that way. + return "0"; + } else if (currentBodyId !== incomingBodyId) { + // If this isn't an all-bodies asset yet, but we've now seen it on two + // different items, then make it an all-bodies asset! + return "0"; + } else { + // Okay, the row already exists, and its body ID matches this one. + // No change! + return currentBodyId; + } + }, + })); + + const biologyAssets = Object.values(customPetData.custom_pet.biology_by_zone); + const incomingPetSwfAssets = biologyAssets.map((biologyAsset) => ({ + type: "biology", + remoteId: String(biologyAsset.part_id), + url: biologyAsset.asset_url, + zoneId: String(biologyAsset.zone_id), + zonesRestrict: biologyAsset.zones_restrict, + bodyId: "0", + })); + + const incomingSwfAssets = [...incomingItemSwfAssets, ...incomingPetSwfAssets]; + + syncToDb(db, incomingSwfAssets, { + loader: swfAssetByRemoteIdLoader, + tableName: "swf_assets", + buildLoaderKey: (row) => ({ type: row.type, remoteId: row.remoteId }), + buildUpdateCondition: (row) => [ + `type = ? AND remote_id = ?`, + row.type, + row.remoteId, + ], + includeUpdatedAt: false, + }); +} + /** * 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 From 740d8415dbb67056a59ac50240505ef1a9bd0544 Mon Sep 17 00:00:00 2001 From: Matchu Date: Tue, 6 Oct 2020 05:01:27 -0700 Subject: [PATCH 16/22] extract modeling logic into modeling.js --- src/server/modeling.js | 370 +++++++++++++++++++++++++++++++++++++ src/server/types/Outfit.js | 365 +----------------------------------- 2 files changed, 373 insertions(+), 362 deletions(-) create mode 100644 src/server/modeling.js diff --git a/src/server/modeling.js b/src/server/modeling.js new file mode 100644 index 0000000..5695ff9 --- /dev/null +++ b/src/server/modeling.js @@ -0,0 +1,370 @@ +/** + * saveModelingData takes data about a pet (generated by `loadCustomPetData` + * and `loadPetMetaData`), and a GQL-y context object with a `db` and some + * loaders; and updates the database to match. + * + * These days, most calls to this function are a no-op: we detect that the + * database already contains this data, and end up doing no writes. But when a + * pet contains data we haven't seen before, we write! + */ +async function saveModelingData(customPetData, petMetaData, context) { + await Promise.all([ + savePetTypeAndStateModelingData(customPetData, petMetaData, context), + saveItemModelingData(customPetData, context), + saveSwfAssetModelingData(customPetData, context), + ]); +} + +async function savePetTypeAndStateModelingData( + customPetData, + petMetaData, + context +) { + const { + db, + petTypeBySpeciesAndColorLoader, + petStateByPetTypeAndAssetsLoader, + swfAssetByRemoteIdLoader, + } = context; + const incomingPetType = { + colorId: String(customPetData.custom_pet.color_id), + speciesId: String(customPetData.custom_pet.species_id), + bodyId: String(customPetData.custom_pet.body_id), + // NOTE: I skip the image_hash stuff here... on Rails, we set a hash on + // creation, and may or may not bother to update it, I forget? But + // here I don't want to bother with an update. We could maybe do + // a merge function to make it on create only, but eh, I don't + // care enough ^_^` + }; + + await syncToDb(db, [incomingPetType], { + loader: petTypeBySpeciesAndColorLoader, + tableName: "pet_types", + buildLoaderKey: (row) => ({ + speciesId: row.speciesId, + colorId: row.colorId, + }), + buildUpdateCondition: (row) => [ + `species_id = ? AND color_id = ?`, + row.speciesId, + row.colorId, + ], + includeUpdatedAt: false, + }); + + // NOTE: This pet type should have been looked up when syncing pet type, so + // this should be cached. + const petType = await petTypeBySpeciesAndColorLoader.load({ + colorId: String(customPetData.custom_pet.color_id), + speciesId: String(customPetData.custom_pet.species_id), + }); + const biologyAssets = Object.values(customPetData.custom_pet.biology_by_zone); + const incomingPetState = { + petTypeId: petType.id, + swfAssetIds: biologyAssets + .map((row) => row.part_id) + .sort((a, b) => Number(a) - Number(b)) + .join(","), + female: petMetaData.gender === 2 ? 1 : 0, // sorry for this column name :/ + moodId: String(petMetaData.mood), + unconverted: biologyAssets.length === 1 ? 1 : 0, + labeled: 1, + }; + + await syncToDb(db, [incomingPetState], { + loader: petStateByPetTypeAndAssetsLoader, + tableName: "pet_states", + buildLoaderKey: (row) => ({ + petTypeId: row.petTypeId, + swfAssetIds: row.swfAssetIds, + }), + buildUpdateCondition: (row) => [ + `pet_type_id = ? AND swf_asset_ids = ?`, + row.petTypeId, + row.swfAssetIds, + ], + includeCreatedAt: false, + includeUpdatedAt: false, + // For pet states, syncing assets is easy: a new set of assets counts as a + // new state, so, whatever! Just insert the relationships when inserting + // the pet state, and ignore them any other time. + afterInsert: async () => { + // We need to load from the db to get the actual inserted IDs. Not lovely + // for perf, but this is a real new-data model, so that's fine! + let [petState, swfAssets] = await Promise.all([ + petStateByPetTypeAndAssetsLoader.load({ + petTypeId: incomingPetState.petTypeId, + swfAssetIds: incomingPetState.swfAssetIds, + }), + swfAssetByRemoteIdLoader.loadMany( + biologyAssets.map((asset) => ({ + type: "biology", + remoteId: String(asset.part_id), + })) + ), + ]); + swfAssets = swfAssets.filter((sa) => sa != null); + if (swfAssets.length === 0) { + throw new Error(`pet state ${petState.id} has no saved assets?`); + } + const qs = swfAssets.map((_) => `(?, ?, ?)`).join(", "); + const values = swfAssets + .map((sa) => ["PetState", petState.id, sa.id]) + .flat(); + await db.execute( + `INSERT INTO parents_swf_assets (parent_type, parent_id, swf_asset_id) + VALUES ${qs};`, + values + ); + }, + }); +} + +async function saveItemModelingData(customPetData, context) { + const { db, itemLoader, itemTranslationLoader } = context; + + const objectInfos = Object.values(customPetData.object_info_registry); + const incomingItems = objectInfos.map((objectInfo) => ({ + id: String(objectInfo.obj_info_id), + zonesRestrict: objectInfo.zones_restrict, + thumbnailUrl: objectInfo.thumbnail_url, + category: objectInfo.category, + type: objectInfo.type, + rarityIndex: objectInfo.rarity_index, + price: objectInfo.price, + weightLbs: objectInfo.weight_lbs, + })); + const incomingItemTranslations = objectInfos.map((objectInfo) => ({ + itemId: String(objectInfo.obj_info_id), + locale: "en", + name: objectInfo.name, + description: objectInfo.description, + rarity: objectInfo.rarity, + })); + + await Promise.all([ + syncToDb(db, incomingItems, { + loader: itemLoader, + tableName: "items", + buildLoaderKey: (row) => row.id, + buildUpdateCondition: (row) => [`id = ?`, row.id], + }), + syncToDb(db, incomingItemTranslations, { + loader: itemTranslationLoader, + tableName: "item_translations", + buildLoaderKey: (row) => row.itemId, + buildUpdateCondition: (row) => [ + `item_id = ? AND locale = "en"`, + row.itemId, + ], + }), + ]); +} + +async function saveSwfAssetModelingData(customPetData, context) { + const { db, swfAssetByRemoteIdLoader } = context; + + const objectAssets = Object.values(customPetData.object_asset_registry); + const incomingItemSwfAssets = objectAssets.map((objectAsset) => ({ + type: "object", + remoteId: String(objectAsset.asset_id), + url: objectAsset.asset_url, + zoneId: String(objectAsset.zone_id), + zonesRestrict: "", + bodyId: (currentBodyId) => { + const incomingBodyId = String(customPetData.custom_pet.body_id); + + if (currentBodyId == null) { + // If this is a new asset, use the incoming body ID. This might not be + // totally true, the real ID might be 0, but we're conservative to + // start and will update it to 0 if we see a contradiction later! + // + // NOTE: There's an explicitly_body_specific column on Item. We don't + // need to consider it here, because it's specifically to + // override the heuristics in the old app that sometimes set + // bodyId=0 for incoming items depending on their zone. We don't + // do that here! + return incomingBodyId; + } else if (currentBodyId === "0") { + // If this is already an all-bodies asset, keep it that way. + return "0"; + } else if (currentBodyId !== incomingBodyId) { + // If this isn't an all-bodies asset yet, but we've now seen it on two + // different items, then make it an all-bodies asset! + return "0"; + } else { + // Okay, the row already exists, and its body ID matches this one. + // No change! + return currentBodyId; + } + }, + })); + + const biologyAssets = Object.values(customPetData.custom_pet.biology_by_zone); + const incomingPetSwfAssets = biologyAssets.map((biologyAsset) => ({ + type: "biology", + remoteId: String(biologyAsset.part_id), + url: biologyAsset.asset_url, + zoneId: String(biologyAsset.zone_id), + zonesRestrict: biologyAsset.zones_restrict, + bodyId: "0", + })); + + const incomingSwfAssets = [...incomingItemSwfAssets, ...incomingPetSwfAssets]; + + syncToDb(db, incomingSwfAssets, { + loader: swfAssetByRemoteIdLoader, + tableName: "swf_assets", + buildLoaderKey: (row) => ({ type: row.type, remoteId: row.remoteId }), + buildUpdateCondition: (row) => [ + `type = ? AND remote_id = ?`, + row.type, + row.remoteId, + ], + includeUpdatedAt: false, + }); +} + +/** + * 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( + db, + incomingRows, + { + loader, + tableName, + buildLoaderKey, + buildUpdateCondition, + includeCreatedAt = true, + includeUpdatedAt = true, + afterInsert = null, + } +) { + const loaderKeys = incomingRows.map(buildLoaderKey); + const currentRows = await loader.loadMany(loaderKeys); + + const inserts = []; + const updates = []; + for (const index in incomingRows) { + const incomingRow = incomingRows[index]; + const currentRow = currentRows[index]; + + // If there is no corresponding row in the database, prepare an insert. + // TODO: Should probably converge on whether not-found is null or an error + if (currentRow == null || currentRow instanceof Error) { + const insert = {}; + for (const key in incomingRow) { + let incomingValue = incomingRow[key]; + + // If you pass a function as a value, we treat it as a merge function: + // we'll pass it the current value, and you'll use it to determine and + // return the incoming value. In this case, the row doesn't exist yet, + // so the current value is `null`. + if (typeof incomingValue === "function") { + incomingValue = incomingValue(null); + } + + insert[key] = incomingValue; + } + + if (includeCreatedAt) { + insert.createdAt = new Date(); + } + if (includeUpdatedAt) { + insert.updatedAt = new Date(); + } + inserts.push(insert); + + // Remove this from the loader cache, so that loading again will fetch + // the inserted row. + loader.clear(buildLoaderKey(incomingRow)); + + continue; + } + + // If there's a row in the database, and some of the values don't match, + // prepare an update with the updated fields only. + const update = {}; + for (const key in incomingRow) { + const currentValue = currentRow[key]; + let incomingValue = incomingRow[key]; + + // If you pass a function as a value, we treat it as a merge function: + // we'll pass it the current value, and you'll use it to determine and + // return the incoming value. + if (typeof incomingValue === "function") { + incomingValue = incomingValue(currentValue); + } + + if (currentValue !== incomingValue) { + update[key] = incomingValue; + } + } + + if (Object.keys(update).length > 0) { + if (includeUpdatedAt) { + update.updatedAt = new Date(); + } + updates.push({ incomingRow, update }); + + // Remove this from the loader cache, so that loading again will fetch + // the updated row. + loader.clear(buildLoaderKey(incomingRow)); + } + } + + // Do a bulk insert of anything that needs added. + if (inserts.length > 0) { + // Get the column names from the first row, and convert them to + // underscore-case instead of camel-case. + const rowKeys = Object.keys(inserts[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 = inserts.map((_) => "(" + qs + ")").join(", "); + const rowValues = inserts.map((row) => rowKeys.map((key) => row[key])); + await db.execute( + `INSERT INTO ${tableName} (${columnsStr}) VALUES ${rowQs};`, + rowValues.flat() + ); + if (afterInsert) { + await afterInsert(); + } + } + + // Do parallel updates of anything that needs updated. + // NOTE: I feel like it's not possible to do bulk updates, even in a + // multi-statement mysql2 request? I might be wrong, but whatever; it's + // very uncommon, and any perf hit would be nbd. + const updatePromises = []; + for (const { incomingRow, update } of updates) { + const rowKeys = Object.keys(update).sort(); + const rowValues = rowKeys.map((k) => update[k]); + const columnNames = rowKeys.map((key) => + key.replace(/[A-Z]/g, (m) => "_" + m[0].toLowerCase()) + ); + const qs = columnNames.map((c) => `${c} = ?`).join(", "); + const [conditionQs, ...conditionValues] = buildUpdateCondition(incomingRow); + updatePromises.push( + db.execute( + `UPDATE ${tableName} SET ${qs} WHERE ${conditionQs} LIMIT 1;`, + [...rowValues, ...conditionValues] + ) + ); + } + await Promise.all(updatePromises); +} + +module.exports = { saveModelingData }; diff --git a/src/server/types/Outfit.js b/src/server/types/Outfit.js index 9a1dede..209fa5f 100644 --- a/src/server/types/Outfit.js +++ b/src/server/types/Outfit.js @@ -1,6 +1,8 @@ const fetch = require("node-fetch"); const { gql } = require("apollo-server"); + const { getPoseFromPetState } = require("../util"); +const { saveModelingData } = require("../modeling"); const typeDefs = gql` type Outfit { @@ -118,8 +120,7 @@ const resolvers = { }; async function loadPetMetaData(petName) { - const url = - `http://www.neopets.com/amfphp/json.php/PetService.getPet` + `/${petName}`; + const url = `http://www.neopets.com/amfphp/json.php/PetService.getPet/${petName}`; const res = await fetch(url); if (!res.ok) { throw new Error( @@ -178,364 +179,4 @@ function getPoseFromPetData(petMetaData, petCustomData) { } } -async function saveModelingData(customPetData, petMetaData, context) { - await Promise.all([ - savePetTypeAndStateModelingData(customPetData, petMetaData, context), - saveItemModelingData(customPetData, context), - saveSwfAssetModelingData(customPetData, context), - ]); -} - -async function savePetTypeAndStateModelingData( - customPetData, - petMetaData, - context -) { - const { - db, - petTypeBySpeciesAndColorLoader, - petStateByPetTypeAndAssetsLoader, - swfAssetByRemoteIdLoader, - } = context; - const incomingPetType = { - colorId: String(customPetData.custom_pet.color_id), - speciesId: String(customPetData.custom_pet.species_id), - bodyId: String(customPetData.custom_pet.body_id), - // NOTE: I skip the image_hash stuff here... on Rails, we set a hash on - // creation, and may or may not bother to update it, I forget? But - // here I don't want to bother with an update. We could maybe do - // a merge function to make it on create only, but eh, I don't - // care enough ^_^` - }; - - await syncToDb(db, [incomingPetType], { - loader: petTypeBySpeciesAndColorLoader, - tableName: "pet_types", - buildLoaderKey: (row) => ({ - speciesId: row.speciesId, - colorId: row.colorId, - }), - buildUpdateCondition: (row) => [ - `species_id = ? AND color_id = ?`, - row.speciesId, - row.colorId, - ], - includeUpdatedAt: false, - }); - - // NOTE: This pet type should have been looked up when syncing pet type, so - // this should be cached. - const petType = await petTypeBySpeciesAndColorLoader.load({ - colorId: String(customPetData.custom_pet.color_id), - speciesId: String(customPetData.custom_pet.species_id), - }); - const biologyAssets = Object.values(customPetData.custom_pet.biology_by_zone); - const incomingPetState = { - petTypeId: petType.id, - swfAssetIds: biologyAssets - .map((row) => row.part_id) - .sort((a, b) => Number(a) - Number(b)) - .join(","), - female: petMetaData.gender === 2 ? 1 : 0, // sorry for this column name :/ - moodId: String(petMetaData.mood), - unconverted: biologyAssets.length === 1 ? 1 : 0, - labeled: 1, - }; - - await syncToDb(db, [incomingPetState], { - loader: petStateByPetTypeAndAssetsLoader, - tableName: "pet_states", - buildLoaderKey: (row) => ({ - petTypeId: row.petTypeId, - swfAssetIds: row.swfAssetIds, - }), - buildUpdateCondition: (row) => [ - `pet_type_id = ? AND swf_asset_ids = ?`, - row.petTypeId, - row.swfAssetIds, - ], - includeCreatedAt: false, - includeUpdatedAt: false, - // For pet states, syncing assets is easy: a new set of assets counts as a - // new state, so, whatever! Just insert the relationships when inserting - // the pet state, and ignore them any other time. - afterInsert: async () => { - // We need to load from the db to get the actual inserted IDs. Not lovely - // for perf, but this is a real new-data model, so that's fine! - let [petState, swfAssets] = await Promise.all([ - petStateByPetTypeAndAssetsLoader.load({ - petTypeId: incomingPetState.petTypeId, - swfAssetIds: incomingPetState.swfAssetIds, - }), - swfAssetByRemoteIdLoader.loadMany( - biologyAssets.map((asset) => ({ - type: "biology", - remoteId: String(asset.part_id), - })) - ), - ]); - swfAssets = swfAssets.filter((sa) => sa != null); - if (swfAssets.length === 0) { - throw new Error(`pet state ${petState.id} has no saved assets?`); - } - const qs = swfAssets.map((_) => `(?, ?, ?)`).join(", "); - const values = swfAssets - .map((sa) => ["PetState", petState.id, sa.id]) - .flat(); - await db.execute( - `INSERT INTO parents_swf_assets (parent_type, parent_id, swf_asset_id) - VALUES ${qs};`, - values - ); - }, - }); -} - -async function saveItemModelingData(customPetData, context) { - const { db, itemLoader, itemTranslationLoader } = context; - - const objectInfos = Object.values(customPetData.object_info_registry); - const incomingItems = objectInfos.map((objectInfo) => ({ - id: String(objectInfo.obj_info_id), - zonesRestrict: objectInfo.zones_restrict, - thumbnailUrl: objectInfo.thumbnail_url, - category: objectInfo.category, - type: objectInfo.type, - rarityIndex: objectInfo.rarity_index, - price: objectInfo.price, - weightLbs: objectInfo.weight_lbs, - })); - const incomingItemTranslations = objectInfos.map((objectInfo) => ({ - itemId: String(objectInfo.obj_info_id), - locale: "en", - name: objectInfo.name, - description: objectInfo.description, - rarity: objectInfo.rarity, - })); - - await Promise.all([ - syncToDb(db, incomingItems, { - loader: itemLoader, - tableName: "items", - buildLoaderKey: (row) => row.id, - buildUpdateCondition: (row) => [`id = ?`, row.id], - }), - syncToDb(db, incomingItemTranslations, { - loader: itemTranslationLoader, - tableName: "item_translations", - buildLoaderKey: (row) => row.itemId, - buildUpdateCondition: (row) => [ - `item_id = ? AND locale = "en"`, - row.itemId, - ], - }), - ]); -} - -async function saveSwfAssetModelingData(customPetData, context) { - const { db, swfAssetByRemoteIdLoader } = context; - - const objectAssets = Object.values(customPetData.object_asset_registry); - const incomingItemSwfAssets = objectAssets.map((objectAsset) => ({ - type: "object", - remoteId: String(objectAsset.asset_id), - url: objectAsset.asset_url, - zoneId: String(objectAsset.zone_id), - zonesRestrict: "", - bodyId: (currentBodyId) => { - const incomingBodyId = String(customPetData.custom_pet.body_id); - - if (currentBodyId == null) { - // If this is a new asset, use the incoming body ID. This might not be - // totally true, the real ID might be 0, but we're conservative to - // start and will update it to 0 if we see a contradiction later! - // - // NOTE: There's an explicitly_body_specific column on Item. We don't - // need to consider it here, because it's specifically to - // override the heuristics in the old app that sometimes set - // bodyId=0 for incoming items depending on their zone. We don't - // do that here! - return incomingBodyId; - } else if (currentBodyId === "0") { - // If this is already an all-bodies asset, keep it that way. - return "0"; - } else if (currentBodyId !== incomingBodyId) { - // If this isn't an all-bodies asset yet, but we've now seen it on two - // different items, then make it an all-bodies asset! - return "0"; - } else { - // Okay, the row already exists, and its body ID matches this one. - // No change! - return currentBodyId; - } - }, - })); - - const biologyAssets = Object.values(customPetData.custom_pet.biology_by_zone); - const incomingPetSwfAssets = biologyAssets.map((biologyAsset) => ({ - type: "biology", - remoteId: String(biologyAsset.part_id), - url: biologyAsset.asset_url, - zoneId: String(biologyAsset.zone_id), - zonesRestrict: biologyAsset.zones_restrict, - bodyId: "0", - })); - - const incomingSwfAssets = [...incomingItemSwfAssets, ...incomingPetSwfAssets]; - - syncToDb(db, incomingSwfAssets, { - loader: swfAssetByRemoteIdLoader, - tableName: "swf_assets", - buildLoaderKey: (row) => ({ type: row.type, remoteId: row.remoteId }), - buildUpdateCondition: (row) => [ - `type = ? AND remote_id = ?`, - row.type, - row.remoteId, - ], - includeUpdatedAt: false, - }); -} - -/** - * 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( - db, - incomingRows, - { - loader, - tableName, - buildLoaderKey, - buildUpdateCondition, - includeCreatedAt = true, - includeUpdatedAt = true, - afterInsert = null, - } -) { - const loaderKeys = incomingRows.map(buildLoaderKey); - const currentRows = await loader.loadMany(loaderKeys); - - const inserts = []; - const updates = []; - for (const index in incomingRows) { - const incomingRow = incomingRows[index]; - const currentRow = currentRows[index]; - - // If there is no corresponding row in the database, prepare an insert. - // TODO: Should probably converge on whether not-found is null or an error - if (currentRow == null || currentRow instanceof Error) { - const insert = {}; - for (const key in incomingRow) { - let incomingValue = incomingRow[key]; - - // If you pass a function as a value, we treat it as a merge function: - // we'll pass it the current value, and you'll use it to determine and - // return the incoming value. In this case, the row doesn't exist yet, - // so the current value is `null`. - if (typeof incomingValue === "function") { - incomingValue = incomingValue(null); - } - - insert[key] = incomingValue; - } - - if (includeCreatedAt) { - insert.createdAt = new Date(); - } - if (includeUpdatedAt) { - insert.updatedAt = new Date(); - } - inserts.push(insert); - - // Remove this from the loader cache, so that loading again will fetch - // the inserted row. - loader.clear(buildLoaderKey(incomingRow)); - - continue; - } - - // If there's a row in the database, and some of the values don't match, - // prepare an update with the updated fields only. - const update = {}; - for (const key in incomingRow) { - const currentValue = currentRow[key]; - let incomingValue = incomingRow[key]; - - // If you pass a function as a value, we treat it as a merge function: - // we'll pass it the current value, and you'll use it to determine and - // return the incoming value. - if (typeof incomingValue === "function") { - incomingValue = incomingValue(currentValue); - } - - if (currentValue !== incomingValue) { - update[key] = incomingValue; - } - } - - if (Object.keys(update).length > 0) { - if (includeUpdatedAt) { - update.updatedAt = new Date(); - } - updates.push({ incomingRow, update }); - - // Remove this from the loader cache, so that loading again will fetch - // the updated row. - loader.clear(buildLoaderKey(incomingRow)); - } - } - - // Do a bulk insert of anything that needs added. - if (inserts.length > 0) { - // Get the column names from the first row, and convert them to - // underscore-case instead of camel-case. - const rowKeys = Object.keys(inserts[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 = inserts.map((_) => "(" + qs + ")").join(", "); - const rowValues = inserts.map((row) => rowKeys.map((key) => row[key])); - await db.execute( - `INSERT INTO ${tableName} (${columnsStr}) VALUES ${rowQs};`, - rowValues.flat() - ); - if (afterInsert) { - await afterInsert(); - } - } - - // Do parallel updates of anything that needs updated. - // NOTE: I feel like it's not possible to do bulk updates, even in a - // multi-statement mysql2 request? I might be wrong, but whatever; it's - // very uncommon, and any perf hit would be nbd. - const updatePromises = []; - for (const { incomingRow, update } of updates) { - const rowKeys = Object.keys(update).sort(); - const rowValues = rowKeys.map((k) => update[k]); - const columnNames = rowKeys.map((key) => - key.replace(/[A-Z]/g, (m) => "_" + m[0].toLowerCase()) - ); - const qs = columnNames.map((c) => `${c} = ?`).join(", "); - const [conditionQs, ...conditionValues] = buildUpdateCondition(incomingRow); - updatePromises.push( - db.execute( - `UPDATE ${tableName} SET ${qs} WHERE ${conditionQs} LIMIT 1;`, - [...rowValues, ...conditionValues] - ) - ); - } - await Promise.all(updatePromises); -} - module.exports = { typeDefs, resolvers }; From fefb798e8755047d75e17c302c0165dbbe5df37c Mon Sep 17 00:00:00 2001 From: Matchu Date: Tue, 6 Oct 2020 05:04:44 -0700 Subject: [PATCH 17/22] extract Pet GQL to Pet.js from Outfit.js --- src/server/index.js | 1 + src/server/types/Outfit.js | 137 --------------------------------- src/server/types/Pet.js | 150 +++++++++++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 137 deletions(-) create mode 100644 src/server/types/Pet.js diff --git a/src/server/index.js b/src/server/index.js index edfacc9..55c5f55 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -40,6 +40,7 @@ const schema = makeExecutableSchema( require("./types/Item"), require("./types/MutationsForSupport"), require("./types/Outfit"), + require("./types/Pet"), require("./types/PetAppearance"), require("./types/User"), require("./types/Zone"), diff --git a/src/server/types/Outfit.js b/src/server/types/Outfit.js index 209fa5f..19c61c4 100644 --- a/src/server/types/Outfit.js +++ b/src/server/types/Outfit.js @@ -1,9 +1,5 @@ -const fetch = require("node-fetch"); const { gql } = require("apollo-server"); -const { getPoseFromPetState } = require("../util"); -const { saveModelingData } = require("../modeling"); - const typeDefs = gql` type Outfit { id: ID! @@ -13,22 +9,8 @@ const typeDefs = gql` closetedItems: [Item!]! } - # TODO: This maybe should move to a separate file? - type Pet { - id: ID! - name: String! - petAppearance: PetAppearance! - wornItems: [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!): Pet } `; @@ -55,128 +37,9 @@ const resolvers = { .map((oir) => ({ id: oir.itemId })); }, }, - Pet: { - species: ({ customPetData }) => ({ - id: customPetData.custom_pet.species_id, - }), - color: ({ customPetData }) => ({ id: customPetData.custom_pet.color_id }), - pose: ({ customPetData, petMetaData }) => - getPoseFromPetData(petMetaData, customPetData), - petAppearance: async ( - { customPetData, petMetaData }, - _, - { petTypeBySpeciesAndColorLoader, petStatesForPetTypeLoader } - ) => { - const petType = await petTypeBySpeciesAndColorLoader.load({ - speciesId: customPetData.custom_pet.species_id, - colorId: customPetData.custom_pet.color_id, - }); - const petStates = await petStatesForPetTypeLoader.load(petType.id); - const pose = getPoseFromPetData(petMetaData, customPetData); - const petState = petStates.find((ps) => getPoseFromPetState(ps) === pose); - return { id: petState.id }; - }, - wornItems: ({ customPetData }) => - 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, - })), - items: (...args) => resolvers.Pet.wornItems(...args), - }, Query: { outfit: (_, { id }) => ({ id }), - petOnNeopetsDotCom: async ( - _, - { petName }, - { - db, - petTypeBySpeciesAndColorLoader, - petStateByPetTypeAndAssetsLoader, - itemLoader, - itemTranslationLoader, - swfAssetByRemoteIdLoader, - } - ) => { - const [customPetData, petMetaData, __] = await Promise.all([ - loadCustomPetData(petName), - loadPetMetaData(petName), - ]); - - await saveModelingData(customPetData, petMetaData, { - db, - petTypeBySpeciesAndColorLoader, - petStateByPetTypeAndAssetsLoader, - itemLoader, - itemTranslationLoader, - swfAssetByRemoteIdLoader, - }); - - return { name: petName, customPetData, petMetaData }; - }, }, }; -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) { - const moodId = petMetaData.mood; - const genderId = petMetaData.gender; - if (Object.keys(petCustomData.custom_pet.biology_by_zone).length === 1) { - return "UNCONVERTED"; - } else 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 }; diff --git a/src/server/types/Pet.js b/src/server/types/Pet.js new file mode 100644 index 0000000..31f6acf --- /dev/null +++ b/src/server/types/Pet.js @@ -0,0 +1,150 @@ +const fetch = require("node-fetch"); +const { gql } = require("apollo-server"); + +const { getPoseFromPetState } = require("../util"); +const { saveModelingData } = require("../modeling"); + +const typeDefs = gql` + type Pet { + id: ID! + name: String! + petAppearance: PetAppearance! + wornItems: [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 { + petOnNeopetsDotCom(petName: String!): Pet + } +`; + +const resolvers = { + Pet: { + species: ({ customPetData }) => ({ + id: customPetData.custom_pet.species_id, + }), + color: ({ customPetData }) => ({ id: customPetData.custom_pet.color_id }), + pose: ({ customPetData, petMetaData }) => + getPoseFromPetData(petMetaData, customPetData), + petAppearance: async ( + { customPetData, petMetaData }, + _, + { petTypeBySpeciesAndColorLoader, petStatesForPetTypeLoader } + ) => { + const petType = await petTypeBySpeciesAndColorLoader.load({ + speciesId: customPetData.custom_pet.species_id, + colorId: customPetData.custom_pet.color_id, + }); + const petStates = await petStatesForPetTypeLoader.load(petType.id); + const pose = getPoseFromPetData(petMetaData, customPetData); + const petState = petStates.find((ps) => getPoseFromPetState(ps) === pose); + return { id: petState.id }; + }, + wornItems: ({ customPetData }) => + 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, + })), + items: (...args) => resolvers.Pet.wornItems(...args), + }, + Query: { + outfit: (_, { id }) => ({ id }), + petOnNeopetsDotCom: async ( + _, + { petName }, + { + db, + petTypeBySpeciesAndColorLoader, + petStateByPetTypeAndAssetsLoader, + itemLoader, + itemTranslationLoader, + swfAssetByRemoteIdLoader, + } + ) => { + const [customPetData, petMetaData, __] = await Promise.all([ + loadCustomPetData(petName), + loadPetMetaData(petName), + ]); + + await saveModelingData(customPetData, petMetaData, { + db, + petTypeBySpeciesAndColorLoader, + petStateByPetTypeAndAssetsLoader, + itemLoader, + itemTranslationLoader, + swfAssetByRemoteIdLoader, + }); + + return { name: petName, customPetData, petMetaData }; + }, + }, +}; + +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) { + const moodId = petMetaData.mood; + const genderId = petMetaData.gender; + if (Object.keys(petCustomData.custom_pet.biology_by_zone).length === 1) { + return "UNCONVERTED"; + } else 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 }; From da72837d9e4204d0ba8853890aa1c3311103dd8b Mon Sep 17 00:00:00 2001 From: Matchu Date: Tue, 6 Oct 2020 05:41:09 -0700 Subject: [PATCH 18/22] modeling saves item-to-asset relationships this is the last one to get parity with current modeling, I think?? I'm gonna add one more feature though: removing no-longer-used assets from the item --- src/server/modeling.js | 44 ++++- src/server/query-tests/Pet.test.js | 7 + .../__snapshots__/Pet.test.js.snap | 184 +++++++++++++++++- 3 files changed, 230 insertions(+), 5 deletions(-) diff --git a/src/server/modeling.js b/src/server/modeling.js index 5695ff9..a92127b 100644 --- a/src/server/modeling.js +++ b/src/server/modeling.js @@ -212,6 +212,18 @@ async function saveSwfAssetModelingData(customPetData, context) { const incomingSwfAssets = [...incomingItemSwfAssets, ...incomingPetSwfAssets]; + // Build a map from asset ID to item ID. We'll use this later to build the + // new parents_swf_assets rows. + const assetIdToItemIdMap = new Map(); + const objectInfos = Object.values(customPetData.object_info_registry); + for (const objectInfo of objectInfos) { + const itemId = String(objectInfo.obj_info_id); + const assetIds = Object.values(objectInfo.assets_by_zone).map(String); + for (const assetId of assetIds) { + assetIdToItemIdMap.set(assetId, itemId); + } + } + syncToDb(db, incomingSwfAssets, { loader: swfAssetByRemoteIdLoader, tableName: "swf_assets", @@ -222,6 +234,36 @@ async function saveSwfAssetModelingData(customPetData, context) { row.remoteId, ], includeUpdatedAt: false, + afterInsert: async (inserts) => { + // After inserting the assets, insert corresponding rows in + // parents_swf_assets for item assets, to mark the asset as belonging to + // the item. (We do this separately for pet states, so that we can get + // the pet state ID first.) + const itemAssetInserts = inserts.filter((i) => i.type === "object"); + const qs = itemAssetInserts + .map( + (_) => + // A bit cheesy: we use a subquery here to insert _our_ ID for the + // asset, despite only having remote_id available here. This saves + // us from another round-trip to SELECT the inserted IDs. + `(?, ?, ` + + `(SELECT id FROM swf_assets WHERE type = "object" AND remote_id = ?))` + ) + .join(", "); + const values = itemAssetInserts + .map(({ remoteId: swfAssetId }) => [ + "Item", + assetIdToItemIdMap.get(swfAssetId), + swfAssetId, + ]) + .flat(); + + await db.execute( + `INSERT INTO parents_swf_assets (parent_type, parent_id, swf_asset_id) + VALUES ${qs}`, + values + ); + }, }); } @@ -340,7 +382,7 @@ async function syncToDb( rowValues.flat() ); if (afterInsert) { - await afterInsert(); + await afterInsert(inserts); } } diff --git a/src/server/query-tests/Pet.test.js b/src/server/query-tests/Pet.test.js index f0c1f8c..401e975 100644 --- a/src/server/query-tests/Pet.test.js +++ b/src/server/query-tests/Pet.test.js @@ -192,6 +192,13 @@ describe("Pet", () => { rarityIndex isNc createdAt + + appearanceOn(colorId: "75", speciesId: "54") { + layers { + id + swfUrl + } + } } } `, diff --git a/src/server/query-tests/__snapshots__/Pet.test.js.snap b/src/server/query-tests/__snapshots__/Pet.test.js.snap index 1111b37..bcb3f36 100644 --- a/src/server/query-tests/__snapshots__/Pet.test.js.snap +++ b/src/server/query-tests/__snapshots__/Pet.test.js.snap @@ -488,6 +488,36 @@ Array [ "75", ], ], + Array [ + "INSERT INTO parents_swf_assets (parent_type, parent_id, swf_asset_id) + VALUES (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?))", + Array [ + "Item", + "37375", + "6829", + "Item", + "38913", + "14855", + "Item", + "38912", + "14856", + "Item", + "38911", + "14857", + "Item", + "43014", + "36414", + "Item", + "43397", + "39646", + "Item", + "37229", + "51959", + "Item", + "48313", + "56478", + ], + ], Array [ "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", Array [ @@ -561,6 +591,14 @@ exports[`Pet models new pet and item data 3`] = ` Object { "items": Array [ Object { + "appearanceOn": Object { + "layers": Array [ + Object { + "id": "7", + "swfUrl": "http://images.neopets.com/cp/items/swf/000/000/051/51959_4439727c48.swf", + }, + ], + }, "createdAt": "2020-01-01T00:00:00.000Z", "description": "What does this ball actually do?", "id": "37229", @@ -570,6 +608,14 @@ Object { "thumbnailUrl": "http://images.neopets.com/items/gif_magicball_table.gif", }, Object { + "appearanceOn": Object { + "layers": Array [ + Object { + "id": "1", + "swfUrl": "http://images.neopets.com/cp/items/swf/000/000/006/6829_1707e50385.swf", + }, + ], + }, "createdAt": "2020-01-01T00:00:00.000Z", "description": "Dont forget to wish upon a star.", "id": "37375", @@ -579,6 +625,14 @@ Object { "thumbnailUrl": "http://images.neopets.com/items/bg_moonstars.gif", }, Object { + "appearanceOn": Object { + "layers": Array [ + Object { + "id": "4", + "swfUrl": "http://images.neopets.com/cp/items/swf/000/000/014/14857_d43380ef66.swf", + }, + ], + }, "createdAt": "2020-01-01T00:00:00.000Z", "description": "Hide your face and hair so no one can recognise you.", "id": "38911", @@ -588,6 +642,14 @@ Object { "thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_hood.gif", }, Object { + "appearanceOn": Object { + "layers": Array [ + Object { + "id": "3", + "swfUrl": "http://images.neopets.com/cp/items/swf/000/000/014/14856_46c1b32797.swf", + }, + ], + }, "createdAt": "2020-01-01T00:00:00.000Z", "description": "This robe is great for being stealthy.", "id": "38912", @@ -597,6 +659,14 @@ Object { "thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_robe.gif", }, Object { + "appearanceOn": Object { + "layers": Array [ + Object { + "id": "2", + "swfUrl": "http://images.neopets.com/cp/items/swf/000/000/014/14855_215f367070.swf", + }, + ], + }, "createdAt": "2020-01-01T00:00:00.000Z", "description": "Dont leave any trace that you were there with these gloves.", "id": "38913", @@ -606,6 +676,14 @@ Object { "thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_gloves.gif", }, Object { + "appearanceOn": Object { + "layers": Array [ + Object { + "id": "5", + "swfUrl": "http://images.neopets.com/cp/items/swf/000/000/036/36414_1e2aaab4ad.swf", + }, + ], + }, "createdAt": "2020-01-01T00:00:00.000Z", "description": "These leaves almost look magical with their gentle glow.", "id": "43014", @@ -615,6 +693,14 @@ Object { "thumbnailUrl": "http://images.neopets.com/items/toy_stringlight_illleaf.gif", }, Object { + "appearanceOn": Object { + "layers": Array [ + Object { + "id": "6", + "swfUrl": "http://images.neopets.com/cp/items/swf/000/000/039/39646_e129e22ada.swf", + }, + ], + }, "createdAt": "2020-01-01T00:00:00.000Z", "description": "This jewelled staff shines with a magical light.", "id": "43397", @@ -624,6 +710,14 @@ Object { "thumbnailUrl": "http://images.neopets.com/items/mall_staff_jewelled.gif", }, Object { + "appearanceOn": Object { + "layers": Array [ + Object { + "id": "8", + "swfUrl": "http://images.neopets.com/cp/items/swf/000/000/056/56478_eabc28e7c7.swf", + }, + ], + }, "createdAt": "2020-01-01T00:00:00.000Z", "description": "Even the announcers of the Altador Cup celebrate. This was given out by the Advent Calendar in Y11.", "id": "48313", @@ -712,6 +806,31 @@ Array [ "1", ], ], + Array [ + "SELECT sa.*, rel.parent_id FROM swf_assets sa + INNER JOIN parents_swf_assets rel ON + rel.parent_type = \\"Item\\" AND + rel.swf_asset_id = sa.id + WHERE (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0)) OR (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0)) OR (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0)) OR (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0)) OR (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0)) OR (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0)) OR (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0)) OR (rel.parent_id = ? AND (sa.body_id = ? OR sa.body_id = 0))", + Array [ + "37229", + "180", + "37375", + "180", + "38911", + "180", + "38912", + "180", + "38913", + "180", + "43014", + "180", + "43397", + "180", + "48313", + "180", + ], + ], Array [ "SELECT sa.*, rel.parent_id FROM swf_assets sa INNER JOIN parents_swf_assets rel ON @@ -1060,6 +1179,36 @@ Array [ "43397", ], ], + Array [ + "INSERT INTO parents_swf_assets (parent_type, parent_id, swf_asset_id) + VALUES (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?))", + Array [ + "Item", + "37375", + "6829", + "Item", + "38913", + "14855", + "Item", + "38912", + "14856", + "Item", + "38911", + "14857", + "Item", + "43014", + "36414", + "Item", + "43397", + "39646", + "Item", + "37229", + "51959", + "Item", + "48313", + "56478", + ], + ], Array [ "SELECT * FROM pet_states WHERE (pet_type_id = ? AND swf_asset_ids = ?)", Array [ @@ -1473,11 +1622,30 @@ Array [ ], ], Array [ - "UPDATE swf_assets SET body_id = ? WHERE type = ? AND remote_id = ? LIMIT 1;", + "INSERT INTO parents_swf_assets (parent_type, parent_id, swf_asset_id) + VALUES (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?)), (?, ?, (SELECT id FROM swf_assets WHERE type = \\"object\\" AND remote_id = ?))", Array [ - "0", - "object", - "6829", + "Item", + "38913", + "14855", + "Item", + "38912", + "14856", + "Item", + "38911", + "14857", + "Item", + "43014", + "36414", + "Item", + "43397", + "39646", + "Item", + "37229", + "51959", + "Item", + "48313", + "56478", ], ], Array [ @@ -1487,6 +1655,14 @@ Array [ "7941,7942,7946,21057,21060,24008", ], ], + Array [ + "UPDATE swf_assets SET body_id = ? WHERE type = ? AND remote_id = ? LIMIT 1;", + Array [ + "0", + "object", + "6829", + ], + ], Array [ "INSERT INTO pet_states (female, labeled, mood_id, pet_type_id, swf_asset_ids, unconverted) VALUES (?, ?, ?, ?, ?, ?);", Array [ From df2d814c13bb7e190f07706e83c9ac88ad085399 Mon Sep 17 00:00:00 2001 From: Matchu Date: Tue, 6 Oct 2020 06:11:22 -0700 Subject: [PATCH 19/22] enable running against a local dev database had to add some missing tables, but it seems to work! (some known errors though, from assumptions we make e.g. blue acaras existing) --- package.json | 2 +- scripts/setup-mysql-dev-constants.sql | 58 ++++++++++++++++++++++++++- scripts/setup-mysql-dev-schema.sql | 2 +- src/server/db.js | 30 ++++++++++++-- 4 files changed, 85 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 480f3f5..d3ccd45 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "mysql-dev": "mysql --host=localhost --user=impress_2020_dev --password=impress_2020_dev --database=impress_2020_dev", "mysql-admin": "mysql --host=impress.openneo.net --user=matchu --password --database=openneo_impress", "mysqldump": "mysqldump --host=impress.openneo.net --user=$(dotenv -p IMPRESS_MYSQL_USER) --password=$(dotenv -p IMPRESS_MYSQL_PASSWORD) --column-statistics=0", - "download-mysql-schema": "yarn --silent mysqldump --no-data openneo_impress items item_translations parents_swf_assets pet_types pet_states swf_assets | sed 's/ AUTO_INCREMENT=[0-9]*//g' > scripts/setup-mysql-dev-schema.sql && yarn --silent mysqldump openneo_impress species species_translations colors color_translations > scripts/setup-mysql-dev-constants.sql", + "download-mysql-schema": "yarn --silent mysqldump --no-data openneo_impress items item_translations parents_swf_assets pet_types pet_states swf_assets | sed 's/ AUTO_INCREMENT=[0-9]*//g' > scripts/setup-mysql-dev-schema.sql && yarn --silent mysqldump openneo_impress species species_translations colors color_translations zones zone_translations > scripts/setup-mysql-dev-constants.sql", "setup-mysql": "yarn mysql-admin < scripts/setup-mysql.sql", "setup-mysql-dev": "yarn mysql-dev < scripts/setup-mysql-dev-constants.sql && yarn mysql-dev < scripts/setup-mysql-dev-schema.sql", "build-cached-data": "node -r dotenv/config scripts/build-cached-data.js", diff --git a/scripts/setup-mysql-dev-constants.sql b/scripts/setup-mysql-dev-constants.sql index 0180c57..45d64a7 100644 --- a/scripts/setup-mysql-dev-constants.sql +++ b/scripts/setup-mysql-dev-constants.sql @@ -123,6 +123,62 @@ LOCK TABLES `color_translations` WRITE; INSERT INTO `color_translations` VALUES (1,1,'en','alien','2013-01-27 06:10:44','2013-01-27 06:10:44'),(2,2,'en','apple','2013-01-27 06:10:44','2013-01-27 06:10:44'),(3,3,'en','asparagus','2013-01-27 06:10:44','2013-01-27 06:10:44'),(4,4,'en','aubergine','2013-01-27 06:10:44','2013-01-27 06:10:44'),(5,5,'en','avocado','2013-01-27 06:10:44','2013-01-27 06:10:44'),(6,6,'en','baby','2013-01-27 06:10:44','2013-01-27 06:10:44'),(7,7,'en','biscuit','2013-01-27 06:10:44','2013-01-27 06:10:44'),(8,8,'en','blue','2013-01-27 06:10:44','2013-01-27 06:10:44'),(9,9,'en','blueberry','2013-01-27 06:10:44','2013-01-27 06:10:44'),(10,10,'en','brown','2013-01-27 06:10:44','2013-01-27 06:10:44'),(11,11,'en','camouflage','2013-01-27 06:10:44','2013-01-27 06:10:44'),(12,12,'en','carrot','2013-01-27 06:10:44','2013-01-27 06:10:44'),(13,13,'en','checkered','2013-01-27 06:10:44','2013-01-27 06:10:44'),(14,14,'en','chocolate','2013-01-27 06:10:44','2013-01-27 06:10:44'),(15,15,'en','chokato','2013-01-27 06:10:44','2013-01-27 06:10:44'),(16,16,'en','christmas','2013-01-27 06:10:44','2013-01-27 06:10:44'),(17,17,'en','clay','2013-01-27 06:10:44','2013-01-27 06:10:44'),(18,18,'en','cloud','2013-01-27 06:10:44','2013-01-27 06:10:44'),(19,19,'en','coconut','2013-01-27 06:10:44','2013-01-27 06:10:44'),(20,20,'en','custard','2013-01-27 06:10:44','2013-01-27 06:10:44'),(21,21,'en','darigan','2013-01-27 06:10:44','2013-01-27 06:10:44'),(22,22,'en','desert','2013-01-27 06:10:44','2013-01-27 06:10:44'),(23,23,'en','disco','2013-01-27 06:10:44','2013-01-27 06:10:44'),(24,24,'en','durian','2013-01-27 06:10:44','2013-01-27 06:10:44'),(25,25,'en','electric','2013-01-27 06:10:44','2013-01-27 06:10:44'),(26,26,'en','faerie','2013-01-27 06:10:44','2013-01-27 06:10:44'),(27,27,'en','fire','2013-01-27 06:10:44','2013-01-27 06:10:44'),(28,28,'en','garlic','2013-01-27 06:10:44','2013-01-27 06:10:44'),(29,29,'en','ghost','2013-01-27 06:10:44','2013-01-27 06:10:44'),(30,30,'en','glowing','2013-01-27 06:10:44','2013-01-27 06:10:44'),(31,31,'en','gold','2013-01-27 06:10:44','2013-01-27 06:10:44'),(32,32,'en','gooseberry','2013-01-27 06:10:44','2013-01-27 06:10:44'),(33,33,'en','grape','2013-01-27 06:10:44','2013-01-27 06:10:44'),(34,34,'en','green','2013-01-27 06:10:44','2013-01-27 06:10:44'),(35,35,'en','grey','2013-01-27 06:10:44','2013-01-27 06:10:44'),(36,36,'en','halloween','2013-01-27 06:10:44','2013-01-27 06:10:44'),(37,37,'en','ice','2013-01-27 06:10:44','2013-01-27 06:10:44'),(38,38,'en','invisible','2013-01-27 06:10:44','2013-01-27 06:10:44'),(39,39,'en','island','2013-01-27 06:10:44','2013-01-27 06:10:44'),(40,40,'en','jelly','2013-01-27 06:10:44','2013-01-27 06:10:44'),(41,41,'en','lemon','2013-01-27 06:10:44','2013-01-27 06:10:44'),(42,42,'en','lime','2013-01-27 06:10:44','2013-01-27 06:10:44'),(43,43,'en','mallow','2013-01-27 06:10:44','2013-01-27 06:10:44'),(44,44,'en','maraquan','2013-01-27 06:10:44','2013-01-27 06:10:44'),(45,45,'en','msp','2013-01-27 06:10:45','2013-01-27 06:10:45'),(46,46,'en','mutant','2013-01-27 06:10:45','2013-01-27 06:10:45'),(47,47,'en','orange','2013-01-27 06:10:45','2013-01-27 06:10:45'),(48,48,'en','pea','2013-01-27 06:10:45','2013-01-27 06:10:45'),(49,49,'en','peach','2013-01-27 06:10:45','2013-01-27 06:10:45'),(50,50,'en','pear','2013-01-27 06:10:45','2013-01-27 06:10:45'),(51,51,'en','pepper','2013-01-27 06:10:45','2013-01-27 06:10:45'),(52,52,'en','pineapple','2013-01-27 06:10:45','2013-01-27 06:10:45'),(53,53,'en','pink','2013-01-27 06:10:45','2013-01-27 06:10:45'),(54,54,'en','pirate','2013-01-27 06:10:45','2013-01-27 06:10:45'),(55,55,'en','plum','2013-01-27 06:10:45','2013-01-27 06:10:45'),(56,56,'en','plushie','2013-01-27 06:10:45','2013-01-27 06:10:45'),(57,57,'en','purple','2013-01-27 06:10:45','2013-01-27 06:10:45'),(58,58,'en','quigukiboy','2013-01-27 06:10:45','2013-01-27 06:10:45'),(59,59,'en','quigukigirl','2013-01-27 06:10:45','2013-01-27 06:10:45'),(60,60,'en','rainbow','2013-01-27 06:10:45','2013-01-27 06:10:45'),(61,61,'en','red','2013-01-27 06:10:45','2013-01-27 06:10:45'),(62,62,'en','robot','2013-01-27 06:10:45','2013-01-27 06:10:45'),(63,63,'en','royalboy','2013-01-27 06:10:45','2013-01-27 06:10:45'),(64,64,'en','royalgirl','2013-01-27 06:10:45','2013-01-27 06:10:45'),(65,65,'en','shadow','2013-01-27 06:10:45','2013-01-27 06:10:45'),(66,66,'en','silver','2013-01-27 06:10:45','2013-01-27 06:10:45'),(67,67,'en','sketch','2013-01-27 06:10:45','2013-01-27 06:10:45'),(68,68,'en','skunk','2013-01-27 06:10:45','2013-01-27 06:10:45'),(69,69,'en','snot','2013-01-27 06:10:45','2013-01-27 06:10:45'),(70,70,'en','snow','2013-01-27 06:10:45','2013-01-27 06:10:45'),(71,71,'en','speckled','2013-01-27 06:10:45','2013-01-27 06:10:45'),(72,72,'en','split','2013-01-27 06:10:45','2013-01-27 06:10:45'),(73,73,'en','sponge','2013-01-27 06:10:45','2013-01-27 06:10:45'),(74,74,'en','spotted','2013-01-27 06:10:45','2013-01-27 06:10:45'),(75,75,'en','starry','2013-01-27 06:10:45','2013-01-27 06:10:45'),(76,76,'en','strawberry','2013-01-27 06:10:45','2013-01-27 06:10:45'),(77,77,'en','striped','2013-01-27 06:10:45','2013-01-27 06:10:45'),(78,78,'en','thornberry','2013-01-27 06:10:45','2013-01-27 06:10:45'),(79,79,'en','tomato','2013-01-27 06:10:45','2013-01-27 06:10:45'),(80,80,'en','tyrannian','2013-01-27 06:10:45','2013-01-27 06:10:45'),(81,81,'en','usuki boy','2013-01-27 06:10:45','2013-12-21 01:15:29'),(82,82,'en','usuki girl','2013-01-27 06:10:45','2013-12-21 01:15:29'),(83,83,'en','white','2013-01-27 06:10:45','2013-01-27 06:10:45'),(84,84,'en','yellow','2013-01-27 06:10:45','2013-01-27 06:10:45'),(85,85,'en','zombie','2013-01-27 06:10:45','2013-01-27 06:10:45'),(86,86,'en','onion','2013-01-27 06:10:45','2013-01-27 06:10:45'),(87,87,'en','magma','2013-01-27 06:10:45','2013-01-27 06:10:45'),(88,88,'en','relic','2013-01-27 06:10:45','2013-01-27 06:10:45'),(89,89,'en','woodland','2013-01-27 06:10:45','2013-01-27 06:10:45'),(90,90,'en','transparent','2013-01-27 06:10:45','2013-01-27 06:10:45'),(91,91,'en','maractite','2013-01-27 06:10:45','2013-01-27 06:10:45'),(92,92,'en','8-bit','2013-01-27 06:10:45','2013-01-27 06:10:45'),(93,93,'en','swamp gas','2013-01-27 06:10:45','2013-01-27 06:10:45'),(94,94,'en','water','2013-01-27 06:10:45','2013-01-27 06:10:45'),(95,95,'en','wraith','2013-01-27 06:10:45','2013-01-27 06:10:45'),(96,96,'en','eventide','2013-01-27 06:10:45','2013-01-27 06:10:45'),(97,97,'en','elderlyboy','2013-01-27 06:10:45','2013-01-27 06:10:45'),(98,98,'en','elderlygirl','2013-01-27 06:10:45','2013-01-27 06:10:45'),(99,99,'en','stealthy','2013-01-27 06:10:45','2013-01-27 06:10:45'),(100,100,'en','dimensional','2013-01-27 06:10:45','2013-01-27 06:10:45'),(101,94,'pt','Água','2013-01-27 06:11:50','2013-01-27 06:11:50'),(102,92,'pt','8-bit','2013-01-27 06:11:50','2013-01-27 06:11:50'),(103,5,'pt','abacate','2013-01-27 06:11:50','2013-01-27 06:11:50'),(104,52,'pt','abacaxi','2013-01-27 06:11:50','2013-01-27 06:11:50'),(105,28,'pt','alho','2013-01-27 06:11:50','2013-01-27 06:11:50'),(106,1,'pt','alienígena','2013-01-27 06:11:50','2013-01-27 06:11:50'),(107,84,'pt','amarelo','2013-01-27 06:11:50','2013-01-27 06:11:50'),(108,55,'pt','ameixa','2013-01-27 06:11:50','2013-01-27 06:11:50'),(109,60,'pt','arco-íris','2013-01-27 06:11:50','2013-01-27 06:11:50'),(110,17,'pt','argila','2013-01-27 06:11:50','2013-01-27 06:11:50'),(111,3,'pt','aspargos','2013-01-27 06:11:50','2013-01-27 06:11:50'),(112,95,'pt','assombração','2013-01-27 06:11:50','2013-01-27 06:11:50'),(113,8,'pt','azul','2013-01-27 06:11:50','2013-01-27 06:11:50'),(114,6,'pt','bebê','2013-01-27 06:11:50','2013-01-27 06:11:50'),(115,4,'pt','berinjela','2013-01-27 06:11:50','2013-01-27 06:11:50'),(116,7,'pt','biscoito','2013-01-27 06:11:50','2013-01-27 06:11:50'),(117,89,'pt','bosque','2013-01-27 06:11:50','2013-01-27 06:11:50'),(118,83,'pt','branco','2013-01-27 06:11:50','2013-01-27 06:11:50'),(119,30,'pt','brilhante','2013-01-27 06:11:50','2013-01-27 06:11:50'),(120,11,'pt','camuflagem','2013-01-27 06:11:50','2013-01-27 06:11:50'),(121,86,'pt','cebola','2013-01-27 06:11:50','2013-01-27 06:11:50'),(122,12,'pt','cenoura','2013-01-27 06:11:50','2013-01-27 06:11:50'),(123,14,'pt','chocolate','2013-01-27 06:11:50','2013-01-27 06:11:50'),(124,15,'pt','chokato','2013-01-27 06:11:50','2013-01-27 06:11:50'),(125,35,'pt','cinza','2013-01-27 06:11:50','2013-01-27 06:11:50'),(126,19,'pt','coco','2013-01-27 06:11:50','2013-01-27 06:11:50'),(127,20,'pt','creme','2013-01-27 06:11:50','2013-01-27 06:11:50'),(128,96,'pt','crepúsculo','2013-01-27 06:11:50','2013-01-27 06:11:50'),(129,21,'pt','darigan','2013-01-27 06:11:50','2013-01-27 06:11:50'),(130,22,'pt','deserto','2013-01-27 06:11:50','2013-01-27 06:11:50'),(131,100,'pt','dimensional','2013-01-27 06:11:50','2013-01-27 06:11:50'),(132,23,'pt','discoteca','2013-01-27 06:11:50','2013-01-27 06:11:50'),(133,72,'pt','dividido','2013-01-27 06:11:50','2013-01-27 06:11:50'),(134,31,'pt','dourado','2013-01-27 06:11:50','2013-01-27 06:11:50'),(135,24,'pt','durian','2013-01-27 06:11:50','2013-01-27 06:11:50'),(136,25,'pt','elétrico','2013-01-27 06:11:50','2013-01-27 06:11:50'),(137,48,'pt','ervilha','2013-01-27 06:11:50','2013-01-27 06:11:50'),(138,73,'pt','esponja','2013-01-27 06:11:50','2013-01-27 06:11:50'),(139,75,'pt','estrelado','2013-01-27 06:11:50','2013-01-27 06:11:50'),(140,26,'pt','fada','2013-01-27 06:11:50','2013-01-27 06:11:50'),(141,29,'pt','fantasma','2013-01-27 06:11:50','2013-01-27 06:11:50'),(142,27,'pt','fogo','2013-01-27 06:11:50','2013-01-27 06:11:50'),(143,78,'pt','frutathorn','2013-01-27 06:11:50','2013-01-27 06:11:50'),(144,68,'pt','gambá','2013-01-27 06:11:50','2013-01-27 06:11:50'),(145,82,'pt','garota usuki','2013-01-27 06:11:50','2013-01-27 06:11:50'),(146,81,'pt','garoto usuki','2013-01-27 06:11:50','2013-01-27 06:11:50'),(147,93,'pt','gas metano','2013-01-27 06:11:50','2013-01-27 06:11:50'),(148,40,'pt','gelatina','2013-01-27 06:11:50','2013-01-27 06:11:50'),(149,37,'pt','gelo','2013-01-27 06:11:50','2013-01-27 06:11:50'),(150,32,'pt','groselha','2013-01-27 06:11:50','2013-01-27 06:11:50'),(151,36,'pt','halloween','2013-01-27 06:11:50','2013-01-27 06:11:50'),(152,98,'pt','idosa ','2013-01-27 06:11:50','2013-01-27 06:11:50'),(153,97,'pt','idoso ','2013-01-27 06:11:50','2013-01-27 06:11:50'),(154,39,'pt','ilha','2013-01-27 06:11:50','2013-01-27 06:11:50'),(155,38,'pt','invisível','2013-01-27 06:11:50','2013-01-27 06:11:50'),(156,47,'pt','laranja','2013-01-27 06:11:50','2013-01-27 06:11:50'),(157,41,'pt','limão','2013-01-27 06:11:50','2013-01-27 06:11:50'),(158,42,'pt','lima','2013-01-27 06:11:50','2013-01-27 06:11:50'),(159,77,'pt','listrado','2013-01-27 06:11:50','2013-01-27 06:11:50'),(160,2,'pt','maçã','2013-01-27 06:11:51','2013-01-27 06:11:51'),(161,87,'pt','magma','2013-01-27 06:11:51','2013-01-27 06:11:51'),(162,71,'pt','manchado','2013-01-27 06:11:51','2013-01-27 06:11:51'),(163,91,'pt','maractita','2013-01-27 06:11:51','2013-01-27 06:11:51'),(164,44,'pt','maraquano','2013-01-27 06:11:51','2013-01-27 06:11:51'),(165,10,'pt','marrom','2013-01-27 06:11:51','2013-01-27 06:11:51'),(166,43,'pt','marshmallow','2013-01-27 06:11:51','2013-01-27 06:11:51'),(167,69,'pt','meleca','2013-01-27 06:11:51','2013-01-27 06:11:51'),(168,9,'pt','mirtilo','2013-01-27 06:11:51','2013-01-27 06:11:51'),(169,76,'pt','morango','2013-01-27 06:11:51','2013-01-27 06:11:51'),(170,45,'pt','msp','2013-01-27 06:11:51','2013-01-27 06:11:51'),(171,46,'pt','mutante','2013-01-27 06:11:51','2013-01-27 06:11:51'),(172,16,'pt','natal','2013-01-27 06:11:51','2013-01-27 06:11:51'),(173,70,'pt','neve','2013-01-27 06:11:51','2013-01-27 06:11:51'),(174,18,'pt','nuvem','2013-01-27 06:11:51','2013-01-27 06:11:51'),(175,50,'pt','pêra','2013-01-27 06:11:51','2013-01-27 06:11:51'),(176,49,'pt','pêssego','2013-01-27 06:11:51','2013-01-27 06:11:51'),(177,56,'pt','pelúcia','2013-01-27 06:11:51','2013-01-27 06:11:51'),(178,51,'pt','pimenta','2013-01-27 06:11:51','2013-01-27 06:11:51'),(179,74,'pt','pintado','2013-01-27 06:11:51','2013-01-27 06:11:51'),(180,54,'pt','pirata','2013-01-27 06:11:51','2013-01-27 06:11:51'),(181,66,'pt','prata','2013-01-27 06:11:51','2013-01-27 06:11:51'),(182,59,'pt','quiguki fêmea','2013-01-27 06:11:51','2013-01-27 06:11:51'),(183,58,'pt','quiguki macho','2013-01-27 06:11:51','2013-01-27 06:11:51'),(184,67,'pt','rascunho','2013-01-27 06:11:51','2013-01-27 06:11:51'),(185,64,'pt','real fêmea','2013-01-27 06:11:51','2013-01-27 06:11:51'),(186,63,'pt','real macho','2013-01-27 06:11:51','2013-01-27 06:11:51'),(187,88,'pt','relíquia','2013-01-27 06:11:51','2013-01-27 06:11:51'),(188,62,'pt','robô','2013-01-27 06:11:51','2013-01-27 06:11:51'),(189,53,'pt','rosa','2013-01-27 06:11:51','2013-01-27 06:11:51'),(190,57,'pt','roxo','2013-01-27 06:11:51','2013-01-27 06:11:51'),(191,99,'pt','sigiloso','2013-01-27 06:11:51','2013-01-27 06:11:51'),(192,65,'pt','sombra','2013-01-27 06:11:51','2013-01-27 06:11:51'),(193,79,'pt','tomate','2013-01-27 06:11:51','2013-01-27 06:11:51'),(194,90,'pt','transparente','2013-01-27 06:11:51','2013-01-27 06:11:51'),(195,80,'pt','tyranniano','2013-01-27 06:11:51','2013-01-27 06:11:51'),(196,33,'pt','uva','2013-01-27 06:11:51','2013-01-27 06:11:51'),(197,34,'pt','verde','2013-01-27 06:11:51','2013-01-27 06:11:51'),(198,61,'pt','vermelho','2013-01-27 06:11:51','2013-01-27 06:11:51'),(199,13,'pt','xadrez','2013-01-27 06:11:51','2013-01-27 06:11:51'),(200,85,'pt','zumbi','2013-01-27 06:11:51','2013-01-27 06:11:51'),(201,92,'en-MEEP','8-bit','2013-01-28 02:46:52','2013-01-28 02:46:52'),(202,1,'en-MEEP','alien','2013-01-28 02:46:52','2013-01-28 02:46:52'),(203,2,'en-MEEP','apple','2013-01-28 02:46:52','2013-01-28 02:46:52'),(204,3,'en-MEEP','asparagus','2013-01-28 02:46:52','2013-01-28 02:46:52'),(205,4,'en-MEEP','aubergine','2013-01-28 02:46:52','2013-01-28 02:46:52'),(206,5,'en-MEEP','avocado','2013-01-28 02:46:52','2013-01-28 02:46:52'),(207,6,'en-MEEP','baby','2013-01-28 02:46:52','2013-01-28 02:46:52'),(208,7,'en-MEEP','biscuit','2013-01-28 02:46:52','2013-01-28 02:46:52'),(209,8,'en-MEEP','blue','2013-01-28 02:46:52','2013-01-28 02:46:52'),(210,9,'en-MEEP','blueberry','2013-01-28 02:46:52','2013-01-28 02:46:52'),(211,10,'en-MEEP','brown','2013-01-28 02:46:52','2013-01-28 02:46:52'),(212,11,'en-MEEP','camouflage','2013-01-28 02:46:52','2013-01-28 02:46:52'),(213,12,'en-MEEP','carrot','2013-01-28 02:46:52','2013-01-28 02:46:52'),(214,13,'en-MEEP','checkered','2013-01-28 02:46:52','2013-01-28 02:46:52'),(215,14,'en-MEEP','chocolate','2013-01-28 02:46:52','2013-01-28 02:46:52'),(216,15,'en-MEEP','chokato','2013-01-28 02:46:52','2013-01-28 02:46:52'),(217,16,'en-MEEP','christmas','2013-01-28 02:46:52','2013-01-28 02:46:52'),(218,17,'en-MEEP','clay','2013-01-28 02:46:52','2013-01-28 02:46:52'),(219,18,'en-MEEP','cloud','2013-01-28 02:46:52','2013-01-28 02:46:52'),(220,19,'en-MEEP','coconut','2013-01-28 02:46:52','2013-01-28 02:46:52'),(221,20,'en-MEEP','custard','2013-01-28 02:46:52','2013-01-28 02:46:52'),(222,21,'en-MEEP','darigan','2013-01-28 02:46:52','2013-01-28 02:46:52'),(223,22,'en-MEEP','desert','2013-01-28 02:46:52','2013-01-28 02:46:53'),(224,100,'en-MEEP','dimensional','2013-01-28 02:46:53','2013-01-28 02:46:53'),(225,23,'en-MEEP','disco','2013-01-28 02:46:53','2013-01-28 02:46:53'),(226,24,'en-MEEP','durian','2013-01-28 02:46:53','2013-01-28 02:46:53'),(227,97,'en-MEEP','elderlyboy','2013-01-28 02:46:53','2013-01-28 02:46:53'),(228,98,'en-MEEP','elderlygirl','2013-01-28 02:46:53','2013-01-28 02:46:53'),(229,25,'en-MEEP','electric','2013-01-28 02:46:53','2013-01-28 02:46:53'),(230,96,'en-MEEP','eventide','2013-01-28 02:46:53','2013-01-28 02:46:53'),(231,26,'en-MEEP','faerie','2013-01-28 02:46:53','2013-01-28 02:46:53'),(232,27,'en-MEEP','fire','2013-01-28 02:46:53','2013-01-28 02:46:53'),(233,28,'en-MEEP','garlic','2013-01-28 02:46:53','2013-01-28 02:46:53'),(234,29,'en-MEEP','ghost','2013-01-28 02:46:53','2013-01-28 02:46:53'),(235,30,'en-MEEP','glowing','2013-01-28 02:46:53','2013-01-28 02:46:53'),(236,31,'en-MEEP','gold','2013-01-28 02:46:53','2013-01-28 02:46:53'),(237,32,'en-MEEP','gooseberry','2013-01-28 02:46:53','2013-01-28 02:46:53'),(238,33,'en-MEEP','grape','2013-01-28 02:46:53','2013-01-28 02:46:53'),(239,34,'en-MEEP','green','2013-01-28 02:46:53','2013-01-28 02:46:53'),(240,35,'en-MEEP','grey','2013-01-28 02:46:53','2013-01-28 02:46:53'),(241,36,'en-MEEP','halloween','2013-01-28 02:46:53','2013-01-28 02:46:53'),(242,37,'en-MEEP','ice','2013-01-28 02:46:53','2013-01-28 02:46:53'),(243,38,'en-MEEP','invisible','2013-01-28 02:46:53','2013-01-28 02:46:53'),(244,39,'en-MEEP','island','2013-01-28 02:46:53','2013-01-28 02:46:53'),(245,40,'en-MEEP','jelly','2013-01-28 02:46:53','2013-01-28 02:46:53'),(246,41,'en-MEEP','lemon','2013-01-28 02:46:53','2013-01-28 02:46:53'),(247,42,'en-MEEP','lime','2013-01-28 02:46:53','2013-01-28 02:46:53'),(248,87,'en-MEEP','magma','2013-01-28 02:46:53','2013-01-28 02:46:53'),(249,43,'en-MEEP','mallow','2013-01-28 02:46:53','2013-01-28 02:46:53'),(250,91,'en-MEEP','maractite','2013-01-28 02:46:53','2013-01-28 02:46:53'),(251,44,'en-MEEP','maraquan','2013-01-28 02:46:53','2013-01-28 02:46:53'),(252,45,'en-MEEP','msp','2013-01-28 02:46:53','2013-01-28 02:46:53'),(253,46,'en-MEEP','mutant','2013-01-28 02:46:53','2013-01-28 02:46:53'),(254,86,'en-MEEP','onion','2013-01-28 02:46:53','2013-01-28 02:46:53'),(255,47,'en-MEEP','orange','2013-01-28 02:46:53','2013-01-28 02:46:53'),(256,48,'en-MEEP','pea','2013-01-28 02:46:53','2013-01-28 02:46:53'),(257,49,'en-MEEP','peach','2013-01-28 02:46:53','2013-01-28 02:46:53'),(258,50,'en-MEEP','pear','2013-01-28 02:46:53','2013-01-28 02:46:53'),(259,51,'en-MEEP','pepper','2013-01-28 02:46:53','2013-01-28 02:46:53'),(260,52,'en-MEEP','pineapple','2013-01-28 02:46:53','2013-01-28 02:46:53'),(261,53,'en-MEEP','pink','2013-01-28 02:46:53','2013-01-28 02:46:53'),(262,54,'en-MEEP','pirate','2013-01-28 02:46:53','2013-01-28 02:46:53'),(263,55,'en-MEEP','plum','2013-01-28 02:46:53','2013-01-28 02:46:53'),(264,56,'en-MEEP','plushie','2013-01-28 02:46:53','2013-01-28 02:46:53'),(265,57,'en-MEEP','purple','2013-01-28 02:46:53','2013-01-28 02:46:53'),(266,58,'en-MEEP','quigukiboy','2013-01-28 02:46:53','2013-01-28 02:46:53'),(267,59,'en-MEEP','quigukigirl','2013-01-28 02:46:53','2013-01-28 02:46:53'),(268,60,'en-MEEP','rainbow','2013-01-28 02:46:53','2013-01-28 02:46:53'),(269,61,'en-MEEP','red','2013-01-28 02:46:53','2013-01-28 02:46:53'),(270,88,'en-MEEP','relic','2013-01-28 02:46:53','2013-01-28 02:46:53'),(271,62,'en-MEEP','robot','2013-01-28 02:46:53','2013-01-28 02:46:53'),(272,63,'en-MEEP','royalboy','2013-01-28 02:46:53','2013-01-28 02:46:53'),(273,64,'en-MEEP','royalgirl','2013-01-28 02:46:53','2013-01-28 02:46:53'),(274,65,'en-MEEP','shadow','2013-01-28 02:46:53','2013-01-28 02:46:53'),(275,66,'en-MEEP','silver','2013-01-28 02:46:53','2013-01-28 02:46:53'),(276,67,'en-MEEP','sketch','2013-01-28 02:46:53','2013-01-28 02:46:53'),(277,68,'en-MEEP','skunk','2013-01-28 02:46:53','2013-01-28 02:46:53'),(278,69,'en-MEEP','snot','2013-01-28 02:46:53','2013-01-28 02:46:53'),(279,70,'en-MEEP','snow','2013-01-28 02:46:53','2013-01-28 02:46:53'),(280,71,'en-MEEP','speckled','2013-01-28 02:46:53','2013-01-28 02:46:53'),(281,72,'en-MEEP','split','2013-01-28 02:46:53','2013-01-28 02:46:53'),(282,73,'en-MEEP','sponge','2013-01-28 02:46:53','2013-01-28 02:46:53'),(283,74,'en-MEEP','spotted','2013-01-28 02:46:53','2013-01-28 02:46:53'),(284,75,'en-MEEP','starry','2013-01-28 02:46:53','2013-01-28 02:46:53'),(285,99,'en-MEEP','stealthy','2013-01-28 02:46:53','2013-01-28 02:46:53'),(286,76,'en-MEEP','strawberry','2013-01-28 02:46:53','2013-01-28 02:46:53'),(287,77,'en-MEEP','striped','2013-01-28 02:46:53','2013-01-28 02:46:53'),(288,93,'en-MEEP','swamp gas','2013-01-28 02:46:53','2013-01-28 02:46:53'),(289,78,'en-MEEP','thornberry','2013-01-28 02:46:53','2013-01-28 02:46:53'),(290,79,'en-MEEP','tomato','2013-01-28 02:46:53','2013-01-28 02:46:53'),(291,90,'en-MEEP','transparent','2013-01-28 02:46:53','2013-01-28 02:46:53'),(292,80,'en-MEEP','tyrannian','2013-01-28 02:46:53','2013-01-28 02:46:53'),(293,81,'en-MEEP','usuki boy','2013-01-28 02:46:53','2013-01-28 02:46:53'),(294,82,'en-MEEP','usuki girl','2013-01-28 02:46:53','2013-01-28 02:46:53'),(295,94,'en-MEEP','water','2013-01-28 02:46:53','2013-01-28 02:46:53'),(296,83,'en-MEEP','white','2013-01-28 02:46:53','2013-01-28 02:46:53'),(297,89,'en-MEEP','woodland','2013-01-28 02:46:53','2013-01-28 02:46:53'),(298,95,'en-MEEP','wraith','2013-01-28 02:46:53','2013-01-28 02:46:53'),(299,84,'en-MEEP','yellow','2013-01-28 02:46:53','2013-01-28 02:46:53'),(300,85,'en-MEEP','zombie','2013-01-28 02:46:53','2013-01-28 02:46:53'),(301,92,'es','8-bits','2013-02-12 05:33:20','2013-02-12 05:33:20'),(302,13,'es','a cuadros','2013-02-12 05:33:20','2013-02-12 05:33:20'),(303,94,'es','agua','2013-02-12 05:33:20','2013-02-12 05:33:20'),(304,5,'es','aguacate','2013-02-12 05:33:20','2013-02-12 05:33:20'),(305,28,'es','ajo','2013-02-12 05:33:20','2013-02-12 05:33:20'),(306,1,'es','alienígena','2013-02-12 05:33:20','2013-02-12 05:33:20'),(307,84,'es','amarillo','2013-02-12 05:33:20','2013-02-12 05:33:20'),(308,98,'es','anciana','2013-02-12 05:33:20','2013-02-12 05:33:20'),(309,97,'es','anciano','2013-02-12 05:33:20','2013-02-12 05:33:20'),(310,96,'es','anochecer','2013-02-12 05:33:20','2013-02-12 05:33:20'),(311,77,'es','a rayas','2013-02-12 05:33:20','2013-02-12 05:33:20'),(312,17,'es','arcilla ','2013-02-12 05:33:20','2013-02-12 05:33:20'),(313,60,'es','arco iris','2013-02-12 05:33:20','2013-02-12 05:33:20'),(314,8,'es','azul','2013-02-12 05:33:20','2013-02-12 05:33:20'),(315,25,'es','azul eléctrico','2013-02-12 05:33:20','2013-02-12 05:33:20'),(316,6,'es','bebé','2013-02-12 05:33:20','2013-02-12 05:33:20'),(317,4,'es','berenjena','2013-02-12 05:33:20','2013-02-12 05:33:20'),(318,83,'es','blanco','2013-02-12 05:33:21','2013-02-12 05:33:21'),(319,89,'es','bosque','2013-02-12 05:33:21','2013-02-12 05:33:21'),(320,11,'es','camuflaje','2013-02-12 05:33:21','2013-02-12 05:33:21'),(321,86,'es','cebolla','2013-02-12 05:33:21','2013-02-12 05:33:21'),(322,14,'es','chocolate','2013-02-12 05:33:21','2013-02-12 05:33:21'),(323,15,'es','chokato','2013-02-12 05:33:21','2013-02-12 05:33:21'),(324,55,'es','ciruela','2013-02-12 05:33:21','2013-02-12 05:33:21'),(325,19,'es','coco','2013-02-12 05:33:21','2013-02-12 05:33:21'),(326,71,'es','con manchas','2013-02-12 05:33:21','2013-02-12 05:33:21'),(327,74,'es','con motas','2013-02-12 05:33:21','2013-02-12 05:33:21'),(328,21,'es','darigan','2013-02-12 05:33:21','2013-02-12 05:33:21'),(329,22,'es','desierto','2013-02-12 05:33:21','2013-02-12 05:33:21'),(330,67,'es','dibujo','2013-02-12 05:33:21','2013-02-12 05:33:21'),(331,100,'es','dimensional','2013-02-12 05:33:21','2013-02-12 05:33:21'),(332,23,'es','disco','2013-02-12 05:33:21','2013-02-12 05:33:21'),(333,72,'es','dividido','2013-02-12 05:33:21','2013-02-12 05:33:21'),(334,31,'es','dorado','2013-02-12 05:33:21','2013-02-12 05:33:21'),(335,24,'es','durian','2013-02-12 05:33:21','2013-02-12 05:33:21'),(336,3,'es','espárrago','2013-02-12 05:33:21','2013-02-12 05:33:21'),(337,95,'es','espíritu','2013-02-12 05:33:21','2013-02-12 05:33:21'),(338,78,'es','espinabaya','2013-02-12 05:33:21','2013-02-12 05:33:21'),(339,73,'es','esponja','2013-02-12 05:33:21','2013-02-12 05:33:21'),(340,75,'es','estrellado','2013-02-12 05:33:21','2013-02-12 05:33:21'),(341,29,'es','fantasma','2013-02-12 05:33:21','2013-02-12 05:33:21'),(342,20,'es','flan','2013-02-12 05:33:21','2013-02-12 05:33:21'),(343,30,'es','fluorescente','2013-02-12 05:33:21','2013-02-12 05:33:21'),(344,76,'es','fresa','2013-02-12 05:33:21','2013-02-12 05:33:21'),(345,27,'es','fuego','2013-02-12 05:33:21','2013-02-12 05:33:21'),(346,7,'es','galleta','2013-02-12 05:33:21','2013-02-12 05:33:21'),(347,32,'es','gansobaya','2013-02-12 05:33:21','2013-02-12 05:33:21'),(348,40,'es','gelatina','2013-02-12 05:33:21','2013-02-12 05:33:21'),(349,35,'es','gris','2013-02-12 05:33:21','2013-02-12 05:33:21'),(350,48,'es','guisante','2013-02-12 05:33:21','2013-02-12 05:33:21'),(351,26,'es','hada','2013-02-12 05:33:21','2013-02-12 05:33:21'),(352,36,'es','halloween','2013-02-12 05:33:21','2013-02-12 05:33:21'),(353,37,'es','hielo','2013-02-12 05:33:21','2013-02-12 05:33:21'),(354,38,'es','invisible','2013-02-12 05:33:21','2013-02-12 05:33:21'),(355,39,'es','isla','2013-02-12 05:33:21','2013-02-12 05:33:21'),(356,41,'es','limón','2013-02-12 05:33:21','2013-02-12 05:33:21'),(357,42,'es','limón','2013-02-12 05:33:21','2013-02-12 05:33:21'),(358,87,'es','magma','2013-02-12 05:33:21','2013-02-12 05:33:21'),(359,2,'es','manzana','2013-02-12 05:33:21','2013-02-12 05:33:21'),(360,91,'es','maractita','2013-02-12 05:33:21','2013-02-12 05:33:21'),(361,44,'es','maracuático','2013-02-12 05:33:21','2013-02-12 05:33:21'),(362,10,'es','marrón','2013-02-12 05:33:21','2013-02-12 05:33:21'),(363,43,'es','marshmallow','2013-02-12 05:33:21','2013-02-12 05:33:21'),(364,49,'es','melocotón','2013-02-12 05:33:21','2013-02-12 05:33:21'),(365,93,'es','metano','2013-02-12 05:33:21','2013-02-12 05:33:21'),(366,69,'es','moco','2013-02-12 05:33:21','2013-02-12 05:33:21'),(367,68,'es','mofeta','2013-02-12 05:33:21','2013-02-12 05:33:21'),(368,9,'es','mora azul','2013-02-12 05:33:21','2013-02-12 05:33:21'),(369,57,'es','morado','2013-02-12 05:33:21','2013-02-12 05:33:21'),(370,45,'es','msp','2013-02-12 05:33:21','2013-02-12 05:33:21'),(371,46,'es','mutante','2013-02-12 05:33:21','2013-02-12 05:33:21'),(372,47,'es','naranja','2013-02-12 05:33:21','2013-02-12 05:33:21'),(373,16,'es','navidad','2013-02-12 05:33:21','2013-02-12 05:33:21'),(374,64,'es','niña de la realeza','2013-02-12 05:33:21','2013-02-12 05:33:21'),(375,63,'es','niño de la realeza','2013-02-12 05:33:22','2013-02-12 05:33:22'),(376,70,'es','nieve','2013-02-12 05:33:22','2013-02-12 05:33:22'),(377,18,'es','nube','2013-02-12 05:33:22','2013-02-12 05:33:22'),(378,56,'es','peluche','2013-02-12 05:33:22','2013-02-12 05:33:22'),(379,50,'es','pera','2013-02-12 05:33:22','2013-02-12 05:33:22'),(380,52,'es','piña','2013-02-12 05:33:22','2013-02-12 05:33:22'),(381,51,'es','pimiento','2013-02-12 05:33:22','2013-02-12 05:33:22'),(382,54,'es','pirata','2013-02-12 05:33:22','2013-02-12 05:33:22'),(383,66,'es','plateado','2013-02-12 05:33:22','2013-02-12 05:33:22'),(384,59,'es','quiguki niña','2013-02-12 05:33:22','2013-02-12 05:33:22'),(385,58,'es','quiguki niño','2013-02-12 05:33:22','2013-02-12 05:33:22'),(386,88,'es','reliquia','2013-02-12 05:33:22','2013-02-12 05:33:22'),(387,62,'es','robot','2013-02-12 05:33:22','2013-02-12 05:33:22'),(388,61,'es','rojo','2013-02-12 05:33:22','2013-02-12 05:33:22'),(389,53,'es','rosa','2013-02-12 05:33:22','2013-02-12 05:33:22'),(390,99,'es','sigiloso','2013-02-12 05:33:22','2013-02-12 05:33:22'),(391,65,'es','sombra','2013-02-12 05:33:22','2013-02-12 05:33:22'),(392,79,'es','tomate','2013-02-12 05:33:22','2013-02-12 05:33:22'),(393,90,'es','transparente','2013-02-12 05:33:22','2013-02-12 05:33:22'),(394,80,'es','tyranniano','2013-02-12 05:33:22','2013-02-12 05:33:22'),(395,82,'es','usuki niña','2013-02-12 05:33:22','2013-02-12 05:33:22'),(396,81,'es','usuki niño','2013-02-12 05:33:22','2013-02-12 05:33:22'),(397,33,'es','uva','2013-02-12 05:33:22','2013-02-12 05:33:22'),(398,34,'es','verde','2013-02-12 05:33:22','2013-02-12 05:33:22'),(399,12,'es','zanahoria ','2013-02-12 05:33:22','2013-02-12 05:33:22'),(400,85,'es','zombi','2013-02-12 05:33:22','2013-02-12 05:33:22'),(401,101,'en','agueena','2013-02-16 06:01:08','2013-12-21 01:15:19'),(402,101,'pt','Agueena','2013-02-16 06:03:06','2013-02-16 06:03:06'),(403,101,'es','Agüina','2013-02-16 06:03:06','2013-02-16 06:03:06'),(404,102,'en','pastel','2013-12-21 01:19:37','2014-11-15 01:52:51'),(405,-1,'en','nebula','2014-04-01 03:18:20','2014-04-01 03:18:20'),(406,103,'en','ummagine','2014-10-03 21:54:58','2014-11-15 01:54:18'),(407,-1,'pt','nebula','2014-11-15 01:50:34','2014-11-15 01:50:34'),(408,-1,'es','nebula','2014-11-15 01:50:34','2014-11-15 01:50:34'),(409,-1,'en-MEEP','nebula','2014-11-15 01:50:34','2014-11-15 01:50:34'),(410,101,'en-MEEP','agueena','2014-11-15 01:51:08','2014-11-15 01:51:08'),(411,102,'en-MEEP','pastel','2014-11-15 01:52:51','2014-11-15 01:52:51'),(412,102,'pt','pastel','2014-11-15 01:52:51','2014-11-15 01:52:51'),(413,102,'es','pastel','2014-11-15 01:52:51','2014-11-15 01:52:51'),(414,103,'es','rarajena','2014-11-15 01:54:18','2014-11-15 01:54:18'),(415,103,'pt','ummagina','2014-11-15 01:54:18','2014-11-15 01:54:18'),(416,103,'en-MEEP','ummagine','2014-11-15 01:54:18','2014-11-15 01:54:18'),(417,104,'en','Polka Dot','2015-02-12 02:29:50','2015-02-12 02:29:50'),(418,104,'en-MEEP','Polka Dot','2015-02-12 02:31:47','2015-02-12 02:31:47'),(419,104,'pt','Polka Dot','2015-02-12 02:31:47','2015-02-12 02:31:47'),(420,104,'es','Polka Dot','2015-02-12 02:31:47','2015-02-12 02:31:47'),(421,105,'en','Candy','2017-02-23 19:19:07','2017-02-23 19:19:07'),(422,106,'en','marble','2017-02-23 19:29:00','2017-02-23 19:29:00'),(423,107,'en','Steampunk','2018-01-27 23:54:10','2018-01-27 23:54:10'),(424,108,'en','Toy','2018-02-14 22:40:32','2018-02-14 22:40:32'),(425,109,'en','Origami','2018-03-07 04:53:11','2018-03-07 04:53:11'),(426,110,'en','Oil Paint','2019-03-07 03:15:31','2019-03-07 03:20:36'),(427,111,'en','Mosaic','2019-04-29 18:04:39','2019-04-29 18:04:39'),(428,112,'en','Burlap','2019-04-29 18:06:47','2019-04-29 18:06:47'); /*!40000 ALTER TABLE `color_translations` ENABLE KEYS */; UNLOCK TABLES; + +-- +-- Table structure for table `zones` +-- + +DROP TABLE IF EXISTS `zones`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `zones` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `depth` int(11) DEFAULT NULL, + `type_id` int(11) DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=53 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `zones` +-- + +LOCK TABLES `zones` WRITE; +/*!40000 ALTER TABLE `zones` DISABLE KEYS */; +INSERT INTO `zones` VALUES (1,1,4),(2,2,4),(3,3,3),(4,6,1),(5,7,1),(6,8,2),(7,9,1),(8,10,2),(9,11,1),(10,12,1),(11,13,2),(12,14,1),(13,15,2),(14,17,1),(15,18,1),(16,19,2),(17,20,1),(18,21,1),(19,22,2),(20,23,1),(21,24,2),(22,25,1),(23,26,2),(24,28,2),(25,29,2),(26,30,2),(27,31,2),(28,32,1),(29,33,1),(30,34,1),(31,35,2),(32,36,1),(33,37,1),(34,38,1),(35,41,2),(36,39,2),(37,40,1),(38,42,1),(39,43,1),(40,44,2),(41,45,2),(42,46,2),(43,47,2),(44,49,3),(45,50,3),(46,48,2),(47,51,3),(48,4,3),(49,5,2),(50,16,2),(51,27,2),(52,52,3); +/*!40000 ALTER TABLE `zones` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `zone_translations` +-- + +DROP TABLE IF EXISTS `zone_translations`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `zone_translations` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `zone_id` int(11) DEFAULT NULL, + `locale` varchar(255) DEFAULT NULL, + `label` varchar(255) DEFAULT NULL, + `plain_label` varchar(255) DEFAULT NULL, + `created_at` datetime DEFAULT NULL, + `updated_at` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `index_zone_translations_on_zone_id` (`zone_id`), + KEY `index_zone_translations_on_locale` (`locale`) +) ENGINE=InnoDB AUTO_INCREMENT=209 DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `zone_translations` +-- + +LOCK TABLES `zone_translations` WRITE; +/*!40000 ALTER TABLE `zone_translations` DISABLE KEYS */; +INSERT INTO `zone_translations` VALUES (1,1,'en','Music','music','2013-01-27 06:10:45','2013-01-27 06:10:45'),(2,2,'en','Sound Effects','soundeffects','2013-01-27 06:10:45','2013-01-27 06:10:45'),(3,3,'en','Background','background','2013-01-27 06:10:45','2013-01-27 06:10:45'),(4,4,'en','Biology Effects','biologyeffects','2013-01-27 06:10:45','2013-01-27 06:10:45'),(5,5,'en','Hind Biology','hindbiology','2013-01-27 06:10:45','2013-01-27 06:10:45'),(6,6,'en','Markings','markings','2013-01-27 06:10:45','2013-01-27 06:10:45'),(7,7,'en','Hind Disease','hinddisease','2013-01-27 06:10:45','2013-01-27 06:10:45'),(8,8,'en','Hind Cover','hindcover','2013-01-27 06:10:45','2013-01-27 06:10:45'),(9,9,'en','Hind Transient Biology','hindtransientbiology','2013-01-27 06:10:45','2013-01-27 06:10:45'),(10,10,'en','Hind Drippings','hinddrippings','2013-01-27 06:10:45','2013-01-27 06:10:45'),(11,11,'en','Backpack','backpack','2013-01-27 06:10:45','2013-01-27 06:10:45'),(12,12,'en','Wings Transient Biology','wingstransientbiology','2013-01-27 06:10:45','2013-01-27 06:10:45'),(13,13,'en','Wings','wings','2013-01-27 06:10:45','2013-01-27 06:10:45'),(14,14,'en','Hair Back','hairback','2013-01-27 06:10:45','2013-01-27 06:10:45'),(15,15,'en','Body','body','2013-01-27 06:10:45','2013-01-27 06:10:45'),(16,16,'en','Markings','markings','2013-01-27 06:10:45','2013-01-27 06:10:45'),(17,17,'en','Body Disease','bodydisease','2013-01-27 06:10:45','2013-01-27 06:10:45'),(18,18,'en','Feet Transient Biology','feettransientbiology','2013-01-27 06:10:45','2013-01-27 06:10:45'),(19,19,'en','Shoes','shoes','2013-01-27 06:10:45','2013-01-27 06:10:45'),(20,20,'en','Lower-body Transient Biology','lowerbodytransientbiology','2013-01-27 06:10:45','2013-01-27 06:10:45'),(21,21,'en','Trousers','trousers','2013-01-27 06:10:45','2013-01-27 06:10:45'),(22,22,'en','Upper-body Transient Biology','upperbodytransientbiology','2013-01-27 06:10:45','2013-01-27 06:10:45'),(23,23,'en','Shirt/Dress','shirtdress','2013-01-27 06:10:45','2013-01-27 06:10:45'),(24,24,'en','Necklace','necklace','2013-01-27 06:10:45','2013-01-27 06:10:45'),(25,25,'en','Gloves','gloves','2013-01-27 06:10:45','2013-01-27 06:10:45'),(26,26,'en','Jacket','jacket','2013-01-27 06:10:45','2013-01-27 06:10:45'),(27,27,'en','Collar','collar','2013-01-27 06:10:45','2013-01-27 06:10:45'),(28,28,'en','Body Drippings','bodydrippings','2013-01-27 06:10:45','2013-01-27 06:10:45'),(29,29,'en','Ruff','ruff','2013-01-27 06:10:45','2013-01-27 06:10:45'),(30,30,'en','Head','head','2013-01-27 06:10:45','2013-01-27 06:10:45'),(31,31,'en','Markings','markings','2013-01-27 06:10:45','2013-01-27 06:10:45'),(32,32,'en','Head Disease','headdisease','2013-01-27 06:10:45','2013-01-27 06:10:45'),(33,33,'en','Eyes','eyes','2013-01-27 06:10:45','2013-01-27 06:10:45'),(34,34,'en','Mouth','mouth','2013-01-27 06:10:45','2013-01-27 06:10:45'),(35,35,'en','Glasses','glasses','2013-01-27 06:10:45','2013-01-27 06:10:45'),(36,36,'en','Earrings','earrings','2013-01-27 06:10:45','2013-01-27 06:10:45'),(37,37,'en','Hair Front','hairfront','2013-01-27 06:10:45','2013-01-27 06:10:45'),(38,38,'en','Head Transient Biology','headtransientbiology','2013-01-27 06:10:45','2013-01-27 06:10:45'),(39,39,'en','Head Drippings','headdrippings','2013-01-27 06:10:45','2013-01-27 06:10:45'),(40,40,'en','Hat','hat','2013-01-27 06:10:45','2013-01-27 06:10:45'),(41,41,'en','Earrings','earrings','2013-01-27 06:10:45','2013-01-27 06:10:45'),(42,42,'en','Right-hand Item','righthanditem','2013-01-27 06:10:45','2014-04-06 04:25:35'),(43,43,'en','Left-hand Item','lefthanditem','2013-01-27 06:10:45','2014-04-06 04:25:35'),(44,44,'en','Higher Foreground Item','higherforegrounditem','2013-01-27 06:10:45','2014-04-06 04:25:35'),(45,45,'en','Lower Foreground Item','lowerforegrounditem','2013-01-27 06:10:45','2014-04-06 04:25:35'),(46,46,'en','Static','static','2013-01-27 06:10:46','2013-01-27 06:10:46'),(47,47,'en','Thought Bubble','thoughtbubble','2013-01-27 06:10:46','2013-01-27 06:10:46'),(48,48,'en','Background Item','backgrounditem','2013-01-27 06:10:46','2014-04-06 04:25:35'),(49,49,'en','Right-hand Item','righthanditem','2013-01-27 06:10:46','2014-04-06 04:25:35'),(50,50,'en','Hat','hat','2013-01-27 06:10:46','2013-01-27 06:10:46'),(51,51,'en','Belt','belt','2013-01-27 06:10:46','2013-01-27 06:10:46'),(52,52,'en','Foreground','foreground','2013-01-27 06:10:46','2013-01-27 06:10:46'),(53,52,'pt','Frente','frente','2013-01-27 06:11:02','2013-01-27 06:11:02'),(54,47,'pt','Balão de Pensamento','balaodepensamento','2013-01-27 06:11:02','2013-01-27 06:11:02'),(55,45,'pt','Artigo Inferior Frontal','artigoinferiorfrontal','2013-01-27 06:11:02','2013-01-27 06:11:02'),(56,44,'pt','Artigo Superior Frontal','artigosuperiorfrontal','2013-01-27 06:11:02','2013-01-27 06:11:02'),(57,46,'pt','Static','static','2013-01-27 06:11:02','2013-01-27 06:11:02'),(58,43,'pt','Mão Esquerda','maoesquerda','2013-01-27 06:11:02','2013-01-27 06:11:02'),(59,42,'pt','Mão Direita','maodireita','2013-01-27 06:11:02','2013-01-27 06:11:02'),(60,41,'pt','Brincos','brincos','2013-01-27 06:11:02','2013-01-27 06:11:02'),(61,40,'pt','Chapéus','chapeus','2013-01-27 06:11:02','2013-01-27 06:11:02'),(62,39,'pt','Head Drippings','headdrippings','2013-01-27 06:11:02','2013-01-27 06:11:02'),(63,38,'pt','Head Transient Biology','headtransientbiology','2013-01-27 06:11:02','2013-01-27 06:11:02'),(64,35,'pt','Olhos','olhos','2013-01-27 06:11:02','2013-01-27 06:11:02'),(65,37,'pt','Pêlo Frontal','pelofrontal','2013-01-27 06:11:02','2013-01-27 06:11:02'),(66,36,'pt','Brincos','brincos','2013-01-27 06:11:02','2013-01-27 06:11:02'),(67,34,'pt','Mouth','mouth','2013-01-27 06:11:02','2013-01-27 06:11:02'),(68,33,'pt','Eyes','eyes','2013-01-27 06:11:02','2013-01-27 06:11:02'),(69,32,'pt','Head Disease','headdisease','2013-01-27 06:11:02','2013-01-27 06:11:02'),(70,31,'pt','Marcas','marcas','2013-01-27 06:11:02','2013-01-27 06:11:02'),(71,30,'pt','Head','head','2013-01-27 06:11:02','2013-01-27 06:11:02'),(72,29,'pt','Ruff','ruff','2013-01-27 06:11:02','2013-01-27 06:11:02'),(73,28,'pt','Body Drippings','bodydrippings','2013-01-27 06:11:02','2013-01-27 06:11:02'),(74,27,'pt','Pescoço','pescoco','2013-01-27 06:11:02','2013-01-27 06:11:02'),(75,26,'pt','Casacos','casacos','2013-01-27 06:11:02','2013-01-27 06:11:02'),(76,25,'pt','Luvas','luvas','2013-01-27 06:11:02','2013-01-27 06:11:02'),(77,24,'pt','Colares','colares','2013-01-27 06:11:02','2013-01-27 06:11:02'),(78,51,'pt','Cinto','cinto','2013-01-27 06:11:02','2013-01-27 06:11:02'),(79,23,'pt','Camisa/Vestido','camisavestido','2013-01-27 06:11:02','2013-01-27 06:11:02'),(80,22,'pt','Upper-body Transient Biology','upperbodytransientbiology','2013-01-27 06:11:02','2013-01-27 06:11:02'),(81,21,'pt','Calças','calcas','2013-01-27 06:11:02','2013-01-27 06:11:02'),(82,20,'pt','Lower-body Transient Biology','lowerbodytransientbiology','2013-01-27 06:11:02','2013-01-27 06:11:02'),(83,19,'pt','Sapatos','sapatos','2013-01-27 06:11:02','2013-01-27 06:11:02'),(84,18,'pt','Feet Transient Biology','feettransientbiology','2013-01-27 06:11:02','2013-01-27 06:11:02'),(85,17,'pt','Body Disease','bodydisease','2013-01-27 06:11:02','2013-01-27 06:11:02'),(86,16,'pt','Marcas','marcas','2013-01-27 06:11:02','2013-01-27 06:11:02'),(87,15,'pt','Body','body','2013-01-27 06:11:02','2013-01-27 06:11:02'),(88,14,'pt','Pêlo Traseiro','pelotraseiro','2013-01-27 06:11:02','2013-01-27 06:11:02'),(89,50,'pt','Chapéus','chapeus','2013-01-27 06:11:02','2013-01-27 06:11:02'),(90,13,'pt','Asas','asas','2013-01-27 06:11:02','2013-01-27 06:11:02'),(91,12,'pt','Wings Transient Biology','wingstransientbiology','2013-01-27 06:11:02','2013-01-27 06:11:02'),(92,11,'pt','Mochilas','mochilas','2013-01-27 06:11:02','2013-01-27 06:11:02'),(93,10,'pt','Hind Drippings','hinddrippings','2013-01-27 06:11:02','2013-01-27 06:11:02'),(94,9,'pt','Hind Transient Biology','hindtransientbiology','2013-01-27 06:11:02','2013-01-27 06:11:02'),(95,8,'pt','Adornos Traseiros','adornostraseiros','2013-01-27 06:11:02','2013-01-27 06:11:02'),(96,7,'pt','Hind Disease','hinddisease','2013-01-27 06:11:02','2013-01-27 06:11:02'),(97,6,'pt','Marcas','marcas','2013-01-27 06:11:02','2013-01-27 06:11:02'),(98,5,'pt','Hind Biology','hindbiology','2013-01-27 06:11:02','2013-01-27 06:11:02'),(99,4,'pt','Biology Effects','biologyeffects','2013-01-27 06:11:02','2013-01-27 06:11:02'),(100,49,'pt','Mão Direita','maodireita','2013-01-27 06:11:02','2013-01-27 06:11:02'),(101,48,'pt','Artigos de Fundo','artigosdefundo','2013-01-27 06:11:02','2013-01-27 06:11:02'),(102,3,'pt','Fundos de Imagem','fundosdeimagem','2013-01-27 06:11:02','2013-01-27 06:11:02'),(103,2,'pt','Efeitos Sonoros','efeitossonoros','2013-01-27 06:11:02','2013-01-27 06:11:02'),(104,1,'pt','Músicas','musicas','2013-01-27 06:11:02','2013-01-27 06:11:02'),(105,52,'en-MEEP','Foreground','foreground','2013-01-28 02:46:27','2013-01-28 02:46:27'),(106,47,'en-MEEP','Thought Bubble','thoughtbubble','2013-01-28 02:46:27','2013-01-28 02:46:27'),(107,45,'en-MEEP','Lower Foreground Item','lowerforegrounditem','2013-01-28 02:46:27','2013-01-28 02:46:27'),(108,44,'en-MEEP','Higher Foreground Item','higherforegrounditem','2013-01-28 02:46:27','2013-01-28 02:46:27'),(109,46,'en-MEEP','Static','static','2013-01-28 02:46:27','2013-01-28 02:46:27'),(110,43,'en-MEEP','Left-hand Item','lefthanditem','2013-01-28 02:46:27','2013-01-28 02:46:27'),(111,42,'en-MEEP','Right-hand Item','righthanditem','2013-01-28 02:46:27','2013-01-28 02:46:27'),(112,41,'en-MEEP','Earrings','earrings','2013-01-28 02:46:27','2013-01-28 02:46:27'),(113,40,'en-MEEP','Hat','hat','2013-01-28 02:46:27','2013-01-28 02:46:27'),(114,39,'en-MEEP','Head Drippings','headdrippings','2013-01-28 02:46:27','2013-01-28 02:46:27'),(115,38,'en-MEEP','Head Transient Biology','headtransientbiology','2013-01-28 02:46:27','2013-01-28 02:46:27'),(116,35,'en-MEEP','Glasses','glasses','2013-01-28 02:46:27','2013-01-28 02:46:27'),(117,37,'en-MEEP','Hair Front','hairfront','2013-01-28 02:46:27','2013-01-28 02:46:27'),(118,36,'en-MEEP','Earrings','earrings','2013-01-28 02:46:27','2013-01-28 02:46:27'),(119,34,'en-MEEP','Mouth','mouth','2013-01-28 02:46:27','2013-01-28 02:46:27'),(120,33,'en-MEEP','Eyes','eyes','2013-01-28 02:46:27','2013-01-28 02:46:27'),(121,32,'en-MEEP','Head Disease','headdisease','2013-01-28 02:46:27','2013-01-28 02:46:27'),(122,31,'en-MEEP','Markings','markings','2013-01-28 02:46:27','2013-01-28 02:46:27'),(123,30,'en-MEEP','Head','head','2013-01-28 02:46:27','2013-01-28 02:46:27'),(124,29,'en-MEEP','Ruff','ruff','2013-01-28 02:46:27','2013-01-28 02:46:27'),(125,28,'en-MEEP','Body Drippings','bodydrippings','2013-01-28 02:46:27','2013-01-28 02:46:27'),(126,27,'en-MEEP','Collar','collar','2013-01-28 02:46:27','2013-01-28 02:46:27'),(127,26,'en-MEEP','Jacket','jacket','2013-01-28 02:46:27','2013-01-28 02:46:27'),(128,25,'en-MEEP','Gloves','gloves','2013-01-28 02:46:27','2013-01-28 02:46:27'),(129,24,'en-MEEP','Necklace','necklace','2013-01-28 02:46:27','2013-01-28 02:46:27'),(130,51,'en-MEEP','Belt','belt','2013-01-28 02:46:27','2013-01-28 02:46:27'),(131,23,'en-MEEP','Shirt/Dress','shirtdress','2013-01-28 02:46:27','2013-01-28 02:46:27'),(132,22,'en-MEEP','Upper-body Transient Biology','upperbodytransientbiology','2013-01-28 02:46:27','2013-01-28 02:46:27'),(133,21,'en-MEEP','Trousers','trousers','2013-01-28 02:46:27','2013-01-28 02:46:27'),(134,20,'en-MEEP','Lower-body Transient Biology','lowerbodytransientbiology','2013-01-28 02:46:27','2013-01-28 02:46:27'),(135,19,'en-MEEP','Shoes','shoes','2013-01-28 02:46:27','2013-01-28 02:46:27'),(136,18,'en-MEEP','Feet Transient Biology','feettransientbiology','2013-01-28 02:46:28','2013-01-28 02:46:28'),(137,17,'en-MEEP','Body Disease','bodydisease','2013-01-28 02:46:28','2013-01-28 02:46:28'),(138,16,'en-MEEP','Markings','markings','2013-01-28 02:46:28','2013-01-28 02:46:28'),(139,15,'en-MEEP','Body','body','2013-01-28 02:46:28','2013-01-28 02:46:28'),(140,14,'en-MEEP','Hair Back','hairback','2013-01-28 02:46:28','2013-01-28 02:46:28'),(141,50,'en-MEEP','Hat','hat','2013-01-28 02:46:28','2013-01-28 02:46:28'),(142,13,'en-MEEP','Wings','wings','2013-01-28 02:46:28','2013-01-28 02:46:28'),(143,12,'en-MEEP','Wings Transient Biology','wingstransientbiology','2013-01-28 02:46:28','2013-01-28 02:46:28'),(144,11,'en-MEEP','Backpack','backpack','2013-01-28 02:46:28','2013-01-28 02:46:28'),(145,10,'en-MEEP','Hind Drippings','hinddrippings','2013-01-28 02:46:28','2013-01-28 02:46:28'),(146,9,'en-MEEP','Hind Transient Biology','hindtransientbiology','2013-01-28 02:46:28','2013-01-28 02:46:28'),(147,8,'en-MEEP','Hind Cover','hindcover','2013-01-28 02:46:28','2013-01-28 02:46:28'),(148,7,'en-MEEP','Hind Disease','hinddisease','2013-01-28 02:46:28','2013-01-28 02:46:28'),(149,6,'en-MEEP','Markings','markings','2013-01-28 02:46:28','2013-01-28 02:46:28'),(150,5,'en-MEEP','Hind Biology','hindbiology','2013-01-28 02:46:28','2013-01-28 02:46:28'),(151,4,'en-MEEP','Biology Effects','biologyeffects','2013-01-28 02:46:28','2013-01-28 02:46:28'),(152,49,'en-MEEP','Right-hand Item','righthanditem','2013-01-28 02:46:28','2013-01-28 02:46:28'),(153,48,'en-MEEP','Background Item','backgrounditem','2013-01-28 02:46:28','2013-01-28 02:46:28'),(154,3,'en-MEEP','Background','background','2013-01-28 02:46:28','2013-01-28 02:46:28'),(155,2,'en-MEEP','Sound Effects','soundeffects','2013-01-28 02:46:28','2013-01-28 02:46:28'),(156,1,'en-MEEP','Music','music','2013-01-28 02:46:28','2013-01-28 02:46:28'),(157,52,'es','Primer plano ','primerplano','2013-02-12 05:32:05','2013-02-12 05:32:05'),(158,47,'es','Burbujas de ideas','burbujasdeideas','2013-02-12 05:32:05','2013-02-12 05:32:05'),(159,45,'es','Objetos de primer plano bajo','objetosdeprimerplanobajo','2013-02-12 05:32:05','2013-02-12 05:32:05'),(160,44,'es','Objetos en primer plano alto ','objetosenprimerplanoalto','2013-02-12 05:32:05','2013-02-12 05:32:05'),(161,46,'es','Static','static','2013-02-12 05:32:05','2013-02-12 05:32:05'),(162,43,'es','Objetos para la mano izquierda','objetosparalamanoizquierda','2013-02-12 05:32:05','2013-02-12 05:32:05'),(163,42,'es','Objeto para la mano derecha','objetoparalamanoderecha','2013-02-12 05:32:05','2013-02-12 05:32:05'),(164,41,'es','Aretes','aretes','2013-02-12 05:32:05','2013-02-12 05:32:05'),(165,40,'es','Sombrero','sombrero','2013-02-12 05:32:05','2013-02-12 05:32:05'),(166,39,'es','Head Drippings','headdrippings','2013-02-12 05:32:05','2013-02-12 05:32:05'),(167,38,'es','Head Transient Biology','headtransientbiology','2013-02-12 05:32:05','2013-02-12 05:32:05'),(168,35,'es','Lentes','lentes','2013-02-12 05:32:05','2013-02-12 05:32:05'),(169,37,'es','Pelo de enfrente','pelodeenfrente','2013-02-12 05:32:05','2013-02-12 05:32:05'),(170,36,'es','Aretes','aretes','2013-02-12 05:32:05','2013-02-12 05:32:05'),(171,34,'es','Mouth','mouth','2013-02-12 05:32:05','2013-02-12 05:32:05'),(172,33,'es','Eyes','eyes','2013-02-12 05:32:05','2013-02-12 05:32:05'),(173,32,'es','Head Disease','headdisease','2013-02-12 05:32:05','2013-02-12 05:32:05'),(174,31,'es','Marcas','marcas','2013-02-12 05:32:05','2013-02-12 05:32:05'),(175,30,'es','Head','head','2013-02-12 05:32:05','2013-02-12 05:32:05'),(176,29,'es','Ruff','ruff','2013-02-12 05:32:05','2013-02-12 05:32:05'),(177,28,'es','Body Drippings','bodydrippings','2013-02-12 05:32:05','2013-02-12 05:32:05'),(178,27,'es','Cuello','cuello','2013-02-12 05:32:05','2013-02-12 05:32:05'),(179,26,'es','Saco','saco','2013-02-12 05:32:05','2013-02-12 05:32:05'),(180,25,'es','Guantes','guantes','2013-02-12 05:32:05','2013-02-12 05:32:05'),(181,24,'es','Collar','collar','2013-02-12 05:32:05','2013-02-12 05:32:05'),(182,51,'es','Cinturones','cinturones','2013-02-12 05:32:05','2013-02-12 05:32:05'),(183,23,'es','Camisetas/Vestidos','camisetasvestidos','2013-02-12 05:32:05','2013-02-12 05:32:05'),(184,22,'es','Upper-body Transient Biology','upperbodytransientbiology','2013-02-12 05:32:05','2013-02-12 05:32:05'),(185,21,'es','Pantalones/Faldas','pantalonesfaldas','2013-02-12 05:32:05','2013-02-12 05:32:05'),(186,20,'es','Lower-body Transient Biology','lowerbodytransientbiology','2013-02-12 05:32:06','2013-02-12 05:32:06'),(187,19,'es','Zapatos','zapatos','2013-02-12 05:32:06','2013-02-12 05:32:06'),(188,18,'es','Feet Transient Biology','feettransientbiology','2013-02-12 05:32:06','2013-02-12 05:32:06'),(189,17,'es','Body Disease','bodydisease','2013-02-12 05:32:06','2013-02-12 05:32:06'),(190,16,'es','Marcas','marcas','2013-02-12 05:32:06','2013-02-12 05:32:06'),(191,15,'es','Body','body','2013-02-12 05:32:06','2013-02-12 05:32:06'),(192,14,'es','Pelo de atrás','pelodeatras','2013-02-12 05:32:06','2013-02-12 05:32:06'),(193,50,'es','Sombrero','sombrero','2013-02-12 05:32:06','2013-02-12 05:32:06'),(194,13,'es','Alas','alas','2013-02-12 05:32:06','2013-02-12 05:32:06'),(195,12,'es','Wings Transient Biology','wingstransientbiology','2013-02-12 05:32:06','2013-02-12 05:32:06'),(196,11,'es','Mochila','mochila','2013-02-12 05:32:06','2013-02-12 05:32:06'),(197,10,'es','Hind Drippings','hinddrippings','2013-02-12 05:32:06','2013-02-12 05:32:06'),(198,9,'es','Hind Transient Biology','hindtransientbiology','2013-02-12 05:32:06','2013-02-12 05:32:06'),(199,8,'es','Cubierta trasera','cubiertatrasera','2013-02-12 05:32:06','2013-02-12 05:32:06'),(200,7,'es','Hind Disease','hinddisease','2013-02-12 05:32:06','2013-02-12 05:32:06'),(201,6,'es','Marcas','marcas','2013-02-12 05:32:06','2013-02-12 05:32:06'),(202,5,'es','Hind Biology','hindbiology','2013-02-12 05:32:06','2013-02-12 05:32:06'),(203,4,'es','Biology Effects','biologyeffects','2013-02-12 05:32:06','2013-02-12 05:32:06'),(204,49,'es','Objeto para la mano derecha','objetoparalamanoderecha','2013-02-12 05:32:06','2013-02-12 05:32:06'),(205,48,'es','Objetos de fondo','objetosdefondo','2013-02-12 05:32:06','2013-02-12 05:32:06'),(206,3,'es','Fondo','fondo','2013-02-12 05:32:06','2013-02-12 05:32:06'),(207,2,'es','Efectos de sonido','efectosdesonido','2013-02-12 05:32:06','2013-02-12 05:32:06'),(208,1,'es','Música','musica','2013-02-12 05:32:06','2013-02-12 05:32:06'); +/*!40000 ALTER TABLE `zone_translations` ENABLE KEYS */; +UNLOCK TABLES; /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; @@ -133,4 +189,4 @@ UNLOCK TABLES; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2020-09-19 4:19:07 +-- Dump completed on 2020-10-06 6:09:54 diff --git a/scripts/setup-mysql-dev-schema.sql b/scripts/setup-mysql-dev-schema.sql index 2623c24..85a5608 100644 --- a/scripts/setup-mysql-dev-schema.sql +++ b/scripts/setup-mysql-dev-schema.sql @@ -167,4 +167,4 @@ CREATE TABLE `swf_assets` ( /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2020-09-19 4:19:01 +-- Dump completed on 2020-10-06 6:09:45 diff --git a/src/server/db.js b/src/server/db.js index f06b9d9..5acdac0 100644 --- a/src/server/db.js +++ b/src/server/db.js @@ -2,11 +2,33 @@ const mysql = require("mysql2"); let globalDbs = new Map(); +// We usually run against the production database, even in local testing, +// to easily test against real data. (Not a wise general practice, but fine +// for this low-stakes project and small dev team with mostly read-only +// operations!) +// +// But you can also specify `DB_ENV=development` to use a local database, +// which is especially helpful for end-to-end modeling testing. +const defaultOptions = + process.env["DB_ENV"] === "development" + ? { + host: "localhost", + user: "impress_2020_dev", + password: "impress_2020_dev", + database: "impress_2020_dev", + } + : { + host: "impress.openneo.net", + user: process.env["IMPRESS_MYSQL_USER"], + password: process.env["IMPRESS_MYSQL_PASSWORD"], + database: "openneo_impress", + }; + async function connectToDb({ - host = "impress.openneo.net", - user = process.env["IMPRESS_MYSQL_USER"], - password = process.env["IMPRESS_MYSQL_PASSWORD"], - database = "openneo_impress", + host = defaultOptions.host, + user = defaultOptions.user, + password = defaultOptions.password, + database = defaultOptions.database, } = {}) { if (globalDbs.has(host)) { return globalDbs.get(host); From 6ec6bbec57689420b1a205f289c2d9ff1f34b4aa Mon Sep 17 00:00:00 2001 From: Matchu Date: Tue, 6 Oct 2020 06:37:51 -0700 Subject: [PATCH 20/22] first-time modeling UX improvements These changes are most relevant for playing around in the dev server, modeing against an empty database. But they'll also help in real-world modeling scenarios! e.g. modeling a new species/color combo is now a bit nicer, we don't show a blank entry in the color picker --- src/app/components/SpeciesColorPicker.js | 7 ++++++- src/server/types/AppearanceLayer.js | 12 +++++++++--- src/server/types/PetAppearance.js | 11 +++++++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/app/components/SpeciesColorPicker.js b/src/app/components/SpeciesColorPicker.js index e5e517a..26c490e 100644 --- a/src/app/components/SpeciesColorPicker.js +++ b/src/app/components/SpeciesColorPicker.js @@ -171,10 +171,15 @@ function SpeciesColorPicker({ // think this matches users' mental hierarchy of species -> color: showing // supported colors for a species makes sense, but the other way around feels // confusing and restrictive.) + // + // Also, if a color is provided that wouldn't normally be visible, we still + // show it. This can happen when someone models a new species/color combo for + // the first time - the boxes will still be red as if it were invalid, but + // this still smooths out the experience a lot. let visibleColors = allColors; if (stateMustAlwaysBeValid && valids && speciesId) { visibleColors = visibleColors.filter( - (c) => getValidPoses(valids, speciesId, c.id).size > 0 + (c) => getValidPoses(valids, speciesId, c.id).size > 0 || c.id === colorId ); } diff --git a/src/server/types/AppearanceLayer.js b/src/server/types/AppearanceLayer.js index 386f84e..afa6780 100644 --- a/src/server/types/AppearanceLayer.js +++ b/src/server/types/AppearanceLayer.js @@ -94,7 +94,12 @@ const resolvers = { imageUrl: async ({ id }, { size }, { swfAssetLoader }) => { const layer = await swfAssetLoader.load(id); - if (!layer.hasImage) { + // If there's no image, return null. (In the development db, which isn't + // aware which assets we have images for on the DTI CDN, assume we _do_ + // have the image - it's usually true, and better for testing.) + const hasImage = + layer.hasImage || process.env["DB_ENV"] === "development"; + if (!hasImage) { return null; } @@ -234,12 +239,13 @@ async function loadAndCacheAssetManifest(db, layer) { // // TODO: Someday the manifests will all exist, right? So we'll want to // reload all the missing ones at that time. - manifest = manifest || ""; + const manifestJson = manifest ? JSON.stringify(manifest) : ""; + const [ result, ] = await db.execute( `UPDATE swf_assets SET manifest = ? WHERE id = ? LIMIT 1;`, - [manifest, layer.id] + [manifestJson, layer.id] ); if (result.affectedRows !== 1) { throw new Error( diff --git a/src/server/types/PetAppearance.js b/src/server/types/PetAppearance.js index b804b05..9f2fa9f 100644 --- a/src/server/types/PetAppearance.js +++ b/src/server/types/PetAppearance.js @@ -106,6 +106,17 @@ const resolvers = { speciesId: id, colorId: "8", // Blue }); + + // In production, this should ~never happen, because all species have a + // Blue version, or at least if a new one is added it will be modeled + // quickly! But in development, before modeling happens, it's possible + // for this to be empty, so we return a fake body ID. (This seems better + // than making it nullable, which adds downstream complexity for a + // particularly edge-y case that generally isn't worth considering.) + if (!petType) { + return ``; + } + return petType.bodyId; }, }, From bb812a2b811734c9f7627878507a0ee65892a369 Mon Sep 17 00:00:00 2001 From: Matchu Date: Tue, 6 Oct 2020 06:38:21 -0700 Subject: [PATCH 21/22] oops, fix bug modeling pet with no items --- src/server/modeling.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/server/modeling.js b/src/server/modeling.js index a92127b..23422f0 100644 --- a/src/server/modeling.js +++ b/src/server/modeling.js @@ -240,6 +240,10 @@ async function saveSwfAssetModelingData(customPetData, context) { // the item. (We do this separately for pet states, so that we can get // the pet state ID first.) const itemAssetInserts = inserts.filter((i) => i.type === "object"); + if (itemAssetInserts.length === 0) { + return; + } + const qs = itemAssetInserts .map( (_) => From 99e64804868e293304f3f9ad45ce6d6a3c23d4b8 Mon Sep 17 00:00:00 2001 From: Matchu Date: Tue, 6 Oct 2020 07:06:19 -0700 Subject: [PATCH 22/22] add logging to modeling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit my hope is that, if we fuck things up, this will make it clear 😅 --- package.json | 2 +- scripts/setup-mysql-dev-constants.sql | 2 +- scripts/setup-mysql-dev-schema.sql | 18 ++++++- scripts/setup-mysql.sql | 14 +++-- src/server/modeling.js | 78 +++++++++++++++++++++++---- 5 files changed, 97 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index d3ccd45..fa2e1a8 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "mysql-dev": "mysql --host=localhost --user=impress_2020_dev --password=impress_2020_dev --database=impress_2020_dev", "mysql-admin": "mysql --host=impress.openneo.net --user=matchu --password --database=openneo_impress", "mysqldump": "mysqldump --host=impress.openneo.net --user=$(dotenv -p IMPRESS_MYSQL_USER) --password=$(dotenv -p IMPRESS_MYSQL_PASSWORD) --column-statistics=0", - "download-mysql-schema": "yarn --silent mysqldump --no-data openneo_impress items item_translations parents_swf_assets pet_types pet_states swf_assets | sed 's/ AUTO_INCREMENT=[0-9]*//g' > scripts/setup-mysql-dev-schema.sql && yarn --silent mysqldump openneo_impress species species_translations colors color_translations zones zone_translations > scripts/setup-mysql-dev-constants.sql", + "download-mysql-schema": "yarn --silent mysqldump --no-data openneo_impress items item_translations modeling_logs parents_swf_assets pet_types pet_states swf_assets | sed 's/ AUTO_INCREMENT=[0-9]*//g' > scripts/setup-mysql-dev-schema.sql && yarn --silent mysqldump openneo_impress species species_translations colors color_translations zones zone_translations > scripts/setup-mysql-dev-constants.sql", "setup-mysql": "yarn mysql-admin < scripts/setup-mysql.sql", "setup-mysql-dev": "yarn mysql-dev < scripts/setup-mysql-dev-constants.sql && yarn mysql-dev < scripts/setup-mysql-dev-schema.sql", "build-cached-data": "node -r dotenv/config scripts/build-cached-data.js", diff --git a/scripts/setup-mysql-dev-constants.sql b/scripts/setup-mysql-dev-constants.sql index 45d64a7..a0993e6 100644 --- a/scripts/setup-mysql-dev-constants.sql +++ b/scripts/setup-mysql-dev-constants.sql @@ -189,4 +189,4 @@ UNLOCK TABLES; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2020-10-06 6:09:54 +-- Dump completed on 2020-10-06 6:59:56 diff --git a/scripts/setup-mysql-dev-schema.sql b/scripts/setup-mysql-dev-schema.sql index 85a5608..f663d01 100644 --- a/scripts/setup-mysql-dev-schema.sql +++ b/scripts/setup-mysql-dev-schema.sql @@ -68,6 +68,22 @@ CREATE TABLE `item_translations` ( ) ENGINE=InnoDB DEFAULT CHARSET=latin1; /*!40101 SET character_set_client = @saved_cs_client */; +-- +-- Table structure for table `modeling_logs` +-- + +DROP TABLE IF EXISTS `modeling_logs`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `modeling_logs` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `log_json` text NOT NULL, + `pet_name` varchar(128) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; +/*!40101 SET character_set_client = @saved_cs_client */; + -- -- Table structure for table `parents_swf_assets` -- @@ -167,4 +183,4 @@ CREATE TABLE `swf_assets` ( /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2020-10-06 6:09:45 +-- Dump completed on 2020-10-06 6:59:49 diff --git a/scripts/setup-mysql.sql b/scripts/setup-mysql.sql index 68d69f8..4a5c8dc 100644 --- a/scripts/setup-mysql.sql +++ b/scripts/setup-mysql.sql @@ -5,6 +5,7 @@ GRANT SELECT ON colors TO impress2020; GRANT SELECT ON color_translations TO impress2020; GRANT SELECT ON items TO impress2020; GRANT SELECT ON item_translations TO impress2020; +GRANT SELECT ON modeling_logs TO impress2020; GRANT SELECT ON parents_swf_assets TO impress2020; GRANT SELECT ON pet_types TO impress2020; GRANT SELECT ON pet_states TO impress2020; @@ -14,11 +15,14 @@ GRANT SELECT ON swf_assets TO impress2020; GRANT SELECT ON zones TO impress2020; GRANT SELECT ON zone_translations TO impress2020; --- Public data tables: write -GRANT UPDATE ON items TO impress2020; -GRANT DELETE ON parents_swf_assets TO impress2020; -GRANT UPDATE ON pet_states TO impress2020; -GRANT UPDATE ON swf_assets TO impress2020; +-- Public data tables: write. Used in modeling and support tools. +GRANT INSERT, UPDATE ON items TO impress2020; +GRANT INSERT, UPDATE ON item_translations TO impress2020; +GRANT INSERT, UPDATE, DELETE ON parents_swf_assets TO impress2020; +GRANT INSERT, UPDATE ON pet_types TO impress2020; +GRANT INSERT, UPDATE ON pet_states TO impress2020; +GRANT INSERT, UPDATE ON swf_assets TO impress2020; +GRANT INSERT ON modeling_logs TO impress2020; -- User data tables GRANT SELECT ON closet_hangers TO impress2020; diff --git a/src/server/modeling.js b/src/server/modeling.js index 23422f0..7ac117b 100644 --- a/src/server/modeling.js +++ b/src/server/modeling.js @@ -8,11 +8,26 @@ * pet contains data we haven't seen before, we write! */ async function saveModelingData(customPetData, petMetaData, context) { + const modelingLogs = []; + const addToModelingLogs = (entry) => { + console.log("[Modeling] " + JSON.stringify(entry, null, 4)); + modelingLogs.push(entry); + }; + context = { ...context, addToModelingLogs }; + await Promise.all([ savePetTypeAndStateModelingData(customPetData, petMetaData, context), saveItemModelingData(customPetData, context), saveSwfAssetModelingData(customPetData, context), ]); + + if (modelingLogs.length > 0) { + const { db } = context; + await db.execute( + `INSERT INTO modeling_logs (log_json, pet_name) VALUES (?, ?)`, + [JSON.stringify(modelingLogs, null, 4), petMetaData.name] + ); + } } async function savePetTypeAndStateModelingData( @@ -25,7 +40,9 @@ async function savePetTypeAndStateModelingData( petTypeBySpeciesAndColorLoader, petStateByPetTypeAndAssetsLoader, swfAssetByRemoteIdLoader, + addToModelingLogs, } = context; + const incomingPetType = { colorId: String(customPetData.custom_pet.color_id), speciesId: String(customPetData.custom_pet.species_id), @@ -50,6 +67,7 @@ async function savePetTypeAndStateModelingData( row.colorId, ], includeUpdatedAt: false, + addToModelingLogs, }); // NOTE: This pet type should have been looked up when syncing pet type, so @@ -107,21 +125,39 @@ async function savePetTypeAndStateModelingData( if (swfAssets.length === 0) { throw new Error(`pet state ${petState.id} has no saved assets?`); } + + const relationshipInserts = swfAssets.map((sa) => ({ + parentType: "PetState", + parentId: petState.id, + swfAssetId: sa.id, + })); + const qs = swfAssets.map((_) => `(?, ?, ?)`).join(", "); - const values = swfAssets - .map((sa) => ["PetState", petState.id, sa.id]) + const values = relationshipInserts + .map(({ parentType, parentId, swfAssetId }) => [ + parentType, + parentId, + swfAssetId, + ]) .flat(); await db.execute( `INSERT INTO parents_swf_assets (parent_type, parent_id, swf_asset_id) VALUES ${qs};`, values ); + + addToModelingLogs({ + tableName: "parents_swf_assets", + inserts: relationshipInserts, + updates: [], + }); }, + addToModelingLogs, }); } async function saveItemModelingData(customPetData, context) { - const { db, itemLoader, itemTranslationLoader } = context; + const { db, itemLoader, itemTranslationLoader, addToModelingLogs } = context; const objectInfos = Object.values(customPetData.object_info_registry); const incomingItems = objectInfos.map((objectInfo) => ({ @@ -148,6 +184,7 @@ async function saveItemModelingData(customPetData, context) { tableName: "items", buildLoaderKey: (row) => row.id, buildUpdateCondition: (row) => [`id = ?`, row.id], + addToModelingLogs, }), syncToDb(db, incomingItemTranslations, { loader: itemTranslationLoader, @@ -157,12 +194,13 @@ async function saveItemModelingData(customPetData, context) { `item_id = ? AND locale = "en"`, row.itemId, ], + addToModelingLogs, }), ]); } async function saveSwfAssetModelingData(customPetData, context) { - const { db, swfAssetByRemoteIdLoader } = context; + const { db, swfAssetByRemoteIdLoader, addToModelingLogs } = context; const objectAssets = Object.values(customPetData.object_asset_registry); const incomingItemSwfAssets = objectAssets.map((objectAsset) => ({ @@ -244,6 +282,12 @@ async function saveSwfAssetModelingData(customPetData, context) { return; } + const relationshipInserts = itemAssetInserts.map(({ remoteId }) => ({ + parentType: "Item", + parentId: assetIdToItemIdMap.get(remoteId), + remoteId, + })); + const qs = itemAssetInserts .map( (_) => @@ -254,11 +298,11 @@ async function saveSwfAssetModelingData(customPetData, context) { `(SELECT id FROM swf_assets WHERE type = "object" AND remote_id = ?))` ) .join(", "); - const values = itemAssetInserts - .map(({ remoteId: swfAssetId }) => [ - "Item", - assetIdToItemIdMap.get(swfAssetId), - swfAssetId, + const values = relationshipInserts + .map(({ parentType, parentId, remoteId }) => [ + parentType, + parentId, + remoteId, ]) .flat(); @@ -267,7 +311,14 @@ async function saveSwfAssetModelingData(customPetData, context) { VALUES ${qs}`, values ); + + addToModelingLogs({ + tableName: "parents_swf_assets", + inserts: relationshipInserts, + updates: [], + }); }, + addToModelingLogs, }); } @@ -294,6 +345,7 @@ async function syncToDb( includeCreatedAt = true, includeUpdatedAt = true, afterInsert = null, + addToModelingLogs, } ) { const loaderKeys = incomingRows.map(buildLoaderKey); @@ -411,6 +463,14 @@ async function syncToDb( ); } await Promise.all(updatePromises); + + if (inserts.length > 0 || updates.length > 0) { + addToModelingLogs({ + tableName, + inserts, + updates, + }); + } } module.exports = { saveModelingData };