[object Object]

← back to Nationalrealestate

fix(sublease): reject non-finite numerics + malformed dates in admin insert

289894d12922e98c332a8a5a0be54d5df0817d92 · 2026-07-24 08:20:59 -0700 · Steve Abrams

Bad numeric input reached Postgres verbatim: Number('abc')=NaN → term_months (integer) 500s
('invalid input syntax for type integer: NaN'), and sqft/asking_rate/lat/lng (numeric/double)
silently stored literal NaN, poisoning sorts + the map. available_date took any string → date
column 500 on a bad value. Now coerced to null (num/int/isoDate helpers) so bad input degrades
to unset instead of a 500 or a NaN row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 289894d12922e98c332a8a5a0be54d5df0817d92
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Jul 24 08:20:59 2026 -0700

    fix(sublease): reject non-finite numerics + malformed dates in admin insert
    
    Bad numeric input reached Postgres verbatim: Number('abc')=NaN → term_months (integer) 500s
    ('invalid input syntax for type integer: NaN'), and sqft/asking_rate/lat/lng (numeric/double)
    silently stored literal NaN, poisoning sorts + the map. available_date took any string → date
    column 500 on a bad value. Now coerced to null (num/int/isoDate helpers) so bad input degrades
    to unset instead of a 500 or a NaN row.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 src/server/sublease.ts | 33 +++++++++++++++++++++++++++------
 1 file changed, 27 insertions(+), 6 deletions(-)

diff --git a/src/server/sublease.ts b/src/server/sublease.ts
index 0a24b7f..c04faab 100644
--- a/src/server/sublease.ts
+++ b/src/server/sublease.ts
@@ -16,26 +16,47 @@
 import type { Express } from 'express';
 import { query } from '../../db/pool.ts';
 
+// Numeric coercers: a non-numeric value (e.g. "abc") must NOT reach Postgres.
+// Number("abc") === NaN → an integer column (term_months) 500s ("invalid input
+// syntax for type integer: NaN"), and a numeric/double column (sqft/asking_rate/
+// lat/lng) silently STORES literal NaN, poisoning sorts and the map. Coerce
+// non-finite input to null so bad input degrades to "unset", never a 500 or NaN row.
+const num = (v: unknown): number | null => {
+  if (v === '' || v == null) return null;
+  const n = Number(v);
+  return Number.isFinite(n) ? n : null;
+};
+const int = (v: unknown): number | null => {
+  const n = num(v);
+  return n == null ? null : Math.round(n);
+};
+// A `date` column rejects a malformed string with a 500; require an ISO-ish date, else null.
+const isoDate = (v: unknown): string | null => {
+  if (v === '' || v == null) return null;
+  const s = String(v).trim();
+  return /^\d{4}-\d{2}-\d{2}/.test(s) && !Number.isNaN(Date.parse(s)) ? s : null;
+};
+
 // Columns the admin form may set, mapped to their coercers. Anything else is ignored.
 const FIELDS: Record<string, (v: unknown) => unknown> = {
   address: v => String(v).trim(),
   city: v => (v == null ? null : String(v).trim()),
   submarket: v => (v == null ? null : String(v).trim()),
   floor_suite: v => (v == null ? null : String(v).trim()),
-  sqft: v => (v === '' || v == null ? null : Number(v)),
-  asking_rate: v => (v === '' || v == null ? null : Number(v)),
+  sqft: num,
+  asking_rate: num,
   rate_period: v => (['yr', 'mo'].includes(String(v)) ? String(v) : 'yr'),
   lease_type: v => (['sublease', 'direct'].includes(String(v)) ? String(v) : 'sublease'),
-  term_months: v => (v === '' || v == null ? null : Math.round(Number(v))),
-  available_date: v => (v === '' || v == null ? null : String(v)),
+  term_months: int,
+  available_date: isoDate,
   sublandlord: v => (v == null ? null : String(v).trim()),
   firm_name: v => (v == null ? null : String(v).trim()),
   broker_name: v => (v == null ? null : String(v).trim()),
   broker_phone: v => (v == null ? null : String(v).trim()),
   broker_email: v => (v == null ? null : String(v).trim()),
   listing_url: v => (v == null ? null : String(v).trim()),
-  lat: v => (v === '' || v == null ? null : Number(v)),
-  lng: v => (v === '' || v == null ? null : Number(v)),
+  lat: num,
+  lng: num,
   status: v => (['active', 'leased', 'withdrawn'].includes(String(v)) ? String(v) : 'active'),
   notes: v => (v == null ? null : String(v).trim()),
 };

← d7819af auth: add Scott login  ·  back to Nationalrealestate  ·  Add hourly self-balancing ingest loop + Google Places discov 8b31083 →