@ritas-inc/sapb1commandapi-client
v1.5.3
Published
A stateless TypeScript client for SAP B1 Service Layer Command API with comprehensive error handling, type safety, and batch operations
Maintainers
Readme
@ritas-inc/sapb1commandapi-client
A stateless TypeScript client for the SAP B1 Service Layer Command API. This client provides a clean, type-safe interface for managing production plans and work orders in SAP Business One.
Features
- 🚀 Modern TypeScript: Built for Node.js 24+ with native ESM support
- 🔒 Type-safe: Full TypeScript support with Zod schema validation
- 🔄 Automatic retry: Built-in retry logic for network failures
- 🏗️ Stateless design: No session management needed - perfect for serverless
- 📦 Minimal dependencies: Only axios, axios-retry, and zod required
- 🛡️ Comprehensive error handling: Detailed error messages with RFC 7807 Problem Details format
- ✅ Consistent API responses: All responses follow a unified success/error structure
Requirements
- Node.js >= 24.0.0
- SAP B1 Service Layer Command API endpoint
Installation
npm install @ritas-inc/sapb1commandapi-clientQuick Start
import { SAPB1CommandClient } from '@ritas-inc/sapb1commandapi-client';
// Initialize the client
const client = new SAPB1CommandClient({
baseUrl: 'https://your-api-endpoint.com',
timeout: 30000,
retryConfig: {
retries: 3
}
});
// Authenticate
const authResponse = await client.auth.login({
dbName: 'YourDatabase',
user: 'manager',
password: 'your-password'
});
if (authResponse.success) {
const userId = authResponse.data.userId;
// Create a plan
const planResponse = await client.plans.create(userId, 12345, [
{ itemCode: 'ITEM001', quantity: 10 },
{ itemCode: 'ITEM002', quantity: 5 }
]);
if (planResponse.success) {
console.log(`Created plan with ID: ${planResponse.data.planId}`);
}
}Idempotency
All mutating operations (POST, PATCH, DELETE) support idempotency to prevent duplicate operations when requests are retried.
What is Idempotency?
Idempotency ensures that multiple identical requests have the same effect as a single request. This is critical for:
- Retry safety: Safely retry failed requests without creating duplicates
- Network resilience: Handle connection timeouts and network errors
- Reliability: Guarantee operations complete exactly once
Usage
Pass an optional idempotencyKey parameter (must be a valid UUID) to any mutating operation:
import { SAPB1CommandClient, generateIdempotencyKey } from '@ritas-inc/sapb1commandapi-client';
const client = new SAPB1CommandClient({ baseUrl: 'https://your-api.com' });
// Generate a UUID v4 idempotency key
const idempotencyKey = generateIdempotencyKey();
// Create a plan with idempotency
const response = await client.plans.create(
userId,
12345,
[{ itemCode: 'ITEM001', quantity: 10 }],
null, // salesFromDate
null, // salesToDate
idempotencyKey // UUID for idempotent requests
);
// If the request fails or times out, retry with the same key
if (!response.success && response.problem.type === 'network-error') {
// Retry with the SAME idempotency key
const retryResponse = await client.plans.create(
userId,
12345,
[{ itemCode: 'ITEM001', quantity: 10 }],
null,
null,
idempotencyKey // Same key = no duplicate created
);
}Key Requirements
- Format: Must be a valid UUID (v1-v5)
- Uniqueness: Each distinct operation should have a unique key
- Consistency: Use the same key for retrying the same operation
- Storage: Store the key for 24 hours (API retention period)
Utility Functions
import {
generateIdempotencyKey,
isValidIdempotencyKey
} from '@ritas-inc/sapb1commandapi-client';
// Generate a new UUID v4 key
const key = generateIdempotencyKey();
// "550e8400-e29b-41d4-a716-446655440000"
// Validate a key format
isValidIdempotencyKey('550e8400-e29b-41d4-a716-446655440000'); // true
isValidIdempotencyKey('invalid-key'); // falseSupported Operations
All mutating operations support idempotency:
Plans:
client.plans.create(userId, user, products, salesFromDate, salesToDate, idempotencyKey)client.plans.updateStatus(userId, planId, status, idempotencyKey)client.plans.updateProducts(userId, planId, products, salesFromDate, salesToDate, idempotencyKey)client.plans.updateReleaseDate(userId, planId, releaseDate, idempotencyKey)client.plans.cancel(userId, planId, idempotencyKey)
Work Orders:
client.workOrders.create(userId, planId, workOrder, origin, idempotencyKey)client.workOrders.release(userId, workOrderId, idempotencyKey)client.workOrders.cancel(userId, workOrderId, idempotencyKey)client.workOrders.close(userId, workOrderId, idempotencyKey)client.workOrders.batch.create(userId, workOrders, idempotencyKey)client.workOrders.batch.release(userId, workOrderIds, idempotencyKey)client.workOrders.batch.cancel(userId, workOrderIds, idempotencyKey)client.workOrders.batch.close(userId, workOrderIds, idempotencyKey)
Tags:
client.tags.create(userId, workorderEntry, idempotencyKey)client.tags.activate(userId, tagCode, options, idempotencyKey)client.tags.quantify(userId, tagCode, request, idempotencyKey)client.tags.discard(userId, tagCode, options, idempotencyKey)client.tags.clone(userId, tagCode, request, idempotencyKey)client.tags.clear(userId, tagCode, idempotencyKey)client.tags.createMultiple(userId, workorderEntry, count, idempotencyKey)client.tags.batch.create(userId, tags, idempotencyKey)
Production Entries:
client.productionEntries.create(userId, lines, journalMemo, idempotencyKey)
Error Responses
Invalid idempotency key format returns a 400 error:
{
success: false,
problem: {
status: 400,
type: 'bad-request',
title: 'Invalid Idempotency Key Format',
detail: 'Idempotency-Key header must be a valid UUID format (e.g., "550e8400-e29b-41d4-a716-446655440000")',
issues: ['Expected format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (UUID v1-v5)']
}
}Concurrent requests with the same key return a 409 conflict:
{
success: false,
problem: {
status: 409,
type: 'bad-request',
title: 'Request Already Processing',
detail: 'A request with idempotency key "..." is currently being processed',
issues: ['Original request ID: req-0001']
}
}Best Practices
- Generate keys per operation: Create a new key for each distinct operation
// Good: Different keys for different operations
const planKey = generateIdempotencyKey();
const workOrderKey = generateIdempotencyKey();
await client.plans.create(userId, user, products, null, null, planKey);
await client.workOrders.create(userId, planId, workOrder, origin, workOrderKey);- Store keys for retries: Persist keys to retry failed operations
const operation = {
idempotencyKey: generateIdempotencyKey(),
userId,
planId,
products
};
// Store operation to database
await db.save(operation);
try {
await client.plans.create(userId, user, products, null, null, operation.idempotencyKey);
} catch (error) {
// Retry later with same key from database
}- Handle cached responses: Successful requests are cached for 24 hours
const key = generateIdempotencyKey();
// First request creates the resource
const response1 = await client.plans.create(userId, user, products, null, null, key);
// Returns: { success: true, data: { planId: 123 } }
// Duplicate request returns cached response (no duplicate created)
const response2 = await client.plans.create(userId, user, products, null, null, key);
// Returns: { success: true, data: { planId: 123 } } (same response)API Response Structure
All API responses follow a consistent structure:
Success Response
{
success: true,
data: T, // Response data (type varies by endpoint)
metadata?: { // Optional metadata
// Additional information like timestamps, user info, etc.
}
}Error Response
{
success: false,
problem: {
status: number, // HTTP status code
type: 'unauthorized' | 'bad-request' | 'not-found' | 'internal-server-error', // Error type identifier
title: string, // Error title
detail: string, // Detailed error message
instance: string, // Request path
context: {
request: string, // Request method and path
responseText: string // Raw response text
},
issues: string[] // List of specific issues
}
}API Reference
Client Configuration
const client = new SAPB1CommandClient({
baseUrl: string; // Required: API base URL
timeout?: number; // Optional: Request timeout (default: 30000ms)
retryConfig?: {
retries?: number; // Number of retries (default: 3)
retryDelay?: (retryCount: number) => number;
retryCondition?: (error: any) => boolean;
};
headers?: Record<string, string>; // Optional: Custom headers
});Authentication
Login
const response = await client.auth.login({
dbName: string;
user: string;
password: string;
});
// Success response type:
{
success: true,
data: {
userId: string
},
metadata?: {
CompanyDB: string,
UserName: string
}
}The userId from the response must be passed to all subsequent authenticated requests.
Session Management
Check Session Validity
Validate if your current SAP B1 session is still active. Useful for long-running applications or when you need to verify session status before performing operations.
// Detailed session check
const sessionStatus = await client.session.checkSession(userId);
if (sessionStatus.data.sessionValid) {
console.log('Session is active');
console.log('Session checked at:', sessionStatus.metadata.checkedAt);
} else {
console.log('Session is invalid');
}Simple Boolean Check
For convenience, you can also use a simple boolean check:
const isValid = await client.session.isSessionValid(userId);
if (!isValid) {
// Re-authenticate
const authResponse = await client.auth.login(credentials);
userId = authResponse.data.userId;
}
// Proceed with operations
await client.plans.create(userId, planData);The isSessionValid() method automatically handles authentication errors and returns false for invalid sessions, while re-throwing other types of errors (network issues, service problems).
Plans Management
Create Plan
const response = await client.plans.create(
userId: string,
user: number, // Planner user ID
products: Array<{
itemCode: string;
quantity: number; // Must be positive
}>
);
// Success response type:
{
success: true,
data: {
planId: number
}
}Update Plan Status
const response = await client.plans.updateStatus(
userId: string,
planId: number,
status: 'Draft' | 'Generating' | 'Generated' | 'Releasing' |
'Released' | 'Completed' | 'Canceling' | 'Canceled'
);Update Plan Products
const response = await client.plans.updateProducts(
userId: string,
planId: number,
products: Array<{
itemCode: string;
quantity: number;
}>
);Cancel Plan
const response = await client.plans.cancel(userId: string, planId: number);Work Orders Management
Create Work Order
const response = await client.workOrders.create(
userId: string,
planId: number,
workOrder: {
itemCode: string;
quantity: number;
injections?: number;
productionSector?: string;
},
origin?: {
origin: 'manual' | 'workorder',
originAbsoluteEntry: number,
originDocumentNumber: number
}
);
// Success response type:
{
success: true,
data: {
absoluteEntry: number,
documentEntry: number,
createDate: string
}
}Release Work Order
const response = await client.workOrders.release(
userId: string,
workOrderId: number
);Cancel Work Order
const response = await client.workOrders.cancel(
userId: string,
workOrderId: number
);Batch Operations
Create Multiple Work Orders
const response = await client.workOrders.batch.create(userId, [
{
planId: number,
workOrder: {
itemCode: string,
quantity: number,
injections?: number,
productionSector?: string
},
origin: {
origin: 'manual' | 'workorder',
originAbsoluteEntry: number,
originDocumentNumber: number
}
},
// ... more work orders
]);
// Success response type:
{
success: true,
data: Array<{
absoluteEntry: number,
documentEntry: number,
createDate: string
}>
}Release Multiple Work Orders
const response = await client.workOrders.batch.release(
userId: string,
workOrderIds: number[]
);
// Success response type:
{
success: true,
data: Array<{
absoluteEntry: number
}>
}Cancel Multiple Work Orders
const response = await client.workOrders.batch.cancel(
userId: string,
workOrderIds: number[]
);
// Success response type:
{
success: true,
data: Array<{
absoluteEntry: number
}>
}Health Check
const health = await client.health(); // No authentication requiredError Handling
The client provides detailed error information through custom error classes:
import {
SAPB1APIError,
AuthError,
NetworkError,
ValidationError,
isErrorResponse
} from '@ritas-inc/sapb1commandapi-client';
try {
const response = await client.auth.login({ ... });
// Type-safe error checking
if (!response.success) {
console.error('Login failed:', response.problem.detail);
console.error('Issues:', response.problem.issues);
return;
}
// Success - TypeScript knows response.data exists
const userId = response.data.userId;
} catch (error) {
if (error instanceof AuthError) {
console.error('Authentication failed:', error.detail);
console.error('Issues:', error.issues);
} else if (error instanceof ValidationError) {
console.error('Validation failed:', error.issues);
} else if (error instanceof NetworkError) {
console.error('Network error:', error.message);
} else if (error instanceof SAPB1APIError) {
console.error('API error:', {
status: error.status,
type: error.type,
title: error.title,
detail: error.detail,
issues: error.issues
});
}
}Using the Error Response Helper
const response = await client.plans.create(userId, user, products);
if (isErrorResponse(response)) {
// TypeScript knows this is an error response
console.error(`Error: ${response.problem.detail}`);
} else {
// TypeScript knows this is a success response
console.log(`Created plan: ${response.data.planId}`);
}Stateless Design
This client is completely stateless - it does not store any session information. You must:
- Store the
userIdreturned from login - Pass the
userIdto every authenticated API call - Handle session expiry (30 minutes) by re-authenticating when needed
This design makes the client perfect for:
- Serverless functions
- Multi-tenant applications
- Microservices
- Load-balanced environments
TypeScript Support
All methods are fully typed with TypeScript. The client exports all necessary types:
import type {
// Request types
AuthRequest,
CreatePlanRequest,
UpdatePlanStatusRequest,
UpdatePlanProductsRequest,
CreateWorkOrderItem,
// Response types
AuthResponse,
CreatePlanResponse,
UpdatePlanStatusResponse,
CreateWorkOrderResponse,
CreateWorkOrderBatchResponse,
// Enum types
PlanStatus,
WorkOrderOriginType,
// Configuration
ClientConfig
} from '@ritas-inc/sapb1commandapi-client';Testing
This package includes comprehensive tests:
Unit Tests
npm testIntegration Tests
Integration tests perform a complete API lifecycle test:
- Authenticate with SAP B1
- Create plan with 3 products
- Update plan with 4 products
- Generate 4 work orders
- Release and cancel individual work orders
- Batch release and cancel work orders
Configure test environment:
cp .env.test.example .env.test
# Edit .env.test with your test API credentials
INTEGRATION_TEST=true npm testSee tests/README.md for detailed testing documentation.
Migration Guide from v1.0.x
Version 1.1.0 introduces a new response structure. Here are the key changes:
Response Structure
All responses now follow a consistent success/error pattern:
// Before (v1.0.x)
const { userId } = await client.auth.login(credentials);
// After (v1.1.0)
const response = await client.auth.login(credentials);
if (response.success) {
const userId = response.data.userId;
}Error Handling
Errors are now included in the response object:
// Before (v1.0.x)
try {
const plan = await client.plans.create(userId, user, products);
} catch (error) {
// Handle error
}
// After (v1.1.0)
const response = await client.plans.create(userId, user, products);
if (!response.success) {
console.error(response.problem.detail);
}Type Imports
Types are now exported from schema files:
// Before (v1.0.x)
import type { AuthRequest } from '@ritas-inc/sapb1commandapi-client/types';
// After (v1.1.0)
import type { AuthRequest } from '@ritas-inc/sapb1commandapi-client';Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Support
For issues and feature requests, please use the GitHub issue tracker.
