diff --git a/app/config/openapi.js b/app/config/openapi.js index adcc13a..bd7a2e4 100644 --- a/app/config/openapi.js +++ b/app/config/openapi.js @@ -549,6 +549,7 @@ const spec = { post: { summary: 'Create a customer', security: [{ authKey: [] }], + parameters: [idempotencyKeyHeader], requestBody: { required: true, content: { @@ -566,6 +567,7 @@ const spec = { post: { summary: 'Create a time entry', security: [{ authKey: [] }], + parameters: [idempotencyKeyHeader], requestBody: { required: true, content: { @@ -642,6 +644,7 @@ const spec = { post: { summary: 'Create a worker', security: [{ authKey: [] }], + parameters: [idempotencyKeyHeader], requestBody: { required: true, content: { @@ -692,6 +695,7 @@ const spec = { post: { summary: 'Create a billing type', security: [{ authKey: [] }], + parameters: [idempotencyKeyHeader], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/BillingType' } } } }, responses: { 201: { description: 'Created' }, 400: { description: 'Bad request' }, 403: { description: 'Auth failure' } }, }, @@ -733,6 +737,7 @@ const spec = { post: { summary: 'Create an inventory item', security: [{ authKey: [] }], + parameters: [idempotencyKeyHeader], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/InventoryItem' } } } }, responses: { 201: { description: 'Created' }, 400: { description: 'Bad request' }, 403: { description: 'Auth failure' } }, }, @@ -774,6 +779,7 @@ const spec = { post: { summary: 'Create a company (master keys only)', security: [{ authKey: [] }], + parameters: [idempotencyKeyHeader], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/Company' } } } }, responses: { 201: { description: 'Created' }, 400: { description: 'Bad request' }, 403: { description: 'Non-master key' } }, }, @@ -812,6 +818,7 @@ const spec = { post: { summary: 'Create a job', security: [{ authKey: [] }], + parameters: [idempotencyKeyHeader], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/Job' } } } }, responses: { 201: { description: 'Created' }, 400: { description: 'Bad request' }, 403: { description: 'Auth failure' } }, }, @@ -837,6 +844,7 @@ const spec = { post: { summary: 'Create an invoice', security: [{ authKey: [] }], + parameters: [idempotencyKeyHeader], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/Invoice' } } } }, responses: { 201: { description: 'Created' }, 400: { description: 'Bad request' }, 403: { description: 'Auth failure' } }, }, @@ -862,6 +870,7 @@ const spec = { post: { summary: 'Create a customer payment', security: [{ authKey: [] }], + parameters: [idempotencyKeyHeader], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/CustomerPayment' } } } }, responses: { 201: { description: 'Created' }, 400: { description: 'Bad request' }, 403: { description: 'Auth failure' } }, }, @@ -887,6 +896,7 @@ const spec = { post: { summary: 'Create an invoice line (job → invoice)', security: [{ authKey: [] }], + parameters: [idempotencyKeyHeader], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/InvoiceJob' } } } }, responses: { 201: { description: 'Created' }, 400: { description: 'Bad request' }, 403: { description: 'Auth failure' } }, }, @@ -912,6 +922,7 @@ const spec = { post: { summary: 'Create a product entry', security: [{ authKey: [] }], + parameters: [idempotencyKeyHeader], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/ProductEntry' } } } }, responses: { 201: { description: 'Created' }, 400: { description: 'Bad request' }, 403: { description: 'Auth failure' } }, }, @@ -937,6 +948,7 @@ const spec = { post: { summary: 'Create a version info record (master keys only)', security: [{ authKey: [] }], + parameters: [idempotencyKeyHeader], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/VersionInfo' } } } }, responses: { 201: { description: 'Created' }, 400: { description: 'Bad request' }, 403: { description: 'Non-master key' } }, }, @@ -959,6 +971,7 @@ const spec = { post: { summary: 'Create a PO vendor', security: [{ authKey: [] }], + parameters: [idempotencyKeyHeader], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/PurchaseOrderVendor' } } } }, responses: { 201: { description: 'Created' }, 400: { description: 'Bad request' }, 403: { description: 'Auth failure' } }, }, @@ -981,7 +994,7 @@ const spec = { }, }, '/v1/purchaseorderheader': { - post: { summary: 'Create a PO header', security: [{ authKey: [] }], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/PurchaseOrderHeader' } } } }, responses: { 201: { description: 'Created' }, 400: { description: 'Bad request' }, 403: { description: 'Auth failure' } } }, + post: { summary: 'Create a PO header', security: [{ authKey: [] }], parameters: [idempotencyKeyHeader], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/PurchaseOrderHeader' } } } }, responses: { 201: { description: 'Created' }, 400: { description: 'Bad request' }, 403: { description: 'Auth failure' } } }, }, '/v1/purchaseorderheader/{id}': { get: { summary: 'Get one PO header', security: [{ authKey: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }], responses: { 200: { description: 'Found' }, 404: { description: 'Not found' }, 403: { description: 'Auth failure' } } }, @@ -1001,7 +1014,7 @@ const spec = { }, }, '/v1/purchaseorderline': { - post: { summary: 'Create a PO line', security: [{ authKey: [] }], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/PurchaseOrderLine' } } } }, responses: { 201: { description: 'Created' }, 400: { description: 'Bad request' }, 403: { description: 'Auth failure' } } }, + post: { summary: 'Create a PO line', security: [{ authKey: [] }], parameters: [idempotencyKeyHeader], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/PurchaseOrderLine' } } } }, responses: { 201: { description: 'Created' }, 400: { description: 'Bad request' }, 403: { description: 'Auth failure' } } }, }, '/v1/purchaseorderline/{id}': { get: { summary: 'Get one PO line', security: [{ authKey: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }], responses: { 200: { description: 'Found' }, 404: { description: 'Not found' }, 403: { description: 'Auth failure' } } }, @@ -1021,7 +1034,7 @@ const spec = { }, }, '/v1/inventorytransaction': { - post: { summary: 'Create an inventory transaction', security: [{ authKey: [] }], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/InventoryTransaction' } } } }, responses: { 201: { description: 'Created' }, 400: { description: 'Bad request' }, 403: { description: 'Auth failure' } } }, + post: { summary: 'Create an inventory transaction', security: [{ authKey: [] }], parameters: [idempotencyKeyHeader], requestBody: { required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/InventoryTransaction' } } } }, responses: { 201: { description: 'Created' }, 400: { description: 'Bad request' }, 403: { description: 'Auth failure' } } }, }, '/v1/inventorytransaction/{id}': { get: { summary: 'Get one inventory transaction', security: [{ authKey: [] }], parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }], responses: { 200: { description: 'Found' }, 404: { description: 'Not found' }, 403: { description: 'Auth failure' } } }, diff --git a/tests/api/openapi.test.js b/tests/api/openapi.test.js index fd753c6..1fc7895 100644 --- a/tests/api/openapi.test.js +++ b/tests/api/openapi.test.js @@ -93,6 +93,31 @@ describe('OpenAPI spec', () => { } }); + test('single-create POSTs document the Idempotency-Key header', async () => { + // The middleware applies to every /v1/* POST, so the spec + // should advertise the header on the single-create endpoints + // too — not just the bulk variants. We don't pin the 409 + // response on single POSTs (the same-key-different-body case + // is rare enough that documenting just the request header is + // sufficient for SDK code-gen). + const res = await request(app).get('/openapi.json'); + const targets = [ + '/v1/customer', '/v1/timeentry', '/v1/worker', '/v1/billingtype', + '/v1/inventoryitem', '/v1/company', '/v1/job', '/v1/invoice', + '/v1/customerpayment', '/v1/invoicejob', '/v1/productentry', + '/v1/versioninfo', '/v1/purchaseordervendor', + '/v1/purchaseorderheader', '/v1/purchaseorderline', + '/v1/inventorytransaction', + ]; + for (const path of targets) { + const post = res.body.paths[path] && res.body.paths[path].post; + expect(post, `${path} POST should be documented`).toBeDefined(); + const params = post.parameters || []; + const idem = params.find((p) => p.name === 'Idempotency-Key'); + expect(idem, `${path} POST should document the Idempotency-Key header`).toBeDefined(); + } + }); + test('bulk endpoints document the Idempotency-Key header', async () => { const res = await request(app).get('/openapi.json'); const customer = res.body.paths['/v1/customer/bulk'];