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

@mkja/o-data

v0.1.3

Published

**o-data** is a TypeScript‑first OData 4.01 client and schema generator.

Downloads

1,282

Readme

o-data

o-data is a TypeScript‑first OData 4.01 client and schema generator.

It has two parts:

  • Runtime library – a strongly‑typed client for querying and mutating OData services.
  • CLI parser / schema generator – reads an OData CSDL XML document and generates a typed schema module the runtime can consume.

Features

  • Schema‑driven, fully typed client
    • Describe your service once in a TypeScript schema; get strong types for queries, payloads, and responses.
  • Fluent query builder
    • $select, $expand (with nested options), $filter, $orderby, $top, $skip, $count.
    • $filter DSL with navigation, any / all, enums, dates, and string functions.
  • Navigation‑aware create/update
    • Supports @odata.bind for single and collection navigations, deep inserts, and batch references.
  • Actions & functions
    • Bound and unbound operations, with correct URL shapes and parameter serialization.
  • Batch requests ($batch)
    • Same fluent API as the client; queue operations and send them in a single multipart request. GET/query/function outside changesets; create/update/delete/action inside changesets.
  • Schema generator from CSDL
    • CLI reads your OData metadata XML and emits a typed schema({...}) module.
    • Powerful include/exclude/masking rules for keeping the generated surface small and relevant.

Installation

# with bun
bun add @mkja/o-data

# or with npm
npm install @mkja/o-data

# or with pnpm
pnpm add @mkja/o-data

# or with yarn
yarn add @mkja/o-data

The runtime expects a fetch‑compatible environment (Request, Response, Headers); it works in modern Node (with fetch) and browsers.


Runtime: Getting started

1. Define or generate a schema

You can either write a schema by hand:

// schema.ts
import { schema } from "@mkja/o-data/schema";

export const crmSchema = schema({
  namespace: "Microsoft.Dynamics.CRM",
  alias: "mscrm",
  enumtypes: {
    IncidentStatus: {
      isFlags: false,
      members: {
        Active: 0,
        Resolved: 1,
        Cancelled: 2,
      },
    },
  },
  entitytypes: {
    Incident: {
      properties: {
        id: { type: "Edm.Guid" },
        title: { type: "Edm.String" },
        status: { type: "enum", target: "IncidentStatus" },
      },
    },
  },
  entitysets: {
    incidents: { entitytype: "Incident" },
  },
});

…or generate one from a CSDL XML using the CLI (see Schema generator (CLI) below).

2. Create an OdataClient

// client.ts
import { OdataClient } from "@mkja/o-data";
import { crmSchema } from "./schema"; // or generated-o-data-schema

const client = new OdataClient(crmSchema, {
  baseUrl: "https://example.com/api/data/v9.0/",
  transport: fetch, // any (req: Request) => Promise<Response>
});

Querying data

OData response shapes: collections use result.value; single-entity, CRUD, actions, and functions use a flat result (data at top level alongside @odata.* metadata).

Collection queries

// GET /incidents?$select=title,status&$top=10&$orderby=title asc
const response = await client.entitysets("incidents").query({
  select: ["title", "status"],
  top: 10,
  orderby: ["title", "asc"],
});

if (response.ok) {
  const incidents = response.result.value; // typed by schema + query
}

Expands and nested options

// GET /incidents?$expand=incident_contact($select=name,email)
const res = await client.entitysets("incidents").query({
  expand: {
    incident_contact: {
      select: ["name", "email"],
    },
  },
});

Filter builder

Filters use a small builder DSL that respects your schema:

// GET /incidents?$filter=status eq Namespace.IncidentStatus'Active'
const res = await client.entitysets("incidents").query({
  filter: (h) => h.clause("status", "eq", "Active"),
});

// Navigation + logical operators
const res2 = await client.entitysets("incidents").query({
  filter: (h) =>
    h
      .clause("title", "contains", "Support")
      .and(
        h.nav("incident_contact", (nh) =>
          nh.clause("email", "eq", "[email protected]"),
        ),
      ),
});

Supported operators include eq, ne, gt, ge, lt, le, in, contains, startswith, endswith.
For enums, you can pass either the member name ("Active") or the underlying numeric value (1); they are serialized as FQN enum literals.

Single‑entity queries and navigation

// GET /incidents(guid-123)?$select=title
const incident = await client
  .entitysets("incidents")
  .key("guid-123")
  .query({ select: ["title"] });

// GET /incidents(guid-123)/incident_contact
const contact = await client
  .entitysets("incidents")
  .key("guid-123")
  .navigate("incident_contact")
  .query({});

Creating and updating entities

The library infers create/update shapes from your schema and takes care of @odata.bind and deep inserts.

Create

// Basic create
const created = await client.entitysets("incidents").create({
  title: "New incident",
  description: "Description",
});

// Create with navigation bind (single‑valued)
await client.entitysets("incidents").create({
  title: "Linked to contact",
  incident_contact: "guid-contact-id", // → "[email protected]": "/contacts(guid-contact-id)"
});

// Create with collection navigation bind
await client.entitysets("contacts").create({
  name: "John",
  contact_incidents: ["incident-id-1", "incident-id-2"],
  // → "[email protected]": ["/incidents(incident-id-1)", "/incidents(incident-id-2)"]
});

// Deep insert
await client.entitysets("incidents").create({
  title: "Deep insert example",
  incident_contact: {
    name: "Nested contact",
    email: "[email protected]",
  },
});

You can control response shape via options:

await client.entitysets("incidents").create(
  { title: "Return representation" },
  {
    select: ["title", "description"],
    prefer: { return_representation: true },
  },
);

Update

// Simple PATCH
await client.entitysets("incidents").key("guid-123").update({
  title: "Updated title",
});

// Repoint single navigation
await client.entitysets("incidents").key("guid-123").update({
  incident_contact: "guid-new-contact",
});

// Collection navigation operations
await client
  .entitysets("contacts")
  .key("guid-contact")
  .update({
    contact_incidents: {
      add: ["incident-id-3"],
      remove: ["incident-id-1"],
    },
  });

Options for update mirror create: select, prefer.return_representation, custom headers.


Batch requests

Use client.batch() to build a $batch request with the same fluent API. Operations are queued and sent in a single multipart request:

  • GET, query, function – outside changesets (read-only)
  • Create, update, delete, action – inside changesets (atomic)
const batch = client.batch();

batch.entitysets("incidents").query({ select: ["title"], top: 10 });
batch.entitysets("incidents").create({ title: "New" });
batch.entitysets("incidents").key("guid-123").update({ title: "Updated" });
batch.entitysets("incidents").key("guid-456").delete();

const result = await batch.execute();

batch.execute() returns a parsed BatchExecuteResult with ok, status, statusText, headers, and results (one BatchItemResult per queued operation, in order). Each item has ok, status, statusText, headers, and result (parsed JSON body). Use batch.buildRequest() to obtain the Request without sending it.

You can also use .navigate(...), bound and unbound actions, and functions within a batch with the same API as the client.


Actions and functions

Bound actions

// POST /incidents(guid-123)/Namespace.assignIncident
const res = await client
  .entitysets("incidents")
  .key("guid-123")
  .action("assignIncident", {
    parameters: {
      assigneeId: "guid-user",
      priority: 1,
    },
  });

if (res.ok) {
  const ok: boolean = res.result; // mapped from Edm.Boolean (flat at top level)
}

Unbound actions (via imports)

// POST /BulkCreate
const res = await client.action("BulkCreate", {
  parameters: {
    entities: ["1", "2", "3"],
  },
});

Bound functions

// GET /incidents(guid-123)/Namespace.getRelatedCount(relationType=@relationType)?@relationType='contact'
const res = await client
  .entitysets("incidents")
  .key("guid-123")
  .function("getRelatedCount", {
    parameters: { relationType: "contact" },
  });

if (res.ok) {
  const count: number = res.result; // flat at top level
}

Unbound functions (via imports)

// GET /Search(query=@query,entityTypes=@entityTypes)?@query='test'&@entityTypes=...
const res = await client.function("Search", {
  parameters: {
    query: "test",
    entityTypes: ["Incident", "Contact"],
  },
});

For navigation‑typed parameters (actions/functions), you can use the same patterns as for create/update: IDs, [entityset, id], deep insert objects, or arrays thereof; the library converts them to @odata.bind or nested objects as needed.


Schema generator (CLI)

The CLI reads an OData CSDL XML document and generates a strongly‑typed schema module that plugs into the runtime.

1. Create a config file

Create odata-parser.config.js (or .ts) in your project root:

// odata-parser.config.ts
import { defineConfig } from "o-data/parser";

export default defineConfig({
  inputPath: "./metadata.xml",
  outputPath: "./src/schema",
  wantedEntities: "ALL", // or ['incidents', 'contacts']
  wantedUnboundActions: "ALL",
  wantedUnboundFunctions: "ALL",
  excludeFilters: {
    entities: [/^msdyn_/],          // drop system sets
    properties: [/^adx_/],          // drop noisy props
  },
  selectionMode: "additive",         // or 'only' for strict whitelists
  // onlyEntities, onlyBoundActions, onlyUnboundActions, mask, ... are available
});

Running the generator will produce e.g. src/schema/generated-o-data-schema.ts that looks like:

import { schema } from "o-data/schema";

export const myservice_schema = schema({
  namespace: "My.Service",
  alias: "ms",
  enumtypes: { /* ... */ },
  complextypes: { /* ... */ },
  entitytypes: { /* ... */ },
  entitysets: { /* ... */ },
  actions: { /* ... */ },
  functions: { /* ... */ },
  actionImports: { /* ... */ },
  functionImports: { /* ... */ },
});

2. Run the CLI

From your project root:

# using the global CLI name exposed by this package
npx o-data path/to/odata-parser.config.js

# or (when installed locally in a Node/Bun project)
bun x o-data path/to/odata-parser.config.js

If you omit the path, the CLI looks for odata-parser.config.js (and then .ts) in the current working directory.

Then in your code:

import { myservice_schema } from "./schema/generated-o-data-schema";
import { OdataClient } from "o-data";

const client = new OdataClient(myservice_schema, {
  baseUrl: "https://example.com/odata/",
  transport: fetch,
});

Status and limitations

  • The library is still early (0.0.x); APIs may change.
  • The generator doesn’t yet handle OData operation overloading beyond keeping the first operation per name.

Development

  • Build
bun x tsc -p tsconfig.build.json
  • Tests
bun test