Skip to content

Commit

Permalink
feat(commerce): create very basic search controller, actions and slice (
Browse files Browse the repository at this point in the history
#3400)

* create very basic search controller

* simplify mock builder definition

* apply review suggestions

* normalize type name
  • Loading branch information
Spuffynism authored Nov 17, 2023
1 parent b5e479b commit 2432693
Show file tree
Hide file tree
Showing 32 changed files with 723 additions and 226 deletions.
187 changes: 108 additions & 79 deletions packages/headless/src/api/commerce/commerce-api-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import {SortBy} from '../../features/sort/sort';
import {buildMockCommerceAPIClient} from '../../test/mock-commerce-api-client';
import {PlatformClient} from '../platform-client';
import {CommerceAPIClient} from './commerce-api-client';
import {ProductListingV2Request} from './product-listings/v2/product-listing-v2-request';
import {ProductListingV2} from './product-listings/v2/product-listing-v2-response';
import {CommerceAPIRequest} from './common/request';
import {CommerceResponse} from './common/response';

describe('commerce api client', () => {
const platformUrl = 'https://platformdev.cloud.coveo.com';
Expand All @@ -29,94 +29,123 @@ describe('commerce api client', () => {
PlatformClient.call = platformCallMock;
};

describe('getProductListing', () => {
const buildGetListingV2Request = (
req: Partial<ProductListingV2Request> = {}
): ProductListingV2Request => ({
accessToken: accessToken,
organizationId: organizationId,
url: platformUrl,
trackingId: trackingId,
language: req.language ?? '',
currency: req.currency ?? '',
clientId: req.clientId ?? '',
context: req.context ?? {
view: {url: ''},
const buildCommerceAPIRequest = (
req: Partial<CommerceAPIRequest> = {}
): CommerceAPIRequest => ({
accessToken: accessToken,
organizationId: organizationId,
url: platformUrl,
trackingId: trackingId,
language: req.language ?? '',
currency: req.currency ?? '',
clientId: req.clientId ?? '',
context: req.context ?? {
view: {url: ''},
},
});

it('#getProductListing should call the platform endpoint with the correct arguments', async () => {
const request = buildCommerceAPIRequest();

mockPlatformCall({
ok: true,
json: () => Promise.resolve('some content'),
});

await client.getProductListing(request);

expect(platformCallMock).toBeCalled();
const mockRequest = platformCallMock.mock.calls[0][0];
expect(mockRequest).toMatchObject({
method: 'POST',
contentType: 'application/json',
url: `${platformUrl}/rest/organizations/${organizationId}/trackings/${trackingId}/commerce/v2/listing`,
accessToken: request.accessToken,
origin: 'commerceApiFetch',
requestParams: {
clientId: request.clientId,
context: request.context,
language: request.language,
currency: request.currency,
},
});
});

it('#search should call the platform endpoint with the correct arguments', async () => {
const request = {
...buildCommerceAPIRequest(),
query: 'some query',
};

mockPlatformCall({
ok: true,
json: () => Promise.resolve('some content'),
});

it('should call the platform endpoint with the correct arguments', async () => {
const request = buildGetListingV2Request();

mockPlatformCall({
ok: true,
json: () => Promise.resolve('some content'),
});

await client.getProductListing(request);

expect(platformCallMock).toBeCalled();
const mockRequest = platformCallMock.mock.calls[0][0];
expect(mockRequest).toMatchObject({
method: 'POST',
contentType: 'application/json',
url: `${platformUrl}/rest/organizations/${organizationId}/trackings/${trackingId}/commerce/v2/listing`,
accessToken: request.accessToken,
origin: 'commerceApiFetch',
requestParams: {
clientId: request.clientId,
context: request.context,
language: request.language,
currency: request.currency,
},
});
await client.search(request);

expect(platformCallMock).toBeCalled();
const mockRequest = platformCallMock.mock.calls[0][0];
expect(mockRequest).toMatchObject({
method: 'POST',
contentType: 'application/json',
url: `${platformUrl}/rest/organizations/${organizationId}/trackings/${trackingId}/commerce/v2/search`,
accessToken: request.accessToken,
origin: 'commerceApiFetch',
requestParams: {
query: 'some query',
clientId: request.clientId,
context: request.context,
language: request.language,
currency: request.currency,
},
});
});

it('should return error response on failure', async () => {
const request = buildGetListingV2Request();
it('should return error response on failure', async () => {
const request = buildCommerceAPIRequest();

const expectedError = {
statusCode: 401,
message: 'Unauthorized',
type: 'authorization',
};
const expectedError = {
statusCode: 401,
message: 'Unauthorized',
type: 'authorization',
};

mockPlatformCall({
ok: false,
json: () => Promise.resolve(expectedError),
});
mockPlatformCall({
ok: false,
json: () => Promise.resolve(expectedError),
});

const response = await client.getProductListing(request);
const response = await client.getProductListing(request);

expect(response).toMatchObject({
error: expectedError,
});
expect(response).toMatchObject({
error: expectedError,
});
});

it('should return success response on success', async () => {
const request = buildCommerceAPIRequest();

const expectedBody: CommerceResponse = {
products: [],
facets: [],
pagination: {page: 0, perPage: 0, totalCount: 0, totalPages: 0},
responseId: '',
sort: {
appliedSort: {sortCriteria: SortBy.Relevance},
availableSorts: [{sortCriteria: SortBy.Relevance}],
},
};

mockPlatformCall({
ok: true,
json: () => Promise.resolve(expectedBody),
});

const response = await client.getProductListing(request);

it('should return success response on success', async () => {
const request = buildGetListingV2Request();

const expectedBody: ProductListingV2 = {
products: [],
facets: [],
pagination: {page: 0, perPage: 0, totalCount: 0, totalPages: 0},
responseId: '',
sort: {
appliedSort: {sortCriteria: SortBy.Relevance},
availableSorts: [{sortCriteria: SortBy.Relevance}],
},
};

mockPlatformCall({
ok: true,
json: () => Promise.resolve(expectedBody),
});

const response = await client.getProductListing(request);

expect(response).toMatchObject({
success: expectedBody,
});
expect(response).toMatchObject({
success: expectedBody,
});
});
});
38 changes: 27 additions & 11 deletions packages/headless/src/api/commerce/commerce-api-client.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import {Logger} from 'pino';
import {CommerceThunkExtraArguments} from '../../app/commerce-thunk-extra-arguments';
import {CommerceAppState} from '../../state/commerce-app-state';
import {PlatformClient} from '../platform-client';
import {PlatformClient, PlatformClientCallOptions} from '../platform-client';
import {PreprocessRequest} from '../preprocess-request';
import {buildAPIResponseFromErrorOrThrow} from '../search/search-api-error-response';
import {
CommerceAPIErrorResponse,
CommerceAPIErrorStatusResponse,
} from './commerce-api-error-response';
import {
buildProductListingV2Request,
ProductListingV2Request,
} from './product-listings/v2/product-listing-v2-request';
import {ProductListingV2SuccessResponse} from './product-listings/v2/product-listing-v2-response';
import {buildRequest, CommerceAPIRequest} from './common/request';
import {CommerceSuccessResponse} from './common/response';
import {CommerceSearchRequest} from './search/request';

export interface AsyncThunkCommerceOptions<
T extends Partial<CommerceAppState>,
Expand All @@ -39,20 +37,38 @@ export class CommerceAPIClient {
constructor(private options: CommerceAPIClientOptions) {}

async getProductListing(
req: ProductListingV2Request
): Promise<CommerceAPIResponse<ProductListingV2SuccessResponse>> {
const response = await PlatformClient.call({
...buildProductListingV2Request(req),
req: CommerceAPIRequest
): Promise<CommerceAPIResponse<CommerceSuccessResponse>> {
return this.query({
...buildRequest(req, 'listing'),
...this.options,
});
}

async search(
req: CommerceSearchRequest
): Promise<CommerceAPIResponse<CommerceSuccessResponse>> {
const requestOptions = buildRequest(req, 'search');
return this.query({
...requestOptions,
requestParams: {
...requestOptions.requestParams,
query: req?.query,
},
...this.options,
});
}

private async query(options: PlatformClientCallOptions) {
const response = await PlatformClient.call(options);

if (response instanceof Error) {
return buildAPIResponseFromErrorOrThrow(response);
}

const body = await response.json();
return response.ok
? {success: body as ProductListingV2SuccessResponse}
? {success: body as CommerceSuccessResponse}
: {error: body as CommerceAPIErrorStatusResponse};
}
}
10 changes: 7 additions & 3 deletions packages/headless/src/api/commerce/commerce-api-params.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {CommerceFacetRequest} from '../../features/commerce/facets/facet-set/interfaces/request';
import {SortOption} from './product-listings/v2/sort';
import {SortOption} from './common/sort';

export interface TrackingIdParam {
trackingId: string;
Expand Down Expand Up @@ -60,10 +60,14 @@ export interface FacetsParam {
facets?: CommerceFacetRequest[];
}

export interface SelectedPageParam {
export interface PageParam {
page?: number;
}

export interface SelectedSortParam {
export interface SortParam {
sort?: SortOption;
}

export interface QueryParam {
query?: string;
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,34 @@
import {
HTTPContentType,
HttpMethods,
PlatformClientCallOptions,
} from '../../../platform-client';
import {BaseParam} from '../../../platform-service-params';
import {PlatformClientCallOptions} from '../../platform-client';
import {BaseParam} from '../../platform-service-params';
import {
ClientIdParam,
ContextParam,
LanguageParam,
CurrencyParam,
LanguageParam,
FacetsParam,
SelectedPageParam,
SelectedSortParam,
PageParam,
SortParam,
TrackingIdParam,
} from '../../commerce-api-params';
} from '../commerce-api-params';

export type ProductListingV2Request = BaseParam &
export type CommerceAPIRequest = BaseParam &
TrackingIdParam &
LanguageParam &
CurrencyParam &
ClientIdParam &
ContextParam &
FacetsParam &
SelectedPageParam &
SelectedSortParam;
PageParam &
SortParam;

export const buildProductListingV2Request = (req: ProductListingV2Request) => {
export const buildRequest = (req: CommerceAPIRequest, path: string) => {
return {
...baseProductListingV2Request(req, 'POST', 'application/json'),
...baseRequest(req, path),
requestParams: prepareRequestParams(req),
};
};

const prepareRequestParams = (req: ProductListingV2Request) => {
const prepareRequestParams = (req: CommerceAPIRequest) => {
const {clientId, context, language, currency, page, facets, sort} = req;
return {
clientId,
Expand All @@ -45,21 +41,20 @@ const prepareRequestParams = (req: ProductListingV2Request) => {
};
};

export const baseProductListingV2Request = (
req: ProductListingV2Request,
method: HttpMethods,
contentType: HTTPContentType
export const baseRequest = (
req: CommerceAPIRequest,
path: string
): Pick<
PlatformClientCallOptions,
'accessToken' | 'method' | 'contentType' | 'url' | 'origin'
> => {
const {url, organizationId, accessToken, trackingId} = req;
const baseUrl = `${url}/rest/organizations/${organizationId}/trackings/${trackingId}/commerce/v2/listing`;
const baseUrl = `${url}/rest/organizations/${organizationId}/trackings/${trackingId}/commerce/v2/${path}`;

return {
accessToken,
method,
contentType,
method: 'POST',
contentType: 'application/json',
url: baseUrl,
origin: 'commerceApiFetch',
};
Expand Down
Loading

0 comments on commit 2432693

Please sign in to comment.