← back to Commercialrealestate
auto-save: 2026-06-27T05:37:39 (1 files) — scripts/sources/firms.js
e133dd48631d164e716cef62b23fb9b0885267fb · 2026-06-27 05:37:44 -0700 · Steve Abrams
Files touched
M scripts/sources/firms.js
Diff
commit e133dd48631d164e716cef62b23fb9b0885267fb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Jun 27 05:37:44 2026 -0700
auto-save: 2026-06-27T05:37:39 (1 files) — scripts/sources/firms.js
---
scripts/sources/firms.js | 44 ++++++++++++++++++++++++++++----------------
1 file changed, 28 insertions(+), 16 deletions(-)
diff --git a/scripts/sources/firms.js b/scripts/sources/firms.js
index 02fc8cd..4430987 100644
--- a/scripts/sources/firms.js
+++ b/scripts/sources/firms.js
@@ -52,14 +52,22 @@ const today = () => new Date().toISOString().slice(0, 10);
// Given any parsed JSON body from a firm's API, dig out an array of "asset-like" objects and map
// each to the common intermediate shape { id, price, city, address, cap, types, desc, urlSlug }.
// Heuristic but conservative: it only keeps objects that have BOTH a plausible price and a city.
+const ARRAY_KEYS = ['data', 'results', 'assets', 'items', 'listings', 'properties', 'hits', 'records', 'docs'];
const pickArray = (j) => {
if (Array.isArray(j)) return j;
- for (const k of ['data', 'results', 'assets', 'items', 'listings', 'properties', 'hits', 'records', 'docs']) {
- if (Array.isArray(j?.[k])) return j[k];
- if (Array.isArray(j?.[k]?.items)) return j[k].items; // {data:{items:[...]}}
+ if (!j || typeof j !== 'object') return [];
+ // Case-insensitive match against known container keys (M&M's .NET API uses "Results", etc.).
+ const lower = {}; for (const k of Object.keys(j)) lower[k.toLowerCase()] = k;
+ for (const want of ARRAY_KEYS) {
+ const k = lower[want]; if (k == null) continue;
+ const v = j[k];
+ if (Array.isArray(v)) return v;
+ if (v && typeof v === 'object') { // {Results:{Items:[...]}} / {data:{items:[...]}}
+ const inner = pickArray(v); if (inner.length) return inner;
+ }
}
- // Last resort: first array-of-objects value on the top level.
- for (const v of Object.values(j || {})) {
+ // Last resort: first non-empty array-of-objects value on the top level.
+ for (const v of Object.values(j)) {
if (Array.isArray(v) && v.length && typeof v[0] === 'object') return v;
}
return [];
@@ -67,14 +75,18 @@ const pickArray = (j) => {
const num = (v) => { const n = +String(v == null ? '' : v).replace(/[^0-9.]/g, ''); return Number.isFinite(n) && n > 0 ? n : null; };
-const firstStr = (obj, keys) => { for (const k of keys) { const v = obj?.[k]; if (typeof v === 'string' && v.trim()) return v.trim(); } return ''; };
+// Case-insensitive field access — many firm APIs (M&M's .NET, etc.) capitalize keys (ListPrice, City).
+const ciIndex = (o) => { const m = {}; if (o && typeof o === 'object') for (const k of Object.keys(o)) m[k.toLowerCase()] = o[k]; return m; };
+const ciStr = (o, names) => { const m = ciIndex(o); for (const n of names) { const v = m[n.toLowerCase()]; if (typeof v === 'string' && v.trim()) return v.trim(); } return ''; };
+const ciVal = (o, names) => { const m = ciIndex(o); for (const n of names) { const v = m[n.toLowerCase()]; if (v != null) return v; } return null; };
// Pull a {city,address} out of an asset that may nest them under locations[0] / address{} / fields.
const locOf = (a) => {
- const loc = (Array.isArray(a.locations) && a.locations[0]) || a.location || a.address || a.geo || a;
- const city = firstStr(loc, ['city', 'cityName', 'town']) || firstStr(a, ['city', 'cityName']);
- const address = firstStr(loc, ['address', 'addressLine1', 'street', 'line1', 'fullAddress', 'name'])
- || firstStr(a, ['address', 'name', 'title', 'displayAddress']);
+ const m = ciIndex(a);
+ const loc = (Array.isArray(m.locations) && m.locations[0]) || m.location || (m.address && typeof m.address === 'object' ? m.address : null) || m.geo || a;
+ const city = ciStr(loc, ['city', 'cityName', 'town']) || ciStr(a, ['city', 'cityName', 'town']);
+ const address = ciStr(loc, ['address', 'addressLine1', 'street', 'line1', 'fullAddress', 'streetAddress', 'name'])
+ || ciStr(a, ['address', 'streetAddress', 'addressLine1', 'name', 'title', 'displayAddress']);
return { city, address };
};
@@ -82,17 +94,17 @@ function genericExtract(json) {
const out = [];
for (const a of pickArray(json)) {
if (!a || typeof a !== 'object') continue;
- const price = num(a.askingPrice ?? a.price ?? a.listPrice ?? a.salePrice ?? a.priceValue ?? a.amount);
+ const price = num(ciVal(a, ['askingPrice', 'price', 'listPrice', 'salePrice', 'priceValue', 'amount']));
const { city, address } = locOf(a);
if (!price || !city || !address) continue;
- const id = String(a.id ?? a.assetId ?? a.listingId ?? a.propertyId ?? a.slug ?? address).slice(0, 64);
- const capRaw = a.capRate ?? a.cap ?? a.capRateValue ?? null;
+ const id = String(ciVal(a, ['id', 'assetId', 'listingId', 'propertyId', 'slug']) ?? address).slice(0, 64);
+ const capRaw = ciVal(a, ['capRate', 'cap', 'capRateValue']);
out.push({
id, price, city, address,
cap: typeof capRaw === 'number' ? capRaw : num(capRaw),
- urlSlug: firstStr(a, ['urlSlug', 'slug', 'url', 'detailUrl', 'permalink']),
- desc: firstStr(a, ['description', 'summary', 'remarks', 'subtitle']),
- types: a.types || a.propertyTypes || (a.propertyType ? [a.propertyType] : [])
+ urlSlug: ciStr(a, ['urlSlug', 'slug', 'url', 'detailUrl', 'permalink']),
+ desc: ciStr(a, ['description', 'summary', 'remarks', 'subtitle']),
+ types: ciVal(a, ['types', 'propertyTypes']) || (ciVal(a, ['propertyType']) ? [ciVal(a, ['propertyType'])] : [])
});
}
return out;
← 6594d54 CRE: qwen narratives backfilled for all 1028 properties (the
·
back to Commercialrealestate
·
CRE: case-insensitive generic sniffer (handles M&M/.NET capi c70d4a6 →