diff --git a/.changeset/list-workspace-filter-and-list-all.md b/.changeset/list-workspace-filter-and-list-all.md new file mode 100644 index 0000000..259ccd6 --- /dev/null +++ b/.changeset/list-workspace-filter-and-list-all.md @@ -0,0 +1,5 @@ +--- +"pdfmonkey": minor +--- + +Adding a required `workspace_id` to `snippets.list()` and `templateFolders.list()` (the API returns nothing without it), and a `listAll()` method on workspaces, document templates, snippets and template folders that fetches every item in a single `page=all` request. diff --git a/CHANGELOG.md b/CHANGELOG.md index 44e1b8f..ebdf7c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # pdfmonkey +## Unreleased + +- `snippets.list()` and `templateFolders.list()` now require `workspace_id`; the API returns nothing without it. +- Adding `listAll()` to workspaces, document templates, snippets and template folders to fetch every item in a single `page=all` request. + ## 1.0.0 ### Major Changes - a0a2381: Initial public release of the PDFMonkey Node.js SDK — a zero-dependency, dual ESM + CommonJS client for the PDFMonkey API. Covers documents (create/get/update/delete, synchronous generation, polling, and PDF download/stream), document cards, templates, template folders, snippets, PDF engines, rest hooks, workspaces, and the current user. Includes paginated list helpers, a typed error hierarchy, request/response/error hooks, configurable retries with backoff, and Web Crypto webhook signature verification for Node 20+ and edge runtimes. - -## Unreleased - -- Initial release. diff --git a/README.md b/README.md index 900b633..73be21a 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,15 @@ if (page.hasNextPage()) { } ``` +Workspaces, document templates, snippets and template folders also expose `listAll()`, which fetches every item in a single request (`page=all`) and returns a plain array. Document cards do not support it, so iterate with `for await (const card of page)` instead. + +```ts +const workspaces = await client.workspaces.listAll(); +const templates = await client.documentTemplates.listAll({ workspace_id: 'ws_xxx' }); +const snippets = await client.snippets.listAll({ workspace_id: 'ws_xxx' }); +const folders = await client.templateFolders.listAll({ workspace_id: 'ws_xxx' }); +``` + ### Webhooks Register webhook endpoints: @@ -273,11 +282,11 @@ export async function POST(request: Request): Promise { ```ts // Snippets -const snippets = await client.snippets.list(); +const snippets = await client.snippets.list({ workspace_id: 'ws_xxx' }); await client.snippets.create({ identifier: 'header', code: '
Header
', workspace_id: 'ws_xxx' }); // Template Folders -const folders = await client.templateFolders.list(); +const folders = await client.templateFolders.list({ workspace_id: 'ws_xxx' }); // Workspaces const workspaces = await client.workspaces.list(); diff --git a/llms.txt b/llms.txt index 6bd9eae..fea16b4 100644 --- a/llms.txt +++ b/llms.txt @@ -159,7 +159,7 @@ const engines = await client.pdfEngines.list(); ### Template Folders ```ts -const page = await client.templateFolders.list(); +const page = await client.templateFolders.list({ workspace_id: 'ws_xxx' }); const folder = await client.templateFolders.get('fold_xxx'); const created = await client.templateFolders.create({ identifier: 'invoices' }); const updated = await client.templateFolders.update('fold_xxx', { identifier: 'reports' }); @@ -171,7 +171,7 @@ await client.templateFolders.delete('fold_xxx'); Reusable HTML partials shared across templates. ```ts -const page = await client.snippets.list(); +const page = await client.snippets.list({ workspace_id: 'ws_xxx' }); const snippet = await client.snippets.get('snp_xxx'); const created = await client.snippets.create({ identifier: 'header', @@ -247,6 +247,15 @@ const next = await page.getNextPage(); // throws if no next page const prev = await page.getPreviousPage(); // throws if no previous page ``` +`listAll()` on workspaces, documentTemplates, snippets and templateFolders fetches everything in one request (`page=all`) and returns `T[]`. Not available on documentCards. + +```ts +const workspaces = await client.workspaces.listAll(); +const templates = await client.documentTemplates.listAll({ workspace_id: 'ws_xxx' }); +const snippets = await client.snippets.listAll({ workspace_id: 'ws_xxx' }); +const folders = await client.templateFolders.listAll({ workspace_id: 'ws_xxx' }); +``` + ## Error Handling All errors extend `PDFMonkeyError`. diff --git a/src/__tests__/resources.test.ts b/src/__tests__/resources.test.ts index 4766b0e..44e253f 100644 --- a/src/__tests__/resources.test.ts +++ b/src/__tests__/resources.test.ts @@ -485,7 +485,7 @@ describe('TemplateFolders', () => { }, ]); - await client.templateFolders.list({ page: 2 }); + await client.templateFolders.list({ workspace_id: 'ws_1', page: 2 }); const [url] = fetch.mock.calls[0] as [string]; const parsed = new URL(url); @@ -493,7 +493,7 @@ describe('TemplateFolders', () => { }); it('lists folders', async () => { - const { client } = createClient([ + const { client, fetch } = createClient([ { status: 200, body: { @@ -503,8 +503,22 @@ describe('TemplateFolders', () => { }, ]); - const page = await client.templateFolders.list(); + const page = await client.templateFolders.list({ workspace_id: 'ws_1' }); expect(page.data).toHaveLength(1); + const parsed = new URL(fetch.mock.calls[0]?.[0] as string); + expect(parsed.searchParams.get('q[workspace_id]')).toBe('ws_1'); + }); + + it('lists all folders in a single request', async () => { + const { client, fetch } = createClient([ + { status: 200, body: { template_folders: [{ id: 'fold_1' }, { id: 'fold_2' }] } }, + ]); + + const folders = await client.templateFolders.listAll({ workspace_id: 'ws_1' }); + expect(folders).toHaveLength(2); + const parsed = new URL(fetch.mock.calls[0]?.[0] as string); + expect(parsed.searchParams.get('page')).toBe('all'); + expect(parsed.searchParams.get('q[workspace_id]')).toBe('ws_1'); }); it('gets a folder by ID', async () => { @@ -588,6 +602,16 @@ describe('Workspaces', () => { const ws = await client.workspaces.get('ws_1'); expect(ws.identifier).toBe('main'); }); + + it('lists all workspaces in a single request', async () => { + const { client, fetch } = createClient([ + { status: 200, body: { workspaces: [{ id: 'ws_1' }, { id: 'ws_2' }] } }, + ]); + + const workspaces = await client.workspaces.listAll(); + expect(workspaces).toHaveLength(2); + expect(new URL(fetch.mock.calls[0]?.[0] as string).searchParams.get('page')).toBe('all'); + }); }); // ── Snippets ─────────────────────────────────────────────────────────────── @@ -609,7 +633,7 @@ describe('Snippets', () => { }); it('lists snippets', async () => { - const { client } = createClient([ + const { client, fetch } = createClient([ { status: 200, body: { @@ -619,8 +643,29 @@ describe('Snippets', () => { }, ]); - const page = await client.snippets.list(); + const page = await client.snippets.list({ workspace_id: 'ws_1' }); expect(page.data).toHaveLength(1); + const parsed = new URL(fetch.mock.calls[0]?.[0] as string); + expect(parsed.searchParams.get('q[workspace_id]')).toBe('ws_1'); + }); + + it('lists all snippets in a single request', async () => { + const { client, fetch } = createClient([ + { status: 200, body: { snippets: [{ id: 'snp_1' }, { id: 'snp_2' }] } }, + ]); + + const snippets = await client.snippets.listAll({ workspace_id: 'ws_1' }); + expect(snippets).toHaveLength(2); + const parsed = new URL(fetch.mock.calls[0]?.[0] as string); + expect(parsed.searchParams.get('page')).toBe('all'); + expect(parsed.searchParams.get('q[workspace_id]')).toBe('ws_1'); + }); + + it('rejects a malformed listAll response', async () => { + const { client } = createClient([{ status: 200, body: { snippet: {} } }]); + await expect(client.snippets.listAll({ workspace_id: 'ws_1' })).rejects.toThrow( + 'expected "snippets" to be an array', + ); }); it('lists snippets with page param', async () => { @@ -634,7 +679,7 @@ describe('Snippets', () => { }, ]); - await client.snippets.list({ page: 2 }); + await client.snippets.list({ workspace_id: 'ws_1', page: 2 }); const [url] = fetch.mock.calls[0] as [string]; const parsed = new URL(url); diff --git a/src/pagination.ts b/src/pagination.ts index 61584d6..0c82172 100644 --- a/src/pagination.ts +++ b/src/pagination.ts @@ -173,3 +173,24 @@ export async function fetchPage( } return new Page(client, path, opts?.query ?? {}, data as T[], response.meta, extractKey); } + +/** + * Fetch every item of a list endpoint in a single request (`page=all`). + * Only endpoints backed by the generic index support it; document cards do not. + */ +export async function fetchAll( + client: PDFMonkey, + path: string, + extractKey: string, + opts?: RequestOptions, +): Promise { + const response = await client.get>(path, { + ...opts, + query: { ...opts?.query, page: 'all' }, + }); + const data = response[extractKey]; + if (!Array.isArray(data)) { + throw new PDFMonkeyError(`Invalid list response: expected "${extractKey}" to be an array`); + } + return data as T[]; +} diff --git a/src/resources/document-templates.ts b/src/resources/document-templates.ts index 36b7dd7..b240558 100644 --- a/src/resources/document-templates.ts +++ b/src/resources/document-templates.ts @@ -1,5 +1,5 @@ import type { ResourceRequestOptions } from '../client.js'; -import { buildListQuery, fetchPage, type Page } from '../pagination.js'; +import { buildListQuery, fetchAll, fetchPage, type Page } from '../pagination.js'; import { APIResource } from '../resource.js'; // ── Types ────────────────────────────────────────────────────────────────── @@ -121,6 +121,23 @@ export class DocumentTemplates extends APIResource { ); } + /** List all document template cards in a single request. */ + async listAll( + params?: Omit, + options?: ResourceRequestOptions, + ): Promise { + const query = buildListQuery( + { workspace_id: params?.workspace_id, folders: params?.folders }, + { sort: params?.sort }, + ); + return fetchAll( + this._client, + '/document_template_cards', + 'document_template_cards', + { ...options, query }, + ); + } + /** Retrieve a document template by ID. */ async get(id: string, options?: ResourceRequestOptions): Promise { const response = await this._client.get( diff --git a/src/resources/snippets.ts b/src/resources/snippets.ts index e916fa8..f085630 100644 --- a/src/resources/snippets.ts +++ b/src/resources/snippets.ts @@ -1,5 +1,5 @@ import type { ResourceRequestOptions } from '../client.js'; -import { buildListQuery, fetchPage, type Page } from '../pagination.js'; +import { buildListQuery, fetchAll, fetchPage, type Page } from '../pagination.js'; import { APIResource } from '../resource.js'; // ── Types ────────────────────────────────────────────────────────────────── @@ -27,6 +27,8 @@ export interface SnippetUpdateParams { } export interface SnippetListParams { + /** Required: the API returns no snippets without a workspace filter. */ + workspace_id: string; page?: number; } @@ -38,12 +40,21 @@ interface SnippetResponse { /** Manage reusable HTML snippets shared across templates. */ export class Snippets extends APIResource { - /** List snippets. Returns a paginated result. */ - async list(params?: SnippetListParams, options?: ResourceRequestOptions): Promise> { - const query = buildListQuery({}, { page: params?.page }); + /** List a workspace's snippets. Returns a paginated result. */ + async list(params: SnippetListParams, options?: ResourceRequestOptions): Promise> { + const query = buildListQuery({ workspace_id: params.workspace_id }, { page: params.page }); return fetchPage(this._client, '/snippets', 'snippets', { ...options, query }); } + /** List all of a workspace's snippets in a single request. */ + async listAll( + params: Omit, + options?: ResourceRequestOptions, + ): Promise { + const query = buildListQuery({ workspace_id: params.workspace_id }); + return fetchAll(this._client, '/snippets', 'snippets', { ...options, query }); + } + /** Retrieve a snippet by ID. */ async get(id: string, options?: ResourceRequestOptions): Promise { const response = await this._client.get( diff --git a/src/resources/template-folders.ts b/src/resources/template-folders.ts index 71b0e26..bdee136 100644 --- a/src/resources/template-folders.ts +++ b/src/resources/template-folders.ts @@ -1,5 +1,5 @@ import type { ResourceRequestOptions } from '../client.js'; -import { buildListQuery, fetchPage, type Page } from '../pagination.js'; +import { buildListQuery, fetchAll, fetchPage, type Page } from '../pagination.js'; import { APIResource } from '../resource.js'; // ── Types ────────────────────────────────────────────────────────────────── @@ -21,6 +21,8 @@ export interface TemplateFolderUpdateParams { } export interface TemplateFolderListParams { + /** Required: the API returns no folders without a workspace filter. */ + workspace_id: string; page?: number; } @@ -32,18 +34,30 @@ interface TemplateFolderResponse { /** Manage folders for organizing document templates. */ export class TemplateFolders extends APIResource { - /** List template folders. Returns a paginated result. */ + /** List a workspace's template folders. Returns a paginated result. */ async list( - params?: TemplateFolderListParams, + params: TemplateFolderListParams, options?: ResourceRequestOptions, ): Promise> { - const query = buildListQuery({}, { page: params?.page }); + const query = buildListQuery({ workspace_id: params.workspace_id }, { page: params.page }); return fetchPage(this._client, '/template_folders', 'template_folders', { ...options, query, }); } + /** List all of a workspace's template folders in a single request. */ + async listAll( + params: Omit, + options?: ResourceRequestOptions, + ): Promise { + const query = buildListQuery({ workspace_id: params.workspace_id }); + return fetchAll(this._client, '/template_folders', 'template_folders', { + ...options, + query, + }); + } + /** Retrieve a template folder by ID. */ async get(id: string, options?: ResourceRequestOptions): Promise { const response = await this._client.get( diff --git a/src/resources/workspaces.ts b/src/resources/workspaces.ts index af6ab49..82f9335 100644 --- a/src/resources/workspaces.ts +++ b/src/resources/workspaces.ts @@ -1,5 +1,5 @@ import type { ResourceRequestOptions } from '../client.js'; -import { buildListQuery, fetchPage, type Page } from '../pagination.js'; +import { buildListQuery, fetchAll, fetchPage, type Page } from '../pagination.js'; import { APIResource } from '../resource.js'; // ── Types ────────────────────────────────────────────────────────────────── @@ -29,6 +29,11 @@ export class Workspaces extends APIResource { return fetchPage(this._client, '/workspaces', 'workspaces', { ...options, query }); } + /** List all workspaces in a single request. */ + async listAll(options?: ResourceRequestOptions): Promise { + return fetchAll(this._client, '/workspaces', 'workspaces', options); + } + /** Retrieve a workspace by ID. */ async get(id: string, options?: ResourceRequestOptions): Promise { const response = await this._client.get<{ workspace: Workspace }>(