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();