DEV Community

Roberto Luna
Roberto Luna

Posted on

Fixing the “D.map is not a function” crash by tightening DB indexes and normalizing the API payload

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))
Enter fullscreen mode Exit fullscreen mode

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

  1. Guarding the Front‑end – I added a quick check in CondoSelector.tsx:
const options = Array.isArray(data) ? data : [];
Enter fullscreen mode Exit fullscreen mode

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.

  1. 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]);
Enter fullscreen mode Exit fullscreen mode

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;
+`);
Enter fullscreen mode Exit fullscreen mode

Why these indexes?

  • uq_broker_tokens guarantees a single token per broker, eliminating the duplicate rows that broke the metrics aggregation.
  • idx_condo_metrics_condo_id speeds up the join that builds the selector payload, reducing the chance of timeouts that previously left the query partially resolved (hence null).
  • 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' });
  }
};
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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/metrics that 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)