|
1 | 1 | //var sizeOf = require("image-size"); |
2 | 2 | const axios = require("axios"); |
3 | 3 |
|
| 4 | +function extractArrayData(payload) { |
| 5 | + try { |
| 6 | + if (!payload || !payload.success) return null; |
| 7 | + const data = payload.success.data; |
| 8 | + if (Array.isArray(data)) return data; |
| 9 | + if (data && data.type === "FeatureCollection" && Array.isArray(data.features)) { |
| 10 | + return data.features; |
| 11 | + } |
| 12 | + return null; |
| 13 | + } catch { |
| 14 | + return null; |
| 15 | + } |
| 16 | +} |
| 17 | +// Returns the object to scan for keys (GeoJSON Feature.properties if present) |
| 18 | +function surfaceForIdScan(item) { |
| 19 | + if (!item) return null; |
| 20 | + if (item.properties && typeof item.properties === "object") return item.properties; |
| 21 | + return item; |
| 22 | +} |
| 23 | + |
| 24 | +// Find the first *_id key by insertion order on a single item |
| 25 | +function firstStarIdKey(item) { |
| 26 | + const base = surfaceForIdScan(item); |
| 27 | + if (!base || typeof base !== "object") return null; |
| 28 | + for (const k of Object.keys(base)) { |
| 29 | + if (/_id$/i.test(k)) return k; // first *_id encountered wins |
| 30 | + } |
| 31 | + if (Object.prototype.hasOwnProperty.call(base, "id")) return "id"; |
| 32 | + return null; |
| 33 | +} |
| 34 | + |
| 35 | +// Does at least one item in `arr` expose `key` (on root or Feature.properties)? |
| 36 | +function arrayExposesKey(arr, key, sampleSize = 50) { |
| 37 | + const n = Math.min(arr.length, sampleSize); |
| 38 | + for (let i = 0; i < n; i++) { |
| 39 | + const it = arr[i]; |
| 40 | + if (!it) continue; |
| 41 | + const props = it.properties && typeof it.properties === "object" ? it.properties : null; |
| 42 | + if ((props && Object.prototype.hasOwnProperty.call(props, key)) || |
| 43 | + Object.prototype.hasOwnProperty.call(it, key)) { |
| 44 | + return true; |
| 45 | + } |
| 46 | + } |
| 47 | + return false; |
| 48 | +} |
| 49 | + |
| 50 | +// Choose a shared id key by prioritizing the FIRST *_id in the objects. |
| 51 | +function chooseCommonIdKey(localArr, prodArr) { |
| 52 | + // 1) Prefer first *_id on the FIRST local item (insertion order) |
| 53 | + const localFirst = firstStarIdKey(localArr[0]); |
| 54 | + if (localFirst && arrayExposesKey(prodArr, localFirst)) return localFirst; |
| 55 | + |
| 56 | + // 2) Otherwise try first *_id on the FIRST prod item |
| 57 | + const prodFirst = firstStarIdKey(prodArr[0]); |
| 58 | + if (prodFirst && arrayExposesKey(localArr, prodFirst)) return prodFirst; |
| 59 | + |
| 60 | + // 3) (Optional) fall back to legacy heuristic or null |
| 61 | + return null; |
| 62 | +} |
| 63 | + |
| 64 | + |
| 65 | +// Build a set of unique IDs from an array given a chosen id key. |
| 66 | +// Supports both flat objects and GeoJSON Feature properties. |
| 67 | +function uniqueIds(arr, idKey) { |
| 68 | + const out = new Set(); |
| 69 | + for (const item of arr) { |
| 70 | + if (!item) continue; |
| 71 | + // Prefer .properties[idKey] if available (GeoJSON Feature) |
| 72 | + const props = item.properties && typeof item.properties === "object" ? item.properties : null; |
| 73 | + if (props && Object.prototype.hasOwnProperty.call(props, idKey)) { |
| 74 | + out.add(props[idKey]); |
| 75 | + continue; |
| 76 | + } |
| 77 | + if (Object.prototype.hasOwnProperty.call(item, idKey)) { |
| 78 | + out.add(item[idKey]); |
| 79 | + continue; |
| 80 | + } |
| 81 | + // Handle plain "id" on the Feature object itself (rare but possible) |
| 82 | + if (idKey === "id" && Object.prototype.hasOwnProperty.call(item, "id")) { |
| 83 | + out.add(item.id); |
| 84 | + } |
| 85 | + } |
| 86 | + return out; |
| 87 | +} |
| 88 | + |
| 89 | + |
| 90 | + |
4 | 91 | module.exports = { |
5 | 92 | aSuccessfulRequest: function (res: { |
6 | 93 | statusCode: number; |
@@ -201,20 +288,52 @@ module.exports = { |
201 | 288 | ); |
202 | 289 | }, |
203 | 290 |
|
204 | | - async compareWithProduction(queryParams = "", localResponse: any) { |
| 291 | + |
| 292 | + async compareWithProduction(queryParams = "", localResponse) { |
205 | 293 | const prodUrl = `https://www.macrostrat.org/api/v2${queryParams}`; |
206 | | - const externalResponse = await axios.get(prodUrl); |
207 | | - if ( |
208 | | - JSON.stringify(localResponse.body) !== |
209 | | - JSON.stringify(externalResponse.data) |
210 | | - ) { |
211 | | - throw new Error( |
212 | | - `Mismatch for endpoint: ${queryParams}\nLocal: ${JSON.stringify( |
213 | | - localResponse.body, |
214 | | - null, |
215 | | - 2, |
216 | | - )}\nProduction: ${JSON.stringify(externalResponse.data, null, 2)}`, |
217 | | - ); |
| 294 | + const { data: prodData } = await axios.get(prodUrl); |
| 295 | + |
| 296 | + // Exact JSON match still passes quickly. |
| 297 | + if (JSON.stringify(localResponse.body) === JSON.stringify(prodData)) { |
| 298 | + return; |
| 299 | + } |
| 300 | + |
| 301 | + // Lenient path for array-like payloads |
| 302 | + const localArr = extractArrayData(localResponse.body); |
| 303 | + const prodArr = extractArrayData(prodData); |
| 304 | + |
| 305 | + if (Array.isArray(localArr) && Array.isArray(prodArr) && localArr.length && prodArr.length) { |
| 306 | + // Auto-detect a shared *_id (or "id") to compare by counts |
| 307 | + const idKey = chooseCommonIdKey(localArr, prodArr); |
| 308 | + |
| 309 | + if (idKey) { |
| 310 | + console.info(`[compareWithProduction] Using id key: ${idKey}`); |
| 311 | + const localIds = uniqueIds(localArr, idKey); |
| 312 | + const prodIds = uniqueIds(prodArr, idKey); |
| 313 | + const localCount = localIds.size; |
| 314 | + const prodCount = prodIds.size; |
| 315 | + |
| 316 | + if (localCount !== prodCount) { |
| 317 | + console.warn( |
| 318 | + [ |
| 319 | + `⚠️ ${idKey} count mismatch for endpoint: ${queryParams}`, |
| 320 | + ` - Dev (current host) ${idKey} count: ${localCount}`, |
| 321 | + ` - Prod (host_prod) ${idKey} count: ${prodCount}`, |
| 322 | + ].join("\n") |
| 323 | + ); |
| 324 | + |
| 325 | + return; |
| 326 | + } |
| 327 | + // If counts match but payloads differ, fall through to strict error to surface real mismatches. |
| 328 | + } |
| 329 | + // If we couldn't find a shared idKey, we’ll fall back to strict diff below. |
218 | 330 | } |
| 331 | + |
| 332 | + // Strict mismatch error with helpful diff |
| 333 | + throw new Error( |
| 334 | + `Mismatch for endpoint: ${queryParams}\n` + |
| 335 | + `Local: ${JSON.stringify(localResponse.body, null, 2)}\n` + |
| 336 | + `Production: ${JSON.stringify(prodData, null, 2)}` |
| 337 | + ); |
219 | 338 | }, |
220 | 339 | }; |
0 commit comments