From 838e7d30dad2e6df31a68f4901a44f0da88808a6 Mon Sep 17 00:00:00 2001 From: "Aaron K. Clark" Date: Tue, 19 May 2026 12:27:14 -0500 Subject: [PATCH] docs(openapi): fix GET /v1/customer/{id} 200 response-shape drift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OpenAPI spec declared the 200 body as a bare Customer (`$ref: '#/components/schemas/Customer'`). The controller actually wraps the row in a `{message, customer, customers}` envelope — the dual `customer` (singular) + `customers` (plural, backward-compat wart from before #292) key is documented in customercontroller.js's findAndRespond. SDK code-gen (openapi-typescript, quicktype, etc.) reading the spec built clients expecting the bare row, then failed to find the fields at runtime because they live one level deeper inside the envelope. Fix the spec to describe the actual envelope shape: { message: string, customer: Customer, customers: Customer } Also document the 404 response that the controller emits when the row is missing — was previously not in the spec, so SDK clients couldn't model that branch either. Pin both with a test in tests/api/openapi.test.js. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/config/openapi.js | 26 +++++++++++++++++++++++++- tests/api/openapi.test.js | 18 ++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/app/config/openapi.js b/app/config/openapi.js index 15084c2..870406a 100644 --- a/app/config/openapi.js +++ b/app/config/openapi.js @@ -629,8 +629,32 @@ const spec = { { name: 'id', in: 'path', required: true, schema: { type: 'integer' } }, ], responses: { - 200: { description: 'Found', content: { 'application/json': { schema: { $ref: '#/components/schemas/Customer' } } } }, + // The controller wraps the row in a `{message, customer, + // customers}` envelope. The historical `customers` + // (plural) key stays for backward compat; the singular + // `customer` was added in #292 to match the + // singular-for-single-row shape every other entity + // GET uses. Surface both in the spec so SDK + // generators can reach either field — the previous + // `$ref: Customer` declaration was misleading + // (the body is the envelope, not the raw row). + 200: { + description: 'Found — wraps the row in a {message, customer, customers} envelope.', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + message: { type: 'string' }, + customer: { $ref: '#/components/schemas/Customer' }, + customers: { $ref: '#/components/schemas/Customer' }, + }, + }, + }, + }, + }, 403: { description: 'Missing or invalid authKey', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } } }, + 404: { description: 'Not found', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } } }, }, }, }, diff --git a/tests/api/openapi.test.js b/tests/api/openapi.test.js index 21f1432..0ba3053 100644 --- a/tests/api/openapi.test.js +++ b/tests/api/openapi.test.js @@ -70,6 +70,24 @@ describe('OpenAPI spec', () => { expect(schemas.TimeEntry.properties.teStartedAt).toBeDefined(); }); + test('GET /v1/customer/{id} 200 declares the {message, customer, customers} envelope', async () => { + // Pre-#292 the spec said the body was a bare Customer. The + // controller actually returns a `{message, customer, customers}` + // envelope (the dual key is the backward-compat wart documented + // in customercontroller.js). SDK code-gen builds the wrong type + // unless the spec mirrors the runtime shape. + const res = await request(app).get('/openapi.json'); + const r200 = res.body.paths['/v1/customer/{id}'].get.responses['200']; + const schema = r200.content['application/json'].schema; + expect(schema.type).toBe('object'); + expect(schema.properties.message).toBeDefined(); + expect(schema.properties.customer.$ref).toBe('#/components/schemas/Customer'); + expect(schema.properties.customers.$ref).toBe('#/components/schemas/Customer'); + // 404 is documented now too (controller short-circuits on missing rows). + expect(r200).toBeDefined(); + expect(res.body.paths['/v1/customer/{id}'].get.responses['404']).toBeDefined(); + }); + test('VersionInfo.viVersion pins the 1..255 bound from the validator', async () => { // Mirrors versioninfo.schema.js. SDK generators expose viVersion // as `string`; without minLength/maxLength they can't catch a