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!
This commit is contained in:
Emi Matchu 2023-11-11 13:28:47 -08:00
parent c3a9c24d22
commit 4cd5cf5800
4 changed files with 102 additions and 101 deletions

View file

@ -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) {

View file

@ -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")

View file

@ -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;

View file

@ -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 || "<ignore>", preferredColorId || "<ignore>"]
[id, preferredSpeciesId || "<ignore>", preferredColorId || "<ignore>"],
);
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],
);
}
}