generated from coulomb/repo-seed
Add Binect SDK implementation, Explorer, and test suite
SDK (@binect/js): - BinectClient with domain sub-clients (documents, sendings, accounts, attachments, invoices) - HTTP Basic Auth, native fetch only (no runtime dependencies) - TypeScript types matching Binect API vocabulary - Status predicates and polling helpers in helpers.ts - Structured error handling (BinectApiError, BinectAuthError) Explorer: - Standalone browser-based API explorer (explorer/index.html) - Interactive testing without code Tests: - Unit tests for client, types, errors, helpers, http - E2E tests for upload/delete and send/cancel workflows Also includes: - Architecture Decision Records (ADRs) - Example DIN 5008 letter PDFs for testing - API specification research notes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
274
src/helpers.ts
Normal file
274
src/helpers.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* 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 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 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;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user