From f3f285c2acff5fbd19e1d9c7478b306d16fcce97 Mon Sep 17 00:00:00 2001 From: Eric Sitarz Date: Mon, 28 Sep 2026 10:31:37 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=A5=20constrain=20nested=20model=20typ?= =?UTF-8?q?e=20parameters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hoisting every nested xp made OrderWorksheet take 17 parameters. Own xp stays first. Each nested model is now one parameter constrained to that model, so Order still works. Refs #78 --- CHANGELOG.md | 3 + MIGRATION_GUIDE.md | 20 +++ README.md | 21 ++- codegen/handlebars-extensions.ts | 10 +- codegen/hooks.ts | 148 +++++++++--------- .../templates/models/ListPageWithFacets.ts | 9 +- .../models/OrderReturnCalculatePayload.ts | 38 +---- src/models/BundleItems.ts | 4 +- src/models/BuyerProduct.ts | 4 +- src/models/ExtendedLineItem.ts | 12 +- src/models/InventoryRecord.ts | 4 +- src/models/LineItem.ts | 10 +- src/models/LineItemOverride.ts | 4 +- src/models/ListPageWithFacets.ts | 9 +- src/models/MetaWithFacets.ts | 4 +- src/models/Order.ts | 6 +- src/models/OrderApproval.ts | 4 +- src/models/OrderCalculateResponse.ts | 4 +- src/models/OrderRepeatResponse.ts | 4 +- src/models/OrderReturnApproval.ts | 4 +- src/models/OrderReturnCalculatePayload.ts | 38 +---- src/models/OrderSplitResult.ts | 4 +- src/models/OrderWorksheet.ts | 22 +-- src/models/Payment.ts | 4 +- src/models/ProductCollectionBuyerProduct.ts | 4 +- src/models/ProductSeller.ts | 4 +- src/models/RefreshPromosResponse.ts | 6 +- src/models/ShipEstimate.ts | 4 +- src/models/ShipEstimateResponse.ts | 4 +- src/models/Shipment.ts | 6 +- src/models/ShipmentItem.ts | 6 +- src/models/Spec.ts | 4 +- tests/model-type-params.ts | 90 +++++++++++ tests/tsconfig.model-types.json | 12 ++ 34 files changed, 317 insertions(+), 213 deletions(-) create mode 100644 tests/model-type-params.ts create mode 100644 tests/tsconfig.model-types.json diff --git a/CHANGELOG.md b/CHANGELOG.md index ec4e3f42..bd858086 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` is unchanged. `Order` and `OrderWorksheet` 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. diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 7b696dde..a22c11b2 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -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 {} + type Worksheet = OrderWorksheet + ``` + + After: + ```typescript + interface MyOrderUser extends OrderUser {} + interface MyAddress extends Address {} + interface MyOrder extends Order {} + interface MyLineItem extends LineItem {} + type Worksheet = OrderWorksheet + ``` + + `Order` still types only the order `xp`. `Category` is unchanged. `ListPageWithFacets` is unchanged. Its second parameter is now a `ListFacet`, so facet `xp` is declared as `ListPageWithFacets>`. + ## 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. diff --git a/README.md b/README.md index 078c9c7b..f501d898 100644 --- a/README.md +++ b/README.md @@ -428,7 +428,26 @@ Categories.List('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`. 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` and `Order` 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 {} + +interface MyLineItemXp { + GiftWrap?: boolean; +} +interface MyLineItem extends LineItem {} + +type Worksheet = OrderWorksheet +``` + +`Order` does not compile: the second parameter is the user model, not that user's `xp`. `OrderWorksheet` does not compile: the first parameter is `TOrder extends Order`. ### Typescript utilities diff --git a/codegen/handlebars-extensions.ts b/codegen/handlebars-extensions.ts index 020fa0ca..c1be5519 100644 --- a/codegen/handlebars-extensions.ts +++ b/codegen/handlebars-extensions.ts @@ -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(', ') } ) } diff --git a/codegen/hooks.ts b/codegen/hooks.ts index 391c0bef..0ea2011f 100644 --- a/codegen/hooks.ts +++ b/codegen/hooks.ts @@ -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 @@ -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() + + 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 otherwise, the xp type would be at the end of the list - // lke so: ShipEstimate - 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 { + // 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 } diff --git a/codegen/templates/models/ListPageWithFacets.ts b/codegen/templates/models/ListPageWithFacets.ts index 008e6624..6749bfc6 100644 --- a/codegen/templates/models/ListPageWithFacets.ts +++ b/codegen/templates/models/ListPageWithFacets.ts @@ -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 { +export interface ListPageWithFacets< + TItem, + TListFacet extends ListFacet = ListFacet +> { Items?: TItem[] - Meta?: MetaWithFacets + Meta?: MetaWithFacets } diff --git a/codegen/templates/models/OrderReturnCalculatePayload.ts b/codegen/templates/models/OrderReturnCalculatePayload.ts index 32e918d3..8b7348e8 100644 --- a/codegen/templates/models/OrderReturnCalculatePayload.ts +++ b/codegen/templates/models/OrderReturnCalculatePayload.ts @@ -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 - OrderWorksheet?: OrderWorksheet< - TFromUserXp, - TBillingAddressXp, - TOrderXp, - TProductXp, - TVariantXp, - TShippingAddressXp, - TShipFromAddressXp, - TLineItemsXp, - TOrderPromotionsXp, - TShipEstimatesXp, - TShipEstimateResponseXp, - TOrderCalculateResponseXp, - TOrderSubmitResponseXp, - TOrderSubmitForApprovalResponseXp, - TOrderApprovedResponseXp - > + OrderReturn?: TOrderReturn + OrderWorksheet?: TOrderWorksheet } diff --git a/src/models/BundleItems.ts b/src/models/BundleItems.ts index 5d97ef05..22570ab1 100644 --- a/src/models/BundleItems.ts +++ b/src/models/BundleItems.ts @@ -1,5 +1,5 @@ import { LineItem } from './LineItem'; -export interface BundleItems { - LineItems?: LineItem[] +export interface BundleItems { + LineItems?: TLineItem[] } \ No newline at end of file diff --git a/src/models/BuyerProduct.ts b/src/models/BuyerProduct.ts index 3fb446c8..f5ef9576 100644 --- a/src/models/BuyerProduct.ts +++ b/src/models/BuyerProduct.ts @@ -1,8 +1,8 @@ import { BuyerPriceSchedule } from './BuyerPriceSchedule'; import { Inventory } from './Inventory'; -export interface BuyerProduct { - readonly PriceSchedule?: BuyerPriceSchedule +export interface BuyerProduct { + readonly PriceSchedule?: TPriceSchedule ID?: string ParentID?: string IsParent?: boolean diff --git a/src/models/ExtendedLineItem.ts b/src/models/ExtendedLineItem.ts index 901a7d37..e15e1f57 100644 --- a/src/models/ExtendedLineItem.ts +++ b/src/models/ExtendedLineItem.ts @@ -4,9 +4,9 @@ import { LineItemVariant } from './LineItemVariant'; import { Address } from './Address'; import { LineItemSpec } from './LineItemSpec'; -export interface ExtendedLineItem { +export interface ExtendedLineItem { OrderID?: string - Order?: Order + Order?: TOrder ID?: string ProductID?: string Quantity?: number @@ -25,10 +25,10 @@ export interface ExtendedLineItem - readonly Variant?: LineItemVariant - readonly ShippingAddress?: Address - readonly ShipFromAddress?: Address + readonly Product?: TProduct + readonly Variant?: TVariant + readonly ShippingAddress?: TShippingAddress + readonly ShipFromAddress?: TShipFromAddress readonly SupplierID?: string InventoryRecordID?: string readonly PriceScheduleID?: string diff --git a/src/models/InventoryRecord.ts b/src/models/InventoryRecord.ts index 28be4822..a58d13fd 100644 --- a/src/models/InventoryRecord.ts +++ b/src/models/InventoryRecord.ts @@ -1,10 +1,10 @@ import { Address } from './Address'; -export interface InventoryRecord { +export interface InventoryRecord { ID?: string OwnerID?: string AllowAllBuyers?: boolean - readonly Address?: Address + readonly Address?: TAddress AddressID: string OrderCanExceed?: boolean QuantityAvailable?: number diff --git a/src/models/LineItem.ts b/src/models/LineItem.ts index f4a1d64c..ef49e1c2 100644 --- a/src/models/LineItem.ts +++ b/src/models/LineItem.ts @@ -3,7 +3,7 @@ import { LineItemVariant } from './LineItemVariant'; import { Address } from './Address'; import { LineItemSpec } from './LineItemSpec'; -export interface LineItem { +export interface LineItem { ID?: string ProductID: string Quantity?: number @@ -22,10 +22,10 @@ export interface LineItem - readonly Variant?: LineItemVariant - readonly ShippingAddress?: Address - readonly ShipFromAddress?: Address + readonly Product?: TProduct + readonly Variant?: TVariant + readonly ShippingAddress?: TShippingAddress + readonly ShipFromAddress?: TShipFromAddress readonly SupplierID?: string InventoryRecordID?: string readonly PriceScheduleID?: string diff --git a/src/models/LineItemOverride.ts b/src/models/LineItemOverride.ts index f4ce80ec..bcffa084 100644 --- a/src/models/LineItemOverride.ts +++ b/src/models/LineItemOverride.ts @@ -1,10 +1,10 @@ import { AdHocProduct } from './AdHocProduct'; import { PromotionOverride } from './PromotionOverride'; -export interface LineItemOverride { +export interface LineItemOverride { LineItemID?: string UnitPrice?: number - Product?: AdHocProduct + Product?: TProduct PromotionOverrides?: PromotionOverride[] InventoryRecordID?: string Remove?: boolean diff --git a/src/models/ListPageWithFacets.ts b/src/models/ListPageWithFacets.ts index 008e6624..6749bfc6 100644 --- a/src/models/ListPageWithFacets.ts +++ b/src/models/ListPageWithFacets.ts @@ -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 { +export interface ListPageWithFacets< + TItem, + TListFacet extends ListFacet = ListFacet +> { Items?: TItem[] - Meta?: MetaWithFacets + Meta?: MetaWithFacets } diff --git a/src/models/MetaWithFacets.ts b/src/models/MetaWithFacets.ts index a7fa8fe2..97940812 100644 --- a/src/models/MetaWithFacets.ts +++ b/src/models/MetaWithFacets.ts @@ -1,7 +1,7 @@ import { ListFacet } from './ListFacet'; -export interface MetaWithFacets { - Facets?: ListFacet[] +export interface MetaWithFacets { + Facets?: TListFacet[] Page?: number PageSize?: number TotalCount?: number diff --git a/src/models/Order.ts b/src/models/Order.ts index cc92e522..6c8186af 100644 --- a/src/models/Order.ts +++ b/src/models/Order.ts @@ -2,14 +2,14 @@ import { OrderUser } from './OrderUser'; import { Address } from './Address'; import { OrderStatus } from './OrderStatus'; -export interface Order { +export interface Order { ID?: string - readonly FromUser?: OrderUser + readonly FromUser?: TFromUser FromCompanyID?: string ToCompanyID?: string FromUserID?: string BillingAddressID?: string - readonly BillingAddress?: Address + readonly BillingAddress?: TBillingAddress ShippingAddressID?: string Comments?: string readonly LineItemCount?: number diff --git a/src/models/OrderApproval.ts b/src/models/OrderApproval.ts index cb0e3d26..abb2aad3 100644 --- a/src/models/OrderApproval.ts +++ b/src/models/OrderApproval.ts @@ -1,13 +1,13 @@ import { ApprovalStatus } from './ApprovalStatus'; import { User } from './User'; -export interface OrderApproval { +export interface OrderApproval { readonly AllowResubmit?: boolean readonly ApprovalRuleID?: string readonly ApprovingGroupID?: string readonly Status?: ApprovalStatus readonly DateCreated?: string readonly DateCompleted?: string - readonly Approver?: User + readonly Approver?: TApprover readonly Comments?: string } \ No newline at end of file diff --git a/src/models/OrderCalculateResponse.ts b/src/models/OrderCalculateResponse.ts index 82a2c916..47c8a3bb 100644 --- a/src/models/OrderCalculateResponse.ts +++ b/src/models/OrderCalculateResponse.ts @@ -1,7 +1,7 @@ import { LineItemOverride } from './LineItemOverride'; -export interface OrderCalculateResponse { - LineItemOverrides?: LineItemOverride[] +export interface OrderCalculateResponse { + LineItemOverrides?: TLineItemOverride[] ShippingTotal?: number TaxTotal?: number FeeTotal?: number diff --git a/src/models/OrderRepeatResponse.ts b/src/models/OrderRepeatResponse.ts index 3ed67bdb..bcd02cd9 100644 --- a/src/models/OrderRepeatResponse.ts +++ b/src/models/OrderRepeatResponse.ts @@ -1,7 +1,7 @@ import { Order } from './Order'; import { UnavailableLineItem } from './UnavailableLineItem'; -export interface OrderRepeatResponse { - Order?: Order +export interface OrderRepeatResponse { + Order?: TOrder UnavailableItems?: UnavailableLineItem[] } \ No newline at end of file diff --git a/src/models/OrderReturnApproval.ts b/src/models/OrderReturnApproval.ts index 4fca2a3c..f25d180d 100644 --- a/src/models/OrderReturnApproval.ts +++ b/src/models/OrderReturnApproval.ts @@ -1,13 +1,13 @@ import { ApprovalStatus } from './ApprovalStatus'; import { User } from './User'; -export interface OrderReturnApproval { +export interface OrderReturnApproval { OrderReturnID?: string readonly ApprovalRuleID?: string readonly ApprovingGroupID?: string readonly Status?: ApprovalStatus readonly DateCreated?: string readonly DateCompleted?: string - readonly Approver?: User + readonly Approver?: TApprover readonly Comments?: string } \ No newline at end of file diff --git a/src/models/OrderReturnCalculatePayload.ts b/src/models/OrderReturnCalculatePayload.ts index 32e918d3..8b7348e8 100644 --- a/src/models/OrderReturnCalculatePayload.ts +++ b/src/models/OrderReturnCalculatePayload.ts @@ -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 - OrderWorksheet?: OrderWorksheet< - TFromUserXp, - TBillingAddressXp, - TOrderXp, - TProductXp, - TVariantXp, - TShippingAddressXp, - TShipFromAddressXp, - TLineItemsXp, - TOrderPromotionsXp, - TShipEstimatesXp, - TShipEstimateResponseXp, - TOrderCalculateResponseXp, - TOrderSubmitResponseXp, - TOrderSubmitForApprovalResponseXp, - TOrderApprovedResponseXp - > + OrderReturn?: TOrderReturn + OrderWorksheet?: TOrderWorksheet } diff --git a/src/models/OrderSplitResult.ts b/src/models/OrderSplitResult.ts index f82369cb..9881a6f2 100644 --- a/src/models/OrderSplitResult.ts +++ b/src/models/OrderSplitResult.ts @@ -1,6 +1,6 @@ import { Order } from './Order'; -export interface OrderSplitResult { - OutgoingOrders?: Order[] +export interface OrderSplitResult { + OutgoingOrders?: TOrder[] RemainingLineItemIDs?: string[] } \ No newline at end of file diff --git a/src/models/OrderWorksheet.ts b/src/models/OrderWorksheet.ts index 7822de20..1bf762b8 100644 --- a/src/models/OrderWorksheet.ts +++ b/src/models/OrderWorksheet.ts @@ -9,15 +9,15 @@ import { OrderSubmitForApprovalResponse } from './OrderSubmitForApprovalResponse import { OrderApprovedResponse } from './OrderApprovedResponse'; import { SubscriptionIntegrationResponse } from './SubscriptionIntegrationResponse'; -export interface OrderWorksheet { - Order?: Order - LineItems?: LineItem[] - OrderPromotions?: OrderPromotion[] - Subscription?: Subscription - ShipEstimateResponse?: ShipEstimateResponse - OrderCalculateResponse?: OrderCalculateResponse - OrderSubmitResponse?: OrderSubmitResponse - OrderSubmitForApprovalResponse?: OrderSubmitForApprovalResponse - OrderApprovedResponse?: OrderApprovedResponse - SubscriptionIntegrationResponse?: SubscriptionIntegrationResponse +export interface OrderWorksheet { + Order?: TOrder + LineItems?: TLineItem[] + OrderPromotions?: TOrderPromotion[] + Subscription?: TSubscription + ShipEstimateResponse?: TShipEstimateResponse + OrderCalculateResponse?: TOrderCalculateResponse + OrderSubmitResponse?: TOrderSubmitResponse + OrderSubmitForApprovalResponse?: TOrderSubmitForApprovalResponse + OrderApprovedResponse?: TOrderApprovedResponse + SubscriptionIntegrationResponse?: TSubscriptionIntegrationResponse } \ No newline at end of file diff --git a/src/models/Payment.ts b/src/models/Payment.ts index 98835832..d9dd9dc4 100644 --- a/src/models/Payment.ts +++ b/src/models/Payment.ts @@ -1,7 +1,7 @@ import { PaymentType } from './PaymentType'; import { PaymentTransaction } from './PaymentTransaction'; -export interface Payment { +export interface Payment { ID?: string Type: PaymentType readonly DateCreated?: string @@ -13,5 +13,5 @@ export interface Payment { Accepted?: boolean OrderReturnID?: string xp?: TPaymentXp - readonly Transactions?: PaymentTransaction[] + readonly Transactions?: TPaymentTransaction[] } \ No newline at end of file diff --git a/src/models/ProductCollectionBuyerProduct.ts b/src/models/ProductCollectionBuyerProduct.ts index 6081efbb..303dee85 100644 --- a/src/models/ProductCollectionBuyerProduct.ts +++ b/src/models/ProductCollectionBuyerProduct.ts @@ -1,10 +1,10 @@ import { BuyerPriceSchedule } from './BuyerPriceSchedule'; import { Inventory } from './Inventory'; -export interface ProductCollectionBuyerProduct { +export interface ProductCollectionBuyerProduct { ListOrder?: number DateAdded?: string - readonly PriceSchedule?: BuyerPriceSchedule + readonly PriceSchedule?: TPriceSchedule ID?: string ParentID?: string IsParent?: boolean diff --git a/src/models/ProductSeller.ts b/src/models/ProductSeller.ts index b44a2a3f..fc6dc370 100644 --- a/src/models/ProductSeller.ts +++ b/src/models/ProductSeller.ts @@ -1,7 +1,7 @@ import { BuyerPriceSchedule } from './BuyerPriceSchedule'; -export interface ProductSeller { - PriceSchedule?: BuyerPriceSchedule +export interface ProductSeller { + PriceSchedule?: TPriceSchedule ID?: string Name?: string } \ No newline at end of file diff --git a/src/models/RefreshPromosResponse.ts b/src/models/RefreshPromosResponse.ts index c41e419f..44d4f2a2 100644 --- a/src/models/RefreshPromosResponse.ts +++ b/src/models/RefreshPromosResponse.ts @@ -1,7 +1,7 @@ import { AddedPromo } from './AddedPromo'; import { RemovedPromo } from './RemovedPromo'; -export interface RefreshPromosResponse { - PromosAdded?: AddedPromo[] - PromosRemoved?: RemovedPromo[] +export interface RefreshPromosResponse { + PromosAdded?: TAddedPromo[] + PromosRemoved?: TRemovedPromo[] } \ No newline at end of file diff --git a/src/models/ShipEstimate.ts b/src/models/ShipEstimate.ts index 77dc6e2b..3595485b 100644 --- a/src/models/ShipEstimate.ts +++ b/src/models/ShipEstimate.ts @@ -1,10 +1,10 @@ import { ShipEstimateItem } from './ShipEstimateItem'; import { ShipMethod } from './ShipMethod'; -export interface ShipEstimate { +export interface ShipEstimate { ID?: string xp?: TShipEstimateXp SelectedShipMethodID?: string ShipEstimateItems?: ShipEstimateItem[] - ShipMethods?: ShipMethod[] + ShipMethods?: TShipMethod[] } \ No newline at end of file diff --git a/src/models/ShipEstimateResponse.ts b/src/models/ShipEstimateResponse.ts index b2307fca..53d0f2f7 100644 --- a/src/models/ShipEstimateResponse.ts +++ b/src/models/ShipEstimateResponse.ts @@ -1,7 +1,7 @@ import { ShipEstimate } from './ShipEstimate'; -export interface ShipEstimateResponse { - ShipEstimates?: ShipEstimate[] +export interface ShipEstimateResponse { + ShipEstimates?: TShipEstimate[] HttpStatusCode?: number UnhandledErrorBody?: string xp?: TShipEstimateResponseXp diff --git a/src/models/Shipment.ts b/src/models/Shipment.ts index 3cce448e..8c25b60d 100644 --- a/src/models/Shipment.ts +++ b/src/models/Shipment.ts @@ -1,6 +1,6 @@ import { Address } from './Address'; -export interface Shipment { +export interface Shipment { ID?: string BuyerID?: string Shipper?: string @@ -13,6 +13,6 @@ export interface Shipment - readonly ToAddress?: Address + readonly FromAddress?: TFromAddress + readonly ToAddress?: TToAddress } \ No newline at end of file diff --git a/src/models/ShipmentItem.ts b/src/models/ShipmentItem.ts index 38e997fd..4ad14234 100644 --- a/src/models/ShipmentItem.ts +++ b/src/models/ShipmentItem.ts @@ -2,15 +2,15 @@ import { LineItemProduct } from './LineItemProduct'; import { LineItemVariant } from './LineItemVariant'; import { LineItemSpec } from './LineItemSpec'; -export interface ShipmentItem { +export interface ShipmentItem { OrderID?: string LineItemID?: string QuantityShipped?: number readonly UnitPrice?: number readonly CostCenter?: string readonly DateNeeded?: string - readonly Product?: LineItemProduct - readonly Variant?: LineItemVariant + readonly Product?: TProduct + readonly Variant?: TVariant readonly Specs?: LineItemSpec[] readonly xp?: TShipmentItemXp } \ No newline at end of file diff --git a/src/models/Spec.ts b/src/models/Spec.ts index 0bcc6fe9..f86d4718 100644 --- a/src/models/Spec.ts +++ b/src/models/Spec.ts @@ -1,6 +1,6 @@ import { SpecOption } from './SpecOption'; -export interface Spec { +export interface Spec { OwnerID?: string ID?: string ListOrder?: number @@ -12,5 +12,5 @@ export interface Spec { DefinesVariant?: boolean xp?: TSpecXp readonly OptionCount?: number - readonly Options?: SpecOption[] + readonly Options?: TSpecOption[] } \ No newline at end of file diff --git a/tests/model-type-params.ts b/tests/model-type-params.ts new file mode 100644 index 00000000..b8ef8e4a --- /dev/null +++ b/tests/model-type-params.ts @@ -0,0 +1,90 @@ +import { + Address, + Category, + LineItem, + LineItemProduct, + LineItemVariant, + ListFacet, + ListPageWithFacets, + MessageSenderOrderSubmittedPayload, + Order, + OrderUser, + OrderWorksheet, + Product, +} from '../src/models' + +interface MyOrderXp { + ErpId?: string +} +interface MyFromUserXp { + Department?: string +} +interface MyAddressXp { + Dock?: string +} +interface MyFacetXp { + Color?: string +} + +type OrderXpOnly = Order +const orderXpOnly: OrderXpOnly = { xp: { ErpId: '1' } } + +interface MyOrderUser extends OrderUser {} +interface MyAddress extends Address {} +interface MyOrder extends Order {} +interface MyLineItem + extends LineItem< + { GiftWrap?: boolean }, + LineItemProduct, + LineItemVariant, + MyAddress, + MyAddress + > {} + +type Worksheet = OrderWorksheet +const worksheet: Worksheet = { + Order: { + xp: { ErpId: '1' }, + FromUser: { xp: { Department: 'sales' } }, + BillingAddress: { + Street1: '1 Main', + City: 'Minneapolis', + Country: 'US', + xp: { Dock: 'A' }, + }, + }, + LineItems: [{ ProductID: 'sku', xp: { GiftWrap: true } }], +} + +type CategoryXpOnly = Category<{ Featured?: boolean }> +const category: CategoryXpOnly = { xp: { Featured: true } } + +type ProductList = ListPageWithFacets +type FacetList = ListPageWithFacets> +const facetName: string | undefined = ({} as FacetList).Meta?.Facets?.[0]?.xp + ?.Color + +type Submitted = MessageSenderOrderSubmittedPayload< + any, + MyOrder, + any, + MyLineItem +> + +// Own-xp-only models do not grow a second parameter. +// @ts-expect-error Category accepts only its own xp +type CategoryWithExtra = Category<{ Featured?: boolean }, { no: true }> + +// The second parameter is the user model, not an xp bag. +// @ts-expect-error Order's second parameter must extend OrderUser +type OrderWithNestedXp = Order + +// The first parameter is the order model, not an xp bag. +// @ts-expect-error OrderWorksheet's first parameter must extend Order +type WorksheetWithXp = OrderWorksheet + +void orderXpOnly +void worksheet +void category +void facetName +void (null as ProductList | Submitted | null) diff --git a/tests/tsconfig.model-types.json b/tests/tsconfig.model-types.json new file mode 100644 index 00000000..8508d84f --- /dev/null +++ b/tests/tsconfig.model-types.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "strict": true, + "noEmit": true, + "target": "ES2015", + "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true + }, + "files": ["model-type-params.ts"] +}