From 528d38150b9d0b4f631b0c1ddf5903ccc6b4a952 Mon Sep 17 00:00:00 2001 From: cewald Date: Wed, 8 Apr 2020 09:13:46 +0000 Subject: [PATCH 1/6] Update `storefront-query-builder`object in unit tests --- core/modules/cart/test/unit/store/productActions.spec.ts | 2 +- core/modules/cms/test/unit/createHierarchyLoadQuery.spec.ts | 2 +- core/modules/cms/test/unit/createLoadingBlockQuery.spec.ts | 2 +- core/modules/cms/test/unit/createPageLoadingQuery.spec.ts | 2 +- core/modules/cms/test/unit/createSingleBlockQuery.spec.ts | 2 +- core/modules/cms/test/unit/createSinglePageLoadQuery.spec.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/modules/cart/test/unit/store/productActions.spec.ts b/core/modules/cart/test/unit/store/productActions.spec.ts index 5f8a9d90fe..bf51b81ac1 100644 --- a/core/modules/cart/test/unit/store/productActions.spec.ts +++ b/core/modules/cart/test/unit/store/productActions.spec.ts @@ -76,7 +76,7 @@ describe('Cart productActions', () => { (contextMock.dispatch as jest.Mock).mockImplementationOnce(() => ({ items: [serverItem] })); const result = await (cartActions as any).findProductOption(contextMock, { serverItem }); - expect(contextMock.dispatch).toBeCalledWith('product/list', { query: { _appliedFilters: [{ attribute: 'configurable_children.sku', options: Object, scope: 'default', value: { eq: 1 } }], _availableFilters: [], _searchText: '' }, size: 1, start: 0, updateState: false }, { root: true }) + expect(contextMock.dispatch).toBeCalledWith('product/list', { query: { _appliedFilters: [{ attribute: 'configurable_children.sku', options: Object, scope: 'default', value: { eq: 1 } }], _availableFilters: [], _appliedSort: [], _searchText: '' }, size: 1, start: 0, updateState: false }, { root: true }) expect(result).toEqual({ childSku: 1, sku: 1 }) }); diff --git a/core/modules/cms/test/unit/createHierarchyLoadQuery.spec.ts b/core/modules/cms/test/unit/createHierarchyLoadQuery.spec.ts index bd9e32df6d..b21c9494a0 100644 --- a/core/modules/cms/test/unit/createHierarchyLoadQuery.spec.ts +++ b/core/modules/cms/test/unit/createHierarchyLoadQuery.spec.ts @@ -20,6 +20,6 @@ describe('createHierarchyLoadQuery', () => { const filter = { id: null } let hierarchyLoadQuery = createHierarchyLoadQuery(filter) - expect(hierarchyLoadQuery).toEqual({ _availableFilters: [], _appliedFilters: [], _searchText: '' }) + expect(hierarchyLoadQuery).toEqual({ _availableFilters: [], _appliedFilters: [], _appliedSort: [], _searchText: '' }) }) }) diff --git a/core/modules/cms/test/unit/createLoadingBlockQuery.spec.ts b/core/modules/cms/test/unit/createLoadingBlockQuery.spec.ts index 625d2393cf..4673de015a 100644 --- a/core/modules/cms/test/unit/createLoadingBlockQuery.spec.ts +++ b/core/modules/cms/test/unit/createLoadingBlockQuery.spec.ts @@ -15,6 +15,6 @@ describe('createLoadingBlockQuery', () => { const filter = { filterField: 'test', filterValues: undefined } let loadingBlockQuery = createLoadingBlockQuery(filter) - expect(loadingBlockQuery).toEqual({ _availableFilters: [], _appliedFilters: [], _searchText: '' }) + expect(loadingBlockQuery).toEqual({ _availableFilters: [], _appliedFilters: [], _appliedSort: [], _searchText: '' }) }) }) diff --git a/core/modules/cms/test/unit/createPageLoadingQuery.spec.ts b/core/modules/cms/test/unit/createPageLoadingQuery.spec.ts index 4876ae4f27..471fe1ac59 100644 --- a/core/modules/cms/test/unit/createPageLoadingQuery.spec.ts +++ b/core/modules/cms/test/unit/createPageLoadingQuery.spec.ts @@ -15,6 +15,6 @@ describe('createPageLoadingQuery', () => { const filter = { filterField: 'test', filterValues: undefined } let pageLoadingQuery = createPageLoadingQuery(filter) - expect(pageLoadingQuery).toEqual({ _availableFilters: [], _appliedFilters: [], _searchText: '' }) + expect(pageLoadingQuery).toEqual({ _availableFilters: [], _appliedFilters: [], _appliedSort: [], _searchText: '' }) }) }) diff --git a/core/modules/cms/test/unit/createSingleBlockQuery.spec.ts b/core/modules/cms/test/unit/createSingleBlockQuery.spec.ts index 1b5964a329..feca385fa9 100644 --- a/core/modules/cms/test/unit/createSingleBlockQuery.spec.ts +++ b/core/modules/cms/test/unit/createSingleBlockQuery.spec.ts @@ -15,6 +15,6 @@ describe('createSingleBlockLoadQuery should', () => { const argsMock = { key: 'test', value: undefined } let mockSingleBlockQuery = createSingleBlockQuery(argsMock) - expect(mockSingleBlockQuery).toEqual({ _availableFilters: [], _appliedFilters: [], _searchText: '' }) + expect(mockSingleBlockQuery).toEqual({ _availableFilters: [], _appliedFilters: [], _appliedSort: [], _searchText: '' }) }) }) diff --git a/core/modules/cms/test/unit/createSinglePageLoadQuery.spec.ts b/core/modules/cms/test/unit/createSinglePageLoadQuery.spec.ts index cbc0f630da..343b3d4b4a 100644 --- a/core/modules/cms/test/unit/createSinglePageLoadQuery.spec.ts +++ b/core/modules/cms/test/unit/createSinglePageLoadQuery.spec.ts @@ -15,6 +15,6 @@ describe('createSinglePageLoadQuery should', () => { const filter = { key: 'test', value: undefined } let singlePageMockQuery = createSinglePageLoadQuery(filter) - expect(singlePageMockQuery).toEqual({ _availableFilters: [], _appliedFilters: [], _searchText: '' }) + expect(singlePageMockQuery).toEqual({ _availableFilters: [], _appliedFilters: [], _appliedSort: [], _searchText: '' }) }) }) From 658990894fb02dfe76eafd7aae4ac5638664a1ac Mon Sep 17 00:00:00 2001 From: cewald Date: Wed, 8 Apr 2020 09:51:47 +0000 Subject: [PATCH 2/6] Update to `storefront-query-builder` version `1.0.0` --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 42bba80a67..ccd48aa423 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "redis-tag-cache": "^1.2.1", "reflect-metadata": "^0.1.12", "register-service-worker": "^1.5.2", - "storefront-query-builder": "^0.0.9", + "storefront-query-builder": "^1.0.0", "ts-node": "^8.6.2", "vue": "^2.6.11", "vue-analytics": "^5.16.1", From 260aec4c249116c8534f59f29c1dfb3ea43b17ad Mon Sep 17 00:00:00 2001 From: cewald Date: Wed, 8 Apr 2020 09:58:14 +0000 Subject: [PATCH 3/6] Update `api-search-query` search-adapter to support new `storefront-query-builder` sorting --- core/lib/search/adapter/api-search-query/searchAdapter.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/lib/search/adapter/api-search-query/searchAdapter.ts b/core/lib/search/adapter/api-search-query/searchAdapter.ts index 551e13b58c..326ccf95ab 100644 --- a/core/lib/search/adapter/api-search-query/searchAdapter.ts +++ b/core/lib/search/adapter/api-search-query/searchAdapter.ts @@ -42,6 +42,11 @@ export class SearchAdapter { if (Request.hasOwnProperty('groupToken') && Request.groupToken !== null) { rawQueryObject['groupToken'] = Request.groupToken } + if (Request.sort) { + const [ field, options ] = Request.sort.split(':') + rawQueryObject.applySort({ field, options }) + delete Request.sort + } const storeView = (Request.store === null) ? currentStoreView() : await prepareStoreView(Request.store) Request.index = storeView.elasticsearch.index From e8fdd76c56b2043ea71b2b3c308f4f579f2408f9 Mon Sep 17 00:00:00 2001 From: cewald Date: Wed, 8 Apr 2020 10:01:57 +0000 Subject: [PATCH 4/6] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4b986b05a..892191309a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Product Page Schema implementation as JSON-LD - @Michal-Dziedzinski (#3704) - Add `/cache-version.json` route to get current cache version - Built-in module for detecting device type based on UserAgent with SSR support - @Fifciu +- Update to `storefront-query-builder` version `1.0.0` - @cewald (#4234) ### Fixed From 877fd7ab8edd96c79f7bb280b8a712178d0b3588 Mon Sep 17 00:00:00 2001 From: cewald Date: Wed, 8 Apr 2020 14:26:36 +0000 Subject: [PATCH 5/6] Add cookbook recipe for extending the query builder to docs --- docs/guide/cookbook/module.md | 100 ++++++++++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 6 deletions(-) diff --git a/docs/guide/cookbook/module.md b/docs/guide/cookbook/module.md index 3ec015afef..45f50decd2 100644 --- a/docs/guide/cookbook/module.md +++ b/docs/guide/cookbook/module.md @@ -678,10 +678,98 @@ It's hands down no-brainer to bootstrap a module _manually_ because the skeleton ### 2. Recipe ### 3. Peep into the kitchen (what happens internally) ### 4. Chef's secret (protip) -
-
-## 6. Anti-patterns & Common pitfalls +## 6. Extend Elasticsearch request body using `storefront-query-builder` + +If you're using the new [`storefront-query-builder`](https://github.com/DivanteLtd/storefront-query-builder) and the `api-search-query` search-adapter ([introduced with v1.1.12](/guide/upgrade-notes/#_1-11-1-12)) it is now possible to extend it by new filters, or even overwrite a existing filter, to customize your Elasticsearch request-bodies. + +So, this way you can add custom Elasticsearch queries to the query-chain and still use the notation of `SearchQuery` in the Vue Storefront. + +> **Note:** This will only work from `storefront-query-builder` version `1.0.0` and `vue-storefront` version `1.12.2`. + +### Usecases + +One usecases where this feature would come in handy is for example if you like to add complex queries on multiple points in your source code. Using the following technique you can just add a custom filter to your `SearchQuery` in a single line inside your VSF source-code using the `query.applyFilter(...)` method and then add the complex logic into your custom-filter inside the API. + +### Registering a new filter + +The `vue-storefront-api` will only try to load filters that are registered in the configs. The extension/module, that contains the filter, must be enabled and the new filter module-classes needs to be registered in its extension config inside the `catalogFilter` array. The filter files must be located inside `filter/catalog/` of your module folder. + +For example: If you have a module called `extend-catalog` with a filter called `StockFilter`, the file path to filter would be `src/api/extensions/extend-catalog/filter/catalog/StockFilter.ts` and the config would look like: +``` +{ + "registeredExtensions": [ "extend-catalog" ], + "extensions": { + "extend-catalog": { + "catalogFilter": [ "StockFilter" ] + } + } +} +``` + +### Filter module-class properties + +The filter can contain four different properties. Followed a short explaination, what they are doing. + +* `check` – This method checks the condition that be must matched to execute the filter. The first valid filter is executed – all afterwards are ignored. +* `priority` – This is the priority in which the filters are going to be called. The sort is lower to higher. +* `mutator` – The mutator method is in charge of prehandling the filter value, to e.g. set defaults or check and change the type. +* `filter` – This method contains the query logic we wan't to add and mutates the `bodybuilder` query-chain. + +### Example + +Lets assume we like to add a possibility to add a default set of product-attribute filters we can apply to each `SearchQuery` without repeating ourselfs in source-code. So, for example, it should filter for two `color`'s and a specific `cut` to supply a filter for spring-coloured short's we implement at several places in our VSF. + +#### Changes in `vue-storefront` repository + +The query in the VSF code would look like this (that's it on the VSF side): +```js +import { SearchQuery } from 'storefront-query-builder' +import { quickSearchByQuery } from '@vue-storefront/core/lib/search' + +//... + +const query = new SearchQuery() +query.applyFilter({ key: 'spring-shorts', value: 'male', scope: 'default' }) +const products = await dispatch('product/list', { query, size: 5 }) +``` + +#### Changes in `vue-storefront-api` repository + +In the `vue-storefront-api` we are going to add the real filter/query magic. +There is already an example module called `example-custom-filter` which we are going to use for our filter. + +As you look inside its module folder `src/api/extensions/example-custom-filter/`, you will find a child folder `filter/catalog/` with all existing custom filters for this module. Inside this folder we are going to duplicate the existing `SampleFilter.ts` into another one called `SpringShorts.ts` – this is our new custom filter module-class. + +This file needs to be registered in the config JSON to let the API know that there is a new custom filter inside our extension. +Therefore you open your `default.json` or specific config JSON file and add our new filename `SpringShorts` to the config node `extensions.example-custom-filter.catalogFilter` array. + +Our `SpringShorts.ts` contains an object that contains [four properties](#filter-module-class-properties): `priority`, `check`, `filter`, `mutator`. We don't need a `mutator` nor `priority`, so we can remove these lines. `check` and `filter` needs to be changed to fulfill our needs. So, this is how our filter finally looks like: + +```js +import { FilterInterface } from 'storefront-query-builder' + +const filter: FilterInterface = { + check: ({ attribute }) => attribute === 'spring-shorts', + filter ({ value, attribute, operator, queryChain }) { + return queryChain + .filter('terms', 'pants', [ 'shorts' ]) + .filter('terms', 'cut', [ 1, 2 ]) + .filter('terms', 'color', [ 3, 4 ]) + .filter('terms', 'gender', [ value ]) + } +} + +export default filter +``` + +Inside `check` we tell the filter to just be applied if the attribute is named exactly `spring-shorts`. + +Inside `filter` we extend the Elasticsearch query-chain by our desired filters, using the `bodybuilder` library syntax. + +That's it, now we are able to filter by a complex query in only one line inside VSF. + +## 7. Anti-patterns & Common pitfalls ### 1. Preparation ### 2. Recipe @@ -699,7 +787,7 @@ _[INSERT VIDEO HERE]_

-## 7. Building a module from A to Z in an iteration +## 8. Building a module from A to Z in an iteration ### 1. Preparation @@ -709,7 +797,7 @@ _[INSERT VIDEO HERE]_

-## 8. Deprecated legacy of Modules +## 9. Deprecated legacy of Modules In this recipe, we will take a review of how to deal with modules in an old fashioned way , just in case you really need it. ### 1. Preparation @@ -720,7 +808,7 @@ In this recipe, we will take a review of how to deal with modules in an old fash
-## 9. Converting old modules to new modules +## 10. Converting old modules to new modules There are useful modules out there already developed in the old way. ### 1. Preparation From 47e836fb7ed93f473114293f8540a05733e2f886 Mon Sep 17 00:00:00 2001 From: cewald Date: Thu, 9 Apr 2020 11:54:56 +0000 Subject: [PATCH 6/6] Update `yarn.lock` --- yarn.lock | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index e16452b6af..dac8412f70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2321,6 +2321,16 @@ "@typescript-eslint/typescript-estree" "1.13.0" eslint-scope "^4.0.0" +"@typescript-eslint/experimental-utils@2.27.0": + version "2.27.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.27.0.tgz#801a952c10b58e486c9a0b36cf21e2aab1e9e01a" + integrity sha512-vOsYzjwJlY6E0NJRXPTeCGqjv5OHgRU1kzxHKWJVPjDYGbPgLudBXjIlc+OD1hDBZ4l1DLbOc5VjofKahsu9Jw== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.27.0" + eslint-scope "^5.0.0" + eslint-utils "^2.0.0" + "@typescript-eslint/parser@^1.7.1-alpha.17": version "1.13.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-1.13.0.tgz#61ac7811ea52791c47dc9fd4dd4a184fae9ac355" @@ -2331,6 +2341,16 @@ "@typescript-eslint/typescript-estree" "1.13.0" eslint-visitor-keys "^1.0.0" +"@typescript-eslint/parser@^2.26.0": + version "2.27.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.27.0.tgz#d91664335b2c46584294e42eb4ff35838c427287" + integrity sha512-HFUXZY+EdwrJXZo31DW4IS1ujQW3krzlRjBrFRrJcMDh0zCu107/nRfhk/uBasO8m0NVDbBF5WZKcIUMRO7vPg== + dependencies: + "@types/eslint-visitor-keys" "^1.0.0" + "@typescript-eslint/experimental-utils" "2.27.0" + "@typescript-eslint/typescript-estree" "2.27.0" + eslint-visitor-keys "^1.1.0" + "@typescript-eslint/typescript-estree@1.13.0": version "1.13.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz#8140f17d0f60c03619798f1d628b8434913dc32e" @@ -2339,6 +2359,19 @@ lodash.unescape "4.0.1" semver "5.5.0" +"@typescript-eslint/typescript-estree@2.27.0": + version "2.27.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.27.0.tgz#a288e54605412da8b81f1660b56c8b2e42966ce8" + integrity sha512-t2miCCJIb/FU8yArjAvxllxbTiyNqaXJag7UOpB5DVoM3+xnjeOngtqlJkLRnMtzaRcJhe3CIR9RmL40omubhg== + dependencies: + debug "^4.1.1" + eslint-visitor-keys "^1.1.0" + glob "^7.1.6" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^6.3.0" + tsutils "^3.17.1" + "@vue/babel-helper-vue-jsx-merge-props@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz#048fe579958da408fb7a8b2a3ec050b50a661040" @@ -6751,6 +6784,14 @@ eslint-scope@^4.0.0, eslint-scope@^4.0.3: esrecurse "^4.1.0" estraverse "^4.1.1" +eslint-scope@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" + integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + eslint-utils@^1.3.1: version "1.4.3" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" @@ -6758,6 +6799,13 @@ eslint-utils@^1.3.1: dependencies: eslint-visitor-keys "^1.1.0" +eslint-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.0.0.tgz#7be1cc70f27a72a76cd14aa698bcabed6890e1cd" + integrity sha512-0HCPuJv+7Wv1bACm8y5/ECVfYdfsAm9xmVb7saeFlxjPYALefjhbYoCkBjPdPzGH8wWyTpAez82Fh3VKYEZ8OA== + dependencies: + eslint-visitor-keys "^1.1.0" + eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" @@ -14607,11 +14655,12 @@ stealthy-require@^1.1.1: resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= -storefront-query-builder@^0.0.9: - version "0.0.9" - resolved "https://registry.yarnpkg.com/storefront-query-builder/-/storefront-query-builder-0.0.9.tgz#e2733851d08dd98566d4f81281326658c74d660a" - integrity sha512-c6zIRyJwFQJtlLG1R+qlrHHDea1Z4O/EJbTvjbGRHIrsMKav2XfX5GzEai7SWU4+GomySU9eIR/M2YAJhVo5rw== +storefront-query-builder@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/storefront-query-builder/-/storefront-query-builder-1.0.0.tgz#8bab0b6bd61a574dfa913cad5c2fdd17858f8b07" + integrity sha512-j0JhHOYhcfDcsBUMYWVJ0UApQDOem5zo20XikxJcxfFEcM5Et1Z16674wx3++KrWCbU5raEMgHvqEPxkDFajdw== dependencies: + "@typescript-eslint/parser" "^2.26.0" clone-deep "^4.0.1" stream-browserify@^2.0.1: @@ -15443,7 +15492,7 @@ tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== -tsutils@^3.7.0: +tsutils@^3.17.1, tsutils@^3.7.0: version "3.17.1" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==