From dfeeb9fe0d5f27c92bd4e7cb6b71abfd6eb39ef4 Mon Sep 17 00:00:00 2001 From: Matchu Date: Fri, 18 Sep 2020 07:34:41 -0700 Subject: [PATCH] modeling will save new item data (but not assets) just a first step! --- scripts/setup-mysql-dev-constants.sql | 2 +- scripts/setup-mysql-dev-schema.sql | 4 +- src/server/query-tests/Pet.test.js | 36 ++- .../__snapshots__/Pet.test.js.snap | 264 ++++++++++++++++++ src/server/query-tests/setup.js | 11 +- src/server/types/Outfit.js | 161 ++++++++++- 6 files changed, 467 insertions(+), 11 deletions(-) diff --git a/scripts/setup-mysql-dev-constants.sql b/scripts/setup-mysql-dev-constants.sql index 94a9806..ba7d6f8 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 5:47:49 +-- Dump completed on 2020-09-18 6:27:12 diff --git a/scripts/setup-mysql-dev-schema.sql b/scripts/setup-mysql-dev-schema.sql index ed94ff3..ea3fb0f 100644 --- a/scripts/setup-mysql-dev-schema.sql +++ b/scripts/setup-mysql-dev-schema.sql @@ -32,7 +32,7 @@ CREATE TABLE `items` ( `price` mediumint(9) NOT NULL, `weight_lbs` smallint(6) DEFAULT NULL, `species_support_ids` mediumtext COLLATE utf8_unicode_ci, - `sold_in_mall` tinyint(1) NOT NULL, + `sold_in_mall` tinyint(1) NOT NULL DEFAULT '0', `last_spidered` datetime DEFAULT NULL, `created_at` datetime DEFAULT NULL, `updated_at` datetime DEFAULT NULL, @@ -77,4 +77,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 5:47:52 +-- Dump completed on 2020-09-18 6:27:15 diff --git a/src/server/query-tests/Pet.test.js b/src/server/query-tests/Pet.test.js index cea327d..f33aac3 100644 --- a/src/server/query-tests/Pet.test.js +++ b/src/server/query-tests/Pet.test.js @@ -51,7 +51,7 @@ describe("Pet", () => { `); }); - it.skip("models new item data", async () => { + it("models new item data", async () => { useTestDb(); const res = await query({ @@ -73,7 +73,7 @@ describe("Pet", () => { expect(res).toHaveNoErrors(); expect(res.data).toMatchSnapshot(); - expect(getDbCalls()).toMatchInlineSnapshot(`Array []`); + expect(getDbCalls()).toMatchSnapshot(); clearDbCalls(); @@ -98,6 +98,7 @@ describe("Pet", () => { thumbnailUrl rarityIndex isNc + createdAt } } `, @@ -105,6 +106,35 @@ describe("Pet", () => { expect(res2).toHaveNoErrors(); expect(res2.data).toMatchSnapshot(); - expect(getDbCalls()).toMatchInlineSnapshot(); + 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", + ], + ], + ] + `); }); }); diff --git a/src/server/query-tests/__snapshots__/Pet.test.js.snap b/src/server/query-tests/__snapshots__/Pet.test.js.snap index 4647fa6..fdc50c8 100644 --- a/src/server/query-tests/__snapshots__/Pet.test.js.snap +++ b/src/server/query-tests/__snapshots__/Pet.test.js.snap @@ -154,3 +154,267 @@ Object { }, } `; + +exports[`Pet models new item data 2`] = ` +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 + ( + id, zones_restrict, thumbnail_url, category, type, rarity_index, + price, weight_lbs, created_at, updated_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + ", + Array [ + "37229", + "0000000000000000000000000000000000000000000000", + "http://images.neopets.com/items/gif_magicball_table.gif", + "Gift", + "Gift", + 101, + 0, + 1, + 2020-01-01T00:00:00.000Z, + 2020-01-01T00:00:00.000Z, + "37375", + "0000000000000000000000000000000000000000000000000000", + "http://images.neopets.com/items/bg_moonstars.gif", + "Special", + "Mystical Surroundings", + 75, + 209, + 1, + 2020-01-01T00:00:00.000Z, + 2020-01-01T00:00:00.000Z, + "38911", + "0000000000000000000000000000000000001100000000000000", + "http://images.neopets.com/items/clo_zafara_agent_hood.gif", + "Clothes", + "Clothes", + 92, + 980, + 1, + 2020-01-01T00:00:00.000Z, + 2020-01-01T00:00:00.000Z, + "38912", + "0000000000000000000101000000000000000000000000000000", + "http://images.neopets.com/items/clo_zafara_agent_robe.gif", + "Clothes", + "Clothes", + 90, + 1476, + 1, + 2020-01-01T00:00:00.000Z, + 2020-01-01T00:00:00.000Z, + "38913", + "0000000000000000000000000000000000000000000000000000", + "http://images.neopets.com/items/clo_zafara_agent_gloves.gif", + "Clothes", + "Clothes", + 88, + 1177, + 1, + 2020-01-01T00:00:00.000Z, + 2020-01-01T00:00:00.000Z, + "43014", + "0000000000000000000000000000000000000000000000", + "http://images.neopets.com/items/toy_stringlight_illleaf.gif", + "Toys", + "Toy", + 80, + 1033, + 1, + 2020-01-01T00:00:00.000Z, + 2020-01-01T00:00:00.000Z, + "43397", + "0000000000000000000000000000000000000000000000", + "http://images.neopets.com/items/mall_staff_jewelled.gif", + "Clothes", + "Clothes", + 500, + 0, + 1, + 2020-01-01T00:00:00.000Z, + 2020-01-01T00:00:00.000Z, + "48313", + "0000000000000000000000000000000000000000000000000000", + "http://images.neopets.com/items/clo_altcuplogo_brooch.gif", + "Clothes", + "Clothes", + 101, + 0, + 1, + 2020-01-01T00:00:00.000Z, + 2020-01-01T00:00:00.000Z, + ], + ], + Array [ + "INSERT INTO item_translations + (item_id, locale, name, description, rarity, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?);", + Array [ + "37229", + "en", + "Magic Ball Table", + "What does this ball actually do?", + "Special", + 2020-01-01T00:00:00.000Z, + 2020-01-01T00:00:00.000Z, + "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, + "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, + "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, + "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, + "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, + "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, + "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, + ], + ], +] +`; + +exports[`Pet models new item data 3`] = ` +Object { + "items": Array [ + Object { + "createdAt": "2020-01-01T00:00:00.000Z", + "description": "What does this ball actually do?", + "id": "37229", + "isNc": false, + "name": "Magic Ball Table", + "rarityIndex": 101, + "thumbnailUrl": "http://images.neopets.com/items/gif_magicball_table.gif", + }, + Object { + "createdAt": "2020-01-01T00:00:00.000Z", + "description": "Dont forget to wish upon a star.", + "id": "37375", + "isNc": false, + "name": "Moon and Stars Background", + "rarityIndex": 75, + "thumbnailUrl": "http://images.neopets.com/items/bg_moonstars.gif", + }, + Object { + "createdAt": "2020-01-01T00:00:00.000Z", + "description": "Hide your face and hair so no one can recognise you.", + "id": "38911", + "isNc": false, + "name": "Zafara Agent Hood", + "rarityIndex": 92, + "thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_hood.gif", + }, + Object { + "createdAt": "2020-01-01T00:00:00.000Z", + "description": "This robe is great for being stealthy.", + "id": "38912", + "isNc": false, + "name": "Zafara Agent Robe", + "rarityIndex": 90, + "thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_robe.gif", + }, + Object { + "createdAt": "2020-01-01T00:00:00.000Z", + "description": "Dont leave any trace that you were there with these gloves.", + "id": "38913", + "isNc": false, + "name": "Zafara Agent Gloves", + "rarityIndex": 88, + "thumbnailUrl": "http://images.neopets.com/items/clo_zafara_agent_gloves.gif", + }, + Object { + "createdAt": "2020-01-01T00:00:00.000Z", + "description": "These leaves almost look magical with their gentle glow.", + "id": "43014", + "isNc": false, + "name": "Green Leaf String Lights", + "rarityIndex": 80, + "thumbnailUrl": "http://images.neopets.com/items/toy_stringlight_illleaf.gif", + }, + Object { + "createdAt": "2020-01-01T00:00:00.000Z", + "description": "This jewelled staff shines with a magical light.", + "id": "43397", + "isNc": true, + "name": "Jewelled Staff", + "rarityIndex": 500, + "thumbnailUrl": "http://images.neopets.com/items/mall_staff_jewelled.gif", + }, + Object { + "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", + "isNc": false, + "name": "Altador Cup Brooch", + "rarityIndex": 101, + "thumbnailUrl": "http://images.neopets.com/items/clo_altcuplogo_brooch.gif", + }, + ], +} +`; diff --git a/src/server/query-tests/setup.js b/src/server/query-tests/setup.js index 4e60ddf..82548e3 100644 --- a/src/server/query-tests/setup.js +++ b/src/server/query-tests/setup.js @@ -33,6 +33,7 @@ jest.mock("../db"); let dbExecuteFn; let db; let dbEnvironment = "production"; +let dbSetupDone = false; const dbSetupScripts = [ fs .readFileSync( @@ -60,15 +61,20 @@ beforeAll(() => { db = await actualConnectToDb(options); - if (dbEnvironment === "test") { + if (dbEnvironment === "test" && !dbSetupDone) { for (const script of dbSetupScripts) { await db.query(script); } } + dbSetupDone = true; dbExecuteFn = jest.spyOn(db, "execute"); return db; }); + + // Mock out a current "now" date, for consistent snapshots + const NOW = new Date("2020-01-01T00:00:00.000Z"); + jest.spyOn(global, "Date").mockImplementation(() => NOW); }); beforeEach(() => { accessTokenForQueries = null; @@ -76,11 +82,13 @@ beforeEach(() => { dbExecuteFn.mockClear(); } dbEnvironment = "production"; + dbSetupDone = false; }); afterAll(() => { if (db) { db.end(); } + Date.mockRestore(); }); const getDbCalls = () => (dbExecuteFn ? dbExecuteFn.mock.calls : []); const clearDbCalls = () => dbExecuteFn?.mockClear(); @@ -130,6 +138,7 @@ module.exports = { query, getDbCalls, clearDbCalls, + getDb: () => db, useTestDb, logInAsTestUser, }; diff --git a/src/server/types/Outfit.js b/src/server/types/Outfit.js index 78bfe1f..2422f6f 100644 --- a/src/server/types/Outfit.js +++ b/src/server/types/Outfit.js @@ -46,11 +46,30 @@ const resolvers = { }, Query: { outfit: (_, { id }) => ({ id }), - petOnNeopetsDotCom: async (_, { petName }) => { - const [petMetaData, customPetData] = await Promise.all([ - loadPetMetaData(petName), - loadCustomPetData(petName), + petOnNeopetsDotCom: async ( + _, + { petName }, + { db, itemLoader, itemTranslationLoader } + ) => { + // 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, + }) + ); + + // ...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 outfit = { // TODO: This isn't a fully-working Outfit object. It works for the // client as currently implemented, but we'll probably want to @@ -66,6 +85,7 @@ const resolvers = { rarityIndex: o.rarity_index, })), }; + return outfit; }, }, @@ -131,4 +151,137 @@ function getPoseFromPetData(petMetaData, petCustomData) { } } +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 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, + 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, + name: objectInfo.name, + description: objectInfo.description, + rarity: objectInfo.rarity, + }; + + if (item instanceof Error) { + // New item, we'll just insert it! + rowsToInsert.push({ + ...objectInfoFields, + 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() + ), + ]); + } + + // 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; +} + module.exports = { typeDefs, resolvers };