diff --git a/.changeset/document-card-filters.md b/.changeset/document-card-filters.md new file mode 100644 index 0000000..f68d9d9 --- /dev/null +++ b/.changeset/document-card-filters.md @@ -0,0 +1,7 @@ +--- +"pdfmonkey": minor +--- + +Adding the `search` filter to `documentCards.list()` (exact document ID or partial filename) and letting `status` take several statuses. Query values now accept arrays, sent as `key[]=a&key[]=b`. Documenting the accepted `folders` and `sort` values on `documentTemplates.list()`. + +Fixing `workspaceCards.update()` to send `PUT` instead of `PATCH`, matching the API and the other update endpoints. Removing the `'error'` value from `DocumentStatus` (and the `document.error` webhook payload status): the API never returns it. diff --git a/CHANGELOG.md b/CHANGELOG.md index 468f6eb..9630b93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # pdfmonkey +## Unreleased + +### Minor Changes + +- Adding the `search` filter to `documentCards.list()` (exact document ID or partial filename) and letting `status` take several statuses. Query values now accept arrays, sent as `key[]=a&key[]=b`. Documenting the accepted `folders` and `sort` values on `documentTemplates.list()`. + +### Patch Changes + +- Fixing `workspaceCards.update()` to send `PUT` instead of `PATCH`, matching the API and the other update endpoints. Removing the `'error'` value from `DocumentStatus` (and the `document.error` webhook payload status): the API never returns it. +- ad9d7cd: Fixing `documents.waitForGeneration()` so its `timeout` is a true total budget: in-flight polls and their retries are now aborted when it expires, instead of resolving with a late success or reporting the timeout only after a slow response came back. + ## 1.3.0 ### Minor Changes diff --git a/README.md b/README.md index 8b7681d..6b41cff 100644 --- a/README.md +++ b/README.md @@ -126,7 +126,7 @@ Documents and document cards share a `DocumentStatus` type: ```ts import type { DocumentStatus } from 'pdfmonkey'; -// 'draft' | 'pending' | 'generating' | 'success' | 'failure' | 'error' +// 'draft' | 'pending' | 'generating' | 'success' | 'failure' ``` ### Document Templates @@ -164,6 +164,9 @@ All list methods return a `Page` with built-in navigation: ```ts const page = await client.documentCards.list({ document_template_id: 'tpl_xxx' }); +// Filter by filename (or exact document ID) and any of several statuses +const inProgress = await client.documentCards.list({ search: 'invoice', status: ['pending', 'generating'] }); + console.log(page.data); // items on this page console.log(page.currentPage); // 1 console.log(page.totalPages); // 5 diff --git a/src/__tests__/api.integration.test.ts b/src/__tests__/api.integration.test.ts index d55bb0f..f7b010d 100644 --- a/src/__tests__/api.integration.test.ts +++ b/src/__tests__/api.integration.test.ts @@ -1,5 +1,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { PDFMonkey } from '../client.js'; +import type { DocumentStatus } from '../resources/documents.js'; // Hits the live API with PDFMONKEY_API_KEY. Run with `pnpm test:integration`. // Every resource created here is prefixed and deleted in afterAll. @@ -210,6 +211,13 @@ describe.runIf(enabled)('live API', () => { expect(page.data.map((c) => c.id)).toContain(documentId); }); + it('documentCards.list by search and statuses', async () => { + const list = (status: DocumentStatus[]) => + client.documentCards.list({ workspace_id: workspaceId, search: prefix, status }); + expect((await list(['success', 'failure'])).data.map((c) => c.id)).toContain(documentId); + expect((await list(['draft'])).data.map((c) => c.id)).not.toContain(documentId); + }); + it('documents.generateSync', async () => { const card = await client.documents.generateSync({ document_template_id: templateId, diff --git a/src/__tests__/client.test.ts b/src/__tests__/client.test.ts index b7d1749..8dd283a 100644 --- a/src/__tests__/client.test.ts +++ b/src/__tests__/client.test.ts @@ -170,6 +170,19 @@ describe('PDFMonkey Client', () => { expect(parsed.searchParams.get('page[number]')).toBe('2'); expect(parsed.searchParams.get('status')).toBe('success'); }); + + it('expands array query values as repeated key[] parameters', async () => { + const fetch = mockFetch(200, { data: [] }); + const client = new PDFMonkey({ apiKey: 'sk_test', fetch }); + + await client.get('/documents', { query: { ids: ['a', 1, true], empty: [] } }); + + const [url] = (fetch as ReturnType).mock.calls[0] as [string]; + const parsed = new URL(url); + expect(parsed.searchParams.getAll('ids[]')).toEqual(['a', '1', 'true']); + expect(parsed.searchParams.has('ids')).toBe(false); + expect(parsed.searchParams.has('empty[]')).toBe(false); + }); }); describe('error handling', () => { diff --git a/src/__tests__/generation.test.ts b/src/__tests__/generation.test.ts index 4aa460d..c398775 100644 --- a/src/__tests__/generation.test.ts +++ b/src/__tests__/generation.test.ts @@ -106,11 +106,11 @@ describe('waitForGeneration', () => { ).rejects.toThrow('Template error'); }); - it('throws on error status', async () => { + it('throws a generic message on failure without a cause', async () => { const { client } = createClient([ { status: 200, - body: { document: { ...docFixture, status: 'error', failure_cause: null } }, + body: { document: { ...docFixture, status: 'failure', failure_cause: null } }, }, ]); diff --git a/src/__tests__/resources.test.ts b/src/__tests__/resources.test.ts index 75553de..3e943df 100644 --- a/src/__tests__/resources.test.ts +++ b/src/__tests__/resources.test.ts @@ -360,6 +360,43 @@ describe('DocumentCards', () => { expect(parsed.searchParams.get('q[updated_since]')).toBe('1700000000'); }); + it('lists document cards matching a search and any of several statuses', async () => { + const { client, fetch } = createClient([ + { + status: 200, + body: { + document_cards: [], + meta: { current_page: 1, total_pages: 1, next_page: null, prev_page: null }, + }, + }, + ]); + + await client.documentCards.list({ search: 'invoice', status: ['success', 'failure'] }); + + const parsed = new URL(fetch.mock.calls[0]?.[0] as string); + expect(parsed.searchParams.get('q[search]')).toBe('invoice'); + expect(parsed.searchParams.getAll('q[status][]')).toEqual(['success', 'failure']); + expect(parsed.searchParams.has('q[status]')).toBe(false); + }); + + it('treats an empty status array as no status filter', async () => { + const { client, fetch } = createClient([ + { + status: 200, + body: { + document_cards: [], + meta: { current_page: 1, total_pages: 1, next_page: null, prev_page: null }, + }, + }, + ]); + + await client.documentCards.list({ status: [] }); + + const parsed = new URL(fetch.mock.calls[0]?.[0] as string); + expect(parsed.searchParams.has('q[status][]')).toBe(false); + expect(parsed.searchParams.has('q[status]')).toBe(false); + }); + it('lists document cards without params (empty query)', async () => { const { client, fetch } = createClient([ { @@ -656,7 +693,7 @@ describe('WorkspaceCards', () => { expect(new URL(fetch.mock.calls[0]?.[0] as string).searchParams.get('page')).toBe('all'); }); - it('updates a workspace card with PATCH', async () => { + it('updates a workspace card with PUT', async () => { const { client, fetch } = createClient([ { status: 200, body: { workspace_card: { id: 'ws_1', identifier: 'renamed' } } }, ]); @@ -666,7 +703,7 @@ describe('WorkspaceCards', () => { expect(card.identifier).toBe('renamed'); const [url, init] = fetch.mock.calls[0] as [string, RequestInit]; expect(url).toContain('/workspace_cards/ws_1'); - expect(init.method).toBe('PATCH'); + expect(init.method).toBe('PUT'); expect(JSON.parse(init.body as string)).toEqual({ workspace_card: { identifier: 'renamed' } }); }); }); diff --git a/src/client.ts b/src/client.ts index 0025656..515ed44 100644 --- a/src/client.ts +++ b/src/client.ts @@ -75,7 +75,13 @@ export interface Logger { error(message: string, data?: Record): void; } -export type QueryValue = string | number | boolean | null | undefined; +export type QueryValue = + | string + | number + | boolean + | null + | undefined + | readonly (string | number | boolean)[]; export interface RequestOptions { body?: Record; @@ -385,7 +391,10 @@ function buildURL(baseURL: string, path: string, query?: Record(this._client, '/workspace_cards', 'workspace_cards', options); } - /** Update a workspace by ID. Uses PATCH. */ + /** Update a workspace by ID. Uses PUT. */ async update( id: string, params: WorkspaceCardUpdateParams, options?: ResourceRequestOptions, ): Promise { - const response = await this._client.patch( + const response = await this._client.put( `/workspace_cards/${encodeURIComponent(id)}`, { ...options, body: { workspace_card: params } }, ); diff --git a/src/webhooks.ts b/src/webhooks.ts index fb58e6d..a7c043c 100644 --- a/src/webhooks.ts +++ b/src/webhooks.ts @@ -27,7 +27,7 @@ export interface DocumentDoneEventData { /** Payload shape for a `document.error` webhook event. */ export interface DocumentErrorEventData { readonly id: string; - readonly status: 'failure' | 'error'; + readonly status: 'failure'; readonly failure_cause: string | null; readonly app_id?: string; readonly document_template_id?: string;