@labdigital/commercetools-mock 2.29.1 → 2.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +175 -51
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +175 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/searchQueryTypeChecker.test.ts +96 -0
- package/src/lib/searchQueryTypeChecker.ts +120 -0
- package/src/product-projection-search.ts +1 -1
- package/src/product-search.ts +123 -0
- package/src/repositories/product/index.ts +13 -0
- package/src/services/product.test.ts +76 -1
- package/src/services/product.ts +15 -1
- package/src/types.ts +2 -2
package/package.json
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SearchAndExpression,
|
|
3
|
+
SearchOrExpression,
|
|
4
|
+
_SearchQuery,
|
|
5
|
+
_SearchQueryExpression,
|
|
6
|
+
} from "@commercetools/platform-sdk";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
isSearchAndExpression,
|
|
10
|
+
isSearchAnyValue,
|
|
11
|
+
isSearchExactExpression,
|
|
12
|
+
isSearchExistsExpression,
|
|
13
|
+
isSearchFilterExpression,
|
|
14
|
+
isSearchFullTextExpression,
|
|
15
|
+
isSearchFullTextPrefixExpression,
|
|
16
|
+
isSearchNotExpression,
|
|
17
|
+
isSearchOrExpression,
|
|
18
|
+
isSearchPrefixExpression,
|
|
19
|
+
isSearchRangeExpression,
|
|
20
|
+
isSearchWildCardExpression,
|
|
21
|
+
validateSearchQuery,
|
|
22
|
+
} from "./searchQueryTypeChecker";
|
|
23
|
+
|
|
24
|
+
describe("searchQueryTypeChecker", () => {
|
|
25
|
+
it("should validate SearchAndExpression", () => {
|
|
26
|
+
const query: SearchAndExpression = { and: [] };
|
|
27
|
+
expect(isSearchAndExpression(query)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("should validate SearchOrExpression", () => {
|
|
31
|
+
const query: SearchOrExpression = { or: [] };
|
|
32
|
+
expect(isSearchOrExpression(query)).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should validate SearchNotExpression", () => {
|
|
36
|
+
const query: _SearchQueryExpression = { not: {} };
|
|
37
|
+
expect(isSearchNotExpression(query)).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should validate SearchFilterExpression", () => {
|
|
41
|
+
const query: _SearchQueryExpression = { filter: [] };
|
|
42
|
+
expect(isSearchFilterExpression(query)).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("should validate SearchRangeExpression", () => {
|
|
46
|
+
const query: _SearchQueryExpression = { range: {} };
|
|
47
|
+
expect(isSearchRangeExpression(query)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should validate SearchExactExpression", () => {
|
|
51
|
+
const query: _SearchQueryExpression = { exact: "some-exact" };
|
|
52
|
+
expect(isSearchExactExpression(query)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should validate SearchExistsExpression", () => {
|
|
56
|
+
const query: _SearchQueryExpression = { exists: true };
|
|
57
|
+
expect(isSearchExistsExpression(query)).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should validate SearchFullTextExpression", () => {
|
|
61
|
+
const query: _SearchQueryExpression = { fullText: "some-text" };
|
|
62
|
+
expect(isSearchFullTextExpression(query)).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should validate SearchFullTextPrefixExpression", () => {
|
|
66
|
+
const query: _SearchQueryExpression = { fullTextPrefix: "some-prefix" };
|
|
67
|
+
expect(isSearchFullTextPrefixExpression(query)).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should validate SearchPrefixExpression", () => {
|
|
71
|
+
const query: _SearchQueryExpression = { prefix: "some-prefix" };
|
|
72
|
+
expect(isSearchPrefixExpression(query)).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should validate SearchWildCardExpression", () => {
|
|
76
|
+
const query: _SearchQueryExpression = { wildcard: "some-wildcard" };
|
|
77
|
+
expect(isSearchWildCardExpression(query)).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should validate SearchAnyValue", () => {
|
|
81
|
+
const query: _SearchQueryExpression = { value: "some-value" };
|
|
82
|
+
expect(isSearchAnyValue(query)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should throw an error for unsupported query", () => {
|
|
86
|
+
const query = { unsupported: "unsupported" } as _SearchQuery;
|
|
87
|
+
expect(() => validateSearchQuery(query)).toThrow(
|
|
88
|
+
"Unsupported search query expression",
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should not throw an error for supported query", () => {
|
|
93
|
+
const query: SearchAndExpression = { and: [] };
|
|
94
|
+
expect(() => validateSearchQuery(query)).not.toThrow();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SearchAndExpression,
|
|
3
|
+
SearchAnyValue,
|
|
4
|
+
SearchDateRangeExpression,
|
|
5
|
+
SearchDateTimeRangeExpression,
|
|
6
|
+
SearchExactExpression,
|
|
7
|
+
SearchExistsExpression,
|
|
8
|
+
SearchFilterExpression,
|
|
9
|
+
SearchFullTextExpression,
|
|
10
|
+
SearchFullTextPrefixExpression,
|
|
11
|
+
SearchLongRangeExpression,
|
|
12
|
+
SearchNotExpression,
|
|
13
|
+
SearchNumberRangeExpression,
|
|
14
|
+
SearchOrExpression,
|
|
15
|
+
SearchPrefixExpression,
|
|
16
|
+
SearchTimeRangeExpression,
|
|
17
|
+
SearchWildCardExpression,
|
|
18
|
+
_SearchQuery,
|
|
19
|
+
_SearchQueryExpression,
|
|
20
|
+
} from "@commercetools/platform-sdk";
|
|
21
|
+
|
|
22
|
+
export const validateSearchQuery = (query: _SearchQuery): void => {
|
|
23
|
+
if (isSearchAndExpression(query)) {
|
|
24
|
+
query.and.forEach((expr) => validateSearchQuery(expr));
|
|
25
|
+
} else if (isSearchOrExpression(query)) {
|
|
26
|
+
query.or.forEach((expr) => validateSearchQuery(expr));
|
|
27
|
+
} else if (isSearchNotExpression(query)) {
|
|
28
|
+
validateSearchQuery(query.not);
|
|
29
|
+
} else if (
|
|
30
|
+
isSearchFilterExpression(query) ||
|
|
31
|
+
isSearchRangeExpression(query) ||
|
|
32
|
+
isSearchExactExpression(query) ||
|
|
33
|
+
isSearchExistsExpression(query) ||
|
|
34
|
+
isSearchFullTextExpression(query) ||
|
|
35
|
+
isSearchFullTextPrefixExpression(query) ||
|
|
36
|
+
isSearchPrefixExpression(query) ||
|
|
37
|
+
isSearchWildCardExpression(query) ||
|
|
38
|
+
isSearchAnyValue(query)
|
|
39
|
+
) {
|
|
40
|
+
return;
|
|
41
|
+
} else {
|
|
42
|
+
throw new Error("Unsupported search query expression");
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Type guards
|
|
47
|
+
export const isSearchAndExpression = (
|
|
48
|
+
expr: _SearchQuery,
|
|
49
|
+
): expr is SearchAndExpression =>
|
|
50
|
+
(expr as SearchAndExpression).and !== undefined;
|
|
51
|
+
|
|
52
|
+
export const isSearchOrExpression = (
|
|
53
|
+
expr: _SearchQuery,
|
|
54
|
+
): expr is SearchOrExpression => (expr as SearchOrExpression).or !== undefined;
|
|
55
|
+
|
|
56
|
+
export type SearchRangeExpression =
|
|
57
|
+
| SearchDateRangeExpression
|
|
58
|
+
| SearchDateTimeRangeExpression
|
|
59
|
+
| SearchLongRangeExpression
|
|
60
|
+
| SearchNumberRangeExpression
|
|
61
|
+
| SearchTimeRangeExpression;
|
|
62
|
+
|
|
63
|
+
// Type guard for SearchNotExpression
|
|
64
|
+
export const isSearchNotExpression = (
|
|
65
|
+
expr: _SearchQueryExpression,
|
|
66
|
+
): expr is SearchNotExpression =>
|
|
67
|
+
(expr as SearchNotExpression).not !== undefined;
|
|
68
|
+
|
|
69
|
+
// Type guard for SearchFilterExpression
|
|
70
|
+
export const isSearchFilterExpression = (
|
|
71
|
+
expr: _SearchQueryExpression,
|
|
72
|
+
): expr is SearchFilterExpression =>
|
|
73
|
+
(expr as SearchFilterExpression).filter !== undefined;
|
|
74
|
+
|
|
75
|
+
// Type guard for SearchDateRangeExpression
|
|
76
|
+
export const isSearchRangeExpression = (
|
|
77
|
+
expr: _SearchQueryExpression,
|
|
78
|
+
): expr is SearchRangeExpression =>
|
|
79
|
+
(expr as SearchRangeExpression).range !== undefined;
|
|
80
|
+
|
|
81
|
+
// Type guard for SearchExactExpression
|
|
82
|
+
export const isSearchExactExpression = (
|
|
83
|
+
expr: _SearchQueryExpression,
|
|
84
|
+
): expr is SearchExactExpression =>
|
|
85
|
+
(expr as SearchExactExpression).exact !== undefined;
|
|
86
|
+
|
|
87
|
+
// Type guard for SearchExistsExpression
|
|
88
|
+
export const isSearchExistsExpression = (
|
|
89
|
+
expr: _SearchQueryExpression,
|
|
90
|
+
): expr is SearchExistsExpression =>
|
|
91
|
+
(expr as SearchExistsExpression).exists !== undefined;
|
|
92
|
+
|
|
93
|
+
// Type guard for SearchFullTextExpression
|
|
94
|
+
export const isSearchFullTextExpression = (
|
|
95
|
+
expr: _SearchQueryExpression,
|
|
96
|
+
): expr is SearchFullTextExpression =>
|
|
97
|
+
(expr as SearchFullTextExpression).fullText !== undefined;
|
|
98
|
+
|
|
99
|
+
// Type guard for SearchFullTextPrefixExpression
|
|
100
|
+
export const isSearchFullTextPrefixExpression = (
|
|
101
|
+
expr: _SearchQueryExpression,
|
|
102
|
+
): expr is SearchFullTextPrefixExpression =>
|
|
103
|
+
(expr as SearchFullTextPrefixExpression).fullTextPrefix !== undefined;
|
|
104
|
+
|
|
105
|
+
// Type guard for SearchPrefixExpression
|
|
106
|
+
export const isSearchPrefixExpression = (
|
|
107
|
+
expr: _SearchQueryExpression,
|
|
108
|
+
): expr is SearchPrefixExpression =>
|
|
109
|
+
(expr as SearchPrefixExpression).prefix !== undefined;
|
|
110
|
+
|
|
111
|
+
// Type guard for SearchWildCardExpression
|
|
112
|
+
export const isSearchWildCardExpression = (
|
|
113
|
+
expr: _SearchQueryExpression,
|
|
114
|
+
): expr is SearchWildCardExpression =>
|
|
115
|
+
(expr as SearchWildCardExpression).wildcard !== undefined;
|
|
116
|
+
|
|
117
|
+
// Type guard for SearchAnyValue
|
|
118
|
+
export const isSearchAnyValue = (
|
|
119
|
+
expr: _SearchQueryExpression,
|
|
120
|
+
): expr is SearchAnyValue => (expr as SearchAnyValue).value !== undefined;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {
|
|
2
|
+
InvalidInputError,
|
|
3
|
+
Product,
|
|
4
|
+
ProductPagedSearchResponse,
|
|
5
|
+
ProductProjection,
|
|
6
|
+
ProductSearchRequest,
|
|
7
|
+
ProductSearchResult,
|
|
8
|
+
} from "@commercetools/platform-sdk";
|
|
9
|
+
import { CommercetoolsError } from "./exceptions";
|
|
10
|
+
import { validateSearchQuery } from "./lib/searchQueryTypeChecker";
|
|
11
|
+
import { applyPriceSelector } from "./priceSelector";
|
|
12
|
+
import { AbstractStorage } from "./storage";
|
|
13
|
+
|
|
14
|
+
export class ProductSearch {
|
|
15
|
+
protected _storage: AbstractStorage;
|
|
16
|
+
|
|
17
|
+
constructor(storage: AbstractStorage) {
|
|
18
|
+
this._storage = storage;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
search(
|
|
22
|
+
projectKey: string,
|
|
23
|
+
params: ProductSearchRequest,
|
|
24
|
+
): ProductPagedSearchResponse {
|
|
25
|
+
const resources = this._storage
|
|
26
|
+
.all(projectKey, "product")
|
|
27
|
+
.map((r) =>
|
|
28
|
+
this.transform(r, params.productProjectionParameters?.staged ?? false),
|
|
29
|
+
)
|
|
30
|
+
.filter((p) => {
|
|
31
|
+
if (!params.productProjectionParameters?.staged ?? false) {
|
|
32
|
+
return p.published;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Validate query, if given
|
|
38
|
+
if (params.query) {
|
|
39
|
+
try {
|
|
40
|
+
validateSearchQuery(params.query);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(err);
|
|
43
|
+
throw new CommercetoolsError<InvalidInputError>(
|
|
44
|
+
{
|
|
45
|
+
code: "InvalidInput",
|
|
46
|
+
message: (err as any).message,
|
|
47
|
+
},
|
|
48
|
+
400,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Apply the priceSelector
|
|
54
|
+
if (params.productProjectionParameters) {
|
|
55
|
+
applyPriceSelector(resources, {
|
|
56
|
+
country: params.productProjectionParameters.priceCountry,
|
|
57
|
+
channel: params.productProjectionParameters.priceChannel,
|
|
58
|
+
customerGroup: params.productProjectionParameters.priceCustomerGroup,
|
|
59
|
+
currency: params.productProjectionParameters.priceCurrency,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// @TODO: Determine whether or not to spoof search, facet filtering, wildcard, boosting and/or sorting.
|
|
64
|
+
// For now this is deliberately not supported.
|
|
65
|
+
|
|
66
|
+
const offset = params.offset || 0;
|
|
67
|
+
const limit = params.limit || 20;
|
|
68
|
+
const productProjectionsResult = resources.slice(offset, offset + limit);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Do not supply productProjection if productProjectionParameters are not given
|
|
72
|
+
* https://docs.commercetools.com/api/projects/product-search#with-product-projection-parameters
|
|
73
|
+
*/
|
|
74
|
+
const productProjectionsParameterGiven =
|
|
75
|
+
!!params?.productProjectionParameters;
|
|
76
|
+
|
|
77
|
+
// Transform to ProductSearchResult
|
|
78
|
+
const results: ProductSearchResult[] = productProjectionsResult.map(
|
|
79
|
+
(product) => ({
|
|
80
|
+
productProjection: productProjectionsParameterGiven
|
|
81
|
+
? product
|
|
82
|
+
: undefined,
|
|
83
|
+
id: product.id,
|
|
84
|
+
/**
|
|
85
|
+
* @TODO: possibly add support for optional matchingVariants
|
|
86
|
+
* https://docs.commercetools.com/api/projects/product-search#productsearchmatchingvariants
|
|
87
|
+
*/
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
total: resources.length,
|
|
93
|
+
offset: offset,
|
|
94
|
+
limit: limit,
|
|
95
|
+
results: results,
|
|
96
|
+
facets: [],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
transform(product: Product, staged: boolean): ProductProjection {
|
|
101
|
+
const obj = !staged
|
|
102
|
+
? product.masterData.current
|
|
103
|
+
: product.masterData.staged;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
id: product.id,
|
|
107
|
+
createdAt: product.createdAt,
|
|
108
|
+
lastModifiedAt: product.lastModifiedAt,
|
|
109
|
+
version: product.version,
|
|
110
|
+
name: obj.name,
|
|
111
|
+
key: product.key,
|
|
112
|
+
description: obj.description,
|
|
113
|
+
metaDescription: obj.metaDescription,
|
|
114
|
+
slug: obj.slug,
|
|
115
|
+
categories: obj.categories,
|
|
116
|
+
masterVariant: obj.masterVariant,
|
|
117
|
+
variants: obj.variants,
|
|
118
|
+
productType: product.productType,
|
|
119
|
+
hasStagedChanges: product.masterData.hasStagedChanges,
|
|
120
|
+
published: product.masterData.published,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -4,12 +4,15 @@ import type {
|
|
|
4
4
|
Product,
|
|
5
5
|
ProductData,
|
|
6
6
|
ProductDraft,
|
|
7
|
+
ProductPagedSearchResponse,
|
|
8
|
+
ProductSearchRequest,
|
|
7
9
|
ProductTypeReference,
|
|
8
10
|
StateReference,
|
|
9
11
|
TaxCategoryReference,
|
|
10
12
|
} from "@commercetools/platform-sdk";
|
|
11
13
|
import { CommercetoolsError } from "~src/exceptions";
|
|
12
14
|
import { getBaseResourceProperties } from "~src/helpers";
|
|
15
|
+
import { ProductSearch } from "~src/product-search";
|
|
13
16
|
import { AbstractStorage } from "~src/storage/abstract";
|
|
14
17
|
import { AbstractResourceRepository, RepositoryContext } from "../abstract";
|
|
15
18
|
import { getReferenceFromResourceIdentifier } from "../helpers";
|
|
@@ -17,9 +20,12 @@ import { ProductUpdateHandler } from "./actions";
|
|
|
17
20
|
import { variantFromDraft } from "./helpers";
|
|
18
21
|
|
|
19
22
|
export class ProductRepository extends AbstractResourceRepository<"product"> {
|
|
23
|
+
protected _searchService: ProductSearch;
|
|
24
|
+
|
|
20
25
|
constructor(storage: AbstractStorage) {
|
|
21
26
|
super("product", storage);
|
|
22
27
|
this.actions = new ProductUpdateHandler(storage);
|
|
28
|
+
this._searchService = new ProductSearch(storage);
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
create(context: RepositoryContext, draft: ProductDraft): Product {
|
|
@@ -127,4 +133,11 @@ export class ProductRepository extends AbstractResourceRepository<"product"> {
|
|
|
127
133
|
|
|
128
134
|
return this.saveNew(context, resource);
|
|
129
135
|
}
|
|
136
|
+
|
|
137
|
+
search(
|
|
138
|
+
context: RepositoryContext,
|
|
139
|
+
searchRequest: ProductSearchRequest,
|
|
140
|
+
): ProductPagedSearchResponse {
|
|
141
|
+
return this._searchService.search(context.projectKey, searchRequest);
|
|
142
|
+
}
|
|
130
143
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
2
|
Category,
|
|
3
3
|
CategoryDraft,
|
|
4
4
|
Image,
|
|
@@ -6,6 +6,9 @@ import type {
|
|
|
6
6
|
Product,
|
|
7
7
|
ProductData,
|
|
8
8
|
ProductDraft,
|
|
9
|
+
ProductPagedSearchResponse,
|
|
10
|
+
ProductSearchRequest,
|
|
11
|
+
ProductSearchResult,
|
|
9
12
|
ProductType,
|
|
10
13
|
ProductTypeDraft,
|
|
11
14
|
State,
|
|
@@ -1485,4 +1488,76 @@ describe("Product update actions", () => {
|
|
|
1485
1488
|
?.myCustomField,
|
|
1486
1489
|
).toBe("MyRandomValue");
|
|
1487
1490
|
});
|
|
1491
|
+
|
|
1492
|
+
// Test the general product search implementation
|
|
1493
|
+
describe("Product Search - Generic", () => {
|
|
1494
|
+
test("Pagination", async () => {
|
|
1495
|
+
{
|
|
1496
|
+
const body: ProductSearchRequest = {
|
|
1497
|
+
productProjectionParameters: {
|
|
1498
|
+
storeProjection: "dummy-store",
|
|
1499
|
+
localeProjection: ["en-US"],
|
|
1500
|
+
priceCurrency: "EUR",
|
|
1501
|
+
priceChannel: "dummy-channel",
|
|
1502
|
+
expand: ["categories[*]", "categories[*].ancestors[*]"],
|
|
1503
|
+
},
|
|
1504
|
+
limit: 24,
|
|
1505
|
+
};
|
|
1506
|
+
const response = await supertest(ctMock.app)
|
|
1507
|
+
.post("/dummy/products/search")
|
|
1508
|
+
.send(body);
|
|
1509
|
+
|
|
1510
|
+
const pagedSearchResponse: ProductPagedSearchResponse = response.body;
|
|
1511
|
+
expect(pagedSearchResponse.limit).toBe(24);
|
|
1512
|
+
expect(pagedSearchResponse.offset).toBe(0);
|
|
1513
|
+
expect(pagedSearchResponse.total).toBeGreaterThan(0);
|
|
1514
|
+
|
|
1515
|
+
// Deliberately not supported fow now
|
|
1516
|
+
expect(pagedSearchResponse.facets).toEqual([]);
|
|
1517
|
+
|
|
1518
|
+
const results: ProductSearchResult[] = pagedSearchResponse.results;
|
|
1519
|
+
expect(results).toBeDefined();
|
|
1520
|
+
expect(results.length).toBeGreaterThan(0);
|
|
1521
|
+
|
|
1522
|
+
// Find product with sku "1337" to be part of the search results
|
|
1523
|
+
const productFound = results.find(
|
|
1524
|
+
(result) => result?.productProjection?.masterVariant?.sku === "1337",
|
|
1525
|
+
);
|
|
1526
|
+
expect(productFound).toBeDefined();
|
|
1527
|
+
|
|
1528
|
+
const priceCurrencyMatch = results.find((result) =>
|
|
1529
|
+
result?.productProjection?.masterVariant?.prices?.find(
|
|
1530
|
+
(price) => price?.value?.currencyCode === "EUR",
|
|
1531
|
+
),
|
|
1532
|
+
);
|
|
1533
|
+
expect(priceCurrencyMatch).toBeDefined();
|
|
1534
|
+
}
|
|
1535
|
+
{
|
|
1536
|
+
const body: ProductSearchRequest = {
|
|
1537
|
+
limit: 88,
|
|
1538
|
+
offset: 88,
|
|
1539
|
+
};
|
|
1540
|
+
|
|
1541
|
+
const response = await supertest(ctMock.app)
|
|
1542
|
+
.post("/dummy/products/search")
|
|
1543
|
+
.send(body);
|
|
1544
|
+
|
|
1545
|
+
const pagedSearchResponse: ProductPagedSearchResponse = response.body;
|
|
1546
|
+
expect(pagedSearchResponse.limit).toBe(88);
|
|
1547
|
+
expect(pagedSearchResponse.offset).toBe(88);
|
|
1548
|
+
expect(pagedSearchResponse.total).toBeGreaterThan(0);
|
|
1549
|
+
|
|
1550
|
+
// No results, since we start at offset 88
|
|
1551
|
+
const results: ProductSearchResult[] = pagedSearchResponse.results;
|
|
1552
|
+
expect(results).toBeDefined();
|
|
1553
|
+
expect(results.length).toBe(0);
|
|
1554
|
+
|
|
1555
|
+
// Product with sku "1337" should not be part of the results
|
|
1556
|
+
const productFound = results.find(
|
|
1557
|
+
(result) => result?.productProjection?.masterVariant?.sku === "1337",
|
|
1558
|
+
);
|
|
1559
|
+
expect(productFound).toBeUndefined();
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
});
|
|
1488
1563
|
});
|
package/src/services/product.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Router } from "express";
|
|
1
|
+
import { Request, Response, Router } from "express";
|
|
2
|
+
import { getRepositoryContext } from "~src/repositories/helpers";
|
|
2
3
|
import { ProductRepository } from "../repositories/product";
|
|
3
4
|
import AbstractService from "./abstract";
|
|
4
5
|
|
|
@@ -13,4 +14,17 @@ export class ProductService extends AbstractService {
|
|
|
13
14
|
getBasePath() {
|
|
14
15
|
return "products";
|
|
15
16
|
}
|
|
17
|
+
|
|
18
|
+
extraRoutes(router: Router) {
|
|
19
|
+
router.post("/search", this.search.bind(this));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
search(request: Request, response: Response) {
|
|
23
|
+
const searchBody = request.body;
|
|
24
|
+
const resource = this.repository.search(
|
|
25
|
+
getRepositoryContext(request),
|
|
26
|
+
searchBody,
|
|
27
|
+
);
|
|
28
|
+
return response.status(200).send(resource);
|
|
29
|
+
}
|
|
16
30
|
}
|
package/src/types.ts
CHANGED
|
@@ -10,11 +10,11 @@ export type ShallowWritable<T> = { -readonly [P in keyof T]: T[P] };
|
|
|
10
10
|
export type ServiceTypes =
|
|
11
11
|
| ctp.ReferenceTypeId
|
|
12
12
|
| "product-projection"
|
|
13
|
+
| "product-search"
|
|
13
14
|
| "my-cart"
|
|
14
15
|
| "my-order"
|
|
15
16
|
| "my-payment"
|
|
16
|
-
| "my-customer"
|
|
17
|
-
| "product-projection";
|
|
17
|
+
| "my-customer";
|
|
18
18
|
|
|
19
19
|
export type Services = Partial<{
|
|
20
20
|
[index in ServiceTypes]: AbstractService;
|