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

notsapodata

v0.0.15

Published

OData utilities and client package

Readme

notsapodata

Overview

A truly type-safe TypeScript OData v2/v4 client with comprehensive IDE support. Probably the first OData client where IntelliSense guides you through entity sets, fields, and navigation properties — while TypeScript catches service misuse at compile time.

Demo Application

Features

  • Full type safety: Entity sets, fields, navigation properties, and function parameters validated at compile time
  • Wide IDE support: IntelliSense and autocomplete throughout the entire query building process
  • Static analysis: Catches wrong service usage before runtime
  • Vite plugin generates TypeScript types from OData metadata
  • CSRF handling, batching, and deep navigation
  • Filter builder with all OData operators
  • Excel export and value help utilities

Works with SAP, HANA, Microsoft, and other OData services.

Installation

npm install notsapodata
# or
pnpm add notsapodata
yarn add notsapodata

Type Generation

Using the Vite Plugin

Import notSapODataVitePlugin from notsapodata/vite. The plugin fetches metadata and generates typed models to src/.odata.types.ts.

Minimal Example - Fetching Metadata from Service

import { defineConfig } from 'vite'
import notSapODataVitePlugin from 'notsapodata/vite'

export default defineConfig({
  plugins: [
    notSapODataVitePlugin({
      services: {
        NorthwindV4: {
          host: 'https://services.odata.org',
          path: '/V4/Northwind/Northwind.svc',
        },
      },
    }),
  ],
})

With Authentication and Entity Selection

import { defineConfig } from 'vite'
import notSapODataVitePlugin from 'notsapodata/vite'

const cookie = process.env.ODATA_COOKIE_NAME && process.env.ODATA_COOKIE_VALUE
  ? `${process.env.ODATA_COOKIE_NAME}=${process.env.ODATA_COOKIE_VALUE}`
  : undefined

export default defineConfig({
  plugins: [
    notSapODataVitePlugin({
      filename: 'src/.odata.types.ts',
      services: {
        sapV4: {
          host: process.env.ODATA_HOST ?? 'https://my.sap.example',
          path: '/sap/opu/odata4/sap/zsb_bp_data/srvd/sap/zsd_bp_data/0001',
          alias: 'SapV4',
          entitySets: [
            'ZSD_BP_DATA_CDS.CustomerSet',
            'ZSD_BP_DATA_CDS.Groups',
          ],
          headers: cookie ? { cookie } : {},
        },
      },
    }),
  ],
})

Using Offline Metadata

import { defineConfig } from 'vite'
import notSapODataVitePlugin from 'notsapodata/vite'
import { readFileSync } from 'node:fs'

const metadataXml = readFileSync('./fixtures/northwind-v4.xml', 'utf-8')

export default defineConfig({
  plugins: [
    notSapODataVitePlugin({
      services: {
        MockNorthwind: {
          metadata: metadataXml,  // Provide XML directly, no network call
          path: '/mock/Northwind',
          alias: 'NorthwindMock',
        },
      },
    }),
  ],
})

Environment variables:

  • ODATA_HOST – default host when service.host is omitted
  • ODATA_COOKIE_NAME and ODATA_COOKIE_VALUE – authentication credentials

Service Configuration Options

| Option | Type | Description | | --- | --- | --- | | host? | string | Base host for all requests. Required when metadata is fetched from the service. | | path? | string | Service path (e.g. /sap/opu/odata/sap/MY_SERVICE). Used by generated models and metadata fetch. | | headers? | Record<string,string> | Extra headers (cookies, language, etc.). | | alias? | string | Overrides the generated class/interface base name. | | entitySets? | string[] | Restrict generation to specific entity sets (fully-qualified names). Omit to include everything. | | metadata? | string | Provide inline metadata XML (no network call). |

Programmatic Generation

import { generate } from 'notsapodata/codegen'
import { writeFileSync } from 'node:fs'

// Generate types without Vite
const content = await generate({
  NorthwindV4: {
    host: 'https://services.odata.org',
    path: '/V4/Northwind/Northwind.svc',
  },
})

writeFileSync('src/.odata.types.ts', content)

Generated Output Structure

For a service named MyModel:

  1. Constants: myModelConsts - Field, key, and measure arrays per entity type
  2. Types Interface: TMyModel - TypeScript types from constants
  3. OData Interface: TMyModelOData - Entity sets, types, and functions
  4. Service Class: MyModel - Extends OData<TMyModelOData>
// Constants with field/key/measure lists
export const myModelConsts = {
  "Namespace": {
    "EntityType": {
      fields: ['Field1', 'Field2'] as const,
      keys: ['Field1'] as const,
      measures: [] as const,
    }
  }
}

// Service class with static helpers
export class MyModel extends OData<TMyModelOData> {
  public static getInstance() { /* singleton */ }
  public static async entitySet(name) { /* direct entity access */ }
}

Simple OData Requests

Getting an Entity Set

import { NorthwindV4 } from '@/.odata.types'

// Get entity set using the static method (recommended)
const products = await NorthwindV4.entitySet('Products')

// Simple query with top 10 records
const result = await products.query({ top: 10 })
// GET: .../Products?$top=10

Reading a Single Record

const products = await NorthwindV4.entitySet('Products')

// Read a single record by key
const product = await products.readRecord({ ProductID: 42 })
// GET: .../Products(ProductID=42)

Complex Queries

All Query Parameters

const products = await NorthwindV4.entitySet('Products')
const orders = await NorthwindV4.entitySet('Orders')

// Example with all supported parameters
const result = await products.query({
  top: 10,
  skip: 20,
  select: ['ProductID', 'ProductName', 'UnitPrice'],
  filter: { UnitPrice: { gt: 50 } },
  sorters: [{ name: 'ProductName', desc: false }],
  expand: products.expand('Category'),
  inlinecount: 'allpages',  // V2: returns count
  // count: true,            // V4: use this instead
  apply: 'filter(CategoryID eq 1)',  // V4 only
})

Filter Parameter

Filters support type-safe operators and logical combinations:

Basic Operators

// Equality and comparison
filter: {
  ProductID: { eq: 42 },          // ProductID eq 42
  UnitPrice: { gt: 50 },           // UnitPrice gt 50
  UnitsInStock: { le: 100 },       // UnitsInStock le 100
}

// String operations
filter: {
  ProductName: { contains: 'Chai' },    // contains(ProductName,'Chai')
  ProductName: { starts: 'A' },         // startswith(ProductName,'A')
  ProductName: { ends: 'tea' },         // endswith(ProductName,'tea')
}

// Range (between)
filter: {
  UnitPrice: { bw: ['10', '50'] }       // (UnitPrice ge 10 and UnitPrice le 50)
}

// Null/empty checks
filter: {
  Discontinued: { empty: true },        // Discontinued eq null
  ProductName: { notEmpty: true }       // ProductName ne null
}

Logical Operators

// AND (implicit with array or object)
filter: [
  { ProductName: { contains: 'Chai' } },
  { UnitPrice: { bw: ['10', '50'] } },
]
// Result: (contains(ProductName,'Chai') and (UnitPrice ge 10 and UnitPrice le 50))

// OR expression
filter: {
  $or: [
    { ProductID: { eq: 1 } },
    { ProductID: { eq: 2 } },
    { ProductID: { eq: 3 } },
  ],
}
// Result: (ProductID eq 1 or ProductID eq 2 or ProductID eq 3)

// Complex nesting
filter: {
  $or: [
    {
      $and: [
        { CategoryID: { eq: 1 } },
        { UnitPrice: { gt: 10 } }
      ]
    },
    { Discontinued: { eq: true } }
  ]
}

Select Parameter

// Select specific fields (type-safe)
select: ['ProductID', 'ProductName', 'UnitPrice']
// Result: $select=ProductID,ProductName,UnitPrice

Sorters Parameter

// Multiple sort criteria
sorters: [
  { name: 'UnitPrice', desc: true },   // Descending
  'ProductName'                        // Ascending (shorthand)
]
// Result: $orderby=UnitPrice desc,ProductName

Apply Parameter (V4 only)

// OData V4 aggregations and transformations
apply: 'filter(CategoryID eq 1)/aggregate(UnitPrice with sum as TotalPrice)'

Type-Safe Expands

One of the key features is type-safe expand building with full IntelliSense support.

OData V4 Expands

V4 supports inline query options within expand:

const orders = await NorthwindV4.entitySet('Orders')

// Build type-safe expands with nested options
const customerExpand = orders.expand('Customer', {
  select: ['CustomerID', 'CompanyName', 'Country']
})

const orderDetailsExpand = orders
  .expand('Order_Details', {
    filter: { Quantity: { gt: 5 } },
    select: ['ProductID', 'Quantity', 'UnitPrice'],
    top: 10
  })
  .expand('Product', {  // Nested expand
    select: ['ProductName', 'CategoryID']
  })
  .expand('Category')  // Further nesting

const result = await orders.query({
  top: 2,
  expand: [customerExpand, orderDetailsExpand]
})
// Result: $expand=Customer($select=CustomerID,CompanyName,Country),Order_Details($filter=Quantity gt 5;$select=ProductID,Quantity,UnitPrice;$top=10;$expand=Product($select=ProductName,CategoryID;$expand=Category))

OData V2 Expands

V2 uses simpler path-based expansion. Filters and selects are applied at the root level with navigation prefixes:

// V2: Simple expansion paths
expand: 'Order_Details/Product'

// V2: Filters use navigation prefix at root
filter: {
  'Order_Details/Quantity': { gt: 10 },  // Applied at root with path prefix
}
select: ['OrderID', 'Order_Details/ProductID', 'Order_Details/Product/ProductName']

Working with Large Result Sets

const model = NorthwindV4.getInstance()
const progress: number[] = []

const query = model.readAllEntries('Products', {
  chunkSize: 500,
  progressCb: (loaded, total, done) => {
    progress.push(loaded)
    if (done) {
      console.log(`Loaded ${loaded} of ${total}`)
    }
  },
})

const rows = await query.promise
// Abort if needed: query.abort()

readAllEntries performs chunked reads (defaults to 100 records) and reuses the batching engine under the hood.

Navigation

Navigation allows you to traverse relationships between entities:

const orders = await NorthwindV4.entitySet('Orders')

// Single-level navigation: Orders -> Customer
const customer = await orders
  .withKey({ OrderID: 10248 })
  .toOne('Customer')
  .read()
// GET: .../Orders(OrderID=10248)/Customer

// Multi-level navigation: Orders -> Customer -> Orders
const customerOrders = orders
  .withKey({ OrderID: 10248 })
  .toOne('Customer')
  .toMany('Orders')

const result = await customerOrders.query({ top: 5 })
// GET: .../Orders(OrderID=10248)/Customer/Orders?$top=5

// Complex 3+ level navigation
const deepNav = orders
  .withKey({ OrderID: 10249 })
  .toMany('Order_Details')
  .withKey({ OrderID: 10249, ProductID: 11 })
  .toOne('Product')
  .toOne('Category')

const category = await deepNav.read()
// GET: .../Orders(OrderID=10249)/Order_Details(OrderID=10249,ProductID=11)/Product/Category

How Navigation Works

  • withKey() - Specifies which record to start from
  • toOne() - Navigate to a single related entity (1:1 or N:1)
  • toMany() - Navigate to a collection of related entities (1:N)
  • Chain methods to build deep navigation paths
  • End with .query() or .read() to execute the request

Calling Function Imports

const model = NorthwindV4.getInstance()

await model.callFunction('SapService.CalculateTotals', {
  FiscalYear: '2024',
})

Batch Execution

There are two batching modes:

  1. Implicit queueing – set model.options.useBatch = true and the client will coalesce consecutive calls automatically.
  2. Manual control – create a dedicated batch instance.
const model = NorthwindV4.getInstance()
const batch = model.createBatch()

const productsPromise = batch.read('Products', { $top: 10 })
const ordersPromise = batch.read('Orders', { $top: 10 })

const { promise } = batch.execute({ maxBatchLength: 10, maxConcurrentBatches: 2 })
await promise

const products = await productsPromise
const orders = await ordersPromise

Metadata Helpers

Accessing Metadata

// If you need metadata for advanced operations
const model = NorthwindV4.getInstance()
const metadata = await model.getMetadata()
const productType = metadata.getEntityType('NorthwindModel.Product')

Refining Metadata

const products = await NorthwindV4.entitySet('Products')

products.refineField('ProductName', {
  $label: 'Product',
  $MaxLength: 100,
})

Excel Export

const products = await NorthwindV4.entitySet('Products')

const { data } = await products.query({
  select: ['ProductID', 'ProductName', 'UnitPrice', 'CategoryID'],
})

const buffer = await products.generateExcel(
  ['ProductID', 'ProductName', 'UnitPrice', 'CategoryID'],
  data,
  {
    subtotals: [
      {
        grpBy: ['CategoryID'],
        aggregate: ['UnitPrice'],
      },
    ],
  }
)
// buffer is a Node.js Buffer (Excel workbook)

Errors and Fetch

ifetch wraps the Fetch API and raises rich error objects:

  • SapODataError – thrown when the response body matches the SAP error schema.
  • IFetchError – thrown for network errors or non-OData error responses.
import { IFetchError, SapODataError } from 'notsapodata'

try {
  await model.updateRecordByKey('Products(1)', { ProductName: 'Test' })
} catch (error) {
  if (error instanceof SapODataError) {
    console.error(error.code, error.message)
  } else if (error instanceof IFetchError) {
    console.error(error.status, error.statusText)
  }
}

Best Practices

  • Use Service.entitySet() for quick access without fetching metadata first
  • Use structured filters over manual OData strings: { Field: { gt: '10' } }
  • Cache entity sets—they store field maps and annotations
  • Chain toOne() and toMany() for deep navigation
  • Group batch calls logically to stay under 100 requests per batch
  • Catch SapODataError separately to show SAP diagnostics