Skip to content
Closed
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ 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).

# [Unreleased]
- 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.

# [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
20 changes: 20 additions & 0 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

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
* 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
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,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
148 changes: 76 additions & 72 deletions codegen/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ const filterResources: FilterResourcesHook = function(resource) {

const postFormatModel: PostFormatModelHook = function(model, models) {
// add model.typeParams and prop.typeParams
inspectModelForTypeParams(models, model, model)
inspectModelForTypeParams(models, model)

// add model.hasTypeParams and prop.hasTypeParams
model['hasTypeParams'] = Boolean(model['typeParams'].length)
model.properties.forEach(prop => {
prop['hasTypeParams'] = Boolean(prop['typeParams'].length)
prop['hasTypeParams'] = Boolean(prop['modelTypeParam'])
})

// add prop.typescriptType to props on model
Expand Down Expand Up @@ -198,91 +198,95 @@ function findTypeForModelProps(prop: Param, model: Model) {
return enumString
}

const typeParams = prop['hasTypeParams']
? `<${prop['typeParams'].join(',')}>`
: ''
if (prop['modelTypeParam']) {
return prop.isArray ? `${prop['modelTypeParam']}[]` : prop['modelTypeParam']
}

const jsType = javascriptTypes[prop.type] || prop.type

if (prop.isArray) {
return prop.isCustomType ? `${prop.type + typeParams}[]` : `${jsType}[]`
return prop.isCustomType ? `${prop.type}[]` : `${jsType}[]`
}

if (!prop.hasRequiredFields && prop.isCustomType) {
return prop.type + typeParams
return prop.type
}

return jsType
}

function inspectModelForTypeParams(
allModels: Model[],
rootModel: Model,
inspectModel: Model,
rootProp?: Param,
parentProp?: Param
) {
if (!inspectModel['typeParams']) {
inspectModel['typeParams'] = []
/**
* Own xp stays the first parameter and defaults to any.
* Each direct custom-type property that today contributes nested xp
* becomes one parameter constrained to that model. Nested xp names
* are not hoisted onto the parent.
*/
function inspectModelForTypeParams(allModels: Model[], model: Model) {
const typeParams: { name: string; constraint?: string }[] = []
const usedNames = new Set<string>()

if (model.properties.some(prop => prop.isXp)) {
const ownXpName = `T${model.name}Xp`
typeParams.push({ name: ownXpName })
usedNames.add(ownXpName)
}
inspectModel.properties.forEach(prop => {
if (!prop['typeParams']) {
prop['typeParams'] = []
}
if (prop.isXp) {
if (!rootProp) {
// first level of properties, only the xp for that resource will exist
// move it to front for convention where first xp is always that resource's xp
const typeParam = `T${inspectModel.name}Xp`
rootModel['typeParams'].unshift(typeParam)
prop['typeParams'].unshift(typeParam)
} else {
let typeParam = parentProp
? `T${parentProp.name}Xp`
: `T${rootProp.name}Xp`
if (typeParam === 'TItemsXp') {
typeParam = `T${rootProp.type}Xp`
rootModel['typeParams'].unshift(typeParam)
rootProp['typeParams'].unshift(typeParam)
} else {
rootModel['typeParams'].push(typeParam)
rootProp['typeParams'].push(typeParam)
}
}

model.properties.forEach(prop => {
prop['modelTypeParam'] = undefined
if (!prop.isCustomType || prop.isXp) {
return
}
if (prop.isCustomType) {
const toInspect = allModels.find(model => {
return model.name === prop.type || model.type === prop.type
})
if (!toInspect) {
throw new Error(`Unable to find next model to inspect for ${prop.type}`)
}
if (!rootProp && !parentProp) {
inspectModelForTypeParams(allModels, rootModel, toInspect, prop)
} else if (rootProp && !parentProp) {
inspectModelForTypeParams(
allModels,
rootModel,
toInspect,
rootProp,
prop
)
}
if (!customTypeContributesXp(allModels, prop.type)) {
return
}
const name = modelTypeParamName(prop, usedNames)
usedNames.add(name)
typeParams.push({ name, constraint: prop.type })
prop['modelTypeParam'] = name
})

// This is a little hacky but basically we want the first type parameter to be the type for the xp of the model
// for example ShipEstimate<ShipEstimateXp...> otherwise, the xp type would be at the end of the list
// lke so: ShipEstimate<SomeXp, SomeOtherXp...ShipEstimateXp>
if (
rootProp &&
rootProp['typeParams'] &&
rootProp['typeParams'].length > 1 &&
rootProp['typeParams'].includes(`T${rootProp.name}Xp`)
) {
const toMoveIndex = rootProp['typeParams'].findIndex(
p => p === `T${rootProp.name}Xp`
)
const toMove = rootProp['typeParams'].splice(toMoveIndex, 1)
rootProp['typeParams'].unshift(toMove[0])
model['typeParams'] = typeParams
}

function modelTypeParamName(prop: Param, usedNames: Set<string>): string {
// Arrays take the element type name (LineItems -> TLineItem).
// Other properties take the property name (FromUser -> TFromUser)
// so two Address fields stay distinct.
const preferred = prop.isArray ? `T${prop.type}` : `T${prop.name}`
if (!usedNames.has(preferred)) {
return preferred
}
const byProperty = `T${prop.name}`
if (!usedNames.has(byProperty)) {
return byProperty
}
return `T${prop.name}_${prop.type}`
}

function customTypeContributesXp(
allModels: Model[],
typeName: string,
depth = 0
): boolean {
const model = allModels.find(
candidate => candidate.name === typeName || candidate.type === typeName
)
if (!model) {
throw new Error(`Unable to find next model to inspect for ${typeName}`)
}
for (const prop of model.properties) {
if (prop.isXp) {
return true
}
// Match the previous hoist depth: xp on this model, and xp on its
// direct custom-type children. Do not walk further.
if (
depth < 1 &&
prop.isCustomType &&
customTypeContributesXp(allModels, prop.type, depth + 1)
) {
return true
}
}
return false
}
9 changes: 7 additions & 2 deletions codegen/templates/models/ListPageWithFacets.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { ListFacet } from './ListFacet'
import { MetaWithFacets } from './MetaWithFacets'

/**
* @typeParam TItem represents the type for each item in the list
* @typeParam TListFacet represents the facet model, including that facet's xp
*/
export interface ListPageWithFacets<TItem, TFacetsXp = any> {
export interface ListPageWithFacets<
TItem,
TListFacet extends ListFacet = ListFacet
> {
Items?: TItem[]
Meta?: MetaWithFacets<TFacetsXp>
Meta?: MetaWithFacets<TListFacet>
}
38 changes: 4 additions & 34 deletions codegen/templates/models/OrderReturnCalculatePayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,9 @@ import { OrderReturn } from './OrderReturn'
import { OrderWorksheet } from './OrderWorksheet'

export interface OrderReturnCalculatePayload<
TOrderReturnXp = any,
TFromUserXp = any,
TBillingAddressXp = any,
TOrderXp = any,
TProductXp = any,
TVariantXp = any,
TShippingAddressXp = any,
TShipFromAddressXp = any,
TLineItemsXp = any,
TOrderPromotionsXp = any,
TShipEstimatesXp = any,
TShipEstimateResponseXp = any,
TOrderCalculateResponseXp = any,
TOrderSubmitResponseXp = any,
TOrderSubmitForApprovalResponseXp = any,
TOrderApprovedResponseXp = any
TOrderReturn extends OrderReturn = OrderReturn,
TOrderWorksheet extends OrderWorksheet = OrderWorksheet
> {
OrderReturn?: OrderReturn<TOrderReturnXp>
OrderWorksheet?: OrderWorksheet<
TFromUserXp,
TBillingAddressXp,
TOrderXp,
TProductXp,
TVariantXp,
TShippingAddressXp,
TShipFromAddressXp,
TLineItemsXp,
TOrderPromotionsXp,
TShipEstimatesXp,
TShipEstimateResponseXp,
TOrderCalculateResponseXp,
TOrderSubmitResponseXp,
TOrderSubmitForApprovalResponseXp,
TOrderApprovedResponseXp
>
OrderReturn?: TOrderReturn
OrderWorksheet?: TOrderWorksheet
}
4 changes: 2 additions & 2 deletions src/models/BundleItems.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LineItem } from './LineItem';

export interface BundleItems<TProductXp = any, TVariantXp = any, TShippingAddressXp = any, TShipFromAddressXp = any, TLineItemsXp = any> {
LineItems?: LineItem<TLineItemsXp,TProductXp,TVariantXp,TShippingAddressXp,TShipFromAddressXp>[]
export interface BundleItems<TLineItem extends LineItem = LineItem> {
LineItems?: TLineItem[]
}
4 changes: 2 additions & 2 deletions src/models/BuyerProduct.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { BuyerPriceSchedule } from './BuyerPriceSchedule';
import { Inventory } from './Inventory';

export interface BuyerProduct<TBuyerProductXp = any, TPriceScheduleXp = any> {
readonly PriceSchedule?: BuyerPriceSchedule<TPriceScheduleXp>
export interface BuyerProduct<TBuyerProductXp = any, TPriceSchedule extends BuyerPriceSchedule = BuyerPriceSchedule> {
readonly PriceSchedule?: TPriceSchedule
ID?: string
ParentID?: string
IsParent?: boolean
Expand Down
12 changes: 6 additions & 6 deletions src/models/ExtendedLineItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { LineItemVariant } from './LineItemVariant';
import { Address } from './Address';
import { LineItemSpec } from './LineItemSpec';

export interface ExtendedLineItem<TExtendedLineItemXp = any, TFromUserXp = any, TBillingAddressXp = any, TOrderXp = any, TProductXp = any, TVariantXp = any, TShippingAddressXp = any, TShipFromAddressXp = any> {
export interface ExtendedLineItem<TExtendedLineItemXp = any, TOrder extends Order = Order, TProduct extends LineItemProduct = LineItemProduct, TVariant extends LineItemVariant = LineItemVariant, TShippingAddress extends Address = Address, TShipFromAddress extends Address = Address> {
OrderID?: string
Order?: Order<TOrderXp,TFromUserXp,TBillingAddressXp>
Order?: TOrder
ID?: string
ProductID?: string
Quantity?: number
Expand All @@ -25,10 +25,10 @@ export interface ExtendedLineItem<TExtendedLineItemXp = any, TFromUserXp = any,
ShippingAccount?: string
ShippingAddressID?: string
ShipFromAddressID?: string
readonly Product?: LineItemProduct<TProductXp>
readonly Variant?: LineItemVariant<TVariantXp>
readonly ShippingAddress?: Address<TShippingAddressXp>
readonly ShipFromAddress?: Address<TShipFromAddressXp>
readonly Product?: TProduct
readonly Variant?: TVariant
readonly ShippingAddress?: TShippingAddress
readonly ShipFromAddress?: TShipFromAddress
readonly SupplierID?: string
InventoryRecordID?: string
readonly PriceScheduleID?: string
Expand Down
4 changes: 2 additions & 2 deletions src/models/InventoryRecord.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Address } from './Address';

export interface InventoryRecord<TInventoryRecordXp = any, TAddressXp = any> {
export interface InventoryRecord<TInventoryRecordXp = any, TAddress extends Address = Address> {
ID?: string
OwnerID?: string
AllowAllBuyers?: boolean
readonly Address?: Address<TAddressXp>
readonly Address?: TAddress
AddressID: string
OrderCanExceed?: boolean
QuantityAvailable?: number
Expand Down
Loading
Loading