Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
3 changes: 3 additions & 0 deletions packages/corsair/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export const BaseProviders = [
'oura',
'outlook',
'pagerduty',
'pdfmonkey',
'perplexityai',
'posthog',
'razorpay',
Expand Down Expand Up @@ -341,6 +342,7 @@ export const ProviderDisplayNames = {
oura: 'Oura',
outlook: 'Outlook',
pagerduty: 'PagerDuty',
pdfmonkey: 'PDFMonkey',
perplexityai: 'Perplexity AI',
posthog: 'PostHog',
razorpay: 'Razorpay',
Expand Down Expand Up @@ -536,6 +538,7 @@ export type AllProviders =
| 'oura'
| 'outlook'
| 'pagerduty'
| 'pdfmonkey'
| 'perplexityai'
| 'posthog'
| 'razorpay'
Expand Down
161 changes: 161 additions & 0 deletions packages/pdfmonkey/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import type { ApiRequestOptions, OpenAPIConfig } from 'corsair/http';
import { ApiError, request } from 'corsair/http';

export class Api2PdfAPIError extends Error {
public readonly status?: number;
public readonly statusText?: string;
// API error bodies vary by endpoint; unknown forces callers to narrow before use.
public readonly body?: unknown;
public readonly retryAfter?: number;

constructor(
message: string,
public readonly code?: number,
options?: { cause?: Error },
) {
super(message, options);
this.name = 'Api2PdfAPIError';

if (options?.cause instanceof ApiError) {
this.status = options.cause.status;
this.statusText = options.cause.statusText;
this.body = options.cause.body;
this.retryAfter = options.cause.retryAfter;
}
}
}

const API2PDF_API_BASE = 'https://api.pdfmonkey.io';

export type PdfMonkeyRequestOptions = {
apiKey?: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
// Endpoint payloads differ per operation; Record keeps the client generic.
body?: Record<string, unknown>;
query?: Record<string, string | number | boolean | undefined>;
};

function buildConfig(apiKey?: string, isWrite = false): OpenAPIConfig {
return {
BASE: API2PDF_API_BASE,
VERSION: '2.0.0',
WITH_CREDENTIALS: false,
CREDENTIALS: 'omit',
TOKEN: undefined,
HEADERS: {
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
...(isWrite ? { 'Content-Type': 'application/json' } : {}),
},
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// Catch values are untyped at runtime; unknown forces narrowing to ApiError/Error
// before rethrowing as Api2PdfAPIError.
async function handleRequestError(error: unknown): Promise<never> {
if (error instanceof Api2PdfAPIError) {
throw error;
}
if (error instanceof ApiError) {
throw new Api2PdfAPIError(error.message, error.status, {
cause: error,
});
}
if (error instanceof Error) {
throw new Api2PdfAPIError(error.message, undefined, { cause: error });
}
throw new Api2PdfAPIError('Unknown error');
}

/**
* Performs a request to the PDFMonkey REST API.
*
* Auth: API key via the `Authorization` header using `Bearer <secret_key>`.
* The `/status` health check does not require authentication.
*/
export async function makePdfMonkeyRequest<T>(
endpoint: string,
options: PdfMonkeyRequestOptions = {},
): Promise<T> {
const { apiKey, method = 'GET', body, query = {} } = options;
const isWrite = method === 'POST' || method === 'PUT' || method === 'PATCH';

const config = buildConfig(apiKey, isWrite);

const requestOptions: ApiRequestOptions = {
method,
url: endpoint,
body: isWrite ? body : undefined,
mediaType: isWrite ? 'application/json; charset=utf-8' : undefined,
query,
};

try {
return await request<T>(config, requestOptions);
} catch (error) {
return handleRequestError(error);
}
}

/** Plain-text health check (returns e.g. "OK"). */
export async function makePdfMonkeyTextRequest(
endpoint: string,
options: Pick<PdfMonkeyRequestOptions, 'apiKey' | 'method' | 'query'> = {},
): Promise<string> {
const { apiKey, method = 'GET', query = {} } = options;
const config = buildConfig(apiKey);

const requestOptions: ApiRequestOptions = {
method,
url: endpoint,
query,
};

try {
const response = await request<string>(config, requestOptions);
return typeof response === 'string' ? response : String(response);
} catch (error) {
return handleRequestError(error);
}
}

export function assertApi2PdfSuccess<
// Error field shape varies by endpoint (string | object | null); unknown forces
// callers to narrow before reading it.
T extends { Success?: boolean; Error?: unknown },
>(response: T): T {
if (response.Success === false) {
const message =
typeof response.Error === 'string'
? response.Error
: 'API2PDF request failed';
throw new Api2PdfAPIError(message);
}
return response;
}

// Endpoint payloads differ per operation; Record keeps the client generic across
// chrome/pdfsharp/libreoffice field sets without a union of every wire shape.
export function buildPostPayload(
fields: Record<string, unknown>,
options?: {
inline?: boolean;
fileName?: string;
// Headless Chrome options bag is open-ended upstream.
chromeOptions?: Record<string, unknown>;
},
): Record<string, unknown> {
const payload: Record<string, unknown> = {
inline: options?.inline ?? true,
...fields,
};

if (options?.fileName) {
payload.fileName = options.fileName;
}

if (options?.chromeOptions) {
payload.options = options.chromeOptions;
}

return payload;
}
204 changes: 204 additions & 0 deletions packages/pdfmonkey/endpoints/documents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { logEventFromContext } from 'corsair/core';
import { makePdfMonkeyRequest } from '../client';
import type { PDFMonkeyEndpoints } from '../index';
import type {
PDFMonkeyEndpointInputs,
PDFMonkeyEndpointOutputs,
} from './types';

/** Create a document (async, queues for generation) */
export const createDocument: PDFMonkeyEndpoints['createDocument'] = async (
ctx,
input,
) => {
const response = await makePdfMonkeyRequest<
PDFMonkeyEndpointOutputs['createDocument']
>('/api/v1/documents', {
apiKey: ctx.key,
method: 'POST',
body: {
document: {
document_template_id: input.document.document_template_id,
status: input.document.status,
payload: input.document.payload,
meta: input.document.meta,
},
},
});

await logEventFromContext(
ctx,
'pdfmonkey.documents.createDocument',
{
document_template_id: input.document.document_template_id,
status: input.document.status,
},
'completed',
);

return response;
};

/** Create a document synchronously (waits for generation to complete) */
export const createDocumentSync: PDFMonkeyEndpoints['createDocumentSync'] =
async (ctx, input) => {
const response = await makePdfMonkeyRequest<
PDFMonkeyEndpointOutputs['createDocumentSync']
>('/api/v1/documents/sync', {
apiKey: ctx.key,
method: 'POST',
body: {
document: {
document_template_id: input.document.document_template_id,
status: input.document.status,
payload: input.document.payload,
meta: input.document.meta,
},
},
});

await logEventFromContext(
ctx,
'pdfmonkey.documents.createDocumentSync',
{
document_template_id: input.document.document_template_id,
status: input.document.status,
},
'completed',
);

return response;
};

/** Get a document card (status + download URL) */
export const getDocumentCard: PDFMonkeyEndpoints['getDocumentCard'] = async (
ctx,
input,
) => {
const response = await makePdfMonkeyRequest<
PDFMonkeyEndpointOutputs['getDocumentCard']
>('/api/v1/document_cards/' + input.id, {
apiKey: ctx.key,
method: 'GET',
});

await logEventFromContext(
ctx,
'pdfmonkey.documents.getDocumentCard',
{ id: input.id },
'completed',
);

return response;
};

/** List document cards (paginated with filters) */
export const listDocumentCards: PDFMonkeyEndpoints['listDocumentCards'] =
async (ctx, input) => {
const response = await makePdfMonkeyRequest<
PDFMonkeyEndpointOutputs['listDocumentCards']
>('/api/v1/document_cards', {
apiKey: ctx.key,
method: 'GET',
query: {
page: input.page,
q_document_template_id: input.q_document_template_id,
q_status: input.q_status,
q_workspace_id: input.q_workspace_id,
q_updated_since: input.q_updated_since,
q_search: input.q_search,
},
});

await logEventFromContext(
ctx,
'pdfmonkey.documents.listDocumentCards',
{
page: input.page,
q_status: input.q_status,
},
'completed',
);

return response;
};

/** Get a full document (with payload and generation logs) */
export const getDocument: PDFMonkeyEndpoints['getDocument'] = async (
ctx,
input,
) => {
const response = await makePdfMonkeyRequest<
PDFMonkeyEndpointOutputs['getDocument']
>('/api/v1/documents/' + input.id, {
apiKey: ctx.key,
method: 'GET',
});

await logEventFromContext(
ctx,
'pdfmonkey.documents.getDocument',
{ id: input.id },
'completed',
);

return response;
};

/** Update a document */
export const updateDocument: PDFMonkeyEndpoints['updateDocument'] = async (
ctx,
input,
) => {
const document = input.document;
if (!document) {
throw new Error('document is required for update');
}
const body: Record<string, unknown> = {};
if (document.document_template_id !== undefined)
body.document_template_id = document.document_template_id;
if (document.status !== undefined) body.status = document.status;
if (document.payload !== undefined) body.payload = document.payload;
if (document.meta !== undefined) body.meta = document.meta;

const response = await makePdfMonkeyRequest<
PDFMonkeyEndpointOutputs['updateDocument']
>('/api/v1/documents/' + input.document_id, {
apiKey: ctx.key,
method: 'PUT',
body: {
document: body,
},
});

await logEventFromContext(
ctx,
'pdfmonkey.documents.updateDocument',
{ document_id: input.document_id },
'completed',
);

return response;
};

/** Delete a document */
export const deleteDocument: PDFMonkeyEndpoints['deleteDocument'] = async (
ctx,
input,
) => {
const response = await makePdfMonkeyRequest<
PDFMonkeyEndpointOutputs['deleteDocument']
>('/api/v1/documents/' + input.id, {
apiKey: ctx.key,
method: 'DELETE',
});

await logEventFromContext(
ctx,
'pdfmonkey.documents.deleteDocument',
{ id: input.id },
'completed',
);

return response;
};
Loading
Loading