From d45d972e6667a177a233e320493433e3f22a99ae Mon Sep 17 00:00:00 2001 From: "Aaron K. Clark" Date: Tue, 19 May 2026 14:43:11 -0500 Subject: [PATCH] docs(openapi): declare GET/PATCH/DELETE /v1/timeentry/{id} response envelopes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The single-row time-entry endpoints had only `description: ...` on their 200 responses — same missing-content-schema pattern fixed for the customer single GET (#312), POST (#316), bulk (#332), bycompany list (#340), and timeentry POST (#326) + bycompany list (#348). Without the declaration, SDK code-gen treated the response body as untyped. Pin all three operations on this path: - GET → {message, timeEntry} - PATCH → {message, timeEntry} - DELETE → {message, id} (no entity body; the row was archived, the controller echoes the int teId so the client can confirm what got soft-deleted) Add a sweep test in tests/api/openapi.test.js covering all three. The DELETE assertion also pins the absence of a `timeEntry` field so a future "echo the row back on delete" refactor would surface as a test failure. Test count: 794 → 795. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/config/openapi.js | 58 +++++++++++++++++++++++++++++++++++++-- tests/api/openapi.test.js | 29 ++++++++++++++++++++ 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/app/config/openapi.js b/app/config/openapi.js index 42a1412..9dbc7f5 100644 --- a/app/config/openapi.js +++ b/app/config/openapi.js @@ -843,20 +843,72 @@ const spec = { summary: 'Get one time entry', security: [{ authKey: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }], - responses: { 200: { description: 'Found' }, 404: { description: 'Not found' }, 403: { description: 'Auth failure' } }, + responses: { + 200: { + description: 'Found — {message, timeEntry} envelope', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + message: { type: 'string' }, + timeEntry: { $ref: '#/components/schemas/TimeEntry' }, + }, + }, + }, + }, + }, + 404: { description: 'Not found' }, + 403: { description: 'Auth failure' }, + }, }, patch: { summary: 'Partial update of a time entry', security: [{ authKey: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }], requestBody: { content: { 'application/json': { schema: { $ref: '#/components/schemas/TimeEntry' } } } }, - responses: { 200: { description: 'Updated' }, 400: { description: 'No updatable fields supplied' }, 404: { description: 'Not found' }, 403: { description: 'Auth failure' } }, + responses: { + 200: { + description: 'Updated — {message, timeEntry} envelope', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + message: { type: 'string' }, + timeEntry: { $ref: '#/components/schemas/TimeEntry' }, + }, + }, + }, + }, + }, + 400: { description: 'No updatable fields supplied' }, + 404: { description: 'Not found' }, + 403: { description: 'Auth failure' }, + }, }, delete: { summary: 'Soft-delete a time entry', security: [{ authKey: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }], - responses: { 200: { description: 'Archived' }, 404: { description: 'Not found' }, 403: { description: 'Auth failure' } }, + responses: { + 200: { + description: 'Archived — {message, id} envelope (id echoes the deleted row\'s teId)', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + message: { type: 'string' }, + id: { type: 'integer' }, + }, + }, + }, + }, + }, + 404: { description: 'Not found' }, + 403: { description: 'Auth failure' }, + }, }, }, '/v1/timeentry/export.csv': { diff --git a/tests/api/openapi.test.js b/tests/api/openapi.test.js index 55833c7..442ed0d 100644 --- a/tests/api/openapi.test.js +++ b/tests/api/openapi.test.js @@ -129,6 +129,35 @@ describe('OpenAPI spec', () => { } }); + test('/v1/timeentry/{id} GET / PATCH / DELETE 200 declare their envelopes', async () => { + // Same missing-content-schema pattern as #312 (customer GET), + // #326 (timeentry POST), #340 (customer bycompany), #348 + // (timeentry bycompany). The single-row endpoints + // historically had only `description: ...` on the 200s. Pin + // the controller-emitted envelopes: + // GET → {message, timeEntry} + // PATCH → {message, timeEntry} + // DELETE → {message, id} (no entity body — the row is archived) + const res = await request(app).get('/openapi.json'); + const ops = res.body.paths['/v1/timeentry/{id}']; + + const getSchema = ops.get.responses['200'].content['application/json'].schema; + expect(getSchema.properties.message.type).toBe('string'); + expect(getSchema.properties.timeEntry.$ref).toBe('#/components/schemas/TimeEntry'); + + const patchSchema = ops.patch.responses['200'].content['application/json'].schema; + expect(patchSchema.properties.message.type).toBe('string'); + expect(patchSchema.properties.timeEntry.$ref).toBe('#/components/schemas/TimeEntry'); + + const deleteSchema = ops.delete.responses['200'].content['application/json'].schema; + expect(deleteSchema.properties.message.type).toBe('string'); + expect(deleteSchema.properties.id.type).toBe('integer'); + // DELETE responds with the row's id, NOT the full entity — + // pin the absence so a future "echo the deleted row back" + // refactor surfaces here. + expect(deleteSchema.properties.timeEntry).toBeUndefined(); + }); + test('GET /v1/timeentry/bycompany/{id} 200 declares the {message, count, limit, offset, timeEntries} envelope', async () => { // Parallel to the customer/bycompany declaration in #340. // Pre-fix the spec said only `description: 'OK'`; SDK code-gen