Skip to content

Tighten response schemas: add required and additionalProperties: false #7126

@n-lark

Description

@n-lark

Tighten response schemas: add required and additionalProperties: false

Generated frontend types (frontend/src/types/generated.ts) are all-optional because response schemas don't declare required or additionalProperties. This forces defensive ?. chains even for fields like id and produces [key: string]: unknown indexers on types whose views return a closed shape.

Scope

For each response schema across ~23 files (see below), two passes:

  • required — list fields the view unconditionally assigns. Conditional assignments (if (x) {...}, obj?.x) stay optional. For views with branching shapes (e.g. Device.statusOnly), require only the intersection or split the schema. nullable: true fields can still be required — "required" means "always present", not "non-null".
  • additionalProperties: false — add it wherever the view returns a closed shape. Keep true only where a schema genuinely passes through arbitrary data (audit log scope, body, trigger); call those out in the PR.

Files (79 schemas)

forge/db/views/: AccessToken (8), Application (5), AuditLog (5), BOM (2), BrokerCredentials (1), Device (5), DeviceGroup (3), Invitation (2), Notification (2), Project (7), ProjectSnapshot (5), ProjectStack (2), ProjectTemplate (2), ProjectType (2), Team (3), TeamType (3), User (5)

forge/ee/db/views/: FlowTemplate (4), MCPRegistrations (2), Pipeline (2), PipelineStage (2), Table (2)

forge/routes/api-docs.js: APIStatus, APIError, PaginationParams, PaginationMeta, LinksMeta (5)


Test plan

All tests passed
  • NODE_ENV=development npm run test:system — 61 passing, 0 failing, 0 validator errors.

  • npm run test:unit:forge — run on CI (local env has pre-existing async_hooks crash, unrelated to this PR).

  • npm run generate:types + npm run test:unit:frontend + npm run build — types regenerated (599-line diff — 315 insertions, 284 deletions), 429/429 frontend tests pass, build clean.

  • Manual dev browser test — any 500 with FST_RESPONSE_VALIDATION_FAILED_VALIDATION is drift. A 200 is the pass signal.

    How to run: boot NODE_ENV=development npm run serve, log into the UI in Chrome, DevTools → Network tab → save HAR as packages/flowfuse/log.md after each set of clicks, paste the new-log marker in chat. Same workflow as PR 1.

    GET list + GET single for each (POST/PUT/DELETE optional — most of the schema surface is in reads). Tick when the endpoint returns 200 with no validator errors:

    OS

    • GET /api/v1/templates — Admin → Templates list
    • GET /api/v1/templates/:id — click into a specific template
    • GET /api/v1/user/invitations — user menu → Invitations
    • GET /api/v1/user/notifications — user menu → bell icon
    • GET /api/v1/user/tokens — User settings → Access Tokens
    • GET /api/v1/teams/:teamId/audit-log (both simple + scope=team&includeChildren=true variants)
    • GET /api/v1/projects/:id/audit-log — Instance → Audit Log tab
    • GET /api/v1/applications/:id/audit-log?scope=application&includeChildren=true — fix confirmed
    • GET /api/v1/admin/audit-log?scope=platform — Admin → Audit Log
    • GET /api/v1/teams/:teamId/devices/provisioning — Team → Devices → Provisioning tab

    EE

    • GET /api/v1/applications/:id/pipelines — Application → DevOps Pipelines
    • GET /api/v1/teams/:teamId/pipelines — team-level pipelines list
    • GET /api/v1/flow-blueprints — New Instance → blueprint picker
    • GET /api/v1/flow-blueprints/:id — click a specific blueprint
    • GET /api/v1/teams/:teamId/databases — Team → Tables
    • GET /api/v1/teams/:teamId/databases/:id — via console fetch
    • GET /api/v1/teams/:teamId/databases/:id/tables/:tableName — same DatabaseTable schema as adjacent endpoints, drivers all return the declared 6-field shape
    • GET /api/v1/teams/:teamId/mcp — via console fetch

    Main entity smoke

    • GET /api/v1/teams/slug/:slug (team by slug)
    • GET /api/v1/teams?limit=30&sort=createdAt-desc (admin team list — TeamType attrs fix)
    • GET /api/v1/user/teams (UserTeamList — restructured during audit)
    • GET /api/v1/teams/:teamId/members
    • GET /api/v1/teams/:teamId/applications?includeApplicationSummary=true...
    • GET /api/v1/teams/:teamId/applications?excludeOwnerFiltering=true (Instances createdAt/updatedAt fix)
    • GET /api/v1/teams/:teamId/devices (Team/Application include fix)
    • GET /api/v1/teams/:teamId/devices?statusOnly=true
    • GET /api/v1/teams/:teamId/device-groups (Application include fix)
    • GET /api/v1/teams/:teamId/bom (dependency.wanted optional)
    • GET /api/v1/teams/:teamId/brokers/:id (MQTTBroker status/credentials fields added)
    • GET /api/v1/teams/:teamId/brokers (broker list)
    • GET /api/v1/teams/:teamId/brokers/:id/topics
    • GET /api/v1/teams/:teamId/broker/clients
    • GET /api/v1/teams/:teamId/invitations
    • GET /api/v1/teams/:teamId/instance-counts (various filters)
    • GET /api/v1/teams/:teamId/projects
    • GET /api/v1/teams/:teamId/user
    • GET /api/v1/applications/:id
    • GET /api/v1/applications/:id/instances
    • GET /api/v1/applications/:id/devices
    • GET /api/v1/applications/:id/snapshots (Device.deviceSummary fix + query attrs)
    • GET /api/v1/projects/:id (Instance — 12 projects hit, all 200)
    • GET /api/v1/projects/:id/status
    • GET /api/v1/projects/:id/snapshots (Device.deviceSummary view swap + query attrs)
    • GET /api/v1/projects/:id/logs (PaginationMeta reopened)
    • GET /api/v1/projects/:id/devices/settings — fixed in PR 1, re-verified
    • GET /api/v1/project-types (various filters), /team-types, /stacks
    • GET /api/v1/settings
    • GET /api/v1/admin/license, /admin/stats
    • GET /api/v1/users, /users?limit=30

Notable changes beyond field lists

Most of the diff is mechanical (required arrays, additionalProperties: false). These are the judgment calls.

Change Category Why Risk
Snapshot.device view: DeviceDeviceSummary Wire shape Full Device shape unnecessary on snapshot lists Frontend grep showed only id / name are read
DeviceSummary.application: ApplicationApplicationSummary Wire shape Narrower embedded object No frontend reads of team / createdAt / updatedAt on embedded device app
applicationDeviceGroups PUT /settings: {}{ status: 'okay' } Wire shape APIStatus requires status Frontend ignores body
Team GET /:teamId and /slug/:teamSlug: TeamanyOf: [Team, TeamSummary] Wire shape Handler already emitted either based on role Schema now matches runtime — no behavioral change
Team-broker GET /:brokerId: MQTTBrokeranyOf: [MQTTBroker, { state }] Wire shape Suspended team-broker returns a { state: 'suspended' } stub Additive
Invitation: removed allOf: [UserSummary] at root Wire shape View never spread UserSummary at root — schema was lying No wire change
Snapshot views inline a narrow { id, username, name, avatar } user shape instead of $ref: 'UserSummary' Privacy Avoids leaking admin / suspended / createdAt on every snapshot list. Mirrors AuditLog.TimelineEntry Don't DRY this up to UserSummary — would re-introduce the leak
flowBlueprints POST/PUT/import and 3rdPartyBroker POST inline body schemas Pattern Shared response $refs break as request bodies once required is added (server fields like id, createdAt aren't sent by clients) Body shape now duplicated — keep in sync with response on edits
teamInvitations resend / pipeline stage PUT: re-hydrate via byId after reload() Pattern Sequelize reload() doesn't reload associations; view was reading stale invitor / Devices Adds one DB roundtrip per request
Dev response-validator Ajv uses removeAdditional: false Pattern Avoids an Ajv quirk where allOf + additionalProperties: true strips fields during outer required evaluation Dev-only (NODE_ENV === 'development'); Fastify's request-body validator is a separate Ajv instance and is unaffected
expert.js /mcp/features keeps additionalProperties: true Pattern fast-json-stringify strips undeclared fields at serialization, but Ajv response-validation runs before that — locking would reject data the wire never emits Comment in code; test relies on this

Deferred

  • PUT /projects/:instanceId import/start early-return now hydrates a full Project view to satisfy the tightened response schema, adding a DB roundtrip on a "return fast, finish async" path. Follow-up: narrow the 200 schema for that branch so {} (or a thin { status: 'importing' }) validates if we want to.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

Status

Review

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions