Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
72 changes: 72 additions & 0 deletions src/__tests__/request-config.spec.ts
Original file line number Diff line number Diff line change
@@ -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<unknown>][] = [
['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')
})
})
15 changes: 10 additions & 5 deletions src/services/PdfService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AsyncJob, AsyncPdf, CreatePdf, SyncJob, SyncPdf } from '../generated'
import type { AxiosRequestConfig } from 'axios'

import { BaseService } from './BaseService'

Expand All @@ -7,33 +8,37 @@ 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<Buffer> {
public direct(requestBody: CreatePdf, config: AxiosRequestConfig = {}): Promise<Buffer> {
return this.request('POST', '/v1/pdf', requestBody, {
...config,
responseType: 'arraybuffer'
})
}

/**
* 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<SyncJob> {
return this.request('POST', '/v1/pdf/sync', requestBody)
public async sync(requestBody: SyncPdf, config: AxiosRequestConfig = {}): Promise<SyncJob> {
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<AsyncJob> {
return this.request('POST', '/v1/pdf/async', requestBody)
public async(requestBody: AsyncPdf, config: AxiosRequestConfig = {}): Promise<AsyncJob> {
return this.request('POST', '/v1/pdf/async', requestBody, config)
}

}
15 changes: 10 additions & 5 deletions src/services/ScreenshotService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AsyncJob, AsyncScreenshot, CreateScreenshot, SyncJob, SyncScreenshot } from '../generated'
import type { AxiosRequestConfig } from 'axios'

import { BaseService } from './BaseService'

Expand All @@ -7,33 +8,37 @@ 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<Buffer> {
public direct(requestBody: CreateScreenshot, config: AxiosRequestConfig = {}): Promise<Buffer> {
return this.request('POST', '/v1/screenshot', requestBody, {
...config,
responseType: 'arraybuffer'
})
}

/**
* 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<SyncJob> {
return this.request('POST', '/v1/screenshot/sync', requestBody)
public sync(requestBody: SyncScreenshot, config: AxiosRequestConfig = {}): Promise<SyncJob> {
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<AsyncJob> {
return this.request('POST', '/v1/screenshot/async', requestBody)
public async(requestBody: AsyncScreenshot, config: AxiosRequestConfig = {}): Promise<AsyncJob> {
return this.request('POST', '/v1/screenshot/async', requestBody, config)
}

}
41 changes: 26 additions & 15 deletions src/services/TemplateService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
Template,
UpdateTemplate
} from '../generated'
import type { AxiosRequestConfig } from 'axios'

import { BaseService } from './BaseService'

Expand All @@ -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<Buffer> {
public direct(id: string, requestBody: CreateFromTemplate, config: AxiosRequestConfig = {}): Promise<Buffer> {
return this.request('POST', `/v1/template/${id}`, requestBody, {
...config,
responseType: 'arraybuffer'
})
}
Expand All @@ -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<SyncJob> {
return this.request('POST', `/v1/template/${id}/sync`, requestBody)
public async sync(id: string, requestBody: SyncFromTemplate, config: AxiosRequestConfig = {}): Promise<SyncJob> {
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<AsyncJob> {
return this.request('POST', `/v1/template/${id}/async`, requestBody)
public async(id: string, requestBody: AsyncFromTemplate, config: AxiosRequestConfig = {}): Promise<AsyncJob> {
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<ListTemplateResponse> {
public list(params: { page?: number } = {}, config: AxiosRequestConfig = {}): Promise<ListTemplateResponse> {
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<Template> {
public create(requestBody: CreateTemplate, config: AxiosRequestConfig = {}): Promise<Template> {
return this.request('POST', '/v1/templates', {
...requestBody,
variables: this.encodeRequestBody(requestBody.variables)
})
}, config)
}

/**
* Get one template.
* @param id
* @param config Optional Axios request configuration.
* @returns Template
* @throws AxiosError
*/
public get(id: string): Promise<Template> {
return this.request('GET', `/v1/templates/${id}`)
public get(id: string, config: AxiosRequestConfig = {}): Promise<Template> {
return this.request('GET', `/v1/templates/${id}`, undefined, config)
}

/**
* Update one template.
* @param id
* @param requestBody
* @param config Optional Axios request configuration.
* @throws AxiosError
*/
public update(id: string, requestBody: UpdateTemplate): Promise<void> {
public update(id: string, requestBody: UpdateTemplate, config: AxiosRequestConfig = {}): Promise<void> {
return this.request('POST', `/v1/templates/${id}`, {
...requestBody,
...(requestBody.variables && ({
variables: this.encodeRequestBody(requestBody.variables)
}))
})
}, config)
}

/**
* Delete one template.
* @param id
* @param config Optional Axios request configuration.
* @throws AxiosError
*/
public delete(id: string): Promise<void> {
return this.request('DELETE', `/v1/templates/${id}`)
public delete(id: string, config: AxiosRequestConfig = {}): Promise<void> {
return this.request('DELETE', `/v1/templates/${id}`, undefined, config)
}

}
Loading