From 4cd5cf5800c1cbe01508290dfd01acde86d46a21 Mon Sep 17 00:00:00 2001 From: Matchu Date: Sat, 11 Nov 2023 13:28:47 -0800 Subject: [PATCH] Fix handling of assets with `https://images.neopets.com/temp` SWF URLs Weird! Well! This caused two issues. One is that we used to filter down to only assets whose urls end in `.swf`, because I never added support for the sound ones :p The other is that we were using the SWF URL to infer the potential manifest URLs, which we can't do when the SWF URL is a weird placeholder! Thankfully, just yesterday (wow!) we happened to have Classic DTI keep track of `manifest_url` during the modeling process, and we backfilled everything. So the most recent `temp` assets have their manifest! But slightly older ones (not that old tho!!) didn't, so I manually inferred the manifest URL from the asset's `remote_id` field instead (which worked cuz there was no hash component to the manifest URL, unlike some manifests). The item "Flowing Black Cloak" (86668) on the Zafara is the one we were looking at and testing with! --- scripts/cache-asset-manifests.js | 21 +++--- src/server/neopets-assets.js | 14 ++-- src/server/types/AppearanceLayer.js | 63 +++++++---------- src/server/types/Item.js | 105 +++++++++++++++------------- 4 files changed, 102 insertions(+), 101 deletions(-) diff --git a/scripts/cache-asset-manifests.js b/scripts/cache-asset-manifests.js index f3435a0..596549b 100644 --- a/scripts/cache-asset-manifests.js +++ b/scripts/cache-asset-manifests.js @@ -24,10 +24,10 @@ async function cacheAssetManifests(db) { : `manifest IS NULL OR manifest = ""`; const [rows] = await db.execute( - `SELECT id, url, manifest FROM swf_assets ` + + `SELECT id, url, manifest, manifest_url FROM swf_assets ` + `WHERE ${condition} AND id >= ? ` + `ORDER BY id`, - [argv.start || 0] + [argv.start || 0], ); const numRowsTotal = rows.length; @@ -37,7 +37,10 @@ async function cacheAssetManifests(db) { async function cacheAssetManifest(row) { try { - let manifest = await neopetsAssets.loadAssetManifest(row.url); + let manifest = await neopetsAssets.loadAssetManifest( + row.manifest_url, + row.url, + ); // After loading, write the new manifest. We make sure to write an empty // string if there was no manifest, to signify that it doesn't exist, so @@ -55,20 +58,18 @@ async function cacheAssetManifests(db) { console.info( `UPDATE swf_assets SET manifest = ${escapedManifest}, ` + `manifest_cached_at = CURRENT_TIMESTAMP() ` + - `WHERE id = ${row.id} LIMIT 1;` + `WHERE id = ${row.id} LIMIT 1;`, ); } else { - const [ - result, - ] = await db.execute( + const [result] = await db.execute( `UPDATE swf_assets SET manifest = ?, ` + `manifest_cached_at = CURRENT_TIMESTAMP() ` + `WHERE id = ? LIMIT 1;`, - [manifest, row.id] + [manifest, row.id], ); if (result.affectedRows !== 1) { throw new Error( - `Expected to affect 1 asset, but affected ${result.affectedRows}` + `Expected to affect 1 asset, but affected ${result.affectedRows}`, ); } } @@ -85,7 +86,7 @@ async function cacheAssetManifests(db) { console.error( `${percent}% ${numRowsDone}/${numRowsTotal} ` + `(Exists? ${Boolean(manifest)}. Updated? ${updated}. ` + - `Layer: ${row.id}, ${row.url}.)` + `Layer: ${row.id}, ${row.url}.)`, ); } } catch (e) { diff --git a/src/server/neopets-assets.js b/src/server/neopets-assets.js index dfe0464..39d96af 100644 --- a/src/server/neopets-assets.js +++ b/src/server/neopets-assets.js @@ -1,10 +1,13 @@ import fetch from "node-fetch"; -async function loadAssetManifest(swfUrl) { - const possibleManifestUrls = convertSwfUrlToPossibleManifestUrls(swfUrl); +async function loadAssetManifest(manifestUrl, swfUrl) { + const possibleManifestUrls = + manifestUrl != null + ? [manifestUrl] + : convertSwfUrlToPossibleManifestUrls(swfUrl); const responses = await Promise.all( - possibleManifestUrls.map((url) => fetch(url)) + possibleManifestUrls.map((url) => fetch(url)), ); // Print errors for any responses with unexpected statuses. We'll do this @@ -13,7 +16,7 @@ async function loadAssetManifest(swfUrl) { if (!res.ok && res.status !== 404) { console.error( `for asset manifest, images.neopets.com returned: ` + - `${res.status} ${res.statusText}. (${res.url})` + `${res.status} ${res.statusText}. (${res.url})`, ); } } @@ -34,7 +37,8 @@ async function loadAssetManifest(swfUrl) { }; } -const SWF_URL_PATTERN = /^https?:\/\/images\.neopets\.com\/cp\/(bio|items)\/swf\/(.+?)_([a-z0-9]+)\.swf$/; +const SWF_URL_PATTERN = + /^https?:\/\/images\.neopets\.com\/cp\/(bio|items)\/swf\/(.+?)_([a-z0-9]+)\.swf$/; function convertSwfUrlToPossibleManifestUrls(swfUrl) { const match = new URL(swfUrl, "https://images.neopets.com") diff --git a/src/server/types/AppearanceLayer.js b/src/server/types/AppearanceLayer.js index 2c94a29..59a4791 100644 --- a/src/server/types/AppearanceLayer.js +++ b/src/server/types/AppearanceLayer.js @@ -198,7 +198,7 @@ const typeDefs = gql` const ALL_KNOWN_GLITCH_VALUES = new Set( typeDefs.definitions .find((d) => d.name.value === "AppearanceLayerKnownGlitch") - .values.map((v) => v.name.value) + .values.map((v) => v.name.value), ); const resolvers = { @@ -222,11 +222,8 @@ const resolvers = { imageUrl: async ({ id }, { size = "SIZE_150" }, { swfAssetLoader, db }) => { const layer = await swfAssetLoader.load(id); - const { - format, - jsAssetUrl, - pngAssetUrl, - } = await loadAndCacheAssetDataFromManifest(db, layer); + const { format, jsAssetUrl, pngAssetUrl } = + await loadAndCacheAssetDataFromManifest(db, layer); // For the largest size, try to use the official Neopets PNG! if (size === "SIZE_600") { @@ -280,11 +277,8 @@ const resolvers = { imageUrlV2: async ({ id }, { idealSize }, { swfAssetLoader, db }) => { const layer = await swfAssetLoader.load(id); - const { - format, - jsAssetUrl, - pngAssetUrl, - } = await loadAndCacheAssetDataFromManifest(db, layer); + const { format, jsAssetUrl, pngAssetUrl } = + await loadAndCacheAssetDataFromManifest(db, layer); // If there's an official single-image PNG we can use, use it! This is // what the official /customise editor uses at time of writing. @@ -350,11 +344,8 @@ const resolvers = { return null; } - const { - format, - jsAssetUrl, - svgAssetUrl, - } = await loadAndCacheAssetDataFromManifest(db, layer); + const { format, jsAssetUrl, svgAssetUrl } = + await loadAndCacheAssetDataFromManifest(db, layer); // If there's an official single-image SVG we can use, use it! The NC // Mall player uses this at time of writing, and we generally prefer it @@ -377,7 +368,7 @@ const resolvers = { const { format, jsAssetUrl } = await loadAndCacheAssetDataFromManifest( db, - layer + layer, ); if (format === "lod" && jsAssetUrl) { @@ -395,7 +386,7 @@ const resolvers = { SELECT parent_id FROM parents_swf_assets WHERE swf_asset_id = ? AND parent_type = "Item" LIMIT 1; `, - [id] + [id], ); if (rows.length === 0) { @@ -419,7 +410,7 @@ const resolvers = { knownGlitches = knownGlitches.filter((knownGlitch) => { if (!ALL_KNOWN_GLITCH_VALUES.has(knownGlitch)) { console.warn( - `Layer ${id}: Skipping unexpected knownGlitches value: ${knownGlitch}` + `Layer ${id}: Skipping unexpected knownGlitches value: ${knownGlitch}`, ); return false; } @@ -434,17 +425,17 @@ const resolvers = { itemAppearanceLayersByRemoteId: async ( _, { remoteIds }, - { swfAssetByRemoteIdLoader } + { swfAssetByRemoteIdLoader }, ) => { const layers = await swfAssetByRemoteIdLoader.loadMany( - remoteIds.map((remoteId) => ({ type: "object", remoteId })) + remoteIds.map((remoteId) => ({ type: "object", remoteId })), ); return layers.filter((l) => l).map(({ id }) => ({ id })); }, numAppearanceLayersConverted: async ( _, { type }, - { swfAssetCountLoader } + { swfAssetCountLoader }, ) => { const count = await swfAssetCountLoader.load({ type: convertLayerTypeToSwfAssetType(type), @@ -461,7 +452,7 @@ const resolvers = { appearanceLayerByRemoteId: async ( _, { type, remoteId }, - { swfAssetByRemoteIdLoader } + { swfAssetByRemoteIdLoader }, ) => { const swfAsset = await swfAssetByRemoteIdLoader.load({ type: type === "PET_LAYER" ? "biology" : "object", @@ -512,7 +503,7 @@ async function loadAndCacheAssetDataFromManifest(db, layer) { } catch (e) { console.error( `Layer ${layer.id} has invalid manifest JSON: ` + - `${JSON.stringify(layer.manifest)}` + `${JSON.stringify(layer.manifest)}`, ); manifest = null; } @@ -526,7 +517,7 @@ async function loadAndCacheAssetDataFromManifest(db, layer) { if (manifest === null || manifestAgeInMs > MAX_MANIFEST_AGE_IN_MS) { console.info( `[Layer ${layer.id}] Updating manifest cache, ` + - `last updated ${manifestAgeInMs}ms ago: ${layer.manifestCachedAt}` + `last updated ${manifestAgeInMs}ms ago: ${layer.manifestCachedAt}`, ); manifest = await loadAndCacheAssetManifest(db, layer); } @@ -540,7 +531,7 @@ async function loadAndCacheAssetDataFromManifest(db, layer) { const format = asset.format; const assetUrls = asset.assetData.map( - (ad) => new URL(ad.path, "https://images.neopets.com") + (ad) => new URL(ad.path, "https://images.neopets.com"), ); // In the case of JS assets, we want the *last* one in the list, because @@ -554,7 +545,7 @@ async function loadAndCacheAssetDataFromManifest(db, layer) { // TODO: There's a file_ext field in the full manifest, but it's not // included in our cached copy. That would probably be more // reliable! - (url) => path.extname(url.pathname) === ".js" + (url) => path.extname(url.pathname) === ".js", ); const svgAssetUrl = assetUrls.find( @@ -563,7 +554,7 @@ async function loadAndCacheAssetDataFromManifest(db, layer) { // TODO: There's a file_ext field in the full manifest, but it's not // included in our cached copy. That would probably be more // reliable! - (url) => path.extname(url.pathname) === ".svg" + (url) => path.extname(url.pathname) === ".svg", ); // NOTE: Unlike movies, we *must* use the first PNG and not the last, because @@ -576,7 +567,7 @@ async function loadAndCacheAssetDataFromManifest(db, layer) { // TODO: There's a file_ext field in the full manifest, but it's not // included in our cached copy. That would probably be more // reliable! - (url) => path.extname(url.pathname) === ".png" + (url) => path.extname(url.pathname) === ".png", ); return { format, jsAssetUrl, svgAssetUrl, pngAssetUrl }; @@ -586,14 +577,14 @@ async function loadAndCacheAssetManifest(db, layer) { let manifest; try { manifest = await Promise.race([ - loadAssetManifest(layer.url), + loadAssetManifest(layer.manifestUrl, layer.url), new Promise((_, reject) => - setTimeout(() => reject(new Error(`manifest request timed out`)), 2000) + setTimeout(() => reject(new Error(`manifest request timed out`)), 2000), ), ]); } catch (e) { console.error( - new Error("Error loading asset manifest, caused by the error below") + new Error("Error loading asset manifest, caused by the error below"), ); console.error(e); return null; @@ -610,7 +601,7 @@ async function loadAndCacheAssetManifest(db, layer) { if (manifestJson.length > 16777215) { console.warn( `Skipping saving asset manifest for layer ${layer.id}, because its ` + - `length is ${manifestJson.length}, which exceeds the database limit.` + `length is ${manifestJson.length}, which exceeds the database limit.`, ); return manifest; } @@ -621,16 +612,16 @@ async function loadAndCacheAssetManifest(db, layer) { SET manifest = ?, manifest_cached_at = CURRENT_TIMESTAMP() WHERE id = ? LIMIT 1; `, - [manifestJson, layer.id] + [manifestJson, layer.id], ); if (result.affectedRows !== 1) { throw new Error( - `Expected to affect 1 asset, but affected ${result.affectedRows}` + `Expected to affect 1 asset, but affected ${result.affectedRows}`, ); } console.info( `Loaded and saved manifest for ${layer.type} ${layer.remoteId}. ` + - `DTI ID: ${layer.id}. Exists?: ${Boolean(manifest)}` + `DTI ID: ${layer.id}. Exists?: ${Boolean(manifest)}`, ); return manifest; diff --git a/src/server/types/Item.js b/src/server/types/Item.js index b2a65f5..208e8cb 100644 --- a/src/server/types/Item.js +++ b/src/server/types/Item.js @@ -339,12 +339,12 @@ const resolvers = { const translation = await itemTranslationLoader.load(id); if (!translation) { console.warn( - `Item.isPb: Translation not found for item ${id}. Returning false.` + `Item.isPb: Translation not found for item ${id}. Returning false.`, ); return false; } return translation.description.includes( - "This item is part of a deluxe paint brush set!" + "This item is part of a deluxe paint brush set!", ); }, createdAt: async ({ id }, _, { itemLoader }) => { @@ -358,7 +358,7 @@ const resolvers = { ncTradeValueText: async ( { id }, _, - { itemLoader, itemTranslationLoader } + { itemLoader, itemTranslationLoader }, ) => { // Skip this lookup for non-NC items, as a perf optimization. const item = await itemLoader.load(id); @@ -382,7 +382,7 @@ const resolvers = { ncTradeValue = await getOWLSTradeValue(itemName); } catch (e) { console.error( - `Error loading ncTradeValueText for item ${id}, skipping:` + `Error loading ncTradeValueText for item ${id}, skipping:`, ); console.error(e); ncTradeValue = null; @@ -395,7 +395,7 @@ const resolvers = { currentUserOwnsThis: async ( { id }, _, - { currentUserId, userItemClosetHangersLoader } + { currentUserId, userItemClosetHangersLoader }, ) => { if (currentUserId == null) return false; const closetHangers = await userItemClosetHangersLoader.load({ @@ -407,7 +407,7 @@ const resolvers = { currentUserWantsThis: async ( { id }, _, - { currentUserId, userItemClosetHangersLoader } + { currentUserId, userItemClosetHangersLoader }, ) => { if (currentUserId == null) return false; const closetHangers = await userItemClosetHangersLoader.load({ @@ -419,7 +419,7 @@ const resolvers = { currentUserHasInLists: async ( { id }, _, - { currentUserId, userItemClosetHangersLoader } + { currentUserId, userItemClosetHangersLoader }, ) => { if (currentUserId == null) return false; const closetHangers = await userItemClosetHangersLoader.load({ @@ -444,7 +444,7 @@ const resolvers = { numUsersOfferingThis: async ( { id }, _, - { itemTradesLoader, userLastTradeActivityLoader } + { itemTradesLoader, userLastTradeActivityLoader }, ) => { // First, get the trades themselves. TODO: Optimize into one query? const trades = await itemTradesLoader.load({ @@ -455,7 +455,7 @@ const resolvers = { // Then, get the last active dates for those users. const userIds = trades.map((t) => t.user.id); const lastActiveDates = await userLastTradeActivityLoader.loadMany( - userIds + userIds, ); // Finally, count how many of those dates were in the last 6 months. @@ -469,7 +469,7 @@ const resolvers = { numUsersSeekingThis: async ( { id }, _, - { itemTradesLoader, userLastTradeActivityLoader } + { itemTradesLoader, userLastTradeActivityLoader }, ) => { // First, get the trades themselves. TODO: Optimize into one query? const trades = await itemTradesLoader.load({ @@ -480,7 +480,7 @@ const resolvers = { // Then, get the last active dates for those users. const userIds = trades.map((t) => t.user.id); const lastActiveDates = await userLastTradeActivityLoader.loadMany( - userIds + userIds, ); // Finally, count how many of those dates were in the last 6 months. @@ -528,7 +528,7 @@ const resolvers = { appearanceOn: async ( { id }, { speciesId, colorId }, - { petTypeBySpeciesAndColorLoader } + { petTypeBySpeciesAndColorLoader }, ) => { const petType = await petTypeBySpeciesAndColorLoader.load({ speciesId, @@ -553,7 +553,7 @@ const resolvers = { speciesThatNeedModels: async ( { id }, { colorId = "8" }, // Blue - { speciesThatNeedModelsForItemLoader, allSpeciesIdsForColorLoader } + { speciesThatNeedModelsForItemLoader, allSpeciesIdsForColorLoader }, ) => { // NOTE: If we're running this in the context of `itemsThatNeedModels`, // this loader should already be primed, no extra query! @@ -567,25 +567,25 @@ const resolvers = { const modeledSpeciesIds = row.modeledSpeciesIds.split(","); const allSpeciesIdsForThisColor = await allSpeciesIdsForColorLoader.load( - colorId + colorId, ); let allModelableSpeciesIds = allSpeciesIdsForThisColor; if (!row.supportsVandagyre) { allModelableSpeciesIds = allModelableSpeciesIds.filter( - (s) => s !== "55" + (s) => s !== "55", ); } const unmodeledSpeciesIds = allModelableSpeciesIds.filter( - (id) => !modeledSpeciesIds.includes(id) + (id) => !modeledSpeciesIds.includes(id), ); return unmodeledSpeciesIds.map((id) => ({ id })); }, canonicalAppearance: async ( { id }, { preferredSpeciesId, preferredColorId }, - { db } + { db }, ) => { const [rows] = await db.query( ` @@ -607,7 +607,7 @@ const resolvers = { colors.standard DESC LIMIT 1 `, - [id, preferredSpeciesId || "", preferredColorId || ""] + [id, preferredSpeciesId || "", preferredColorId || ""], ); if (rows.length === 0) { return null; @@ -649,7 +649,7 @@ const resolvers = { parents_swf_assets.swf_asset_id = swf_assets.id WHERE items.id = ? `, - [id] + [id], ); const bodyIds = rows.map((row) => row.body_id); const bodies = bodyIds.map((id) => ({ id })); @@ -658,7 +658,7 @@ const resolvers = { compatibleBodiesAndTheirZones: async ( { id }, _, - { itemCompatibleBodiesAndTheirZonesLoader } + { itemCompatibleBodiesAndTheirZonesLoader }, ) => { const rows = await itemCompatibleBodiesAndTheirZonesLoader.load(id); return rows.map((row) => ({ @@ -682,7 +682,7 @@ const resolvers = { parents_swf_assets.swf_asset_id = swf_assets.id WHERE items.id = ? `, - [id] + [id], ); const bodyIds = rows.map((row) => String(row.body_id)); return bodyIds.map((bodyId) => ({ item: { id }, bodyId })); @@ -698,7 +698,12 @@ const resolvers = { bodyId, }); - let assets = allSwfAssets.filter((sa) => sa.url.endsWith(".swf")); + // NOTE: Previously, I used this to filter assets to just SWFs, to avoid + // dealing with the audio assets altogether cuz I never built support for + // them. But now, some assets have the url `https://images.neopets.com/temp` + // instead? So uhh. Disabling this for now. + // let assets = allSwfAssets.filter((sa) => sa.url.endsWith(".swf")); + let assets = allSwfAssets; // If there are no body-specific assets in this appearance, then remove // assets with the glitch flag REQUIRES_OTHER_BODY_SPECIFIC_ASSETS: the @@ -717,7 +722,7 @@ const resolvers = { restrictedZones: async ( { item: { id: itemId }, bodyId }, _, - { itemSwfAssetLoader, itemLoader } + { itemSwfAssetLoader, itemLoader }, ) => { // Check whether this appearance is empty. If so, restrict no zones. const allSwfAssets = await itemSwfAssetLoader.load({ itemId, bodyId }); @@ -760,7 +765,7 @@ const resolvers = { petTypeBySpeciesAndColorLoader, currentUserId, }, - { cacheControl } + { cacheControl }, ) => { if (currentUserOwnsOrWants != null) { cacheControl.setCacheHint({ scope: "PRIVATE" }); @@ -775,7 +780,7 @@ const resolvers = { if (!petType) { throw new Error( `pet type not found: speciesId=${fitsPet.speciesId}, ` + - `colorId: ${fitsPet.colorId}` + `colorId: ${fitsPet.colorId}`, ); } bodyId = petType.bodyId; @@ -806,7 +811,7 @@ const resolvers = { itemSearchV2: async ( _, { query, fitsPet, itemKind, currentUserOwnsOrWants, zoneIds = [] }, - { petTypeBySpeciesAndColorLoader } + { petTypeBySpeciesAndColorLoader }, ) => { let bodyId = null; if (fitsPet) { @@ -817,7 +822,7 @@ const resolvers = { if (!petType) { throw new Error( `pet type not found: speciesId=${fitsPet.speciesId}, ` + - `colorId: ${fitsPet.colorId}` + `colorId: ${fitsPet.colorId}`, ); } bodyId = petType.bodyId; @@ -851,7 +856,7 @@ const resolvers = { offset, limit, }, - { petTypeBySpeciesAndColorLoader, itemSearchLoader, currentUserId } + { petTypeBySpeciesAndColorLoader, itemSearchLoader, currentUserId }, ) => { const petType = await petTypeBySpeciesAndColorLoader.load({ speciesId, @@ -859,7 +864,7 @@ const resolvers = { }); if (!petType) { throw new Error( - `pet type not found: speciesId=${speciesId}, colorId: ${colorId}` + `pet type not found: speciesId=${speciesId}, colorId: ${colorId}`, ); } const { bodyId } = petType; @@ -883,10 +888,10 @@ const resolvers = { itemsThatNeedModels: async ( _, { colorId = "8" }, // Defaults to Blue - { itemsThatNeedModelsLoader } + { itemsThatNeedModelsLoader }, ) => { const speciesIdsByColorIdAndItemId = await itemsThatNeedModelsLoader.load( - "all" + "all", ); const speciesIdsByItemIds = speciesIdsByColorIdAndItemId.get(colorId); const itemIds = (speciesIdsByItemIds && speciesIdsByItemIds.keys()) || []; @@ -899,7 +904,7 @@ const resolvers = { { query, bodyId, itemKind, currentUserOwnsOrWants, zoneIds }, { offset, limit }, { currentUserId, itemSearchNumTotalItemsLoader }, - { cacheControl } + { cacheControl }, ) => { if (currentUserOwnsOrWants != null) { cacheControl.setCacheHint({ scope: "PRIVATE" }); @@ -920,7 +925,7 @@ const resolvers = { { query, bodyId, itemKind, currentUserOwnsOrWants, zoneIds }, { offset, limit }, { currentUserId, itemSearchItemsLoader }, - { cacheControl } + { cacheControl }, ) => { if (currentUserOwnsOrWants != null) { cacheControl.setCacheHint({ scope: "PRIVATE" }); @@ -944,7 +949,7 @@ const resolvers = { addToItemsCurrentUserOwns: async ( _, { itemId }, - { currentUserId, db, itemLoader } + { currentUserId, db, itemLoader }, ) => { if (currentUserId == null) { throw new Error(`must be logged in`); @@ -969,7 +974,7 @@ const resolvers = { WHERE item_id = ? AND user_id = ? AND owned = ? ) `, - [itemId, currentUserId, 1, now, now, true, itemId, currentUserId, true] + [itemId, currentUserId, 1, now, now, true, itemId, currentUserId, true], ); return { id: itemId }; @@ -977,7 +982,7 @@ const resolvers = { removeFromItemsCurrentUserOwns: async ( _, { itemId }, - { currentUserId, db, itemLoader } + { currentUserId, db, itemLoader }, ) => { if (currentUserId == null) { throw new Error(`must be logged in`); @@ -991,7 +996,7 @@ const resolvers = { await db.query( `DELETE FROM closet_hangers WHERE item_id = ? AND user_id = ? AND owned = ?;`, - [itemId, currentUserId, true] + [itemId, currentUserId, true], ); return { id: itemId }; @@ -999,7 +1004,7 @@ const resolvers = { addToItemsCurrentUserWants: async ( _, { itemId }, - { currentUserId, db, itemLoader } + { currentUserId, db, itemLoader }, ) => { if (currentUserId == null) { throw new Error(`must be logged in`); @@ -1034,7 +1039,7 @@ const resolvers = { itemId, currentUserId, false, - ] + ], ); return { id: itemId }; @@ -1042,7 +1047,7 @@ const resolvers = { removeFromItemsCurrentUserWants: async ( _, { itemId }, - { currentUserId, db, itemLoader } + { currentUserId, db, itemLoader }, ) => { if (currentUserId == null) { throw new Error(`must be logged in`); @@ -1056,7 +1061,7 @@ const resolvers = { await db.query( `DELETE FROM closet_hangers WHERE item_id = ? AND user_id = ? AND owned = ?;`, - [itemId, currentUserId, false] + [itemId, currentUserId, false], ); return { id: itemId }; @@ -1064,11 +1069,11 @@ const resolvers = { addItemToClosetList: async ( _, { listId, itemId, removeFromDefaultList }, - { currentUserId, db, closetListLoader } + { currentUserId, db, closetListLoader }, ) => { const closetListRef = await loadClosetListOrDefaultList( listId, - closetListLoader + closetListLoader, ); if (closetListRef == null) { throw new Error(`list ${listId} not found`); @@ -1094,7 +1099,7 @@ const resolvers = { AND owned = ? LIMIT 1; `, - [itemId, userId, ownsOrWantsItems === "OWNS"] + [itemId, userId, ownsOrWantsItems === "OWNS"], ); } @@ -1105,7 +1110,7 @@ const resolvers = { (item_id, user_id, owned, list_id, quantity, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?); `, - [itemId, userId, ownsOrWantsItems === "OWNS", listId, 1, now, now] + [itemId, userId, ownsOrWantsItems === "OWNS", listId, 1, now, now], ); await connection.commit(); @@ -1126,11 +1131,11 @@ const resolvers = { removeItemFromClosetList: async ( _, { listId, itemId, ensureInSomeList }, - { currentUserId, db, closetListLoader } + { currentUserId, db, closetListLoader }, ) => { const closetListRef = await loadClosetListOrDefaultList( listId, - closetListLoader + closetListLoader, ); if (closetListRef == null) { throw new Error(`list ${listId} not found`); @@ -1157,7 +1162,7 @@ const resolvers = { DELETE FROM closet_hangers WHERE ${listMatcherCondition} AND item_id = ? LIMIT 1; `, - [...listMatcherValues, itemId] + [...listMatcherValues, itemId], ); if (ensureInSomeList) { @@ -1168,7 +1173,7 @@ const resolvers = { SELECT COUNT(*) AS count FROM closet_hangers WHERE user_id = ? AND item_id = ? AND owned = ? `, - [userId, itemId, ownsOrWantsItems === "OWNS"] + [userId, itemId, ownsOrWantsItems === "OWNS"], ); if (rows[0].count === 0) { @@ -1179,7 +1184,7 @@ const resolvers = { (item_id, user_id, owned, list_id, quantity, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?); `, - [itemId, userId, ownsOrWantsItems === "OWNS", null, 1, now, now] + [itemId, userId, ownsOrWantsItems === "OWNS", null, 1, now, now], ); } }