From 67d9ba3b19eac09c98aa4bed2c29231c4dd2fc1c Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Wed, 6 Jul 2022 12:04:16 -0400 Subject: [PATCH] Add CRUD data store typing (#33680) * Rename and export selectors for easier typing * Add CRUD action and selector type mapping * Add product attribute types * Separate getItem to provide correct return type * Rename config to type * Add generator return types --- packages/js/data/src/crud/selectors.ts | 155 +++++++++--------- packages/js/data/src/crud/test/selectors.ts | 6 +- packages/js/data/src/crud/types.ts | 94 ++++++++++- packages/js/data/src/index.ts | 4 + .../js/data/src/product-attributes/types.ts | 44 +++++ 5 files changed, 223 insertions(+), 80 deletions(-) create mode 100644 packages/js/data/src/product-attributes/types.ts diff --git a/packages/js/data/src/crud/selectors.ts b/packages/js/data/src/crud/selectors.ts index 52e3df05c9d..9e8d970a8bb 100644 --- a/packages/js/data/src/crud/selectors.ts +++ b/packages/js/data/src/crud/selectors.ts @@ -16,90 +16,93 @@ type SelectorOptions = { pluralResourceName: string; }; +export const getItemCreateError = ( + state: ResourceState, + query: ItemQuery +) => { + const itemQuery = getResourceName( CRUD_ACTIONS.CREATE_ITEM, query ); + return state.errors[ itemQuery ]; +}; + +export const getItemDeleteError = ( state: ResourceState, id: IdType ) => { + const itemQuery = getResourceName( CRUD_ACTIONS.DELETE_ITEM, { id } ); + return state.errors[ itemQuery ]; +}; + +export const getItem = ( state: ResourceState, id: IdType ) => { + return state.data[ id ]; +}; + +export const getItemError = ( state: ResourceState, id: IdType ) => { + const itemQuery = getResourceName( CRUD_ACTIONS.GET_ITEM, { id } ); + return state.errors[ itemQuery ]; +}; + +export const getItems = createSelector( + ( state: ResourceState, query: ItemQuery ) => { + const itemQuery = getResourceName( CRUD_ACTIONS.GET_ITEMS, query ); + + const ids = state.items[ itemQuery ] + ? state.items[ itemQuery ].data + : undefined; + + if ( ! ids ) { + return null; + } + + if ( query._fields ) { + return ids.map( ( id: IdType ) => { + return query._fields.reduce( + ( item: Partial< Item >, field: string ) => { + return { + ...item, + [ field ]: state.data[ id ][ field ], + }; + }, + {} as Partial< Item > + ); + } ); + } + + return ids.map( ( id: IdType ) => { + return state.data[ id ]; + } ); + }, + ( state, query ) => { + const itemQuery = getResourceName( CRUD_ACTIONS.GET_ITEMS, query ); + const ids = state.items[ itemQuery ] + ? state.items[ itemQuery ].data + : undefined; + return [ + state.items[ itemQuery ], + ...( ids || [] ).map( ( id: string ) => { + return state.data[ id ]; + } ), + ]; + } +); + +export const getItemsError = ( state: ResourceState, query: ItemQuery ) => { + const itemQuery = getResourceName( CRUD_ACTIONS.GET_ITEMS, query ); + return state.errors[ itemQuery ]; +}; + +export const getItemUpdateError = ( state: ResourceState, id: IdType ) => { + const itemQuery = getResourceName( CRUD_ACTIONS.UPDATE_ITEM, { id } ); + return state.errors[ itemQuery ]; +}; + export const createSelectors = ( { resourceName, pluralResourceName, }: SelectorOptions ) => { - const getCreateItemError = ( state: ResourceState, query: ItemQuery ) => { - const itemQuery = getResourceName( CRUD_ACTIONS.CREATE_ITEM, query ); - return state.errors[ itemQuery ]; - }; - - const getDeleteItemError = ( state: ResourceState, id: IdType ) => { - const itemQuery = getResourceName( CRUD_ACTIONS.DELETE_ITEM, { id } ); - return state.errors[ itemQuery ]; - }; - - const getItem = ( state: ResourceState, id: IdType ) => { - return state.data[ id ]; - }; - - const getItemError = ( state: ResourceState, id: IdType ) => { - const itemQuery = getResourceName( CRUD_ACTIONS.GET_ITEM, { id } ); - return state.errors[ itemQuery ]; - }; - - const getItems = createSelector( - ( state: ResourceState, query: ItemQuery ) => { - const itemQuery = getResourceName( CRUD_ACTIONS.GET_ITEMS, query ); - - const ids = state.items[ itemQuery ] - ? state.items[ itemQuery ].data - : undefined; - - if ( ! ids ) { - return null; - } - - if ( query._fields ) { - return ids.map( ( id: IdType ) => { - return query._fields.reduce( - ( item: Partial< Item >, field: string ) => { - return { - ...item, - [ field ]: state.data[ id ][ field ], - }; - }, - {} as Partial< Item > - ); - } ); - } - - return ids.map( ( id: IdType ) => { - return state.data[ id ]; - } ); - }, - ( state, query ) => { - const itemQuery = getResourceName( resourceName, query ); - const ids = state.items[ itemQuery ] - ? state.items[ itemQuery ].data - : undefined; - return [ - state.items[ itemQuery ], - ...( ids || [] ).map( ( id: string ) => { - return state.data[ id ]; - } ), - ]; - } - ); - - const getItemsError = ( state: ResourceState, query: ItemQuery ) => { - const itemQuery = getResourceName( CRUD_ACTIONS.GET_ITEMS, query ); - return state.errors[ itemQuery ]; - }; - - const getUpdateItemError = ( state: ResourceState, id: IdType ) => { - const itemQuery = getResourceName( CRUD_ACTIONS.UPDATE_ITEM, { id } ); - return state.errors[ itemQuery ]; - }; - return { [ `get${ resourceName }` ]: getItem, [ `get${ resourceName }Error` ]: getItemError, [ `get${ pluralResourceName }` ]: getItems, [ `get${ pluralResourceName }Error` ]: getItemsError, - [ `getCreate${ resourceName }Error` ]: getCreateItemError, - [ `getDelete${ resourceName }Error` ]: getDeleteItemError, - [ `getUpdate${ resourceName }Error` ]: getUpdateItemError, + [ `get${ resourceName }CreateError` ]: getItemCreateError, + [ `get${ resourceName }DeleteError` ]: getItemDeleteError, + [ `get${ resourceName }UpdateError` ]: getItemUpdateError, }; }; diff --git a/packages/js/data/src/crud/test/selectors.ts b/packages/js/data/src/crud/test/selectors.ts index 1c6ad4a1324..99116eb2a98 100644 --- a/packages/js/data/src/crud/test/selectors.ts +++ b/packages/js/data/src/crud/test/selectors.ts @@ -15,8 +15,8 @@ describe( 'crud selectors', () => { expect( selectors ).toHaveProperty( 'getProducts' ); expect( selectors ).toHaveProperty( 'getProductError' ); expect( selectors ).toHaveProperty( 'getProductsError' ); - expect( selectors ).toHaveProperty( 'getCreateProductError' ); - expect( selectors ).toHaveProperty( 'getDeleteProductError' ); - expect( selectors ).toHaveProperty( 'getUpdateProductError' ); + expect( selectors ).toHaveProperty( 'getProductCreateError' ); + expect( selectors ).toHaveProperty( 'getProductDeleteError' ); + expect( selectors ).toHaveProperty( 'getProductUpdateError' ); } ); } ); diff --git a/packages/js/data/src/crud/types.ts b/packages/js/data/src/crud/types.ts index 5e8eb0577c9..744df392343 100644 --- a/packages/js/data/src/crud/types.ts +++ b/packages/js/data/src/crud/types.ts @@ -1,7 +1,16 @@ /** * Internal dependencies */ -import { BaseQueryParams } from '../types'; +import { BaseQueryParams, WPDataSelector, WPDataSelectors } from '../types'; +import { + getItem, + getItemError, + getItems, + getItemsError, + getItemCreateError, + getItemDeleteError, + getItemUpdateError, +} from './selectors'; export type IdType = number | string; @@ -13,3 +22,86 @@ export type Item = { export type ItemQuery = BaseQueryParams & { [ key: string ]: unknown; }; + +export type CrudActions< ResourceName, ItemType, MutableProperties > = + MapActions< + { + create: ( query: ItemQuery ) => Item; + update: ( query: ItemQuery ) => Item; + }, + ResourceName, + MutableProperties, + Generator< unknown, ItemType > + > & + MapActions< + { + delete: ( id: IdType ) => Item; + }, + ResourceName, + IdType, + Generator< unknown, ItemType > + >; + +export type CrudSelectors< + ResourceName, + PluralResourceName, + ItemType, + ItemQueryType, + MutableProperties +> = MapSelectors< + { + '': WPDataSelector< typeof getItem >; + }, + ResourceName, + IdType, + ItemType +> & + MapSelectors< + { + Error: WPDataSelector< typeof getItemError >; + DeleteError: WPDataSelector< typeof getItemDeleteError >; + UpdateError: WPDataSelector< typeof getItemUpdateError >; + }, + ResourceName, + IdType, + unknown + > & + MapSelectors< + { + '': WPDataSelector< typeof getItems >; + }, + PluralResourceName, + ItemQueryType, + ItemType[] + > & + MapSelectors< + { + Error: WPDataSelector< typeof getItemsError >; + }, + PluralResourceName, + ItemQueryType, + unknown + > & + MapSelectors< + { + CreateError: WPDataSelector< typeof getItemCreateError >; + }, + PluralResourceName, + MutableProperties, + unknown + > & + WPDataSelectors; + +export type MapSelectors< Type, ResourceName, ParamType, ReturnType > = { + [ Property in keyof Type as `get${ Capitalize< + string & ResourceName + > }${ Capitalize< string & Property > }` ]: ( x: ParamType ) => ReturnType; +}; + +export type MapActions< Type, ResourceName, ParamType, ReturnType > = { + [ Property in keyof Type as `${ Lowercase< + string & Property + > }${ Capitalize< string & ResourceName > }` ]: ( + x: ParamType + ) => ReturnType; +}; diff --git a/packages/js/data/src/index.ts b/packages/js/data/src/index.ts index 7ae97513cfe..f1a36009485 100644 --- a/packages/js/data/src/index.ts +++ b/packages/js/data/src/index.ts @@ -119,6 +119,7 @@ import { OnboardingSelectors } from './onboarding/selectors'; import { OptionsSelectors } from './options/types'; import { ProductsSelectors } from './products/selectors'; import { OrdersSelectors } from './orders/selectors'; +import { ProductAttributeSelectors } from './product-attributes/types'; // As we add types to all the package selectors we can fill out these unknown types with real ones. See one // of the already typed selectors for an example of how you can do this. @@ -148,6 +149,8 @@ export type WCSelectorType< T > = T extends typeof REVIEWS_STORE_NAME ? WPDataSelectors : T extends typeof PRODUCTS_STORE_NAME ? ProductsSelectors + : T extends typeof PRODUCT_ATTRIBUTES_STORE_NAME + ? ProductAttributeSelectors : T extends typeof ORDERS_STORE_NAME ? OrdersSelectors : never; @@ -158,4 +161,5 @@ export interface WCDataSelector { // Other exports export { ActionDispatchers as PluginsStoreActions } from './plugins/actions'; +export { ActionDispatchers as ProductAttributesActions } from './product-attributes/types'; export { ActionDispatchers as ProductsStoreActions } from './products/actions'; diff --git a/packages/js/data/src/product-attributes/types.ts b/packages/js/data/src/product-attributes/types.ts new file mode 100644 index 00000000000..11f38ddad18 --- /dev/null +++ b/packages/js/data/src/product-attributes/types.ts @@ -0,0 +1,44 @@ +/** + * External dependencies + */ +import { DispatchFromMap } from '@automattic/data-stores'; + +/** + * Internal dependencies + */ +import { CrudActions, CrudSelectors } from '../crud/types'; + +type ProductAttribute = { + id: number; + slug: string; + name: string; + type: string; + order_by: string; + has_archives: boolean; +}; + +type Query = { + context?: string; +}; + +type ReadOnlyProperties = 'id'; + +type MutableProperties = Partial< + Omit< ProductAttribute, ReadOnlyProperties > +>; + +type ProductAttributeActions = CrudActions< + 'ProductAttribute', + ProductAttribute, + MutableProperties +>; + +export type ProductAttributeSelectors = CrudSelectors< + 'ProductAttribute', + 'ProductAttributes', + ProductAttribute, + Query, + MutableProperties +>; + +export type ActionDispatchers = DispatchFromMap< ProductAttributeActions >;