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
This commit is contained in:
Joshua T Flowers 2022-07-06 12:04:16 -04:00 committed by GitHub
parent 8d281241a9
commit 67d9ba3b19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 223 additions and 80 deletions

View File

@ -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,
};
};

View File

@ -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' );
} );
} );

View File

@ -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;
};

View File

@ -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';

View File

@ -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 >;