From dfc09a81a29a0d435de9caa840bea9cfbd2087ca Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 17 Nov 2023 14:15:44 +0100 Subject: [PATCH 01/35] chore: Update CI workflows to latest Node versions --- .github/workflows/build_and_test.yml | 26 +---------------------- .github/workflows/publish_and_install.yml | 17 +-------------- 2 files changed, 2 insertions(+), 41 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index aef31ec5c8..ec427388c5 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -14,7 +14,7 @@ on: - minor env: CI: true - node: 18.x + node: 20.x jobs: build: name: build @@ -25,15 +25,7 @@ jobs: uses: actions/setup-node@v3 with: node-version: ${{ env.node }} -# - uses: actions/cache@v3 -# id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) -# with: -# path: '**/node_modules' -# key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} -# restore-keys: | -# ${{ runner.os }}-yarn- - name: Yarn install -# if: steps.yarn-cache.outputs.cache-hit != 'true' run: yarn install - name: Build run: yarn build @@ -46,15 +38,7 @@ jobs: uses: actions/setup-node@v3 with: node-version: ${{ env.node }} -# - uses: actions/cache@v3 -# id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) -# with: -# path: '**/node_modules' -# key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} -# restore-keys: | -# ${{ runner.os }}-yarn- - name: Yarn install -# if: steps.yarn-cache.outputs.cache-hit != 'true' run: yarn install --prefer-offline - name: Build run: yarn lerna run ci @@ -113,15 +97,7 @@ jobs: uses: actions/setup-node@v3 with: node-version: ${{ env.node }} -# - uses: actions/cache@v3 -# id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) -# with: -# path: '**/node_modules' -# key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} -# restore-keys: | -# ${{ runner.os }}-yarn- - name: Yarn install -# if: steps.yarn-cache.outputs.cache-hit != 'true' run: yarn install --prefer-offline - name: Build run: yarn lerna run ci diff --git a/.github/workflows/publish_and_install.yml b/.github/workflows/publish_and_install.yml index a649c5e77e..36ee877a58 100644 --- a/.github/workflows/publish_and_install.yml +++ b/.github/workflows/publish_and_install.yml @@ -20,11 +20,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - # Temporarily disabled Node v18 because of this issue: - # https://github.com/vendure-ecommerce/vendure/actions/runs/5200017548/jobs/9378196658#step:4:48 - # which is related to our Verdaccio setup. Will need some investigation. - #node-version: [16.x, 18.x] - node-version: [16.x] + node-version: [18.x, 20.x] fail-fast: false steps: - uses: actions/checkout@v3 @@ -46,18 +42,7 @@ jobs: - name: Windows dependencies if: matrix.os == 'windows-latest' run: npm install -g @angular/cli -# - name: Get yarn cache directory path -# id: yarn-cache-dir-path -# run: echo "::set-output name=dir::$(yarn cache dir)" -# - uses: actions/cache@v3 -# id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) -# with: -# path: '**/node_modules' -# key: ${{ runner.os }}-${{ matrix.node-version }}-yarn-${{ hashFiles('**/yarn.lock') }} -# restore-keys: | -# ${{ runner.os }}-${{ matrix.node-version }}-yarn- - name: Yarn install -# if: steps.yarn-cache.outputs.cache-hit != 'true' run: | yarn config set unsafe-perm true yarn install --network-timeout 1000000 --prefer-offline From 1b2cc04211602c23e90d9a856592ee7ed7160f27 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 17 Nov 2023 14:34:16 +0100 Subject: [PATCH 02/35] chore: Attempt to get publish & install workflow working again The `npm-auth-to-token` dependency is probably breaking. It has no readme or repo link: https://www.npmjs.com/package/npm-auth-to-token and has not been updated for 6 years. The issue is discussed here: https://github.com/ethereumjs/ethereumjs-monorepo/issues/1537 and I am attempting the fix as seen here: https://github.com/ethereumjs/ethereumjs-monorepo/pull/1579/files#diff-02c8f04118065b423b5b599fe96dc405107c1a84c7840d9ec3b04627021aef69 --- .github/workflows/publish_and_install.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish_and_install.yml b/.github/workflows/publish_and_install.yml index 36ee877a58..e481536526 100644 --- a/.github/workflows/publish_and_install.yml +++ b/.github/workflows/publish_and_install.yml @@ -33,12 +33,16 @@ jobs: npm install -g verdaccio npm install -g verdaccio-auth-memory npm install -g verdaccio-memory - npm install -g npm-auth-to-token@1.0.0 tmp_registry_log=`mktemp` mkdir -p $HOME/.config/verdaccio cp -v ./.github/workflows/verdaccio/config.yaml $HOME/.config/verdaccio/config.yaml nohup verdaccio --config $HOME/.config/verdaccio/config.yaml &>$tmp_registry_log & - npm-auth-to-token -u test -p test -e test@test.com -r http://0.0.0.0:4873 + TOKEN=$(curl -XPUT \ + -H "Content-type: application/json" \ + -d '{ "name": "test", "password": "test" }' \ + 'http://localhost:4873/-/user/org.couchdb.user:test') + npm set registry "http://localhost:4873" + npm set //localhost:4873/:_authToken $TOKEN - name: Windows dependencies if: matrix.os == 'windows-latest' run: npm install -g @angular/cli From f700b89750889fef8b81a9bf830f0b11dabda094 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 17 Nov 2023 14:52:46 +0100 Subject: [PATCH 03/35] chore: Add note on security advisory to readme --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe725a5473..0e435f5b8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## 2.1.3 (2023-11-17) +#### Security + +This patch addresses the following security advisory: https://github.com/vendure-ecommerce/vendure/security/advisories/GHSA-wm63-7627-ch33 #### Fixes From 975244049860f20765025b68e53974c17a9445ad Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 17 Nov 2023 14:58:14 +0100 Subject: [PATCH 04/35] chore: Publish and install fix 2 --- .github/workflows/publish_and_install.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish_and_install.yml b/.github/workflows/publish_and_install.yml index e481536526..918bbe9cf4 100644 --- a/.github/workflows/publish_and_install.yml +++ b/.github/workflows/publish_and_install.yml @@ -33,10 +33,12 @@ jobs: npm install -g verdaccio npm install -g verdaccio-auth-memory npm install -g verdaccio-memory + npm install -g wait-on tmp_registry_log=`mktemp` mkdir -p $HOME/.config/verdaccio cp -v ./.github/workflows/verdaccio/config.yaml $HOME/.config/verdaccio/config.yaml nohup verdaccio --config $HOME/.config/verdaccio/config.yaml &>$tmp_registry_log & + wait-on http://localhost:4873 TOKEN=$(curl -XPUT \ -H "Content-type: application/json" \ -d '{ "name": "test", "password": "test" }' \ From b4232c9d5414749b9ccee9c9df981d721ad66dad Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 17 Nov 2023 15:53:10 +0100 Subject: [PATCH 05/35] chore: Publish and install fix 3 --- .github/workflows/publish_and_install.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish_and_install.yml b/.github/workflows/publish_and_install.yml index 918bbe9cf4..209acab2df 100644 --- a/.github/workflows/publish_and_install.yml +++ b/.github/workflows/publish_and_install.yml @@ -31,13 +31,13 @@ jobs: - name: Install Verdaccio run: | npm install -g verdaccio - npm install -g verdaccio-auth-memory - npm install -g verdaccio-memory +# npm install -g verdaccio-auth-memory +# npm install -g verdaccio-memory npm install -g wait-on - tmp_registry_log=`mktemp` - mkdir -p $HOME/.config/verdaccio - cp -v ./.github/workflows/verdaccio/config.yaml $HOME/.config/verdaccio/config.yaml - nohup verdaccio --config $HOME/.config/verdaccio/config.yaml &>$tmp_registry_log & +# tmp_registry_log=`mktemp` +# mkdir -p $HOME/.config/verdaccio +# cp -v ./.github/workflows/verdaccio/config.yaml $HOME/.config/verdaccio/config.yaml + nohup verdaccio & wait-on http://localhost:4873 TOKEN=$(curl -XPUT \ -H "Content-type: application/json" \ From a9ec6b1f4396d2ad9290a2e6c650284554bed8f7 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 17 Nov 2023 16:00:08 +0100 Subject: [PATCH 06/35] chore: Publish and install fix 4 --- .github/workflows/publish_and_install.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/publish_and_install.yml b/.github/workflows/publish_and_install.yml index 209acab2df..0ebe6d825d 100644 --- a/.github/workflows/publish_and_install.yml +++ b/.github/workflows/publish_and_install.yml @@ -31,12 +31,7 @@ jobs: - name: Install Verdaccio run: | npm install -g verdaccio -# npm install -g verdaccio-auth-memory -# npm install -g verdaccio-memory npm install -g wait-on -# tmp_registry_log=`mktemp` -# mkdir -p $HOME/.config/verdaccio -# cp -v ./.github/workflows/verdaccio/config.yaml $HOME/.config/verdaccio/config.yaml nohup verdaccio & wait-on http://localhost:4873 TOKEN=$(curl -XPUT \ From d885adb511affdbf65f6bfa8cb32955a11b821b9 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 17 Nov 2023 16:08:26 +0100 Subject: [PATCH 07/35] chore: Publish and install fix 5 --- .github/workflows/publish_and_install.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish_and_install.yml b/.github/workflows/publish_and_install.yml index 0ebe6d825d..d18dc70a54 100644 --- a/.github/workflows/publish_and_install.yml +++ b/.github/workflows/publish_and_install.yml @@ -30,14 +30,17 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install Verdaccio run: | + apt-get update + apt-get install jq npm install -g verdaccio npm install -g wait-on nohup verdaccio & wait-on http://localhost:4873 - TOKEN=$(curl -XPUT \ + TOKEN_RES=$(curl -XPUT \ -H "Content-type: application/json" \ -d '{ "name": "test", "password": "test" }' \ 'http://localhost:4873/-/user/org.couchdb.user:test') + TOKEN=$(echo "$TOKEN_RES" | jq -r '.token') npm set registry "http://localhost:4873" npm set //localhost:4873/:_authToken $TOKEN - name: Windows dependencies From f66294224d582cc65de0fd8dd5ce8c51934e23c5 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 17 Nov 2023 16:14:27 +0100 Subject: [PATCH 08/35] chore: Publish and install fix 6 --- .github/workflows/publish_and_install.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/publish_and_install.yml b/.github/workflows/publish_and_install.yml index d18dc70a54..601f011f94 100644 --- a/.github/workflows/publish_and_install.yml +++ b/.github/workflows/publish_and_install.yml @@ -30,8 +30,6 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install Verdaccio run: | - apt-get update - apt-get install jq npm install -g verdaccio npm install -g wait-on nohup verdaccio & From b40aa88809fd154a0f46abb5acd33bc0328b52d8 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 17 Nov 2023 16:26:55 +0100 Subject: [PATCH 09/35] chore: Publish and install fix 7 --- .github/workflows/publish_and_install.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish_and_install.yml b/.github/workflows/publish_and_install.yml index 601f011f94..9328c71ea2 100644 --- a/.github/workflows/publish_and_install.yml +++ b/.github/workflows/publish_and_install.yml @@ -32,15 +32,16 @@ jobs: run: | npm install -g verdaccio npm install -g wait-on - nohup verdaccio & + tmp_registry_log=`mktemp` + nohup verdaccio &>$tmp_registry_log & wait-on http://localhost:4873 TOKEN_RES=$(curl -XPUT \ -H "Content-type: application/json" \ -d '{ "name": "test", "password": "test" }' \ 'http://localhost:4873/-/user/org.couchdb.user:test') TOKEN=$(echo "$TOKEN_RES" | jq -r '.token') - npm set registry "http://localhost:4873" - npm set //localhost:4873/:_authToken $TOKEN + npm set registry "http://0.0.0.0:4873" + npm set //0.0.0.0:4873/:_authToken $TOKEN - name: Windows dependencies if: matrix.os == 'windows-latest' run: npm install -g @angular/cli @@ -52,7 +53,7 @@ jobs: CI: true - name: Publish to Verdaccio run: | - yarn lerna publish prepatch --preid ci --no-push --no-git-tag-version --no-commit-hooks --force-publish "*" --yes --dist-tag ci --registry http://0.0.0.0:4873 + yarn lerna publish prepatch --preid ci --no-push --no-git-tag-version --no-commit-hooks --force-publish "*" --yes --dist-tag ci --registry http://localhost:4873 - name: Install via @vendure/create run: | mkdir -p $HOME/install From 8d8ae8dd8ccc45c1b2b86c6d6f1d44d2796ee620 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 20 Nov 2023 09:17:54 +0100 Subject: [PATCH 10/35] docs: Remove misleading line on VendureEntityEvent --- packages/core/src/event-bus/vendure-entity-event.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/event-bus/vendure-entity-event.ts b/packages/core/src/event-bus/vendure-entity-event.ts index b2fa60dd20..71ecdce4ae 100644 --- a/packages/core/src/event-bus/vendure-entity-event.ts +++ b/packages/core/src/event-bus/vendure-entity-event.ts @@ -5,7 +5,6 @@ import { VendureEvent } from './vendure-event'; /** * @description * The base class for all entity events used by the EventBus system. - * * For event type `'updated'` the entity is the one before applying the patch (if not documented otherwise). * * For event type `'deleted'` the input will most likely be an `id: ID` * * @docsCategory events From f0a0e5d62655ddaf32f7fe14151c5adb8c5d6d1d Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 20 Nov 2023 10:17:42 +0100 Subject: [PATCH 11/35] chore: Publish and install fix 8 --- .github/workflows/publish_and_install.yml | 17 +++++++++++++---- .github/workflows/verdaccio/config.yaml | 2 +- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish_and_install.yml b/.github/workflows/publish_and_install.yml index 9328c71ea2..8542bddef3 100644 --- a/.github/workflows/publish_and_install.yml +++ b/.github/workflows/publish_and_install.yml @@ -16,6 +16,9 @@ defaults: shell: bash jobs: publish_install: + services: + verdaccio: + image: ./verdaccio/Dockerfile runs-on: ${{ matrix.os }} strategy: matrix: @@ -33,32 +36,38 @@ jobs: npm install -g verdaccio npm install -g wait-on tmp_registry_log=`mktemp` - nohup verdaccio &>$tmp_registry_log & + mkdir -p $HOME/.config/verdaccio + cp -v ./.github/workflows/verdaccio/config.yaml $HOME/.config/verdaccio/config.yaml + nohup verdaccio --config $HOME/.config/verdaccio/config.yaml & wait-on http://localhost:4873 TOKEN_RES=$(curl -XPUT \ -H "Content-type: application/json" \ -d '{ "name": "test", "password": "test" }' \ 'http://localhost:4873/-/user/org.couchdb.user:test') TOKEN=$(echo "$TOKEN_RES" | jq -r '.token') - npm set registry "http://0.0.0.0:4873" - npm set //0.0.0.0:4873/:_authToken $TOKEN + npm set //localhost:4873/:_authToken $TOKEN - name: Windows dependencies if: matrix.os == 'windows-latest' run: npm install -g @angular/cli - name: Yarn install run: | + npm i -g yarn yarn config set unsafe-perm true yarn install --network-timeout 1000000 --prefer-offline env: CI: true - name: Publish to Verdaccio run: | + nohup verdaccio --config $HOME/.config/verdaccio/config.yaml & + wait-on http://localhost:4873 yarn lerna publish prepatch --preid ci --no-push --no-git-tag-version --no-commit-hooks --force-publish "*" --yes --dist-tag ci --registry http://localhost:4873 - name: Install via @vendure/create run: | mkdir -p $HOME/install cd $HOME/install - npm set registry=http://0.0.0.0:4873 + nohup verdaccio --config $HOME/.config/verdaccio/config.yaml & + wait-on http://localhost:4873 + npm set registry=http://localhost:4873 npm dist-tag ls @vendure/create npx @vendure/create@ci test-app --ci --use-npm --log-level info - name: Server smoke tests diff --git a/.github/workflows/verdaccio/config.yaml b/.github/workflows/verdaccio/config.yaml index caeb2fa0c5..a86438ccad 100644 --- a/.github/workflows/verdaccio/config.yaml +++ b/.github/workflows/verdaccio/config.yaml @@ -10,7 +10,7 @@ plugins: ./plugins max_body_size: 1000mb web: # WebUI is enabled as default, if you want disable it, just uncomment this line - enable: false + enable: true title: Verdaccio auth: From f01533a44abd3d0216e1439d6b3ba648cd663cdf Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 20 Nov 2023 10:41:13 +0100 Subject: [PATCH 12/35] chore: Publish and install fix 9 --- .github/workflows/publish_and_install.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/publish_and_install.yml b/.github/workflows/publish_and_install.yml index 8542bddef3..f972543042 100644 --- a/.github/workflows/publish_and_install.yml +++ b/.github/workflows/publish_and_install.yml @@ -16,9 +16,6 @@ defaults: shell: bash jobs: publish_install: - services: - verdaccio: - image: ./verdaccio/Dockerfile runs-on: ${{ matrix.os }} strategy: matrix: From cf301eb788fca9ea9682e957369880ec6cf64804 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 20 Nov 2023 11:29:02 +0100 Subject: [PATCH 13/35] fix(core): Relax validation of custom process states The last release caused an error to be thrown on bootstrap in the case of unreachable states. There are however valid cases for making states unreachable, so now we will just print a warning. --- .../validate-transition-definition.spec.ts | 2 +- .../validate-transition-definition.ts | 24 +++++++++---------- .../fulfillment-state-machine.ts | 3 +++ .../order-state-machine.ts | 3 +++ .../payment-state-machine.ts | 3 +++ 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/packages/core/src/common/finite-state-machine/validate-transition-definition.spec.ts b/packages/core/src/common/finite-state-machine/validate-transition-definition.spec.ts index 5772e662b5..87dfe755ee 100644 --- a/packages/core/src/common/finite-state-machine/validate-transition-definition.spec.ts +++ b/packages/core/src/common/finite-state-machine/validate-transition-definition.spec.ts @@ -74,7 +74,7 @@ describe('FSM validateTransitionDefinition()', () => { const result = validateTransitionDefinition(valid, 'Start'); - expect(result.valid).toBe(false); + expect(result.valid).toBe(true); expect(result.error).toBe('The following states are unreachable: Unreachable'); }); diff --git a/packages/core/src/common/finite-state-machine/validate-transition-definition.ts b/packages/core/src/common/finite-state-machine/validate-transition-definition.ts index f37ed7b55d..aa5c49e6a2 100644 --- a/packages/core/src/common/finite-state-machine/validate-transition-definition.ts +++ b/packages/core/src/common/finite-state-machine/validate-transition-definition.ts @@ -53,17 +53,15 @@ export function validateTransitionDefinition( }; } - if (!allStatesReached()) { - return { - valid: false, - error: `The following states are unreachable: ${Object.entries(result) - .filter(([s, v]) => !(v as ValidationResult).reachable) - .map(([s]) => s) - .join(', ')}`, - }; - } else { - return { - valid: true, - }; - } + const error = !allStatesReached() + ? `The following states are unreachable: ${Object.entries(result) + .filter(([s, v]) => !(v as ValidationResult).reachable) + .map(([s]) => s) + .join(', ')}` + : undefined; + + return { + valid: true, + error, + }; } diff --git a/packages/core/src/service/helpers/fulfillment-state-machine/fulfillment-state-machine.ts b/packages/core/src/service/helpers/fulfillment-state-machine/fulfillment-state-machine.ts index 99bf3f753f..03bfc149c7 100644 --- a/packages/core/src/service/helpers/fulfillment-state-machine/fulfillment-state-machine.ts +++ b/packages/core/src/service/helpers/fulfillment-state-machine/fulfillment-state-machine.ts @@ -63,6 +63,9 @@ export class FulfillmentStateMachine { Logger.error(`The fulfillment process has an invalid configuration:`); throw new Error(validationResult.error); } + if (validationResult.valid && validationResult.error) { + Logger.warn(`Fulfillment process: ${validationResult.error}`); + } return { transitions: allTransitions, onTransitionStart: async (fromState, toState, data) => { diff --git a/packages/core/src/service/helpers/order-state-machine/order-state-machine.ts b/packages/core/src/service/helpers/order-state-machine/order-state-machine.ts index 6c1a985c4d..23e27acf42 100644 --- a/packages/core/src/service/helpers/order-state-machine/order-state-machine.ts +++ b/packages/core/src/service/helpers/order-state-machine/order-state-machine.ts @@ -56,6 +56,9 @@ export class OrderStateMachine { Logger.error(`The order process has an invalid configuration:`); throw new Error(validationResult.error); } + if (validationResult.valid && validationResult.error) { + Logger.warn(`Order process: ${validationResult.error}`); + } return { transitions: allTransitions, onTransitionStart: async (fromState, toState, data) => { diff --git a/packages/core/src/service/helpers/payment-state-machine/payment-state-machine.ts b/packages/core/src/service/helpers/payment-state-machine/payment-state-machine.ts index d96e74d030..3a59dc1746 100644 --- a/packages/core/src/service/helpers/payment-state-machine/payment-state-machine.ts +++ b/packages/core/src/service/helpers/payment-state-machine/payment-state-machine.ts @@ -58,6 +58,9 @@ export class PaymentStateMachine { Logger.error(`The payment process has an invalid configuration:`); throw new Error(validationResult.error); } + if (validationResult.valid && validationResult.error) { + Logger.warn(`Payment process: ${validationResult.error}`); + } return { transitions: allTransitions, onTransitionStart: async (fromState, toState, data) => { From 3828cdfd857445e14ac42a5f534b762f58174329 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 20 Nov 2023 13:00:50 +0100 Subject: [PATCH 14/35] docs: Fix example for relation form input component --- .../guides/extending-the-admin-ui/custom-form-inputs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/guides/extending-the-admin-ui/custom-form-inputs/index.md b/docs/docs/guides/extending-the-admin-ui/custom-form-inputs/index.md index 5d012c9b78..55deb3aa77 100644 --- a/docs/docs/guides/extending-the-admin-ui/custom-form-inputs/index.md +++ b/docs/docs/guides/extending-the-admin-ui/custom-form-inputs/index.md @@ -201,7 +201,7 @@ import { Component, OnInit } from '@angular/core'; import { FormControl } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { RelationCustomFieldConfig } from '@vendure/common/lib/generated-types'; -import { CustomFieldControl, DataService, SharedModule } from '@vendure/admin-ui/core'; +import { FormInputComponent, DataService, SharedModule } from '@vendure/admin-ui/core'; import { Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; From 32e6e2bbf169c052aa825bcfdcc413bd318f3375 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 20 Nov 2023 14:33:06 +0100 Subject: [PATCH 15/35] docs: Fix ui detail component example --- .../extending-the-admin-ui/creating-detail-views/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/guides/extending-the-admin-ui/creating-detail-views/index.md b/docs/docs/guides/extending-the-admin-ui/creating-detail-views/index.md index b90b285f19..79c4c5b809 100644 --- a/docs/docs/guides/extending-the-admin-ui/creating-detail-views/index.md +++ b/docs/docs/guides/extending-the-admin-ui/creating-detail-views/index.md @@ -117,7 +117,7 @@ export class ReviewDetailComponent extends TypedBaseDetailComponent { From cdb801bfca60b6c90ac6ae30673c7aff88411b55 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 20 Nov 2023 15:05:25 +0100 Subject: [PATCH 16/35] docs: Add note on form-grid classes --- .../extending-the-admin-ui/ui-library/index.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/docs/guides/extending-the-admin-ui/ui-library/index.md b/docs/docs/guides/extending-the-admin-ui/ui-library/index.md index 08bc94cc05..58d718e164 100644 --- a/docs/docs/guides/extending-the-admin-ui/ui-library/index.md +++ b/docs/docs/guides/extending-the-admin-ui/ui-library/index.md @@ -223,6 +223,19 @@ export function DemoComponent() { +The `form-grid` class is used to lay out the form fields into a 2-column grid on larger screens, and a single column on smaller screens. +If you want to force a particular field to always take up the full width (i.e. to span 2 columns at all screen sizes), you can add the +`form-grid-span` class to that form field. + +```html +
+ // highlight-next-line + + + +
+``` + ## Cards Cards are used as a general-purpose container for page content, as a way to visually group related sets of components. From 63846f8a551e0da554b392eb012d29be25de44fe Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 20 Nov 2023 21:25:02 +0100 Subject: [PATCH 17/35] docs: Improve docs on custom permission usage --- .../custom-permissions/index.md | 2 +- .../nav-builder/nav-builder-types.ts | 28 ++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/docs/docs/guides/developer-guide/custom-permissions/index.md b/docs/docs/guides/developer-guide/custom-permissions/index.md index d3505420fa..261930ce93 100644 --- a/docs/docs/guides/developer-guide/custom-permissions/index.md +++ b/docs/docs/guides/developer-guide/custom-permissions/index.md @@ -81,7 +81,7 @@ For example, let's imagine we are creating a plugin which adds a new entity call ```ts title="src/plugins/product-review/constants.ts" import { CrudPermissionDefinition } from '@vendure/core'; -export const productReview = new CrudPermissionDefinition(ProductReview); +export const productReview = new CrudPermissionDefinition('ProductReview'); ``` These permissions can then be used in our resolver: diff --git a/packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder-types.ts b/packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder-types.ts index 29007a705d..2a30f00b19 100644 --- a/packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder-types.ts +++ b/packages/admin-ui/src/lib/core/src/providers/nav-builder/nav-builder-types.ts @@ -62,7 +62,18 @@ export interface NavMenuSection { displayMode?: 'regular' | 'settings'; /** * @description - * Control the display of this item based on the user permissions. + * Control the display of this item based on the user permissions. Note: if you attempt to pass a + * {@link PermissionDefinition} object, you will get a compilation error. Instead, pass the plain + * string version. For example, if the permission is defined as: + * ```ts + * export const MyPermission = new PermissionDefinition('ProductReview'); + * ``` + * then the generated permission strings will be: + * + * - `CreateProductReview` + * - `ReadProductReview` + * - `UpdateProductReview` + * - `DeleteProductReview` */ requiresPermission?: string | ((userPermissions: string[]) => boolean); collapsible?: boolean; @@ -116,6 +127,21 @@ export interface ActionBarItem { buttonColor?: 'primary' | 'success' | 'warning'; buttonStyle?: 'solid' | 'outline' | 'link'; icon?: string; + /** + * @description + * Control the display of this item based on the user permissions. Note: if you attempt to pass a + * {@link PermissionDefinition} object, you will get a compilation error. Instead, pass the plain + * string version. For example, if the permission is defined as: + * ```ts + * export const MyPermission = new PermissionDefinition('ProductReview'); + * ``` + * then the generated permission strings will be: + * + * - `CreateProductReview` + * - `ReadProductReview` + * - `UpdateProductReview` + * - `DeleteProductReview` + */ requiresPermission?: string | string[]; } From 9b6dce696fe251a609d99ac34e464a1dff1a01ad Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Tue, 21 Nov 2023 10:58:25 +0100 Subject: [PATCH 18/35] docs: Fix missing import in custom detail component example --- .../extending-the-admin-ui/custom-detail-components/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/guides/extending-the-admin-ui/custom-detail-components/index.md b/docs/docs/guides/extending-the-admin-ui/custom-detail-components/index.md index ef8ad447b2..11f727e5eb 100644 --- a/docs/docs/guides/extending-the-admin-ui/custom-detail-components/index.md +++ b/docs/docs/guides/extending-the-admin-ui/custom-detail-components/index.md @@ -23,7 +23,7 @@ Let's imagine that your project has an external content management system (CMS) ```ts title="src/plugins/cms/ui/components/product-info/product-info.component.ts" import { Component, OnInit } from '@angular/core'; -import { switchMap } from 'rxjs'; +import { Observable, switchMap } from 'rxjs'; import { FormGroup } from '@angular/forms'; import { DataService, CustomDetailComponent, SharedModule } from '@vendure/admin-ui/core'; import { CmsDataService } from '../../providers/cms-data.service'; From a09c2b2022a0380569f1c15dd5a11b1869fb8f63 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Tue, 21 Nov 2023 17:01:45 +0100 Subject: [PATCH 19/35] fix(core): Fix custom MoneyStrategy handling from plugins Closes #2527 --- packages/core/e2e/money-strategy.e2e-spec.ts | 69 ++++++++++++++++---- packages/core/src/bootstrap.ts | 2 +- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/packages/core/e2e/money-strategy.e2e-spec.ts b/packages/core/e2e/money-strategy.e2e-spec.ts index 05dd7bfe53..fb8006eaa3 100644 --- a/packages/core/e2e/money-strategy.e2e-spec.ts +++ b/packages/core/e2e/money-strategy.e2e-spec.ts @@ -1,8 +1,8 @@ -import { DefaultMoneyStrategy, Logger, mergeConfig, MoneyStrategy } from '@vendure/core'; +import { DefaultMoneyStrategy, Logger, mergeConfig, MoneyStrategy, VendurePlugin } from '@vendure/core'; import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing'; import path from 'path'; import { ColumnOptions } from 'typeorm'; -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; import { initialData } from '../../../e2e-common/e2e-initial-data'; import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config'; @@ -21,6 +21,7 @@ const orderGuard: ErrorResultGuard = createErr ); class CustomMoneyStrategy implements MoneyStrategy { + static transformerFromSpy = vi.fn(); readonly moneyColumnOptions: ColumnOptions = { type: 'bigint', transformer: { @@ -28,6 +29,7 @@ class CustomMoneyStrategy implements MoneyStrategy { return entityValue; }, from: (databaseValue: string): number => { + CustomMoneyStrategy.transformerFromSpy(databaseValue); if (databaseValue == null) { return databaseValue; } @@ -48,12 +50,18 @@ class CustomMoneyStrategy implements MoneyStrategy { } } +@VendurePlugin({ + configuration: config => { + config.entityOptions.moneyStrategy = new CustomMoneyStrategy(); + return config; + }, +}) +class MyPlugin {} + describe('Custom MoneyStrategy', () => { const { server, adminClient, shopClient } = createTestEnvironment( mergeConfig(testConfig(), { - entityOptions: { - moneyStrategy: new CustomMoneyStrategy(), - }, + plugins: [MyPlugin], }), ); @@ -74,6 +82,8 @@ describe('Custom MoneyStrategy', () => { }); it('check initial prices', async () => { + expect(CustomMoneyStrategy.transformerFromSpy).toHaveBeenCalledTimes(0); + const { productVariants } = await adminClient.query< Codegen.GetProductVariantListQuery, Codegen.GetProductVariantListQueryVariables @@ -91,6 +101,8 @@ describe('Custom MoneyStrategy', () => { cheapVariantId = productVariants.items[0].id; expensiveVariantId = productVariants.items[1].id; + + expect(CustomMoneyStrategy.transformerFromSpy).toHaveBeenCalledTimes(6); }); // https://github.com/vendure-ecommerce/vendure/issues/838 @@ -127,9 +139,44 @@ describe('Custom MoneyStrategy', () => { expect(addItemToOrder.lines[0].linePriceWithTax).toBe(372); }); }); - -class CustomRoundingStrategy extends DefaultMoneyStrategy { - round(value: number): number { - return value; - } -} +// +// +// describe('custom MoneyStrategy as part of a plugin', () => { +// const testConfigValue = testConfig(); +// delete testConfigValue.entityOptions.moneyStrategy; +// const { server, adminClient, shopClient } = createTestEnvironment( +// mergeConfig(testConfigValue, { +// plugins: [MyPlugin], +// }), +// ); +// +// beforeAll(async () => { +// await server.init({ +// initialData, +// productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-money-handling.csv'), +// customerCount: 1, +// }); +// await adminClient.asSuperAdmin(); +// }, TEST_SETUP_TIMEOUT_MS); +// +// afterAll(async () => { +// await server.destroy(); +// }); +// +// it('invokes the transformer', async () => { +// CustomMoneyStrategy.transformerFromSpy.mockReset(); +// +// expect(CustomMoneyStrategy.transformerFromSpy).toHaveBeenCalledTimes(0); +// +// await shopClient.asAnonymousUser(); +// const { addItemToOrder } = await shopClient.query< +// AddItemToOrderMutation, +// AddItemToOrderMutationVariables +// >(ADD_ITEM_TO_ORDER, { +// productVariantId: 'T_1', +// quantity: 2, +// }); +// +// expect(CustomMoneyStrategy.transformerFromSpy).toHaveBeenCalledTimes(2); +// }); +// }); diff --git a/packages/core/src/bootstrap.ts b/packages/core/src/bootstrap.ts index b4cb6c5e87..43cfa3c3de 100644 --- a/packages/core/src/bootstrap.ts +++ b/packages/core/src/bootstrap.ts @@ -147,6 +147,7 @@ export async function preBootstrapConfig( }); let config = getConfig(); + config = await runPluginConfigurations(config); const entityIdStrategy = config.entityOptions.entityIdStrategy ?? config.entityIdStrategy; setEntityIdStrategy(entityIdStrategy, entities); const moneyStrategy = config.entityOptions.moneyStrategy; @@ -156,7 +157,6 @@ export async function preBootstrapConfig( process.exitCode = 1; throw new Error('CustomFields config error:\n- ' + customFieldValidationResult.errors.join('\n- ')); } - config = await runPluginConfigurations(config); registerCustomEntityFields(config); await runEntityMetadataModifiers(config); setExposedHeaders(config); From fc70230fa5db5e934bcf53e9c4dbd09bbe081b73 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Tue, 21 Nov 2023 17:11:23 +0100 Subject: [PATCH 20/35] chore(core): Tidy up e2e test file --- packages/core/e2e/money-strategy.e2e-spec.ts | 41 -------------------- 1 file changed, 41 deletions(-) diff --git a/packages/core/e2e/money-strategy.e2e-spec.ts b/packages/core/e2e/money-strategy.e2e-spec.ts index fb8006eaa3..dba876be27 100644 --- a/packages/core/e2e/money-strategy.e2e-spec.ts +++ b/packages/core/e2e/money-strategy.e2e-spec.ts @@ -139,44 +139,3 @@ describe('Custom MoneyStrategy', () => { expect(addItemToOrder.lines[0].linePriceWithTax).toBe(372); }); }); -// -// -// describe('custom MoneyStrategy as part of a plugin', () => { -// const testConfigValue = testConfig(); -// delete testConfigValue.entityOptions.moneyStrategy; -// const { server, adminClient, shopClient } = createTestEnvironment( -// mergeConfig(testConfigValue, { -// plugins: [MyPlugin], -// }), -// ); -// -// beforeAll(async () => { -// await server.init({ -// initialData, -// productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-money-handling.csv'), -// customerCount: 1, -// }); -// await adminClient.asSuperAdmin(); -// }, TEST_SETUP_TIMEOUT_MS); -// -// afterAll(async () => { -// await server.destroy(); -// }); -// -// it('invokes the transformer', async () => { -// CustomMoneyStrategy.transformerFromSpy.mockReset(); -// -// expect(CustomMoneyStrategy.transformerFromSpy).toHaveBeenCalledTimes(0); -// -// await shopClient.asAnonymousUser(); -// const { addItemToOrder } = await shopClient.query< -// AddItemToOrderMutation, -// AddItemToOrderMutationVariables -// >(ADD_ITEM_TO_ORDER, { -// productVariantId: 'T_1', -// quantity: 2, -// }); -// -// expect(CustomMoneyStrategy.transformerFromSpy).toHaveBeenCalledTimes(2); -// }); -// }); From 3d6edb5d704cae4e61600f536f1b04d5799e4be7 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Tue, 21 Nov 2023 17:37:19 +0100 Subject: [PATCH 21/35] fix(core): Fix i18n custom fields in Promotion & PaymentMethod --- packages/core/src/entity/register-custom-entity-fields.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/entity/register-custom-entity-fields.ts b/packages/core/src/entity/register-custom-entity-fields.ts index 6a869eb0f9..6d7b357d76 100644 --- a/packages/core/src/entity/register-custom-entity-fields.ts +++ b/packages/core/src/entity/register-custom-entity-fields.ts @@ -35,6 +35,7 @@ import { CustomOrderFields, CustomOrderLineFields, CustomPaymentMethodFields, + CustomPaymentMethodFieldsTranslation, CustomProductFields, CustomProductFieldsTranslation, CustomProductOptionFields, @@ -44,6 +45,7 @@ import { CustomProductVariantFields, CustomProductVariantFieldsTranslation, CustomPromotionFields, + CustomPromotionFieldsTranslation, CustomRegionFields, CustomRegionFieldsTranslation, CustomSellerFields, @@ -274,6 +276,7 @@ export function registerCustomEntityFields(config: VendureConfig) { registerCustomFieldsForEntity(config, 'Order', CustomOrderFields); registerCustomFieldsForEntity(config, 'OrderLine', CustomOrderLineFields); registerCustomFieldsForEntity(config, 'PaymentMethod', CustomPaymentMethodFields); + registerCustomFieldsForEntity(config, 'PaymentMethod', CustomPaymentMethodFieldsTranslation, true); registerCustomFieldsForEntity(config, 'Product', CustomProductFields); registerCustomFieldsForEntity(config, 'Product', CustomProductFieldsTranslation, true); registerCustomFieldsForEntity(config, 'ProductOption', CustomProductOptionFields); @@ -288,6 +291,7 @@ export function registerCustomEntityFields(config: VendureConfig) { registerCustomFieldsForEntity(config, 'ProductVariant', CustomProductVariantFields); registerCustomFieldsForEntity(config, 'ProductVariant', CustomProductVariantFieldsTranslation, true); registerCustomFieldsForEntity(config, 'Promotion', CustomPromotionFields); + registerCustomFieldsForEntity(config, 'Promotion', CustomPromotionFieldsTranslation, true); registerCustomFieldsForEntity(config, 'TaxCategory', CustomTaxCategoryFields); registerCustomFieldsForEntity(config, 'TaxRate', CustomTaxRateFields); registerCustomFieldsForEntity(config, 'User', CustomUserFields); From d665ec66fa6ab99a5266e8195218ddc885280346 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Tue, 21 Nov 2023 17:37:49 +0100 Subject: [PATCH 22/35] fix(admin-ui): Fix localized custom fields in Promotion & PaymentMethod --- .../promotion-detail/promotion-detail.component.ts | 7 ++++++- .../payment-method-detail.component.ts | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/admin-ui/src/lib/marketing/src/components/promotion-detail/promotion-detail.component.ts b/packages/admin-ui/src/lib/marketing/src/components/promotion-detail/promotion-detail.component.ts index a4b94494bc..6f349d5055 100644 --- a/packages/admin-ui/src/lib/marketing/src/components/promotion-detail/promotion-detail.component.ts +++ b/packages/admin-ui/src/lib/marketing/src/components/promotion-detail/promotion-detail.component.ts @@ -266,7 +266,12 @@ export class PromotionDetailComponent }); entity.actions.forEach(o => this.addOperation('actions', o)); if (this.customFields.length) { - this.setCustomFieldFormValues(this.customFields, this.detailForm.get('customFields'), entity); + this.setCustomFieldFormValues( + this.customFields, + this.detailForm.get('customFields'), + entity, + currentTranslation, + ); } } diff --git a/packages/admin-ui/src/lib/settings/src/components/payment-method-detail/payment-method-detail.component.ts b/packages/admin-ui/src/lib/settings/src/components/payment-method-detail/payment-method-detail.component.ts index 9838ed87fa..7dfa5f7cd6 100644 --- a/packages/admin-ui/src/lib/settings/src/components/payment-method-detail/payment-method-detail.component.ts +++ b/packages/admin-ui/src/lib/settings/src/components/payment-method-detail/payment-method-detail.component.ts @@ -273,6 +273,7 @@ export class PaymentMethodDetailComponent this.customFields, this.detailForm.get('customFields'), paymentMethod, + currentTranslation, ); } } From 577544748f785d07a4cb4386ae85d1422f7c47bb Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Tue, 21 Nov 2023 20:44:20 +0100 Subject: [PATCH 23/35] fix(core): Log error on misconfigured localized custom fields --- packages/core/src/bootstrap.ts | 3 + .../entity/register-custom-entity-fields.ts | 55 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/packages/core/src/bootstrap.ts b/packages/core/src/bootstrap.ts index 43cfa3c3de..643de956ad 100644 --- a/packages/core/src/bootstrap.ts +++ b/packages/core/src/bootstrap.ts @@ -147,6 +147,9 @@ export async function preBootstrapConfig( }); let config = getConfig(); + // The logger is set here so that we are able to log any messages prior to the final + // logger (which may depend on config coming from a plugin) being set. + Logger.useLogger(config.logger); config = await runPluginConfigurations(config); const entityIdStrategy = config.entityOptions.entityIdStrategy ?? config.entityIdStrategy; setEntityIdStrategy(entityIdStrategy, entities); diff --git a/packages/core/src/entity/register-custom-entity-fields.ts b/packages/core/src/entity/register-custom-entity-fields.ts index 6d7b357d76..3c90ae44d7 100644 --- a/packages/core/src/entity/register-custom-entity-fields.ts +++ b/packages/core/src/entity/register-custom-entity-fields.ts @@ -255,32 +255,70 @@ function getDefault(customField: CustomFieldConfig, dbEngine: DataSourceOptions[ return type === 'datetime' ? formatDefaultDatetime(dbEngine, defaultValue) : defaultValue; } +function assertLocaleFieldsNotSpecified(config: VendureConfig, entityName: keyof CustomFields) { + const customFields = config.customFields && config.customFields[entityName]; + if (customFields) { + for (const customField of customFields) { + if (customField.type === 'localeString' || customField.type === 'localeText') { + Logger.error( + `Custom field "${customField.name}" on entity "${entityName}" cannot be of type "localeString" or "localeText". ` + + `This entity does not support localization.`, + ); + } + } + } +} + /** * Dynamically registers any custom fields with TypeORM. This function should be run at the bootstrap * stage of the app lifecycle, before the AppModule is initialized. */ export function registerCustomEntityFields(config: VendureConfig) { registerCustomFieldsForEntity(config, 'Address', CustomAddressFields); + assertLocaleFieldsNotSpecified(config, 'Address'); + registerCustomFieldsForEntity(config, 'Administrator', CustomAdministratorFields); + assertLocaleFieldsNotSpecified(config, 'Administrator'); + registerCustomFieldsForEntity(config, 'Asset', CustomAssetFields); + assertLocaleFieldsNotSpecified(config, 'Asset'); + registerCustomFieldsForEntity(config, 'Collection', CustomCollectionFields); registerCustomFieldsForEntity(config, 'Collection', CustomCollectionFieldsTranslation, true); + registerCustomFieldsForEntity(config, 'Channel', CustomChannelFields); + assertLocaleFieldsNotSpecified(config, 'Channel'); + registerCustomFieldsForEntity(config, 'Customer', CustomCustomerFields); + assertLocaleFieldsNotSpecified(config, 'Customer'); + registerCustomFieldsForEntity(config, 'CustomerGroup', CustomCustomerGroupFields); + assertLocaleFieldsNotSpecified(config, 'CustomerGroup'); + registerCustomFieldsForEntity(config, 'Facet', CustomFacetFields); registerCustomFieldsForEntity(config, 'Facet', CustomFacetFieldsTranslation, true); + registerCustomFieldsForEntity(config, 'FacetValue', CustomFacetValueFields); registerCustomFieldsForEntity(config, 'FacetValue', CustomFacetValueFieldsTranslation, true); + registerCustomFieldsForEntity(config, 'Fulfillment', CustomFulfillmentFields); + assertLocaleFieldsNotSpecified(config, 'Fulfillment'); + registerCustomFieldsForEntity(config, 'Order', CustomOrderFields); + assertLocaleFieldsNotSpecified(config, 'Order'); + registerCustomFieldsForEntity(config, 'OrderLine', CustomOrderLineFields); + assertLocaleFieldsNotSpecified(config, 'OrderLine'); + registerCustomFieldsForEntity(config, 'PaymentMethod', CustomPaymentMethodFields); registerCustomFieldsForEntity(config, 'PaymentMethod', CustomPaymentMethodFieldsTranslation, true); + registerCustomFieldsForEntity(config, 'Product', CustomProductFields); registerCustomFieldsForEntity(config, 'Product', CustomProductFieldsTranslation, true); + registerCustomFieldsForEntity(config, 'ProductOption', CustomProductOptionFields); registerCustomFieldsForEntity(config, 'ProductOption', CustomProductOptionFieldsTranslation, true); + registerCustomFieldsForEntity(config, 'ProductOptionGroup', CustomProductOptionGroupFields); registerCustomFieldsForEntity( config, @@ -288,19 +326,36 @@ export function registerCustomEntityFields(config: VendureConfig) { CustomProductOptionGroupFieldsTranslation, true, ); + registerCustomFieldsForEntity(config, 'ProductVariant', CustomProductVariantFields); registerCustomFieldsForEntity(config, 'ProductVariant', CustomProductVariantFieldsTranslation, true); + registerCustomFieldsForEntity(config, 'Promotion', CustomPromotionFields); registerCustomFieldsForEntity(config, 'Promotion', CustomPromotionFieldsTranslation, true); + registerCustomFieldsForEntity(config, 'TaxCategory', CustomTaxCategoryFields); + assertLocaleFieldsNotSpecified(config, 'TaxCategory'); + registerCustomFieldsForEntity(config, 'TaxRate', CustomTaxRateFields); + assertLocaleFieldsNotSpecified(config, 'TaxRate'); + registerCustomFieldsForEntity(config, 'User', CustomUserFields); + assertLocaleFieldsNotSpecified(config, 'User'); registerCustomFieldsForEntity(config, 'GlobalSettings', CustomGlobalSettingsFields); + assertLocaleFieldsNotSpecified(config, 'GlobalSettings'); + registerCustomFieldsForEntity(config, 'Region', CustomRegionFields); registerCustomFieldsForEntity(config, 'Region', CustomRegionFieldsTranslation, true); + registerCustomFieldsForEntity(config, 'Seller', CustomSellerFields); + assertLocaleFieldsNotSpecified(config, 'Seller'); + registerCustomFieldsForEntity(config, 'ShippingMethod', CustomShippingMethodFields); registerCustomFieldsForEntity(config, 'ShippingMethod', CustomShippingMethodFieldsTranslation, true); + registerCustomFieldsForEntity(config, 'StockLocation', CustomStockLocationFields); + assertLocaleFieldsNotSpecified(config, 'StockLocation'); + registerCustomFieldsForEntity(config, 'Zone', CustomZoneFields); + assertLocaleFieldsNotSpecified(config, 'Zone'); } From fb0ea1326e5ee6cd240db146415dffc13af9ab9b Mon Sep 17 00:00:00 2001 From: Hans <111351665+hans-rollingridges-dev@users.noreply.github.com> Date: Wed, 22 Nov 2023 22:26:19 +0100 Subject: [PATCH 24/35] fix(core): Fix DefaultSearchPlugin for non-default languages (#2515) Fixes #2197 --- e2e-common/vitest.config.ts | 2 +- .../e2e/default-search-plugin.e2e-spec.ts | 894 +++++++++++------- packages/core/e2e/utils/await-running-jobs.ts | 21 +- .../search-strategy/mysql-search-strategy.ts | 3 +- .../postgres-search-strategy.ts | 4 +- .../search-strategy/search-strategy-common.ts | 8 - .../search-strategy/search-strategy-utils.ts | 29 +- .../search-strategy/sqlite-search-strategy.ts | 5 +- .../graphql/shop/complete-order.graphql | 2 +- .../dev-server/load-testing/init-load-test.ts | 16 +- .../load-testing/load-test-config.ts | 2 +- .../dev-server/load-testing/run-load-test.ts | 4 +- .../scripts/search-and-checkout.js | 10 +- .../load-testing/utils/api-request.js | 11 +- 14 files changed, 634 insertions(+), 377 deletions(-) diff --git a/e2e-common/vitest.config.ts b/e2e-common/vitest.config.ts index 44f9775966..25bdd44b5e 100644 --- a/e2e-common/vitest.config.ts +++ b/e2e-common/vitest.config.ts @@ -4,7 +4,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - include: '**/*.e2e-spec.ts', + include: ['**/*.e2e-spec.ts'], /** * For local debugging of the e2e tests, we set a very long timeout value otherwise tests will * automatically fail for going over the 5 second default timeout. diff --git a/packages/core/e2e/default-search-plugin.e2e-spec.ts b/packages/core/e2e/default-search-plugin.e2e-spec.ts index 49b77b2382..247bf7ab02 100644 --- a/packages/core/e2e/default-search-plugin.e2e-spec.ts +++ b/packages/core/e2e/default-search-plugin.e2e-spec.ts @@ -20,7 +20,6 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { initialData } from '../../../e2e-common/e2e-initial-data'; import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config'; -import * as Codegen from './graphql/generated-e2e-admin-types'; import { ChannelFragment, CurrencyCode, @@ -28,6 +27,51 @@ import { SearchInput, SearchResultSortParameter, SortOrder, + SearchProductsAdminQuery, + SearchProductsAdminQueryVariables, + SearchFacetValuesQuery, + SearchFacetValuesQueryVariables, + UpdateProductMutation, + UpdateProductMutationVariables, + SearchCollectionsQuery, + SearchCollectionsQueryVariables, + SearchGetPricesQuery, + SearchGetPricesQueryVariables, + CreateFacetMutation, + CreateFacetMutationVariables, + UpdateProductVariantsMutation, + UpdateProductVariantsMutationVariables, + DeleteProductVariantMutation, + DeleteProductVariantMutationVariables, + DeleteProductMutation, + DeleteProductMutationVariables, + UpdateCollectionMutation, + UpdateCollectionMutationVariables, + CreateCollectionMutation, + CreateCollectionMutationVariables, + UpdateTaxRateMutation, + UpdateTaxRateMutationVariables, + SearchGetAssetsQuery, + SearchGetAssetsQueryVariables, + UpdateAssetMutation, + UpdateAssetMutationVariables, + DeleteAssetMutation, + DeleteAssetMutationVariables, + ReindexMutation, + CreateProductMutation, + CreateProductMutationVariables, + CreateProductVariantsMutation, + CreateProductVariantsMutationVariables, + CreateChannelMutation, + CreateChannelMutationVariables, + AssignProductsToChannelMutation, + AssignProductsToChannelMutationVariables, + RemoveProductsFromChannelMutation, + RemoveProductsFromChannelMutationVariables, + AssignProductVariantsToChannelMutation, + AssignProductVariantsToChannelMutationVariables, + RemoveProductVariantsFromChannelMutation, + RemoveProductVariantsFromChannelMutationVariables, } from './graphql/generated-e2e-admin-types'; import { LogicalOperator, @@ -93,9 +137,12 @@ describe('Default search plugin', () => { }); function doAdminSearchQuery(input: SearchInput) { - return adminClient.query(SEARCH_PRODUCTS, { - input, - }); + return adminClient.query( + SEARCH_PRODUCTS, + { + input, + }, + ); } async function testGroupByProduct(client: SimpleGraphQLClient) { @@ -405,15 +452,15 @@ describe('Default search plugin', () => { } async function testSinglePrices(client: SimpleGraphQLClient) { - const result = await client.query< - Codegen.SearchGetPricesQuery, - Codegen.SearchGetPricesQueryVariables - >(SEARCH_GET_PRICES, { - input: { - groupByProduct: false, - take: 3, - } as SearchInput, - }); + const result = await client.query( + SEARCH_GET_PRICES, + { + input: { + groupByProduct: false, + take: 3, + } as SearchInput, + }, + ); expect(result.search.items).toEqual([ { price: { value: 129900 }, @@ -431,15 +478,15 @@ describe('Default search plugin', () => { } async function testPriceRanges(client: SimpleGraphQLClient) { - const result = await client.query< - Codegen.SearchGetPricesQuery, - Codegen.SearchGetPricesQueryVariables - >(SEARCH_GET_PRICES, { - input: { - groupByProduct: true, - take: 3, - } as SearchInput, - }); + const result = await client.query( + SEARCH_GET_PRICES, + { + input: { + groupByProduct: true, + take: 3, + } as SearchInput, + }, + ); expect(result.search.items).toEqual([ { price: { min: 129900, max: 229900 }, @@ -490,14 +537,14 @@ describe('Default search plugin', () => { it('price ranges', () => testPriceRanges(shopClient)); it('returns correct facetValues when not grouped by product', async () => { - const result = await shopClient.query< - Codegen.SearchFacetValuesQuery, - Codegen.SearchFacetValuesQueryVariables - >(SEARCH_GET_FACET_VALUES, { - input: { - groupByProduct: false, + const result = await shopClient.query( + SEARCH_GET_FACET_VALUES, + { + input: { + groupByProduct: false, + }, }, - }); + ); expect(result.search.facetValues).toEqual([ { count: 21, facetValue: { id: 'T_1', name: 'electronics' } }, { count: 17, facetValue: { id: 'T_2', name: 'computers' } }, @@ -509,14 +556,14 @@ describe('Default search plugin', () => { }); it('returns correct facetValues when grouped by product', async () => { - const result = await shopClient.query< - Codegen.SearchFacetValuesQuery, - Codegen.SearchFacetValuesQueryVariables - >(SEARCH_GET_FACET_VALUES, { - input: { - groupByProduct: true, + const result = await shopClient.query( + SEARCH_GET_FACET_VALUES, + { + input: { + groupByProduct: true, + }, }, - }); + ); expect(result.search.facetValues).toEqual([ { count: 10, facetValue: { id: 'T_1', name: 'electronics' } }, { count: 6, facetValue: { id: 'T_2', name: 'computers' } }, @@ -529,15 +576,15 @@ describe('Default search plugin', () => { // https://github.com/vendure-ecommerce/vendure/issues/1236 it('returns correct facetValues when not grouped by product, with search term', async () => { - const result = await shopClient.query< - Codegen.SearchFacetValuesQuery, - Codegen.SearchFacetValuesQueryVariables - >(SEARCH_GET_FACET_VALUES, { - input: { - groupByProduct: false, - term: 'laptop', + const result = await shopClient.query( + SEARCH_GET_FACET_VALUES, + { + input: { + groupByProduct: false, + term: 'laptop', + }, }, - }); + ); expect(result.search.facetValues).toEqual([ { count: 4, facetValue: { id: 'T_1', name: 'electronics' } }, { count: 4, facetValue: { id: 'T_2', name: 'computers' } }, @@ -546,8 +593,8 @@ describe('Default search plugin', () => { it('omits facetValues of private facets', async () => { const { createFacet } = await adminClient.query< - Codegen.CreateFacetMutation, - Codegen.CreateFacetMutationVariables + CreateFacetMutation, + CreateFacetMutationVariables >(CREATE_FACET, { input: { code: 'profit-margin', @@ -561,27 +608,24 @@ describe('Default search plugin', () => { ], }, }); - await adminClient.query( - UPDATE_PRODUCT, - { - input: { - id: 'T_2', - // T_1 & T_2 are the existing facetValues (electronics & photo) - facetValueIds: ['T_1', 'T_2', createFacet.values[0].id], - }, + await adminClient.query(UPDATE_PRODUCT, { + input: { + id: 'T_2', + // T_1 & T_2 are the existing facetValues (electronics & photo) + facetValueIds: ['T_1', 'T_2', createFacet.values[0].id], }, - ); + }); await awaitRunningJobs(adminClient); - const result = await shopClient.query< - Codegen.SearchFacetValuesQuery, - Codegen.SearchFacetValuesQueryVariables - >(SEARCH_GET_FACET_VALUES, { - input: { - groupByProduct: true, + const result = await shopClient.query( + SEARCH_GET_FACET_VALUES, + { + input: { + groupByProduct: true, + }, }, - }); + ); expect(result.search.facetValues).toEqual([ { count: 10, facetValue: { id: 'T_1', name: 'electronics' } }, { count: 6, facetValue: { id: 'T_2', name: 'computers' } }, @@ -593,28 +637,28 @@ describe('Default search plugin', () => { }); it('returns correct collections when not grouped by product', async () => { - const result = await shopClient.query< - Codegen.SearchCollectionsQuery, - Codegen.SearchCollectionsQueryVariables - >(SEARCH_GET_COLLECTIONS, { - input: { - groupByProduct: false, + const result = await shopClient.query( + SEARCH_GET_COLLECTIONS, + { + input: { + groupByProduct: false, + }, }, - }); + ); expect(result.search.collections).toEqual([ { collection: { id: 'T_2', name: 'Plants' }, count: 3 }, ]); }); it('returns correct collections when grouped by product', async () => { - const result = await shopClient.query< - Codegen.SearchCollectionsQuery, - Codegen.SearchCollectionsQueryVariables - >(SEARCH_GET_COLLECTIONS, { - input: { - groupByProduct: true, + const result = await shopClient.query( + SEARCH_GET_COLLECTIONS, + { + input: { + groupByProduct: true, + }, }, - }); + ); expect(result.search.collections).toEqual([ { collection: { id: 'T_2', name: 'Plants' }, count: 3 }, ]); @@ -645,12 +689,12 @@ describe('Default search plugin', () => { it('sort price without grouping', () => testSortingNoGrouping(shopClient, 'price')); it('omits results for disabled ProductVariants', async () => { - await adminClient.query< - Codegen.UpdateProductVariantsMutation, - Codegen.UpdateProductVariantsMutationVariables - >(UPDATE_PRODUCT_VARIANTS, { - input: [{ id: 'T_3', enabled: false }], - }); + await adminClient.query( + UPDATE_PRODUCT_VARIANTS, + { + input: [{ id: 'T_3', enabled: false }], + }, + ); await awaitRunningJobs(adminClient); const result = await shopClient.query( SEARCH_PRODUCTS_SHOP, @@ -812,8 +856,8 @@ describe('Default search plugin', () => { ]); await adminClient.query< - Codegen.UpdateProductVariantsMutation, - Codegen.UpdateProductVariantsMutationVariables + UpdateProductVariantsMutation, + UpdateProductVariantsMutationVariables >(UPDATE_PRODUCT_VARIANTS, { input: search.items.map(i => ({ id: i.productVariantId, @@ -843,12 +887,12 @@ describe('Default search plugin', () => { const variantToDelete = search.items[0]; expect(variantToDelete.sku).toBe('IHD455T1_updated'); - await adminClient.query< - Codegen.DeleteProductVariantMutation, - Codegen.DeleteProductVariantMutationVariables - >(DELETE_PRODUCT_VARIANT, { - id: variantToDelete.productVariantId, - }); + await adminClient.query( + DELETE_PRODUCT_VARIANT, + { + id: variantToDelete.productVariantId, + }, + ); await awaitRunningJobs(adminClient); const { search: search2 } = await doAdminSearchQuery({ @@ -865,15 +909,15 @@ describe('Default search plugin', () => { }); it('updates index when a Product is changed', async () => { - await adminClient.query< - Codegen.UpdateProductMutation, - Codegen.UpdateProductMutationVariables - >(UPDATE_PRODUCT, { - input: { - id: 'T_1', - facetValueIds: [], + await adminClient.query( + UPDATE_PRODUCT, + { + input: { + id: 'T_1', + facetValueIds: [], + }, }, - }); + ); await awaitRunningJobs(adminClient); const result = await doAdminSearchQuery({ facetValueIds: ['T_2'], groupByProduct: true }); expect(result.search.items.map(i => i.productName)).toEqual([ @@ -888,12 +932,12 @@ describe('Default search plugin', () => { it('updates index when a Product is deleted', async () => { const { search } = await doAdminSearchQuery({ facetValueIds: ['T_2'], groupByProduct: true }); expect(search.items.map(i => i.productId)).toEqual(['T_2', 'T_3', 'T_4', 'T_5', 'T_6']); - await adminClient.query< - Codegen.DeleteProductMutation, - Codegen.DeleteProductMutationVariables - >(DELETE_PRODUCT, { - id: 'T_5', - }); + await adminClient.query( + DELETE_PRODUCT, + { + id: 'T_5', + }, + ); await awaitRunningJobs(adminClient); const { search: search2 } = await doAdminSearchQuery({ facetValueIds: ['T_2'], @@ -903,29 +947,29 @@ describe('Default search plugin', () => { }); it('updates index when a Collection is changed', async () => { - await adminClient.query< - Codegen.UpdateCollectionMutation, - Codegen.UpdateCollectionMutationVariables - >(UPDATE_COLLECTION, { - input: { - id: 'T_2', - filters: [ - { - code: facetValueCollectionFilter.code, - arguments: [ - { - name: 'facetValueIds', - value: '["T_4"]', - }, - { - name: 'containsAny', - value: 'false', - }, - ], - }, - ], + await adminClient.query( + UPDATE_COLLECTION, + { + input: { + id: 'T_2', + filters: [ + { + code: facetValueCollectionFilter.code, + arguments: [ + { + name: 'facetValueIds', + value: '["T_4"]', + }, + { + name: 'containsAny', + value: 'false', + }, + ], + }, + ], + }, }, - }); + ); await awaitRunningJobs(adminClient); // add an additional check for the collection filters to update await awaitRunningJobs(adminClient); @@ -956,8 +1000,8 @@ describe('Default search plugin', () => { it('updates index when a Collection created', async () => { const { createCollection } = await adminClient.query< - Codegen.CreateCollectionMutation, - Codegen.CreateCollectionMutationVariables + CreateCollectionMutation, + CreateCollectionMutationVariables >(CREATE_COLLECTION, { input: { translations: [ @@ -1001,27 +1045,27 @@ describe('Default search plugin', () => { }); it('updates index when a taxRate is changed', async () => { - await adminClient.query< - Codegen.UpdateTaxRateMutation, - Codegen.UpdateTaxRateMutationVariables - >(UPDATE_TAX_RATE, { - input: { - // Default Channel's defaultTaxZone is Europe (id 2) and the id of the standard TaxRate - // to Europe is 2. - id: 'T_2', - value: 50, + await adminClient.query( + UPDATE_TAX_RATE, + { + input: { + // Default Channel's defaultTaxZone is Europe (id 2) and the id of the standard TaxRate + // to Europe is 2. + id: 'T_2', + value: 50, + }, }, - }); + ); await awaitRunningJobs(adminClient); - const result = await adminClient.query< - Codegen.SearchGetPricesQuery, - Codegen.SearchGetPricesQueryVariables - >(SEARCH_GET_PRICES, { - input: { - groupByProduct: true, - term: 'laptop', - } as SearchInput, - }); + const result = await adminClient.query( + SEARCH_GET_PRICES, + { + input: { + groupByProduct: true, + term: 'laptop', + } as SearchInput, + }, + ); expect(result.search.items).toEqual([ { price: { min: 129900, max: 229900 }, @@ -1032,15 +1076,15 @@ describe('Default search plugin', () => { describe('asset changes', () => { function searchForLaptop() { - return adminClient.query< - Codegen.SearchGetAssetsQuery, - Codegen.SearchGetAssetsQueryVariables - >(SEARCH_GET_ASSETS, { - input: { - term: 'laptop', - take: 1, + return adminClient.query( + SEARCH_GET_ASSETS, + { + input: { + term: 'laptop', + take: 1, + }, }, - }); + ); } it('updates index when asset focalPoint is changed', async () => { @@ -1049,10 +1093,7 @@ describe('Default search plugin', () => { expect(search1.items[0].productAsset!.id).toBe('T_1'); expect(search1.items[0].productAsset!.focalPoint).toBeNull(); - await adminClient.query< - Codegen.UpdateAssetMutation, - Codegen.UpdateAssetMutationVariables - >(UPDATE_ASSET, { + await adminClient.query(UPDATE_ASSET, { input: { id: 'T_1', focalPoint: { @@ -1076,10 +1117,7 @@ describe('Default search plugin', () => { const assetId = search1.items[0].productAsset?.id; expect(assetId).toBeTruthy(); - await adminClient.query< - Codegen.DeleteAssetMutation, - Codegen.DeleteAssetMutationVariables - >(DELETE_ASSET, { + await adminClient.query(DELETE_ASSET, { input: { assetId: assetId!, force: true, @@ -1101,15 +1139,15 @@ describe('Default search plugin', () => { }); const { deleteProductVariant } = await adminClient.query< - Codegen.DeleteProductVariantMutation, - Codegen.DeleteProductVariantMutationVariables + DeleteProductVariantMutation, + DeleteProductVariantMutationVariables >(DELETE_PRODUCT_VARIANT, { id: s1.items[0].productVariantId }); await awaitRunningJobs(adminClient); const { search } = await adminClient.query< - Codegen.SearchGetPricesQuery, - Codegen.SearchGetPricesQueryVariables + SearchGetPricesQuery, + SearchGetPricesQueryVariables >(SEARCH_GET_PRICES, { input: { term: 'hard drive', groupByProduct: true } }); expect(search.items[0].price).toEqual({ min: 7896, @@ -1128,8 +1166,8 @@ describe('Default search plugin', () => { it('when grouped, enabled is true if at least one variant is enabled', async () => { await adminClient.query< - Codegen.UpdateProductVariantsMutation, - Codegen.UpdateProductVariantsMutationVariables + UpdateProductVariantsMutation, + UpdateProductVariantsMutationVariables >(UPDATE_PRODUCT_VARIANTS, { input: [ { id: 'T_1', enabled: false }, @@ -1147,8 +1185,8 @@ describe('Default search plugin', () => { it('when grouped, enabled is false if all variants are disabled', async () => { await adminClient.query< - Codegen.UpdateProductVariantsMutation, - Codegen.UpdateProductVariantsMutationVariables + UpdateProductVariantsMutation, + UpdateProductVariantsMutationVariables >(UPDATE_PRODUCT_VARIANTS, { input: [{ id: 'T_4', enabled: false }], }); @@ -1162,15 +1200,15 @@ describe('Default search plugin', () => { }); it('when grouped, enabled is false if product is disabled', async () => { - await adminClient.query< - Codegen.UpdateProductMutation, - Codegen.UpdateProductMutationVariables - >(UPDATE_PRODUCT, { - input: { - id: 'T_3', - enabled: false, + await adminClient.query( + UPDATE_PRODUCT, + { + input: { + id: 'T_3', + enabled: false, + }, }, - }); + ); await awaitRunningJobs(adminClient); const result = await doAdminSearchQuery({ groupByProduct: true, take: 3 }); expect(result.search.items.map(pick(['productId', 'enabled']))).toEqual([ @@ -1182,7 +1220,7 @@ describe('Default search plugin', () => { // https://github.com/vendure-ecommerce/vendure/issues/295 it('enabled status survives reindex', async () => { - await adminClient.query(REINDEX); + await adminClient.query(REINDEX); await awaitRunningJobs(adminClient); const result = await doAdminSearchQuery({ groupByProduct: true, take: 3 }); @@ -1195,16 +1233,16 @@ describe('Default search plugin', () => { // https://github.com/vendure-ecommerce/vendure/issues/1482 it('price range omits disabled variant', async () => { - const result1 = await shopClient.query< - Codegen.SearchGetPricesQuery, - Codegen.SearchGetPricesQueryVariables - >(SEARCH_GET_PRICES, { - input: { - groupByProduct: true, - term: 'monitor', - take: 3, - } as SearchInput, - }); + const result1 = await shopClient.query( + SEARCH_GET_PRICES, + { + input: { + groupByProduct: true, + term: 'monitor', + take: 3, + } as SearchInput, + }, + ); expect(result1.search.items).toEqual([ { price: { min: 14374, max: 16994 }, @@ -1212,23 +1250,23 @@ describe('Default search plugin', () => { }, ]); await adminClient.query< - Codegen.UpdateProductVariantsMutation, - Codegen.UpdateProductVariantsMutationVariables + UpdateProductVariantsMutation, + UpdateProductVariantsMutationVariables >(UPDATE_PRODUCT_VARIANTS, { input: [{ id: 'T_5', enabled: false }], }); await awaitRunningJobs(adminClient); - const result2 = await shopClient.query< - Codegen.SearchGetPricesQuery, - Codegen.SearchGetPricesQueryVariables - >(SEARCH_GET_PRICES, { - input: { - groupByProduct: true, - term: 'monitor', - take: 3, - } as SearchInput, - }); + const result2 = await shopClient.query( + SEARCH_GET_PRICES, + { + input: { + groupByProduct: true, + term: 'monitor', + take: 3, + } as SearchInput, + }, + ); expect(result2.search.items).toEqual([ { price: { min: 16994, max: 16994 }, @@ -1247,8 +1285,8 @@ describe('Default search plugin', () => { .join(' '); const { createProduct } = await adminClient.query< - Codegen.CreateProductMutation, - Codegen.CreateProductMutationVariables + CreateProductMutation, + CreateProductMutationVariables >(CREATE_PRODUCT, { input: { translations: [ @@ -1262,8 +1300,8 @@ describe('Default search plugin', () => { }, }); await adminClient.query< - Codegen.CreateProductVariantsMutation, - Codegen.CreateProductVariantsMutationVariables + CreateProductVariantsMutation, + CreateProductVariantsMutationVariables >(CREATE_PRODUCT_VARIANTS, { input: [ { @@ -1281,12 +1319,12 @@ describe('Default search plugin', () => { expect(result.search.items.map(i => i.productName)).toEqual([ 'Very long description aabbccdd', ]); - await adminClient.query< - Codegen.DeleteProductMutation, - Codegen.DeleteProductMutationVariables - >(DELETE_PRODUCT, { - id: createProduct.id, - }); + await adminClient.query( + DELETE_PRODUCT, + { + id: createProduct.id, + }, + ); }); }); @@ -1296,8 +1334,8 @@ describe('Default search plugin', () => { it('creates synthetic index item for Product with no variants', async () => { const { createProduct } = await adminClient.query< - Codegen.CreateProductMutation, - Codegen.CreateProductMutationVariables + CreateProductMutation, + CreateProductMutationVariables >(CREATE_PRODUCT, { input: { facetValueIds: ['T_1'], @@ -1340,8 +1378,8 @@ describe('Default search plugin', () => { it('removes synthetic index item once a variant is created', async () => { const { createProductVariants } = await adminClient.query< - Codegen.CreateProductVariantsMutation, - Codegen.CreateProductVariantsMutationVariables + CreateProductVariantsMutation, + CreateProductVariantsMutationVariables >(CREATE_PRODUCT_VARIANTS, { input: [ { @@ -1369,8 +1407,8 @@ describe('Default search plugin', () => { beforeAll(async () => { const { createChannel } = await adminClient.query< - Codegen.CreateChannelMutation, - Codegen.CreateChannelMutationVariables + CreateChannelMutation, + CreateChannelMutationVariables >(CREATE_CHANNEL, { input: { code: 'second-channel', @@ -1385,11 +1423,11 @@ describe('Default search plugin', () => { secondChannel = createChannel as ChannelFragment; }); - it('adding product to channel', async () => { + it('assign product to channel', async () => { adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN); await adminClient.query< - Codegen.AssignProductsToChannelMutation, - Codegen.AssignProductsToChannelMutationVariables + AssignProductsToChannelMutation, + AssignProductsToChannelMutationVariables >(ASSIGN_PRODUCT_TO_CHANNEL, { input: { channelId: secondChannel.id, productIds: ['T_1', 'T_2'] }, }); @@ -1403,8 +1441,8 @@ describe('Default search plugin', () => { it('removing product from channel', async () => { adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN); const { removeProductsFromChannel } = await adminClient.query< - Codegen.RemoveProductsFromChannelMutation, - Codegen.RemoveProductsFromChannelMutationVariables + RemoveProductsFromChannelMutation, + RemoveProductsFromChannelMutationVariables >(REMOVE_PRODUCT_FROM_CHANNEL, { input: { productIds: ['T_2'], @@ -1418,11 +1456,11 @@ describe('Default search plugin', () => { expect(search.items.map(i => i.productId)).toEqual(['T_1']); }, 10000); - it('adding product variant to channel', async () => { + it('assign product variant to channel', async () => { adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN); await adminClient.query< - Codegen.AssignProductVariantsToChannelMutation, - Codegen.AssignProductVariantsToChannelMutationVariables + AssignProductVariantsToChannelMutation, + AssignProductVariantsToChannelMutationVariables >(ASSIGN_PRODUCTVARIANT_TO_CHANNEL, { input: { channelId: secondChannel.id, productVariantIds: ['T_10', 'T_15'] }, }); @@ -1447,8 +1485,8 @@ describe('Default search plugin', () => { it('removing product variant from channel', async () => { adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN); await adminClient.query< - Codegen.RemoveProductVariantsFromChannelMutation, - Codegen.RemoveProductVariantsFromChannelMutationVariables + RemoveProductVariantsFromChannelMutation, + RemoveProductVariantsFromChannelMutationVariables >(REMOVE_PRODUCTVARIANT_FROM_CHANNEL, { input: { channelId: secondChannel.id, productVariantIds: ['T_1', 'T_15'] }, }); @@ -1471,8 +1509,8 @@ describe('Default search plugin', () => { it('updating product affects current channel', async () => { adminClient.setChannelToken(SECOND_CHANNEL_TOKEN); const { updateProduct } = await adminClient.query< - Codegen.UpdateProductMutation, - Codegen.UpdateProductMutationVariables + UpdateProductMutation, + UpdateProductMutationVariables >(UPDATE_PRODUCT, { input: { id: 'T_3', @@ -1503,32 +1541,32 @@ describe('Default search plugin', () => { it('removing from channel with multiple languages', async () => { adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN); - await adminClient.query< - Codegen.UpdateProductMutation, - Codegen.UpdateProductMutationVariables - >(UPDATE_PRODUCT, { - input: { - id: 'T_4', - translations: [ - { - languageCode: LanguageCode.en, - name: 'product en', - slug: 'product-en', - description: 'en', - }, - { - languageCode: LanguageCode.de, - name: 'product de', - slug: 'product-de', - description: 'de', - }, - ], + await adminClient.query( + UPDATE_PRODUCT, + { + input: { + id: 'T_4', + translations: [ + { + languageCode: LanguageCode.en, + name: 'product en', + slug: 'product-en', + description: 'en', + }, + { + languageCode: LanguageCode.de, + name: 'product de', + slug: 'product-de', + description: 'de', + }, + ], + }, }, - }); + ); await adminClient.query< - Codegen.AssignProductsToChannelMutation, - Codegen.AssignProductsToChannelMutationVariables + AssignProductsToChannelMutation, + AssignProductsToChannelMutationVariables >(ASSIGN_PRODUCT_TO_CHANNEL, { input: { channelId: secondChannel.id, productIds: ['T_4'] }, }); @@ -1537,8 +1575,8 @@ describe('Default search plugin', () => { async function searchSecondChannelForDEProduct() { adminClient.setChannelToken(SECOND_CHANNEL_TOKEN); const { search } = await adminClient.query< - SearchProductsShopQuery, - SearchProductShopQueryVariables + SearchProductsAdminQuery, + SearchProductsAdminQueryVariables >( SEARCH_PRODUCTS, { @@ -1554,8 +1592,8 @@ describe('Default search plugin', () => { adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN); const { removeProductsFromChannel } = await adminClient.query< - Codegen.RemoveProductsFromChannelMutation, - Codegen.RemoveProductsFromChannelMutationVariables + RemoveProductsFromChannelMutation, + RemoveProductsFromChannelMutationVariables >(REMOVE_PRODUCT_FROM_CHANNEL, { input: { productIds: ['T_4'], @@ -1570,55 +1608,94 @@ describe('Default search plugin', () => { }); describe('multiple language handling', () => { - function searchInLanguage(languageCode: LanguageCode) { - return adminClient.query( - SEARCH_PRODUCTS, - { - input: { - take: 100, - }, - }, - { - languageCode, - }, - ); - } - beforeAll(async () => { + adminClient.setChannelToken(E2E_DEFAULT_CHANNEL_TOKEN); + const { updateProduct } = await adminClient.query< - Codegen.UpdateProductMutation, - Codegen.UpdateProductMutationVariables + UpdateProductMutation, + UpdateProductMutationVariables >(UPDATE_PRODUCT, { input: { id: 'T_1', translations: [ + { + languageCode: LanguageCode.en, + name: 'Laptop en', + slug: 'laptop-slug-en', + description: 'Laptop description en', + }, { languageCode: LanguageCode.de, - name: 'laptop name de', + name: 'Laptop de', slug: 'laptop-slug-de', - description: 'laptop description de', + description: 'Laptop description de', }, { languageCode: LanguageCode.zh, - name: 'laptop name zh', + name: 'Laptop zh', slug: 'laptop-slug-zh', - description: 'laptop description zh', + description: 'Laptop description zh', }, ], }, }); + expect(updateProduct.variants.length).toEqual(4); + await adminClient.query< - Codegen.UpdateProductVariantsMutation, - Codegen.UpdateProductVariantsMutationVariables + UpdateProductVariantsMutation, + UpdateProductVariantsMutationVariables >(UPDATE_PRODUCT_VARIANTS, { input: [ { id: updateProduct.variants[0].id, translations: [ { - languageCode: LanguageCode.fr, - name: 'laptop variant fr', + languageCode: LanguageCode.en, + name: 'Laptop variant T_1 en', + }, + { + languageCode: LanguageCode.de, + name: 'Laptop variant T_1 de', + }, + { + languageCode: LanguageCode.zh, + name: 'Laptop variant T_1 zh', + }, + ], + }, + { + id: updateProduct.variants[1].id, + translations: [ + { + languageCode: LanguageCode.en, + name: 'Laptop variant T_2 en', + }, + { + languageCode: LanguageCode.de, + name: 'Laptop variant T_2 de', + }, + ], + }, + { + id: updateProduct.variants[2].id, + translations: [ + { + languageCode: LanguageCode.en, + name: 'Laptop variant T_3 en', + }, + { + languageCode: LanguageCode.zh, + name: 'Laptop variant T_3 zh', + }, + ], + }, + { + id: updateProduct.variants[3].id, + translations: [ + { + languageCode: LanguageCode.en, + name: 'Laptop variant T_4 en', }, ], }, @@ -1628,64 +1705,229 @@ describe('Default search plugin', () => { await awaitRunningJobs(adminClient); }); - it('fallbacks to default language', async () => { - const { search } = await searchInLanguage(LanguageCode.af); - // No records for AF language, but we expect > 0 - // because of fallback to default language (EN) - expect(search.totalItems).toBeGreaterThan(0); - }); - - it('indexes product-level languages', async () => { - const { search: search1 } = await searchInLanguage(LanguageCode.de); - - expect(search1.items.map(i => i.productName)).toContain('laptop name de'); - expect(search1.items.map(i => i.productName)).not.toContain('laptop name zh'); - expect(search1.items.map(i => i.slug)).toContain('laptop-slug-de'); - expect(search1.items.map(i => i.description)).toContain('laptop description de'); - - const { search: search2 } = await searchInLanguage(LanguageCode.zh); + describe('search products', () => { + function searchInLanguage(languageCode: LanguageCode) { + return adminClient.query( + SEARCH_PRODUCTS, + { + input: { + take: 100, + }, + }, + { + languageCode, + }, + ); + } - expect(search2.items.map(i => i.productName)).toContain('laptop name zh'); - expect(search2.items.map(i => i.productName)).not.toContain('laptop name de'); - expect(search2.items.map(i => i.slug)).toContain('laptop-slug-zh'); - expect(search2.items.map(i => i.description)).toContain('laptop description zh'); - }); + it('fallbacks to default language en', async () => { + const { search } = await searchInLanguage(LanguageCode.af); + + const laptopVariants = search.items.filter(i => i.productId === 'T_1'); + expect(laptopVariants.length).toEqual(4); + + const laptopVariantT1 = laptopVariants.find(i => i.productVariantId === 'T_1'); + expect(laptopVariantT1?.productVariantName).toEqual('Laptop variant T_1 en'); + expect(laptopVariantT1?.productName).toEqual('Laptop en'); + expect(laptopVariantT1?.slug).toEqual('laptop-slug-en'); + expect(laptopVariantT1?.description).toEqual('Laptop description en'); + + const laptopVariantT2 = laptopVariants.find(i => i.productVariantId === 'T_2'); + expect(laptopVariantT2?.productVariantName).toEqual('Laptop variant T_2 en'); + expect(laptopVariantT2?.productName).toEqual('Laptop en'); + expect(laptopVariantT2?.slug).toEqual('laptop-slug-en'); + expect(laptopVariantT2?.description).toEqual('Laptop description en'); + + const laptopVariantT3 = laptopVariants.find(i => i.productVariantId === 'T_3'); + expect(laptopVariantT3?.productVariantName).toEqual('Laptop variant T_3 en'); + expect(laptopVariantT3?.productName).toEqual('Laptop en'); + expect(laptopVariantT3?.slug).toEqual('laptop-slug-en'); + expect(laptopVariantT3?.description).toEqual('Laptop description en'); + + const laptopVariantT4 = laptopVariants.find(i => i.productVariantId === 'T_4'); + expect(laptopVariantT4?.productVariantName).toEqual('Laptop variant T_4 en'); + expect(laptopVariantT4?.productName).toEqual('Laptop en'); + expect(laptopVariantT4?.slug).toEqual('laptop-slug-en'); + expect(laptopVariantT4?.description).toEqual('Laptop description en'); + }); - it('indexes product variant-level languages', async () => { - const { search: search1 } = await searchInLanguage(LanguageCode.fr); + it('indexes non-default language de', async () => { + const { search } = await searchInLanguage(LanguageCode.de); + + const laptopVariants = search.items.filter(i => i.productId === 'T_1'); + expect(laptopVariants.length).toEqual(4); + + const laptopVariantT1 = laptopVariants.find(i => i.productVariantId === 'T_1'); + expect(laptopVariantT1?.productVariantName).toEqual('Laptop variant T_1 de'); + expect(laptopVariantT1?.productName).toEqual('Laptop de'); + expect(laptopVariantT1?.slug).toEqual('laptop-slug-de'); + expect(laptopVariantT1?.description).toEqual('Laptop description de'); + + const laptopVariantT2 = laptopVariants.find(i => i.productVariantId === 'T_2'); + expect(laptopVariantT2?.productVariantName).toEqual('Laptop variant T_2 de'); + expect(laptopVariantT2?.productName).toEqual('Laptop de'); + expect(laptopVariantT2?.slug).toEqual('laptop-slug-de'); + expect(laptopVariantT2?.description).toEqual('Laptop description de'); + + const laptopVariantT3 = laptopVariants.find(i => i.productVariantId === 'T_3'); + expect(laptopVariantT3?.productVariantName).toEqual('Laptop variant T_3 en'); + expect(laptopVariantT3?.productName).toEqual('Laptop de'); + expect(laptopVariantT3?.slug).toEqual('laptop-slug-de'); + expect(laptopVariantT3?.description).toEqual('Laptop description de'); + + const laptopVariantT4 = laptopVariants.find(i => i.productVariantId === 'T_4'); + expect(laptopVariantT4?.productVariantName).toEqual('Laptop variant T_4 en'); + expect(laptopVariantT4?.productName).toEqual('Laptop de'); + expect(laptopVariantT4?.slug).toEqual('laptop-slug-de'); + expect(laptopVariantT4?.description).toEqual('Laptop description de'); + }); - expect(search1.items.map(i => i.productName)).toContain('Laptop'); - expect(search1.items.map(i => i.productVariantName)).toContain('laptop variant fr'); + it('indexes non-default language zh', async () => { + const { search } = await searchInLanguage(LanguageCode.zh); + + const laptopVariants = search.items.filter(i => i.productId === 'T_1'); + expect(laptopVariants.length).toEqual(4); + + const laptopVariantT1 = laptopVariants.find(i => i.productVariantId === 'T_1'); + expect(laptopVariantT1?.productVariantName).toEqual('Laptop variant T_1 zh'); + expect(laptopVariantT1?.productName).toEqual('Laptop zh'); + expect(laptopVariantT1?.slug).toEqual('laptop-slug-zh'); + expect(laptopVariantT1?.description).toEqual('Laptop description zh'); + + const laptopVariantT2 = laptopVariants.find(i => i.productVariantId === 'T_2'); + expect(laptopVariantT2?.productVariantName).toEqual('Laptop variant T_2 en'); + expect(laptopVariantT2?.productName).toEqual('Laptop zh'); + expect(laptopVariantT2?.slug).toEqual('laptop-slug-zh'); + expect(laptopVariantT2?.description).toEqual('Laptop description zh'); + + const laptopVariantT3 = laptopVariants.find(i => i.productVariantId === 'T_3'); + expect(laptopVariantT3?.productVariantName).toEqual('Laptop variant T_3 zh'); + expect(laptopVariantT3?.productName).toEqual('Laptop zh'); + expect(laptopVariantT3?.slug).toEqual('laptop-slug-zh'); + expect(laptopVariantT3?.description).toEqual('Laptop description zh'); + + const laptopVariantT4 = laptopVariants.find(i => i.productVariantId === 'T_4'); + expect(laptopVariantT4?.productVariantName).toEqual('Laptop variant T_4 en'); + expect(laptopVariantT4?.productName).toEqual('Laptop zh'); + expect(laptopVariantT4?.slug).toEqual('laptop-slug-zh'); + expect(laptopVariantT4?.description).toEqual('Laptop description zh'); + }); }); - // https://github.com/vendure-ecommerce/vendure/issues/1752 - // https://github.com/vendure-ecommerce/vendure/issues/1746 - it('sort by name with non-default languageCode', async () => { - const result = await adminClient.query< - SearchProductsShopQuery, - SearchProductShopQueryVariables - >( - SEARCH_PRODUCTS, - { - input: { - take: 2, - sort: { - name: SortOrder.ASC, + describe('search products grouped by product and sorted by name ASC', () => { + function searchInLanguage(languageCode: LanguageCode) { + return adminClient.query( + SEARCH_PRODUCTS, + { + input: { + groupByProduct: true, + take: 100, + sort: { + name: SortOrder.ASC, + }, }, }, - }, - { - languageCode: LanguageCode.de, - }, - ); - expect(result.search.items.length).toEqual(2); + { + languageCode, + }, + ); + } + + // https://github.com/vendure-ecommerce/vendure/issues/1752 + // https://github.com/vendure-ecommerce/vendure/issues/1746 + it('fallbacks to default language en', async () => { + const { search } = await searchInLanguage(LanguageCode.af); + + const productNames = [ + 'Bonsai Tree', + 'Boxing Gloves', + 'Camera Lens', + 'Cruiser Skateboard', + 'Curvy Monitor', + 'Football', + 'Gaming PC', + 'Instant Camera', + 'Laptop en', // fallback language en + 'Orchid', + 'product en', // fallback language en + 'Road Bike', + 'Running Shoe', + 'Skipping Rope', + 'Slr Camera', + 'Spiky Cactus', + 'Strawberry cheesecake', + 'Tent', + 'Tripod', + 'USB Cable', + ]; + + expect(search.items.map(i => i.productName)).toEqual(productNames); + }); + + it('indexes non-default language de', async () => { + const { search } = await searchInLanguage(LanguageCode.de); + + const productNames = [ + 'Bonsai Tree', + 'Boxing Gloves', + 'Camera Lens', + 'Cruiser Skateboard', + 'Curvy Monitor', + 'Football', + 'Gaming PC', + 'Instant Camera', + 'Laptop de', // language de + 'Orchid', + 'product de', // language de + 'Road Bike', + 'Running Shoe', + 'Skipping Rope', + 'Slr Camera', + 'Spiky Cactus', + 'Strawberry cheesecake', + 'Tent', + 'Tripod', + 'USB Cable', + ]; + + expect(search.items.map(i => i.productName)).toEqual(productNames); + }); + + it('indexes non-default language zh', async () => { + const { search } = await searchInLanguage(LanguageCode.zh); + + const productNames = [ + 'Bonsai Tree', + 'Boxing Gloves', + 'Camera Lens', + 'Cruiser Skateboard', + 'Curvy Monitor', + 'Football', + 'Gaming PC', + 'Instant Camera', + 'Laptop zh', // language zh + 'Orchid', + 'product en', // fallback language en + 'Road Bike', + 'Running Shoe', + 'Skipping Rope', + 'Slr Camera', + 'Spiky Cactus', + 'Strawberry cheesecake', + 'Tent', + 'Tripod', + 'USB Cable', + ]; + + expect(search.items.map(i => i.productName)).toEqual(productNames); + }); }); }); // https://github.com/vendure-ecommerce/vendure/issues/1789 describe('input escaping', () => { function search(term: string) { - return adminClient.query( + return adminClient.query( SEARCH_PRODUCTS, { input: { take: 10, term }, diff --git a/packages/core/e2e/utils/await-running-jobs.ts b/packages/core/e2e/utils/await-running-jobs.ts index 4d9c99df3f..a81f2b7cf3 100644 --- a/packages/core/e2e/utils/await-running-jobs.ts +++ b/packages/core/e2e/utils/await-running-jobs.ts @@ -1,7 +1,6 @@ import { SimpleGraphQLClient } from '@vendure/testing'; -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import { GetRunningJobs, JobState } from '../graphql/generated-e2e-admin-types'; +import { GetRunningJobsQuery, GetRunningJobsQueryVariables } from '../graphql/generated-e2e-admin-types'; import { GET_RUNNING_JOBS } from '../graphql/shared-definitions'; /** @@ -20,18 +19,18 @@ export async function awaitRunningJobs( // e.g. event debouncing is used before triggering the job. await new Promise(resolve => setTimeout(resolve, delay)); do { - const { jobs } = await adminClient.query< - Codegen.GetRunningJobsQuery, - Codegen.GetRunningJobsQueryVariables - >(GET_RUNNING_JOBS, { - options: { - filter: { - isSettled: { - eq: false, + const { jobs } = await adminClient.query( + GET_RUNNING_JOBS, + { + options: { + filter: { + isSettled: { + eq: false, + }, }, }, }, - }); + ); runningJobs = jobs.totalItems; timedOut = timeout < +new Date() - startTime; } while (runningJobs > 0 && !timedOut); diff --git a/packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts b/packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts index 5c1c7a722e..7b18bfe5b0 100644 --- a/packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts +++ b/packages/core/src/plugin/default-search-plugin/search-strategy/mysql-search-strategy.ts @@ -249,8 +249,9 @@ export class MysqlSearchStrategy implements SearchStrategy { qb.andWhere('FIND_IN_SET (:collectionSlug, si.collectionSlugs)', { collectionSlug }); } - applyLanguageConstraints(qb, ctx.languageCode, ctx.channel.defaultLanguageCode); qb.andWhere('si.channelId = :channelId', { channelId: ctx.channelId }); + applyLanguageConstraints(qb, ctx.languageCode, ctx.channel.defaultLanguageCode); + if (input.groupByProduct === true) { qb.groupBy('si.productId'); qb.addSelect('BIT_OR(si.enabled)', 'productEnabled'); diff --git a/packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts b/packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts index 2bfc99d28b..935a6e6716 100644 --- a/packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts +++ b/packages/core/src/plugin/default-search-plugin/search-strategy/postgres-search-strategy.ts @@ -247,11 +247,13 @@ export class PostgresSearchStrategy implements SearchStrategy { }); } - applyLanguageConstraints(qb, ctx.languageCode, ctx.channel.defaultLanguageCode); qb.andWhere('si.channelId = :channelId', { channelId: ctx.channelId }); + applyLanguageConstraints(qb, ctx.languageCode, ctx.channel.defaultLanguageCode); + if (input.groupByProduct === true) { qb.groupBy('si.productId'); } + return qb; } diff --git a/packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-common.ts b/packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-common.ts index 053190aa6d..0f8eac82b8 100644 --- a/packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-common.ts +++ b/packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-common.ts @@ -22,14 +22,6 @@ export const fieldsToSelect = [ 'productVariantPreviewFocalPoint', ]; -export const identifierFields = [ - 'channelId', - 'productVariantId', - 'productId', - 'productAssetId', - 'productVariantAssetId', -]; - export function getFieldsToSelect(includeStockStatus: boolean = false) { return includeStockStatus ? [...fieldsToSelect, 'inStock', 'productInStock'] : fieldsToSelect; } diff --git a/packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-utils.ts b/packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-utils.ts index c8c54c5db8..0a551f9508 100644 --- a/packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-utils.ts +++ b/packages/core/src/plugin/default-search-plugin/search-strategy/search-strategy-utils.ts @@ -9,12 +9,10 @@ import { } from '@vendure/common/lib/generated-types'; import { ID } from '@vendure/common/lib/shared-types'; import { unique } from '@vendure/common/lib/unique'; -import { QueryBuilder, SelectQueryBuilder } from 'typeorm'; +import { Brackets, QueryBuilder, SelectQueryBuilder } from 'typeorm'; import { SearchIndexItem } from '../entities/search-index-item.entity'; -import { identifierFields } from './search-strategy-common'; - /** * Maps a raw database result to a SearchResult. */ @@ -131,31 +129,34 @@ export function applyLanguageConstraints( defaultLanguageCode: LanguageCode, ) { const lcEscaped = qb.escape('languageCode'); + const ciEscaped = qb.escape('channelId'); + const pviEscaped = qb.escape('productVariantId'); + if (languageCode === defaultLanguageCode) { - qb.andWhere(`si.${lcEscaped} = :languageCode`, { languageCode }); + qb.andWhere(`si.${lcEscaped} = :languageCode`, { + languageCode, + }); } else { qb.andWhere(`si.${lcEscaped} IN (:...languageCodes)`, { languageCodes: [languageCode, defaultLanguageCode], }); - const joinFieldConditions = identifierFields - .map(field => `si.${qb.escape(field)} = sil.${qb.escape(field)}`) - .join(' AND '); - qb.leftJoin( SearchIndexItem, 'sil', - ` - ${joinFieldConditions} - AND si.${lcEscaped} != sil.${lcEscaped} - AND sil.${lcEscaped} = :languageCode - `, + `sil.${lcEscaped} = :languageCode AND sil.${ciEscaped} = si.${ciEscaped} AND sil.${pviEscaped} = si.${pviEscaped}`, { languageCode, }, ); - qb.andWhere(`sil.${lcEscaped} IS NULL`); + qb.andWhere( + new Brackets(qb1 => { + qb1.where(`si.${lcEscaped} = :languageCode1`, { + languageCode1: languageCode, + }).orWhere(`sil.${lcEscaped} IS NULL`); + }), + ); } return qb; diff --git a/packages/core/src/plugin/default-search-plugin/search-strategy/sqlite-search-strategy.ts b/packages/core/src/plugin/default-search-plugin/search-strategy/sqlite-search-strategy.ts index 1b3c7782f8..51d3d23889 100644 --- a/packages/core/src/plugin/default-search-plugin/search-strategy/sqlite-search-strategy.ts +++ b/packages/core/src/plugin/default-search-plugin/search-strategy/sqlite-search-strategy.ts @@ -99,7 +99,8 @@ export class SqliteSearchStrategy implements SearchStrategy { } if (sort) { if (sort.name) { - qb.addOrderBy('si.productName', sort.name); + // TODO: v3 - set the collation on the SearchIndexItem entity + qb.addOrderBy('si.productName COLLATE NOCASE', sort.name); } if (sort.price) { qb.addOrderBy('si.price', sort.price); @@ -230,8 +231,8 @@ export class SqliteSearchStrategy implements SearchStrategy { }); } - applyLanguageConstraints(qb, ctx.languageCode, ctx.channel.defaultLanguageCode); qb.andWhere('si.channelId = :channelId', { channelId: ctx.channelId }); + applyLanguageConstraints(qb, ctx.languageCode, ctx.channel.defaultLanguageCode); if (input.groupByProduct === true) { qb.groupBy('si.productId'); diff --git a/packages/dev-server/load-testing/graphql/shop/complete-order.graphql b/packages/dev-server/load-testing/graphql/shop/complete-order.graphql index d0a24ba4ce..959185f0aa 100644 --- a/packages/dev-server/load-testing/graphql/shop/complete-order.graphql +++ b/packages/dev-server/load-testing/graphql/shop/complete-order.graphql @@ -1,4 +1,4 @@ -mutation SetShippingMethod($id: ID!) { +mutation SetShippingMethod($id: [ID!]!) { setOrderShippingMethod(shippingMethodId: $id) { ...on Order { code diff --git a/packages/dev-server/load-testing/init-load-test.ts b/packages/dev-server/load-testing/init-load-test.ts index 4526ab996c..48f96b3fce 100644 --- a/packages/dev-server/load-testing/init-load-test.ts +++ b/packages/dev-server/load-testing/init-load-test.ts @@ -2,7 +2,7 @@ /// import { bootstrap, JobQueueService, Logger } from '@vendure/core'; import { populate } from '@vendure/core/cli/populate'; -import { clearAllTables, populateCustomers } from '@vendure/testing'; +import { clearAllTables, populateCustomers, SimpleGraphQLClient } from '@vendure/testing'; import stringify from 'csv-stringify'; import fs from 'fs'; import path from 'path'; @@ -17,6 +17,8 @@ import { getProductCsvFilePath, } from './load-test-config'; +import { awaitRunningJobs } from '../../core/e2e/utils/await-running-jobs'; + /* eslint-disable no-console */ /** @@ -49,6 +51,18 @@ if (require.main === module) { csvFile, ), ) + .then(async app => { + console.log('synchronize on search index updated...'); + const { port, adminApiPath, shopApiPath } = config.apiOptions; + const adminClient = new SimpleGraphQLClient( + config, + `http://localhost:${port}/${adminApiPath!}`, + ); + await adminClient.asSuperAdmin(); + await new Promise(resolve => setTimeout(resolve, 5000)); + await awaitRunningJobs(adminClient, 5000000); + return app; + }) .then(async app => { console.log('populating customers...'); await populateCustomers(app, 10, message => Logger.error(message)); diff --git a/packages/dev-server/load-testing/load-test-config.ts b/packages/dev-server/load-testing/load-test-config.ts index bc4391017f..d574446e21 100644 --- a/packages/dev-server/load-testing/load-test-config.ts +++ b/packages/dev-server/load-testing/load-test-config.ts @@ -72,7 +72,7 @@ export function getLoadTestConfig( assetUploadDir: path.join(__dirname, 'static/assets'), route: 'assets', }), - DefaultSearchPlugin, + DefaultSearchPlugin.init({ bufferUpdates: false, indexStockStatus: false }), DefaultJobQueuePlugin.init({ pollInterval: 1000, }), diff --git a/packages/dev-server/load-testing/run-load-test.ts b/packages/dev-server/load-testing/run-load-test.ts index d008a88e27..4b062970a6 100644 --- a/packages/dev-server/load-testing/run-load-test.ts +++ b/packages/dev-server/load-testing/run-load-test.ts @@ -26,7 +26,7 @@ if (require.main === module) { stdio: 'inherit', }); - init.on('exit', code => { + init.on('exit', async code => { if (code === 0) { const databaseName = `vendure-load-testing-${count}`; return bootstrap(getLoadTestConfig('cookie', databaseName)) @@ -49,7 +49,7 @@ if (require.main === module) { }); } -function runLoadTestScript(script: string): Promise { +async function runLoadTestScript(script: string): Promise { const rawResultsFile = `${script}.${count}.json`; return new Promise((resolve, reject) => { diff --git a/packages/dev-server/load-testing/scripts/search-and-checkout.js b/packages/dev-server/load-testing/scripts/search-and-checkout.js index 467c6874f0..7a93814a8c 100644 --- a/packages/dev-server/load-testing/scripts/search-and-checkout.js +++ b/packages/dev-server/load-testing/scripts/search-and-checkout.js @@ -26,10 +26,12 @@ export default function () { addToCart(randomItem(product.variants).id); } setShippingAddressAndCustomer(); - const data = getShippingMethodsQuery.post().data; - const result = completeOrderMutation.post({ id: data.eligibleShippingMethods[0].id }).data; - check(result, { - 'Order completed': r => r.addPaymentToOrder.state === 'PaymentAuthorized', + const { data: shippingMethods } = getShippingMethodsQuery.post(); + const { data: order } = completeOrderMutation.post({ + id: [shippingMethods.eligibleShippingMethods.at(0).id], + }); + check(order, { + 'Order completed': o => o.addPaymentToOrder.state === 'PaymentAuthorized', }); } diff --git a/packages/dev-server/load-testing/utils/api-request.js b/packages/dev-server/load-testing/utils/api-request.js index 433e8e7e5f..3bd17cefbd 100644 --- a/packages/dev-server/load-testing/utils/api-request.js +++ b/packages/dev-server/load-testing/utils/api-request.js @@ -17,13 +17,16 @@ export class ApiRequest { post(variables = {}, authToken) { const res = http.post( this.apiUrl, - { + JSON.stringify({ query: this.document, - variables: JSON.stringify(variables), - }, + variables, + }), { timeout: 120 * 1000, - headers: { Authorization: authToken ? `Bearer ${authToken}` : undefined }, + headers: { + Authorization: authToken ? `Bearer ${authToken}` : '', + 'Content-Type': 'application/json', + }, }, ); check(res, { From 56f65ccb281121d6c66659ec69ae2daaf4314145 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Wed, 22 Nov 2023 08:27:10 +0100 Subject: [PATCH 25/35] docs: Fix imports in populate script example --- docs/docs/guides/developer-guide/importing-data/index.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/docs/guides/developer-guide/importing-data/index.md b/docs/docs/guides/developer-guide/importing-data/index.md index 087a6cfb2c..8546f63184 100644 --- a/docs/docs/guides/developer-guide/importing-data/index.md +++ b/docs/docs/guides/developer-guide/importing-data/index.md @@ -182,9 +182,10 @@ The `@vendure/core` package exposes a [`populate()` function](/reference/typescr ```ts title="src/my-populate-script.ts" import { bootstrap, DefaultJobQueuePlugin } from '@vendure/core'; import { populate } from '@vendure/core/cli'; +import path from "path"; -import { config } from './vendure-config.ts'; -import { initialData } from './my-initial-data.ts'; +import { config } from './vendure-config'; +import { initialData } from './my-initial-data'; const productsCsvFile = path.join(__dirname, 'path/to/products.csv') @@ -198,7 +199,7 @@ const populateConfig = { } populate( - () => bootstrap(config), + () => bootstrap(populateConfig), initialData, productsCsvFile, 'my-channel-token' // optional - used to assign imported From 84764b17c471a983b7bae49b9d840068875246f3 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 23 Nov 2023 14:21:31 +0100 Subject: [PATCH 26/35] fix(admin-ui): Fix encoding of configurable arg values Fixes #2539. Previously we were encoding with `.toString()` but decoding with `JSON.parse()` which broke in the case that we saved a valid JSON string, in which case it would be parsed into an actual object rather than a string. --- .../common/utilities/configurable-operation-utils.ts | 10 +++++----- .../configurable-input/configurable-input.component.ts | 4 ---- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts b/packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts index 5852caab2f..d1b370bb76 100644 --- a/packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts +++ b/packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts @@ -1,4 +1,4 @@ -import { ConfigArgType, CustomFieldType } from '@vendure/common/lib/shared-types'; +import { ConfigArgType } from '@vendure/common/lib/shared-types'; import { assertNever } from '@vendure/common/lib/shared-utils'; import { @@ -22,7 +22,7 @@ export function getConfigArgValue(value: any) { } export function encodeConfigArgValue(value: any): string { - return Array.isArray(value) ? JSON.stringify(value) : (value ?? '').toString(); + return JSON.stringify(value ?? ''); } /** @@ -34,9 +34,9 @@ export function configurableDefinitionToInstance( return { ...def, args: def.args.map(arg => ({ - ...arg, - value: getDefaultConfigArgValue(arg), - })), + ...arg, + value: getDefaultConfigArgValue(arg), + })), } as ConfigurableOperation; } diff --git a/packages/admin-ui/src/lib/core/src/shared/components/configurable-input/configurable-input.component.ts b/packages/admin-ui/src/lib/core/src/shared/components/configurable-input/configurable-input.component.ts index 4d3079f813..71af70d53a 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/configurable-input/configurable-input.component.ts +++ b/packages/admin-ui/src/lib/core/src/shared/components/configurable-input/configurable-input.component.ts @@ -21,11 +21,7 @@ import { Validator, Validators, } from '@angular/forms'; -import { ConfigArgType } from '@vendure/common/lib/shared-types'; -import { assertNever } from '@vendure/common/lib/shared-utils'; import { BehaviorSubject, Observable, Subscription } from 'rxjs'; - -import { InputComponentConfig } from '../../../common/component-registry-types'; import { ConfigArg, ConfigArgDefinition, From f7b4f46f62eed21e42f96d334054bc1fc4fa2d08 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 23 Nov 2023 14:30:22 +0100 Subject: [PATCH 27/35] fix(admin-ui): Fix stack overflow when datetime picker inside a list --- .../components/datetime-picker/datetime-picker.service.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.service.ts b/packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.service.ts index 65fc7fa81d..c5338b9b4a 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.service.ts +++ b/packages/admin-ui/src/lib/core/src/shared/components/datetime-picker/datetime-picker.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import dayjs from 'dayjs'; import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { distinctUntilChanged, map } from 'rxjs/operators'; import { dayOfWeekIndex } from './constants'; import { CalendarView, DayCell, DayOfWeek } from './types'; @@ -19,7 +19,10 @@ export class DatetimePickerService { private jumping = false; constructor() { - this.selected$ = this.selectedDatetime$.pipe(map(value => value && value.toDate())); + this.selected$ = this.selectedDatetime$.pipe( + map(value => value && value.toDate()), + distinctUntilChanged((a, b) => a?.getTime() === b?.getTime()), + ); this.viewing$ = this.viewingDatetime$.pipe(map(value => value.toDate())); this.weekStartDayIndex = dayOfWeekIndex['mon']; this.calendarView$ = combineLatest(this.viewingDatetime$, this.selectedDatetime$).pipe( From 517601781953d3e36aa23fe990b58a4459fc651a Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 23 Nov 2023 15:06:14 +0100 Subject: [PATCH 28/35] fix(admin-ui): Fix responsive layout of modal dialog for assets Fixes #2537 --- .../asset-picker-dialog.component.scss | 2 +- .../modal-dialog/modal-dialog.component.scss | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/admin-ui/src/lib/core/src/shared/components/asset-picker-dialog/asset-picker-dialog.component.scss b/packages/admin-ui/src/lib/core/src/shared/components/asset-picker-dialog/asset-picker-dialog.component.scss index 4acf211f79..97bade3172 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/asset-picker-dialog/asset-picker-dialog.component.scss +++ b/packages/admin-ui/src/lib/core/src/shared/components/asset-picker-dialog/asset-picker-dialog.component.scss @@ -2,7 +2,7 @@ :host { display: flex; flex-direction: column; - height: 70vh; + //height: 70vh; overflow-y: auto; } diff --git a/packages/admin-ui/src/lib/core/src/shared/components/modal-dialog/modal-dialog.component.scss b/packages/admin-ui/src/lib/core/src/shared/components/modal-dialog/modal-dialog.component.scss index 14509ee100..ed397e52f6 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/modal-dialog/modal-dialog.component.scss +++ b/packages/admin-ui/src/lib/core/src/shared/components/modal-dialog/modal-dialog.component.scss @@ -5,6 +5,29 @@ &.modal-valign-bottom .modal { justify-content: flex-end; } + .modal-dialog { + display: flex; + } + .modal-content-wrapper { + flex: 1; + display: flex; + } + .modal-dialog .modal-content { + flex: 1; + display: flex; + flex-direction: column; + } + @media screen and (max-height: 700px) { + .modal-dialog .modal-content { + padding: 0.8rem; + } + .modal-header, .modal-header--accessible { + padding-bottom: 0.8rem; + } + .modal-footer { + padding-top: 0.8rem; + } + } } .modal-body { From c077e15eced5eb2a003e802ecd281fc72b3eab88 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 23 Nov 2023 15:13:24 +0100 Subject: [PATCH 29/35] fix(admin-ui): Fix card component colors in dark theme --- packages/admin-ui/src/lib/static/styles/theme/dark.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/admin-ui/src/lib/static/styles/theme/dark.scss b/packages/admin-ui/src/lib/static/styles/theme/dark.scss index 2f754d2fbb..8d42effd3c 100644 --- a/packages/admin-ui/src/lib/static/styles/theme/dark.scss +++ b/packages/admin-ui/src/lib/static/styles/theme/dark.scss @@ -250,8 +250,9 @@ /********** * Card */ + --clr-card-header-title-color: var(--color-text-200); --clr-card-bg-color: hsl(198, 28%, 18%); - --clr-card-border-color: hsl(203, 30%, 8%); + --clr-card-border-color: hsl(203, 30%, 13%); --clr-card-title-color: hsl(210, 16%, 93%); --clr-card-box-shadow-color: var(--clr-card-border-color); --clr-card-box-shadow: 0 0.15rem 0 0 var(--clr-card-border-color); From 9eb9d9d53d79838b487480089166906c7c3ef902 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 23 Nov 2023 15:37:37 +0100 Subject: [PATCH 30/35] fix(admin-ui): Fix code editor border color for dark mode --- .../code-editor-form-input/base-code-editor.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/code-editor-form-input/base-code-editor.scss b/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/code-editor-form-input/base-code-editor.scss index 87ecdb12e6..c7045d7dd6 100644 --- a/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/code-editor-form-input/base-code-editor.scss +++ b/packages/admin-ui/src/lib/core/src/shared/dynamic-form-inputs/code-editor-form-input/base-code-editor.scss @@ -3,7 +3,7 @@ min-height: 6rem; background-color: var(--color-json-editor-background-color); color: var(--color-json-editor-text); - border: 1px solid var(--color-component-border-200); + border: 1px solid var(--color-weight-200); border-radius: 3px; padding: 6px; tab-size: 4; From c1b806231a694607e4043e3b8213bf92a7535b39 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Thu, 23 Nov 2023 15:53:48 +0100 Subject: [PATCH 31/35] chore(admin-ui): Alternate solution for #2539 The prior solution in https://github.com/vendure-ecommerce/vendure/commit/84764b17c471a983b7bae49b9d840068875246f3 is potentially too far-reaching a change. This is a more localized approach which is less likely to have unwanted side-effects. --- .../common/utilities/configurable-operation-utils.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts b/packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts index d1b370bb76..3e01243ffa 100644 --- a/packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts +++ b/packages/admin-ui/src/lib/core/src/common/utilities/configurable-operation-utils.ts @@ -15,14 +15,21 @@ import { */ export function getConfigArgValue(value: any) { try { - return value != null ? JSON.parse(value) : undefined; + const result = value != null ? JSON.parse(value) : undefined; + if (result && typeof result === 'object' && !Array.isArray(result)) { + // There is an edge-case where the value is a valid JSON-encoded string and + // will get parsed as an object, but we actually want it to be a string. + return JSON.stringify(result); + } else { + return result; + } } catch (e: any) { return value; } } export function encodeConfigArgValue(value: any): string { - return JSON.stringify(value ?? ''); + return Array.isArray(value) ? JSON.stringify(value) : (value ?? '').toString(); } /** From a9e67fe5226e2393b4de693f26fc808c825d344e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Hunziker?= Date: Fri, 24 Nov 2023 08:27:20 +0100 Subject: [PATCH 32/35] fix(admin-ui): Fix admin ui code templates (#2545) --- .../ui-extension-point.component.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.ts b/packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.ts index a8a954fe63..5b789f81d1 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.ts +++ b/packages/admin-ui/src/lib/core/src/shared/components/ui-extension-point/ui-extension-point.component.ts @@ -107,8 +107,8 @@ export default [ id: 'my-button', label: 'My Action', locationId: '${locationId}', - }); -]`, + }), +];`, navMenu: locationId => ` import { addNavMenuSection } from '@vendure/admin-ui/core'; @@ -117,10 +117,10 @@ export default [ id: 'my-menu-item', label: 'My Menu Item', routerLink: ['/extensions/my-plugin'], - } - '${locationId}' - ); -]`, + }, + '${locationId}', + ), +];`, detailComponent: locationId => ` import { registerCustomDetailComponent } from '@vendure/admin-ui/core'; @@ -128,8 +128,8 @@ export default [ registerCustomDetailComponent({ locationId: '${locationId}', component: MyCustomComponent, - }); -]`, + }), +];`, dataTable: (locationId, metadata) => ` import { registerDataTableComponent } from '@vendure/admin-ui/core'; @@ -138,6 +138,6 @@ export default [ tableId: '${locationId}', columnId: '${metadata}', component: MyCustomComponent, - }); -]`, + }), +];`, }; From 9546d1b67faab4129294fbf1f9ff6bf7248a5fd6 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 24 Nov 2023 08:53:12 +0100 Subject: [PATCH 33/35] fix(core): Fix entity hydration postgres edge-case Fixes #2546 --- packages/core/e2e/entity-hydrator.e2e-spec.ts | 46 ++++++++++++++++++- .../entity-hydrator.service.ts | 21 +++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/packages/core/e2e/entity-hydrator.e2e-spec.ts b/packages/core/e2e/entity-hydrator.e2e-spec.ts index 7a596522d7..e6ff9034ab 100644 --- a/packages/core/e2e/entity-hydrator.e2e-spec.ts +++ b/packages/core/e2e/entity-hydrator.e2e-spec.ts @@ -8,6 +8,10 @@ import { ProductVariant, RequestContext, ActiveOrderService, + OrderService, + TransactionalConnection, + OrderLine, + RequestContextService, } from '@vendure/core'; import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing'; import gql from 'graphql-tag'; @@ -43,7 +47,7 @@ describe('Entity hydration', () => { await server.init({ initialData, productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-full.csv'), - customerCount: 1, + customerCount: 2, }); await adminClient.asSuperAdmin(); }, TEST_SETUP_TIMEOUT_MS); @@ -290,6 +294,46 @@ describe('Entity hydration', () => { expect(order!.lines[1].productVariant.priceWithTax).toBeGreaterThan(0); }); }); + + // https://github.com/vendure-ecommerce/vendure/issues/2546 + it('Preserves ordering when merging arrays of relations', async () => { + await shopClient.asUserWithCredentials('trevor_donnelly96@hotmail.com', 'test'); + await shopClient.query(AddItemToOrderDocument, { + productVariantId: '1', + quantity: 1, + }); + const { addItemToOrder } = await shopClient.query(AddItemToOrderDocument, { + productVariantId: '2', + quantity: 2, + }); + orderResultGuard.assertSuccess(addItemToOrder); + const internalOrderId = +addItemToOrder.id.replace(/^\D+/g, ''); + const ctx = await server.app.get(RequestContextService).create({ apiType: 'admin' }); + const order = await server.app + .get(OrderService) + .findOne(ctx, internalOrderId, ['lines.productVariant']); + + for (const line of order?.lines ?? []) { + // Assert that things are as we expect before hydrating + expect(line.productVariantId).toBe(line.productVariant.id); + } + + // modify the first order line to make postgres tend to return the lines in the wrong order + await server.app + .get(TransactionalConnection) + .getRepository(ctx, OrderLine) + .update(order!.lines[0].id, { + sellerChannelId: 1, + }); + + await server.app.get(EntityHydrator).hydrate(ctx, order!, { + relations: ['lines.sellerChannel'], + }); + + for (const line of order?.lines ?? []) { + expect(line.productVariantId).toBe(line.productVariant.id); + } + }); }); function getVariantWithName(product: Product, name: string) { diff --git a/packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts b/packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts index 6316b64aba..7d2497a1c7 100644 --- a/packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts +++ b/packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts @@ -302,6 +302,27 @@ export class EntityHydrator { if (!a) { return b; } + if (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.length > 1) { + if (a[0].hasOwnProperty('id')) { + // If the array contains entities, we can use the id to match them up + // so that we ensure that we don't merge properties from different entities + // with the same index. + const aIds = a.map(e => e.id); + const bIds = b.map(e => e.id); + if (JSON.stringify(aIds) !== JSON.stringify(bIds)) { + // The entities in the arrays are not in the same order, so we can't + // safely merge them. We need to sort the `b` array so that the entities + // are in the same order as the `a` array. + const idToIndexMap = new Map(); + a.forEach((item, index) => { + idToIndexMap.set(item.id, index); + }); + b.sort((_a, _b) => { + return idToIndexMap.get(_a.id) - idToIndexMap.get(_b.id); + }); + } + } + } for (const [key, value] of Object.entries(b)) { if (Object.getOwnPropertyDescriptor(b, key)?.writable) { if (Array.isArray(value)) { From 2089af1cd50eee51cfa984fa11c5d789282812ce Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 24 Nov 2023 08:54:58 +0100 Subject: [PATCH 34/35] chore(core): Assign shippingLine.shippingMethodId No indication that lack of this was causing a bug, but it is a case for "belt and braces" --- packages/core/src/service/services/order.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/service/services/order.service.ts b/packages/core/src/service/services/order.service.ts index 591656377e..715db9857b 100644 --- a/packages/core/src/service/services/order.service.ts +++ b/packages/core/src/service/services/order.service.ts @@ -876,6 +876,7 @@ export class OrderService { let shippingLine: ShippingLine | undefined = order.shippingLines[i]; if (shippingLine) { shippingLine.shippingMethod = shippingMethod; + shippingLine.shippingMethodId = shippingMethod.id; } else { shippingLine = await this.connection.getRepository(ctx, ShippingLine).save( new ShippingLine({ From 5057f3e9d44fa3e5a65aeb0bd7ef41e5e1189970 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Fri, 24 Nov 2023 09:36:24 +0100 Subject: [PATCH 35/35] chore: Publish v2.1.4 --- CHANGELOG.md | 19 +++++++++++++++++++ lerna.json | 2 +- packages/admin-ui-plugin/package.json | 6 +++--- packages/admin-ui/package-lock.json | 2 +- packages/admin-ui/package.json | 4 ++-- .../src/lib/core/src/common/version.ts | 2 +- packages/asset-server-plugin/package.json | 6 +++--- packages/cli/package.json | 4 ++-- packages/common/package.json | 2 +- packages/core/package.json | 4 ++-- packages/create/package.json | 6 +++--- packages/dev-server/package.json | 18 +++++++++--------- packages/elasticsearch-plugin/package.json | 6 +++--- packages/email-plugin/package.json | 6 +++--- packages/harden-plugin/package.json | 6 +++--- packages/job-queue-plugin/package.json | 6 +++--- packages/payments-plugin/package.json | 8 ++++---- packages/testing/package.json | 6 +++--- packages/ui-devkit/package.json | 8 ++++---- 19 files changed, 70 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e435f5b8c..94035f362b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +## 2.1.4 (2023-11-24) + + +#### Fixes + +* **admin-ui** Fix admin ui code templates (#2545) ([a9e67fe](https://github.com/vendure-ecommerce/vendure/commit/a9e67fe)), closes [#2545](https://github.com/vendure-ecommerce/vendure/issues/2545) +* **admin-ui** Fix card component colors in dark theme ([c077e15](https://github.com/vendure-ecommerce/vendure/commit/c077e15)) +* **admin-ui** Fix code editor border color for dark mode ([9eb9d9d](https://github.com/vendure-ecommerce/vendure/commit/9eb9d9d)) +* **admin-ui** Fix encoding of configurable arg values ([84764b1](https://github.com/vendure-ecommerce/vendure/commit/84764b1)), closes [#2539](https://github.com/vendure-ecommerce/vendure/issues/2539) +* **admin-ui** Fix localized custom fields in Promotion & PaymentMethod ([d665ec6](https://github.com/vendure-ecommerce/vendure/commit/d665ec6)) +* **admin-ui** Fix responsive layout of modal dialog for assets ([5176017](https://github.com/vendure-ecommerce/vendure/commit/5176017)), closes [#2537](https://github.com/vendure-ecommerce/vendure/issues/2537) +* **admin-ui** Fix stack overflow when datetime picker inside a list ([f7b4f46](https://github.com/vendure-ecommerce/vendure/commit/f7b4f46)) +* **core** Fix custom MoneyStrategy handling from plugins ([a09c2b2](https://github.com/vendure-ecommerce/vendure/commit/a09c2b2)), closes [#2527](https://github.com/vendure-ecommerce/vendure/issues/2527) +* **core** Fix DefaultSearchPlugin for non-default languages (#2515) ([fb0ea13](https://github.com/vendure-ecommerce/vendure/commit/fb0ea13)), closes [#2515](https://github.com/vendure-ecommerce/vendure/issues/2515) [#2197](https://github.com/vendure-ecommerce/vendure/issues/2197) +* **core** Fix entity hydration postgres edge-case ([9546d1b](https://github.com/vendure-ecommerce/vendure/commit/9546d1b)), closes [#2546](https://github.com/vendure-ecommerce/vendure/issues/2546) +* **core** Fix i18n custom fields in Promotion & PaymentMethod ([3d6edb5](https://github.com/vendure-ecommerce/vendure/commit/3d6edb5)) +* **core** Log error on misconfigured localized custom fields ([5775447](https://github.com/vendure-ecommerce/vendure/commit/5775447)) +* **core** Relax validation of custom process states ([cf301eb](https://github.com/vendure-ecommerce/vendure/commit/cf301eb)) + ## 2.1.3 (2023-11-17) #### Security diff --git a/lerna.json b/lerna.json index f8495c408b..41a9399532 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "packages": ["packages/*"], - "version": "2.1.3", + "version": "2.1.4", "npmClient": "yarn", "command": { "version": { diff --git a/packages/admin-ui-plugin/package.json b/packages/admin-ui-plugin/package.json index 7dc0715c8e..5b9d054f11 100644 --- a/packages/admin-ui-plugin/package.json +++ b/packages/admin-ui-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@vendure/admin-ui-plugin", - "version": "2.1.3", + "version": "2.1.4", "main": "lib/index.js", "types": "lib/index.d.ts", "files": [ @@ -21,8 +21,8 @@ "devDependencies": { "@types/express": "^4.17.8", "@types/fs-extra": "^9.0.1", - "@vendure/common": "^2.1.3", - "@vendure/core": "^2.1.3", + "@vendure/common": "^2.1.4", + "@vendure/core": "^2.1.4", "express": "^4.17.1", "rimraf": "^3.0.2", "typescript": "4.9.5" diff --git a/packages/admin-ui/package-lock.json b/packages/admin-ui/package-lock.json index d2476b7260..2bf866d7b9 100644 --- a/packages/admin-ui/package-lock.json +++ b/packages/admin-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "@vendure/admin-ui", - "version": "2.1.3", + "version": "2.1.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/admin-ui/package.json b/packages/admin-ui/package.json index 236e5858c2..b269ac3729 100644 --- a/packages/admin-ui/package.json +++ b/packages/admin-ui/package.json @@ -1,6 +1,6 @@ { "name": "@vendure/admin-ui", - "version": "2.1.3", + "version": "2.1.4", "license": "MIT", "scripts": { "ng": "ng", @@ -49,7 +49,7 @@ "@ng-select/ng-select": "^11.1.1", "@ngx-translate/core": "^15.0.0", "@ngx-translate/http-loader": "^8.0.0", - "@vendure/common": "^2.1.3", + "@vendure/common": "^2.1.4", "@webcomponents/custom-elements": "^1.6.0", "apollo-angular": "^5.0.0", "apollo-upload-client": "^17.0.0", diff --git a/packages/admin-ui/src/lib/core/src/common/version.ts b/packages/admin-ui/src/lib/core/src/common/version.ts index faca0350d9..07b9507588 100644 --- a/packages/admin-ui/src/lib/core/src/common/version.ts +++ b/packages/admin-ui/src/lib/core/src/common/version.ts @@ -1,2 +1,2 @@ // Auto-generated by the set-version.js script. -export const ADMIN_UI_VERSION = '2.1.3'; +export const ADMIN_UI_VERSION = '2.1.4'; diff --git a/packages/asset-server-plugin/package.json b/packages/asset-server-plugin/package.json index 6b56735258..8289a15bcc 100644 --- a/packages/asset-server-plugin/package.json +++ b/packages/asset-server-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@vendure/asset-server-plugin", - "version": "2.1.3", + "version": "2.1.4", "main": "lib/index.js", "types": "lib/index.d.ts", "files": [ @@ -27,8 +27,8 @@ "@types/fs-extra": "^11.0.1", "@types/node-fetch": "^2.5.8", "@types/sharp": "^0.30.4", - "@vendure/common": "^2.1.3", - "@vendure/core": "^2.1.3", + "@vendure/common": "^2.1.4", + "@vendure/core": "^2.1.4", "express": "^4.17.1", "node-fetch": "^2.6.7", "rimraf": "^3.0.2", diff --git a/packages/cli/package.json b/packages/cli/package.json index 57f9186d99..f69dcc0311 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@vendure/cli", - "version": "2.1.3", + "version": "2.1.4", "description": "A modern, headless ecommerce framework", "repository": { "type": "git", @@ -34,7 +34,7 @@ ], "dependencies": { "@clack/prompts": "^0.7.0", - "@vendure/common": "^2.1.3", + "@vendure/common": "^2.1.4", "change-case": "^4.1.2", "commander": "^11.0.0", "fs-extra": "^11.1.1", diff --git a/packages/common/package.json b/packages/common/package.json index 9753924532..d3c0115e97 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,6 +1,6 @@ { "name": "@vendure/common", - "version": "2.1.3", + "version": "2.1.4", "main": "index.js", "license": "MIT", "scripts": { diff --git a/packages/core/package.json b/packages/core/package.json index 5d9deae6dc..c34ab3644a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@vendure/core", - "version": "2.1.3", + "version": "2.1.4", "description": "A modern, headless ecommerce framework", "repository": { "type": "git", @@ -50,7 +50,7 @@ "@nestjs/testing": "10.2.1", "@nestjs/typeorm": "10.0.0", "@types/fs-extra": "^9.0.1", - "@vendure/common": "^2.1.3", + "@vendure/common": "^2.1.4", "bcrypt": "^5.1.1", "body-parser": "^1.20.2", "chalk": "^4.1.2", diff --git a/packages/create/package.json b/packages/create/package.json index 449bb2e7e8..255876c5c3 100644 --- a/packages/create/package.json +++ b/packages/create/package.json @@ -1,6 +1,6 @@ { "name": "@vendure/create", - "version": "2.1.3", + "version": "2.1.4", "license": "MIT", "bin": { "create": "./index.js" @@ -28,14 +28,14 @@ "@types/fs-extra": "^9.0.1", "@types/handlebars": "^4.1.0", "@types/semver": "^6.2.2", - "@vendure/core": "^2.1.3", + "@vendure/core": "^2.1.4", "rimraf": "^3.0.2", "ts-node": "^10.9.1", "typescript": "4.9.5" }, "dependencies": { "@clack/prompts": "^0.7.0", - "@vendure/common": "^2.1.3", + "@vendure/common": "^2.1.4", "commander": "^11.0.0", "cross-spawn": "^7.0.3", "detect-port": "^1.5.1", diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index d8b76a6a39..deeb4cdb76 100644 --- a/packages/dev-server/package.json +++ b/packages/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "dev-server", - "version": "2.1.3", + "version": "2.1.4", "main": "index.js", "license": "MIT", "private": true, @@ -15,18 +15,18 @@ }, "dependencies": { "@nestjs/axios": "^3.0.0", - "@vendure/admin-ui-plugin": "^2.1.3", - "@vendure/asset-server-plugin": "^2.1.3", - "@vendure/common": "^2.1.3", - "@vendure/core": "^2.1.3", - "@vendure/elasticsearch-plugin": "^2.1.3", - "@vendure/email-plugin": "^2.1.3", + "@vendure/admin-ui-plugin": "^2.1.4", + "@vendure/asset-server-plugin": "^2.1.4", + "@vendure/common": "^2.1.4", + "@vendure/core": "^2.1.4", + "@vendure/elasticsearch-plugin": "^2.1.4", + "@vendure/email-plugin": "^2.1.4", "typescript": "4.9.5" }, "devDependencies": { "@types/csv-stringify": "^3.1.0", - "@vendure/testing": "^2.1.3", - "@vendure/ui-devkit": "^2.1.3", + "@vendure/testing": "^2.1.4", + "@vendure/ui-devkit": "^2.1.4", "commander": "^7.1.0", "concurrently": "^8.2.1", "csv-stringify": "^5.3.3", diff --git a/packages/elasticsearch-plugin/package.json b/packages/elasticsearch-plugin/package.json index ba5ff8f01e..8c019262ca 100644 --- a/packages/elasticsearch-plugin/package.json +++ b/packages/elasticsearch-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@vendure/elasticsearch-plugin", - "version": "2.1.3", + "version": "2.1.4", "license": "MIT", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -26,8 +26,8 @@ "fast-deep-equal": "^3.1.3" }, "devDependencies": { - "@vendure/common": "^2.1.3", - "@vendure/core": "^2.1.3", + "@vendure/common": "^2.1.4", + "@vendure/core": "^2.1.4", "rimraf": "^3.0.2", "typescript": "4.9.5" } diff --git a/packages/email-plugin/package.json b/packages/email-plugin/package.json index f166f90c61..537524cac4 100644 --- a/packages/email-plugin/package.json +++ b/packages/email-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@vendure/email-plugin", - "version": "2.1.3", + "version": "2.1.4", "license": "MIT", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -35,8 +35,8 @@ "@types/fs-extra": "^9.0.1", "@types/handlebars": "^4.1.0", "@types/mjml": "^4.0.4", - "@vendure/common": "^2.1.3", - "@vendure/core": "^2.1.3", + "@vendure/common": "^2.1.4", + "@vendure/core": "^2.1.4", "rimraf": "^3.0.2", "typescript": "4.9.5" } diff --git a/packages/harden-plugin/package.json b/packages/harden-plugin/package.json index 2e9001e2af..b68c63d9b0 100644 --- a/packages/harden-plugin/package.json +++ b/packages/harden-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@vendure/harden-plugin", - "version": "2.1.3", + "version": "2.1.4", "license": "MIT", "main": "lib/index.js", "types": "lib/index.d.ts", @@ -21,7 +21,7 @@ "graphql-query-complexity": "^0.12.0" }, "devDependencies": { - "@vendure/common": "^2.1.3", - "@vendure/core": "^2.1.3" + "@vendure/common": "^2.1.4", + "@vendure/core": "^2.1.4" } } diff --git a/packages/job-queue-plugin/package.json b/packages/job-queue-plugin/package.json index 0a49d00c6f..36d75cd2e8 100644 --- a/packages/job-queue-plugin/package.json +++ b/packages/job-queue-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@vendure/job-queue-plugin", - "version": "2.1.3", + "version": "2.1.4", "license": "MIT", "main": "package/index.js", "types": "package/index.d.ts", @@ -23,8 +23,8 @@ }, "devDependencies": { "@google-cloud/pubsub": "^2.8.0", - "@vendure/common": "^2.1.3", - "@vendure/core": "^2.1.3", + "@vendure/common": "^2.1.4", + "@vendure/core": "^2.1.4", "bullmq": "^3.15.5", "ioredis": "^5.3.0", "rimraf": "^3.0.2", diff --git a/packages/payments-plugin/package.json b/packages/payments-plugin/package.json index 5b0be1d505..2a1dc0d135 100644 --- a/packages/payments-plugin/package.json +++ b/packages/payments-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@vendure/payments-plugin", - "version": "2.1.3", + "version": "2.1.4", "license": "MIT", "main": "package/index.js", "types": "package/index.d.ts", @@ -46,9 +46,9 @@ "@mollie/api-client": "^3.7.0", "@types/braintree": "^2.22.15", "@types/localtunnel": "2.0.1", - "@vendure/common": "^2.1.3", - "@vendure/core": "^2.1.3", - "@vendure/testing": "^2.1.3", + "@vendure/common": "^2.1.4", + "@vendure/core": "^2.1.4", + "@vendure/testing": "^2.1.4", "braintree": "^3.16.0", "localtunnel": "2.0.2", "nock": "^13.1.4", diff --git a/packages/testing/package.json b/packages/testing/package.json index 763fd8c8ef..97b82aa9ef 100644 --- a/packages/testing/package.json +++ b/packages/testing/package.json @@ -1,6 +1,6 @@ { "name": "@vendure/testing", - "version": "2.1.3", + "version": "2.1.4", "description": "End-to-end testing tools for Vendure projects", "keywords": [ "vendure", @@ -38,7 +38,7 @@ "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", "@types/node-fetch": "^2.6.4", - "@vendure/common": "^2.1.3", + "@vendure/common": "^2.1.4", "faker": "^4.1.0", "form-data": "^4.0.0", "graphql": "16.8.0", @@ -49,7 +49,7 @@ "devDependencies": { "@types/mysql": "^2.15.15", "@types/pg": "^7.14.5", - "@vendure/core": "^2.1.3", + "@vendure/core": "^2.1.4", "mysql": "^2.18.1", "pg": "^8.4.0", "rimraf": "^3.0.0", diff --git a/packages/ui-devkit/package.json b/packages/ui-devkit/package.json index 8176e27735..013cad6fd7 100644 --- a/packages/ui-devkit/package.json +++ b/packages/ui-devkit/package.json @@ -1,6 +1,6 @@ { "name": "@vendure/ui-devkit", - "version": "2.1.3", + "version": "2.1.4", "description": "A library for authoring Vendure Admin UI extensions", "keywords": [ "vendure", @@ -40,8 +40,8 @@ "@angular/cli": "^16.2.0", "@angular/compiler": "^16.2.2", "@angular/compiler-cli": "^16.2.2", - "@vendure/admin-ui": "^2.1.3", - "@vendure/common": "^2.1.3", + "@vendure/admin-ui": "^2.1.4", + "@vendure/common": "^2.1.4", "chalk": "^4.1.0", "chokidar": "^3.5.3", "fs-extra": "^11.1.1", @@ -51,7 +51,7 @@ "devDependencies": { "@rollup/plugin-node-resolve": "^15.2.1", "@types/fs-extra": "^11.0.1", - "@vendure/core": "^2.1.3", + "@vendure/core": "^2.1.4", "react": "^18.2.0", "react-dom": "^18.2.0", "rimraf": "^3.0.2",