Skip to content

feat(openapi): auto-generate spec from Drizzle schema with CI drift check#203

Merged
texture-fleet-agent[bot] merged 1 commit intomainfrom
meridian/auto-generate-openapi
May 6, 2026
Merged

feat(openapi): auto-generate spec from Drizzle schema with CI drift check#203
texture-fleet-agent[bot] merged 1 commit intomainfrom
meridian/auto-generate-openapi

Conversation

@texture-fleet-agent
Copy link
Copy Markdown
Contributor

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

Summary

Auto-generate public/openapi.json from the Drizzle schema, so the published spec can never drift again. CI fails if the committed file doesn't match the generator output.

Fixes ALL-728.

Why this matters

The hand-written public/openapi.json (last touched 2026-04-23, PR #149) had drifted badly from the real API. A few highlights:

Resource Old spec fields New spec fields
Utility 14 37
PowerPlant 9 31
EvStation 11 26
Program 7 31
PricingNode 5 17
BalancingAuthority 4 17
Iso / Rto 5 / 4 14 / 14
Region 4 14
Substation 22
TransmissionLine 18
Territory 5 9

On top of field drift, several endpoint families shipped since April weren't documented at all. This PR adds them:

  • /substations, /substations/{slug}, /substations/{slug}/transmission-lines
  • /power-plants/{slug}/substations
  • /pricing-nodes/{slug}/versions, /programs/{slug}/versions
  • /territories/lookup
  • /changelog

Total: 19 schemas, 35 paths now documented.

How it works

  • scripts/generate-openapi.ts walks every Drizzle table via getTableColumns() and maps Postgres column types → OpenAPI 3.1 schemas.
  • Internal fields (submittedBy, reviewedAt, reviewedBy, lockedStatus, searchVector, notionPageId, geography, geometry) are stripped by default. Geometry endpoints return a GeoJSON envelope separately.
  • jsonb columns with known runtime shape (e.g. states: string[], assetTypes: string[]) get per-field overrides in scripts/openapi/resources.ts.
  • Query params in scripts/openapi/endpoints.ts mirror the actual Zod schemas / URL parsing from app/api/v1/**/route.ts — nothing fabricated.
  • Auth-required / internal endpoints (mod/*, developer/*, contributions, discussions, follows, notifications, me, editable-fields, webhooks, revalidate, health, tiles) are intentionally excluded from the v1 public spec.
  • Base metadata (info, servers, tags, shared components) preserved from the previous spec so the Developer Portal prose is unchanged.

New npm scripts

"openapi": "tsx scripts/generate-openapi.ts",
"openapi:check": "tsx scripts/generate-openapi.ts --check"
  • npm run openapi regenerates public/openapi.json in place.
  • npm run openapi:check regenerates in memory and exits non-zero if the committed file is stale (prints first-divergence hint).

CI drift check

A new openapi job in .github/workflows/ci.yml runs npm run openapi:check on every PR. If a column is added/renamed/removed and the generator output changes but the committed spec isn't regenerated, CI fails.

Local verification

  • npm run openapi → "19 schemas, 35 paths"
  • npm run openapi:check → green
  • npm run lint:biome → green (no new errors introduced)
  • npm run build → green

Maintenance

Adding a column:

  1. Edit the Drizzle table in lib/db/schema/*.ts.
  2. Run npm run openapi.
  3. Commit the regenerated public/openapi.json.

Adding an endpoint:

  1. Add the route under app/api/v1/**.
  2. Add an entry in scripts/openapi/endpoints.ts with the actual query params, tag, and response shape.
  3. Run npm run openapi and commit.

@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 10:01pm

Request Review

…heck

The hand-written public/openapi.json (last touched 2026-04-23) had drifted
badly from reality — e.g. Utility documented 14 fields while the DB table
and API returned all 43. Multiple endpoint families shipped since then
(substations, transmission-lines, power-plants/substations,
pricing-nodes/versions, programs/versions, territories/lookup, changelog)
weren't in the spec at all.

Before vs after field counts (public schema objects):
  Utility              14 → 37
  PowerPlant            9 → 31
  EvStation            11 → 26
  Program               7 → 31
  PricingNode           5 → 17
  BalancingAuthority    4 → 17
  Iso                   5 → 14
  Rto                   4 → 14
  Region                4 → 14
  Substation            — → 22
  TransmissionLine      — → 18
  Territory             5 →  9

Approach:
- scripts/generate-openapi.ts walks every Drizzle table via getTableColumns
  and maps PG column types → OpenAPI 3.1 schemas. Internal fields
  (submittedBy, reviewedAt, reviewedBy, lockedStatus, searchVector,
  notionPageId, geography/geometry) are stripped by default. jsonb columns
  with known shape get per-field overrides.
- scripts/openapi/endpoints.ts is the canonical per-endpoint registry.
  Query params mirror the actual Zod schemas / URL parsing in
  app/api/v1/**. Auth-required/internal endpoints (mod/*, developer/*,
  contributions, discussions, follows, notifications, me, editable-fields,
  webhooks, revalidate, health, tiles) are intentionally excluded.
- scripts/openapi/base.ts preserves the previous info/servers/tags and
  shared components (ErrorResponse, PaginatedMeta, SearchResults) plus
  adds ChangelogResponse, EntityVersion, GeoJsonFeature.

New endpoints now documented:
  /substations, /substations/{slug}, /substations/{slug}/transmission-lines
  /power-plants/{slug}/substations
  /pricing-nodes/{slug}/versions, /programs/{slug}/versions
  /territories/lookup
  /changelog

npm scripts:
  npm run openapi          # regenerate public/openapi.json
  npm run openapi:check    # exit 1 on drift, used by CI

CI: new 'openapi' job runs openapi:check on every PR; drift fails the build.

Fixes ALL-728
@texture-fleet-agent texture-fleet-agent Bot force-pushed the meridian/auto-generate-openapi branch from 2a0999a to 160f2a0 Compare May 6, 2026 21:58
@texture-fleet-agent texture-fleet-agent Bot merged commit 048d9fc into main May 6, 2026
7 checks passed
@texture-fleet-agent texture-fleet-agent Bot deleted the meridian/auto-generate-openapi branch May 6, 2026 22:06
texture-fleet-agent Bot pushed a commit that referenced this pull request May 6, 2026
Sanitize public API responses to exclude fields that are intentionally
absent from the OpenAPI spec (notionPageId, searchVector, reviewedAt,
reviewedBy, lockedStatus, submittedBy), plus geometry fields that have
their own dedicated endpoints.

Single source of truth for the internal-field list now lives in
lib/api/internal-fields.ts, consumed by both the OpenAPI generator
(scripts/openapi/schema-from-drizzle.ts) and the API route handlers
(lib/api/public-response.ts).

Applied the new stripInternal / publicJsonResponse / publicPaginatedResponse
helpers across every public resource endpoint: utilities, isos, rtos,
balancing-authorities, regions, territories, power-plants, ev-stations,
substations, pricing-nodes, programs, transmission-lines, search, and
their /versions sub-endpoints.

Auth-gated endpoints (/mod/*, /me, /contributions, /discussions, /follows,
/notifications, /developer/*, /editable-fields) continue to use jsonResponse
directly since they legitimately surface review/moderation metadata to
their audiences.

Regression coverage in lib/api/__tests__/public-response.test.ts (10 tests)
asserts the helpers strip every INTERNAL_FIELDS key from single objects,
arrays, nested include payloads, and grouped /search responses, while
leaving public fields intact.

From Morgan's Relay bug report (memory/specs/relay-commongrid-bugs-2026-05-06.md),
bug #1 residual after PR #203.

Fixes ALL-730
texture-fleet-agent Bot added a commit that referenced this pull request May 6, 2026
* fix(api): strip internal fields from public responses

Sanitize public API responses to exclude fields that are intentionally
absent from the OpenAPI spec (notionPageId, searchVector, reviewedAt,
reviewedBy, lockedStatus, submittedBy), plus geometry fields that have
their own dedicated endpoints.

Single source of truth for the internal-field list now lives in
lib/api/internal-fields.ts, consumed by both the OpenAPI generator
(scripts/openapi/schema-from-drizzle.ts) and the API route handlers
(lib/api/public-response.ts).

Applied the new stripInternal / publicJsonResponse / publicPaginatedResponse
helpers across every public resource endpoint: utilities, isos, rtos,
balancing-authorities, regions, territories, power-plants, ev-stations,
substations, pricing-nodes, programs, transmission-lines, search, and
their /versions sub-endpoints.

Auth-gated endpoints (/mod/*, /me, /contributions, /discussions, /follows,
/notifications, /developer/*, /editable-fields) continue to use jsonResponse
directly since they legitimately surface review/moderation metadata to
their audiences.

Regression coverage in lib/api/__tests__/public-response.test.ts (10 tests)
asserts the helpers strip every INTERNAL_FIELDS key from single objects,
arrays, nested include payloads, and grouped /search responses, while
leaving public fields intact.

From Morgan's Relay bug report (memory/specs/relay-commongrid-bugs-2026-05-06.md),
bug #1 residual after PR #203.

Fixes ALL-730

* fix(lint): update Node.js import protocol and biome schema version

* fix(lint): remove unused variables and fix optional chaining

* chore(api): sort imports in public-response.ts

---------

Co-authored-by: texture-coding-agent <coding-agent@texturehq.com>
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