Skip to content

Commit 3758e96

Browse files
authored
Harden API error handling and guard value extractors against missing fields (#14)
1 parent d2c29cd commit 3758e96

1 file changed

Lines changed: 93 additions & 54 deletions

File tree

index.js

Lines changed: 93 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -58,19 +58,27 @@ main();
5858

5959
async function main() {
6060
// Fetch Game Pass game ID's and properties for each pass type and market specified in the configuration
61-
// We do this in parallel to speed up the process
62-
// While the functions do return the formatted properties, we currently do not use them here, as writing the output files is handled by the functions themselves
61+
// The tasks run in parallel to speed up the process
62+
// Each one writes its own output file
63+
const tasks = [];
6364
for (const market of CONFIG.markets) {
64-
if (CONFIG.platformsToFetch.includes("console")) {
65-
const consoleFormattedProperties = runScriptForPassTypeAndMarket("console", market);
66-
}
67-
if (CONFIG.platformsToFetch.includes("pc")) {
68-
const pcFormattedProperties = runScriptForPassTypeAndMarket("pc", market);
69-
}
70-
if (CONFIG.platformsToFetch.includes("eaPlay")) {
71-
const eaPlayFormattedProperties = runScriptForPassTypeAndMarket("eaPlay", market);
65+
for (const passType of ["console", "pc", "eaPlay"]) {
66+
if (CONFIG.platformsToFetch.includes(passType)) {
67+
tasks.push(
68+
runScriptForPassTypeAndMarket(passType, market).catch((error) => {
69+
console.error(`\nError fetching ${passType} games for market "${market}": ${error.message ?? error}`);
70+
return { failed: true };
71+
})
72+
);
73+
}
7274
}
7375
}
76+
77+
const failures = (await Promise.all(tasks)).filter((result) => result && result.failed).length;
78+
if (failures > 0) {
79+
console.error(`\n${failures} of ${tasks.length} fetch task(s) failed. See the errors above.`);
80+
process.exit(1);
81+
}
7482
}
7583

7684
async function runScriptForPassTypeAndMarket(passType, market) {
@@ -94,32 +102,50 @@ async function fetchGameIDs(passType, market) {
94102
}
95103

96104
console.log(`Fetching ${passType} Game Pass game ID's for market "${market}"...`);
97-
let gameIds = await fetch(`https://catalog.gamepass.com/sigls/v2?id=${APIIds[passType]}&language=${CONFIG.language}&market=${market}`)
98-
.then((response) => response.json())
99-
.then((data) => data.filter((entry) => entry.id).map((entry) => entry.id));
105+
const response = await fetch(`https://catalog.gamepass.com/sigls/v2?id=${APIIds[passType]}&language=${CONFIG.language}&market=${market}`);
106+
if (!response.ok) {
107+
throw new Error(`The Game Pass catalog API responded with status ${response.status}${response.statusText ? ` ${response.statusText}` : ""}.`);
108+
}
109+
110+
let data;
111+
try {
112+
data = await response.json();
113+
} catch (error) {
114+
throw new Error(`Could not parse the Game Pass catalog API response as JSON: ${error.message ?? error}`);
115+
}
100116

101-
return gameIds;
117+
return data.filter((entry) => entry.id).map((entry) => entry.id);
102118
}
103119

104120
async function fetchGameProperties(gameIds, passType, market) {
105121
console.log(`Fetching game properties for ${gameIds.length} ${passType} games for market "${market}"...`);
106-
return await fetch(`https://displaycatalog.mp.microsoft.com/v7.0/products?bigIds=${gameIds}&market=${market}&languages=${CONFIG.language}`)
107-
.then((response) => response.json())
108-
.then((data) => {
109-
if (CONFIG.keepCompleteProperties) {
110-
fs.writeFileSync(`./output/completeGameProperties_${passType}_${market}.json`, JSON.stringify(data, null, 2));
111-
}
112-
return data;
113-
});
122+
const response = await fetch(`https://displaycatalog.mp.microsoft.com/v7.0/products?bigIds=${gameIds}&market=${market}&languages=${CONFIG.language}`);
123+
if (!response.ok) {
124+
throw new Error(`The Microsoft display catalog API responded with status ${response.status}${response.statusText ? ` ${response.statusText}` : ""}.`);
125+
}
126+
127+
let data;
128+
try {
129+
data = await response.json();
130+
} catch (error) {
131+
throw new Error(`Could not parse the Microsoft display catalog API response as JSON: ${error.message ?? error}`);
132+
}
133+
134+
if (CONFIG.keepCompleteProperties) {
135+
fs.writeFileSync(`./output/completeGameProperties_${passType}_${market}.json`, JSON.stringify(data, null, 2));
136+
}
137+
138+
return data;
114139
}
115140

116141
// Format the data according to the configuration
117142
function formatData(gameProperties, passType) {
118-
console.log(`Formatting game properties for ${gameProperties.Products.length} ${passType} games...`);
143+
const products = gameProperties.Products ?? [];
144+
console.log(`Formatting game properties for ${products.length} ${passType} games...`);
119145

120146
let formattedData = CONFIG.outputFormat === "array" ? [] : {};
121147

122-
for (const game of gameProperties.Products) {
148+
for (const game of products) {
123149
let index;
124150
switch (CONFIG.outputFormat) {
125151
case "array":
@@ -202,42 +228,42 @@ function getPropertyValue(game, property, propertyValue) {
202228
function getProductTitle(game, productTitleProperty) {
203229
if (!productTitleProperty) { return undefined; }
204230

205-
return game.LocalizedProperties[0].ProductTitle.length > 0
231+
return game.LocalizedProperties?.[0]?.ProductTitle?.length > 0
206232
? game.LocalizedProperties[0].ProductTitle
207233
: emptyValuePlaceholder;
208234
}
209235

210236
function getProductId(game, productIdProperty) {
211237
if (!productIdProperty) { return undefined; }
212238

213-
return game.ProductId.length > 0
239+
return game.ProductId?.length > 0
214240
? game.ProductId
215241
: emptyValuePlaceholder;
216242
}
217243

218244
function getDeveloperName(game, developerNameProperty) {
219245
if (!developerNameProperty) { return undefined; }
220246

221-
return game.LocalizedProperties[0].DeveloperName.length > 0
247+
return game.LocalizedProperties?.[0]?.DeveloperName?.length > 0
222248
? game.LocalizedProperties[0].DeveloperName
223249
: emptyValuePlaceholder;
224250
}
225251

226252
function getPublisherName(game, publisherNameProperty) {
227253
if (!publisherNameProperty) { return undefined; }
228254

229-
return game.LocalizedProperties[0].PublisherName.length > 0
255+
return game.LocalizedProperties?.[0]?.PublisherName?.length > 0
230256
? game.LocalizedProperties[0].PublisherName
231257
: emptyValuePlaceholder;
232258
}
233259

234260
function getProductDescription(game, productDescriptionProperty) {
235261
if (!productDescriptionProperty.enabled) { return undefined; }
236262

237-
if (productDescriptionProperty.preferShort && game.LocalizedProperties[0].ShortDescription?.length > 0) {
263+
if (productDescriptionProperty.preferShort && game.LocalizedProperties?.[0]?.ShortDescription?.length > 0) {
238264
return game.LocalizedProperties[0].ShortDescription;
239265
} else {
240-
return game.LocalizedProperties[0].ProductDescription?.length > 0
266+
return game.LocalizedProperties?.[0]?.ProductDescription?.length > 0
241267
? game.LocalizedProperties[0].ProductDescription
242268
: emptyValuePlaceholder;
243269
}
@@ -259,29 +285,37 @@ function getImages(game, imageProperty) {
259285
"FeaturePromotionalSquareArt": 0
260286
};
261287

262-
for (const image of game.LocalizedProperties[0].Images) {
263-
if (imageProperty.imageTypes[image.ImagePurpose] && (imageProperty.imageTypes[image.ImagePurpose] === -1 || numImagesByType[image.ImagePurpose] < imageProperty.imageTypes[image.ImagePurpose])) {
264-
if (!images[image.ImagePurpose]) {
265-
images[image.ImagePurpose] = [];
266-
}
267-
images[image.ImagePurpose].push(image.Uri.startsWith('https:') ? image.Uri : `https:${image.Uri}`);
268-
numImagesByType[image.ImagePurpose] = numImagesByType[image.ImagePurpose] ? numImagesByType[image.ImagePurpose] + 1 : 1;
288+
for (const image of game.LocalizedProperties?.[0]?.Images ?? []) {
289+
const limit = imageProperty.imageTypes[image.ImagePurpose];
290+
if (!limit || (limit !== -1 && numImagesByType[image.ImagePurpose] >= limit)) {
291+
continue;
269292
}
293+
294+
const uri = image.Uri.startsWith('https:') ? image.Uri : `https:${image.Uri}`;
295+
296+
// Skip true duplicates - the same URL can appear more than once in the API response
297+
if (images[image.ImagePurpose]?.includes(uri)) {
298+
continue;
299+
}
300+
301+
(images[image.ImagePurpose] ??= []).push(uri);
302+
numImagesByType[image.ImagePurpose] = (numImagesByType[image.ImagePurpose] ?? 0) + 1;
270303
}
271304
return images;
272305
}
273306

274307
function getReleaseDate(game, releaseDateProperty) {
275308
if (!releaseDateProperty.enabled) { return undefined; }
276309

310+
const releaseDate = game.MarketProperties?.[0]?.OriginalReleaseDate;
311+
if (!releaseDate || releaseDate.length === 0) {
312+
return emptyValuePlaceholder;
313+
}
314+
277315
if (releaseDateProperty.format === "date") {
278-
return game.MarketProperties[0].OriginalReleaseDate.length > 0
279-
? game.MarketProperties[0].OriginalReleaseDate?.split("T")[0]
280-
: emptyValuePlaceholder;
316+
return releaseDate.split("T")[0];
281317
} else if (releaseDateProperty.format === "dateTime") {
282-
return game.MarketProperties[0].OriginalReleaseDate.length > 0
283-
? game.MarketProperties[0].OriginalReleaseDate
284-
: emptyValuePlaceholder;
318+
return releaseDate;
285319
} else {
286320
// Due to our config validation, this should never happen, but just in case...
287321
console.log(`Invalid release date format: ${releaseDateProperty.format}`);
@@ -299,7 +333,12 @@ function getUserRating(game, userRatingProperty) {
299333
}
300334

301335
// Get the x-out-of-5 stars rating
302-
let userRating = game.MarketProperties[0].UsageData[intervalMapping[userRatingProperty.aggregationInterval]]?.AverageRating;
336+
let userRating = game.MarketProperties?.[0]?.UsageData?.[intervalMapping[userRatingProperty.aggregationInterval]]?.AverageRating;
337+
338+
// Games without any rating data for the requested interval
339+
if (typeof userRating !== "number") {
340+
return emptyValuePlaceholder;
341+
}
303342

304343
// Convert to a percentage if requested
305344
if (userRatingProperty.format === "percentage") {
@@ -330,12 +369,13 @@ function getPricing(game, pricingProperty) {
330369
}
331370

332371
let prices = {};
333-
prices["currencyCode"] = game.DisplaySkuAvailabilities[0]?.Availabilities[0]?.OrderManagementData?.Price?.CurrencyCode;
372+
const price = game.DisplaySkuAvailabilities?.[0]?.Availabilities?.[0]?.OrderManagementData?.Price;
373+
prices["currencyCode"] = price?.CurrencyCode;
334374

335375
for (const priceType of pricingProperty.priceTypes) {
336376
// Small workaround to not exclude 0-values
337-
prices[priceType] = typeof game.DisplaySkuAvailabilities[0].Availabilities[0].OrderManagementData.Price[priceType] === 'number'
338-
? game.DisplaySkuAvailabilities[0].Availabilities[0].OrderManagementData.Price[priceType]
377+
prices[priceType] = typeof price?.[priceType] === 'number'
378+
? price[priceType]
339379
: missingPricePlaceholder;
340380
}
341381

@@ -345,13 +385,12 @@ function getPricing(game, pricingProperty) {
345385
function getCategories(game, categoriesProperty) {
346386
if (!categoriesProperty) { return undefined; }
347387

348-
let categories = [];
349-
if (game.Properties.Categories) {
350-
categories = game.Properties.Categories;
351-
}
388+
const properties = game.Properties ?? {};
389+
let categories = Array.isArray(properties.Categories) ? [...properties.Categories] : [];
390+
352391
// Each game also has a "main" category, which may or may not be included in the list of categories
353-
if (!categories.includes(game.Properties.Category)) {
354-
categories.push(game.Properties.Category);
392+
if (properties.Category && !categories.includes(properties.Category)) {
393+
categories.push(properties.Category);
355394
}
356395

357396
return categories;
@@ -360,7 +399,7 @@ function getCategories(game, categoriesProperty) {
360399
function getStorePageUrl(game, storePageUrlProperty) {
361400
if (!storePageUrlProperty) { return undefined; }
362401

363-
if(!game.LocalizedProperties[0].ProductTitle || !game.ProductId) {
402+
if (!game.LocalizedProperties?.[0]?.ProductTitle || !game.ProductId) {
364403
return undefined;
365404
}
366405

0 commit comments

Comments
 (0)