feat(agent-bff): publish typed record schemas in the openapi document - #1867
feat(agent-bff): publish typed record schemas in the openapi document#1867Tonours wants to merge 6 commits into
Conversation
|
Coverage Impact This PR will not change total coverage. Modified Files with Diff Coverage (5)
🛟 Help
|
nbouliol
left a comment
There was a problem hiding this comment.
Two-axis review (standards + spec against PRD-1105). The spec side holds up: parity with jsonapi-serializer's camelCase branch is real, the id string-vs-number contract is pinned by a genuine real-agent test, degraded collections are covered on both halves, and "no runtime change" checks out. One blocking question on nullability, one on DRY in the new integration test.
|
Review pass done. Two cleanups from it, both mine from the last push:
Worth recording from the pass, since it is not obvious from the diff: Two ceilings named rather than fixed:
|
nbouliol
left a comment
There was a problem hiding this comment.
Round 2. The three findings from the first pass are confirmed fixed: the harness extraction is clean (the search suite keeps the same secrets, timezone and middleware order), @types/inflected sits correctly in devDependencies and matches the pinned 1.x, and the nullable fix is the right call. One follow-up on how deep it goes.
One thing outside the diff: the PR body still says properties are "typed from the capabilities field types" and never mentions nullability, though a generated client's field types went from string to string | null since the first revision — worth a line.
| * declare non-null — a generated client validating the response would reject what the runtime | ||
| * really sends. An unconstrained schema already accepts null and is left alone. | ||
| */ | ||
| function nullable(schema: SchemaObject): SchemaObject { | ||
| if (typeof schema.type !== 'string') return schema; |
There was a problem hiding this comment.
nullable() stops at the container, so items and a nested object's properties stay non-null — the same failure mode one level down, while the record prose says a null is always possible whatever the type says. Recurse, or narrow the prose to the top level?
| export const TIMEZONE = 'Europe/Paris'; | ||
| export const AUTH_SECRET = 'b0bdf0a639c16bae8851dd24ee3d79ef0a352e957c5b86cb'; | ||
| export const ENV_SECRET = 'ceba742f5bc73946b34da192816a4d7177b3233fee7769955c29c0e90fd584f2'; | ||
| export const BOOT_TIMEOUT_MS = 60_000; | ||
|
|
||
| // agent-testing only deletes a schema file whose name carries this prefix, so reusing it keeps the | ||
| // temporary schema cleaned up by `agent.stop()` even though the path is chosen here. | ||
| export const RESERVED_SCHEMA_PREFIX = 'reserved-forestadmin-schema-test-'; |
There was a problem hiding this comment.
TIMEZONE and noopLogger are exported but neither suite imports them.

What
The unfolded OpenAPI document emits a
Record_<collection>schema per collection with a known field set, typed from the capabilities field types and published nullable — the capabilities never report nullability — and aListResponse_<collection>wrapper referencing it. A relation list operation answers with the FOREIGN collection's response schema. A degraded collection keeps the shared untypedListResponse, for its own list and for relations pointing at it.No runtime behaviour change: the record shape, the key transform and the id string form are untouched. This documents them.
fixes PRD-1105
Three traps the document now names
agent-clientdeserializes JSON:API withkeyForAttribute: 'camelCase', so afirst_namecolumn is projected under that name and returned asfirstName. The transform comes frominflected— the same libraryjsonapi-serializeruses — so parity holds by construction rather than through a hand-rolled mirror that would drift silently.idis a string on every record, even when the key column is a Number. It is the JSON:API resource id.__forest.primaryKeycarries the same id typed, so the two forms of one record disagree by construction, and comparing them without coercion fails. Stated on the sharedForestRecordMetaandListResponsetoo, so the generic document carries it, including the|-joined packed form of a composite key.nullagainst a type a non-null schema would reject.firstNameis['string', 'null']; aJsoncolumn stays unconstrained because it already accepted null, and nesteditemsand sub-properties are widened too, since the same holds at every depth. Publishing non-null would have been a claim the runtime breaks on the first empty column.Verification
yarn workspace @forestadmin/agent-bff test— includes redocly spec validity, the generated-client suite, and a real-agent integration suite pinningid: "8"(string) against__forest.primaryKey.id: 8(number), andfirst_namereturned asfirstName.lint,build,yarn install --frozen-lockfile: clean.The live-agent harness those integration suites share (
findFreePort,buildApp, the schema transport) lives intest/data/fixtures/live-agent-harness.tsrather than being copied per suite.Known limitations
first_namevsfirstName) collapse onto one response key. The record schema publishes that key unconstrained and names the fields that reach it. The underlying data loss is out of scope and reported separately.Jsoncolumn are camelized too.packages/workflow-executor/src/adapters/agent-client-agent-port.tstranscribes this transform by hand; the monorepo now holds both that transcription and a direct use ofinflected. Unifying them is tracked as PRD-1113.Definition of Done
General
Security