Skip to content

refactor(utilities): unify list/detail serializer + ?fields= support (ALL-733)#208

Merged
texture-fleet-agent[bot] merged 1 commit intomainfrom
meridian/fix-list-serializer
May 6, 2026
Merged

refactor(utilities): unify list/detail serializer + ?fields= support (ALL-733)#208
texture-fleet-agent[bot] merged 1 commit intomainfrom
meridian/fix-list-serializer

Conversation

@texture-fleet-agent
Copy link
Copy Markdown
Contributor

@texture-fleet-agent texture-fleet-agent Bot commented May 6, 2026

Problem

Morgan's Relay recon (bug #2, 2026-05-06) flagged two related issues with /utilities:

  1. Shape drift — list and detail endpoints went through different code paths for sparse-fieldset projection (list had a local helper; detail had nothing), so they could silently diverge.
  2. No ?fields= on detail/utilities/{slug}?fields=id,name returned the full shape, so a client that opted into a sparse list couldn't pair it with a sparse detail.

At the Registered 5k/hr tier that forced any sync of 3,133 utilities to ~38 min of list-then-detail calls instead of ~2 sec.

Fix

Hoist parseFieldsParam + selectFields into lib/api/public-response.ts so every public route uses the same serializer pipeline, and extend both envelope builders to accept a { fields } option:

publicJsonResponse(utility, 200, {}, { fields: "id,slug,customerCount" });
publicPaginatedResponse(rows, meta, 200, {}, { fields: "id,slug,customerCount" });

The option accepts either the raw ?fields= string or a pre-parsed string[]. Order is enforced: stripInternal → selectFields, so internal fields (e.g. searchVector, submittedBy) can never be resurrected via ?fields=searchVector.

Wiring:

  • GET /api/v1/utilities/{slug} now reads ?fields= and passes it through publicJsonResponse.
  • GET /api/v1/utilities replaced its ad-hoc local selectFields with the shared helpers.

Tests

lib/api/__tests__/public-response.test.ts now covers:

  • parseFieldsParam — null/empty/whitespace handling, comma/whitespace splitting, de-duping with preserved order.
  • selectFields — existing-keys-only, null/0 values preserved (not confused with missing), non-object inputs passed through.
  • publicJsonResponse + publicPaginatedResponse with ?fields= — happy path, pre-parsed array input, internal-field resurrection guard, omitted-param fallthrough.
  • List/detail shape parity — for the same input row, publicJsonResponse(row) and publicPaginatedResponse([row]) produce identical keys and values; numeric fields (customerCount, totalMeterCount, amiMeterCount, peakDemandMw) survive both envelopes; ?fields=id,slug,name,customerCount yields identical projections on both.

All 126 tests pass. npm run build is green. biome check on touched files is clean.

Fixes ALL-733.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
commongrid Ready Ready Preview, Comment May 6, 2026 11:18pm

Request Review

…(ALL-733)

Problem: The /utilities list endpoint and /utilities/{slug} detail endpoint
used different code paths for sparse-fieldset projection. The list route had
a local selectFields helper; the detail route had no sparse-fieldset support
at all. This meant:

  - Detail endpoint couldn't honor ?fields= (returned the full shape).
  - List endpoint's sparse projection wasn't reused anywhere else.
  - No invariant that list and detail produce the same per-record shape
    when given the same inputs.

Morgan caught the end-user impact in the Relay recon (2026-05-06, bug #2):
for a 3,133-utility sync at the Registered 5k/hr tier, having to fall back
to list-then-detail was ~38 min instead of ~2 sec.

Fix:
- Hoist selectFields + parseFieldsParam into lib/api/public-response.ts so
  every public route uses the same serializer pipeline.
- Extend publicJsonResponse and publicPaginatedResponse with an optional
  { fields } option (accepts raw ?fields= string or pre-parsed string[]).
- Enforce order: stripInternal → selectFields. Internal fields can never be
  resurrected via ?fields=searchVector etc.
- Wire ?fields= into /utilities/{slug}.
- Swap the ad-hoc selectFields in /utilities route.ts for the shared helpers.

Regression tests (lib/api/__tests__/public-response.test.ts):
- parseFieldsParam: null/empty/whitespace handling, de-duping w/ preserved
  order.
- selectFields: existing-keys-only, null/0 preserved, non-object passthrough.
- publicJsonResponse + publicPaginatedResponse with ?fields=.
- Internal-field resurrection guard.
- List/detail shape parity: same keys, same numeric values, same ?fields=
  projection across both envelopes.

Fixes ALL-733
@texture-fleet-agent texture-fleet-agent Bot force-pushed the meridian/fix-list-serializer branch from 4fd0353 to 5975206 Compare May 6, 2026 23:16
@texture-fleet-agent texture-fleet-agent Bot changed the title refactor(utilities): unify list/detail serializer + fix numeric fields (ALL-733) refactor(utilities): unify list/detail serializer + ?fields= support (ALL-733) May 6, 2026
@texture-fleet-agent texture-fleet-agent Bot merged commit 7577145 into main May 6, 2026
7 checks passed
@texture-fleet-agent texture-fleet-agent Bot deleted the meridian/fix-list-serializer branch May 6, 2026 23:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant