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
7 changes: 7 additions & 0 deletions .changeset/document-card-filters.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -164,6 +164,9 @@ All list methods return a `Page<T>` 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
Expand Down
8 changes: 8 additions & 0 deletions src/__tests__/api.integration.test.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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,
Expand Down
13 changes: 13 additions & 0 deletions src/__tests__/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof vi.fn>).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', () => {
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/generation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } },
},
]);

Expand Down
41 changes: 39 additions & 2 deletions src/__tests__/resources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
{
Expand Down Expand Up @@ -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' } } },
]);
Expand All @@ -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' } });
});
});
Expand Down
13 changes: 11 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,13 @@ export interface Logger {
error(message: string, data?: Record<string, unknown>): 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<string, unknown>;
Expand Down Expand Up @@ -385,7 +391,10 @@ function buildURL(baseURL: string, path: string, query?: Record<string, QueryVal
const url = new URL(`${baseURL}${normalizedPath}`);
if (query) {
for (const [key, value] of Object.entries(query)) {
if (value !== undefined && value !== null) {
if (Array.isArray(value)) {
// Rails array convention: q[status][]=a&q[status][]=b
for (const item of value) url.searchParams.append(`${key}[]`, String(item));
} else if (value !== undefined && value !== null) {
url.searchParams.set(key, String(value));
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/resources/document-cards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ export interface DocumentCard {
export interface DocumentCardListParams {
page?: number;
document_template_id?: string;
status?: DocumentStatus;
/** A document ID matches exactly; any other text matches part of the filename (case-sensitive). */
search?: string;
/** One status, or several to match any of them. An empty array applies no status filter. */
status?: DocumentStatus | DocumentStatus[];
workspace_id?: string;
updated_since?: number;
}
Expand All @@ -46,6 +49,7 @@ export class DocumentCards extends APIResource {
const query = buildListQuery(
{
document_template_id: params?.document_template_id,
search: params?.search,
status: params?.status,
workspace_id: params?.workspace_id,
updated_since: params?.updated_since,
Expand Down
5 changes: 5 additions & 0 deletions src/resources/document-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ export interface DocumentTemplateUpdateParams {
export interface DocumentTemplateListParams {
page?: number;
workspace_id?: string;
/**
* `'all'` (default), `'none'` for templates outside any folder,
* or a comma-separated list of folder IDs.
*/
folders?: string;
/** Attribute to sort by, prefixed with `-` for descending (e.g. `'identifier'`, `'-updated_at'`). */
sort?: string;
}

Expand Down
4 changes: 2 additions & 2 deletions src/resources/documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { DocumentCard } from './document-cards.js';

// ── Types ──────────────────────────────────────────────────────────────────

export type DocumentStatus = 'draft' | 'pending' | 'generating' | 'success' | 'failure' | 'error';
export type DocumentStatus = 'draft' | 'pending' | 'generating' | 'success' | 'failure';

export interface Document {
readonly id: string;
Expand Down Expand Up @@ -299,7 +299,7 @@ export class Documents extends APIResource {
return doc;
}

if (doc.status === 'failure' || doc.status === 'error') {
if (doc.status === 'failure') {
throw new PDFMonkeyError(
`Document generation failed: ${doc.failure_cause ?? 'Unknown error'}`,
);
Expand Down
4 changes: 2 additions & 2 deletions src/resources/workspace-cards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ export class WorkspaceCards extends APIResource {
return fetchAll<WorkspaceCard>(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<WorkspaceCard> {
const response = await this._client.patch<WorkspaceCardResponse>(
const response = await this._client.put<WorkspaceCardResponse>(
`/workspace_cards/${encodeURIComponent(id)}`,
{ ...options, body: { workspace_card: params } },
);
Expand Down
2 changes: 1 addition & 1 deletion src/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading