Merge pull request #29236 from woocommerce/fix/28883

add variable product support to api package
This commit is contained in:
Greg 2021-03-03 17:20:20 -07:00 committed by GitHub
commit e52504679d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 1029 additions and 173 deletions

View File

@ -3,7 +3,8 @@
## Added
- Support for the external product type.
- Added support for grouped product type
- Support for grouped product type.
- Support for variable products and product variations.
- Support for coupons.
# 0.1.1

View File

@ -1,5 +1,6 @@
import { Model } from '../../models';
import {
CreatesChildModels,
CreatesModels,
DeletesChildModels,
DeletesModels,
@ -90,7 +91,7 @@ describe( 'ModelRepository', () => {
it( 'should create child', async () => {
const model = new DummyChildModel();
const callback = jest.fn().mockResolvedValue( model );
const repository: CreatesModels< DummyChildParams > = new ModelRepository< DummyChildParams >(
const repository: CreatesChildModels< DummyChildParams > = new ModelRepository< DummyChildParams >(
null,
callback,
null,
@ -98,7 +99,7 @@ describe( 'ModelRepository', () => {
null,
);
const created = await repository.create( { childName: 'test' } );
const created = await repository.create( { parent: 'yes' }, { childName: 'test' } );
expect( created ).toBe( model );
expect( callback ).toHaveBeenCalledWith( { childName: 'test' } );
} );

View File

@ -77,6 +77,20 @@ export type ListChildFn< T extends ModelRepositoryParams > = (
*/
export type CreateFn< T extends ModelRepositoryParams > = ( properties: Partial< ModelClass< T > > ) => Promise< ModelClass< T > >;
/**
* A callback for creating a child model using a data source.
*
* @callback CreateChildFn
* @param {ModelID} parent The parent identifier for the model.
* @param {Partial.<T>} properties The properties of the model to create.
* @return {Promise.<T>} Resolves to the created model.
* @template {Model} T
*/
export type CreateChildFn< T extends ModelRepositoryParams > = (
parent: ParentID< T >,
properties: Partial< ModelClass< T > >
) => Promise< ModelClass< T > >;
/**
* A callback for reading a model using a data source.
*
@ -189,6 +203,21 @@ export interface CreatesModels< T extends ModelRepositoryParams > {
create( properties: Partial< ModelClass< T > > ): Promise< ModelClass< T > >;
}
/**
* An interface for repositories that can create child models.
*
* @typedef CreatesChildModels
* @property {CreateChildFn.<T>} create Creates a child model using the repository.
* @template {Model} T
* @template {ModelParentID} P
*/
export interface CreatesChildModels< T extends ModelRepositoryParams > {
create(
parent: HasParent< T, ParentID< T >, never >,
properties: HasParent< T, Partial< ModelClass< T > >, never >,
): Promise< ModelClass< T > >;
}
/**
* An interface for repositories that can read models.
*
@ -301,7 +330,7 @@ export class ModelRepository< T extends ModelRepositoryParams > implements
* @type {CreateFn.<T>}
* @private
*/
private readonly createHook: CreateFn< T > | null;
private readonly createHook: HasParent< T, CreateChildFn< T >, CreateFn< T > > | null;
/**
* The hook used to read models.
@ -338,7 +367,7 @@ export class ModelRepository< T extends ModelRepositoryParams > implements
*/
public constructor(
listHook: HasParent< T, ListChildFn< T >, ListFn< T > > | null,
createHook: CreateFn< T > | null,
createHook: HasParent< T, CreateChildFn< T >, CreateFn< T > > | null,
readHook: HasParent< T, ReadChildFn< T >, ReadFn< T > > | null,
updateHook: HasParent< T, UpdateChildFn< T >, UpdateFn< T > > | null,
deleteHook: HasParent< T, DeleteChildFn< T >, DeleteFn > | null,
@ -380,15 +409,28 @@ export class ModelRepository< T extends ModelRepositoryParams > implements
/**
* Creates a new model using the repository.
*
* @param {P|ModelID} propertiesOrParent The properties to create the model with or the model parent.
* @param {Partial.<T>} properties The properties to create the model with.
* @return {Promise.<T>} Resolves to the created model.
*/
public create( properties: Partial< ModelClass< T > > ): Promise< ModelClass< T > > {
public create(
propertiesOrParent?: HasParent< T, ParentID< T >, Partial< ModelClass<T> > >,
properties?: HasParent< T, Partial< ModelClass<T> >, never >,
): Promise< ModelClass< T > > {
if ( ! this.createHook ) {
throw new Error( 'The \'create\' operation is not supported on this model.' );
}
return this.createHook( properties );
if ( properties === undefined ) {
return ( this.createHook as CreateFn< T > )(
propertiesOrParent as Partial< ModelClass<T> >,
);
}
return ( this.createHook as CreateChildFn< T > )(
propertiesOrParent as ParentID<T>,
properties as Partial< ModelClass<T> >,
);
}
/**

View File

@ -1,11 +1,10 @@
import { Model, ModelID } from '../../model';
import { MetaData, PostStatus } from '../../shared-types';
import { AbstractProductData } from './data';
import { ModelID } from '../../model';
import {
CatalogVisibility,
ProductAttribute,
ProductImage,
ProductTerm,
ProductLinks,
ProductAttribute,
} from '../shared';
/**
@ -31,7 +30,7 @@ export const buildProductURL = ( id: ModelID ) => baseProductURL() + id;
/**
* The base for all product types.
*/
export abstract class AbstractProduct extends Model {
export abstract class AbstractProduct extends AbstractProductData {
/**
* The name of the product.
*
@ -46,34 +45,6 @@ export abstract class AbstractProduct extends Model {
*/
public readonly slug: string = '';
/**
* The permalink of the product.
*
* @type {string}
*/
public readonly permalink: string = '';
/**
* The Id of the product.
*
* @type {number}
*/
public readonly id: number = 0;
/**
* The parent Id of the product.
*
* @type {number}
*/
public readonly parentId: number = 0;
/**
* The menu order assigned to the product.
*
* @type {number}
*/
public readonly menuOrder: number = 0;
/**
* The GMT datetime when the product was created.
*
@ -88,13 +59,6 @@ export abstract class AbstractProduct extends Model {
*/
public readonly modified: Date = new Date();
/**
* The product's current post status.
*
* @type {PostStatus}
*/
public readonly postStatus: PostStatus = '';
/**
* The product's short description.
*
@ -102,20 +66,6 @@ export abstract class AbstractProduct extends Model {
*/
public readonly shortDescription: string = '';
/**
* The product's full description.
*
* @type {string}
*/
public readonly description: string = '';
/**
* The product's SKU.
*
* @type {string}
*/
public readonly sku: string = '';
/**
* An array of the categories this product is in.
*
@ -130,13 +80,6 @@ export abstract class AbstractProduct extends Model {
*/
public readonly tags: readonly ProductTerm[] = [];
/**
* Indicates whether or not the product is currently able to be purchased.
*
* @type {boolean}
*/
public readonly isPurchasable: boolean = true;
/**
* Indicates whether or not the product should be featured.
*
@ -144,20 +87,6 @@ export abstract class AbstractProduct extends Model {
*/
public readonly isFeatured: boolean = false;
/**
* The attributes for the product.
*
* @type {ReadonlyArray.<ProductAttribute>}
*/
public readonly attributes: readonly ProductAttribute[] = [];
/**
* The images for the product.
*
* @type {ReadonlyArray.<ProductImage>}
*/
public readonly images: readonly ProductImage[] = [];
/**
* Indicates whether or not the product should be visible in the catalog.
*
@ -165,55 +94,6 @@ export abstract class AbstractProduct extends Model {
*/
public readonly catalogVisibility: CatalogVisibility = CatalogVisibility.Everywhere;
/**
* The current price of the product.
*
* @type {string}
*/
public readonly price: string = '';
/**
* The rendered HTML for the current price of the product.
*
* @type {string}
*/
public readonly priceHtml: string = '';
/**
* The regular price of the product when not discounted.
*
* @type {string}
*/
public readonly regularPrice: string = '';
/**
* Indicates whether or not the product is currently on sale.
*
* @type {boolean}
*/
public readonly onSale: boolean = false;
/**
* The price of the product when on sale.
*
* @type {string}
*/
public readonly salePrice: string = '';
/**
* The GMT datetime when the product should start to be on sale.
*
* @type {Date|null}
*/
public readonly saleStart: Date | null = null;
/**
* The GMT datetime when the product should no longer be on sale.
*
* @type {Date|null}
*/
public readonly saleEnd: Date | null = null;
/**
* The count of sales of the product
*
@ -250,11 +130,11 @@ export abstract class AbstractProduct extends Model {
public readonly relatedIds: Array<number> = [];
/**
* The extra metadata for the product.
* The attributes for the product.
*
* @type {ReadonlyArray.<MetaData>}
* @type {ReadonlyArray.<ProductAttribute>}
*/
public readonly metaData: readonly MetaData[] = [];
public readonly attributes: readonly ProductAttribute[] = [];
/**
* The products links.

View File

@ -0,0 +1,102 @@
import { Model } from '../../model';
import { MetaData, PostStatus } from '../../shared-types';
import { ProductImage, ProductLinks } from '../shared';
/**
* Base product data.
*/
export abstract class AbstractProductData extends Model {
/**
* The permalink of the product.
*
* @type {string}
*/
public readonly permalink: string = '';
/**
* The Id of the product.
*
* @type {number}
*/
public readonly id: number = 0;
/**
* The parent Id of the product.
*
* @type {number}
*/
public readonly parentId: number = 0;
/**
* The menu order assigned to the product.
*
* @type {number}
*/
public readonly menuOrder: number = 0;
/**
* The GMT datetime when the product was created.
*
* @type {Date}
*/
public readonly created: Date = new Date();
/**
* The GMT datetime when the product was last modified.
*
* @type {Date}
*/
public readonly modified: Date = new Date();
/**
* The product's current post status.
*
* @type {PostStatus}
*/
public readonly postStatus: PostStatus = '';
/**
* The product's full description.
*
* @type {string}
*/
public readonly description: string = '';
/**
* The product's SKU.
*
* @type {string}
*/
public readonly sku: string = '';
/**
* Indicates whether or not the product is currently able to be purchased.
*
* @type {boolean}
*/
public readonly isPurchasable: boolean = true;
/**
* The images for the product.
*
* @type {ReadonlyArray.<ProductImage>}
*/
public readonly images: readonly ProductImage[] = [];
/**
* The extra metadata for the product.
*
* @type {ReadonlyArray.<MetaData>}
*/
public readonly metaData: readonly MetaData[] = [];
/**
* The product data links.
*
* @type {ReadonlyArray.<ProductLinks>}
*/
public readonly links: ProductLinks = {
collection: [ { href: '' } ],
self: [ { href: '' } ],
};
}

View File

@ -1,5 +1,6 @@
export * from './common';
export * from './cross-sell';
export * from './data';
export * from './delivery';
export * from './external';
export * from './grouped';

View File

@ -40,7 +40,7 @@ export type GroupedProductRepositoryParams =
* @typedef ListsGroupedProducts
* @alias ListsModels.<GroupedProduct>
*/
export type ListsGroupedProducts = ListsModels< GroupedProductUpdateParams >;
export type ListsGroupedProducts = ListsModels< GroupedProductRepositoryParams >;
/**
* An interface for creating Grouped products using the repository.
@ -48,7 +48,7 @@ export type ListsGroupedProducts = ListsModels< GroupedProductUpdateParams >;
* @typedef CreatesGroupedProducts
* @alias CreatesModels.<GroupedProduct>
*/
export type CreatesGroupedProducts = CreatesModels< GroupedProductUpdateParams >;
export type CreatesGroupedProducts = CreatesModels< GroupedProductRepositoryParams >;
/**
* An interface for reading Grouped products using the repository.
@ -56,7 +56,7 @@ export type CreatesGroupedProducts = CreatesModels< GroupedProductUpdateParams >
* @typedef ReadsGroupedProducts
* @alias ReadsModels.<GroupedProduct>
*/
export type ReadsGroupedProducts = ReadsModels< GroupedProductUpdateParams >;
export type ReadsGroupedProducts = ReadsModels< GroupedProductRepositoryParams >;
/**
* An interface for updating Grouped products using the repository.
@ -64,7 +64,7 @@ export type ReadsGroupedProducts = ReadsModels< GroupedProductUpdateParams >;
* @typedef UpdatesGroupedProducts
* @alias UpdatesModels.<GroupedProduct>
*/
export type UpdatesGroupedProducts = UpdatesModels< GroupedProductUpdateParams >;
export type UpdatesGroupedProducts = UpdatesModels< GroupedProductRepositoryParams >;
/**
* An interface for deleting Grouped products using the repository.
@ -72,7 +72,7 @@ export type UpdatesGroupedProducts = UpdatesModels< GroupedProductUpdateParams >
* @typedef DeletesGroupedProducts
* @alias DeletesModels.<GroupedProduct>
*/
export type DeletesGroupedProducts = DeletesModels< GroupedProductUpdateParams >;
export type DeletesGroupedProducts = DeletesModels< GroupedProductRepositoryParams >;
/**
* The base for the Grouped product object.

View File

@ -3,3 +3,5 @@ export * from './shared';
export * from './simple-product';
export * from './grouped-product';
export * from './external-product';
export * from './variation';
export * from './variable-product';

View File

@ -70,9 +70,9 @@ export class ProductDownload {
}
/**
* A product's attributes.
* Attribute base class.
*/
export class ProductAttribute {
export abstract class AbstractAttribute {
/**
* The ID of the attribute.
*
@ -86,7 +86,12 @@ export class ProductAttribute {
* @type {string}
*/
public readonly name: string = '';
}
/**
* A product's attributes.
*/
export class ProductAttribute extends AbstractAttribute {
/**
* The sort order of the attribute.
*
@ -121,6 +126,29 @@ export class ProductAttribute {
* @param {Partial.<ProductAttribute>} properties The properties to set.
*/
public constructor( properties?: Partial< ProductAttribute > ) {
super();
Object.assign( this, properties );
}
}
/**
* Default attributes for variable products.
*/
export class ProductDefaultAttribute extends AbstractAttribute {
/**
* The option selected for the attribute.
*
* @type {string}
*/
public readonly option: string = '';
/**
* Creates a new product default attribute.
*
* @param {Partial.<ProductDefaultAttribute>} properties The properties to set.
*/
public constructor( properties?: Partial< ProductDefaultAttribute > ) {
super();
Object.assign( this, properties );
}
}
@ -220,6 +248,13 @@ export class ProductLinks {
*/
public readonly self: readonly ProductLinkItem[] = [];
/**
* The link to the parent.
*
* @type {ReadonlyArray.<ProductLinkItem>}
*/
public readonly up?: readonly ProductLinkItem[] = [];
/**
* Creates a new product link list.
*

View File

@ -6,14 +6,22 @@
*/
export type StockStatus = 'instock' | 'outofstock' | 'onbackorder' | string
/**
* Base product properties.
*/
export type ProductDataUpdateParams = 'created' | 'postStatus'
| 'id' | 'permalink' | 'price' | 'priceHtml'
| 'description' | 'sku' | 'attributes' | 'images'
| 'regularPrice' | 'salePrice' | 'saleStart' | 'saleEnd'
| 'metaData' | 'menuOrder' | 'parentId' | 'links';
/**
* Properties common to all product types.
*/
export type ProductCommonUpdateParams = 'name' | 'slug' | 'created' | 'postStatus' | 'shortDescription'
| 'id' | 'permalink' | 'type' | 'description' | 'sku' | 'categories' | 'tags' | 'isFeatured'
| 'attributes' | 'images' | 'catalogVisibility' | 'allowReviews'
| 'metaData' | 'menuOrder' | 'parentId' | 'relatedIds' | 'upsellIds'
| 'links' | 'relatedIds' | 'menuOrder' | 'parentId';
export type ProductCommonUpdateParams = 'name' | 'slug' | 'shortDescription'
| 'categories' | 'tags' | 'isFeatured' | 'averageRating' | 'numRatings'
| 'catalogVisibility' | 'allowReviews' | 'upsellIds' | 'type'
& ProductDataUpdateParams;
/**
* Cross sells property.
@ -67,4 +75,4 @@ export type ProductDeliveryUpdateParams = 'daysToDownload' | 'downloadLimit' | '
/**
* Properties exclusive to the Variable product type.
*/
export type ProductVariableTypeUpdateParams = 'defaultAttributes' | 'variations';
export type ProductVariableUpdateParams = 'defaultAttributes' | 'variations';

View File

@ -0,0 +1,173 @@
import {
AbstractProduct,
IProductCommon,
IProductCrossSells,
IProductInventory,
IProductSalesTax,
IProductShipping,
IProductUpSells,
ProductSearchParams,
} from './abstract';
import {
ProductInventoryUpdateParams,
ProductCommonUpdateParams,
ProductDefaultAttribute,
ProductSalesTaxUpdateParams,
ProductCrossUpdateParams,
ProductShippingUpdateParams,
ProductUpSellUpdateParams,
ProductVariableUpdateParams,
StockStatus,
BackorderStatus,
Taxability,
} from './shared';
import { HTTPClient } from '../../http';
import { variableProductRESTRepository } from '../../repositories';
import {
CreatesModels,
DeletesModels,
ListsModels,
ModelRepositoryParams,
ReadsModels,
UpdatesModels,
} from '../../framework';
/**
* The parameters that variable products can update.
*/
type VariableProductUpdateParams = ProductVariableUpdateParams
& ProductCommonUpdateParams
& ProductCrossUpdateParams
& ProductInventoryUpdateParams
& ProductSalesTaxUpdateParams
& ProductShippingUpdateParams
& ProductUpSellUpdateParams;
/**
* The parameters embedded in this generic can be used in the ModelRepository in order to give
* type-safety in an incredibly granular way.
*/
export type VariableProductRepositoryParams =
ModelRepositoryParams< VariableProduct, never, ProductSearchParams, VariableProductUpdateParams >;
/**
* An interface for listing variable products using the repository.
*
* @typedef ListsVariableProducts
* @alias ListsModels.<VariableProduct>
*/
export type ListsVariableProducts = ListsModels< VariableProductRepositoryParams >;
/**
* An interface for creating variable products using the repository.
*
* @typedef CreatesVariableProducts
* @alias CreatesModels.<VariableProduct>
*/
export type CreatesVariableProducts = CreatesModels< VariableProductRepositoryParams >;
/**
* An interface for reading variable products using the repository.
*
* @typedef ReadsVariableProducts
* @alias ReadsModels.<VariableProduct>
*/
export type ReadsVariableProducts = ReadsModels< VariableProductRepositoryParams >;
/**
* An interface for updating variable products using the repository.
*
* @typedef UpdatesVariableProducts
* @alias UpdatesModels.<VariableProduct>
*/
export type UpdatesVariableProducts = UpdatesModels< VariableProductRepositoryParams >;
/**
* An interface for deleting variable products using the repository.
*
* @typedef DeletesVariableProducts
* @alias DeletesModels.<VariableProduct>
*/
export type DeletesVariableProducts = DeletesModels< VariableProductRepositoryParams >;
/**
* The base for the Variable product object.
*/
export class VariableProduct extends AbstractProduct implements
IProductCommon,
IProductCrossSells,
IProductInventory,
IProductSalesTax,
IProductShipping,
IProductUpSells {
/**
* @see ./abstracts/cross-sells.ts
*/
public readonly crossSellIds: Array<number> = [];
/**
* @see ./abstracts/upsell.ts
*/
public readonly upSellIds: Array<number> = [];
/**
* @see ./abstracts/inventory.ts
*/
public readonly onePerOrder: boolean = false;
public readonly trackInventory: boolean = false;
public readonly remainingStock: number = -1;
public readonly stockStatus: StockStatus = ''
public readonly backorderStatus: BackorderStatus = BackorderStatus.Allowed;
public readonly canBackorder: boolean = false;
public readonly isOnBackorder: boolean = false;
/**
* @see ./abstracts/sales-tax.ts
*/
public readonly taxStatus: Taxability = Taxability.ProductAndShipping;
public readonly taxClass: string = '';
/**
* @see ./abstracts/shipping.ts
*/
public readonly weight: string = '';
public readonly length: string = '';
public readonly width: string = '';
public readonly height: string = '';
public readonly requiresShipping: boolean = false;
public readonly isShippingTaxable: boolean = false;
public readonly shippingClass: string = '';
public readonly shippingClassId: number = 0;
/**
* Default product attributes.
*
* @type {ReadonlyArray.<ProductDefaultAttribute>}
*/
public readonly defaultAttributes: readonly ProductDefaultAttribute[] = [];
/**
* Product variations.
*
* @type {ReadonlyArray.<number>}
*/
public readonly variations: Array<number> = [];
/**
* Creates a new Variable product instance with the given properties
*
* @param {Object} properties The properties to set in the object.
*/
public constructor( properties?: Partial< VariableProduct > ) {
super();
Object.assign( this, properties );
}
/**
* Creates a model repository configured for communicating via the REST API.
*
* @param {HTTPClient} httpClient The client for communicating via HTTP.
*/
public static restRepository( httpClient: HTTPClient ): ReturnType< typeof variableProductRESTRepository > {
return variableProductRESTRepository( httpClient );
}
}

View File

@ -0,0 +1,187 @@
import { ModelID } from '../model';
import {
AbstractProductData,
IProductDelivery,
IProductInventory,
IProductPrice,
IProductSalesTax,
IProductShipping,
ProductSearchParams,
} from './abstract';
import {
ProductDataUpdateParams,
ProductDeliveryUpdateParams,
ProductInventoryUpdateParams,
ProductPriceUpdateParams,
ProductSalesTaxUpdateParams,
ProductShippingUpdateParams,
ProductLinks,
Taxability,
ProductDownload,
StockStatus,
BackorderStatus,
ProductDefaultAttribute,
} from './shared';
import {
CreatesChildModels,
DeletesChildModels,
ListsChildModels,
ModelRepositoryParams,
ReadsChildModels,
UpdatesChildModels,
} from '../../framework';
import { HTTPClient } from '../../http';
import { productVariationRESTRepository } from '../../repositories';
/**
* The parameters that product variations can update.
*/
type ProductVariationUpdateParams = ProductDataUpdateParams
& ProductDeliveryUpdateParams
& ProductInventoryUpdateParams
& ProductPriceUpdateParams
& ProductSalesTaxUpdateParams
& ProductShippingUpdateParams;
/**
* The parameters embedded in this generic can be used in the ModelRepository in order to give
* type-safety in an incredibly granular way.
*/
export type ProductVariationRepositoryParams =
ModelRepositoryParams< ProductVariation, ModelID, ProductSearchParams, ProductVariationUpdateParams >;
/**
* An interface for listing variable products using the repository.
*
* @typedef ListsProductVariations
* @alias ListsModels.<ProductVariation>
*/
export type ListsProductVariations = ListsChildModels< ProductVariationRepositoryParams >;
/**
* An interface for creating variable products using the repository.
*
* @typedef CreatesProductVariations
* @alias CreatesModels.<ProductVariation>
*/
export type CreatesProductVariations = CreatesChildModels< ProductVariationRepositoryParams >;
/**
* An interface for reading variable products using the repository.
*
* @typedef ReadsProductVariations
* @alias ReadsModels.<ProductVariation>
*/
export type ReadsProductVariations = ReadsChildModels< ProductVariationRepositoryParams >;
/**
* An interface for updating variable products using the repository.
*
* @typedef UpdatesProductVariations
* @alias UpdatesModels.<ProductVariation>
*/
export type UpdatesProductVariations = UpdatesChildModels< ProductVariationRepositoryParams >;
/**
* An interface for deleting variable products using the repository.
*
* @typedef DeletesProductVariations
* @alias DeletesModels.<ProductVariation>
*/
export type DeletesProductVariations = DeletesChildModels< ProductVariationRepositoryParams >;
/**
* The base for the product variation object.
*/
export class ProductVariation extends AbstractProductData implements
IProductDelivery,
IProductInventory,
IProductPrice,
IProductSalesTax,
IProductShipping {
/**
* @see ./abstracts/delivery.ts
*/
public readonly isVirtual: boolean = false;
public readonly isDownloadable: boolean = false;
public readonly downloads: readonly ProductDownload[] = [];
public readonly downloadLimit: number = -1;
public readonly daysToDownload: number = -1;
public readonly purchaseNote: string = '';
/**
* @see ./abstracts/inventory.ts
*/
public readonly onePerOrder: boolean = false;
public readonly trackInventory: boolean = false;
public readonly remainingStock: number = -1;
public readonly stockStatus: StockStatus = ''
public readonly backorderStatus: BackorderStatus = BackorderStatus.Allowed;
public readonly canBackorder: boolean = false;
public readonly isOnBackorder: boolean = false;
/**
* @see ./abstracts/price.ts
*/
public readonly price: string = '';
public readonly priceHtml: string = '';
public readonly regularPrice: string = '';
public readonly onSale: boolean = false;
public readonly salePrice: string = '';
public readonly saleStart: Date | null = null;
public readonly saleEnd: Date | null = null;
/**
* @see ./abstracts/sales-tax.ts
*/
public readonly taxStatus: Taxability = Taxability.ProductAndShipping;
public readonly taxClass: string = '';
/**
* @see ./abstracts/shipping.ts
*/
public readonly weight: string = '';
public readonly length: string = '';
public readonly width: string = '';
public readonly height: string = '';
public readonly requiresShipping: boolean = false;
public readonly isShippingTaxable: boolean = false;
public readonly shippingClass: string = '';
public readonly shippingClassId: number = 0;
/**
* The variation links.
*
* @type {ReadonlyArray.<ProductLinks>}
*/
public readonly links: ProductLinks = {
collection: [ { href: '' } ],
self: [ { href: '' } ],
up: [ { href: '' } ],
};
/**
* The attributes for the variation.
*
* @type {ReadonlyArray.<ProductDefaultAttribute>}
*/
public readonly attributes: readonly ProductDefaultAttribute[] = [];
/**
* Creates a new product variation instance with the given properties
*
* @param {Object} properties The properties to set in the object.
*/
public constructor( properties?: Partial< ProductVariation > ) {
super();
Object.assign( this, properties );
}
/**
* Creates a model repository configured for communicating via the REST API.
*
* @param {HTTPClient} httpClient The client for communicating via HTTP.
*/
public static restRepository( httpClient: HTTPClient ): ReturnType< typeof productVariationRESTRepository > {
return productVariationRESTRepository( httpClient );
}
}

View File

@ -2,10 +2,14 @@ import { createProductTransformer } from './shared';
import { groupedProductRESTRepository } from './grouped-product';
import { simpleProductRESTRepository } from './simple-product';
import { externalProductRESTRepository } from './external-product';
import { variableProductRESTRepository } from './variable-product';
import { productVariationRESTRepository } from './variation';
export {
createProductTransformer,
externalProductRESTRepository,
groupedProductRESTRepository,
simpleProductRESTRepository,
variableProductRESTRepository,
productVariationRESTRepository,
};

View File

@ -12,6 +12,7 @@ import {
} from '../../../framework';
import {
AbstractProduct,
AbstractProductData,
IProductCrossSells,
IProductDelivery,
IProductExternal,
@ -26,6 +27,7 @@ import {
ProductDownload,
ProductImage,
ProductTerm,
VariableProduct,
} from '../../../models';
import { createMetaDataTransformer } from '../shared';
@ -110,6 +112,55 @@ function createProductDownloadTransformer(): ModelTransformer< ProductDownload >
);
}
/**
* Creates a transformer for the base product property data.
*
* @param {Array.<ModelTransformation>} transformations Optional transformers to add to the transformer.
* @return {ModelTransformer} The created transformer.
*/
export function createProductDataTransformer< T extends AbstractProductData >(
transformations?: ModelTransformation[],
): ModelTransformer< T > {
if ( ! transformations ) {
transformations = [];
}
transformations.push(
new IgnorePropertyTransformation(
[
'date_created',
'date_modified',
],
),
new ModelTransformerTransformation( 'images', ProductImage, createProductImageTransformer() ),
new ModelTransformerTransformation( 'metaData', MetaData, createMetaDataTransformer() ),
new PropertyTypeTransformation(
{
created: PropertyType.Date,
modified: PropertyType.Date,
isPurchasable: PropertyType.Boolean,
parentId: PropertyType.Integer,
menuOrder: PropertyType.Integer,
permalink: PropertyType.String,
},
),
new KeyChangeTransformation< AbstractProductData >(
{
created: 'date_created_gmt',
modified: 'date_modified_gmt',
postStatus: 'status',
isPurchasable: 'purchasable',
metaData: 'meta_data',
parentId: 'parent_id',
menuOrder: 'menu_order',
links: '_links',
},
),
);
return new ModelTransformer( transformations );
}
/**
* Creates a transformer for the shared properties of all products.
*
@ -127,56 +178,34 @@ export function createProductTransformer< T extends AbstractProduct >(
transformations.push(
new AddPropertyTransformation( {}, { type } ),
new IgnorePropertyTransformation(
[
'date_created',
'date_modified',
],
),
new ModelTransformerTransformation( 'categories', ProductTerm, createProductTermTransformer() ),
new ModelTransformerTransformation( 'tags', ProductTerm, createProductTermTransformer() ),
new ModelTransformerTransformation( 'attributes', ProductAttribute, createProductAttributeTransformer() ),
new ModelTransformerTransformation( 'images', ProductImage, createProductImageTransformer() ),
new ModelTransformerTransformation( 'metaData', MetaData, createMetaDataTransformer() ),
new PropertyTypeTransformation(
{
created: PropertyType.Date,
modified: PropertyType.Date,
isPurchasable: PropertyType.Boolean,
isFeatured: PropertyType.Boolean,
allowReviews: PropertyType.Boolean,
averageRating: PropertyType.Integer,
numRatings: PropertyType.Integer,
totalSales: PropertyType.Integer,
parentId: PropertyType.Integer,
menuOrder: PropertyType.Integer,
permalink: PropertyType.String,
relatedIds: PropertyType.Integer,
},
),
new KeyChangeTransformation< AbstractProduct >(
{
created: 'date_created_gmt',
modified: 'date_modified_gmt',
postStatus: 'status',
shortDescription: 'short_description',
isPurchasable: 'purchasable',
isFeatured: 'featured',
catalogVisibility: 'catalog_visibility',
allowReviews: 'reviews_allowed',
averageRating: 'average_rating',
numRatings: 'rating_count',
metaData: 'meta_data',
totalSales: 'total_sales',
parentId: 'parent_id',
menuOrder: 'menu_order',
relatedIds: 'related_ids',
links: '_links',
},
),
);
return new ModelTransformer( transformations );
return createProductDataTransformer< T >( transformations );
}
/**
@ -412,6 +441,29 @@ export function createProductShippingTransformation(): ModelTransformation[] {
return transformations;
}
/**
* Variable product specific properties transformations
*/
export function createProductVariableTransformation(): ModelTransformation[] {
const transformations = [
new PropertyTypeTransformation(
{
id: PropertyType.Integer,
name: PropertyType.String,
option: PropertyType.String,
variations: PropertyType.Integer,
},
),
new KeyChangeTransformation< VariableProduct >(
{
defaultAttributes: 'default_attributes',
},
),
];
return transformations;
}
/**
* Transformer for the properties unique to the external product type.
*/
@ -433,4 +485,3 @@ export function createProductExternalTransformation(): ModelTransformation[] {
return transformations;
}

View File

@ -0,0 +1,72 @@
import { HTTPClient } from '../../../http';
import { ModelRepository } from '../../../framework';
import {
VariableProduct,
CreatesVariableProducts,
DeletesVariableProducts,
ListsVariableProducts,
ReadsVariableProducts,
VariableProductRepositoryParams,
UpdatesVariableProducts,
baseProductURL,
buildProductURL,
} from '../../../models';
import {
createProductTransformer,
createProductCrossSellsTransformation,
createProductInventoryTransformation,
createProductSalesTaxTransformation,
createProductShippingTransformation,
createProductUpSellsTransformation,
createProductVariableTransformation,
} from './shared';
import {
restCreate,
restDelete,
restList,
restRead,
restUpdate,
} from '../shared';
/**
* Creates a new ModelRepository instance for interacting with models via the REST API.
*
* @param {HTTPClient} httpClient The HTTP client for the REST requests to be made using.
* @return {
* ListsVariableProducts|
* CreatesVariableProducts|
* ReadsVariableProducts|
* UpdatesVariableProducts|
* DeletesVariableProducts
* } The created repository.
*/
export function variableProductRESTRepository( httpClient: HTTPClient ): ListsVariableProducts
& CreatesVariableProducts
& ReadsVariableProducts
& UpdatesVariableProducts
& DeletesVariableProducts {
const crossSells = createProductCrossSellsTransformation();
const inventory = createProductInventoryTransformation();
const salesTax = createProductSalesTaxTransformation();
const shipping = createProductShippingTransformation();
const upsells = createProductUpSellsTransformation();
const variable = createProductVariableTransformation();
const transformations = [
...crossSells,
...inventory,
...salesTax,
...shipping,
...upsells,
...variable,
];
const transformer = createProductTransformer<VariableProduct>( 'variable', transformations );
return new ModelRepository(
restList< VariableProductRepositoryParams >( baseProductURL, VariableProduct, httpClient, transformer ),
restCreate< VariableProductRepositoryParams >( baseProductURL, VariableProduct, httpClient, transformer ),
restRead< VariableProductRepositoryParams >( buildProductURL, VariableProduct, httpClient, transformer ),
restUpdate< VariableProductRepositoryParams >( buildProductURL, VariableProduct, httpClient, transformer ),
restDelete< VariableProductRepositoryParams >( buildProductURL, httpClient ),
);
}

View File

@ -0,0 +1,73 @@
import { HTTPClient } from '../../../http';
import { ModelRepository } from '../../../framework';
import {
ProductVariation,
ModelID,
CreatesProductVariations,
DeletesProductVariations,
ListsProductVariations,
ReadsProductVariations,
ProductVariationRepositoryParams,
UpdatesProductVariations,
buildProductURL,
} from '../../../models';
import {
createProductDataTransformer,
createProductDeliveryTransformation,
createProductInventoryTransformation,
createProductPriceTransformation,
createProductSalesTaxTransformation,
createProductShippingTransformation,
} from './shared';
import {
restCreateChild,
restDeleteChild,
restListChild,
restReadChild,
restUpdateChild,
} from '../shared';
/**
* Creates a new ModelRepository instance for interacting with models via the REST API.
*
* @param {HTTPClient} httpClient The HTTP client for the REST requests to be made using.
* @return {
* ListsProductVariations|
* CreatesProductVariations|
* ReadsProductVariations|
* UpdatesProductVariations|
* DeletesProductVariations
* } The created repository.
*/
export function productVariationRESTRepository( httpClient: HTTPClient ): ListsProductVariations
& CreatesProductVariations
& ReadsProductVariations
& UpdatesProductVariations
& DeletesProductVariations {
const buildURL = ( parent: ModelID ) => buildProductURL( parent ) + '/variations/';
const buildChildURL = ( parent: ModelID, id: ModelID ) => buildURL( parent ) + id;
const buildDeleteURL = ( parent: ModelID, id: ModelID ) => buildChildURL( parent, id ) + '?force=true';
const delivery = createProductDeliveryTransformation();
const inventory = createProductInventoryTransformation();
const price = createProductPriceTransformation();
const salesTax = createProductSalesTaxTransformation();
const shipping = createProductShippingTransformation();
const transformations = [
...delivery,
...inventory,
...price,
...salesTax,
...shipping,
];
const transformer = createProductDataTransformer<ProductVariation>( transformations );
return new ModelRepository(
restListChild< ProductVariationRepositoryParams >( buildURL, ProductVariation, httpClient, transformer ),
restCreateChild< ProductVariationRepositoryParams >( buildURL, ProductVariation, httpClient, transformer ),
restReadChild< ProductVariationRepositoryParams >( buildChildURL, ProductVariation, httpClient, transformer ),
restUpdateChild< ProductVariationRepositoryParams >( buildChildURL, ProductVariation, httpClient, transformer ),
restDeleteChild< ProductVariationRepositoryParams >( buildDeleteURL, httpClient ),
);
}

View File

@ -13,6 +13,7 @@ import {
UpdateChildFn,
DeleteChildFn,
CreateFn,
CreateChildFn,
ModelTransformer,
IgnorePropertyTransformation,
// @ts-ignore
@ -138,6 +139,31 @@ export function restCreate< T extends ModelRepositoryParams >(
};
}
/**
* Creates a callback for creating child models using the REST API.
*
* @param {Function} buildURL A callback to build the URL. (This is passed the properties for the new model.)
* @param {Function} modelClass The model we're listing.
* @param {HTTPClient} httpClient The HTTP client to use for the request.
* @param {ModelTransformer} transformer The transformer to use for the response data.
* @return {CreateChildFn} The callback for the repository.
*/
export function restCreateChild< T extends ModelRepositoryParams >(
buildURL: ( parent: ParentID< T >, properties: Partial< ModelClass< T > > ) => string,
modelClass: ModelConstructor< ModelClass< T > >,
httpClient: HTTPClient,
transformer: ModelTransformer< ModelClass< T > >,
): CreateChildFn< T > {
return async ( parent, properties ) => {
const response = await httpClient.post(
buildURL( parent, properties ),
transformer.fromModel( properties ),
);
return Promise.resolve( transformer.toModel( modelClass, response.data ) );
};
}
/**
* Creates a callback for reading models using the REST API.
*

View File

@ -15,8 +15,98 @@
"name": "Simple product"
},
"variable": {
"name": "Variable Product with Three Variations"
"name": "Variable Product with Three Attributes",
"defaultAttributes": [
{
"id": 0,
"name": "Size",
"option": "Medium"
},
{
"id": 0,
"name": "Colour",
"option": "Blue"
}
],
"attributes": [
{
"id": 0,
"name": "Colour",
"isVisibleOnProductPage": true,
"isForVariations": true,
"options": [
"Red",
"Green",
"Blue"
],
"sortOrder": 0
},
{
"id": 0,
"name": "Size",
"isVisibleOnProductPage": true,
"isForVariations": true,
"options": [
"Small",
"Medium",
"Large"
],
"sortOrder": 0
},
{
"id": 0,
"name": "Logo",
"isVisibleOnProductPage": true,
"isForVariations": true,
"options": [
"Woo",
"WordPress"
],
"sortOrder": 0
}
]
},
"variations": [
{
"regularPrice": "19.99",
"attributes": [
{
"name": "Size",
"option": "Large"
},
{
"name": "Colour",
"option": "Red"
}
]
},
{
"regularPrice": "18.99",
"attributes": [
{
"name": "Size",
"option": "Medium"
},
{
"name": "Colour",
"option": "Green"
}
]
},
{
"regularPrice": "17.99",
"attributes": [
{
"name": "Size",
"option": "Small"
},
{
"name": "Colour",
"option": "Blue"
}
]
}
],
"grouped": {
"name": "Grouped Product with Three Children",
"groupedProducts": [

View File

@ -1,5 +1,12 @@
# Unreleased
## Added
- api package test for variable products and product variations
- api package test for grouped products
- api package test for external products
- api package test for coupons
# 0.1.1
## Added

View File

@ -57,7 +57,7 @@ const runGroupedProductAPITest = () => {
expect( product ).toEqual( expect.objectContaining( baseGroupedProduct ) );
});
it('can retrieve a raw external product', async () => {
it('can retrieve a raw grouped product', async () => {
let rawProperties = {
id: product.id,
grouped_products: baseGroupedProduct.groupedProducts,

View File

@ -0,0 +1,92 @@
/* eslint-disable jest/no-export, jest/no-disabled-tests */
/**
* Internal dependencies
*/
const { HTTPClientFactory, VariableProduct, ProductVariation } = require( '@woocommerce/api' );
/**
* External dependencies
*/
const config = require( 'config' );
const {
it,
describe,
beforeAll,
} = require( '@jest/globals' );
/**
* Create a variable product and retrieve via the API.
*/
const runVariableProductAPITest = () => {
describe('REST API > Variable Product', () => {
let client;
let defaultVariableProduct;
let defaultVariations;
let baseVariableProduct;
let product;
let variations = [];
let productRepository;
let variationRepository;
beforeAll(async () => {
defaultVariableProduct = config.get('products.variable');
defaultVariations = config.get('products.variations');
const admin = config.get('users.admin');
const url = config.get('url');
client = HTTPClientFactory.build(url)
.withBasicAuth(admin.username, admin.password)
.withIndexPermalinks()
.create();
});
it('can create a variable product', async () => {
productRepository = VariableProduct.restRepository(client);
// Check properties of product in the create product response.
product = await productRepository.create(defaultVariableProduct);
expect(product).toEqual(expect.objectContaining(defaultVariableProduct));
});
it('can add variations', async () => {
variationRepository = ProductVariation.restRepository(client);
for (let v = 0; v < defaultVariations.length; v++) {
const variation = await variationRepository.create(product.id, defaultVariations[v]);
// Test that variation id is a number.
expect(variation.id).toBeGreaterThan(0);
variations.push(variation.id);
}
baseVariableProduct = {
id: product.id,
...defaultVariableProduct,
variations,
};
});
it('can retrieve a transformed variable product', async () => {
// Read product via the repository.
const transformed = await productRepository.read(product.id);
expect(transformed).toEqual(expect.objectContaining(baseVariableProduct));
});
it('can retrieve transformed product variations', async () => {
// Read variations via the repository.
const transformed = await variationRepository.list(product.id);
expect(transformed).toHaveLength(defaultVariations.length);
});
it('can delete a variation', async () => {
const variationId = baseVariableProduct.variations.pop();
const status = variationRepository.delete(product.id, variationId);
expect(status).toBeTruthy();
});
it('can delete a variable product', async () => {
const status = productRepository.delete(product.id);
expect(status).toBeTruthy();
});
});
}
module.exports = runVariableProductAPITest;

View File

@ -36,6 +36,7 @@ const runMerchantOrdersCustomerPaymentPage = require( './merchant/wp-admin-order
const runExternalProductAPITest = require( './api/external-product.test' );
const runCouponApiTest = require( './api/coupon.test' );
const runGroupedProductAPITest = require( './api/grouped-product.test' );
const runVariableProductAPITest = require( './api/variable-product.test' );
const runSetupOnboardingTests = () => {
runActivationTest();
@ -73,6 +74,7 @@ const runMerchantTests = () => {
const runApiTests = () => {
runExternalProductAPITest();
runVariableProductAPITest();
runCouponApiTest();
}
@ -84,6 +86,7 @@ module.exports = {
runSetupOnboardingTests,
runExternalProductAPITest,
runGroupedProductAPITest,
runVariableProductAPITest,
runCouponApiTest,
runCartApplyCouponsTest,
runCartPageTest,

View File

@ -0,0 +1,6 @@
/*
* Internal dependencies
*/
const { runVariableProductAPITest } = require( '@woocommerce/e2e-core-tests' );
runVariableProductAPITest();