npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@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

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-client

Quick 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'); // false

Supported 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

  1. 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);
  1. 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
}
  1. 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 required

Error 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:

  1. Store the userId returned from login
  2. Pass the userId to every authenticated API call
  3. 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 test

Integration Tests

Integration tests perform a complete API lifecycle test:

  1. Authenticate with SAP B1
  2. Create plan with 3 products
  3. Update plan with 4 products
  4. Generate 4 work orders
  5. Release and cancel individual work orders
  6. 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 test

See 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.