Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
- name: Install Dependencies
run: npm i

Expand Down
62 changes: 59 additions & 3 deletions .github/workflows/tag-and-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,80 @@ name: Tag, Release, & Publish

permissions:
contents: write
id-token: write

on:
push:
branches:
- master
- ci/npm-trusted-publishing

jobs:
verify-trusted-publishing:
if: github.ref == 'refs/heads/ci/npm-trusted-publishing'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 24
registry-url: https://registry.npmjs.org

- name: Check npm version
run: npm --version

- name: Exchange OIDC token with npm
run: |
node --input-type=module <<'NODE'
const audience = "npm:registry.npmjs.org";
const packageName = "ordercloud-javascript-sdk";
const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
if (!requestUrl || !requestToken) {
throw new Error("GitHub did not provide an OIDC token request endpoint.");
}

const separator = requestUrl.includes("?") ? "&" : "?";
const identityResponse = await fetch(
`${requestUrl}${separator}audience=${encodeURIComponent(audience)}`,
{ headers: { Authorization: `bearer ${requestToken}` } }
);
if (!identityResponse.ok) {
throw new Error(`GitHub OIDC request failed (${identityResponse.status}).`);
}
const { value: identityToken } = await identityResponse.json();
if (!identityToken) {
throw new Error("GitHub OIDC response did not include an identity token.");
}
console.log(`::add-mask::${identityToken}`);

const exchangeResponse = await fetch(
`https://registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/${encodeURIComponent(packageName)}`,
{ method: "POST", headers: { Authorization: `Bearer ${identityToken}` } }
);
const body = await exchangeResponse.json().catch(() => ({}));
if (body.token) console.log(`::add-mask::${body.token}`);
if (!exchangeResponse.ok || !body.token) {
const detail = { ...body };
delete detail.token;
throw new Error(
`npm rejected the trusted publisher (${exchangeResponse.status}): ${JSON.stringify(detail)}`
);
}
console.log(`Trusted publishing accepted. Publish token expires ${body.expires ?? "unknown"}.`);
NODE

build:
if: github.ref == 'refs/heads/master'
runs-on: windows-latest # ubuntu throws error on build
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
registry-url: https://registry.npmjs.org

- name: Install Dependencies
Expand Down Expand Up @@ -105,6 +163,4 @@ jobs:

- name: Publish to npm
if: steps.autotagger.outputs.tagname != ''
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --access public
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to the ordercloud-javascript-sdk will be documented in this
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

# [14.0.0] - 2026-10-15
- Breaking: `customRoles` is a property of the auth options object on `Auth.Login`, `Auth.ElevatedLogin`, `Auth.ClientCredentials`, and `Auth.Anonymous`, instead of its own argument. Custom roles with no standard roles are sent as the scope string. That case previously sent the literal text `undefined`.
- Breaking: a nested model is one type parameter constrained to that model, instead of one parameter per nested `xp`. `Order<MyOrderXp>` is unchanged. `Order<MyOrderXp, MyFromUserXp>` and `OrderWorksheet<SomeXp>` are not. See the migration guide.
- Breaking: axios is no longer a peer dependency. Requests use the platform `fetch`. `requestOptions.cancelToken` is now `requestOptions.signal`. `axiosAdapter` and axios interceptors are replaced by `Configuration.Set({ fetch, onRequest, onResponse })`. See the migration guide.
- Docs "Defined in" links are written on a single build. `src/Configuration.ts` matches the TypeScript filename casing.

# [13.0.0] - 2026-09-25
- Bring SDK up to date with API v1.0.470. Breaking: `ApiClient.ClientSecret` and `SecurityProfile.IsExternalRoleGroup` are removed. Buyer product `PriceSchedule` is now `BuyerPriceSchedule`.
- Update dev dependencies, including TypeScript 5.9 and TypeDoc 0.28. TypeDoc 0.28 requires the missing-exports plugin to be listed explicitly.
Expand Down
63 changes: 63 additions & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,69 @@

The objective of this guide is to document the breaking changes and updates required to migrate from one major version to the next.

## version 13.x.x to version 14.x.x
* `customRoles` is no longer a positional argument on `Auth.Login`, `Auth.ElevatedLogin`, `Auth.ClientCredentials`, or `Auth.Anonymous`. Pass it on the request options object, along with `signal`, `requestType`, and (for `Anonymous`) `anonuserid`.

Before:
```typescript
Auth.Login(username, password, clientID, scope, customRoles, requestOptions)
Auth.Anonymous(clientID, scope, customRoles, { anonuserid })
```

After:
```typescript
Auth.Login(username, password, clientID, scope, { ...requestOptions, customRoles })
Auth.Anonymous(clientID, scope, { anonuserid, customRoles })
```

* Axios is no longer a peer dependency. The SDK sends requests with the platform `fetch`. Remove `axios` from your app if you installed it only for the SDK.

* `requestOptions.cancelToken` is now `requestOptions.signal`, an `AbortSignal`. A cancelled call rejects with `CancellationError`. `isCancellation(error)` identifies it. It is not an `OrderCloudError`.

Before:
```typescript
import axios from 'axios'
const source = axios.CancelToken.source()
Products.List({}, { cancelToken: source.token })
source.cancel()
```

After:
```typescript
import { isCancellation } from 'ordercloud-javascript-sdk'
const controller = new AbortController()
Products.List({}, { signal: controller.signal }).catch(error => {
if (isCancellation(error)) {
return
}
throw error
})
controller.abort()
```

* `SdkConfiguration.axiosAdapter` is removed. Pass `fetch` to use your own HTTP implementation. Global axios interceptors no longer see SDK calls. Use `onRequest` and `onResponse` on `Configuration.Set`.

* `OrderCloudError` still exposes `status`, `statusText`, `errorCode`, `errors`, `response`, and `isOrderCloudError`. `error.request` is no longer an axios `XMLHttpRequest` or Node `ClientRequest`. It is `{ url, method }`.

* Nested models are no longer described by hoisting each nested `xp` onto the parent. A model's own `xp` stays the first parameter. Each nested model that previously contributed `xp` parameters is now one parameter constrained to that model.

Before:
```typescript
interface MyOrder extends Order<MyOrderXp, MyFromUserXp, MyBillingAddressXp> {}
type Worksheet = OrderWorksheet<MyFromUserXp, MyBillingAddressXp, MyOrderXp, MyProductXp>
```

After:
```typescript
interface MyOrderUser extends OrderUser<MyFromUserXp> {}
interface MyAddress extends Address<MyBillingAddressXp> {}
interface MyOrder extends Order<MyOrderXp, MyOrderUser, MyAddress> {}
interface MyLineItem extends LineItem<MyLineItemXp, MyProduct, MyVariant, MyAddress, MyAddress> {}
type Worksheet = OrderWorksheet<MyOrder, MyLineItem>
```

`Order<MyOrderXp>` still types only the order `xp`. `Category<MyCategoryXp>` is unchanged. `ListPageWithFacets<Product>` is unchanged. Its second parameter is now a `ListFacet`, so facet `xp` is declared as `ListPageWithFacets<Product, ListFacet<MyFacetXp>>`.

## version 11.x.x to version 12.x.x
* The `URL` and `SharedKey` properties have been removed from the `MessageSender` model, and `DeliveryConfigID` is now required. Configuration must be provided via a `DeliveryConfig` (managed through the new `DeliveryConfigurations` service) using either a `MailchimpConfig` or `MessageSenderConfig` delivery target. See the [updated Message Senders KB article](https://ordercloud.io/knowledge-base/message-senders#configuration-options) for more details.

Expand Down
103 changes: 57 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,7 @@ The OrderCloud SDK for Javascript is a modern client library for building soluti

## Requirements

[Axios](https://www.npmjs.com/package/axios) is a peer dependency of the OrderCloudSDK.

> Note: Peer dependencies are not installed automatically. They must be installed separately.

Why isn't axios a dependency of OrderCloud SDK? Since axios is a fairly popular http library and may already exist as a dependency in your project it is considered best practice to have it defined as a peer dependency. This way, there isn't a potential for two versions of axios to be installed which could result in weird bugs, not to mention a bloated javascript bundle.
The SDK sends HTTP with the platform [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) implementation. It runs in browsers and Node versions that provide global `fetch`. No HTTP library is installed with the SDK.

## ⚙️ Installation

Expand Down Expand Up @@ -103,6 +99,13 @@ Auth.Login(username, password, clientID, scope)
Tokens.SetAccessToken(token)
})
.catch(err => console.log(err));

// Custom roles are optional and belong on the request options object.
// ElevatedLogin, ClientCredentials, and Anonymous use the same shape.
// Anonymous also accepts anonuserid on that object.
Auth.Login(username, password, clientID, scope, {
customRoles: ['InventoryAdmin'],
})
```

## 🔍 Filtering
Expand Down Expand Up @@ -234,81 +237,70 @@ Products.Get('my-product')
// https://ordercloud-api.github.io/ordercloud-javascript-sdk/classes/orderclouderror
console.log(error.message);
console.log(JSON.stringify(error.errors, null, 4));
} else if (error.request) {
// the request was made but no response received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of http.ClientRequest in node.js
console.log(error.request);
} else if (error.isCancellation) {
// the caller aborted the request
console.log(error.message);
} else if (error.isTimeout) {
// the request exceeded timeoutInMilliseconds
console.log(error.message);
} else {
// Something happened in setting up the request that triggered an Error
console.log('Error', error.message);
}
})
```

## Interceptors
## Request and response hooks

You can use [axios interceptors](https://github.com/axios/axios#interceptors) to intercept a request before it goes out to the API or to intercept a response before it gets handled by the SDK. This enables you to log, rewrite, or even retry calls.

The SDK does not use a custom axios instance, so you can set up your interceptors right off of axios.
`Configuration.Set` accepts `onRequest` and `onResponse`. Use them to log, rewrite, or retry a call. `onRequest` returns the request that will be sent. `onResponse` runs before the SDK returns data or throws `OrderCloudError`.

```javascript
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// Do something before request is sent
return config;
}, function (error) {
// Do something with request error
return Promise.reject(error);
});

// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
return response;
}, function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return Promise.reject(error);
});
import { Configuration } from 'ordercloud-javascript-sdk';

Configuration.Set({
onRequest(request) {
console.log(request.method, request.url)
return request
},
onResponse(response) {
console.log(response.status)
return response
},
})
```

## Cancelling Requests

In addition to `requestOptions.accessToken` the sdk provides `requestOptions.cancelToken` which enables [axios request cancellation](https://github.com/axios/axios#cancellation). This option is useful for cleaning up outstanding requests when changes in your user experience no longer require the data requested. For instance, you could use the cancel token to clean up outstanding requests [when your react component unmounts](https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup).
`requestOptions.signal` accepts an [AbortSignal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal). Use it to drop a request when the result is no longer needed, for instance [when a react component unmounts](https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup).

```javascript
import axios from 'axios';
import { Products } from 'ordercloud-javascript-sdk';
import { Products, isCancellation } from 'ordercloud-javascript-sdk';

const signal = axios.CancelToken.source();
const controller = new AbortController();

Products.List({search: 'Tennis balls'}, {cancelToken: signal.token})
Products.List({search: 'Tennis balls'}, {signal: controller.signal})
.catch(ex => {
if (axios.isCancel(ex)) {
if (isCancellation(ex)) {
console.log(ex) // 'This request was cancelled!'
} else {
console.log(ex) // Normal ordercloud exception
}
})

// Oops! I don't want to resolve this request anymore
signal.cancel('This request was cancelled!')
controller.abort()
```

## Axios Adapters

Adapters in Axios allow for custom handling of requests, which opens up possibilities for advanced functionalities such as caching, request mocking, and the option to switch HTTP APIs (for instance, from XHR to Fetch). You can use community created adapters, or create one yourself (just make sure it adheres to the [axios adapter interface](https://github.com/axios/axios/blob/v1.x/lib/adapters/README.md))
## Custom fetch

Like all other sdk level options, Axios adapters can be defined with the Configuration service
Pass a `fetch` function to the Configuration service to mock, cache, or reroute HTTP. The default is the platform global `fetch`.

```typescript
import { Configuration } from 'ordercloud-javascript-sdk';

Configuration.Set({
baseApiUrl: 'https://sandboxapi.ordercloud.io',
timeoutInMilliseconds: 20 * 1000,
axiosAdapter: myAdapter
fetch: myFetch
})
```

Expand Down Expand Up @@ -428,7 +420,26 @@ Categories.List<MyCategory>('mock-catalog-id')
})
```

This is nicer and especially preferable for models like `Order` which have many nested models each with their own `xp` fields that must be defined at the top level. For example: `Order<OrderXp, FromUserXp, BillingAddressXp>`. Declaring those 3 xp types once on a custom `MyOrder` interface is far cleaner than declaring them on every call to `Orders.Get` or `Orders.List`.
A model's own `xp` is still the first type parameter, so `Category<MyCategoryXp>` and `Order<MyOrderXp>` keep working. Nested models are passed as the extended type, not as a separate `xp` parameter for each nested field:

```typescript
interface MyOrderXp {
ErpId?: string;
}

interface MyOrderUser extends OrderUser<{ Department?: string }> {}
interface MyAddress extends Address<{ Dock?: string }> {}
interface MyOrder extends Order<MyOrderXp, MyOrderUser, MyAddress> {}

interface MyLineItemXp {
GiftWrap?: boolean;
}
interface MyLineItem extends LineItem<MyLineItemXp, LineItemProduct, LineItemVariant, MyAddress, MyAddress> {}

type Worksheet = OrderWorksheet<MyOrder, MyLineItem>
```

`Order<MyOrderXp, MyFromUserXp>` does not compile: the second parameter is the user model, not that user's `xp`. `OrderWorksheet<SomeXp>` does not compile: the first parameter is `TOrder extends Order`.

### Typescript utilities

Expand Down
10 changes: 8 additions & 2 deletions codegen/handlebars-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@ function handlebarsExt(Handlebars) {

Handlebars.registerHelper(
'commaSeparateWithDefaultAny',
(types: string[]) => {
return types.map(t => `${t} = any`).join(', ')
(types: { name: string; constraint?: string }[]) => {
return types
.map(t =>
t.constraint
? `${t.name} extends ${t.constraint} = ${t.constraint}`
: `${t.name} = any`
)
.join(', ')
}
)
}
Expand Down
Loading
Loading