UNPKG

@shopgate/pwa-common-commerce

Version:

Commerce library for the Shopgate Connect PWA.

223 lines (213 loc) • 8.86 kB
import difference from 'lodash/difference'; import PipelineRequest from '@shopgate/pwa-core/classes/PipelineRequest'; import { generateResultHash, shouldFetchData } from '@shopgate/pwa-common/helpers/redux'; import { isNumber } from '@shopgate/pwa-common/helpers/validation'; import configuration from '@shopgate/pwa-common/collections/Configuration'; import { DEFAULT_PRODUCTS_FETCH_PARAMS } from '@shopgate/pwa-common/constants/Configuration'; import { SORT_SCOPE_CATEGORY } from '@shopgate/engage/filter/constants'; import { makeGetDefaultSortOrder } from '@shopgate/engage/filter/selectors'; import { getFulfillmentParams } from '@shopgate/pwa-common-commerce/product/selectors/product'; import { SHOPGATE_CATALOG_GET_PRODUCTS, SHOPGATE_CATALOG_GET_HIGHLIGHT_PRODUCTS, SHOPGATE_CATALOG_GET_LIVESHOPPING_PRODUCTS } from "../constants/Pipelines"; import buildRequestFilters from "../../filter/actions/helpers/buildRequestFilters"; import requestProducts from "../action-creators/requestProducts"; import receiveProducts from "../action-creators/receiveProducts"; import errorProducts from "../action-creators/errorProducts"; import deleteProductsByIds from "../action-creators/deleteProductsByIds"; import receiveProductsCached from "../action-creators/receiveProductsCached"; import { makeGetProductResultByCustomHash } from "../selectors/product"; /** * Checks if a product ID type is a product identifiers type. * @param {string} productIdType The product ID type to check. * @returns {boolean} */ export const isProductIdentifiersProductIdType = productIdType => ['sku', 'ean', 'upc'].includes(productIdType); /** * Process the pipeline params to be compatible. * Currently the categoryId field cannot be used in combination with the filter field. In order to * use them together the categoryId field has to be extracted into the filter field. * Then the pipeline is happy. * @param {Object} params The request params. * @param {Object} activeFilters The current active filters. * @param {boolean} includeSort Tells if the sort parameters shall be included to the request. * @param {boolean} includeFilters Tells if the filter parameters shall be included to the request. * @returns {Object} A set of compatible params. */ const processParams = (params, activeFilters, includeSort = true, includeFilters = true) => { const filters = buildRequestFilters(activeFilters); const newParams = { ...params, ...(includeFilters && filters && Object.keys(filters).length && { filters }) }; /** * Check if the sort parameter should be included in the request parameters, * and remove it if necessary. */ if (includeSort === false && params && params.sort) { delete newParams.sort; } if (newParams.productIdType) { if (isProductIdentifiersProductIdType(newParams.productIdType)) { if (newParams.productIdType === 'sku') { newParams.productSkus = newParams.productIds; } else if (newParams.productIdType === 'ean') { newParams.productEans = newParams.productIds; } else if (newParams.productIdType === 'upc') { newParams.productUpcs = newParams.productIds; } delete newParams.productIds; } } delete newParams.productIdType; return newParams; }; const getDefaultSortOrder = makeGetDefaultSortOrder(); /** * Retrieves a product from the Redux store. * @param {Object} options The options for the getProducts request. * @param {Object} options.params The params for the getProduct pipeline. * @param {string} [options.pipeline='getProducts'] The pipeline to call. * @param {Object} [options.filters = null] Filters object for the request * @param {boolean} [options.cached=true] If the result will be cached. * @param {?number} [options.cachedTime=null] Cache TTL in ms. * @param {?string} [options.id=null] A unique id for the component that is using this action. * @param {boolean} [options.includeSort=true] Tells if the sort parameters shall be included * into the product hash and the request. * @param {boolean} [options.includeFilters=true] Tells if the filter parameters shall be included * into the product hash and the request. * @param {Function} [options.onBeforeDispatch=() => {}] A callback which is fired, before new data * will be returned. * @param {boolean} [options.resolveCachedProducts=false] Whether to resolve with products even * when no actual request was done due to cached data. * @return {Function} A Redux Thunk */ function fetchProducts(options) { const { params = {}, filters = null, pipeline = SHOPGATE_CATALOG_GET_PRODUCTS, cached = true, cachedTime = null, id = null, includeSort = true, includeFilters = true, includeFulfillment = true, onBeforeDispatch = () => {}, resolveCachedProducts = false, calledByFetchProductsById = false } = options; return (dispatch, getState) => { const state = getState(); const { offset, limit, ...hashParams } = params; const defaultSort = getDefaultSortOrder(state, { scope: SORT_SCOPE_CATEGORY }); const { sort = defaultSort } = hashParams; let getProductsRequestParams; if (pipeline === SHOPGATE_CATALOG_GET_PRODUCTS) { getProductsRequestParams = configuration.get(DEFAULT_PRODUCTS_FETCH_PARAMS); } // Append additional global fulfillment parameters. let fulfillmentParams = {}; if (includeFulfillment && pipeline === SHOPGATE_CATALOG_GET_PRODUCTS) { fulfillmentParams = getFulfillmentParams(state); } // We need to process the params to handle edge cases in the pipeline params. const requestParams = { ...fulfillmentParams, ...getProductsRequestParams, ...processParams(params, filters, includeSort, includeFilters) }; const hash = generateResultHash({ pipeline, sort, ...fulfillmentParams, ...hashParams, ...(filters && Object.keys(filters).length && { filters }), ...(id && { id }) }, includeSort, includeFilters); const result = state.product.resultsByHash[hash]; let requiredProductCount = null; // Only set a required number of products if both offset and limit are valid. if (isNumber(offset) && isNumber(limit)) { requiredProductCount = offset + limit; } // Stop if we don't need to get any data. if (!shouldFetchData(result, 'products', requiredProductCount)) { const { products } = result; if (Array.isArray(products)) { /** * Fire the onBeforeDispatch callback to inform * a caller that fetchProducts will return data. */ onBeforeDispatch(); dispatch(receiveProductsCached({ hash, requestParams, products })); return Promise.resolve(resolveCachedProducts ? makeGetProductResultByCustomHash(hash)(state) : result); } return null; } // Fire the onBeforeDispatch callback to inform a caller that fetchProducts will return data. onBeforeDispatch(); dispatch(requestProducts({ hash, cached, cachedTime, requestParams })); const request = new PipelineRequest(pipeline).setInput(requestParams).dispatch(); request.then(response => { let totalResultCount = response.totalProductCount; /** * When the next check was written, getHighlightProducts and getLiveshoppingProducts * didn't deliver a totalProductCount within their responses - they simply returned all * available products. * So we set the products count of the response as totalProductCount to decrease the * amount of logic, which is necessary to deal with product related pipeline. */ if (typeof totalResultCount === 'undefined' && (pipeline === SHOPGATE_CATALOG_GET_HIGHLIGHT_PRODUCTS || pipeline === SHOPGATE_CATALOG_GET_LIVESHOPPING_PRODUCTS)) { totalResultCount = response.products.length; } dispatch(receiveProducts({ hash, requestParams, products: response.products, totalResultCount, cached, cachedTime })); if (!isProductIdentifiersProductIdType(params?.productIdType) && calledByFetchProductsById && Array.isArray(params?.productIds)) { const requestIds = params?.productIds; const responseIds = response.products.map(product => product.id); const missingResponseIds = difference(requestIds, responseIds); if (missingResponseIds.length > 0) { dispatch(deleteProductsByIds(missingResponseIds)); } } }).catch(error => { dispatch(errorProducts({ errorCode: error.code, hash, requestParams })); }); return request; }; } export default fetchProducts;