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

@mqa/dataverse-client

v1.0.0

Published

V1-compatible Dataverse / Dynamics 365 Web API client for Node.js. Drop-in replacement for [email protected] with a cached MSAL client-credentials token provider.

Readme

@mqa/dataverse-client

A V1-compatible Microsoft Dataverse / Dynamics 365 Web API client for Node.js, built on axios and @azure/msal-node.

It is a drop-in replacement for [email protected]: method names, argument order and response shapes are unchanged, so existing call sites can be swapped mechanically.

  • TypeScript, compiled to CommonJS — works with require() in plain-JS apps and with import.
  • Ships compiled .js + .d.ts type declarations + source maps.
  • Cached MSAL client-credentials token provider (getAccessToken() / getAccessTokenCallback()).
  • Automatic paging over @odata.nextLink for collection queries.

Install

npm install @mqa/dataverse-client

Node.js >= 18 is required.

Quick start

const { DataverseClient, getAccessTokenCallback } = require('@mqa/dataverse-client');

// 1. Provide a token source (see "Token provider" below).
// getAccessTokenCallback() is a ready-made MSAL client-credentials callback:
const onTokenRefresh = getAccessTokenCallback();

// 2. Create the client.
const client = new DataverseClient({
    webApiUrl: process.env.D365_WEBAPI_URL, // e.g. https://org.api.crm.dynamics.com/api/data/v9.2/
    onTokenRefresh,
});

// 3. Use it.
async function main() {
    const contact = await client.retrieve('11111111-1111-1111-1111-111111111111', 'contacts', [
        'fullname',
        'emailaddress1',
    ]);
    console.log(contact?.fullname);
}

Configuration

interface DataverseClientConfig {
    /** Full Web API URL (trailing slash optional, normalized internally). */
    webApiUrl?: string;
    /** Alternative: organization URL, e.g. https://org.crm.dynamics.com. */
    serverUrl?: string;
    /** Used with serverUrl. Defaults to version "9.2". */
    dataApi?: { version?: string } | string;
    /** V1-compatible token callback: called with a (token) => void callback. */
    onTokenRefresh?: (callback: (token: string) => void, errorCallback?: (error: unknown) => void) => void;
    /** Static bearer token. When set, onTokenRefresh is bypassed. */
    token?: string;
    /** Impersonate a Dynamics user on every request (sets MSCRMCallerID). */
    impersonate?: string;
    /** Request timeout in milliseconds. */
    timeout?: number;
    /** Prefer: odata.maxpagesize for collection requests. */
    maxPageSize?: number;
}

Exactly one of webApiUrl or serverUrl is required. Provide either token or onTokenRefresh (requests reject with a clear error if neither is set).

Token provider

getAccessToken() returns a Promise of a client-credentials access token. A single cached ConfidentialClientApplication is reused (tokens cached, refreshed on expiry), concurrent calls share one in-flight acquisition, and failures reject rather than being swallowed.

const { getAccessToken } = require('@mqa/dataverse-client');
const token = await getAccessToken();

Configuration comes from environment variables:

| Variable | Description | | ------------------- | ------------------------------------------------- | | MSAL_CLIENTID | Azure AD / Entra ID app (client) id | | MSAL_AUTHORITY | e.g. https://login.microsoftonline.com/<tenant> | | MSAL_CLIENTSECRET | Client secret | | MSAL_SCOPE | e.g. https://org.api.crm.dynamics.com/.default |

getAccessTokenCallback() returns a v1-style (callback, errorCallback) => void function suitable for onTokenRefresh. It accepts an optional second argument so authentication failures can reject the pending request instead of hanging.

API reference

All methods return Promises. collection is an entity set name (e.g. contacts). Entity = Record<string, any>.

retrieve(key, collection, select?, expand?)

const contact = await client.retrieve(
    '11111111-1111-1111-1111-111111111111',
    'contacts',
    ['fullname'],
    [{ property: 'aas_campaigncode', select: ['name', 'statecode'] }],
);

Resolves a single entity. Resolves null when the record is not found (HTTP 404) — check with if (!contact) throw .... Expanded navigation properties appear as direct properties.

retrieveMultiple(collection, select?, filter?)

const entities = await client.retrieveMultiple('contacts', ['fullname'], 'statecode eq 0');
entities.value.forEach((e) => {});

Resolves { value: Entity[], ... }. filter is a plain OData $filter string (may be omitted). Pages are fetched internally over @odata.nextLink, so .value always contains all pages. See Error handling for the { value: [], error } behavior.

create(object, collection, prefer?, select?)

const id = await client.create({ firstname: 'John' }, 'contacts'); // -> new GUID string
// or, to get the created entity back:
const entity = await client.create(obj, 'contacts', ['return=representation'], ['fullname']);

prefer may be an array (["return=representation"]), a string, or null. Without return=representation the new record's GUID string is resolved (from the Location header); with it, the created entity is resolved. Payloads are passed through verbatim, including @odata.bind navigation properties.

update(key, collection, object, prefer?, select?)

await client.update('11111111-1111-1111-1111-111111111111', 'contacts', { firstname: 'Jane' });

Without prefer, resolves undefined when done; with return=representation it resolves the updated entity.

deleteRecord(key, collection)

await client.deleteRecord('11111111-1111-1111-1111-111111111111', 'contacts');

Resolves when done.

retrieveRequest(request)

const contact = await client.retrieveRequest({ collection: 'contacts', key: id, select: ['fullname'] });

Same as retrieve; 404 resolves to null.

retrieveMultipleRequest(request)

const result = await client.retrieveMultipleRequest({
    collection: 'contacts',
    select: ['fullname'],
    filter: 'statecode eq 0',
    orderBy: ['createdon desc'],
    top: 1,
});

Resolves { value: Entity[], ... } with paging, $orderby (string[]) and $top (number) support.

updateRequest(request)

const entity = await client.updateRequest({
    key: id,
    collection: 'contacts',
    entity: { firstname: 'Jane' },
    returnRepresentation: true,
    select: ['fullname'],
    impersonate: '22222222-2222-2222-2222-222222222222', // optional per-request user
});

impersonate (a user GUID) is applied per-request via the MSCRMCallerID header. With returnRepresentation: true resolves the updated entity, otherwise void.

executeBoundAction(id, collection, actionName)

await client.executeBoundAction(
    '11111111-1111-1111-1111-111111111111',
    'products',
    'Microsoft.Dynamics.CRM.PublishProductHierarchy',
);
// id may also be an entity object; its primary key is extracted:
await client.executeBoundAction(createdProduct, 'products', 'Microsoft.Dynamics.CRM.PublishProductHierarchy');

When id is an entity object, the primary key is extracted from {collection}id (e.g. productid), then id, then @odata.id.

executeUnboundAction(actionName, requestObject?)

const requestObject = {
    ToastType: 200000000,
    Title: 'Hello',
    getMetadata: function () {
        return {
            boundParameter: null,
            parameterTypes: { ToastType: { typeName: 'Edm.Int32', structuralProperty: 1 } },
            operationType: 0,
            operationName: 'SendAppNotification',
        };
    },
};
await client.executeUnboundAction('SendAppNotification', requestObject);

If requestObject has a getMetadata() function, it is called to build the metadata payload and the function itself is stripped from the body sent over the wire (matching v1).

executeFetchXml(collection, fetchXml)

const result = await client.executeFetchXml('contacts', '<fetch><entity name="contact">...</entity></fetch>');
result.value.forEach((e) => {});

Resolves { value: Entity[], ... }. page="1" is injected into the <fetch> element when paging is not already configured. The passed collection is used verbatim for the URL.

executeFetchXmlAll(collection, fetchXml)

Same as executeFetchXml but loops internally over @odata.nextLink until exhausted, so .value contains every record.

Error handling

  • Non-2xx responses reject with an error object carrying HTTP status plus the parsed error payload — callers can inspect e.status === 400/406/422, e.error === 'application', e.code, and e.message exactly like v1.
  • retrieve / retrieveRequest: HTTP 404 resolves to null (not a rejection).
  • retrieveMultiple / retrieveMultipleRequest: when the server returns an error payload, they resolve { value: [], error, status, message, ... } instead of rejecting, so call sites that check if (!result.error) short-circuit and never crash. Transport failures (network, DNS, timeout) and token-refresh failures still reject.
  • executeFetchXml / executeFetchXmlAll: non-2xx rejects (catch and inspect e.status); a 2xx body that carries .error is resolved as-is.
  • Token refresh failures reject the pending request(s) — nothing is silently swallowed.

Import styles

// CommonJS
const { DataverseClient, getAccessToken, getAccessTokenCallback } = require('@mqa/dataverse-client');
const DataverseClientDefault = require('@mqa/dataverse-client').default;

// ES modules
import { DataverseClient } from '@mqa/dataverse-client';
import DataverseClient from '@mqa/dataverse-client';

Migration note — swap from [email protected]

@mqa/dataverse-client exposes the same method names, argument order and response shapes as [email protected], so only the require / new lines and config change. No method rename and no argument reshaping is needed across ~150 existing call sites.

Before:

const DynamicsWebApi = require('dynamics-web-api');
const { getTokenCallback } = require('./msal_token');

const dynamicsWebApi = new DynamicsWebApi({
    webApiUrl: process.env.D365_WEBAPI_URL,
    onTokenRefresh: getTokenCallback,
});

After:

const { DataverseClient } = require('@mqa/dataverse-client');
const { getAccessTokenCallback } = require('@mqa/dataverse-client'); // or keep your own callback

const dynamicsWebApi = new DataverseClient({
    webApiUrl: process.env.D365_WEBAPI_URL,
    onTokenRefresh: getAccessTokenCallback(), // v1-style: call it to get the callback
});

Notes:

  • new DataverseClient({ webApiUrl, onTokenRefresh }) — same shape as v1. The old getTokenCallback helper also works unchanged; getAccessTokenCallback() just adds caching and failure propagation.
  • If a static token is available, pass token: "<jwt>" and omit onTokenRefresh.
  • webApiUrl may keep a trailing slash — it is normalized internally.
  • Intentional deviation: retrieveMultiple/retrieveMultipleRequest resolve { value: [], error } on server error payloads instead of rejecting. Sites that previously caught and re-checked e.status there can instead check result.status on the resolved object; existing if (!result.error) guards already behave correctly.
  • Everything else (retrieve 404 → null, prefer semantics, impersonation, paging, fetch XML, bound/unbound actions, getMetadata handling) mirrors v1.

Development

npm install
npm run lint
npm test
npm run test:coverage
npm run build

npm run build emits CommonJS + .d.ts + source maps into dist/. The package is published via GitHub Actions on a v* tag push; npm publish is never run without an explicit release.

License

MIT