generated from coulomb/repo-seed
- Add isInProduction() helper for checking status 3 or 4 - Add getErrorSummary() helper for extracting error messages - Add fetchAllPages() pagination helper for auto-pagination - Add comprehensive JSDoc to ListResponse interface - Create ADR-001 documenting decision not to add listAll() method Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
374 lines
11 KiB
TypeScript
374 lines
11 KiB
TypeScript
/**
|
|
* Convenience Layer - Optional Helpers
|
|
*
|
|
* These helpers are purely additive and do not replace core API methods.
|
|
* They provide convenient predicates and utilities for common operations.
|
|
*/
|
|
|
|
import { DocumentStatus, type Document, type ListResponse, type Sending, type ValidationMessage } from './types.js';
|
|
|
|
/**
|
|
* Helper to get status code from document or sending
|
|
*/
|
|
function getStatusCode(doc: Document | Sending): DocumentStatus {
|
|
// Document has status.code, Sending might have status directly
|
|
if (typeof doc.status === 'object' && 'code' in doc.status) {
|
|
return doc.status.code;
|
|
}
|
|
return doc.status as unknown as DocumentStatus;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Status Predicates
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Check if a document is shippable (status 2).
|
|
*/
|
|
export function isShippable(doc: Document | Sending): boolean {
|
|
return getStatusCode(doc) === DocumentStatus.SHIPPABLE;
|
|
}
|
|
|
|
/**
|
|
* Check if a document has errors (status 7).
|
|
*/
|
|
export function isErroneous(doc: Document | Sending): boolean {
|
|
return getStatusCode(doc) === DocumentStatus.ERRONEOUS;
|
|
}
|
|
|
|
/**
|
|
* Check if a document is still being prepared (status 1).
|
|
*/
|
|
export function isInPreparation(doc: Document | Sending): boolean {
|
|
return getStatusCode(doc) === DocumentStatus.IN_PREPARATION;
|
|
}
|
|
|
|
/**
|
|
* Check if a document is in the production queue (status 3).
|
|
*/
|
|
export function isInProductionQueue(doc: Document | Sending): boolean {
|
|
return getStatusCode(doc) === DocumentStatus.PRODUCTION_QUEUE;
|
|
}
|
|
|
|
/**
|
|
* Check if a document is currently being printed (status 4).
|
|
*/
|
|
export function isPrinting(doc: Document | Sending): boolean {
|
|
return getStatusCode(doc) === DocumentStatus.PRINTING;
|
|
}
|
|
|
|
/**
|
|
* Check if a document has been sent (status 5).
|
|
*/
|
|
export function isSent(doc: Document | Sending): boolean {
|
|
return getStatusCode(doc) === DocumentStatus.SENT;
|
|
}
|
|
|
|
/**
|
|
* Check if a document was canceled (status 6).
|
|
*/
|
|
export function isCanceled(doc: Document | Sending): boolean {
|
|
return getStatusCode(doc) === DocumentStatus.CANCELED;
|
|
}
|
|
|
|
/**
|
|
* Check if a document is in production (status 3 or 4).
|
|
* This includes documents in the production queue and those currently printing.
|
|
*/
|
|
export function isInProduction(doc: Document | Sending): boolean {
|
|
const status = getStatusCode(doc);
|
|
return (
|
|
status === DocumentStatus.PRODUCTION_QUEUE ||
|
|
status === DocumentStatus.PRINTING
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if a document is in a terminal state (sent, canceled, or erroneous).
|
|
*/
|
|
export function isTerminal(doc: Document | Sending): boolean {
|
|
const status = getStatusCode(doc);
|
|
return (
|
|
status === DocumentStatus.SENT ||
|
|
status === DocumentStatus.CANCELED ||
|
|
status === DocumentStatus.ERRONEOUS
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if a document can still be canceled (in queue or printing).
|
|
*/
|
|
export function isCancelable(doc: Document | Sending): boolean {
|
|
const status = getStatusCode(doc);
|
|
return (
|
|
status === DocumentStatus.PRODUCTION_QUEUE ||
|
|
status === DocumentStatus.PRINTING
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Validation Helpers
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get all validation messages from a document.
|
|
*/
|
|
function getValidationMessages(doc: Document): ValidationMessage[] {
|
|
return doc.letter?.errors ?? [];
|
|
}
|
|
|
|
/**
|
|
* Extract error messages from validation results.
|
|
*/
|
|
export function getErrors(doc: Document): ValidationMessage[] {
|
|
return getValidationMessages(doc).filter((m: ValidationMessage) => m.type === 'ERROR');
|
|
}
|
|
|
|
/**
|
|
* Extract warning messages from validation results.
|
|
*/
|
|
export function getWarnings(doc: Document): ValidationMessage[] {
|
|
return getValidationMessages(doc).filter((m: ValidationMessage) => m.type === 'WARNING');
|
|
}
|
|
|
|
/**
|
|
* Extract info messages from validation results.
|
|
*/
|
|
export function getInfoMessages(doc: Document): ValidationMessage[] {
|
|
return getValidationMessages(doc).filter((m: ValidationMessage) => m.type === 'INFO');
|
|
}
|
|
|
|
/**
|
|
* Check if document has any validation errors.
|
|
*/
|
|
export function hasErrors(doc: Document): boolean {
|
|
return getErrors(doc).length > 0;
|
|
}
|
|
|
|
/**
|
|
* Check if document has any validation warnings.
|
|
*/
|
|
export function hasWarnings(doc: Document): boolean {
|
|
return getWarnings(doc).length > 0;
|
|
}
|
|
|
|
/**
|
|
* Get a summary of all error messages from a document.
|
|
* Returns undefined if the document has no errors.
|
|
*
|
|
* @param doc - The document to extract error messages from
|
|
* @param separator - Separator between messages (default: '; ')
|
|
* @returns Concatenated error messages or undefined if no errors
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const doc = await client.documents.get(id);
|
|
* if (isErroneous(doc)) {
|
|
* console.error('Upload failed:', getErrorSummary(doc));
|
|
* }
|
|
* ```
|
|
*/
|
|
export function getErrorSummary(doc: Document, separator: string = '; '): string | undefined {
|
|
const errors = getErrors(doc);
|
|
if (errors.length === 0) {
|
|
return undefined;
|
|
}
|
|
return errors.map((e) => e.message).join(separator);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Status Description
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get human-readable description of document status.
|
|
*/
|
|
export function getStatusDescription(status: DocumentStatus): string {
|
|
switch (status) {
|
|
case DocumentStatus.IN_PREPARATION:
|
|
return 'In preparation';
|
|
case DocumentStatus.SHIPPABLE:
|
|
return 'Ready to ship';
|
|
case DocumentStatus.PRODUCTION_QUEUE:
|
|
return 'In production queue';
|
|
case DocumentStatus.PRINTING:
|
|
return 'Printing';
|
|
case DocumentStatus.SENT:
|
|
return 'Sent';
|
|
case DocumentStatus.CANCELED:
|
|
return 'Canceled';
|
|
case DocumentStatus.ERRONEOUS:
|
|
return 'Has errors';
|
|
default:
|
|
return 'Unknown status';
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Base64 Utilities
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Encode a file/blob to base64 string (browser environment).
|
|
* Returns a promise that resolves to the base64-encoded content.
|
|
*/
|
|
export function fileToBase64(file: Blob): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = (): void => {
|
|
const result = reader.result as string;
|
|
// Remove data URL prefix (e.g., "data:application/pdf;base64,")
|
|
const base64 = result.split(',')[1];
|
|
if (base64) {
|
|
resolve(base64);
|
|
} else {
|
|
reject(new Error('Failed to extract base64 content'));
|
|
}
|
|
};
|
|
reader.onerror = (): void => reject(reader.error);
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Encode a Buffer to base64 string (Node.js environment).
|
|
*/
|
|
export function bufferToBase64(buffer: Buffer): string {
|
|
return buffer.toString('base64');
|
|
}
|
|
|
|
// ============================================================================
|
|
// Polling Utilities (Opt-in, no default behavior)
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Options for polling operations.
|
|
*/
|
|
export interface PollOptions {
|
|
/** Interval between polls in milliseconds (default: 2000) */
|
|
intervalMs?: number;
|
|
/** Maximum number of poll attempts (default: 30) */
|
|
maxAttempts?: number;
|
|
/** Abort signal for cancellation */
|
|
signal?: AbortSignal;
|
|
}
|
|
|
|
/**
|
|
* Poll until a condition is met.
|
|
* This is an opt-in utility - no automatic polling occurs.
|
|
*
|
|
* @param fn - Function to poll that returns the current state
|
|
* @param condition - Condition to check against the result
|
|
* @param options - Polling options
|
|
* @returns The final result when condition is met
|
|
* @throws Error if max attempts exceeded or aborted
|
|
*/
|
|
export async function pollUntil<T>(
|
|
fn: () => Promise<T>,
|
|
condition: (result: T) => boolean,
|
|
options: PollOptions = {}
|
|
): Promise<T> {
|
|
const intervalMs = options.intervalMs ?? 2000;
|
|
const maxAttempts = options.maxAttempts ?? 30;
|
|
|
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
if (options.signal?.aborted) {
|
|
throw new Error('Polling aborted');
|
|
}
|
|
|
|
const result = await fn();
|
|
|
|
if (condition(result)) {
|
|
return result;
|
|
}
|
|
|
|
if (attempt < maxAttempts - 1) {
|
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
}
|
|
}
|
|
|
|
throw new Error(`Polling exceeded maximum attempts (${maxAttempts})`);
|
|
}
|
|
|
|
/**
|
|
* Wait for a document to reach a shippable state.
|
|
* Convenience wrapper around pollUntil.
|
|
*
|
|
* @param getDocument - Function that fetches the document
|
|
* @param options - Polling options
|
|
* @returns The document when it becomes shippable or erroneous
|
|
*/
|
|
export async function waitForShippable(
|
|
getDocument: () => Promise<Document>,
|
|
options: PollOptions = {}
|
|
): Promise<Document> {
|
|
return pollUntil(
|
|
getDocument,
|
|
(doc) => isShippable(doc) || isErroneous(doc),
|
|
options
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Pagination Utilities
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Options for fetchAllPages helper.
|
|
*/
|
|
export interface FetchAllPagesOptions {
|
|
/** Number of items per page (default: 100) */
|
|
pageSize?: number;
|
|
/** Abort signal for cancellation */
|
|
signal?: AbortSignal;
|
|
}
|
|
|
|
/**
|
|
* Fetch all pages from a paginated list endpoint.
|
|
* This is an opt-in convenience helper for retrieving complete result sets.
|
|
*
|
|
* @param fetchPage - Function that fetches a single page given limit and offset
|
|
* @param options - Pagination options
|
|
* @returns Array of all items from all pages
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* // Fetch all shippable documents
|
|
* const allDocs = await fetchAllPages(
|
|
* (limit, offset) => client.documents.list({ limit, offset })
|
|
* );
|
|
*
|
|
* // Fetch all erroneous documents with custom page size
|
|
* const allErrors = await fetchAllPages(
|
|
* (limit, offset) => client.documents.listErrors({ limit, offset }),
|
|
* { pageSize: 50 }
|
|
* );
|
|
* ```
|
|
*/
|
|
export async function fetchAllPages<T>(
|
|
fetchPage: (limit: number, offset: number) => Promise<ListResponse<T>>,
|
|
options: FetchAllPagesOptions = {}
|
|
): Promise<T[]> {
|
|
const pageSize = options.pageSize ?? 100;
|
|
const allItems: T[] = [];
|
|
let offset = 0;
|
|
|
|
while (true) {
|
|
if (options.signal?.aborted) {
|
|
throw new Error('Fetch aborted');
|
|
}
|
|
|
|
const response = await fetchPage(pageSize, offset);
|
|
allItems.push(...response.items);
|
|
|
|
// Check if we've fetched all items
|
|
if (response.items.length < pageSize || allItems.length >= response.total) {
|
|
break;
|
|
}
|
|
|
|
offset += pageSize;
|
|
}
|
|
|
|
return allItems;
|
|
}
|