From b6642454e688f16b9ae3c7b79121c864660bf380 Mon Sep 17 00:00:00 2001 From: Tycho Bokdam Date: Sat, 14 Nov 2026 17:44:24 +0100 Subject: [PATCH] feat: Accept Axios request configuration in service methods --- README.md | 17 +++++++ src/__tests__/request-config.spec.ts | 72 ++++++++++++++++++++++++++++ src/services/PdfService.ts | 15 ++++-- src/services/ScreenshotService.ts | 15 ++++-- src/services/TemplateService.ts | 41 ++++++++++------ 5 files changed, 135 insertions(+), 25 deletions(-) create mode 100644 src/__tests__/request-config.spec.ts diff --git a/README.md b/README.md index 883095b..73450a3 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,23 @@ const pdfBuffer = await doczilla.pdf.direct({ }) ``` +### Request configuration + +All PDF, screenshot, and template methods accept an optional Axios request config as the final argument, including `signal`, `timeout`, and `headers`. Direct methods always use `arraybuffer` responses to return a `Buffer`. + +```ts +const controller = new AbortController() +const request = doczilla.pdf.direct({ page: { url: 'https://example.com' } }, { + signal: controller.signal, + timeout: 30_000 +}) + +controller.abort() +await request // Rejects with an Axios cancellation error. +``` + +For template listing, pass `doczilla.template.list({}, { signal: controller.signal })`. + ### Webhook signing Doczilla can optionally sign the webhook events it sends to your endpoint, allowing you to validate that they were not diff --git a/src/__tests__/request-config.spec.ts b/src/__tests__/request-config.spec.ts new file mode 100644 index 0000000..e7a945e --- /dev/null +++ b/src/__tests__/request-config.spec.ts @@ -0,0 +1,72 @@ +import { describe, expect, test } from '@jest/globals' +import axios, { AxiosRequestConfig } from 'axios' +import MockAdapter from 'axios-mock-adapter' + +import { CreateTemplate } from '../generated' +import { PdfService } from '../services/PdfService' +import { ScreenshotService } from '../services/ScreenshotService' +import { TemplateService } from '../services/TemplateService' + +describe('Request configuration', () => { + const client = axios.create() + const mock = new MockAdapter(client) + const pdf = new PdfService(client) + const screenshot = new ScreenshotService(client) + const template = new TemplateService(client) + const page = { url: 'https://example.com' } + const webhook = { url: 'https://example.com/webhook' } + const requests: [string, (config: AxiosRequestConfig) => Promise][] = [ + ['pdf.direct', config => pdf.direct({ page }, config)], + ['pdf.sync', config => pdf.sync({ page }, config)], + ['pdf.async', config => pdf.async({ page, webhook }, config)], + ['screenshot.direct', config => screenshot.direct({ page }, config)], + ['screenshot.sync', config => screenshot.sync({ page }, config)], + ['screenshot.async', config => screenshot.async({ page, webhook }, config)], + ['template.direct', config => template.direct('id', {}, config)], + ['template.sync', config => template.sync('id', {}, config)], + ['template.async', config => template.async('id', { webhook }, config)], + ['template.list', config => template.list({ page: 2 }, config)], + ['template.create', config => template.create({ + name: 'Example', output: CreateTemplate.output.PDF, variables: { page } + }, config)], + ['template.get', config => template.get('id', config)], + ['template.update', config => template.update('id', { name: 'Updated' }, config)], + ['template.delete', config => template.delete('id', config)] + ] + + mock.onAny().reply(200, Buffer.from('result')) + + test.each(requests)('%s forwards Axios configuration', async (_name, request) => { + await request({ timeout: 5000, headers: { 'X-Request-ID': 'example' } }) + + const config = mock.history[mock.history.length - 1] + expect(config.timeout).toBe(5000) + expect(config.headers['X-Request-ID']).toBe('example') + }) + + test.each(requests)('%s rejects when its signal is already aborted', async (_name, request) => { + const controller = new AbortController() + controller.abort() + + await expect(request({ signal: controller.signal })).rejects.toMatchObject({ code: 'ERR_CANCELED' }) + }) + + test('passes the signal to the transport and preserves binary responses', async () => { + const controller = new AbortController() + + await expect(pdf.direct({ page }, { signal: controller.signal, timeout: 5000, responseType: 'json' })).resolves.toEqual(Buffer.from('result')) + expect(mock.history.post[mock.history.post.length - 1].signal).toBe(controller.signal) + expect(mock.history.post[mock.history.post.length - 1].responseType).toBe('arraybuffer') + expect(mock.history.post[mock.history.post.length - 1].timeout).toBe(5000) + }) + + test('preserves pagination when passing a signal', async () => { + const controller = new AbortController() + + await template.list({ page: 2 }, { signal: controller.signal, headers: { 'X-Request-ID': 'example' } }) + + expect(mock.history.get[mock.history.get.length - 1].signal).toBe(controller.signal) + expect(mock.history.get[mock.history.get.length - 1].params).toEqual({ page: 2 }) + expect(mock.history.get[mock.history.get.length - 1].headers['X-Request-ID']).toBe('example') + }) +}) diff --git a/src/services/PdfService.ts b/src/services/PdfService.ts index e98f15c..f44d465 100644 --- a/src/services/PdfService.ts +++ b/src/services/PdfService.ts @@ -1,4 +1,5 @@ import type { AsyncJob, AsyncPdf, CreatePdf, SyncJob, SyncPdf } from '../generated' +import type { AxiosRequestConfig } from 'axios' import { BaseService } from './BaseService' @@ -7,11 +8,13 @@ export class PdfService extends BaseService { /** * Create a PDF and directly return the result. * @param requestBody + * @param config Optional Axios request configuration. * @returns Buffer * @throws AxiosError */ - public direct(requestBody: CreatePdf): Promise { + public direct(requestBody: CreatePdf, config: AxiosRequestConfig = {}): Promise { return this.request('POST', '/v1/pdf', requestBody, { + ...config, responseType: 'arraybuffer' }) } @@ -19,21 +22,23 @@ export class PdfService extends BaseService { /** * Create a PDF and write the result directly in your bucket. * @param requestBody + * @param config Optional Axios request configuration. * @returns SyncJob * @throws AxiosError */ - public async sync(requestBody: SyncPdf): Promise { - return this.request('POST', '/v1/pdf/sync', requestBody) + public async sync(requestBody: SyncPdf, config: AxiosRequestConfig = {}): Promise { + return this.request('POST', '/v1/pdf/sync', requestBody, config) } /** * Queue the creation of a PDF and call the webhook with the result. * @param requestBody + * @param config Optional Axios request configuration. * @returns AsyncJob * @throws AxiosError */ - public async(requestBody: AsyncPdf): Promise { - return this.request('POST', '/v1/pdf/async', requestBody) + public async(requestBody: AsyncPdf, config: AxiosRequestConfig = {}): Promise { + return this.request('POST', '/v1/pdf/async', requestBody, config) } } diff --git a/src/services/ScreenshotService.ts b/src/services/ScreenshotService.ts index dffd1c6..ae4d8c4 100644 --- a/src/services/ScreenshotService.ts +++ b/src/services/ScreenshotService.ts @@ -1,4 +1,5 @@ import type { AsyncJob, AsyncScreenshot, CreateScreenshot, SyncJob, SyncScreenshot } from '../generated' +import type { AxiosRequestConfig } from 'axios' import { BaseService } from './BaseService' @@ -7,11 +8,13 @@ export class ScreenshotService extends BaseService { /** * Create a screenshot and directly return the result. * @param requestBody + * @param config Optional Axios request configuration. * @returns binary * @throws AxiosError */ - public direct(requestBody: CreateScreenshot): Promise { + public direct(requestBody: CreateScreenshot, config: AxiosRequestConfig = {}): Promise { return this.request('POST', '/v1/screenshot', requestBody, { + ...config, responseType: 'arraybuffer' }) } @@ -19,21 +22,23 @@ export class ScreenshotService extends BaseService { /** * Create a screenshot and write the result directly in your bucket. * @param requestBody + * @param config Optional Axios request configuration. * @returns SyncJob * @throws AxiosError */ - public sync(requestBody: SyncScreenshot): Promise { - return this.request('POST', '/v1/screenshot/sync', requestBody) + public sync(requestBody: SyncScreenshot, config: AxiosRequestConfig = {}): Promise { + return this.request('POST', '/v1/screenshot/sync', requestBody, config) } /** * Queue the creation of a screenshot and call the webhook with the result. * @param requestBody + * @param config Optional Axios request configuration. * @returns AsyncJob * @throws AxiosError */ - public async(requestBody: AsyncScreenshot): Promise { - return this.request('POST', '/v1/screenshot/async', requestBody) + public async(requestBody: AsyncScreenshot, config: AxiosRequestConfig = {}): Promise { + return this.request('POST', '/v1/screenshot/async', requestBody, config) } } diff --git a/src/services/TemplateService.ts b/src/services/TemplateService.ts index 6184faf..5bd5d21 100644 --- a/src/services/TemplateService.ts +++ b/src/services/TemplateService.ts @@ -9,6 +9,7 @@ import type { Template, UpdateTemplate } from '../generated' +import type { AxiosRequestConfig } from 'axios' import { BaseService } from './BaseService' @@ -18,11 +19,13 @@ export class TemplateService extends BaseService { * Create the template and directly return the raw result. * @param id * @param requestBody + * @param config Optional Axios request configuration. * @returns Buffer * @throws AxiosError */ - public direct(id: string, requestBody: CreateFromTemplate): Promise { + public direct(id: string, requestBody: CreateFromTemplate, config: AxiosRequestConfig = {}): Promise { return this.request('POST', `/v1/template/${id}`, requestBody, { + ...config, responseType: 'arraybuffer' }) } @@ -31,81 +34,89 @@ export class TemplateService extends BaseService { * Create the template and write the result directly in your bucket. * @param id * @param requestBody + * @param config Optional Axios request configuration. * @returns SyncJob * @throws AxiosError */ - public async sync(id: string, requestBody: SyncFromTemplate): Promise { - return this.request('POST', `/v1/template/${id}/sync`, requestBody) + public async sync(id: string, requestBody: SyncFromTemplate, config: AxiosRequestConfig = {}): Promise { + return this.request('POST', `/v1/template/${id}/sync`, requestBody, config) } /** * Queue the creation of the template and call the webhook with the result. * @param id * @param requestBody + * @param config Optional Axios request configuration. * @returns AsyncJob * @throws AxiosError */ - public async(id: string, requestBody: AsyncFromTemplate): Promise { - return this.request('POST', `/v1/template/${id}/async`, requestBody) + public async(id: string, requestBody: AsyncFromTemplate, config: AxiosRequestConfig = {}): Promise { + return this.request('POST', `/v1/template/${id}/async`, requestBody, config) } /** * List all available templates. * @param params + * @param config Optional Axios request configuration. * @returns ListTemplateResponse * @throws AxiosError */ - public list(params: { page?: number } = {}): Promise { + public list(params: { page?: number } = {}, config: AxiosRequestConfig = {}): Promise { return this.request('GET', '/v1/templates', undefined, { - params + params, + ...config }) } /** * Creates a new template with the provided request body. * @param requestBody + * @param config Optional Axios request configuration. * @returns Template * @throws AxiosError */ - public create(requestBody: CreateTemplate): Promise