Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
306 changes: 205 additions & 101 deletions api/routes/shopdirectory.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,122 +9,226 @@ export default function shopApiRoute(app, config, db, features, lang) {
isFeatureEnabled(features.shopdirectory, res, lang);

const material = optional(req.query, "material");
const limit = pLimit(10);
const includeOutOfStock = optional(req.query, "includeOutOfStock");
const includeOutOfStockValue =
String(includeOutOfStock).toLowerCase() === "true";
const owner = optional(req.query, "owner");
const concurrencyLimit = pLimit(10);

const rawLimit = Number.parseInt(req.query.limit, 10);
const rawOffset = Number.parseInt(req.query.offset, 10);
const limitValue = Math.min(Math.max(rawLimit || 50, 1), 200);
const offsetValue = Math.max(rawOffset || 0, 0);

const requestContext = {
limitValue,
offsetValue,
includeOutOfStock: includeOutOfStockValue,
material,
owner,
};

console.info("[Shop API] Incoming request", requestContext);

try {
async function getShops(dbQuery) {
return new Promise((resolve, reject) => {
db.query(dbQuery, async (error, results) => {
const runQuery = (query, params = []) =>
new Promise((resolve, reject) => {
db.query(query, params, (error, results) => {
if (error) {
console.error("Database error:", error);
console.error("[Shop API] Database error", {
error,
query,
params,
});
reject(error);
return;
}

if (!results.length) {
res.send({
success: false,
message: "There are no shops available.",
});
resolve();
return;
}
resolve(results);
});
});

try {
const modifiedShops = await Promise.all(
results.map((shop) =>
limit(async () => {
const itemName = shop.item.trim();

// Fetch item data from Craftdex
const itemFetchURL = `https://craftdex.onrender.com/search/${itemName}`;
let itemApiData = {};
try {
const itemResponse = await fetch(itemFetchURL);
if (itemResponse.ok) {
itemApiData = await itemResponse.json();
} else {
console.error(
`Failed to fetch item data: ${itemResponse.status}`
);
}
} catch (itemError) {
console.error("Error fetching item data:", itemError);
}

// Fetch user data from internal API
const userFetchURL = `${process.env.siteAddress}/api/user/get?userId=${shop.userId}`;
let userApiData = {};
try {
const userResponse = await fetch(userFetchURL, {
headers: { "x-access-token": process.env.apiKey },
});
if (userResponse.ok) {
userApiData = await userResponse.json();
} else {
console.error(
`Failed to fetch user data: ${userResponse.status}`
);
}
} catch (userError) {
console.error("Error fetching user data:", userError);
}

// Get user profile picture
let profilePicture = "";
try {
profilePicture = await getProfilePicture(
userApiData.data[0]?.username
);
} catch (profileError) {
console.error(
"Error fetching profile picture:",
profileError
);
}

// Return the enriched shop data
return {
...shop,
itemData: itemApiData.data || {},
userData: {
username: userApiData.data[0]?.username || "",
discordId: userApiData.data[0]?.discordId || "",
profilePicture: profilePicture || "",
},
};
})
)
);

res.send({
success: true,
data: modifiedShops,
});
} catch (fetchError) {
console.error("Error processing shop data:", fetchError);
res.send({
success: false,
message: "Error processing shop data.",
});
}
const dbQueryParams = [];
const whereParts = [];
let ownerId;

if (material) {
whereParts.push("(item LIKE ? OR item LIKE ?)");
dbQueryParams.push(`%${material}%`);
dbQueryParams.push(`%${material.toUpperCase().replace(/ /g, "_")}%`);
}

if (owner) {
const ownerResults = await runQuery(
"SELECT id FROM users WHERE username = ?",
[owner]
);
ownerId = ownerResults?.[0]?.id;

resolve();
if (!ownerId) {
console.info("[Shop API] Owner filter returned no results", {
owner,
});
res.send({
success: true,
data: [],
message: "No shops available for that owner.",
meta: {
total: 0,
limit: limitValue,
offset: offsetValue,
hasMore: false,
},
});
return;
}
}

if (!includeOutOfStockValue) {
whereParts.push("stock > 0");
}

if (ownerId) {
whereParts.push("userId = ?");
dbQueryParams.push(ownerId);
}

const whereClause = whereParts.length
? `WHERE ${whereParts.join(" AND ")}`
: "";

const dataQuery = `SELECT * FROM shoppingDirectory ${whereClause} ORDER BY id DESC LIMIT ? OFFSET ?`;

const countQuery = `SELECT COUNT(*) as total FROM shoppingDirectory ${whereClause}`;

const totalResults = await runQuery(countQuery, dbQueryParams);
const total = totalResults?.[0]?.total || 0;

const shopResults = await runQuery(dataQuery, [
...dbQueryParams,
limitValue,
offsetValue,
]);

console.info("[Shop API] Query results", {
total,
returned: shopResults.length,
hasMore: offsetValue + shopResults.length < total,
});

if (!shopResults.length) {
return res.send({
success: true,
data: [],
message: "There are no shops available.",
meta: {
total,
limit: limitValue,
offset: offsetValue,
hasMore: false,
},
});
}

// Construct the database query
const dbQuery = material
? `SELECT * FROM shoppingDirectory WHERE item LIKE '%${material}%' OR item LIKE '%${material
.toUpperCase()
.replace(/ /g, "_")}%';`
: `SELECT * FROM shoppingDirectory;`;
try {
const modifiedShops = await Promise.all(
shopResults.map((shop) =>
concurrencyLimit(async () => {
const itemName = shop.item.trim();

// Execute the query and process the shops
await getShops(dbQuery);
// Fetch item data from Craftdex
const itemFetchURL = `https://craftdex.onrender.com/search/${itemName}`;
let itemApiData = {};
try {
const itemResponse = await fetch(itemFetchURL);
if (itemResponse.ok) {
itemApiData = await itemResponse.json();
} else {
console.error("[Shop API] Failed to fetch item data", {
status: itemResponse.status,
item: itemName,
});
}
} catch (itemError) {
console.error("[Shop API] Error fetching item data", {
error: itemError,
item: itemName,
});
}

// Fetch user data from internal API
const userFetchURL = `${process.env.siteAddress}/api/user/get?userId=${shop.userId}`;
let userApiData = {};
try {
const userResponse = await fetch(userFetchURL, {
headers: { "x-access-token": process.env.apiKey },
});
if (userResponse.ok) {
userApiData = await userResponse.json();
} else {
console.error("[Shop API] Failed to fetch user data", {
status: userResponse.status,
userId: shop.userId,
});
}
} catch (userError) {
console.error("[Shop API] Error fetching user data", {
error: userError,
userId: shop.userId,
});
}

// Get user profile picture
let profilePicture = "";
try {
profilePicture = await getProfilePicture(
userApiData.data?.[0]?.username
);
} catch (profileError) {
console.error("[Shop API] Error fetching profile picture", {
error: profileError,
username: userApiData.data?.[0]?.username,
});
}

// Return the enriched shop data
return {
...shop,
itemData: itemApiData.data || {},
userData: {
username: userApiData.data?.[0]?.username || "",
discordId: userApiData.data?.[0]?.discordId || "",
profilePicture: profilePicture || "",
},
};
})
)
);

res.send({
success: true,
data: modifiedShops,
meta: {
total,
limit: limitValue,
offset: offsetValue,
hasMore: offsetValue + modifiedShops.length < total,
},
});
} catch (fetchError) {
console.error("[Shop API] Error processing shop data", {
error: fetchError,
});
res.send({
success: false,
message: "Error processing shop data.",
});
}
} catch (error) {
console.error("Unhandled error:", error);
console.error("[Shop API] Unhandled error", {
error,
requestContext,
});
res.send({
success: false,
message: `${error}`,
Expand Down
Loading