Merge pull request #32964 from woocommerce/update/32524_order_product_count_api_requests

Add products data store
This commit is contained in:
louwie17 2022-05-25 17:03:21 -03:00 committed by GitHub
commit 434fab9ac5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 831 additions and 62 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: enhancement
Add product data store for retrieving product list.

View File

@ -2,10 +2,12 @@
* External dependencies
*/
import { controls as dataControls } from '@wordpress/data-controls';
import { Action } from '@wordpress/data';
import apiFetch, { APIFetchOptions } from '@wordpress/api-fetch';
import { AnyAction } from 'redux';
export const fetchWithHeaders = ( options: APIFetchOptions ) => {
export const fetchWithHeaders = (
options: APIFetchOptions
): AnyAction & { options: APIFetchOptions } => {
return {
type: 'FETCH_WITH_HEADERS',
options,
@ -20,7 +22,7 @@ export type FetchWithHeadersResponse< Data > = {
const controls = {
...dataControls,
FETCH_WITH_HEADERS( action: Action ) {
FETCH_WITH_HEADERS( action: AnyAction ) {
return apiFetch< Response >( { ...action.options, parse: false } )
.then( ( response ) => {
return Promise.all( [

View File

@ -16,6 +16,7 @@ export { NAVIGATION_STORE_NAME } from './navigation';
export { OPTIONS_STORE_NAME } from './options';
export { ITEMS_STORE_NAME } from './items';
export { PAYMENT_GATEWAYS_STORE_NAME } from './payment-gateways';
export { PRODUCTS_STORE_NAME } from './products';
export { PaymentGateway } from './payment-gateways/types';
// Export hooks
@ -67,6 +68,7 @@ export * from './types';
export * from './countries/types';
export * from './onboarding/types';
export * from './plugins/types';
export * from './products/types';
/**
* Internal dependencies
@ -83,6 +85,7 @@ import type { REPORTS_STORE_NAME } from './reports';
import type { ITEMS_STORE_NAME } from './items';
import type { COUNTRIES_STORE_NAME } from './countries';
import type { PAYMENT_GATEWAYS_STORE_NAME } from './payment-gateways';
import type { PRODUCTS_STORE_NAME } from './products';
export type WCDataStoreName =
| typeof REVIEWS_STORE_NAME
@ -96,7 +99,8 @@ export type WCDataStoreName =
| typeof REPORTS_STORE_NAME
| typeof ITEMS_STORE_NAME
| typeof COUNTRIES_STORE_NAME
| typeof PAYMENT_GATEWAYS_STORE_NAME;
| typeof PAYMENT_GATEWAYS_STORE_NAME
| typeof PRODUCTS_STORE_NAME;
/**
* Internal dependencies
@ -106,6 +110,7 @@ import { PaymentSelectors } from './payment-gateways/selectors';
import { PluginSelectors } from './plugins/selectors';
import { OnboardingSelectors } from './onboarding/selectors';
import { OptionsSelectors } from './options/types';
import { ProductsSelectors } from './products/selectors';
// 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.
@ -133,6 +138,8 @@ export type WCSelectorType< T > = T extends typeof REVIEWS_STORE_NAME
? WPDataSelectors
: T extends typeof COUNTRIES_STORE_NAME
? WPDataSelectors
: T extends typeof PRODUCTS_STORE_NAME
? ProductsSelectors
: never;
export interface WCDataSelector {

View File

@ -1,38 +1,18 @@
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import { NAMESPACE } from '../constants';
import { setError, setItems, setItemsTotalCount } from './actions';
import { fetchWithHeaders } from '../controls';
function* request( itemType, query ) {
const endpoint =
itemType === 'categories' ? 'products/categories' : itemType;
const url = addQueryArgs( `${ NAMESPACE }/${ endpoint }`, query );
const isUnboundedRequest = query.per_page === -1;
const fetch = isUnboundedRequest ? apiFetch : fetchWithHeaders;
const response = yield fetch( {
path: url,
method: 'GET',
} );
if ( isUnboundedRequest ) {
return { items: response, totalCount: response.length };
}
const totalCount = parseInt( response.headers.get( 'x-wp-total' ), 10 );
return { items: response.data, totalCount };
}
import { request } from '../utils';
export function* getItems( itemType, query ) {
try {
const { items, totalCount } = yield request( itemType, query );
const endpoint =
itemType === 'categories' ? 'products/categories' : itemType;
const { items, totalCount } = yield request(
`${ NAMESPACE }/${ endpoint }`,
query
);
yield setItemsTotalCount( itemType, query, totalCount );
yield setItems( itemType, query, items );
} catch ( error ) {
@ -51,7 +31,12 @@ export function* getItemsTotalCount( itemType, query ) {
page: 1,
per_page: 1,
};
const { totalCount } = yield request( itemType, totalsQuery );
const endpoint =
itemType === 'categories' ? 'products/categories' : itemType;
const { totalCount } = yield request(
`${ NAMESPACE }/${ endpoint }`,
totalsQuery
);
yield setItemsTotalCount( itemType, query, totalCount );
} catch ( error ) {
yield setError( itemType, query, error );

View File

@ -12,7 +12,7 @@ import {
OnboardingState,
ExtensionList,
ProfileItemsState,
Product,
OnboardingProductType,
} from './types';
import { WPDataSelectors } from '../types';
import { Plugin } from '../plugins/types';
@ -92,7 +92,9 @@ export const getEmailPrefill = ( state: OnboardingState ): string => {
return state.emailPrefill || '';
};
export const getProductTypes = ( state: OnboardingState ): Product[] => {
export const getProductTypes = (
state: OnboardingState
): OnboardingProductType[] => {
return state.productTypes || [];
};

View File

@ -63,7 +63,7 @@ export type OnboardingState = {
profileItems: ProfileItemsState;
taskLists: Record< string, TaskListType >;
paymentMethods: Plugin[];
productTypes: Product[];
productTypes: OnboardingProductType[];
emailPrefill: string;
// TODO clarify what the error record's type is
errors: Record< string, unknown >;
@ -137,7 +137,7 @@ export type MethodFields = {
value?: string;
};
export type Product = {
export type OnboardingProductType = {
default?: boolean;
label: string;
product?: number;

View File

@ -0,0 +1,10 @@
export enum TYPES {
GET_PRODUCT_SUCCESS = 'GET_PRODUCT_SUCCESS',
GET_PRODUCT_ERROR = 'GET_PRODUCT_ERROR',
GET_PRODUCTS_SUCCESS = 'GET_PRODUCTS_SUCCESS',
GET_PRODUCTS_ERROR = 'GET_PRODUCTS_ERROR',
GET_PRODUCTS_TOTAL_COUNT_SUCCESS = 'GET_PRODUCTS_TOTAL_COUNT_SUCCESS',
GET_PRODUCTS_TOTAL_COUNT_ERROR = 'GET_PRODUCTS_TOTAL_COUNT_ERROR',
}
export default TYPES;

View File

@ -0,0 +1,79 @@
/**
* Internal dependencies
*/
import TYPES from './action-types';
import { PartialProduct, ProductQuery } from './types';
export function getProductSuccess( id: number, product: PartialProduct ) {
return {
type: TYPES.GET_PRODUCT_SUCCESS as const,
id,
product,
};
}
export function getProductError(
query: Partial< ProductQuery >,
error: unknown
) {
return {
type: TYPES.GET_PRODUCT_ERROR as const,
query,
error,
};
}
export function getProductsSuccess(
query: Partial< ProductQuery >,
products: PartialProduct[],
totalCount: number
) {
return {
type: TYPES.GET_PRODUCTS_SUCCESS as const,
products,
query,
totalCount,
};
}
export function getProductsError(
query: Partial< ProductQuery >,
error: unknown
) {
return {
type: TYPES.GET_PRODUCTS_ERROR as const,
query,
error,
};
}
export function getProductsTotalCountSuccess(
query: Partial< ProductQuery >,
totalCount: number
) {
return {
type: TYPES.GET_PRODUCTS_TOTAL_COUNT_SUCCESS as const,
query,
totalCount,
};
}
export function getProductsTotalCountError(
query: Partial< ProductQuery >,
error: unknown
) {
return {
type: TYPES.GET_PRODUCTS_TOTAL_COUNT_ERROR as const,
query,
error,
};
}
export type Actions = ReturnType<
| typeof getProductSuccess
| typeof getProductError
| typeof getProductsSuccess
| typeof getProductsError
| typeof getProductsTotalCountSuccess
| typeof getProductsTotalCountError
>;

View File

@ -0,0 +1,3 @@
export const STORE_NAME = 'wc/admin/products';
export const WC_PRODUCT_NAMESPACE = '/wc/v3/products';

View File

@ -0,0 +1,26 @@
/**
* External dependencies
*/
import { registerStore } from '@wordpress/data';
import { Reducer } from 'redux';
/**
* Internal dependencies
*/
import { STORE_NAME } from './constants';
import * as selectors from './selectors';
import * as actions from './actions';
import * as resolvers from './resolvers';
import reducer, { ProductState, State } from './reducer';
import controls from '../controls';
registerStore< State >( STORE_NAME, {
reducer: reducer as Reducer< ProductState >,
actions,
controls,
selectors,
resolvers,
} );
export const PRODUCTS_STORE_NAME = STORE_NAME;

View File

@ -0,0 +1,108 @@
/**
* External dependencies
*/
import { Reducer } from 'redux';
/**
* Internal dependencies
*/
import TYPES from './action-types';
import { Actions } from './actions';
import { PartialProduct } from './types';
import {
getProductResourceName,
getTotalProductCountResourceName,
} from './utils';
export type ProductState = {
products: Record<
string,
{
data: number[];
}
>;
productsCount: Record< string, number >;
errors: Record< string, unknown >;
data: Record< number, PartialProduct >;
};
const reducer: Reducer< ProductState, Actions > = (
state = {
products: {},
productsCount: {},
errors: {},
data: {},
},
payload
) => {
if ( payload && 'type' in payload ) {
switch ( payload.type ) {
case TYPES.GET_PRODUCT_SUCCESS:
const productData = state.data || {};
return {
...state,
data: {
...productData,
[ payload.id ]: {
...( productData[ payload.id ] || {} ),
...payload.product,
},
},
};
case TYPES.GET_PRODUCTS_SUCCESS:
const ids: number[] = [];
const nextProducts = payload.products.reduce<
Record< number, PartialProduct >
>( ( result, product ) => {
ids.push( product.id );
result[ product.id ] = {
...( state.data[ product.id ] || {} ),
...product,
};
return result;
}, {} );
const resourceName = getProductResourceName( payload.query );
return {
...state,
products: {
...state.products,
[ resourceName ]: { data: ids },
},
data: {
...state.data,
...nextProducts,
},
};
case TYPES.GET_PRODUCTS_TOTAL_COUNT_SUCCESS:
const totalResourceName = getTotalProductCountResourceName(
payload.query
);
return {
...state,
productsCount: {
...state.productsCount,
[ totalResourceName ]: payload.totalCount,
},
};
case TYPES.GET_PRODUCT_ERROR:
case TYPES.GET_PRODUCTS_ERROR:
case TYPES.GET_PRODUCTS_TOTAL_COUNT_ERROR:
return {
...state,
errors: {
...state.errors,
[ getProductResourceName(
payload.query
) ]: payload.error,
},
};
default:
return state;
}
}
return state;
};
export type State = ReturnType< typeof reducer >;
export default reducer;

View File

@ -0,0 +1,60 @@
/**
* Internal dependencies
*/
import { WC_PRODUCT_NAMESPACE } from './constants';
import { Product, ProductQuery } from './types';
import {
getProductsError,
getProductsSuccess,
getProductsTotalCountError,
getProductsTotalCountSuccess,
} from './actions';
import { request } from '../utils';
export function* getProducts( query: Partial< ProductQuery > ) {
// id is always required.
const productsQuery = {
...query,
};
if (
productsQuery &&
productsQuery._fields &&
! productsQuery._fields.includes( 'id' )
) {
productsQuery._fields = [ 'id', ...productsQuery._fields ];
}
try {
const {
items,
totalCount,
}: { items: Product[]; totalCount: number } = yield request<
ProductQuery,
Product
>( WC_PRODUCT_NAMESPACE, productsQuery );
yield getProductsTotalCountSuccess( query, totalCount );
yield getProductsSuccess( query, items, totalCount );
return items;
} catch ( error ) {
yield getProductsError( query, error );
throw error;
}
}
export function* getProductsTotalCount( query: Partial< ProductQuery > ) {
try {
const totalsQuery = {
...query,
page: 1,
per_page: 1,
};
const { totalCount } = yield request< ProductQuery, Product >(
WC_PRODUCT_NAMESPACE,
totalsQuery
);
yield getProductsTotalCountSuccess( query, totalCount );
return totalCount;
} catch ( error ) {
yield getProductsTotalCountError( query, error );
throw error;
}
}

View File

@ -0,0 +1,84 @@
/**
* External dependencies
*/
import createSelector from 'rememo';
/**
* Internal dependencies
*/
import {
getProductResourceName,
getTotalProductCountResourceName,
} from './utils';
import { WPDataSelector, WPDataSelectors } from '../types';
import { ProductState } from './reducer';
import { PartialProduct, ProductQuery } from './types';
export const getProducts = createSelector(
( state: ProductState, query: ProductQuery, defaultValue = undefined ) => {
const resourceName = getProductResourceName( query );
const ids = state.products[ resourceName ]
? state.products[ resourceName ].data
: undefined;
if ( ! ids ) {
return defaultValue;
}
if ( query._fields ) {
return ids.map( ( id ) => {
return query._fields.reduce(
(
product: PartialProduct,
field: keyof PartialProduct
) => {
return {
...product,
[ field ]: state.data[ id ][ field ],
};
},
{} as PartialProduct
);
} );
}
return ids.map( ( id ) => {
return state.data[ id ];
} );
},
( state, query ) => {
const resourceName = getProductResourceName( query );
const ids = state.products[ resourceName ]
? state.products[ resourceName ].data
: undefined;
return [
state.products[ resourceName ],
...( ids || [] ).map( ( id: number ) => {
return state.data[ id ];
} ),
];
}
);
export const getProductsTotalCount = (
state: ProductState,
query: ProductQuery,
defaultValue = undefined
) => {
const resourceName = getTotalProductCountResourceName( query );
const totalCount = state.productsCount.hasOwnProperty( resourceName )
? state.productsCount[ resourceName ]
: defaultValue;
return totalCount;
};
export const getProductsError = (
state: ProductState,
query: ProductQuery
) => {
const resourceName = getProductResourceName( query );
return state.errors[ resourceName ];
};
export type ProductsSelectors = {
getProducts: WPDataSelector< typeof getProducts >;
getProductsTotalCount: WPDataSelector< typeof getProductsTotalCount >;
getProductsError: WPDataSelector< typeof getProductsError >;
} & WPDataSelectors;

View File

@ -0,0 +1,183 @@
/**
* Internal dependencies
*/
import reducer, { ProductState } from '../reducer';
import TYPES from '../action-types';
import {
getProductResourceName,
getTotalProductCountResourceName,
} from '../utils';
import { Actions } from '../actions';
import { PartialProduct, ProductQuery } from '../types';
const defaultState: ProductState = {
products: {},
productsCount: {},
errors: {},
data: {},
};
describe( 'products reducer', () => {
it( 'should return a default state', () => {
const state = reducer( undefined, {} as Actions );
expect( state ).toEqual( defaultState );
expect( state ).not.toBe( defaultState );
} );
it( 'should handle GET_PRODUCT_SUCCESS', () => {
const itemType = 'guyisms';
const initialState: ProductState = {
products: {
[ itemType ]: {
data: [ 1, 2 ],
},
},
productsCount: {
'total-guyisms:{}': 2,
},
errors: {},
data: {
1: { id: 1, name: 'Donkey', status: 'draft' },
2: { id: 2, name: 'Sauce', status: 'publish' },
},
};
const update: PartialProduct = {
id: 2,
status: 'draft',
};
const state = reducer( initialState, {
type: TYPES.GET_PRODUCT_SUCCESS,
id: update.id,
product: update,
} );
expect( state.products ).toEqual( initialState.products );
expect( state.errors ).toEqual( initialState.errors );
expect( state.data[ 1 ] ).toEqual( initialState.data[ 1 ] );
expect( state.data[ 2 ].id ).toEqual( initialState.data[ 2 ].id );
expect( state.data[ 2 ].title ).toEqual( initialState.data[ 2 ].title );
expect( state.data[ 2 ].status ).toEqual( update.status );
} );
it( 'should handle GET_PRODUCTS_SUCCESS', () => {
const products: PartialProduct[] = [
{ id: 1, name: 'Yum!' },
{ id: 2, name: 'Dynamite!' },
];
const totalCount = 45;
const query: Partial< ProductQuery > = { status: 'draft' };
const state = reducer( defaultState, {
type: TYPES.GET_PRODUCTS_SUCCESS,
products,
query,
totalCount,
} );
const resourceName = getProductResourceName( query );
expect( state.products[ resourceName ].data ).toHaveLength( 2 );
expect(
state.products[ resourceName ].data.includes( 1 )
).toBeTruthy();
expect(
state.products[ resourceName ].data.includes( 2 )
).toBeTruthy();
expect( state.data[ 1 ] ).toEqual( products[ 0 ] );
expect( state.data[ 2 ] ).toEqual( products[ 1 ] );
} );
it( 'GET_PRODUCTS_SUCCESS should not remove previously added fields, only update new ones', () => {
const initialState: ProductState = {
...defaultState,
data: {
1: { id: 1, name: 'Yum!', price: '10.00', description: 'test' },
2: {
id: 2,
name: 'Dynamite!',
price: '10.00',
description: 'dynamite',
},
},
};
const products: PartialProduct[] = [
{ id: 1, name: 'Yum! Updated' },
{ id: 2, name: 'Dynamite!' },
];
const totalCount = 45;
const query: Partial< ProductQuery > = { status: 'draft' };
const state = reducer( initialState, {
type: TYPES.GET_PRODUCTS_SUCCESS,
products,
query,
totalCount,
} );
const resourceName = getProductResourceName( query );
expect( state.products[ resourceName ].data ).toHaveLength( 2 );
expect(
state.products[ resourceName ].data.includes( 1 )
).toBeTruthy();
expect(
state.products[ resourceName ].data.includes( 2 )
).toBeTruthy();
expect( state.data[ 1 ].name ).toEqual( products[ 0 ].name );
expect( state.data[ 1 ].price ).toEqual( initialState.data[ 1 ].price );
expect( state.data[ 1 ].description ).toEqual(
initialState.data[ 1 ].description
);
expect( state.data[ 2 ] ).toEqual( initialState.data[ 2 ] );
} );
it( 'should handle GET_PRODUCTS_TOTAL_COUNT_SUCCESS', () => {
const initialQuery: Partial< ProductQuery > = {
status: 'publish',
page: 1,
per_page: 1,
_fields: [ 'id' ],
};
const resourceName = getTotalProductCountResourceName( initialQuery );
const initialState: ProductState = {
...defaultState,
productsCount: {
[ resourceName ]: 1,
},
};
// Additional coverage for getTotalCountResourceName().
const similarQueryForTotals: Partial< ProductQuery > = {
status: 'publish',
page: 2,
per_page: 10,
_fields: [ 'id', 'title', 'status' ],
};
const state = reducer( initialState, {
type: TYPES.GET_PRODUCTS_TOTAL_COUNT_SUCCESS,
query: similarQueryForTotals,
totalCount: 2,
} );
expect( state.productsCount ).toEqual( {
[ resourceName ]: 2,
} );
} );
it( 'should handle GET_PRODUCTS_ERROR', () => {
const query: Partial< ProductQuery > = { status: 'draft' };
const resourceName = getProductResourceName( query );
const error = 'Baaam!';
const state = reducer( defaultState, {
type: TYPES.GET_PRODUCTS_ERROR,
query,
error,
} );
expect( state.errors[ resourceName ] ).toBe( error );
} );
} );

View File

@ -0,0 +1,47 @@
/**
* Internal dependencies
*/
import { ProductQuery } from '../types';
import { getTotalProductCountResourceName } from '../utils';
describe( 'getTotalProductCountResourceName()', () => {
it( "Ignores query params that don't affect total counts", () => {
const fullQuery: Partial< ProductQuery > = {
page: 2,
per_page: 10,
_fields: [ 'id', 'title', 'status', 'price' ],
status: 'publish',
};
const slimQuery: Partial< ProductQuery > = {
page: 1,
per_page: 1,
_fields: [ 'id' ],
status: 'publish',
};
expect( getTotalProductCountResourceName( fullQuery ) ).toEqual(
getTotalProductCountResourceName( slimQuery )
);
} );
it( 'Accounts for query params that do affect total counts', () => {
const firstQuery: Partial< ProductQuery > = {
page: 2,
per_page: 10,
_fields: [ 'id', 'title', 'status', 'price' ],
status: 'publish',
};
const secondQuery: Partial< ProductQuery > = {
page: 1,
per_page: 1,
_fields: [ 'id' ],
status: 'draft',
};
expect( getTotalProductCountResourceName( firstQuery ) ).not.toEqual(
getTotalProductCountResourceName( secondQuery )
);
} );
} );

View File

@ -0,0 +1,73 @@
/**
* External dependencies
*/
import { Schema } from '@wordpress/core-data';
/**
* Internal dependencies
*/
import { BaseQueryParams } from '../types';
export type ProductType = 'simple' | 'grouped' | 'external' | 'variable';
export type ProductStatus =
| 'draft'
| 'pending'
| 'private'
| 'publish'
| 'any'
| 'future';
export type Product<
Status = ProductStatus,
Type = ProductType
> = Schema.Post & {
id: number;
name: string;
slug: string;
permalink: string;
date_created: string;
date_created_gmt: string;
date_modified: string;
date_modified_gmt: string;
type: Type;
status: Status;
featured: boolean;
description: string;
short_description: string;
sku: string;
price: string;
regular_price: string;
sale_price: string;
};
export type PartialProduct = Partial< Product > & Pick< Product, 'id' >;
export type ProductQuery<
Status = ProductStatus,
Type = ProductType
> = BaseQueryParams< keyof Product > & {
orderby:
| 'date'
| 'id'
| 'include'
| 'title'
| 'slug'
| 'price'
| 'popularity'
| 'rating';
slug: string;
status: Status;
type: Type;
sku: string;
featured: boolean;
category: string;
tag: string;
shipping_class: string;
attribute: string;
attribute_term: string;
tax_class: 'standard' | 'reduced-rate' | 'zero-rate';
on_sale: boolean;
min_price: string;
max_price: string;
stock_status: 'instock' | 'outofstock';
};

View File

@ -0,0 +1,37 @@
/**
* Internal dependencies
*/
import { getResourceName } from '../utils';
import { ProductQuery } from './types';
const PRODUCT_PREFIX = 'product';
/**
* Generate a resource name for products.
*
* @param {Object} query Query for products.
* @return {string} Resource name for products.
*/
export function getProductResourceName( query: Partial< ProductQuery > ) {
return getResourceName( PRODUCT_PREFIX, query );
}
/**
* Generate a resource name for product totals count.
*
* It omits query parameters from the identifier that don't affect
* totals values like pagination and response field filtering.
*
* @param {Object} query Query for product totals count.
* @return {string} Resource name for product totals.
*/
export function getTotalProductCountResourceName(
query: Partial< ProductQuery >
) {
// Disable eslint rule because we're using this spread to omit properties
// that don't affect item totals count results.
// eslint-disable-next-line no-unused-vars, camelcase
const { _fields, page, per_page, ...totalsQuery } = query;
return getProductResourceName( totalsQuery );
}

View File

@ -1,3 +1,4 @@
export * from './wp-data';
export * from './rule-processor';
export * from './api';
export * from './query-params';

View File

@ -0,0 +1,16 @@
export type BaseQueryParams< Fields = string > = {
context: string;
page: number;
per_page: number;
search: string;
after: string;
before: string;
exclude: string;
include: string;
offset: number;
order: 'asc' | 'desc';
orderby: 'date' | 'id' | 'include' | 'title' | 'slug';
parent: number[];
parent_exclude: number[];
_fields: Fields[];
};

View File

@ -1,26 +0,0 @@
export function getResourceName( prefix, identifier ) {
const identifierString = JSON.stringify(
identifier,
Object.keys( identifier ).sort()
);
return `${ prefix }:${ identifierString }`;
}
export function getResourcePrefix( resourceName ) {
const hasPrefixIndex = resourceName.indexOf( ':' );
return hasPrefixIndex < 0
? resourceName
: resourceName.substring( 0, hasPrefixIndex );
}
export function isResourcePrefix( resourceName, prefix ) {
const resourcePrefix = getResourcePrefix( resourceName );
return resourcePrefix === prefix;
}
export function getResourceIdentifier( resourceName ) {
const identifierString = resourceName.substring(
resourceName.indexOf( ':' ) + 1
);
return JSON.parse( identifierString );
}

View File

@ -0,0 +1,68 @@
/**
* External dependencies
*/
import { addQueryArgs } from '@wordpress/url';
import { apiFetch } from '@wordpress/data-controls';
/**
* Internal dependencies
*/
import { BaseQueryParams } from './types/query-params';
import { fetchWithHeaders } from './controls';
export function getResourceName(
prefix: string,
identifier: Record< string, unknown >
) {
const identifierString = JSON.stringify(
identifier,
Object.keys( identifier ).sort()
);
return `${ prefix }:${ identifierString }`;
}
export function getResourcePrefix( resourceName: string ) {
const hasPrefixIndex = resourceName.indexOf( ':' );
return hasPrefixIndex < 0
? resourceName
: resourceName.substring( 0, hasPrefixIndex );
}
export function isResourcePrefix( resourceName: string, prefix: string ) {
const resourcePrefix = getResourcePrefix( resourceName );
return resourcePrefix === prefix;
}
export function getResourceIdentifier( resourceName: string ) {
const identifierString = resourceName.substring(
resourceName.indexOf( ':' ) + 1
);
return JSON.parse( identifierString );
}
export function* request< Query extends BaseQueryParams, DataType >(
namespace: string,
query: Partial< Query >
) {
const url: string = addQueryArgs( namespace, query );
const isUnboundedRequest = query.per_page === -1;
const fetch = isUnboundedRequest ? apiFetch : fetchWithHeaders;
const response:
| DataType[]
| ( { data: DataType[] } & Response ) = yield fetch( {
path: url,
method: 'GET',
} );
if ( isUnboundedRequest && ! ( 'data' in response ) ) {
return { items: response, totalCount: response.length };
}
if ( ! isUnboundedRequest && 'data' in response ) {
const totalCount = parseInt(
response.headers.get( 'x-wp-total' ) || '',
10
);
return { items: response.data, totalCount };
}
}