Files
binect-js/src/helpers.ts
tegwick 397cd10a09 Add convenience helpers and improve documentation
- 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>
2026-01-16 22:30:36 +01:00

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;
}