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 🙏

© 2026 – Pkg Stats / Ryan Hefner

odata-batch

v2.0.0

Published

Build and parse OData V2 $batch multipart requests: multiple changesets, per-response error detection and typed results

Downloads

316

Readme

odata-batch

Build and parse OData V2 $batch requests in TypeScript/JavaScript: turn an array of calls into a correct multipart/mixed payload, send it over HTTP, and get each inner response back as a structured object — including per-changeset grouping, embedded error detection, and automatic authentication.

Works in Node.js (TypeScript ≥ 4.8 or JavaScript). HTTP transport is axios by default and is replaceable.

Install

npm install odata-batch

Quick start

Every entry in calls becomes one request inside the batch. Responses come back in the same order as the calls.

import { ODataBatch, Call, BatchResponseParsed } from 'odata-batch';

const calls: Call[] = [
    {
        method: 'PUT',
        url: "https://my.server.sap/sap/opu/odata/sap/API/Candidates('200')",
        headers: { 'If-Match': '*' },
        data: { name: 'Sebastian' },
    },
    {
        method: 'POST',
        url: 'https://my.server.sap/sap/opu/odata/sap/API/Candidates',
        data: { name: 'Bruno' },
    },
];

const batch = new ODataBatch({
    url: 'https://my.server.sap/sap/opu/odata/sap/API/$batch',
    auth: 'user:password',
    calls,
});

batch.send().then(console.log).catch(console.error);

JavaScript works the same way: const { ODataBatch } = require('odata-batch');

The response you get back

send() resolves to BatchResponseParsed[] — one entry per call, in order:

[
    {
        code: '200',              // HTTP status of the inner response
        status: 'OK',             // reason phrase
        headers: [                // inner response headers
            { key: 'Content-Type', value: 'application/json' },
        ],
        data: { name: 'Sebastian' }, // parsed payload (see response types)
        success: true,            // false when code is 4xx or 5xx
        changesetIndex: 0,        // which changeset this response belongs to
    },
]

A batch can transport HTTP 200 while individual operations failed — check success (or code) per entry instead of only catching the promise.

Multiple changesets (atomic groups)

Pass Call[][] to wrap each group in its own changeset. In OData V2 every changeset is atomic: the server applies all operations in a group or none of them. Use one changeset per business transaction.

const calls: Call[][] = [
    // Changeset 0: create the order and its first line — all or nothing
    [
        { method: 'POST', url: '.../Orders', data: { id: 1 } },
        { method: 'POST', url: '.../OrderLines', data: { order: 1, sku: 'A' } },
    ],
    // Changeset 1: an independent operation
    [{ method: 'GET', url: '.../Orders(1)', data: null }],
];

const batch = new ODataBatch({ url: '.../$batch', auth: 'user:password', calls });

const responses = await batch.send();
responses.map((r) => [r.code, r.success, r.changesetIndex]);
// [['201', true, 0], ['201', true, 0], ['200', true, 1]]

changesetIndex tells you which group each response came from, so you can map failures back to the transaction that produced them.

A flat Call[] still produces a single changeset, with the exact same wire format as previous versions.

Configuration

| Option | Type | Default | Description | |---|---|---|---| | url | string | — | The $batch endpoint. Required. | | auth | string | — | Basic-auth credential. See Authentication. Required. | | calls | Call[] \| Call[][] | — | Operations to send. Empty arrays are rejected. | | headers | Record<string, string> | — | Extra headers for the batch request itself. Authorization from here overrides auth; Accept and Content-Type are always derived from the response-type options. | | batchResponseType | 'json' \| 'xml' | 'json' | How data of each call is serialized into the batch body. json uses JSON.stringify; xml writes the payload verbatim. | | individualResponseType | 'json' \| 'xml' | 'json' | How each inner response body is parsed. json runs JSON.parse; xml returns the raw string. Also sent as the batch Accept header. | | (2nd constructor arg) | ODataBatchRepository | axios adapter | Custom HTTP transport. See Custom HTTP adapter. |

Each Call accepts:

| Field | Type | Description | |---|---|---| | method | string | HTTP method (any casing; normalized to uppercase). | | url | string | Request URL. Must not contain line breaks. | | data | any | Payload: serialized per batchResponseType. | | headers | Record<string, string \| number> | Optional. Content-Type/Accept here are overridden by the response-type options; every other header passes through verbatim. |

Authentication

auth builds the Authorization: Basic ... header using a simple, deterministic rule:

  • Contains : → it is a raw user:password pair → base64-encoded automatically (UTF-8), per RFC 7617.
  • No : → assumed pre-encoded or an opaque token → sent untouched.
auth: 'user:password'    // sent as: Basic dXNlcjpwYXNzd29yZA==  (encoded for you)
auth: 'dXNlcjpwYXNz'     // sent as: Basic dXNlcjpwYXNz        (already encoded)
auth: 'my-token'         // sent as: Basic my-token            (colon-less: untouched)

The rule is safe because the base64 alphabet never contains :, while decoded Basic credentials always do. Caveat: a colon-less raw password (rare but legal) cannot be distinguished from a token — encode such passwords yourself before passing them. To bypass the heuristic entirely, set headers: { Authorization: 'Bearer ...' } — it overrides auth.

Errors and validation

| Situation | Behavior | |---|---| | calls missing, empty, or containing an empty changeset | throw at construction: No calls have been passed | | Line breaks (\r/\n) in a call url, method or header | throw before sending — blocks header injection into the MIME part | | Payload contains the generated changeset boundary | Boundary is regenerated (up to 10 attempts), then throw | | Batch endpoint answers non-2xx | Promise rejects (transport error; nothing is parsed) | | Inner response is 4xx/5xx | Parsed normally with success: false — the promise still resolves | | Response body is not JSON while using individualResponseType: 'json' | throw SyntaxError while parsing | | Response has no/invalid content-type boundary | throw with a descriptive message |

Custom HTTP adapter

The transport is one interface. Implement ODataBatchRepository and pass it as the second constructor argument — anything from fetch to a company proxy client works:

import {
    ODataBatch,
    ODataBatchRepository,
    BatchRequestConfig,
    BatchResponseParsed,
    createBatchResponse,
    BatchResponseConstructor,
} from 'odata-batch';

export class ODataBatchFetchRepository implements ODataBatchRepository {
    async send(
        url: string,
        batchRequest: string,
        config: BatchRequestConfig,
        accept: string,
        BatchParser: BatchResponseConstructor
    ): Promise<BatchResponseParsed[]> {
        const response = await fetch(url, {
            method: 'POST',
            headers: config.headers,
            body: batchRequest,
        });

        // Reuse the bundled parser: give it the response body and headers
        return createBatchResponse(BatchParser, {
            data: await response.text(),
            headers: Object.fromEntries(response.headers),
        }, accept).response;
    }
}

const batch = new ODataBatch({ url: '.../$batch', auth: 'user:pass', calls }, new ODataBatchFetchRepository());

The default adapter (ODataBatchAxiosRepository) does exactly this with axios.

Migrating from 1.x

| Change | 1.x behavior | 2.0.0 behavior | Action needed | |---|---|---|---| | axios | 0.21.x bundled | ^1.20.0 | Usually none; retest if you intercept axios | | Raw auth credentials | Sent unencoded | Auto base64-encoded when they contain : | None if you already sent encoded values (README contract); otherwise auth now works correctly | | Line breaks in urls/headers | Sent, corrupting the MIME part | throw | Fix the caller producing them | | BatchResponseParsed.data type | any[] (wrong) | any | Only affects TS consumers reading .data.length — payload shape was never an array | | HTTP/1.0 / quoted boundaries | Parsed as empty | Parsed correctly | None |

Full details in CHANGELOG.md.

Credits

Built on the ideas of batchcall and batch-odata.


OData V2 is the supported and tested dialect. V3 batches share the MIME structure and may work, but are not covered by the test suite.