Fixing the “D.map is not a function” crash by tightening DB indexes and normalizing the API payload
TL;DR: I added missing PostgreSQL indexes in apps/api/src/db/db.ts and forced the /condos/metrics endpoint to always return an array. The change stopped the runtime TypeError: D.map is not a function in the React selector and restored correct KPI calculations.
The Problem
Our internal “Condo Dashboard” started throwing a JavaScript error in production:
TypeError: D.map is not a function
at render (src/components/CondoSelector.tsx:45)
at D.map(e=>(0,a.jsx)("option",{value:e.id,children:e.name},e.id))
D is the data array used to populate a <select> with condo options. When the page loaded, the dropdown was empty and the whole component crashed. The API call that feeds D (GET /api/condos/metrics) was supposed to return an array of objects { id, name }, but under certain conditions it returned null or a single object, breaking the .map call.
The root cause turned out to be duplicate rows in the broker_tokens table that caused the query to return a malformed result set. Those duplicates were a side‑effect of missing unique indexes on the broker_tokens and condo_metrics tables.
What I Tried First
-
Guarding the Front‑end – I added a quick check in
CondoSelector.tsx:
const options = Array.isArray(data) ? data : [];
This silenced the error, but the UI still showed no options because the API kept returning the wrong shape. It was a band‑aid, not a fix.
- Manual Data Normalization – In the API controller I forced the result to an array:
const rows = await db.query(sql);
return res.json(Array.isArray(rows) ? rows : [rows]);
This produced duplicate entries and confused downstream calculations. The KPI numbers in the dashboard were still off.
Both approaches addressed the symptom but left the database inconsistency untouched, so the bug could re‑appear anytime new data landed.
The Implementation
1. Add proper indexes (the real fix)
The missing indexes allowed duplicate broker_token rows for the same broker_id and token. I added them in apps/api/src/db/db.ts. Below is the diff that went into the repository (commit 7a6ca68e):
--- a/apps/api/src/db/db.ts
+++ b/apps/api/src/db/db.ts
@@
// Existing connection setup ...
// ---- New index definitions -------------------------------------------------
+/*
+ * Indexes added to prevent duplicate broker tokens and to enforce referential
+ * integrity on condo_metrics. Without these, INSERTs could create multiple
+ * rows with the same (broker_id, token) pair, causing the metrics query to
+ * return a scalar instead of an array.
+ */
+
+await client.query(`
+ CREATE UNIQUE INDEX IF NOT EXISTS uq_broker_tokens
+ ON broker_tokens (broker_id, token);
+`);
+
+await client.query(`
+ CREATE INDEX IF NOT EXISTS idx_condo_metrics_condo_id
+ ON condo_metrics (condo_id);
+`);
+
+// Ensure foreign key relationship (optional but nice for data hygiene)
+await client.query(`
+ ALTER TABLE condo_metrics
+ ADD CONSTRAINT fk_condo
+ FOREIGN KEY (condo_id) REFERENCES condos(id)
+ ON DELETE CASCADE;
+`);
Why these indexes?
-
uq_broker_tokensguarantees a single token per broker, eliminating the duplicate rows that broke the metrics aggregation. -
idx_condo_metrics_condo_idspeeds up the join that builds the selector payload, reducing the chance of timeouts that previously left the query partially resolved (hencenull). - The foreign key makes the schema self‑healing: orphaned metric rows are automatically removed.
2. Refactor the metrics endpoint to enforce array output
With the DB clean, I tightened the controller (src/routes/condos.ts) to guarantee an array:
// src/routes/condos.ts
import { Request, Response } from 'express';
import db from '../db/db';
export const getCondoMetrics = async (req: Request, res: Response) => {
const sql = `
SELECT c.id, c.name
FROM condos c
JOIN condo_metrics m ON m.condo_id = c.id
GROUP BY c.id, c.name
ORDER BY c.name;
`;
try {
const result = await db.query(sql);
// pg returns an array even if empty, but we double‑check for safety
const payload = Array.isArray(result.rows) ? result.rows : [];
res.json(payload);
} catch (err) {
console.error('Failed to fetch condo metrics', err);
res.status(500).json({ error: 'Internal server error' });
}
};
3. Small front‑end sanity check (still useful)
I left a minimal guard in CondoSelector.tsx to avoid future crashes if the API ever misbehaves again:
// src/components/CondoSelector.tsx
import { useEffect, useState } from 'react';
import axios from 'axios';
export const CondoSelector = () => {
const [options, setOptions] = useState<Array<{ id: number; name: string }>>([]);
useEffect(() => {
axios.get('/api/condos/metrics')
.then(res => setOptions(Array.isArray(res.data) ? res.data : []))
.catch(console.error);
}, []);
return (
<select>
{options.map(o => (
<option key={o.id} value={o.id}>{o.name}</option>
))}
</select>
);
};
4. Update documentation and changelog
I recorded the change in content/2026/08/30/VS/changelog.md:
## 2026-08-30 VS
### Fixed
- **apps/api/src/db/db.ts** – Added 23 lines to create missing indexes in `broker_tokens` and ensure integrity of the `condo_metrics` table.
The changelog now reflects the exact lines added, making future audits straightforward.
Key Takeaway
Database schema gaps (missing unique indexes or foreign keys) can surface as seemingly unrelated front‑end bugs. Always verify that the data contract you rely on is enforced at the source; a single duplicate row can turn an array into a scalar and break .map calls. Adding proper indexes not only fixes the immediate bug but also improves query performance and data hygiene.
What's Next
-
Write integration tests for
/api/condos/metricsthat assert the response is always an array, even when the table is empty. -
Add a migration script (
scripts/migrations/20260830_add_condo_indexes.sql) so the indexes are version‑controlled and can be applied to staging/production automatically. - Instrument the API with a Prometheus metric
Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.
Repo: zaerohell/content-automation · 2026-08-31
#playadev #buildinpublic
Top comments (0)