From 2a7b3b8ca13d541c3b3670e48808b2caec40f382 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Thu, 16 Jan 2025 09:53:44 +0100 Subject: [PATCH 01/17] =?UTF-8?q?=F0=9F=9A=80=20Release=20apps=20(#1688)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: github-actions[bot] --- .changeset/chilly-doors-film.md | 11 ----------- .changeset/empty-ligers-brush.md | 9 --------- .changeset/rotten-seals-suffer.md | 10 ---------- .changeset/silver-mangos-behave.md | 18 ------------------ .changeset/six-lamps-shop.md | 5 ----- apps/avatax/CHANGELOG.md | 17 +++++++++++++++++ apps/avatax/package.json | 2 +- apps/cms-v2/CHANGELOG.md | 16 ++++++++++++++++ apps/cms-v2/package.json | 2 +- apps/klaviyo/CHANGELOG.md | 15 +++++++++++++++ apps/klaviyo/package.json | 2 +- apps/products-feed/CHANGELOG.md | 17 +++++++++++++++++ apps/products-feed/package.json | 2 +- apps/search/CHANGELOG.md | 17 +++++++++++++++++ apps/search/package.json | 2 +- apps/segment/CHANGELOG.md | 14 ++++++++++++++ apps/segment/package.json | 2 +- apps/smtp/CHANGELOG.md | 16 ++++++++++++++++ apps/smtp/package.json | 2 +- packages/e2e/CHANGELOG.md | 6 ++++++ packages/e2e/package.json | 2 +- packages/logger/CHANGELOG.md | 7 +++++++ packages/logger/package.json | 2 +- packages/otel/CHANGELOG.md | 6 ++++++ packages/otel/package.json | 2 +- packages/react-hook-form-macaw/CHANGELOG.md | 6 ++++++ packages/react-hook-form-macaw/package.json | 2 +- packages/shared/CHANGELOG.md | 6 ++++++ packages/shared/package.json | 2 +- packages/trpc/CHANGELOG.md | 8 ++++++++ packages/trpc/package.json | 2 +- packages/ui/CHANGELOG.md | 6 ++++++ packages/ui/package.json | 2 +- packages/webhook-utils/CHANGELOG.md | 6 ++++++ packages/webhook-utils/package.json | 2 +- 35 files changed, 178 insertions(+), 68 deletions(-) delete mode 100644 .changeset/chilly-doors-film.md delete mode 100644 .changeset/empty-ligers-brush.md delete mode 100644 .changeset/rotten-seals-suffer.md delete mode 100644 .changeset/silver-mangos-behave.md delete mode 100644 .changeset/six-lamps-shop.md create mode 100644 apps/segment/CHANGELOG.md diff --git a/.changeset/chilly-doors-film.md b/.changeset/chilly-doors-film.md deleted file mode 100644 index a329e10571..0000000000 --- a/.changeset/chilly-doors-film.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"products-feed": patch -"@saleor/apps-logger": patch -"klaviyo": patch -"app-avatax": patch -"cms-v2": patch -"search": patch -"smtp": patch ---- - -Increased Vercel log limit to new value - 256KB. See [announcement](https://vercel.com/changelog/updated-logging-limits-for-vercel-functions) blog post from Vercel for more details. diff --git a/.changeset/empty-ligers-brush.md b/.changeset/empty-ligers-brush.md deleted file mode 100644 index 7984c1db79..0000000000 --- a/.changeset/empty-ligers-brush.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"products-feed": patch -"klaviyo": patch -"cms-v2": patch -"search": patch -"smtp": patch ---- - -Added new `LoggerVercelTransport` support. It will help us send logs to our infrastructure without need of OTEL unstable logs API. diff --git a/.changeset/rotten-seals-suffer.md b/.changeset/rotten-seals-suffer.md deleted file mode 100644 index 285b4c5023..0000000000 --- a/.changeset/rotten-seals-suffer.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -"products-feed": patch -"klaviyo": patch -"app-avatax": patch -"cms-v2": patch -"search": patch -"smtp": patch ---- - -Escape ALLOWED_DOMAIN_PATTERN regex. It ensures that regex constructed from env variable is sanitized and can't be used to Denial of Service attack. diff --git a/.changeset/silver-mangos-behave.md b/.changeset/silver-mangos-behave.md deleted file mode 100644 index 361b3d8d20..0000000000 --- a/.changeset/silver-mangos-behave.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@saleor/react-hook-form-macaw": patch -"@saleor/webhook-utils": patch -"products-feed": patch -"@saleor/apps-logger": patch -"@saleor/apps-shared": patch -"@saleor/apps-otel": patch -"@saleor/trpc": patch -"klaviyo": patch -"@saleor/e2e": patch -"app-avatax": patch -"cms-v2": patch -"search": patch -"@saleor/apps-ui": patch -"smtp": patch ---- - -Fixed autofixable linting issues. No functional changes. diff --git a/.changeset/six-lamps-shop.md b/.changeset/six-lamps-shop.md deleted file mode 100644 index 1f7973d519..0000000000 --- a/.changeset/six-lamps-shop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"app-avatax": patch ---- - -Add log when suspicious calculation happen - when line tax rate is non-zero but amount of taxes is zero diff --git a/apps/avatax/CHANGELOG.md b/apps/avatax/CHANGELOG.md index c536506af8..d34a6590e2 100644 --- a/apps/avatax/CHANGELOG.md +++ b/apps/avatax/CHANGELOG.md @@ -1,5 +1,22 @@ # app-avatax +## 1.12.4 + +### Patch Changes + +- 9bbf9ee5: Increased Vercel log limit to new value - 256KB. See [announcement](https://vercel.com/changelog/updated-logging-limits-for-vercel-functions) blog post from Vercel for more details. +- 9bbf9ee5: Escape ALLOWED_DOMAIN_PATTERN regex. It ensures that regex constructed from env variable is sanitized and can't be used to Denial of Service attack. +- 9bbf9ee5: Fixed autofixable linting issues. No functional changes. +- 9bbf9ee5: Add log when suspicious calculation happen - when line tax rate is non-zero but amount of taxes is zero +- Updated dependencies [9bbf9ee5] +- Updated dependencies [9bbf9ee5] + - @saleor/apps-logger@1.4.3 + - @saleor/react-hook-form-macaw@0.2.12 + - @saleor/webhook-utils@0.2.3 + - @saleor/apps-shared@1.11.4 + - @saleor/apps-otel@1.3.5 + - @saleor/apps-ui@1.2.10 + ## 1.12.3 ### Patch Changes diff --git a/apps/avatax/package.json b/apps/avatax/package.json index bc98a7328f..ee04a9af5d 100644 --- a/apps/avatax/package.json +++ b/apps/avatax/package.json @@ -1,6 +1,6 @@ { "name": "app-avatax", - "version": "1.12.3", + "version": "1.12.4", "scripts": { "build": " next build", "check-types": "tsc --noEmit", diff --git a/apps/cms-v2/CHANGELOG.md b/apps/cms-v2/CHANGELOG.md index af70db5bde..06a03b2188 100644 --- a/apps/cms-v2/CHANGELOG.md +++ b/apps/cms-v2/CHANGELOG.md @@ -1,5 +1,21 @@ # saleor-app-cms-v2 +## 2.9.17 + +### Patch Changes + +- 9bbf9ee5: Increased Vercel log limit to new value - 256KB. See [announcement](https://vercel.com/changelog/updated-logging-limits-for-vercel-functions) blog post from Vercel for more details. +- 9bbf9ee5: Added new `LoggerVercelTransport` support. It will help us send logs to our infrastructure without need of OTEL unstable logs API. +- 9bbf9ee5: Escape ALLOWED_DOMAIN_PATTERN regex. It ensures that regex constructed from env variable is sanitized and can't be used to Denial of Service attack. +- 9bbf9ee5: Fixed autofixable linting issues. No functional changes. +- Updated dependencies [9bbf9ee5] +- Updated dependencies [9bbf9ee5] + - @saleor/apps-logger@1.4.3 + - @saleor/react-hook-form-macaw@0.2.12 + - @saleor/apps-shared@1.11.4 + - @saleor/apps-otel@1.3.5 + - @saleor/apps-ui@1.2.10 + ## 2.9.16 ### Patch Changes diff --git a/apps/cms-v2/package.json b/apps/cms-v2/package.json index a4567fda3e..8358fbb8fd 100644 --- a/apps/cms-v2/package.json +++ b/apps/cms-v2/package.json @@ -1,6 +1,6 @@ { "name": "cms-v2", - "version": "2.9.16", + "version": "2.9.17", "scripts": { "build": "next build", "check-types": "tsc --noEmit", diff --git a/apps/klaviyo/CHANGELOG.md b/apps/klaviyo/CHANGELOG.md index 9d3f98ddc3..0d44e8ba44 100644 --- a/apps/klaviyo/CHANGELOG.md +++ b/apps/klaviyo/CHANGELOG.md @@ -1,5 +1,20 @@ # saleor-app-klaviyo +## 1.12.18 + +### Patch Changes + +- 9bbf9ee5: Increased Vercel log limit to new value - 256KB. See [announcement](https://vercel.com/changelog/updated-logging-limits-for-vercel-functions) blog post from Vercel for more details. +- 9bbf9ee5: Added new `LoggerVercelTransport` support. It will help us send logs to our infrastructure without need of OTEL unstable logs API. +- 9bbf9ee5: Escape ALLOWED_DOMAIN_PATTERN regex. It ensures that regex constructed from env variable is sanitized and can't be used to Denial of Service attack. +- 9bbf9ee5: Fixed autofixable linting issues. No functional changes. +- Updated dependencies [9bbf9ee5] +- Updated dependencies [9bbf9ee5] + - @saleor/apps-logger@1.4.3 + - @saleor/apps-shared@1.11.4 + - @saleor/apps-otel@1.3.5 + - @saleor/apps-ui@1.2.10 + ## 1.12.17 ### Patch Changes diff --git a/apps/klaviyo/package.json b/apps/klaviyo/package.json index dd2476e26f..60c286aece 100644 --- a/apps/klaviyo/package.json +++ b/apps/klaviyo/package.json @@ -1,6 +1,6 @@ { "name": "klaviyo", - "version": "1.12.17", + "version": "1.12.18", "scripts": { "build": "next build", "check-types": "tsc --noEmit", diff --git a/apps/products-feed/CHANGELOG.md b/apps/products-feed/CHANGELOG.md index e51907796b..8567e81db9 100644 --- a/apps/products-feed/CHANGELOG.md +++ b/apps/products-feed/CHANGELOG.md @@ -1,5 +1,22 @@ # saleor-app-products-feed +## 1.19.17 + +### Patch Changes + +- 9bbf9ee5: Increased Vercel log limit to new value - 256KB. See [announcement](https://vercel.com/changelog/updated-logging-limits-for-vercel-functions) blog post from Vercel for more details. +- 9bbf9ee5: Added new `LoggerVercelTransport` support. It will help us send logs to our infrastructure without need of OTEL unstable logs API. +- 9bbf9ee5: Escape ALLOWED_DOMAIN_PATTERN regex. It ensures that regex constructed from env variable is sanitized and can't be used to Denial of Service attack. +- 9bbf9ee5: Fixed autofixable linting issues. No functional changes. +- Updated dependencies [9bbf9ee5] +- Updated dependencies [9bbf9ee5] + - @saleor/apps-logger@1.4.3 + - @saleor/react-hook-form-macaw@0.2.12 + - @saleor/webhook-utils@0.2.3 + - @saleor/apps-shared@1.11.4 + - @saleor/apps-otel@1.3.5 + - @saleor/apps-ui@1.2.10 + ## 1.19.16 ### Patch Changes diff --git a/apps/products-feed/package.json b/apps/products-feed/package.json index 1b190b4e17..50e789dabb 100644 --- a/apps/products-feed/package.json +++ b/apps/products-feed/package.json @@ -1,6 +1,6 @@ { "name": "products-feed", - "version": "1.19.16", + "version": "1.19.17", "scripts": { "build": "next build", "check-types": "tsc --noEmit", diff --git a/apps/search/CHANGELOG.md b/apps/search/CHANGELOG.md index c435a4ce8c..84bb725100 100644 --- a/apps/search/CHANGELOG.md +++ b/apps/search/CHANGELOG.md @@ -1,5 +1,22 @@ # saleor-app-search +## 1.22.19 + +### Patch Changes + +- 9bbf9ee5: Increased Vercel log limit to new value - 256KB. See [announcement](https://vercel.com/changelog/updated-logging-limits-for-vercel-functions) blog post from Vercel for more details. +- 9bbf9ee5: Added new `LoggerVercelTransport` support. It will help us send logs to our infrastructure without need of OTEL unstable logs API. +- 9bbf9ee5: Escape ALLOWED_DOMAIN_PATTERN regex. It ensures that regex constructed from env variable is sanitized and can't be used to Denial of Service attack. +- 9bbf9ee5: Fixed autofixable linting issues. No functional changes. +- Updated dependencies [9bbf9ee5] +- Updated dependencies [9bbf9ee5] + - @saleor/apps-logger@1.4.3 + - @saleor/react-hook-form-macaw@0.2.12 + - @saleor/webhook-utils@0.2.3 + - @saleor/apps-shared@1.11.4 + - @saleor/apps-otel@1.3.5 + - @saleor/apps-ui@1.2.10 + ## 1.22.18 ### Patch Changes diff --git a/apps/search/package.json b/apps/search/package.json index 1b7ae80214..64cb904ad8 100644 --- a/apps/search/package.json +++ b/apps/search/package.json @@ -1,6 +1,6 @@ { "name": "search", - "version": "1.22.18", + "version": "1.22.19", "scripts": { "build": "next build", "check-types": "tsc --noEmit", diff --git a/apps/segment/CHANGELOG.md b/apps/segment/CHANGELOG.md new file mode 100644 index 0000000000..ae8d8a5a25 --- /dev/null +++ b/apps/segment/CHANGELOG.md @@ -0,0 +1,14 @@ +# segment + +## 2.0.1 + +### Patch Changes + +- Updated dependencies [9bbf9ee5] +- Updated dependencies [9bbf9ee5] + - @saleor/apps-logger@1.4.3 + - @saleor/react-hook-form-macaw@0.2.12 + - @saleor/webhook-utils@0.2.3 + - @saleor/apps-shared@1.11.4 + - @saleor/apps-otel@1.3.5 + - @saleor/apps-ui@1.2.10 diff --git a/apps/segment/package.json b/apps/segment/package.json index ad4e900f31..0299df7534 100644 --- a/apps/segment/package.json +++ b/apps/segment/package.json @@ -1,6 +1,6 @@ { "name": "segment", - "version": "2.0.0", + "version": "2.0.1", "scripts": { "build": "next build", "check-types": "tsc --noEmit", diff --git a/apps/smtp/CHANGELOG.md b/apps/smtp/CHANGELOG.md index e51a70643f..da952286cb 100644 --- a/apps/smtp/CHANGELOG.md +++ b/apps/smtp/CHANGELOG.md @@ -1,5 +1,21 @@ # smtp +## 1.2.20 + +### Patch Changes + +- 9bbf9ee5: Increased Vercel log limit to new value - 256KB. See [announcement](https://vercel.com/changelog/updated-logging-limits-for-vercel-functions) blog post from Vercel for more details. +- 9bbf9ee5: Added new `LoggerVercelTransport` support. It will help us send logs to our infrastructure without need of OTEL unstable logs API. +- 9bbf9ee5: Escape ALLOWED_DOMAIN_PATTERN regex. It ensures that regex constructed from env variable is sanitized and can't be used to Denial of Service attack. +- 9bbf9ee5: Fixed autofixable linting issues. No functional changes. +- Updated dependencies [9bbf9ee5] +- Updated dependencies [9bbf9ee5] + - @saleor/apps-logger@1.4.3 + - @saleor/react-hook-form-macaw@0.2.12 + - @saleor/apps-shared@1.11.4 + - @saleor/apps-otel@1.3.5 + - @saleor/apps-ui@1.2.10 + ## 1.2.19 ### Patch Changes diff --git a/apps/smtp/package.json b/apps/smtp/package.json index 0c7cc1a3d8..bd920076e7 100644 --- a/apps/smtp/package.json +++ b/apps/smtp/package.json @@ -1,6 +1,6 @@ { "name": "smtp", - "version": "1.2.19", + "version": "1.2.20", "scripts": { "build": "next build", "check-types": "tsc --noEmit", diff --git a/packages/e2e/CHANGELOG.md b/packages/e2e/CHANGELOG.md index 9b43d1cefb..b01c93ab3c 100644 --- a/packages/e2e/CHANGELOG.md +++ b/packages/e2e/CHANGELOG.md @@ -1,5 +1,11 @@ # @saleor/e2e +## 1.0.7 + +### Patch Changes + +- 9bbf9ee5: Fixed autofixable linting issues. No functional changes. + ## 1.0.6 ### Patch Changes diff --git a/packages/e2e/package.json b/packages/e2e/package.json index 531eea06ec..472e6c0111 100644 --- a/packages/e2e/package.json +++ b/packages/e2e/package.json @@ -1,7 +1,7 @@ { "name": "@saleor/e2e", "description": "", - "version": "1.0.6", + "version": "1.0.7", "author": "", "scripts": { "check-types": "tsc --noEmit", diff --git a/packages/logger/CHANGELOG.md b/packages/logger/CHANGELOG.md index e8430073b0..ac945152b8 100644 --- a/packages/logger/CHANGELOG.md +++ b/packages/logger/CHANGELOG.md @@ -1,5 +1,12 @@ # @saleor/apps-logger +## 1.4.3 + +### Patch Changes + +- 9bbf9ee5: Increased Vercel log limit to new value - 256KB. See [announcement](https://vercel.com/changelog/updated-logging-limits-for-vercel-functions) blog post from Vercel for more details. +- 9bbf9ee5: Fixed autofixable linting issues. No functional changes. + ## 1.4.2 ### Patch Changes diff --git a/packages/logger/package.json b/packages/logger/package.json index 2638cf00be..c96c0628e8 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@saleor/apps-logger", - "version": "1.4.2", + "version": "1.4.3", "scripts": { "check-types": "tsc --noEmit", "lint": "eslint .", diff --git a/packages/otel/CHANGELOG.md b/packages/otel/CHANGELOG.md index 64108c2e7d..ed7117d55f 100644 --- a/packages/otel/CHANGELOG.md +++ b/packages/otel/CHANGELOG.md @@ -1,5 +1,11 @@ # @saleor/apps-otel +## 1.3.5 + +### Patch Changes + +- 9bbf9ee5: Fixed autofixable linting issues. No functional changes. + ## 1.3.4 ### Patch Changes diff --git a/packages/otel/package.json b/packages/otel/package.json index 42875ff99c..683afbd9a6 100644 --- a/packages/otel/package.json +++ b/packages/otel/package.json @@ -1,6 +1,6 @@ { "name": "@saleor/apps-otel", - "version": "1.3.4", + "version": "1.3.5", "scripts": { "check-types": "tsc --noEmit", "lint": "eslint .", diff --git a/packages/react-hook-form-macaw/CHANGELOG.md b/packages/react-hook-form-macaw/CHANGELOG.md index 8f68676516..6ecf34b183 100644 --- a/packages/react-hook-form-macaw/CHANGELOG.md +++ b/packages/react-hook-form-macaw/CHANGELOG.md @@ -1,5 +1,11 @@ # @saleor/react-hook-form-macaw +## 0.2.12 + +### Patch Changes + +- 9bbf9ee5: Fixed autofixable linting issues. No functional changes. + ## 0.2.11 ### Patch Changes diff --git a/packages/react-hook-form-macaw/package.json b/packages/react-hook-form-macaw/package.json index efbdd5c24f..fdf0661344 100644 --- a/packages/react-hook-form-macaw/package.json +++ b/packages/react-hook-form-macaw/package.json @@ -1,6 +1,6 @@ { "name": "@saleor/react-hook-form-macaw", - "version": "0.2.11", + "version": "0.2.12", "scripts": { "build-storybook": "storybook build", "check-types": "tsc --noEmit", diff --git a/packages/shared/CHANGELOG.md b/packages/shared/CHANGELOG.md index 3f97f74bc7..d6eade08e5 100644 --- a/packages/shared/CHANGELOG.md +++ b/packages/shared/CHANGELOG.md @@ -1,5 +1,11 @@ # @saleor/apps-shared +## 1.11.4 + +### Patch Changes + +- 9bbf9ee5: Fixed autofixable linting issues. No functional changes. + ## 1.11.3 ### Patch Changes diff --git a/packages/shared/package.json b/packages/shared/package.json index 6ded549819..3c9bcf70c3 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@saleor/apps-shared", - "version": "1.11.3", + "version": "1.11.4", "scripts": { "check-types": "tsc --noEmit", "lint": "eslint .", diff --git a/packages/trpc/CHANGELOG.md b/packages/trpc/CHANGELOG.md index 9da7262280..1ebbd83250 100644 --- a/packages/trpc/CHANGELOG.md +++ b/packages/trpc/CHANGELOG.md @@ -1,5 +1,13 @@ # @saleor/trpc +## 3.0.4 + +### Patch Changes + +- 9bbf9ee5: Fixed autofixable linting issues. No functional changes. +- Updated dependencies [9bbf9ee5] + - @saleor/apps-shared@1.11.4 + ## 3.0.3 ### Patch Changes diff --git a/packages/trpc/package.json b/packages/trpc/package.json index 4b5faf71e9..88ef4b38ab 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -1,6 +1,6 @@ { "name": "@saleor/trpc", - "version": "3.0.3", + "version": "3.0.4", "scripts": { "check-types": "tsc --noEmit", "lint": "eslint .", diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 9d1d439ef8..8f2a62f3a4 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,5 +1,11 @@ # @saleor/apps-ui +## 1.2.10 + +### Patch Changes + +- 9bbf9ee5: Fixed autofixable linting issues. No functional changes. + ## 1.2.9 ### Patch Changes diff --git a/packages/ui/package.json b/packages/ui/package.json index eb9ae5021a..8f4725481a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@saleor/apps-ui", - "version": "1.2.9", + "version": "1.2.10", "scripts": { "check-types": "tsc --noEmit", "lint": "eslint .", diff --git a/packages/webhook-utils/CHANGELOG.md b/packages/webhook-utils/CHANGELOG.md index f13410d637..35000ffab9 100644 --- a/packages/webhook-utils/CHANGELOG.md +++ b/packages/webhook-utils/CHANGELOG.md @@ -1,5 +1,11 @@ # @saleor/webhook-utils +## 0.2.3 + +### Patch Changes + +- 9bbf9ee5: Fixed autofixable linting issues. No functional changes. + ## 0.2.2 ### Patch Changes diff --git a/packages/webhook-utils/package.json b/packages/webhook-utils/package.json index 1a9f5575aa..433254b6eb 100644 --- a/packages/webhook-utils/package.json +++ b/packages/webhook-utils/package.json @@ -1,6 +1,6 @@ { "name": "@saleor/webhook-utils", - "version": "0.2.2", + "version": "0.2.3", "scripts": { "check-types": "tsc --noEmit", "fetch-schema": "curl https://raw.githubusercontent.com/saleor/saleor/${npm_package_saleor_schemaVersion}/saleor/graphql/schema.graphql > graphql/schema.graphql", From ba5a1338a876e78bbf983c34e920d10053a39a06 Mon Sep 17 00:00:00 2001 From: Mikail <6186720+NyanKiyoshi@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:56:52 +0100 Subject: [PATCH 02/17] Add workflow to check licenses (#1693) --- .github/workflows/check-licenses.yaml | 45 +++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 46 insertions(+) create mode 100644 .github/workflows/check-licenses.yaml diff --git a/.github/workflows/check-licenses.yaml b/.github/workflows/check-licenses.yaml new file mode 100644 index 0000000000..be3bc1b67a --- /dev/null +++ b/.github/workflows/check-licenses.yaml @@ -0,0 +1,45 @@ +name: Check Licenses +on: + pull_request: + types: + - opened + - synchronize + # Labels are needed to handle external contributors + - labeled + - unlabeled + paths: + # Self + - ".github/workflows/check-licenses.yaml" + # JS/TS Ecosystem + - "**/package.json" + - "**/pnpm-lock.yaml" + - "**/package-lock.json" + +jobs: + default: + permissions: + contents: read + pull-requests: write + uses: saleor/saleor-internal-actions/.github/workflows/run-license-check.yaml@v1 + with: + # List of ecosystems to scan. + ecosystems: >- + javascript + # Grant rules (https://github.com/anchore/grant/blob/4362dc22cf5ea9baeccfa59b2863879afe0c30d7/README.md#usage) + rules: | + # Explicitly allow LGPL as "*GPL*" rule will cause to reject them otherwise. + - pattern: "*lgpl*" + name: "allow-lgpl" + mode: "allow" + reason: "LGPL is allowed." + - pattern: "*gpl*" + name: "deny-gpl" + mode: "deny" + reason: "GPL licenses are not compatible with BSD-3-Clause" + exceptions: + # store2 is under a dual license (MIT OR GPL-3.0), thus is compatible with our project. + # License metadata (for v2.14.2): https://github.com/nbubna/store/blob/20cce53b83b5870b6715fa929e4aa773cfa5e179/package.json#L32 + - store2 + - pattern: "*proprietary*" + name: "deny-proprietary" + mode: "deny" diff --git a/package.json b/package.json index 6eb991abf8..6b7b7bbd8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "saleor-apps", "version": "0.0.0", + "license": "(BSD-3-Clause AND CC-BY-4.0)", "scripts": { "build": "turbo run build", "check-spelling": "cspell '**/*.{jsx,tsx,js,ts,md,mdx}'", From 0db174a882d8e76887ac0eed5f338c5d6e0e0981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:23:57 +0100 Subject: [PATCH 03/17] Fix /register issue with `ALLOWED_DOMAIN_URL` env variable (#1694) --- .changeset/angry-zebras-collect.md | 11 +++ apps/avatax/package.json | 1 - apps/avatax/src/pages/api/register.ts | 4 +- apps/cms-v2/package.json | 1 - apps/cms-v2/src/pages/api/register.ts | 4 +- apps/klaviyo/package.json | 1 - apps/klaviyo/src/pages/api/register.ts | 4 +- apps/products-feed/package.json | 1 - apps/products-feed/src/pages/api/register.ts | 4 +- apps/search/package.json | 1 - apps/search/src/pages/api/register.ts | 4 +- apps/segment/package.json | 1 - apps/segment/src/pages/api/register.ts | 4 +- apps/smtp/package.json | 1 - apps/smtp/src/pages/api/register.ts | 4 +- pnpm-lock.yaml | 74 ++++++++------------ 16 files changed, 54 insertions(+), 66 deletions(-) create mode 100644 .changeset/angry-zebras-collect.md diff --git a/.changeset/angry-zebras-collect.md b/.changeset/angry-zebras-collect.md new file mode 100644 index 0000000000..1e4db74d6e --- /dev/null +++ b/.changeset/angry-zebras-collect.md @@ -0,0 +1,11 @@ +--- +"products-feed": patch +"klaviyo": patch +"segment": patch +"app-avatax": patch +"cms-v2": patch +"search": patch +"smtp": patch +--- + +Removed regex escape for `ALLOWED_DOMAINS_URL` env variable from register handler. It isn't user input and escaping regex was causing problem with apps installation. diff --git a/apps/avatax/package.json b/apps/avatax/package.json index ee04a9af5d..b2eb3ca8f8 100644 --- a/apps/avatax/package.json +++ b/apps/avatax/package.json @@ -60,7 +60,6 @@ "decimal.js-light": "2.5.1", "dotenv": "16.3.1", "dynamodb-toolbox": "1.8.2", - "escape-string-regexp": "5.0.0", "graphql": "16.7.1", "graphql-tag": "2.12.6", "jotai": "^2.4.2", diff --git a/apps/avatax/src/pages/api/register.ts b/apps/avatax/src/pages/api/register.ts index aa8161555e..9fc0d7ac5e 100644 --- a/apps/avatax/src/pages/api/register.ts +++ b/apps/avatax/src/pages/api/register.ts @@ -1,7 +1,6 @@ import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next"; import { wrapWithLoggerContext } from "@saleor/apps-logger/node"; import { withOtel } from "@saleor/apps-otel"; -import escapeStringRegexp from "escape-string-regexp"; import { env } from "@/env"; import { createLogger } from "@/logger"; @@ -28,7 +27,8 @@ export default wrapWithLoggerContext( allowedSaleorUrls: [ (url) => { if (allowedUrlsPattern) { - const regex = new RegExp(escapeStringRegexp(allowedUrlsPattern)); + // we don't escape the pattern because it's not user input - it's an ENV variable controlled by us + const regex = new RegExp(allowedUrlsPattern); return regex.test(url); } diff --git a/apps/cms-v2/package.json b/apps/cms-v2/package.json index 8358fbb8fd..d4773555e2 100644 --- a/apps/cms-v2/package.json +++ b/apps/cms-v2/package.json @@ -49,7 +49,6 @@ "@vitejs/plugin-react": "4.3.1", "contentful-management": "10.46.4", "dotenv": "16.3.1", - "escape-string-regexp": "5.0.0", "graphql": "16.7.1", "graphql-tag": "2.12.6", "jsdom": "^20.0.3", diff --git a/apps/cms-v2/src/pages/api/register.ts b/apps/cms-v2/src/pages/api/register.ts index 55737b36ba..2f9d95829b 100644 --- a/apps/cms-v2/src/pages/api/register.ts +++ b/apps/cms-v2/src/pages/api/register.ts @@ -1,7 +1,6 @@ import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next"; import { wrapWithLoggerContext } from "@saleor/apps-logger/node"; import { withOtel } from "@saleor/apps-otel"; -import escapeStringRegexp from "escape-string-regexp"; import { saleorApp } from "@/saleor-app"; @@ -18,7 +17,8 @@ const handler = createAppRegisterHandler({ allowedSaleorUrls: [ (url) => { if (allowedUrlsPattern) { - const regex = new RegExp(escapeStringRegexp(allowedUrlsPattern)); + // we don't escape the pattern because it's not user input - it's an ENV variable controlled by us + const regex = new RegExp(allowedUrlsPattern); return regex.test(url); } diff --git a/apps/klaviyo/package.json b/apps/klaviyo/package.json index 60c286aece..06ecd61680 100644 --- a/apps/klaviyo/package.json +++ b/apps/klaviyo/package.json @@ -37,7 +37,6 @@ "@sentry/nextjs": "../../node_modules/@sentry/nextjs", "@urql/exchange-auth": "2.1.4", "dotenv": "16.3.1", - "escape-string-regexp": "5.0.0", "graphql": "16.7.1", "graphql-tag": "2.12.6", "next": "14.2.3", diff --git a/apps/klaviyo/src/pages/api/register.ts b/apps/klaviyo/src/pages/api/register.ts index bac0f83ac8..46c329e481 100644 --- a/apps/klaviyo/src/pages/api/register.ts +++ b/apps/klaviyo/src/pages/api/register.ts @@ -1,7 +1,6 @@ import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next"; import { wrapWithLoggerContext } from "@saleor/apps-logger/node"; import { withOtel } from "@saleor/apps-otel"; -import escapeStringRegexp from "escape-string-regexp"; import { saleorApp } from "../../../saleor-app"; import { loggerContext } from "../../logger-context"; @@ -17,7 +16,8 @@ const handler = createAppRegisterHandler({ allowedSaleorUrls: [ (url) => { if (allowedUrlsPattern) { - const regex = new RegExp(escapeStringRegexp(allowedUrlsPattern)); + // we don't escape the pattern because it's not user input - it's an ENV variable controlled by us + const regex = new RegExp(allowedUrlsPattern); return regex.test(url); } diff --git a/apps/products-feed/package.json b/apps/products-feed/package.json index 50e789dabb..70c7904b90 100644 --- a/apps/products-feed/package.json +++ b/apps/products-feed/package.json @@ -48,7 +48,6 @@ "@urql/exchange-auth": "2.1.4", "@vitejs/plugin-react": "4.3.1", "dotenv": "16.3.1", - "escape-string-regexp": "5.0.0", "fast-xml-parser": "^4.0.15", "graphql": "16.7.1", "graphql-tag": "2.12.6", diff --git a/apps/products-feed/src/pages/api/register.ts b/apps/products-feed/src/pages/api/register.ts index 7f82494e31..2d272cf0b3 100644 --- a/apps/products-feed/src/pages/api/register.ts +++ b/apps/products-feed/src/pages/api/register.ts @@ -1,7 +1,6 @@ import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next"; import { wrapWithLoggerContext } from "@saleor/apps-logger/node"; import { withOtel } from "@saleor/apps-otel"; -import escapeStringRegexp from "escape-string-regexp"; import { loggerContext } from "../../logger-context"; import { saleorApp } from "../../saleor-app"; @@ -19,7 +18,8 @@ export default wrapWithLoggerContext( allowedSaleorUrls: [ (url) => { if (allowedUrlsPattern) { - const regex = new RegExp(escapeStringRegexp(allowedUrlsPattern)); + // we don't escape the pattern because it's not user input - it's an ENV variable controlled by us + const regex = new RegExp(allowedUrlsPattern); return regex.test(url); } diff --git a/apps/search/package.json b/apps/search/package.json index 64cb904ad8..0f8a98411b 100644 --- a/apps/search/package.json +++ b/apps/search/package.json @@ -50,7 +50,6 @@ "clsx": "^1.2.1", "debug": "^4.3.4", "dotenv": "16.3.1", - "escape-string-regexp": "5.0.0", "graphql": "16.7.1", "graphql-tag": "2.12.6", "next": "14.2.3", diff --git a/apps/search/src/pages/api/register.ts b/apps/search/src/pages/api/register.ts index f8bfb94bd3..f0eba848b3 100644 --- a/apps/search/src/pages/api/register.ts +++ b/apps/search/src/pages/api/register.ts @@ -1,7 +1,6 @@ import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next"; import { wrapWithLoggerContext } from "@saleor/apps-logger/node"; import { withOtel } from "@saleor/apps-otel"; -import escapeStringRegexp from "escape-string-regexp"; import { saleorApp } from "../../../saleor-app"; import { loggerContext } from "../../lib/logger-context"; @@ -15,7 +14,8 @@ export default wrapWithLoggerContext( allowedSaleorUrls: [ (url) => { if (allowedUrlsPattern) { - const regex = new RegExp(escapeStringRegexp(allowedUrlsPattern)); + // we don't escape the pattern because it's not user input - it's an ENV variable controlled by us + const regex = new RegExp(allowedUrlsPattern); return regex.test(url); } diff --git a/apps/segment/package.json b/apps/segment/package.json index 0299df7534..b66762b0bd 100644 --- a/apps/segment/package.json +++ b/apps/segment/package.json @@ -50,7 +50,6 @@ "@urql/exchange-auth": "2.1.4", "@vitejs/plugin-react": "4.3.1", "dotenv": "16.3.1", - "escape-string-regexp": "5.0.0", "graphql": "16.7.1", "graphql-tag": "2.12.6", "modern-errors": "7.0.1", diff --git a/apps/segment/src/pages/api/register.ts b/apps/segment/src/pages/api/register.ts index 088d098b2e..331bda885e 100644 --- a/apps/segment/src/pages/api/register.ts +++ b/apps/segment/src/pages/api/register.ts @@ -1,6 +1,5 @@ import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next"; import { wrapWithLoggerContext } from "@saleor/apps-logger/node"; -import escapeStringRegexp from "escape-string-regexp"; import { env } from "@/env"; import { loggerContext } from "@/logger-context"; @@ -18,7 +17,8 @@ export default wrapWithLoggerContext( allowedSaleorUrls: [ (url) => { if (allowedUrlsPattern) { - const regex = new RegExp(escapeStringRegexp(allowedUrlsPattern)); + // we don't escape the pattern because it's not user input - it's an ENV variable controlled by us + const regex = new RegExp(allowedUrlsPattern); return regex.test(url); } diff --git a/apps/smtp/package.json b/apps/smtp/package.json index bd920076e7..80828e7a6f 100644 --- a/apps/smtp/package.json +++ b/apps/smtp/package.json @@ -47,7 +47,6 @@ "@urql/exchange-auth": "2.1.4", "@vitejs/plugin-react": "4.3.1", "dotenv": "16.3.1", - "escape-string-regexp": "5.0.0", "graphql": "16.7.1", "graphql-tag": "2.12.6", "handlebars": "^4.7.7", diff --git a/apps/smtp/src/pages/api/register.ts b/apps/smtp/src/pages/api/register.ts index af28784f10..7c7ccdc586 100644 --- a/apps/smtp/src/pages/api/register.ts +++ b/apps/smtp/src/pages/api/register.ts @@ -2,7 +2,6 @@ import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next"; import { wrapWithLoggerContext } from "@saleor/apps-logger/node"; import { withOtel } from "@saleor/apps-otel"; import { SaleorVersionCompatibilityValidator } from "@saleor/apps-shared"; -import escapeStringRegexp from "escape-string-regexp"; import { createInstrumentedGraphqlClient } from "../../lib/create-instrumented-graphql-client"; import { createLogger } from "../../logger"; @@ -23,7 +22,8 @@ export default wrapWithLoggerContext( allowedSaleorUrls: [ (url) => { if (allowedUrlsPattern) { - const regex = new RegExp(escapeStringRegexp(allowedUrlsPattern)); + // we don't escape the pattern because it's not user input - it's an ENV variable controlled by us + const regex = new RegExp(allowedUrlsPattern); return regex.test(url); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d0495c42a..826560b27b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -210,7 +210,7 @@ importers: version: 10.43.1(@trpc/server@10.43.1) '@trpc/next': specifier: 10.43.1 - version: 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@trpc/server': specifier: 10.43.1 version: 10.43.1 @@ -229,9 +229,6 @@ importers: dynamodb-toolbox: specifier: 1.8.2 version: 1.8.2(@aws-sdk/client-dynamodb@3.651.1)(@aws-sdk/lib-dynamodb@3.651.1(@aws-sdk/client-dynamodb@3.651.1)) - escape-string-regexp: - specifier: 5.0.0 - version: 5.0.0 graphql: specifier: 16.7.1 version: 16.7.1 @@ -255,7 +252,7 @@ importers: version: 6.2.1 next: specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -467,9 +464,6 @@ importers: dotenv: specifier: 16.3.1 version: 16.3.1 - escape-string-regexp: - specifier: 5.0.0 - version: 5.0.0 graphql: specifier: 16.7.1 version: 16.7.1 @@ -481,7 +475,7 @@ importers: version: 20.0.3 next: specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) p-ratelimit: specifier: 1.0.1 version: 1.0.1 @@ -645,9 +639,6 @@ importers: dotenv: specifier: 16.3.1 version: 16.3.1 - escape-string-regexp: - specifier: 5.0.0 - version: 5.0.0 graphql: specifier: 16.7.1 version: 16.7.1 @@ -656,7 +647,7 @@ importers: version: 2.12.6(graphql@16.7.1) next: specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) node-fetch: specifier: ^3.2.6 version: 3.2.6 @@ -825,7 +816,7 @@ importers: version: 10.43.1(@trpc/server@10.43.1) '@trpc/next': specifier: 10.43.1 - version: 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@trpc/react-query': specifier: 10.43.1 version: 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -841,9 +832,6 @@ importers: dotenv: specifier: 16.3.1 version: 16.3.1 - escape-string-regexp: - specifier: 5.0.0 - version: 5.0.0 fast-xml-parser: specifier: ^4.0.15 version: 4.0.15 @@ -861,7 +849,7 @@ importers: version: 20.0.3 next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -1046,9 +1034,6 @@ importers: dotenv: specifier: 16.3.1 version: 16.3.1 - escape-string-regexp: - specifier: 5.0.0 - version: 5.0.0 graphql: specifier: 16.7.1 version: 16.7.1 @@ -1057,7 +1042,7 @@ importers: version: 2.12.6(graphql@16.7.1) next: specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -1242,9 +1227,6 @@ importers: dotenv: specifier: 16.3.1 version: 16.3.1 - escape-string-regexp: - specifier: 5.0.0 - version: 5.0.0 graphql: specifier: 16.7.1 version: 16.7.1 @@ -1262,7 +1244,7 @@ importers: version: 6.2.1 next: specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -1465,9 +1447,6 @@ importers: dotenv: specifier: 16.3.1 version: 16.3.1 - escape-string-regexp: - specifier: 5.0.0 - version: 5.0.0 graphql: specifier: 16.7.1 version: 16.7.1 @@ -1500,7 +1479,7 @@ importers: version: 6.2.1 next: specifier: 14.2.3 - version: 14.2.3(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) nodemailer: specifier: ^6.9.1 version: 6.9.1 @@ -1651,7 +1630,7 @@ importers: version: 12.1.1(eslint@node_modules+eslint) next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) typescript: specifier: 5.5.4 version: 5.5.4 @@ -1859,7 +1838,7 @@ importers: version: link:../eslint-config-saleor next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -1920,7 +1899,7 @@ importers: version: link:../eslint-config-saleor next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) typescript: specifier: 5.5.4 version: 5.5.4 @@ -1951,7 +1930,7 @@ importers: version: link:../eslint-config-saleor next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -1984,7 +1963,7 @@ importers: version: 6.1.0(modern-errors@7.0.1) next: specifier: 14.2.3 - version: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + version: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) semver: specifier: 7.5.1 version: 7.5.1 @@ -8921,10 +8900,6 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - escodegen@2.0.0: resolution: {integrity: sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==} engines: {node: '>=6.0'} @@ -20687,13 +20662,24 @@ snapshots: react-dom: 18.2.0(react@18.2.0) react-ssr-prepass: 1.5.0(react@18.2.0) - '@trpc/next@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + '@trpc/next@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@tanstack/react-query': 4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@trpc/client': 10.43.1(@trpc/server@10.43.1) + '@trpc/react-query': 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@trpc/server': 10.43.1 + next: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-ssr-prepass: 1.5.0(react@18.2.0) + + '@trpc/next@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/react-query@10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/server@10.43.1)(next@14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@tanstack/react-query': 4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@trpc/client': 10.43.1(@trpc/server@10.43.1) '@trpc/react-query': 10.43.1(@tanstack/react-query@4.29.19(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@trpc/client@10.43.1(@trpc/server@10.43.1))(@trpc/server@10.43.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@trpc/server': 10.43.1 - next: 14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + next: 14.2.3(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-ssr-prepass: 1.5.0(react@18.2.0) @@ -23453,8 +23439,6 @@ snapshots: escape-string-regexp@4.0.0: {} - escape-string-regexp@5.0.0: {} - escodegen@2.0.0: dependencies: esprima: 4.0.1 @@ -26177,7 +26161,7 @@ snapshots: neverthrow@6.2.1: {} - next@14.2.3(@babel/core@7.24.7)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): + next@14.2.3(@babel/core@7.24.7)(@opentelemetry/api@node_modules+@opentelemetry+api)(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@next/env': 14.2.3 '@swc/helpers': 0.5.5 @@ -26198,7 +26182,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 14.2.3 '@next/swc-win32-ia32-msvc': 14.2.3 '@next/swc-win32-x64-msvc': 14.2.3 - '@opentelemetry/api': 1.9.0 + '@opentelemetry/api': link:node_modules/@opentelemetry/api transitivePeerDependencies: - '@babel/core' - babel-plugin-macros From b8d2dcdaa71631815e9f24709eae1df90dbb2e13 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Thu, 16 Jan 2025 12:46:28 +0100 Subject: [PATCH 04/17] Release apps (#1695) Co-authored-by: github-actions[bot] --- .changeset/angry-zebras-collect.md | 11 ----------- apps/avatax/CHANGELOG.md | 6 ++++++ apps/avatax/package.json | 2 +- apps/cms-v2/CHANGELOG.md | 6 ++++++ apps/cms-v2/package.json | 2 +- apps/klaviyo/CHANGELOG.md | 6 ++++++ apps/klaviyo/package.json | 2 +- apps/products-feed/CHANGELOG.md | 6 ++++++ apps/products-feed/package.json | 2 +- apps/search/CHANGELOG.md | 6 ++++++ apps/search/package.json | 2 +- apps/segment/CHANGELOG.md | 6 ++++++ apps/segment/package.json | 2 +- apps/smtp/CHANGELOG.md | 6 ++++++ apps/smtp/package.json | 2 +- 15 files changed, 49 insertions(+), 18 deletions(-) delete mode 100644 .changeset/angry-zebras-collect.md diff --git a/.changeset/angry-zebras-collect.md b/.changeset/angry-zebras-collect.md deleted file mode 100644 index 1e4db74d6e..0000000000 --- a/.changeset/angry-zebras-collect.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"products-feed": patch -"klaviyo": patch -"segment": patch -"app-avatax": patch -"cms-v2": patch -"search": patch -"smtp": patch ---- - -Removed regex escape for `ALLOWED_DOMAINS_URL` env variable from register handler. It isn't user input and escaping regex was causing problem with apps installation. diff --git a/apps/avatax/CHANGELOG.md b/apps/avatax/CHANGELOG.md index d34a6590e2..2f81e795a1 100644 --- a/apps/avatax/CHANGELOG.md +++ b/apps/avatax/CHANGELOG.md @@ -1,5 +1,11 @@ # app-avatax +## 1.12.5 + +### Patch Changes + +- 0db174a8: Removed regex escape for `ALLOWED_DOMAINS_URL` env variable from register handler. It isn't user input and escaping regex was causing problem with apps installation. + ## 1.12.4 ### Patch Changes diff --git a/apps/avatax/package.json b/apps/avatax/package.json index b2eb3ca8f8..361f7ea326 100644 --- a/apps/avatax/package.json +++ b/apps/avatax/package.json @@ -1,6 +1,6 @@ { "name": "app-avatax", - "version": "1.12.4", + "version": "1.12.5", "scripts": { "build": " next build", "check-types": "tsc --noEmit", diff --git a/apps/cms-v2/CHANGELOG.md b/apps/cms-v2/CHANGELOG.md index 06a03b2188..1b8f0bcc3b 100644 --- a/apps/cms-v2/CHANGELOG.md +++ b/apps/cms-v2/CHANGELOG.md @@ -1,5 +1,11 @@ # saleor-app-cms-v2 +## 2.9.18 + +### Patch Changes + +- 0db174a8: Removed regex escape for `ALLOWED_DOMAINS_URL` env variable from register handler. It isn't user input and escaping regex was causing problem with apps installation. + ## 2.9.17 ### Patch Changes diff --git a/apps/cms-v2/package.json b/apps/cms-v2/package.json index d4773555e2..c29f724409 100644 --- a/apps/cms-v2/package.json +++ b/apps/cms-v2/package.json @@ -1,6 +1,6 @@ { "name": "cms-v2", - "version": "2.9.17", + "version": "2.9.18", "scripts": { "build": "next build", "check-types": "tsc --noEmit", diff --git a/apps/klaviyo/CHANGELOG.md b/apps/klaviyo/CHANGELOG.md index 0d44e8ba44..71d6783362 100644 --- a/apps/klaviyo/CHANGELOG.md +++ b/apps/klaviyo/CHANGELOG.md @@ -1,5 +1,11 @@ # saleor-app-klaviyo +## 1.12.19 + +### Patch Changes + +- 0db174a8: Removed regex escape for `ALLOWED_DOMAINS_URL` env variable from register handler. It isn't user input and escaping regex was causing problem with apps installation. + ## 1.12.18 ### Patch Changes diff --git a/apps/klaviyo/package.json b/apps/klaviyo/package.json index 06ecd61680..fece57a11e 100644 --- a/apps/klaviyo/package.json +++ b/apps/klaviyo/package.json @@ -1,6 +1,6 @@ { "name": "klaviyo", - "version": "1.12.18", + "version": "1.12.19", "scripts": { "build": "next build", "check-types": "tsc --noEmit", diff --git a/apps/products-feed/CHANGELOG.md b/apps/products-feed/CHANGELOG.md index 8567e81db9..b75c9f186d 100644 --- a/apps/products-feed/CHANGELOG.md +++ b/apps/products-feed/CHANGELOG.md @@ -1,5 +1,11 @@ # saleor-app-products-feed +## 1.19.18 + +### Patch Changes + +- 0db174a8: Removed regex escape for `ALLOWED_DOMAINS_URL` env variable from register handler. It isn't user input and escaping regex was causing problem with apps installation. + ## 1.19.17 ### Patch Changes diff --git a/apps/products-feed/package.json b/apps/products-feed/package.json index 70c7904b90..5f8168f0ab 100644 --- a/apps/products-feed/package.json +++ b/apps/products-feed/package.json @@ -1,6 +1,6 @@ { "name": "products-feed", - "version": "1.19.17", + "version": "1.19.18", "scripts": { "build": "next build", "check-types": "tsc --noEmit", diff --git a/apps/search/CHANGELOG.md b/apps/search/CHANGELOG.md index 84bb725100..792396a1a9 100644 --- a/apps/search/CHANGELOG.md +++ b/apps/search/CHANGELOG.md @@ -1,5 +1,11 @@ # saleor-app-search +## 1.22.20 + +### Patch Changes + +- 0db174a8: Removed regex escape for `ALLOWED_DOMAINS_URL` env variable from register handler. It isn't user input and escaping regex was causing problem with apps installation. + ## 1.22.19 ### Patch Changes diff --git a/apps/search/package.json b/apps/search/package.json index 0f8a98411b..bcd68f426b 100644 --- a/apps/search/package.json +++ b/apps/search/package.json @@ -1,6 +1,6 @@ { "name": "search", - "version": "1.22.19", + "version": "1.22.20", "scripts": { "build": "next build", "check-types": "tsc --noEmit", diff --git a/apps/segment/CHANGELOG.md b/apps/segment/CHANGELOG.md index ae8d8a5a25..8263903b30 100644 --- a/apps/segment/CHANGELOG.md +++ b/apps/segment/CHANGELOG.md @@ -1,5 +1,11 @@ # segment +## 2.0.2 + +### Patch Changes + +- 0db174a8: Removed regex escape for `ALLOWED_DOMAINS_URL` env variable from register handler. It isn't user input and escaping regex was causing problem with apps installation. + ## 2.0.1 ### Patch Changes diff --git a/apps/segment/package.json b/apps/segment/package.json index b66762b0bd..96edb4a925 100644 --- a/apps/segment/package.json +++ b/apps/segment/package.json @@ -1,6 +1,6 @@ { "name": "segment", - "version": "2.0.1", + "version": "2.0.2", "scripts": { "build": "next build", "check-types": "tsc --noEmit", diff --git a/apps/smtp/CHANGELOG.md b/apps/smtp/CHANGELOG.md index da952286cb..59bb0d8b40 100644 --- a/apps/smtp/CHANGELOG.md +++ b/apps/smtp/CHANGELOG.md @@ -1,5 +1,11 @@ # smtp +## 1.2.21 + +### Patch Changes + +- 0db174a8: Removed regex escape for `ALLOWED_DOMAINS_URL` env variable from register handler. It isn't user input and escaping regex was causing problem with apps installation. + ## 1.2.20 ### Patch Changes diff --git a/apps/smtp/package.json b/apps/smtp/package.json index 80828e7a6f..e15b1a68b8 100644 --- a/apps/smtp/package.json +++ b/apps/smtp/package.json @@ -1,6 +1,6 @@ { "name": "smtp", - "version": "1.2.20", + "version": "1.2.21", "scripts": { "build": "next build", "check-types": "tsc --noEmit", From b61ce9142fdbdeef383ee4fce60a6ff94dabf29b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Fri, 17 Jan 2025 10:31:44 +0100 Subject: [PATCH 05/17] Add DynamoDB APL to Segment app (#1690) --- .changeset/breezy-buses-greet.md | 5 + apps/segment/README.md | 47 ++++ apps/segment/docker-compose.yml | 12 + apps/segment/package.json | 9 +- apps/segment/scripts/setup-dynamodb.sh | 11 + apps/segment/src/env.ts | 10 +- .../lib/__tests__/in-memory-apl-repository.ts | 47 ++++ apps/segment/src/lib/dyanmodb-apl.test.ts | 231 ++++++++++++++++++ apps/segment/src/lib/dynamodb-apl.ts | 135 ++++++++++ apps/segment/src/lib/dynamodb-client.ts | 12 + apps/segment/src/logger.ts | 5 +- .../src/modules/db/segment-apl-mapper.ts | 28 +++ .../db/segment-apl-repository-factory.ts | 42 ++++ .../modules/db/segment-apl-repository.test.ts | 221 +++++++++++++++++ .../src/modules/db/segment-apl-repository.ts | 139 +++++++++++ .../src/modules/db/segment-main-table.test.ts | 59 +++++ .../src/modules/db/segment-main-table.ts | 80 ++++++ apps/segment/src/modules/db/types.ts | 15 ++ apps/segment/src/saleor-app.ts | 8 + apps/segment/src/setup-tests.ts | 4 + apps/segment/turbo.json | 6 +- pnpm-lock.yaml | 15 ++ 22 files changed, 1136 insertions(+), 5 deletions(-) create mode 100644 .changeset/breezy-buses-greet.md create mode 100644 apps/segment/docker-compose.yml create mode 100755 apps/segment/scripts/setup-dynamodb.sh create mode 100644 apps/segment/src/lib/__tests__/in-memory-apl-repository.ts create mode 100644 apps/segment/src/lib/dyanmodb-apl.test.ts create mode 100644 apps/segment/src/lib/dynamodb-apl.ts create mode 100644 apps/segment/src/lib/dynamodb-client.ts create mode 100644 apps/segment/src/modules/db/segment-apl-mapper.ts create mode 100644 apps/segment/src/modules/db/segment-apl-repository-factory.ts create mode 100644 apps/segment/src/modules/db/segment-apl-repository.test.ts create mode 100644 apps/segment/src/modules/db/segment-apl-repository.ts create mode 100644 apps/segment/src/modules/db/segment-main-table.test.ts create mode 100644 apps/segment/src/modules/db/segment-main-table.ts create mode 100644 apps/segment/src/modules/db/types.ts diff --git a/.changeset/breezy-buses-greet.md b/.changeset/breezy-buses-greet.md new file mode 100644 index 0000000000..8549331ac5 --- /dev/null +++ b/.changeset/breezy-buses-greet.md @@ -0,0 +1,5 @@ +--- +"segment": patch +--- + +Added DynamoDB APL. This APL is using DynamoDB as storage. diff --git a/apps/segment/README.md b/apps/segment/README.md index 5d5513f6a5..c7fa9c354d 100644 --- a/apps/segment/README.md +++ b/apps/segment/README.md @@ -62,3 +62,50 @@ To start the migration run command: ```bash pnpm migrate ``` + +### Setting up DynamoDB + +Segment app uses DynamoDB as it's internal database. + +In order to work properly you need to either set-up local DynamoDB instance or connect to a real DynamoDB on AWS account. + +#### Local DynamoDB + +To use a local DynamoDB instance you can use Docker Compose: + +```bash +docker compose up +``` + +After that a local DynamoDB instance will be spun-up at `http://localhost:8000`. + +To set up tables needed for the app run following command for each table used in app: + +```shell +./scripts/setup-dynamodb.sh +``` + +After setting up database, you must configure following variables: + +```bash +DYNAMODB_MAIN_TABLE_NAME=segment-main-table +AWS_REGION=localhost +AWS_ENDPOINT_URL=http://localhost:8000 +AWS_ACCESS_KEY_ID=fake_id +AWS_SECRET_ACCESS_KEY=fake_key +``` + +Local instance doesn't require providing any authentication details. + +To see data stored by the app you can use [NoSQL Workbench](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/workbench.html) app provided by AWS. After installing the app go to Operation builder > Add connection > DynamoDB local and use the default values. + +#### Production DynamoDB + +To configure DynamoDB for production usage, provide credentials in a default AWS SDK format (see [AWS Docs](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html)) + +```bash +DYNAMODB_MAIN_TABLE_NAME=segment-main-table +AWS_REGION=us-east-1 # Region when DynamoDB was deployed +AWS_ACCESS_KEY_ID=AK... +AWS_SECRET_ACCESS_KEY=... +``` diff --git a/apps/segment/docker-compose.yml b/apps/segment/docker-compose.yml new file mode 100644 index 0000000000..815e6fc349 --- /dev/null +++ b/apps/segment/docker-compose.yml @@ -0,0 +1,12 @@ +services: + dynamodb: + command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data" + image: "amazon/dynamodb-local:latest" + ports: + - "8000:8000" + volumes: + - "./docker/dynamodb:/home/dynamodblocal/data" + working_dir: /home/dynamodblocal +volumes: + dynamodb: + driver: local diff --git a/apps/segment/package.json b/apps/segment/package.json index 96edb4a925..d6133f4a23 100644 --- a/apps/segment/package.json +++ b/apps/segment/package.json @@ -16,6 +16,9 @@ "test": "vitest" }, "dependencies": { + "@aws-sdk/client-dynamodb": "3.651.1", + "@aws-sdk/lib-dynamodb": "3.651.1", + "@aws-sdk/util-dynamodb": "3.651.1", "@hookform/resolvers": "^3.3.1", "@opentelemetry/api": "../../node_modules/@opentelemetry/api", "@opentelemetry/api-logs": "../../node_modules/@opentelemetry/api-logs", @@ -50,6 +53,7 @@ "@urql/exchange-auth": "2.1.4", "@vitejs/plugin-react": "4.3.1", "dotenv": "16.3.1", + "dynamodb-toolbox": "1.8.2", "graphql": "16.7.1", "graphql-tag": "2.12.6", "modern-errors": "7.0.1", @@ -78,12 +82,13 @@ "@total-typescript/ts-reset": "0.6.1", "@types/react": "18.2.5", "@types/react-dom": "18.2.5", + "@typescript-eslint/eslint-plugin": "7.15.0", + "@typescript-eslint/parser": "7.15.0", + "aws-sdk-client-mock": "4.0.1", "eslint": "../../node_modules/eslint", "eslint-config-saleor": "workspace:*", "eslint-plugin-neverthrow": "^1.1.4", "eslint-plugin-node": "11.1.0", - "@typescript-eslint/eslint-plugin": "7.15.0", - "@typescript-eslint/parser": "7.15.0", "graphql-config": "5.0.3", "jsdom": "^20.0.3", "node-mocks-http": "^1.12.2", diff --git a/apps/segment/scripts/setup-dynamodb.sh b/apps/segment/scripts/setup-dynamodb.sh new file mode 100755 index 0000000000..c22910f50f --- /dev/null +++ b/apps/segment/scripts/setup-dynamodb.sh @@ -0,0 +1,11 @@ +#!/bin/bash +if ! aws dynamodb describe-table --table-name segment-main-table --endpoint-url http://localhost:8000 --region localhost >/dev/null 2>&1; then + aws dynamodb create-table --table-name segment-main-table \ + --attribute-definitions AttributeName=PK,AttributeType=S AttributeName=SK,AttributeType=S \ + --key-schema AttributeName=PK,KeyType=HASH AttributeName=SK,KeyType=RANGE \ + --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 \ + --endpoint-url http://localhost:8000 \ + --region localhost +else + echo "Table segment-main-table already exists - creation is skipped" +fi diff --git a/apps/segment/src/env.ts b/apps/segment/src/env.ts index d1080fb6a0..42df8546c7 100644 --- a/apps/segment/src/env.ts +++ b/apps/segment/src/env.ts @@ -14,7 +14,7 @@ export const env = createEnv({ }, server: { ALLOWED_DOMAIN_PATTERN: z.string().optional(), - APL: z.enum(["saleor-cloud", "file"]).optional().default("file"), + APL: z.enum(["saleor-cloud", "file", "dynamodb"]).optional().default("file"), APP_API_BASE_URL: z.string().optional(), APP_IFRAME_BASE_URL: z.string().optional(), APP_LOG_LEVEL: z.enum(["fatal", "error", "warn", "info", "debug", "trace"]).default("info"), @@ -27,6 +27,10 @@ export const env = createEnv({ PORT: z.coerce.number().optional().default(3000), SECRET_KEY: z.string(), VERCEL_URL: z.string().optional(), + DYNAMODB_MAIN_TABLE_NAME: z.string().optional(), + AWS_REGION: z.string().optional(), + AWS_ACCESS_KEY_ID: z.string().optional(), + AWS_SECRET_ACCESS_KEY: z.string().optional(), }, shared: { NODE_ENV: z.enum(["development", "production", "test"]).optional().default("development"), @@ -51,6 +55,10 @@ export const env = createEnv({ REST_APL_TOKEN: process.env.REST_APL_TOKEN, SECRET_KEY: process.env.SECRET_KEY, VERCEL_URL: process.env.VERCEL_URL, + DYNAMODB_MAIN_TABLE_NAME: process.env.DYNAMODB_MAIN_TABLE_NAME, + AWS_REGION: process.env.AWS_REGION, + AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY, }, isServer: typeof window === "undefined" || process.env.NODE_ENV === "test", }); diff --git a/apps/segment/src/lib/__tests__/in-memory-apl-repository.ts b/apps/segment/src/lib/__tests__/in-memory-apl-repository.ts new file mode 100644 index 0000000000..a6dff6bd9a --- /dev/null +++ b/apps/segment/src/lib/__tests__/in-memory-apl-repository.ts @@ -0,0 +1,47 @@ +import { AuthData } from "@saleor/app-sdk/APL"; +import { err, ok, Result } from "neverthrow"; + +import { BaseError } from "@/errors"; +import { APLRepository } from "@/modules/db/types"; + +export class InMemoryAPLRepository implements APLRepository { + public entries: Record = {}; + + async getEntry(args: { + saleorApiUrl: string; + }): Promise>> { + if (this.entries[args.saleorApiUrl]) { + return ok(this.entries[args.saleorApiUrl]); + } + + return ok(null); + } + + async setEntry(args: { + authData: AuthData; + }): Promise>> { + this.entries[args.authData.saleorApiUrl] = args.authData; + return ok(undefined); + } + + async deleteEntry(args: { + saleorApiUrl: string; + }): Promise>> { + if (this.entries[args.saleorApiUrl]) { + delete this.entries[args.saleorApiUrl]; + return ok(undefined); + } + + return err(new BaseError("Error deleting entry")); + } + + async getAllEntries(): Promise>> { + const values = Object.values(this.entries); + + if (values.length === 0) { + return ok(null); + } + + return ok(values); + } +} diff --git a/apps/segment/src/lib/dyanmodb-apl.test.ts b/apps/segment/src/lib/dyanmodb-apl.test.ts new file mode 100644 index 0000000000..bd661f2283 --- /dev/null +++ b/apps/segment/src/lib/dyanmodb-apl.test.ts @@ -0,0 +1,231 @@ +import { AuthData } from "@saleor/app-sdk/APL"; +import { err } from "neverthrow"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { BaseError } from "@/errors"; + +import { InMemoryAPLRepository } from "./__tests__/in-memory-apl-repository"; +import { DynamoAPL } from "./dynamodb-apl"; + +describe("DynamoAPL", () => { + const mockedAuthData: AuthData = { + saleorApiUrl: "saleorApiUrl", + token: "appToken", + domain: "saleorDomain", + appId: "saleorAppId", + }; + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should get auth data if it exists", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + repository.setEntry({ + authData: mockedAuthData, + }); + + const result = await apl.get("saleorApiUrl"); + + expect(result).toStrictEqual(mockedAuthData); + }); + + it("should return undefined if auth data does not exist", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + const result = await apl.get("saleorApiUrl"); + + expect(result).toBeUndefined(); + }); + + it("should throw an error if getting auth data fails", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + vi.spyOn(repository, "getEntry").mockReturnValue( + Promise.resolve(err(new BaseError("Error getting data"))), + ); + + await expect(apl.get("saleorApiUrl")).rejects.toThrowError(DynamoAPL.GetAuthDataError); + }); + + it("should set auth data", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + const result = await apl.set(mockedAuthData); + + expect(result).toBeUndefined(); + + const getEntryResult = await repository.getEntry({ + saleorApiUrl: mockedAuthData.saleorApiUrl, + }); + + expect(getEntryResult._unsafeUnwrap()).toStrictEqual(mockedAuthData); + }); + + it("should throw an error if setting auth data fails", async () => { + const repository = new InMemoryAPLRepository(); + + vi.spyOn(repository, "setEntry").mockResolvedValue(err(new BaseError("Error setting data"))); + + const apl = new DynamoAPL({ repository }); + + await expect(apl.set(mockedAuthData)).rejects.toThrowError(DynamoAPL.SetAuthDataError); + }); + + it("should update existing auth data", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + repository.setEntry({ + authData: mockedAuthData, + }); + + apl.set({ + saleorApiUrl: mockedAuthData.saleorApiUrl, + token: "newAppToken", + domain: "newSaleorDomain", + appId: "newSaleorAppId", + }); + + const getEntryResult = await apl.get(mockedAuthData.saleorApiUrl); + + expect(getEntryResult).toStrictEqual({ + saleorApiUrl: mockedAuthData.saleorApiUrl, + domain: "newSaleorDomain", + appId: "newSaleorAppId", + token: "newAppToken", + }); + }); + + it("should delete auth data", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + repository.setEntry({ + authData: mockedAuthData, + }); + + await apl.delete(mockedAuthData.saleorApiUrl); + + const getEntryResult = await apl.get(mockedAuthData.saleorApiUrl); + + expect(getEntryResult).toBeUndefined(); + }); + + it("should throw an error if deleting auth data fails", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + await expect(apl.delete("saleorApiUrl")).rejects.toThrowError(DynamoAPL.DeleteAuthDataError); + }); + + it("should get all auth data", async () => { + const repository = new InMemoryAPLRepository(); + const secondEntry: AuthData = { + saleorApiUrl: "saleorApiUrl2", + token: "appToken2", + domain: "saleorDomain2", + appId: "saleorAppId2", + }; + const apl = new DynamoAPL({ repository }); + + repository.setEntry({ + authData: mockedAuthData, + }); + + repository.setEntry({ + authData: secondEntry, + }); + + const result = await apl.getAll(); + + expect(result).toStrictEqual([mockedAuthData, secondEntry]); + }); + + it("should throw an error if getting all auth data fails", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + vi.spyOn(repository, "getAllEntries").mockResolvedValue( + err(new BaseError("Error getting data")), + ); + + await expect(apl.getAll()).rejects.toThrowError(DynamoAPL.GetAllAuthDataError); + }); + + it("should return ready:true when APL related env variables are set", async () => { + vi.spyOn(await import("@/env"), "env", "get").mockReturnValue({ + DYNAMODB_MAIN_TABLE_NAME: "table_name", + AWS_REGION: "region", + AWS_ACCESS_KEY_ID: "access_key_id", + AWS_SECRET_ACCESS_KEY: "secret_access_key", + APL: "dynamodb", + APP_LOG_LEVEL: "info", + MANIFEST_APP_ID: "", + OTEL_ENABLED: false, + PORT: 0, + SECRET_KEY: "", + NODE_ENV: "test", + ENV: "local", + }); + + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + const result = await apl.isReady(); + + expect(result).toStrictEqual({ ready: true }); + }); + + it("should return ready:false when APL related env variables are not set", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + const result = await apl.isReady(); + + expect(result).toStrictEqual({ + ready: false, + error: new DynamoAPL.MissingEnvVariablesError("Missing DynamoDB env variables"), + }); + }); + + it("should return configured:true when APL related env variables are set", async () => { + vi.spyOn(await import("@/env"), "env", "get").mockReturnValue({ + DYNAMODB_MAIN_TABLE_NAME: "table_name", + AWS_REGION: "region", + AWS_ACCESS_KEY_ID: "access_key_id", + AWS_SECRET_ACCESS_KEY: "secret_access_key", + APL: "dynamodb", + APP_LOG_LEVEL: "info", + MANIFEST_APP_ID: "", + OTEL_ENABLED: false, + PORT: 0, + SECRET_KEY: "", + NODE_ENV: "test", + ENV: "local", + }); + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + const result = await apl.isConfigured(); + + expect(result).toStrictEqual({ configured: true }); + }); + + it("should return configured:false when APL related env variables are not set", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + const result = await apl.isConfigured(); + + expect(result).toStrictEqual({ + configured: false, + error: new DynamoAPL.MissingEnvVariablesError("Missing DynamoDB env variables"), + }); + }); +}); diff --git a/apps/segment/src/lib/dynamodb-apl.ts b/apps/segment/src/lib/dynamodb-apl.ts new file mode 100644 index 0000000000..bc9658e25f --- /dev/null +++ b/apps/segment/src/lib/dynamodb-apl.ts @@ -0,0 +1,135 @@ +import { APL, AplConfiguredResult, AplReadyResult, AuthData } from "@saleor/app-sdk/APL"; +import { getOtelTracer } from "@saleor/apps-otel/src/otel-tracer"; + +import { env } from "@/env"; +import { BaseError } from "@/errors"; +import { APLRepository } from "@/modules/db/types"; + +export class DynamoAPL implements APL { + static GetAuthDataError = BaseError.subclass("GetAuthDataError"); + static SetAuthDataError = BaseError.subclass("SetAuthDataError"); + static DeleteAuthDataError = BaseError.subclass("DeleteAuthDataError"); + static GetAllAuthDataError = BaseError.subclass("GetAllAuthDataError"); + static MissingEnvVariablesError = BaseError.subclass("MissingEnvVariablesError"); + + private tracer = getOtelTracer(); + + constructor(private deps: { repository: APLRepository }) {} + + async get(saleorApiUrl: string): Promise { + return this.tracer.startActiveSpan("DynamoAPL.get", async (span) => { + const getEntryResult = await this.deps.repository.getEntry({ + saleorApiUrl, + }); + + if (getEntryResult.isErr()) { + span.end(); + throw new DynamoAPL.GetAuthDataError("Failed to get APL entry", { + cause: getEntryResult.error, + }); + } + + if (!getEntryResult.value) { + span.end(); + return undefined; + } + + span.end(); + return getEntryResult.value; + }); + } + + async set(authData: AuthData): Promise { + return this.tracer.startActiveSpan("DynamoAPL.set", async (span) => { + const setEntryResult = await this.deps.repository.setEntry({ + authData, + }); + + if (setEntryResult.isErr()) { + span.end(); + throw new DynamoAPL.SetAuthDataError("Failed to set APL entry", { + cause: setEntryResult.error, + }); + } + + span.end(); + return undefined; + }); + } + + async delete(saleorApiUrl: string): Promise { + return this.tracer.startActiveSpan("DynamoAPL.delete", async (span) => { + const deleteEntryResult = await this.deps.repository.deleteEntry({ + saleorApiUrl, + }); + + if (deleteEntryResult.isErr()) { + span.end(); + throw new DynamoAPL.DeleteAuthDataError("Failed to delete APL entry", { + cause: deleteEntryResult.error, + }); + } + + span.end(); + return undefined; + }); + } + + async getAll(): Promise { + return this.tracer.startActiveSpan("DynamoAPL.getAll", async (span) => { + const getAllEntriesResult = await this.deps.repository.getAllEntries(); + + if (getAllEntriesResult.isErr()) { + span.end(); + throw new DynamoAPL.GetAllAuthDataError("Failed to get all APL entries", { + cause: getAllEntriesResult.error, + }); + } + + if (!getAllEntriesResult.value) { + span.end(); + return []; + } + + span.end(); + return getAllEntriesResult.value; + }); + } + + async isReady(): Promise { + const ready = this.envVariablesRequriedByDynamoDBExist(); + + return ready + ? { + ready: true, + } + : { + ready: false, + error: new DynamoAPL.MissingEnvVariablesError("Missing DynamoDB env variables"), + }; + } + + async isConfigured(): Promise { + const configured = this.envVariablesRequriedByDynamoDBExist(); + + return configured + ? { + configured: true, + } + : { + configured: false, + error: new DynamoAPL.MissingEnvVariablesError("Missing DynamoDB env variables"), + }; + } + + private envVariablesRequriedByDynamoDBExist() { + const variables = [ + "DYNAMODB_MAIN_TABLE_NAME", + "AWS_REGION", + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + ] as const; + + return variables.every((variable) => !!env[variable]); + } +} diff --git a/apps/segment/src/lib/dynamodb-client.ts b/apps/segment/src/lib/dynamodb-client.ts new file mode 100644 index 0000000000..b5d56ee282 --- /dev/null +++ b/apps/segment/src/lib/dynamodb-client.ts @@ -0,0 +1,12 @@ +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; + +export const createDynamoDBClient = () => { + const client = new DynamoDBClient(); + + return client; +}; + +export const createDynamoDBDocumentClient = (client: DynamoDBClient) => { + return DynamoDBDocumentClient.from(client); +}; diff --git a/apps/segment/src/logger.ts b/apps/segment/src/logger.ts index 95a2ecb92b..c0b9e35413 100644 --- a/apps/segment/src/logger.ts +++ b/apps/segment/src/logger.ts @@ -1,11 +1,14 @@ import { attachLoggerConsoleTransport, rootLogger } from "@saleor/apps-logger"; +import { createRequire } from "module"; import packageJson from "../package.json"; import { env } from "./env"; rootLogger.settings.maskValuesOfKeys = ["metadata", "username", "password", "apiKey"]; -if (env.NODE_ENV !== "production") { +const require = createRequire(import.meta.url); + +if (env.NODE_ENV === "development") { attachLoggerConsoleTransport(rootLogger); } diff --git a/apps/segment/src/modules/db/segment-apl-mapper.ts b/apps/segment/src/modules/db/segment-apl-mapper.ts new file mode 100644 index 0000000000..9e613aa8a4 --- /dev/null +++ b/apps/segment/src/modules/db/segment-apl-mapper.ts @@ -0,0 +1,28 @@ +import { AuthData } from "@saleor/app-sdk/APL"; +import { FormattedItem, type PutItemInput } from "dynamodb-toolbox"; + +import { SegmentAPLEntityType, SegmentMainTable } from "@/modules/db/segment-main-table"; + +export class SegmentAPLMapper { + dynamoDBEntityToAuthData(entity: FormattedItem): AuthData { + return { + domain: entity.domain, + token: entity.token, + saleorApiUrl: entity.saleorApiUrl, + appId: entity.appId, + jwks: entity.jwks, + }; + } + + authDataToDynamoPutEntity(authData: AuthData): PutItemInput { + return { + PK: SegmentMainTable.getAPLPrimaryKey({ saleorApiUrl: authData.saleorApiUrl }), + SK: SegmentMainTable.getAPLSortKey(), + domain: authData.domain, + token: authData.token, + saleorApiUrl: authData.saleorApiUrl, + appId: authData.appId, + jwks: authData.jwks, + }; + } +} diff --git a/apps/segment/src/modules/db/segment-apl-repository-factory.ts b/apps/segment/src/modules/db/segment-apl-repository-factory.ts new file mode 100644 index 0000000000..9bb6799fac --- /dev/null +++ b/apps/segment/src/modules/db/segment-apl-repository-factory.ts @@ -0,0 +1,42 @@ +import { env } from "@/env"; +import { BaseError } from "@/errors"; + +import { SegmentAPLRepository } from "./segment-apl-repository"; +import { + documentClient, + SegmentMainTable, + SegmentMainTableEntityFactory, +} from "./segment-main-table"; + +export class SegmentAPLRepositoryFactory { + static RepositoryCreationError = BaseError.subclass("RepositoryCreationError"); + + static create(): SegmentAPLRepository { + if ( + !env.DYNAMODB_MAIN_TABLE_NAME || + !env.AWS_REGION || + !env.AWS_ACCESS_KEY_ID || + !env.AWS_SECRET_ACCESS_KEY + ) { + throw new SegmentAPLRepositoryFactory.RepositoryCreationError( + "DynamoDB APL is not configured - missing env variables.", + ); + } + + try { + // TODO: when we have config in DyanamoDB - move to `segment-main-table.ts` + const table = SegmentMainTable.create({ + tableName: env.DYNAMODB_MAIN_TABLE_NAME, + documentClient, + }); + const segmentAPLEntity = SegmentMainTableEntityFactory.createAPLEntity(table); + + return new SegmentAPLRepository({ segmentAPLEntity }); + } catch (error) { + throw new SegmentAPLRepositoryFactory.RepositoryCreationError( + "Failed to create DynamoDB APL repository", + { cause: error }, + ); + } + } +} diff --git a/apps/segment/src/modules/db/segment-apl-repository.test.ts b/apps/segment/src/modules/db/segment-apl-repository.test.ts new file mode 100644 index 0000000000..335d6f77c4 --- /dev/null +++ b/apps/segment/src/modules/db/segment-apl-repository.test.ts @@ -0,0 +1,221 @@ +import { + DeleteCommand, + DynamoDBDocumentClient, + GetCommand, + PutCommand, + ScanCommand, +} from "@aws-sdk/lib-dynamodb"; +import { AuthData } from "@saleor/app-sdk/APL"; +import { mockClient } from "aws-sdk-client-mock"; +import { SavedItem } from "dynamodb-toolbox"; +import { beforeEach, describe, expect, it } from "vitest"; + +import { SegmentAPLRepository } from "./segment-apl-repository"; +import { SegmentMainTable, SegmentMainTableEntityFactory } from "./segment-main-table"; + +describe("SegmentAPLRepository", () => { + const mockDocumentClient = mockClient(DynamoDBDocumentClient); + + const segmentMainTable = SegmentMainTable.create({ + // @ts-expect-error https://github.com/m-radzikowski/aws-sdk-client-mock/issues/197 + documentClient: mockDocumentClient, + tableName: "segment-test-table", + }); + + const segmentAPLEntity = SegmentMainTableEntityFactory.createAPLEntity(segmentMainTable); + + const mockedAuthData: AuthData = { + appId: "appId", + saleorApiUrl: "saleorApiUrl", + token: "appToken", + }; + + beforeEach(() => { + mockDocumentClient.reset(); + }); + + it("should successfully get AuthData entry from DynamoDB", async () => { + const mockedAPLEntry: SavedItem = { + PK: "saleorApiUrl", + SK: "APL", + token: "appToken", + saleorApiUrl: "saleorApiUrl", + appId: "appId", + _et: "APL", + createdAt: "2023-01-01T00:00:00.000Z", + modifiedAt: "2023-01-01T00:00:00.000Z", + }; + + mockDocumentClient.on(GetCommand, {}).resolvesOnce({ + Item: mockedAPLEntry, + }); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.getEntry({ saleorApiUrl: "saleorApiUrl" }); + + expect(result.isOk()).toBe(true); + + expect(result._unsafeUnwrap()).toStrictEqual({ + appId: "appId", + domain: undefined, + jwks: undefined, + saleorApiUrl: "saleorApiUrl", + token: "appToken", + }); + }); + + it("should handle errors when getting AuthData from DynamoDB", async () => { + mockDocumentClient.on(GetCommand, {}).rejectsOnce("Exception"); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.getEntry({ saleorApiUrl: "saleorApiUrl" }); + + expect(result.isErr()).toBe(true); + + expect(result._unsafeUnwrapErr()).toBeInstanceOf(SegmentAPLRepository.ReadEntityError); + }); + + it("should return null if AuthData entry does not exist in DynamoDB", async () => { + mockDocumentClient.on(GetCommand, {}).resolvesOnce({}); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.getEntry({ saleorApiUrl: "saleorApiUrl" }); + + expect(result.isOk()).toBe(true); + + expect(result._unsafeUnwrap()).toBe(null); + }); + + it("should successfully set AuthData entry in DynamoDB", async () => { + mockDocumentClient.on(PutCommand, {}).resolvesOnce({}); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.setEntry({ + authData: mockedAuthData, + }); + + expect(result.isOk()).toBe(true); + + expect(result._unsafeUnwrap()).toBe(undefined); + }); + + it("should handle errors when setting AuthData entry DynamoDB", async () => { + mockDocumentClient.on(PutCommand, {}).rejectsOnce("Exception"); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.setEntry({ + authData: mockedAuthData, + }); + + expect(result.isErr()).toBe(true); + + expect(result._unsafeUnwrapErr()).toBeInstanceOf(SegmentAPLRepository.WriteEntityError); + }); + + it("should successfully delete AuthData entry from DynamoDB", async () => { + mockDocumentClient.on(DeleteCommand, {}).resolvesOnce({}); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.deleteEntry({ saleorApiUrl: "saleorApiUrl" }); + + expect(result.isOk()).toBe(true); + + expect(result._unsafeUnwrap()).toBe(undefined); + }); + + it("should handle errors when deleting AuthData entry from DynamoDB", async () => { + mockDocumentClient.on(DeleteCommand, {}).rejectsOnce("Exception"); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.deleteEntry({ saleorApiUrl: "saleorApiUrl" }); + + expect(result.isErr()).toBe(true); + + expect(result._unsafeUnwrapErr()).toBeInstanceOf(SegmentAPLRepository.DeleteEntityError); + }); + + it("should successfully get all AuthData entries from DynamoDB", async () => { + const mockedAPLEntries: SavedItem[] = [ + { + PK: "saleorApiUrl", + SK: "APL", + token: "appToken", + saleorApiUrl: "saleorApiUrl", + appId: "appId", + _et: "APL", + createdAt: "2023-01-01T00:00:00.000Z", + modifiedAt: "2023-01-01T00:00:00.000Z", + }, + { + PK: "additionalSaleorApiUrl", + SK: "APL", + token: "newAppToken", + saleorApiUrl: "additionalSaleorApiUrl", + appId: "newAppId", + _et: "APL", + createdAt: "2024-01-01T00:00:00.000Z", + modifiedAt: "2024-01-01T00:00:00.000Z", + }, + ]; + + mockDocumentClient.on(ScanCommand, {}).resolvesOnce({ + Items: mockedAPLEntries, + }); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.getAllEntries(); + + expect(result.isOk()).toBe(true); + + expect(result._unsafeUnwrap()).toStrictEqual([ + { + appId: "appId", + domain: undefined, + jwks: undefined, + saleorApiUrl: "saleorApiUrl", + token: "appToken", + }, + { + appId: "newAppId", + domain: undefined, + jwks: undefined, + saleorApiUrl: "additionalSaleorApiUrl", + token: "newAppToken", + }, + ]); + }); + + it("should return null if there are no AuthData entries in DynamoDB", async () => { + mockDocumentClient.on(ScanCommand, {}).resolvesOnce({ + Items: [], + }); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.getAllEntries(); + + expect(result.isOk()).toBe(true); + + expect(result._unsafeUnwrap()).toBe(null); + }); + + it("should handle error when getting all AuthData entries from DynamoDB", async () => { + mockDocumentClient.on(ScanCommand, {}).rejectsOnce("Exception"); + + const repository = new SegmentAPLRepository({ segmentAPLEntity }); + + const result = await repository.getAllEntries(); + + expect(result.isErr()).toBe(true); + + expect(result._unsafeUnwrapErr()).toBeInstanceOf(SegmentAPLRepository.ScanEntityError); + }); +}); diff --git a/apps/segment/src/modules/db/segment-apl-repository.ts b/apps/segment/src/modules/db/segment-apl-repository.ts new file mode 100644 index 0000000000..def765b114 --- /dev/null +++ b/apps/segment/src/modules/db/segment-apl-repository.ts @@ -0,0 +1,139 @@ +import { AuthData } from "@saleor/app-sdk/APL"; +import { DeleteItemCommand, GetItemCommand, PutItemCommand, ScanCommand } from "dynamodb-toolbox"; +import { err, ok, ResultAsync } from "neverthrow"; + +import { BaseError } from "@/errors"; +import { createLogger } from "@/logger"; +import { SegmentAPLEntityType, SegmentMainTable } from "@/modules/db/segment-main-table"; + +import { SegmentAPLMapper } from "./segment-apl-mapper"; +import { APLRepository } from "./types"; + +export class SegmentAPLRepository implements APLRepository { + private logger = createLogger("SegmentAPLRepository"); + + private segmentAPLMapper = new SegmentAPLMapper(); + + static ReadEntityError = BaseError.subclass("ReadEntityError"); + static WriteEntityError = BaseError.subclass("WriteEntityError"); + static DeleteEntityError = BaseError.subclass("DeleteEntityError"); + static ScanEntityError = BaseError.subclass("ScanEntityError"); + + constructor( + private deps: { + segmentAPLEntity: SegmentAPLEntityType; + }, + ) {} + + async getEntry(args: { saleorApiUrl: string }) { + const getEntryResult = await ResultAsync.fromPromise( + this.deps.segmentAPLEntity + .build(GetItemCommand) + .key({ + PK: SegmentMainTable.getAPLPrimaryKey({ + saleorApiUrl: args.saleorApiUrl, + }), + SK: SegmentMainTable.getAPLSortKey(), + }) + .send(), + (error) => + new SegmentAPLRepository.ReadEntityError("Failed to read APL entity", { cause: error }), + ); + + if (getEntryResult.isErr()) { + this.logger.error("Error while reading APL entity from DynamoDB", { + error: getEntryResult.error, + }); + + return err(getEntryResult.error); + } + + if (!getEntryResult.value.Item) { + this.logger.warn("APL entry not found", { args }); + + return ok(null); + } + + return ok(this.segmentAPLMapper.dynamoDBEntityToAuthData(getEntryResult.value.Item)); + } + + async setEntry(args: { authData: AuthData }) { + const setEntryResult = await ResultAsync.fromPromise( + this.deps.segmentAPLEntity + .build(PutItemCommand) + .item(this.segmentAPLMapper.authDataToDynamoPutEntity(args.authData)) + .send(), + (error) => + new SegmentAPLRepository.WriteEntityError("Failed to write APL entity", { cause: error }), + ); + + if (setEntryResult.isErr()) { + this.logger.error("Error while putting APL into DynamoDB", { + error: setEntryResult.error, + }); + + return err(setEntryResult.error); + } + + return ok(undefined); + } + + async deleteEntry(args: { saleorApiUrl: string }) { + const deleteEntryResult = await ResultAsync.fromPromise( + this.deps.segmentAPLEntity + .build(DeleteItemCommand) + .key({ + PK: SegmentMainTable.getAPLPrimaryKey({ + saleorApiUrl: args.saleorApiUrl, + }), + SK: SegmentMainTable.getAPLSortKey(), + }) + .send(), + (error) => + new SegmentAPLRepository.DeleteEntityError("Failed to delete APL entity", { + cause: error, + }), + ); + + if (deleteEntryResult.isErr()) { + this.logger.error("Error while deleting entry APL from DynamoDB", { + error: deleteEntryResult.error, + }); + + return err(deleteEntryResult.error); + } + + return ok(undefined); + } + + async getAllEntries() { + const scanEntriesResult = await ResultAsync.fromPromise( + this.deps.segmentAPLEntity.table + .build(ScanCommand) + .entities(this.deps.segmentAPLEntity) + .options({ + // keep all the entries in memory - we should introduce pagination in the future + maxPages: Infinity, + }) + .send(), + (error) => + new SegmentAPLRepository.ScanEntityError("Failed to scan APL entities", { cause: error }), + ); + + if (scanEntriesResult.isErr()) { + this.logger.error("Error while scanning APL entities from DynamoDB", { + error: scanEntriesResult.error, + }); + + return err(scanEntriesResult.error); + } + + const possibleItems = scanEntriesResult.value.Items ?? []; + + if (possibleItems.length > 0) { + return ok(possibleItems.map(this.segmentAPLMapper.dynamoDBEntityToAuthData)); + } + + return ok(null); + } +} diff --git a/apps/segment/src/modules/db/segment-main-table.test.ts b/apps/segment/src/modules/db/segment-main-table.test.ts new file mode 100644 index 0000000000..f852dfff8e --- /dev/null +++ b/apps/segment/src/modules/db/segment-main-table.test.ts @@ -0,0 +1,59 @@ +import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; +import { mockClient } from "aws-sdk-client-mock"; +import { EntityParser } from "dynamodb-toolbox"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +import { SegmentMainTable, SegmentMainTableEntityFactory } from "./segment-main-table"; + +describe("SegmentMainTable", () => { + const mockDate = new Date("2023-01-01T00:00:00Z"); + + beforeAll(() => { + vi.setSystemTime(mockDate); + }); + + afterEach(() => { + vi.resetModules(); + }); + + afterAll(() => { + vi.useRealTimers(); + }); + + const mockDocumentClient = mockClient(DynamoDBDocumentClient); + + const segmentMainTable = SegmentMainTable.create({ + // @ts-expect-error https://github.com/m-radzikowski/aws-sdk-client-mock/issues/197 + documentClient: mockDocumentClient, + tableName: "segment-test-table", + }); + + beforeEach(() => { + mockDocumentClient.reset(); + }); + + describe("SegmentAPLEntity", () => { + it("should create a new entity in DynamoDB with default fields", () => { + const aplEntity = SegmentMainTableEntityFactory.createAPLEntity(segmentMainTable); + + const parseResult = aplEntity.build(EntityParser).parse({ + PK: "saleorApiUrl", + SK: "APL", + token: "appToken", + saleorApiUrl: "saleorApiUrl", + appId: "appId", + }); + + expect(parseResult.item).toStrictEqual({ + PK: "saleorApiUrl", + SK: "APL", + _et: "APL", + appId: "appId", + saleorApiUrl: "saleorApiUrl", + token: "appToken", + createdAt: "2023-01-01T00:00:00.000Z", + modifiedAt: "2023-01-01T00:00:00.000Z", + }); + }); + }); +}); diff --git a/apps/segment/src/modules/db/segment-main-table.ts b/apps/segment/src/modules/db/segment-main-table.ts new file mode 100644 index 0000000000..924affbf19 --- /dev/null +++ b/apps/segment/src/modules/db/segment-main-table.ts @@ -0,0 +1,80 @@ +import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; +import { Entity, schema, string, Table } from "dynamodb-toolbox"; + +import { createDynamoDBClient, createDynamoDBDocumentClient } from "@/lib/dynamodb-client"; + +type PartitionKey = { name: "PK"; type: "string" }; +type SortKey = { name: "SK"; type: "string" }; + +/** + * This table is used to store all relevant data for the Segment application meaning: APL, configuration, etc. + */ +export class SegmentMainTable extends Table { + private constructor(args: ConstructorParameters>[number]) { + super(args); + } + + static create({ + documentClient, + tableName, + }: { + documentClient: DynamoDBDocumentClient; + tableName: string; + }): SegmentMainTable { + return new SegmentMainTable({ + documentClient, + name: tableName, + partitionKey: { name: "PK", type: "string" }, + sortKey: { + name: "SK", + type: "string", + }, + }); + } + + static getAPLPrimaryKey({ saleorApiUrl }: { saleorApiUrl: string }) { + return `${saleorApiUrl}` as const; + } + + static getAPLSortKey() { + return `APL` as const; + } +} + +const SegmentConfigTableSchema = { + apl: schema({ + PK: string().key(), + SK: string().key(), + domain: string().optional(), + token: string(), + saleorApiUrl: string(), + appId: string(), + jwks: string().optional(), + }), +}; + +export const client = createDynamoDBClient(); + +export const documentClient = createDynamoDBDocumentClient(client); + +export const SegmentMainTableEntityFactory = { + createAPLEntity: (table: SegmentMainTable) => { + return new Entity({ + table, + name: "APL", + schema: SegmentConfigTableSchema.apl, + timestamps: { + created: { + name: "createdAt", + savedAs: "createdAt", + }, + modified: { + name: "modifiedAt", + savedAs: "modifiedAt", + }, + }, + }); + }, +}; + +export type SegmentAPLEntityType = ReturnType; diff --git a/apps/segment/src/modules/db/types.ts b/apps/segment/src/modules/db/types.ts new file mode 100644 index 0000000000..302c892c76 --- /dev/null +++ b/apps/segment/src/modules/db/types.ts @@ -0,0 +1,15 @@ +import { AuthData } from "@saleor/app-sdk/APL"; +import { Result } from "neverthrow"; + +import { BaseError } from "@/errors"; + +export interface APLRepository { + getEntry(args: { + saleorApiUrl: string; + }): Promise>>; + setEntry(args: { authData: AuthData }): Promise>>; + deleteEntry(args: { + saleorApiUrl: string; + }): Promise>>; + getAllEntries(): Promise>>; +} diff --git a/apps/segment/src/saleor-app.ts b/apps/segment/src/saleor-app.ts index 30c5c5e0d3..9eb9303230 100644 --- a/apps/segment/src/saleor-app.ts +++ b/apps/segment/src/saleor-app.ts @@ -3,12 +3,20 @@ import { SaleorApp } from "@saleor/app-sdk/saleor-app"; import { env } from "./env"; import { BaseError } from "./errors"; +import { DynamoAPL } from "./lib/dynamodb-apl"; +import { SegmentAPLRepositoryFactory } from "./modules/db/segment-apl-repository-factory"; export let apl: APL; const MisconfiguredSaleorCloudAPLError = BaseError.subclass("MisconfiguredSaleorCloudAPLError"); switch (env.APL) { + case "dynamodb": { + const repository = SegmentAPLRepositoryFactory.create(); + + apl = new DynamoAPL({ repository }); + break; + } case "saleor-cloud": { if (!env.REST_APL_ENDPOINT || !env.REST_APL_TOKEN) { throw new MisconfiguredSaleorCloudAPLError( diff --git a/apps/segment/src/setup-tests.ts b/apps/segment/src/setup-tests.ts index cb0ff5c3b5..d788c3b294 100644 --- a/apps/segment/src/setup-tests.ts +++ b/apps/segment/src/setup-tests.ts @@ -1 +1,5 @@ +import { vi } from "vitest"; + +vi.stubEnv("SECRET_KEY", "test_secret_key"); + export {}; diff --git a/apps/segment/turbo.json b/apps/segment/turbo.json index 11410c5ad2..e2206676d1 100644 --- a/apps/segment/turbo.json +++ b/apps/segment/turbo.json @@ -17,7 +17,11 @@ "MANIFEST_APP_ID", "SENTRY_ORG", "SENTRY_PROJECT", - "SENTRY_AUTH_TOKEN" + "SENTRY_AUTH_TOKEN", + "DYNAMODB_MAIN_TABLE_NAME", + "AWS_REGION", + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY" ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 826560b27b..2dd3235b93 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1125,6 +1125,15 @@ importers: apps/segment: dependencies: + '@aws-sdk/client-dynamodb': + specifier: 3.651.1 + version: 3.651.1 + '@aws-sdk/lib-dynamodb': + specifier: 3.651.1 + version: 3.651.1(@aws-sdk/client-dynamodb@3.651.1) + '@aws-sdk/util-dynamodb': + specifier: 3.651.1 + version: 3.651.1(@aws-sdk/client-dynamodb@3.651.1) '@hookform/resolvers': specifier: ^3.3.1 version: 3.3.1(react-hook-form@7.44.3(react@18.2.0)) @@ -1227,6 +1236,9 @@ importers: dotenv: specifier: 16.3.1 version: 16.3.1 + dynamodb-toolbox: + specifier: 1.8.2 + version: 1.8.2(@aws-sdk/client-dynamodb@3.651.1)(@aws-sdk/lib-dynamodb@3.651.1(@aws-sdk/client-dynamodb@3.651.1)) graphql: specifier: 16.7.1 version: 16.7.1 @@ -1312,6 +1324,9 @@ importers: '@typescript-eslint/parser': specifier: 7.15.0 version: 7.15.0(eslint@node_modules+eslint)(typescript@5.5.4) + aws-sdk-client-mock: + specifier: 4.0.1 + version: 4.0.1 eslint: specifier: ../../node_modules/eslint version: link:../../node_modules/eslint From e86926f26e2693d6c7106fdcdb49c6914834f01d Mon Sep 17 00:00:00 2001 From: JZinkl Date: Fri, 17 Jan 2025 10:46:28 +0100 Subject: [PATCH 06/17] feat: related media improvement google feed (#1691) Co-authored-by: Patryk Andrzejewski --- .changeset/lemon-news-give.md | 5 +++++ .../src/modules/google-feed/get-related-media.ts | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 .changeset/lemon-news-give.md diff --git a/.changeset/lemon-news-give.md b/.changeset/lemon-news-give.md new file mode 100644 index 0000000000..def19b0275 --- /dev/null +++ b/.changeset/lemon-news-give.md @@ -0,0 +1,5 @@ +--- +"products-feed": minor +--- + +Product feed: prioritize media assigned to a specific product variant. with a fallback mechanism to use product media when no variant-specific media is available. The changes aim to enhance the precision of media selection diff --git a/apps/products-feed/src/modules/google-feed/get-related-media.ts b/apps/products-feed/src/modules/google-feed/get-related-media.ts index 781220f848..c99166f6b0 100644 --- a/apps/products-feed/src/modules/google-feed/get-related-media.ts +++ b/apps/products-feed/src/modules/google-feed/get-related-media.ts @@ -23,15 +23,14 @@ export const getRelatedMedia = ({ variantMediaMap, productMedia, }: getRelatedMediaArgs) => { - // Saleor always uses the first photo as thumbnail - even if it's assigned to the variant - const productThumbnailUrl = productMedia[0]?.url; - const mediaAssignedToAnyVariant = Object.values(variantMediaMap).flat() || []; const mediaAssignedToNoVariant = productMedia?.filter((m) => !mediaAssignedToAnyVariant.find((vm) => vm.id === m.id)) || []; const mediaAssignedToVariant = variantMediaMap[productVariantId] || []; + // Saleor always uses the first photo as thumbnail - even if it's assigned to the variant + const productThumbnailUrl = mediaAssignedToVariant[0]?.url || productMedia[0]?.url; const additionalImages = [...mediaAssignedToVariant, ...mediaAssignedToNoVariant] From 36aea8d9166e3f0f3c39e84750a2a05367451747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:21:23 +0100 Subject: [PATCH 07/17] App config in DynamoDB for Segment app (#1698) --- .changeset/sour-hotels-nail.md | 5 + apps/segment/src/env.ts | 8 +- apps/segment/src/lib/dyanmodb-apl.test.ts | 43 +++--- .../app-config-metadata-manager.ts | 42 ------ .../src/modules/configuration/app-config.ts | 48 +++---- .../configuration/configuration.router.ts | 84 ++++++++---- .../dynamo-app-config-manager.test.ts | 97 ++++++++++++++ .../dynamo-app-config-manager.ts | 65 +++++++++ .../modules/configuration/metadata-manager.ts | 14 -- .../schemas/root-config.schema.ts | 8 +- .../segment-config-form.tsx | 15 +-- .../db/__tests__/memory-config-repository.ts | 36 +++++ .../src/modules/db/dynamo-config-factory.ts | 18 +++ .../modules/db/dynamo-config-mapper.test.ts | 58 +++++++++ .../src/modules/db/dynamo-config-mapper.ts | 37 ++++++ .../db/dynamo-config-repository.test.ts | 123 ++++++++++++++++++ .../modules/db/dynamo-config-repository.ts | 100 ++++++++++++++ .../db/segment-apl-repository-factory.ts | 25 +--- .../src/modules/db/segment-apl-repository.ts | 8 +- .../src/modules/db/segment-main-table.test.ts | 21 +++ .../src/modules/db/segment-main-table.ts | 39 ++++++ apps/segment/src/modules/db/types.ts | 16 +++ .../segment-event-tracker-factory.test.ts | 31 +---- .../segment/segment-event-tracker-factory.ts | 26 +--- .../track-event.use-case.test.ts | 8 +- .../tracking-events/track-event.use-case.ts | 5 +- .../src/pages/api/webhooks/order-cancelled.ts | 43 ++++-- .../src/pages/api/webhooks/order-created.ts | 43 ++++-- .../pages/api/webhooks/order-fully-paid.ts | 43 ++++-- .../src/pages/api/webhooks/order-refunded.ts | 43 ++++-- .../src/pages/api/webhooks/order-updated.ts | 43 ++++-- apps/segment/src/setup-tests.ts | 4 + 32 files changed, 899 insertions(+), 300 deletions(-) create mode 100644 .changeset/sour-hotels-nail.md delete mode 100644 apps/segment/src/modules/configuration/app-config-metadata-manager.ts create mode 100644 apps/segment/src/modules/configuration/dynamo-app-config-manager.test.ts create mode 100644 apps/segment/src/modules/configuration/dynamo-app-config-manager.ts delete mode 100644 apps/segment/src/modules/configuration/metadata-manager.ts create mode 100644 apps/segment/src/modules/db/__tests__/memory-config-repository.ts create mode 100644 apps/segment/src/modules/db/dynamo-config-factory.ts create mode 100644 apps/segment/src/modules/db/dynamo-config-mapper.test.ts create mode 100644 apps/segment/src/modules/db/dynamo-config-mapper.ts create mode 100644 apps/segment/src/modules/db/dynamo-config-repository.test.ts create mode 100644 apps/segment/src/modules/db/dynamo-config-repository.ts diff --git a/.changeset/sour-hotels-nail.md b/.changeset/sour-hotels-nail.md new file mode 100644 index 0000000000..cb6cbcaf01 --- /dev/null +++ b/.changeset/sour-hotels-nail.md @@ -0,0 +1,5 @@ +--- +"segment": patch +--- + +Store app config in DynamoDB instead of Saleor app metadata. diff --git a/apps/segment/src/env.ts b/apps/segment/src/env.ts index 42df8546c7..44ebca1a98 100644 --- a/apps/segment/src/env.ts +++ b/apps/segment/src/env.ts @@ -27,10 +27,10 @@ export const env = createEnv({ PORT: z.coerce.number().optional().default(3000), SECRET_KEY: z.string(), VERCEL_URL: z.string().optional(), - DYNAMODB_MAIN_TABLE_NAME: z.string().optional(), - AWS_REGION: z.string().optional(), - AWS_ACCESS_KEY_ID: z.string().optional(), - AWS_SECRET_ACCESS_KEY: z.string().optional(), + DYNAMODB_MAIN_TABLE_NAME: z.string(), + AWS_REGION: z.string(), + AWS_ACCESS_KEY_ID: z.string(), + AWS_SECRET_ACCESS_KEY: z.string(), }, shared: { NODE_ENV: z.enum(["development", "production", "test"]).optional().default("development"), diff --git a/apps/segment/src/lib/dyanmodb-apl.test.ts b/apps/segment/src/lib/dyanmodb-apl.test.ts index bd661f2283..7ee9c57963 100644 --- a/apps/segment/src/lib/dyanmodb-apl.test.ts +++ b/apps/segment/src/lib/dyanmodb-apl.test.ts @@ -159,8 +159,18 @@ describe("DynamoAPL", () => { }); it("should return ready:true when APL related env variables are set", async () => { + const repository = new InMemoryAPLRepository(); + const apl = new DynamoAPL({ repository }); + + const result = await apl.isReady(); + + expect(result).toStrictEqual({ ready: true }); + }); + + it("should return ready:false when APL related env variables are not set", async () => { vi.spyOn(await import("@/env"), "env", "get").mockReturnValue({ - DYNAMODB_MAIN_TABLE_NAME: "table_name", + // @ts-expect-error - testing missing env variables + DYNAMODB_MAIN_TABLE_NAME: undefined, AWS_REGION: "region", AWS_ACCESS_KEY_ID: "access_key_id", AWS_SECRET_ACCESS_KEY: "secret_access_key", @@ -173,33 +183,30 @@ describe("DynamoAPL", () => { NODE_ENV: "test", ENV: "local", }); - const repository = new InMemoryAPLRepository(); const apl = new DynamoAPL({ repository }); const result = await apl.isReady(); - expect(result).toStrictEqual({ ready: true }); + expect(result).toStrictEqual({ + ready: false, + error: new DynamoAPL.MissingEnvVariablesError("Missing DynamoDB env variables"), + }); }); - it("should return ready:false when APL related env variables are not set", async () => { + it("should return configured:true when APL related env variables are set", async () => { const repository = new InMemoryAPLRepository(); const apl = new DynamoAPL({ repository }); - const result = await apl.isReady(); + const result = await apl.isConfigured(); - expect(result).toStrictEqual({ - ready: false, - error: new DynamoAPL.MissingEnvVariablesError("Missing DynamoDB env variables"), - }); + expect(result).toStrictEqual({ configured: true }); }); - it("should return configured:true when APL related env variables are set", async () => { + it("should return configured:false when APL related env variables are not set", async () => { vi.spyOn(await import("@/env"), "env", "get").mockReturnValue({ - DYNAMODB_MAIN_TABLE_NAME: "table_name", - AWS_REGION: "region", - AWS_ACCESS_KEY_ID: "access_key_id", - AWS_SECRET_ACCESS_KEY: "secret_access_key", + // @ts-expect-error - testing missing env variables + DYNAMODB_MAIN_TABLE_NAME: undefined, APL: "dynamodb", APP_LOG_LEVEL: "info", MANIFEST_APP_ID: "", @@ -209,15 +216,7 @@ describe("DynamoAPL", () => { NODE_ENV: "test", ENV: "local", }); - const repository = new InMemoryAPLRepository(); - const apl = new DynamoAPL({ repository }); - - const result = await apl.isConfigured(); - expect(result).toStrictEqual({ configured: true }); - }); - - it("should return configured:false when APL related env variables are not set", async () => { const repository = new InMemoryAPLRepository(); const apl = new DynamoAPL({ repository }); diff --git a/apps/segment/src/modules/configuration/app-config-metadata-manager.ts b/apps/segment/src/modules/configuration/app-config-metadata-manager.ts deleted file mode 100644 index a7bddcf5ee..0000000000 --- a/apps/segment/src/modules/configuration/app-config-metadata-manager.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { AuthData } from "@saleor/app-sdk/APL"; -import { SettingsManager } from "@saleor/app-sdk/settings-manager"; -import { createGraphQLClient } from "@saleor/apps-shared"; - -import { AppConfig } from "./app-config"; -import { createSettingsManager } from "./metadata-manager"; - -export interface IAppConfigMetadataManager { - get(): Promise; - set(config: AppConfig): Promise; -} - -export class AppConfigMetadataManager implements IAppConfigMetadataManager { - public readonly metadataKey = "app-config-v1"; - - private constructor(private mm: SettingsManager) {} - - async get() { - const metadata = await this.mm.get(this.metadataKey); - - return metadata ? AppConfig.parse(metadata) : new AppConfig(); - } - - set(config: AppConfig) { - return this.mm.set({ - key: this.metadataKey, - value: config.serialize(), - }); - } - - static createFromAuthData(authData: AuthData): AppConfigMetadataManager { - const settingsManager = createSettingsManager( - createGraphQLClient({ - saleorApiUrl: authData.saleorApiUrl, - token: authData.token, - }), - authData.appId, - ); - - return new AppConfigMetadataManager(settingsManager); - } -} diff --git a/apps/segment/src/modules/configuration/app-config.ts b/apps/segment/src/modules/configuration/app-config.ts index 42008bdb51..cbb11452ce 100644 --- a/apps/segment/src/modules/configuration/app-config.ts +++ b/apps/segment/src/modules/configuration/app-config.ts @@ -1,46 +1,34 @@ -import { z } from "zod"; +import { err, ok, Result } from "neverthrow"; import { BaseError } from "@/errors"; import { RootConfig } from "./schemas/root-config.schema"; export class AppConfig { - private rootData: RootConfig.Shape = null; + static SetSegmentKeyError = BaseError.subclass("SetSegmentKeyError"); - static JSONParseError = BaseError.subclass("JSONParseError"); + constructor(private rootData: RootConfig.Shape) {} - constructor(initialData?: RootConfig.Shape) { - if (initialData) { - this.rootData = RootConfig.Schema.parse(initialData); - } - } + setSegmentWriteKey( + key: string, + ): Result> { + const parsedKey = RootConfig.Schema.shape.segmentWriteKey.safeParse(key); - static parse(serializedSchema: string) { - try { - const parsedJSON = JSON.parse(serializedSchema); - - return new AppConfig(parsedJSON as RootConfig.Shape); - } catch (e) { - throw new AppConfig.JSONParseError("Error parsing JSON with app config", { cause: e }); + if (!parsedKey.success) { + return err( + new AppConfig.SetSegmentKeyError("Invalid segment write key", { + cause: parsedKey.error, + }), + ); } - } - serialize() { - return JSON.stringify(this.rootData); - } - - setSegmentWriteKey(key: string) { - const parsedKey = z.string().min(1).parse(key); + this.rootData.segmentWriteKey = parsedKey.data; - if (this.rootData) { - this.rootData.segmentWriteKey = parsedKey; - } else { - this.rootData = { - segmentWriteKey: parsedKey, - }; - } + return ok(this); + } - return this; + getSegmentWriteKey() { + return this.rootData.segmentWriteKey; } getConfig() { diff --git a/apps/segment/src/modules/configuration/configuration.router.ts b/apps/segment/src/modules/configuration/configuration.router.ts index 28bcc0209a..e71b75352d 100644 --- a/apps/segment/src/modules/configuration/configuration.router.ts +++ b/apps/segment/src/modules/configuration/configuration.router.ts @@ -3,14 +3,19 @@ import { z } from "zod"; import { createLogger } from "@/logger"; +import { DynamoConfigRepositoryFactory } from "../db/dynamo-config-factory"; import { protectedClientProcedure } from "../trpc/protected-client-procedure"; import { router } from "../trpc/trpc-server"; import { WebhooksActivityClient } from "../webhooks/webhook-activity/webhook-activity-client"; import { WebhookActivityService } from "../webhooks/webhook-activity/webhook-activity-service"; -import { AppConfigMetadataManager } from "./app-config-metadata-manager"; +import { AppConfig } from "./app-config"; +import { DynamoAppConfigManager } from "./dynamo-app-config-manager"; const logger = createLogger("configurationRouter"); +const configRepository = DynamoConfigRepositoryFactory.create(); +const configManager = DynamoAppConfigManager.create(configRepository); + export const configurationRouter = router({ getWebhookConfig: protectedClientProcedure.query(async ({ ctx }) => { const webhookActivityClient = new WebhooksActivityClient(ctx.apiClient); @@ -30,45 +35,68 @@ export const configurationRouter = router({ return { areWebhooksActive: isActiveResult.value.some(Boolean) }; }), getConfig: protectedClientProcedure.query(async ({ ctx }) => { - const manager = AppConfigMetadataManager.createFromAuthData({ - appId: ctx.appId, + const config = await configManager.get({ saleorApiUrl: ctx.saleorApiUrl, - token: ctx.appToken, + appId: ctx.appId, }); - const config = await manager.get(); - logger.debug("Successfully fetched config"); - return config.getConfig(); + if (config) { + return config.getConfig(); + } + + return null; }), - setConfig: protectedClientProcedure.input(z.string().min(1)).mutation(async ({ input, ctx }) => { - const manager = AppConfigMetadataManager.createFromAuthData({ - appId: ctx.appId, - saleorApiUrl: ctx.saleorApiUrl, - token: ctx.appToken, - }); + setOrCreateSegmentWriteKey: protectedClientProcedure + .input(z.string().min(1)) + .mutation(async ({ input, ctx }) => { + let config: AppConfig | null; + + config = await configManager.get({ + saleorApiUrl: ctx.saleorApiUrl, + appId: ctx.appId, + }); - const config = await manager.get(); + if (!config) { + // there is no config in DynamoDB - create new one and then set `segmentWriteKey` + config = new AppConfig({ + segmentWriteKey: input, + }); + } - config.setSegmentWriteKey(input); + const setWriteKeyResult = config.setSegmentWriteKey(input); - await manager.set(config); + if (setWriteKeyResult.isErr()) { + logger.error("Error during setting segment write key", { + error: setWriteKeyResult.error, + }); - logger.debug("Successfully set config"); + throw new TRPCError({ + code: "PARSE_ERROR", + message: + "There was an error with setting segment write key. Check if it has at least 1 character.", + }); + } - const webhookActivityClient = new WebhooksActivityClient(ctx.apiClient); - const webhookActivityService = new WebhookActivityService(ctx.appId, webhookActivityClient); + await configManager.set({ config, saleorApiUrl: ctx.saleorApiUrl, appId: ctx.appId }); - const enableAppWebhooksResult = await webhookActivityService.enableAppWebhooks(); + logger.debug("Successfully set config"); - if (enableAppWebhooksResult.isErr()) { - logger.error("Error during enabling app webhooks", { error: enableAppWebhooksResult.error }); + const webhookActivityClient = new WebhooksActivityClient(ctx.apiClient); + const webhookActivityService = new WebhookActivityService(ctx.appId, webhookActivityClient); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "There with enabling app webhooks. Contact Saleor support.", - }); - } - }), + const enableAppWebhooksResult = await webhookActivityService.enableAppWebhooks(); + + if (enableAppWebhooksResult.isErr()) { + logger.error("Error during enabling app webhooks", { + error: enableAppWebhooksResult.error, + }); + + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "There is a problem with enabling app webhooks. Contact Saleor support.", + }); + } + }), }); diff --git a/apps/segment/src/modules/configuration/dynamo-app-config-manager.test.ts b/apps/segment/src/modules/configuration/dynamo-app-config-manager.test.ts new file mode 100644 index 0000000000..53c73ec9c1 --- /dev/null +++ b/apps/segment/src/modules/configuration/dynamo-app-config-manager.test.ts @@ -0,0 +1,97 @@ +import { err } from "neverthrow"; +import { describe, expect, it, vi } from "vitest"; + +import { MemoryConfigRepository } from "../db/__tests__/memory-config-repository"; +import { AppConfig } from "./app-config"; +import { DynamoAppConfigManager } from "./dynamo-app-config-manager"; + +describe("DynamoAppConfigManager", () => { + it("should get App Config from DynamoDB", async () => { + const repository = new MemoryConfigRepository(); + const manager = DynamoAppConfigManager.create(repository); + const config = new AppConfig({ + segmentWriteKey: "segmentWriteKey", + }); + + repository.setAppConfigEntry({ + appId: "appId", + saleorApiUrl: "saleorApiUrl", + configKey: manager.configKey, + config: config, + }); + + const result = await manager.get({ + appId: "appId", + saleorApiUrl: "saleorApiUrl", + }); + + expect(result).toBe(config); + }); + + it("should return null if App Config does not exist in DynamoDB", async () => { + const repository = new MemoryConfigRepository(); + const manager = DynamoAppConfigManager.create(repository); + + const result = await manager.get({ + appId: "appId", + saleorApiUrl: "saleorApiUrl", + }); + + expect(result).toBeNull(); + }); + + it("should throw error if getting App Config fails", () => { + const repository = new MemoryConfigRepository(); + + vi.spyOn(repository, "getAppConfigEntry").mockReturnValue( + Promise.resolve(err(new DynamoAppConfigManager.GetConfigDataError("Error getting data"))), + ); + const manager = DynamoAppConfigManager.create(repository); + + expect(manager.get({ appId: "appId", saleorApiUrl: "saleorApiUrl" })).rejects.toThrowError( + DynamoAppConfigManager.GetConfigDataError, + ); + }); + + it("should set App Config in DynamoDB", async () => { + const repository = new MemoryConfigRepository(); + const manager = DynamoAppConfigManager.create(repository); + + const config = new AppConfig({ + segmentWriteKey: "segmentWriteKey", + }); + + const result = await manager.set({ + appId: "appId", + saleorApiUrl: "saleorApiUrl", + config: config, + }); + + expect(result).toBeUndefined(); + + const respositoryResult = await repository.getAppConfigEntry({ + appId: "appId", + saleorApiUrl: "saleorApiUrl", + configKey: manager.configKey, + }); + + expect(respositoryResult._unsafeUnwrap()).toBe(config); + }); + + it("should throw error if setting App Config fails", () => { + const repository = new MemoryConfigRepository(); + + vi.spyOn(repository, "setAppConfigEntry").mockReturnValue( + Promise.resolve(err(new DynamoAppConfigManager.SetConfigDataError("Error setting data"))), + ); + const manager = DynamoAppConfigManager.create(repository); + + expect( + manager.set({ + appId: "appId", + saleorApiUrl: "saleorApiUrl", + config: new AppConfig({ segmentWriteKey: "segmentWriteKey" }), + }), + ).rejects.toThrowError(DynamoAppConfigManager.SetConfigDataError); + }); +}); diff --git a/apps/segment/src/modules/configuration/dynamo-app-config-manager.ts b/apps/segment/src/modules/configuration/dynamo-app-config-manager.ts new file mode 100644 index 0000000000..a9ab7d9b63 --- /dev/null +++ b/apps/segment/src/modules/configuration/dynamo-app-config-manager.ts @@ -0,0 +1,65 @@ +import { env } from "@/env"; +import { BaseError } from "@/errors"; + +import { ConfigRepository } from "../db/types"; +import { AppConfig } from "./app-config"; + +export interface AppConfigManager { + get(args: { saleorApiUrl: string; appId: string }): Promise; + set(args: { config: AppConfig; saleorApiUrl: string; appId: string }): Promise; +} + +export class DynamoAppConfigManager implements AppConfigManager { + public readonly configKey = "app-config-v1"; + + static GetConfigDataError = BaseError.subclass("GetConfigDataError"); + static SetConfigDataError = BaseError.subclass("SetConfigDataError"); + + private constructor( + private deps: { + repository: ConfigRepository; + encryptionKey: string; + }, + ) {} + + static create(repository: ConfigRepository) { + return new DynamoAppConfigManager({ repository, encryptionKey: env.SECRET_KEY }); + } + + async get(args: { saleorApiUrl: string; appId: string }) { + const getEntryResult = await this.deps.repository.getAppConfigEntry({ + saleorApiUrl: args.saleorApiUrl, + appId: args.appId, + configKey: this.configKey, + }); + + if (getEntryResult.isErr()) { + throw new DynamoAppConfigManager.GetConfigDataError("Failed to get config data", { + cause: getEntryResult.error, + }); + } + + if (!getEntryResult.value) { + return null; + } + + return getEntryResult.value; + } + + async set(args: { config: AppConfig; saleorApiUrl: string; appId: string }) { + const setEntryResult = await this.deps.repository.setAppConfigEntry({ + appId: args.appId, + saleorApiUrl: args.saleorApiUrl, + configKey: this.configKey, + config: args.config, + }); + + if (setEntryResult.isErr()) { + throw new DynamoAppConfigManager.SetConfigDataError("Failed to set config data", { + cause: setEntryResult.error, + }); + } + + return undefined; + } +} diff --git a/apps/segment/src/modules/configuration/metadata-manager.ts b/apps/segment/src/modules/configuration/metadata-manager.ts deleted file mode 100644 index ce52c9ed45..0000000000 --- a/apps/segment/src/modules/configuration/metadata-manager.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { SettingsManager } from "@saleor/app-sdk/settings-manager"; -import { EncryptedMetadataManagerFactory } from "@saleor/apps-shared"; -import { Client } from "urql"; - -import { env } from "@/env"; - -export const createSettingsManager = ( - client: Pick, - appId: string, -): SettingsManager => { - const metadataManagerFactory = new EncryptedMetadataManagerFactory(env.SECRET_KEY); - - return metadataManagerFactory.create(client, appId); -}; diff --git a/apps/segment/src/modules/configuration/schemas/root-config.schema.ts b/apps/segment/src/modules/configuration/schemas/root-config.schema.ts index 33ef3f9bb6..d7369c4f09 100644 --- a/apps/segment/src/modules/configuration/schemas/root-config.schema.ts +++ b/apps/segment/src/modules/configuration/schemas/root-config.schema.ts @@ -6,11 +6,9 @@ export namespace RootConfig { * - Only one request * - Always transactional */ - export const Schema = z - .object({ - segmentWriteKey: z.string(), - }) - .nullable(); + export const Schema = z.object({ + segmentWriteKey: z.string().min(1), + }); export type Shape = z.infer; } diff --git a/apps/segment/src/modules/configuration/segment-config-form/segment-config-form.tsx b/apps/segment/src/modules/configuration/segment-config-form/segment-config-form.tsx index bbe1f138d7..6b40de00d0 100644 --- a/apps/segment/src/modules/configuration/segment-config-form/segment-config-form.tsx +++ b/apps/segment/src/modules/configuration/segment-config-form/segment-config-form.tsx @@ -4,19 +4,17 @@ import { ButtonsBox, Layout, SkeletonLayout, TextLink } from "@saleor/apps-ui"; import { Button, Text } from "@saleor/macaw-ui"; import { Input } from "@saleor/react-hook-form-macaw"; import { useForm } from "react-hook-form"; -import { z } from "zod"; import { trpcClient } from "@/modules/trpc/trpc-client"; import { RootConfig } from "../schemas/root-config.schema"; -const Schema = RootConfig.Schema.unwrap(); - -type Shape = z.infer; - -const SegmentConfigFormBase = (props: { values: Shape; onSubmit(values: Shape): void }) => { +const SegmentConfigFormBase = (props: { + values: RootConfig.Shape; + onSubmit(values: RootConfig.Shape): void; +}) => { const { control, handleSubmit } = useForm({ - resolver: zodResolver(Schema), + resolver: zodResolver(RootConfig.Schema), defaultValues: props.values, }); @@ -35,6 +33,7 @@ const SegmentConfigFormBase = (props: { values: Shape; onSubmit(values: Shape): name="segmentWriteKey" type="password" label="Segment write key" + required helperText={ Read about write keys in{" "} @@ -54,7 +53,7 @@ export const SegmentConfigForm = () => { const { data: config, isLoading, refetch } = trpcClient.configuration.getConfig.useQuery(); const utils = trpcClient.useUtils(); - const { mutate } = trpcClient.configuration.setConfig.useMutation({ + const { mutate } = trpcClient.configuration.setOrCreateSegmentWriteKey.useMutation({ onSuccess() { notifySuccess("Configuration saved"); refetch(); diff --git a/apps/segment/src/modules/db/__tests__/memory-config-repository.ts b/apps/segment/src/modules/db/__tests__/memory-config-repository.ts new file mode 100644 index 0000000000..1e6f65cce9 --- /dev/null +++ b/apps/segment/src/modules/db/__tests__/memory-config-repository.ts @@ -0,0 +1,36 @@ +import { ok, Result } from "neverthrow"; + +import { BaseError } from "@/errors"; +import { AppConfig } from "@/modules/configuration/app-config"; + +import { ConfigRepository } from "../types"; + +export class MemoryConfigRepository implements ConfigRepository { + public entries: Record = {}; + + async getAppConfigEntry(args: { + saleorApiUrl: string; + appId: string; + configKey: string; + }): Promise>> { + const key = `${args.saleorApiUrl}#${args.appId}`; + + if (this.entries[key]) { + return ok(this.entries[key]); + } + + return ok(null); + } + + async setAppConfigEntry(args: { + appId: string; + saleorApiUrl: string; + configKey: string; + config: AppConfig; + }): Promise>> { + const key = `${args.saleorApiUrl}#${args.appId}`; + + this.entries[key] = args.config; + return ok(undefined); + } +} diff --git a/apps/segment/src/modules/db/dynamo-config-factory.ts b/apps/segment/src/modules/db/dynamo-config-factory.ts new file mode 100644 index 0000000000..087af43706 --- /dev/null +++ b/apps/segment/src/modules/db/dynamo-config-factory.ts @@ -0,0 +1,18 @@ +import { BaseError } from "@/errors"; + +import { DynamoConfigRepository } from "./dynamo-config-repository"; + +export class DynamoConfigRepositoryFactory { + static RepositoryCreationError = BaseError.subclass("RepositoryCreationError"); + + static create(): DynamoConfigRepository { + try { + return new DynamoConfigRepository(); + } catch (error) { + throw new DynamoConfigRepositoryFactory.RepositoryCreationError( + "Failed to create DynamoDB config repository", + { cause: error }, + ); + } + } +} diff --git a/apps/segment/src/modules/db/dynamo-config-mapper.test.ts b/apps/segment/src/modules/db/dynamo-config-mapper.test.ts new file mode 100644 index 0000000000..d232785610 --- /dev/null +++ b/apps/segment/src/modules/db/dynamo-config-mapper.test.ts @@ -0,0 +1,58 @@ +import { encrypt } from "@saleor/app-sdk/settings-manager"; +import { FormattedItem } from "dynamodb-toolbox"; +import { describe, expect, it } from "vitest"; + +import { AppConfig } from "../configuration/app-config"; +import { DynamoConfigMapper } from "./dynamo-config-mapper"; +import { SegmentConfigEntityType } from "./segment-main-table"; + +describe("DynamoConfigMapper", () => { + const encryptionKey = "encryptionKey"; + + it("should map DynamoDB entity to AppConfig", () => { + const mapper = new DynamoConfigMapper({ + encryptionKey, + }); + + const entity: FormattedItem = { + PK: "saleorApiUrl#saleorAppId", + SK: "APP_CONFIG#configKey", + encryptedSegmentWriteKey: encrypt("encryptedKey", encryptionKey), + createdAt: "2023-01-01T00:00:00.000Z", + modifiedAt: "2023-01-01T00:00:00.000Z", + }; + + const config = mapper.dynamoEntityToAppConfig({ + entity, + }); + + expect(config).toBeInstanceOf(AppConfig); + + expect(config.getConfig()).toStrictEqual({ + segmentWriteKey: "encryptedKey", + }); + }); + + it("should map AppConfig to DynamoDB PutItemInput", () => { + const mapper = new DynamoConfigMapper({ + encryptionKey, + }); + + const config = new AppConfig({ + segmentWriteKey: "encryptedKey", + }); + + const entity = mapper.appConfigToDynamoPutEntity({ + config, + saleorApiUrl: "saleorApiUrl", + appId: "saleorAppId", + configKey: "configKey", + }); + + expect(entity).toStrictEqual({ + PK: "saleorApiUrl#saleorAppId", + SK: "APP_CONFIG#configKey", + encryptedSegmentWriteKey: expect.any(String), + }); + }); +}); diff --git a/apps/segment/src/modules/db/dynamo-config-mapper.ts b/apps/segment/src/modules/db/dynamo-config-mapper.ts new file mode 100644 index 0000000000..1929730e9d --- /dev/null +++ b/apps/segment/src/modules/db/dynamo-config-mapper.ts @@ -0,0 +1,37 @@ +import { decrypt, encrypt } from "@saleor/app-sdk/settings-manager"; +import { FormattedItem, PutItemInput } from "dynamodb-toolbox"; + +import { AppConfig } from "../configuration/app-config"; +import { SegmentConfigEntityType, SegmentMainTable } from "./segment-main-table"; + +export class DynamoConfigMapper { + constructor( + private deps: { + encryptionKey: string; + }, + ) {} + + dynamoEntityToAppConfig(args: { entity: FormattedItem }): AppConfig { + return new AppConfig({ + segmentWriteKey: decrypt(args.entity.encryptedSegmentWriteKey, this.deps.encryptionKey), + }); + } + + appConfigToDynamoPutEntity(args: { + config: AppConfig; + appId: string; + saleorApiUrl: string; + configKey: string; + }): PutItemInput { + return { + PK: SegmentMainTable.getConfigPrimaryKey({ + saleorApiUrl: args.saleorApiUrl, + appId: args.appId, + }), + SK: SegmentMainTable.getConfigSortKey({ + configKey: args.configKey, + }), + encryptedSegmentWriteKey: encrypt(args.config.getSegmentWriteKey(), this.deps.encryptionKey), + }; + } +} diff --git a/apps/segment/src/modules/db/dynamo-config-repository.test.ts b/apps/segment/src/modules/db/dynamo-config-repository.test.ts new file mode 100644 index 0000000000..300827b791 --- /dev/null +++ b/apps/segment/src/modules/db/dynamo-config-repository.test.ts @@ -0,0 +1,123 @@ +import { DynamoDBDocumentClient, GetCommand, PutCommand } from "@aws-sdk/lib-dynamodb"; +import { encrypt } from "@saleor/app-sdk/settings-manager"; +import { mockClient } from "aws-sdk-client-mock"; +import { SavedItem } from "dynamodb-toolbox"; +import { beforeEach, describe, expect, it } from "vitest"; + +import { env } from "@/env"; + +import { AppConfig } from "../configuration/app-config"; +import { DynamoConfigRepository } from "./dynamo-config-repository"; +import { SegmentMainTable, SegmentMainTableEntityFactory } from "./segment-main-table"; + +describe("DynamoConfigRepository", () => { + const mockDocumentClient = mockClient(DynamoDBDocumentClient); + + const segmentMainTable = SegmentMainTable.create({ + // @ts-expect-error https://github.com/m-radzikowski/aws-sdk-client-mock/issues/197 + documentClient: mockDocumentClient, + tableName: "segment-test-table", + }); + + const segmentConfigEntity = SegmentMainTableEntityFactory.createConfigEntity(segmentMainTable); + + beforeEach(() => { + mockDocumentClient.reset(); + }); + + it("should successfully get AppConfig from DynamoDB", async () => { + const mockedConfigEntry: SavedItem = { + PK: "saleorApiUrl#saleorAppId", + SK: "APP_CONFIG#configKey", + encryptedSegmentWriteKey: encrypt("encryptedKey", env.SECRET_KEY), + _et: "Config", + createdAt: "2023-01-01T00:00:00.000Z", + modifiedAt: "2023-01-01T00:00:00.000Z", + }; + + mockDocumentClient.on(GetCommand, {}).resolvesOnce({ + Item: mockedConfigEntry, + }); + + const repository = new DynamoConfigRepository(); + + const result = await repository.getAppConfigEntry({ + saleorApiUrl: "saleorApiUrl", + appId: "saleorAppId", + configKey: "configKey", + }); + + expect(result.isOk()).toBe(true); + + expect(result._unsafeUnwrap()).toBeInstanceOf(AppConfig); + }); + + it("should handle errors when getting AppConfig from DynamoDB", async () => { + mockDocumentClient.on(GetCommand, {}).rejectsOnce("Exception"); + + const repository = new DynamoConfigRepository(); + + const result = await repository.getAppConfigEntry({ + saleorApiUrl: "saleorApiUrl", + appId: "saleorAppId", + configKey: "configKey", + }); + + expect(result.isErr()).toBe(true); + expect(result._unsafeUnwrapErr()).toBeInstanceOf(DynamoConfigRepository.GetEntryError); + }); + + it("should return null if AppConfig entry does not exist in DynamoDB", async () => { + mockDocumentClient.on(GetCommand, {}).resolvesOnce({}); + + const repository = new DynamoConfigRepository(); + + const result = await repository.getAppConfigEntry({ + saleorApiUrl: "saleorApiUrl", + appId: "saleorAppId", + configKey: "configKey", + }); + + expect(result.isOk()).toBe(true); + + expect(result._unsafeUnwrap()).toBeNull(); + }); + + it("should successfully set AppConfig entry in DynamoDB", async () => { + mockDocumentClient.on(PutCommand, {}).resolvesOnce({}); + + const repository = new DynamoConfigRepository(); + + const result = await repository.setAppConfigEntry({ + saleorApiUrl: "saleorApiUrl", + appId: "saleorAppId", + configKey: "configKey", + config: new AppConfig({ + segmentWriteKey: "segmentWriteKey", + }), + }); + + expect(result.isOk()).toBe(true); + + expect(result._unsafeUnwrap()).toBe(undefined); + }); + + it("should handle errors when setting AppConfig entry in DynamoDB", async () => { + mockDocumentClient.on(PutCommand, {}).rejectsOnce("Exception"); + + const repository = new DynamoConfigRepository(); + + const result = await repository.setAppConfigEntry({ + saleorApiUrl: "saleorApiUrl", + appId: "saleorAppId", + configKey: "configKey", + config: new AppConfig({ + segmentWriteKey: "segmentWriteKey", + }), + }); + + expect(result.isErr()).toBe(true); + + expect(result._unsafeUnwrapErr()).toBeInstanceOf(DynamoConfigRepository.SetEntryError); + }); +}); diff --git a/apps/segment/src/modules/db/dynamo-config-repository.ts b/apps/segment/src/modules/db/dynamo-config-repository.ts new file mode 100644 index 0000000000..532d3e9cbf --- /dev/null +++ b/apps/segment/src/modules/db/dynamo-config-repository.ts @@ -0,0 +1,100 @@ +import { GetItemCommand, PutItemCommand } from "dynamodb-toolbox"; +import { err, ok, Result, ResultAsync } from "neverthrow"; + +import { env } from "@/env"; +import { BaseError } from "@/errors"; +import { createLogger } from "@/logger"; + +import { AppConfig } from "../configuration/app-config"; +import { DynamoConfigMapper } from "./dynamo-config-mapper"; +import { + SegmentMainTable, + segmentMainTable, + SegmentMainTableEntityFactory, +} from "./segment-main-table"; +import { ConfigRepository } from "./types"; + +export class DynamoConfigRepository implements ConfigRepository { + private logger = createLogger("SegmentConfigRepository"); + private mapper = new DynamoConfigMapper({ + encryptionKey: env.SECRET_KEY, + }); + private configEntity = SegmentMainTableEntityFactory.createConfigEntity(segmentMainTable); + + static GetEntryError = BaseError.subclass("GetEntryError"); + static SetEntryError = BaseError.subclass("SetEntryError"); + + constructor() {} + + async getAppConfigEntry(args: { + saleorApiUrl: string; + appId: string; + configKey: string; + }): Promise>> { + const getEntryResult = await ResultAsync.fromPromise( + this.configEntity + .build(GetItemCommand) + .key({ + PK: SegmentMainTable.getConfigPrimaryKey({ + saleorApiUrl: args.saleorApiUrl, + appId: args.appId, + }), + SK: SegmentMainTable.getConfigSortKey({ + configKey: args.configKey, + }), + }) + .send(), + (error) => + new DynamoConfigRepository.GetEntryError("Failed to get config entry", { cause: error }), + ); + + if (getEntryResult.isErr()) { + this.logger.error("Error while reading config entry from DynamoDB", { + error: getEntryResult.error, + }); + + return err(getEntryResult.error); + } + + if (!getEntryResult.value.Item) { + this.logger.warn("Config entry not found", { args }); + + return ok(null); + } + + return ok(this.mapper.dynamoEntityToAppConfig({ entity: getEntryResult.value.Item })); + } + + async setAppConfigEntry(args: { + appId: string; + saleorApiUrl: string; + configKey: string; + config: AppConfig; + }): Promise>> { + const setEntryResult = await ResultAsync.fromPromise( + this.configEntity + .build(PutItemCommand) + .item( + this.mapper.appConfigToDynamoPutEntity({ + config: args.config, + appId: args.appId, + saleorApiUrl: args.saleorApiUrl, + configKey: args.configKey, + }), + ) + .send(), + (error) => + new DynamoConfigRepository.SetEntryError("Failed to set config entry", { cause: error }), + ); + + if (setEntryResult.isErr()) { + this.logger.error("Error while putting config into DynamoDB", { + error: setEntryResult.error, + }); + + return err(setEntryResult.error); + } + + return ok(undefined); + } +} diff --git a/apps/segment/src/modules/db/segment-apl-repository-factory.ts b/apps/segment/src/modules/db/segment-apl-repository-factory.ts index 9bb6799fac..b07eb2a96d 100644 --- a/apps/segment/src/modules/db/segment-apl-repository-factory.ts +++ b/apps/segment/src/modules/db/segment-apl-repository-factory.ts @@ -1,35 +1,14 @@ -import { env } from "@/env"; import { BaseError } from "@/errors"; import { SegmentAPLRepository } from "./segment-apl-repository"; -import { - documentClient, - SegmentMainTable, - SegmentMainTableEntityFactory, -} from "./segment-main-table"; +import { segmentMainTable, SegmentMainTableEntityFactory } from "./segment-main-table"; export class SegmentAPLRepositoryFactory { static RepositoryCreationError = BaseError.subclass("RepositoryCreationError"); static create(): SegmentAPLRepository { - if ( - !env.DYNAMODB_MAIN_TABLE_NAME || - !env.AWS_REGION || - !env.AWS_ACCESS_KEY_ID || - !env.AWS_SECRET_ACCESS_KEY - ) { - throw new SegmentAPLRepositoryFactory.RepositoryCreationError( - "DynamoDB APL is not configured - missing env variables.", - ); - } - try { - // TODO: when we have config in DyanamoDB - move to `segment-main-table.ts` - const table = SegmentMainTable.create({ - tableName: env.DYNAMODB_MAIN_TABLE_NAME, - documentClient, - }); - const segmentAPLEntity = SegmentMainTableEntityFactory.createAPLEntity(table); + const segmentAPLEntity = SegmentMainTableEntityFactory.createAPLEntity(segmentMainTable); return new SegmentAPLRepository({ segmentAPLEntity }); } catch (error) { diff --git a/apps/segment/src/modules/db/segment-apl-repository.ts b/apps/segment/src/modules/db/segment-apl-repository.ts index def765b114..f4d50b595e 100644 --- a/apps/segment/src/modules/db/segment-apl-repository.ts +++ b/apps/segment/src/modules/db/segment-apl-repository.ts @@ -64,7 +64,9 @@ export class SegmentAPLRepository implements APLRepository { .item(this.segmentAPLMapper.authDataToDynamoPutEntity(args.authData)) .send(), (error) => - new SegmentAPLRepository.WriteEntityError("Failed to write APL entity", { cause: error }), + new SegmentAPLRepository.WriteEntityError("Failed to write APL entity", { + cause: error, + }), ); if (setEntryResult.isErr()) { @@ -117,7 +119,9 @@ export class SegmentAPLRepository implements APLRepository { }) .send(), (error) => - new SegmentAPLRepository.ScanEntityError("Failed to scan APL entities", { cause: error }), + new SegmentAPLRepository.ScanEntityError("Failed to scan APL entities", { + cause: error, + }), ); if (scanEntriesResult.isErr()) { diff --git a/apps/segment/src/modules/db/segment-main-table.test.ts b/apps/segment/src/modules/db/segment-main-table.test.ts index f852dfff8e..8cbe5a9134 100644 --- a/apps/segment/src/modules/db/segment-main-table.test.ts +++ b/apps/segment/src/modules/db/segment-main-table.test.ts @@ -56,4 +56,25 @@ describe("SegmentMainTable", () => { }); }); }); + + describe("SegementConfigEntity", () => { + it("should create a new entity in DynamoDB with default fields", () => { + const configEntity = SegmentMainTableEntityFactory.createConfigEntity(segmentMainTable); + + const parseResult = configEntity.build(EntityParser).parse({ + PK: "saleorApiUrl#saleorAppId", + SK: "APP_CONFIG#configKey", + encryptedSegmentWriteKey: "key", + }); + + expect(parseResult.item).toStrictEqual({ + PK: "saleorApiUrl#saleorAppId", + SK: "APP_CONFIG#configKey", + _et: "Config", + createdAt: "2023-01-01T00:00:00.000Z", + modifiedAt: "2023-01-01T00:00:00.000Z", + encryptedSegmentWriteKey: "key", + }); + }); + }); }); diff --git a/apps/segment/src/modules/db/segment-main-table.ts b/apps/segment/src/modules/db/segment-main-table.ts index 924affbf19..f78a07ae52 100644 --- a/apps/segment/src/modules/db/segment-main-table.ts +++ b/apps/segment/src/modules/db/segment-main-table.ts @@ -1,6 +1,7 @@ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; import { Entity, schema, string, Table } from "dynamodb-toolbox"; +import { env } from "@/env"; import { createDynamoDBClient, createDynamoDBDocumentClient } from "@/lib/dynamodb-client"; type PartitionKey = { name: "PK"; type: "string" }; @@ -39,6 +40,14 @@ export class SegmentMainTable extends Table { static getAPLSortKey() { return `APL` as const; } + + static getConfigPrimaryKey({ saleorApiUrl, appId }: { saleorApiUrl: string; appId: string }) { + return `${saleorApiUrl}#${appId}` as const; + } + + static getConfigSortKey({ configKey }: { configKey: string }) { + return `APP_CONFIG#${configKey}` as const; + } } const SegmentConfigTableSchema = { @@ -51,12 +60,22 @@ const SegmentConfigTableSchema = { appId: string(), jwks: string().optional(), }), + config: schema({ + PK: string().key(), + SK: string().key(), + encryptedSegmentWriteKey: string(), + }), }; export const client = createDynamoDBClient(); export const documentClient = createDynamoDBDocumentClient(client); +export const segmentMainTable = SegmentMainTable.create({ + tableName: env.DYNAMODB_MAIN_TABLE_NAME, + documentClient, +}); + export const SegmentMainTableEntityFactory = { createAPLEntity: (table: SegmentMainTable) => { return new Entity({ @@ -75,6 +94,26 @@ export const SegmentMainTableEntityFactory = { }, }); }, + createConfigEntity: (table: SegmentMainTable) => { + return new Entity({ + table, + name: "Config", + schema: SegmentConfigTableSchema.config, + timestamps: { + created: { + name: "createdAt", + savedAs: "createdAt", + }, + modified: { + name: "modifiedAt", + savedAs: "modifiedAt", + }, + }, + }); + }, }; export type SegmentAPLEntityType = ReturnType; +export type SegmentConfigEntityType = ReturnType< + typeof SegmentMainTableEntityFactory.createConfigEntity +>; diff --git a/apps/segment/src/modules/db/types.ts b/apps/segment/src/modules/db/types.ts index 302c892c76..d71ffb6457 100644 --- a/apps/segment/src/modules/db/types.ts +++ b/apps/segment/src/modules/db/types.ts @@ -3,6 +3,8 @@ import { Result } from "neverthrow"; import { BaseError } from "@/errors"; +import { AppConfig } from "../configuration/app-config"; + export interface APLRepository { getEntry(args: { saleorApiUrl: string; @@ -13,3 +15,17 @@ export interface APLRepository { }): Promise>>; getAllEntries(): Promise>>; } + +export interface ConfigRepository { + getAppConfigEntry: (args: { + saleorApiUrl: string; + appId: string; + configKey: string; + }) => Promise>>; + setAppConfigEntry: (args: { + appId: string; + saleorApiUrl: string; + configKey: string; + config: AppConfig; + }) => Promise>>; +} diff --git a/apps/segment/src/modules/segment/segment-event-tracker-factory.test.ts b/apps/segment/src/modules/segment/segment-event-tracker-factory.test.ts index 138348669b..e657306d98 100644 --- a/apps/segment/src/modules/segment/segment-event-tracker-factory.test.ts +++ b/apps/segment/src/modules/segment/segment-event-tracker-factory.test.ts @@ -1,40 +1,17 @@ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it } from "vitest"; import { AppConfig } from "../configuration/app-config"; -import { IAppConfigMetadataManager } from "../configuration/app-config-metadata-manager"; import { SegmentEventTrackerFactory } from "./segment-event-tracker-factory"; import { SegmentEventsTracker } from "./segment-events-tracker"; describe("SegmentEventTrackerFactory", () => { it("should create an instance of SegmentEventsTracker from app config", async () => { const appConfig = new AppConfig({ segmentWriteKey: "key" }); - const mockedAppConfigMetadataManager: IAppConfigMetadataManager = { - get: vi.fn(() => Promise.resolve(appConfig)), - set: vi.fn(), - }; - const factory = new SegmentEventTrackerFactory({ - appConfigMetadataManager: mockedAppConfigMetadataManager, - }); - const instance = await factory.createFromAppConfig(); + const factory = new SegmentEventTrackerFactory(); - expect(instance._unsafeUnwrap()).toBeInstanceOf(SegmentEventsTracker); - }); - - it("should return error when segment write key is not found in app config", async () => { - const appConfig = new AppConfig(); - const mockedAppConfigMetadataManager: IAppConfigMetadataManager = { - get: vi.fn(() => Promise.resolve(appConfig)), - set: vi.fn(), - }; - const factory = new SegmentEventTrackerFactory({ - appConfigMetadataManager: mockedAppConfigMetadataManager, - }); + const instance = await factory.createFromAppConfig({ config: appConfig }); - const instance = await factory.createFromAppConfig(); - - expect(instance._unsafeUnwrapErr()).toBeInstanceOf( - SegmentEventTrackerFactory.SegmentWriteKeyNotFoundError, - ); + expect(instance._unsafeUnwrap()).toBeInstanceOf(SegmentEventsTracker); }); }); diff --git a/apps/segment/src/modules/segment/segment-event-tracker-factory.ts b/apps/segment/src/modules/segment/segment-event-tracker-factory.ts index 834ddd6d9e..81d7d8545d 100644 --- a/apps/segment/src/modules/segment/segment-event-tracker-factory.ts +++ b/apps/segment/src/modules/segment/segment-event-tracker-factory.ts @@ -1,36 +1,22 @@ -import { err, ok, Result } from "neverthrow"; +import { ok, Result } from "neverthrow"; import { BaseError } from "@/errors"; -import { IAppConfigMetadataManager } from "../configuration/app-config-metadata-manager"; +import { AppConfig } from "../configuration/app-config"; import { SegmentClient } from "./segment.client"; import { SegmentEventsTracker } from "./segment-events-tracker"; export interface ISegmentEventTrackerFactory { - createFromAppConfig(): Promise>; + createFromAppConfig(args: { config: AppConfig }): Promise>; } export class SegmentEventTrackerFactory implements ISegmentEventTrackerFactory { static SegmentWriteKeyNotFoundError = BaseError.subclass("SegmentNotConfiguredError"); - constructor( - private deps: { - appConfigMetadataManager: IAppConfigMetadataManager; - }, - ) {} + constructor() {} - async createFromAppConfig() { - const config = await this.deps.appConfigMetadataManager.get(); - - const segmentKey = config.getConfig()?.segmentWriteKey; - - if (!segmentKey) { - return err( - new SegmentEventTrackerFactory.SegmentWriteKeyNotFoundError( - "Segment write key not found in app config", - ), - ); - } + async createFromAppConfig(args: { config: AppConfig }) { + const segmentKey = args.config.getSegmentWriteKey(); return ok( new SegmentEventsTracker( diff --git a/apps/segment/src/modules/tracking-events/track-event.use-case.test.ts b/apps/segment/src/modules/tracking-events/track-event.use-case.test.ts index ac886763f6..ebe038ba1e 100644 --- a/apps/segment/src/modules/tracking-events/track-event.use-case.test.ts +++ b/apps/segment/src/modules/tracking-events/track-event.use-case.test.ts @@ -1,6 +1,7 @@ import { err, ok } from "neverthrow"; import { describe, expect, it, vi } from "vitest"; +import { AppConfig } from "../configuration/app-config"; import { ISegmentClient } from "../segment/segment.client"; import { ISegmentEventTrackerFactory } from "../segment/segment-event-tracker-factory"; import { SegmentEventsTracker } from "../segment/segment-events-tracker"; @@ -19,6 +20,7 @@ describe("TrackEventUseCase", () => { return Promise.resolve(ok(mockedSegmentEventTracker)); }, }; + const mockedAppConfig = new AppConfig({ segmentWriteKey: "key" }); it("creates instance", () => { const instance = new TrackEventUseCase({ @@ -38,7 +40,7 @@ describe("TrackEventUseCase", () => { issuedAt: "2025-01-07", }); - await useCase.track(event); + await useCase.track(event, mockedAppConfig); expect(mockedSegmentClient.track).toHaveBeenCalledWith({ event: "Saleor Order Created", @@ -86,7 +88,7 @@ describe("TrackEventUseCase", () => { issuedAt: "2025-01-07", }); - const result = await useCase.track(event); + const result = await useCase.track(event, mockedAppConfig); expect(result._unsafeUnwrapErr()).toBeInstanceOf( TrackEventUseCase.TrackEventUseCaseSegmentClientError, @@ -116,7 +118,7 @@ describe("TrackEventUseCase", () => { issuedAt: "2025-01-07", }); - const result = await useCase.track(event); + const result = await useCase.track(event, mockedAppConfig); expect(result._unsafeUnwrapErr()).toBeInstanceOf( TrackEventUseCase.TrackEventUseCaseUnknownError, diff --git a/apps/segment/src/modules/tracking-events/track-event.use-case.ts b/apps/segment/src/modules/tracking-events/track-event.use-case.ts index de7d5772de..6a87531a4e 100644 --- a/apps/segment/src/modules/tracking-events/track-event.use-case.ts +++ b/apps/segment/src/modules/tracking-events/track-event.use-case.ts @@ -2,6 +2,7 @@ import { err, ResultAsync } from "neverthrow"; import { BaseError } from "@/errors"; +import { AppConfig } from "../configuration/app-config"; import { ISegmentEventTrackerFactory } from "../segment/segment-event-tracker-factory"; import { TrackingBaseEvent } from "./tracking-events"; @@ -15,9 +16,9 @@ export class TrackEventUseCase { }, ) {} - async track(event: TrackingBaseEvent) { + async track(event: TrackingBaseEvent, config: AppConfig) { const segmentEventTrackerResult = - await this.deps.segmentEventTrackerFactory.createFromAppConfig(); + await this.deps.segmentEventTrackerFactory.createFromAppConfig({ config }); if (segmentEventTrackerResult.isErr()) { return err( diff --git a/apps/segment/src/pages/api/webhooks/order-cancelled.ts b/apps/segment/src/pages/api/webhooks/order-cancelled.ts index 9f3477e282..2c07d397e3 100644 --- a/apps/segment/src/pages/api/webhooks/order-cancelled.ts +++ b/apps/segment/src/pages/api/webhooks/order-cancelled.ts @@ -6,7 +6,8 @@ import { ObservabilityAttributes } from "@saleor/apps-otel/src/lib/observability import { OrderUpdatedSubscriptionPayloadFragment } from "@/generated/graphql"; import { createLogger } from "@/logger"; import { loggerContext } from "@/logger-context"; -import { AppConfigMetadataManager } from "@/modules/configuration/app-config-metadata-manager"; +import { DynamoAppConfigManager } from "@/modules/configuration/dynamo-app-config-manager"; +import { DynamoConfigRepositoryFactory } from "@/modules/db/dynamo-config-factory"; import { SegmentEventTrackerFactory } from "@/modules/segment/segment-event-tracker-factory"; import { TrackEventUseCase } from "@/modules/tracking-events/track-event.use-case"; import { trackingEventFactory } from "@/modules/tracking-events/tracking-events"; @@ -20,33 +21,47 @@ export const config = { const logger = createLogger("orderCancelledAsyncWebhook"); +const configRepository = DynamoConfigRepositoryFactory.create(); +const configManager = DynamoAppConfigManager.create(configRepository); +const segmentEventTrackerFactory = new SegmentEventTrackerFactory(); +const useCase = new TrackEventUseCase({ segmentEventTrackerFactory }); + const handler: NextWebhookApiHandler = async ( req, res, context, ) => { - const { authData, payload } = context; + try { + const { authData, payload } = context; - if (!payload.order) { - logger.info("Payload does not contain order data. Skipping."); - return res - .status(200) - .json({ message: "Payload does not contain order data. It will be skipped by app" }); - } + const config = await configManager.get({ + saleorApiUrl: authData.saleorApiUrl, + appId: authData.appId, + }); - loggerContext.set(ObservabilityAttributes.ORDER_ID, payload.order.id); + if (!config) { + logger.warn("App config not found. Event won't be send to Segment"); - try { - const appConfigMetadataManager = AppConfigMetadataManager.createFromAuthData(authData); - const segmentEventTrackerFactory = new SegmentEventTrackerFactory({ appConfigMetadataManager }); - const useCase = new TrackEventUseCase({ segmentEventTrackerFactory }); + return res.status(200).json({ + message: "App config not found. Event won't be send to Segment", + }); + } + + if (!payload.order) { + logger.info("Payload does not contain order data. Skipping."); + return res + .status(200) + .json({ message: "Payload does not contain order data. It will be skipped by app" }); + } + + loggerContext.set(ObservabilityAttributes.ORDER_ID, payload.order.id); const event = trackingEventFactory.createOrderCancelledEvent({ orderBase: payload.order, issuedAt: payload.issuedAt, }); - return useCase.track(event).then((result) => { + return useCase.track(event, config).then((result) => { return result.match( () => { logger.info("Order cancelled event successfully sent to Segment"); diff --git a/apps/segment/src/pages/api/webhooks/order-created.ts b/apps/segment/src/pages/api/webhooks/order-created.ts index 4ea9489880..d36cabd39c 100644 --- a/apps/segment/src/pages/api/webhooks/order-created.ts +++ b/apps/segment/src/pages/api/webhooks/order-created.ts @@ -6,7 +6,8 @@ import { ObservabilityAttributes } from "@saleor/apps-otel/src/lib/observability import { OrderCreatedSubscriptionPayloadFragment } from "@/generated/graphql"; import { createLogger } from "@/logger"; import { loggerContext } from "@/logger-context"; -import { AppConfigMetadataManager } from "@/modules/configuration/app-config-metadata-manager"; +import { DynamoAppConfigManager } from "@/modules/configuration/dynamo-app-config-manager"; +import { DynamoConfigRepositoryFactory } from "@/modules/db/dynamo-config-factory"; import { SegmentEventTrackerFactory } from "@/modules/segment/segment-event-tracker-factory"; import { TrackEventUseCase } from "@/modules/tracking-events/track-event.use-case"; import { trackingEventFactory } from "@/modules/tracking-events/tracking-events"; @@ -20,34 +21,48 @@ export const config = { const logger = createLogger("orderCreatedAsyncWebhook"); +const configRepository = DynamoConfigRepositoryFactory.create(); +const configManager = DynamoAppConfigManager.create(configRepository); +const segmentEventTrackerFactory = new SegmentEventTrackerFactory(); +const useCase = new TrackEventUseCase({ segmentEventTrackerFactory }); + const handler: NextWebhookApiHandler = async ( req, res, context, ) => { - const { authData, payload } = context; + try { + const { authData, payload } = context; - if (!payload.order) { - logger.info("Payload does not contain order data. Skipping."); + const config = await configManager.get({ + saleorApiUrl: authData.saleorApiUrl, + appId: authData.appId, + }); - return res - .status(200) - .json({ message: "Payload does not contain order data. It will be skipped by app" }); - } + if (!config) { + logger.warn("App config not found. Event won't be send to Segment"); - loggerContext.set(ObservabilityAttributes.ORDER_ID, payload.order.id); + return res.status(200).json({ + message: "App config not found. Event won't be send to Segment", + }); + } - try { - const appConfigMetadataManager = AppConfigMetadataManager.createFromAuthData(authData); - const segmentEventTrackerFactory = new SegmentEventTrackerFactory({ appConfigMetadataManager }); - const useCase = new TrackEventUseCase({ segmentEventTrackerFactory }); + if (!payload.order) { + logger.info("Payload does not contain order data. Skipping."); + + return res + .status(200) + .json({ message: "Payload does not contain order data. It will be skipped by app" }); + } + + loggerContext.set(ObservabilityAttributes.ORDER_ID, payload.order.id); const event = trackingEventFactory.createOrderCreatedEvent({ orderBase: payload.order, issuedAt: payload.issuedAt, }); - return useCase.track(event).then((result) => { + return useCase.track(event, config).then((result) => { return result.match( () => { logger.info("Order created event successfully sent to Segment"); diff --git a/apps/segment/src/pages/api/webhooks/order-fully-paid.ts b/apps/segment/src/pages/api/webhooks/order-fully-paid.ts index 78008b3b6e..63d7a48628 100644 --- a/apps/segment/src/pages/api/webhooks/order-fully-paid.ts +++ b/apps/segment/src/pages/api/webhooks/order-fully-paid.ts @@ -6,7 +6,8 @@ import { ObservabilityAttributes } from "@saleor/apps-otel/src/lib/observability import { OrderFullyPaidSubscriptionPayloadFragment } from "@/generated/graphql"; import { createLogger } from "@/logger"; import { loggerContext } from "@/logger-context"; -import { AppConfigMetadataManager } from "@/modules/configuration/app-config-metadata-manager"; +import { DynamoAppConfigManager } from "@/modules/configuration/dynamo-app-config-manager"; +import { DynamoConfigRepositoryFactory } from "@/modules/db/dynamo-config-factory"; import { SegmentEventTrackerFactory } from "@/modules/segment/segment-event-tracker-factory"; import { TrackEventUseCase } from "@/modules/tracking-events/track-event.use-case"; import { trackingEventFactory } from "@/modules/tracking-events/tracking-events"; @@ -20,34 +21,48 @@ export const config = { const logger = createLogger("orderFullyPaidAsyncWebhook"); +const configRepository = DynamoConfigRepositoryFactory.create(); +const configManager = DynamoAppConfigManager.create(configRepository); +const segmentEventTrackerFactory = new SegmentEventTrackerFactory(); +const useCase = new TrackEventUseCase({ segmentEventTrackerFactory }); + const handler: NextWebhookApiHandler = async ( req, res, context, ) => { - const { authData, payload } = context; + try { + const { authData, payload } = context; - if (!payload.order) { - logger.info("Payload does not contain order data. Skipping."); + const config = await configManager.get({ + saleorApiUrl: authData.saleorApiUrl, + appId: authData.appId, + }); - return res - .status(200) - .json({ message: "Payload does not contain order data. It will be skipped by app" }); - } + if (!config) { + logger.warn("App config not found. Event won't be send to Segment"); - loggerContext.set(ObservabilityAttributes.ORDER_ID, payload.order.id); + return res.status(200).json({ + message: "App config not found. Event won't be send to Segment", + }); + } - try { - const appConfigMetadataManager = AppConfigMetadataManager.createFromAuthData(authData); - const segmentEventTrackerFactory = new SegmentEventTrackerFactory({ appConfigMetadataManager }); - const useCase = new TrackEventUseCase({ segmentEventTrackerFactory }); + if (!payload.order) { + logger.info("Payload does not contain order data. Skipping."); + + return res + .status(200) + .json({ message: "Payload does not contain order data. It will be skipped by app" }); + } + + loggerContext.set(ObservabilityAttributes.ORDER_ID, payload.order.id); const event = trackingEventFactory.createOrderCompletedEvent({ orderBase: payload.order, issuedAt: payload.issuedAt, }); - return useCase.track(event).then((result) => { + return useCase.track(event, config).then((result) => { return result.match( () => { logger.info("Order fully paid event successfully sent to Segment"); diff --git a/apps/segment/src/pages/api/webhooks/order-refunded.ts b/apps/segment/src/pages/api/webhooks/order-refunded.ts index 9953b7daa3..30cd6e830e 100644 --- a/apps/segment/src/pages/api/webhooks/order-refunded.ts +++ b/apps/segment/src/pages/api/webhooks/order-refunded.ts @@ -6,7 +6,8 @@ import { ObservabilityAttributes } from "@saleor/apps-otel/src/lib/observability import { OrderRefundedSubscriptionPayloadFragment } from "@/generated/graphql"; import { createLogger } from "@/logger"; import { loggerContext } from "@/logger-context"; -import { AppConfigMetadataManager } from "@/modules/configuration/app-config-metadata-manager"; +import { DynamoAppConfigManager } from "@/modules/configuration/dynamo-app-config-manager"; +import { DynamoConfigRepositoryFactory } from "@/modules/db/dynamo-config-factory"; import { SegmentEventTrackerFactory } from "@/modules/segment/segment-event-tracker-factory"; import { TrackEventUseCase } from "@/modules/tracking-events/track-event.use-case"; import { trackingEventFactory } from "@/modules/tracking-events/tracking-events"; @@ -20,34 +21,48 @@ export const config = { const logger = createLogger("orderRefundedAsyncWebhook"); +const configRepository = DynamoConfigRepositoryFactory.create(); +const configManager = DynamoAppConfigManager.create(configRepository); +const segmentEventTrackerFactory = new SegmentEventTrackerFactory(); +const useCase = new TrackEventUseCase({ segmentEventTrackerFactory }); + const handler: NextWebhookApiHandler = async ( req, res, context, ) => { - const { authData, payload } = context; + try { + const { authData, payload } = context; - if (!payload.order) { - logger.info("Payload does not contain order data. Skipping."); + const config = await configManager.get({ + saleorApiUrl: authData.saleorApiUrl, + appId: authData.appId, + }); - return res - .status(200) - .json({ message: "Payload does not contain order data. It will be skipped by app" }); - } + if (!config) { + logger.warn("App config not found. Event won't be send to Segment"); - loggerContext.set(ObservabilityAttributes.ORDER_ID, payload.order.id); + return res.status(200).json({ + message: "App config not found. Event won't be send to Segment", + }); + } - try { - const appConfigMetadataManager = AppConfigMetadataManager.createFromAuthData(authData); - const segmentEventTrackerFactory = new SegmentEventTrackerFactory({ appConfigMetadataManager }); - const useCase = new TrackEventUseCase({ segmentEventTrackerFactory }); + if (!payload.order) { + logger.info("Payload does not contain order data. Skipping."); + + return res + .status(200) + .json({ message: "Payload does not contain order data. It will be skipped by app" }); + } + + loggerContext.set(ObservabilityAttributes.ORDER_ID, payload.order.id); const event = trackingEventFactory.createOrderRefundedEvent({ orderBase: payload.order, issuedAt: payload.issuedAt, }); - return useCase.track(event).then((result) => { + return useCase.track(event, config).then((result) => { return result.match( () => { logger.info("Order refunded event successfully sent to Segment"); diff --git a/apps/segment/src/pages/api/webhooks/order-updated.ts b/apps/segment/src/pages/api/webhooks/order-updated.ts index bd2c1a2af4..6147b6d346 100644 --- a/apps/segment/src/pages/api/webhooks/order-updated.ts +++ b/apps/segment/src/pages/api/webhooks/order-updated.ts @@ -6,7 +6,8 @@ import { ObservabilityAttributes } from "@saleor/apps-otel/src/lib/observability import { OrderUpdatedSubscriptionPayloadFragment } from "@/generated/graphql"; import { createLogger } from "@/logger"; import { loggerContext } from "@/logger-context"; -import { AppConfigMetadataManager } from "@/modules/configuration/app-config-metadata-manager"; +import { DynamoAppConfigManager } from "@/modules/configuration/dynamo-app-config-manager"; +import { DynamoConfigRepositoryFactory } from "@/modules/db/dynamo-config-factory"; import { SegmentEventTrackerFactory } from "@/modules/segment/segment-event-tracker-factory"; import { TrackEventUseCase } from "@/modules/tracking-events/track-event.use-case"; import { trackingEventFactory } from "@/modules/tracking-events/tracking-events"; @@ -20,34 +21,48 @@ export const config = { const logger = createLogger("orderUpdatedAsyncWebhook"); +const configRepository = DynamoConfigRepositoryFactory.create(); +const configManager = DynamoAppConfigManager.create(configRepository); +const segmentEventTrackerFactory = new SegmentEventTrackerFactory(); +const useCase = new TrackEventUseCase({ segmentEventTrackerFactory }); + const handler: NextWebhookApiHandler = async ( req, res, context, ) => { - const { authData, payload } = context; + try { + const { authData, payload } = context; - if (!payload.order) { - logger.info("Payload does not contain order data. Skipping."); + const config = await configManager.get({ + saleorApiUrl: authData.saleorApiUrl, + appId: authData.appId, + }); - return res - .status(200) - .json({ message: "Payload does not contain order data. It will be skipped by app" }); - } + if (!config) { + logger.warn("App config not found. Event won't be send to Segment"); - loggerContext.set(ObservabilityAttributes.ORDER_ID, payload.order.id); + return res.status(200).json({ + message: "App config not found. Event won't be send to Segment", + }); + } - try { - const appConfigMetadataManager = AppConfigMetadataManager.createFromAuthData(authData); - const segmentEventTrackerFactory = new SegmentEventTrackerFactory({ appConfigMetadataManager }); - const useCase = new TrackEventUseCase({ segmentEventTrackerFactory }); + if (!payload.order) { + logger.info("Payload does not contain order data. Skipping."); + + return res + .status(200) + .json({ message: "Payload does not contain order data. It will be skipped by app" }); + } + + loggerContext.set(ObservabilityAttributes.ORDER_ID, payload.order.id); const event = trackingEventFactory.createOrderUpdatedEvent({ orderBase: payload.order, issuedAt: payload.issuedAt, }); - return useCase.track(event).then((result) => { + return useCase.track(event, config).then((result) => { return result.match( () => { logger.info("Order updated event successfully sent to Segment"); diff --git a/apps/segment/src/setup-tests.ts b/apps/segment/src/setup-tests.ts index d788c3b294..e2e181957b 100644 --- a/apps/segment/src/setup-tests.ts +++ b/apps/segment/src/setup-tests.ts @@ -1,5 +1,9 @@ import { vi } from "vitest"; vi.stubEnv("SECRET_KEY", "test_secret_key"); +vi.stubEnv("DYNAMODB_MAIN_TABLE_NAME", "test-table"); +vi.stubEnv("AWS_REGION", "test"); +vi.stubEnv("AWS_ACCESS_KEY_ID", "test-id"); +vi.stubEnv("AWS_SECRET_ACCESS_KEY", "test-key"); export {}; From 820f5b90000b32587fcf8c417ca79d2b41e97cff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:52:27 +0100 Subject: [PATCH 08/17] Cleanup Segment app (#1699) --- .changeset/fair-moles-try.md | 5 ++ apps/segment/src/lib/dyanmodb-apl.test.ts | 30 ++++++------ .../webhooks-status/webhooks-status.tsx | 6 +-- .../db/__tests__/memory-apl-repository.ts} | 2 +- ...ent-apl-mapper.ts => dynamo-apl-mapper.ts} | 4 +- .../db/dynamo-apl-repository-factory.ts | 18 ++++++++ ....test.ts => dynamo-apl-repository.test.ts} | 46 ++++++++----------- ...repository.ts => dynamo-apl-repository.ts} | 42 +++++++++-------- .../db/dynamo-config-repository.test.ts | 12 +---- .../db/segment-apl-repository-factory.ts | 21 --------- apps/segment/src/saleor-app.ts | 4 +- 11 files changed, 89 insertions(+), 101 deletions(-) create mode 100644 .changeset/fair-moles-try.md rename apps/segment/src/{lib/__tests__/in-memory-apl-repository.ts => modules/db/__tests__/memory-apl-repository.ts} (95%) rename apps/segment/src/modules/db/{segment-apl-mapper.ts => dynamo-apl-mapper.ts} (87%) create mode 100644 apps/segment/src/modules/db/dynamo-apl-repository-factory.ts rename apps/segment/src/modules/db/{segment-apl-repository.test.ts => dynamo-apl-repository.test.ts} (74%) rename apps/segment/src/modules/db/{segment-apl-repository.ts => dynamo-apl-repository.ts} (75%) delete mode 100644 apps/segment/src/modules/db/segment-apl-repository-factory.ts diff --git a/.changeset/fair-moles-try.md b/.changeset/fair-moles-try.md new file mode 100644 index 0000000000..4c41408c43 --- /dev/null +++ b/.changeset/fair-moles-try.md @@ -0,0 +1,5 @@ +--- +"segment": patch +--- + +Cleanup Segment app - rename of files or fix naming. diff --git a/apps/segment/src/lib/dyanmodb-apl.test.ts b/apps/segment/src/lib/dyanmodb-apl.test.ts index 7ee9c57963..29b2eb9134 100644 --- a/apps/segment/src/lib/dyanmodb-apl.test.ts +++ b/apps/segment/src/lib/dyanmodb-apl.test.ts @@ -4,7 +4,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { BaseError } from "@/errors"; -import { InMemoryAPLRepository } from "./__tests__/in-memory-apl-repository"; +import { MemoryAPLRepository } from "../modules/db/__tests__/memory-apl-repository"; import { DynamoAPL } from "./dynamodb-apl"; describe("DynamoAPL", () => { @@ -20,7 +20,7 @@ describe("DynamoAPL", () => { }); it("should get auth data if it exists", async () => { - const repository = new InMemoryAPLRepository(); + const repository = new MemoryAPLRepository(); const apl = new DynamoAPL({ repository }); repository.setEntry({ @@ -33,7 +33,7 @@ describe("DynamoAPL", () => { }); it("should return undefined if auth data does not exist", async () => { - const repository = new InMemoryAPLRepository(); + const repository = new MemoryAPLRepository(); const apl = new DynamoAPL({ repository }); const result = await apl.get("saleorApiUrl"); @@ -42,7 +42,7 @@ describe("DynamoAPL", () => { }); it("should throw an error if getting auth data fails", async () => { - const repository = new InMemoryAPLRepository(); + const repository = new MemoryAPLRepository(); const apl = new DynamoAPL({ repository }); vi.spyOn(repository, "getEntry").mockReturnValue( @@ -53,7 +53,7 @@ describe("DynamoAPL", () => { }); it("should set auth data", async () => { - const repository = new InMemoryAPLRepository(); + const repository = new MemoryAPLRepository(); const apl = new DynamoAPL({ repository }); const result = await apl.set(mockedAuthData); @@ -68,7 +68,7 @@ describe("DynamoAPL", () => { }); it("should throw an error if setting auth data fails", async () => { - const repository = new InMemoryAPLRepository(); + const repository = new MemoryAPLRepository(); vi.spyOn(repository, "setEntry").mockResolvedValue(err(new BaseError("Error setting data"))); @@ -78,7 +78,7 @@ describe("DynamoAPL", () => { }); it("should update existing auth data", async () => { - const repository = new InMemoryAPLRepository(); + const repository = new MemoryAPLRepository(); const apl = new DynamoAPL({ repository }); repository.setEntry({ @@ -103,7 +103,7 @@ describe("DynamoAPL", () => { }); it("should delete auth data", async () => { - const repository = new InMemoryAPLRepository(); + const repository = new MemoryAPLRepository(); const apl = new DynamoAPL({ repository }); repository.setEntry({ @@ -118,14 +118,14 @@ describe("DynamoAPL", () => { }); it("should throw an error if deleting auth data fails", async () => { - const repository = new InMemoryAPLRepository(); + const repository = new MemoryAPLRepository(); const apl = new DynamoAPL({ repository }); await expect(apl.delete("saleorApiUrl")).rejects.toThrowError(DynamoAPL.DeleteAuthDataError); }); it("should get all auth data", async () => { - const repository = new InMemoryAPLRepository(); + const repository = new MemoryAPLRepository(); const secondEntry: AuthData = { saleorApiUrl: "saleorApiUrl2", token: "appToken2", @@ -148,7 +148,7 @@ describe("DynamoAPL", () => { }); it("should throw an error if getting all auth data fails", async () => { - const repository = new InMemoryAPLRepository(); + const repository = new MemoryAPLRepository(); const apl = new DynamoAPL({ repository }); vi.spyOn(repository, "getAllEntries").mockResolvedValue( @@ -159,7 +159,7 @@ describe("DynamoAPL", () => { }); it("should return ready:true when APL related env variables are set", async () => { - const repository = new InMemoryAPLRepository(); + const repository = new MemoryAPLRepository(); const apl = new DynamoAPL({ repository }); const result = await apl.isReady(); @@ -183,7 +183,7 @@ describe("DynamoAPL", () => { NODE_ENV: "test", ENV: "local", }); - const repository = new InMemoryAPLRepository(); + const repository = new MemoryAPLRepository(); const apl = new DynamoAPL({ repository }); const result = await apl.isReady(); @@ -195,7 +195,7 @@ describe("DynamoAPL", () => { }); it("should return configured:true when APL related env variables are set", async () => { - const repository = new InMemoryAPLRepository(); + const repository = new MemoryAPLRepository(); const apl = new DynamoAPL({ repository }); const result = await apl.isConfigured(); @@ -217,7 +217,7 @@ describe("DynamoAPL", () => { ENV: "local", }); - const repository = new InMemoryAPLRepository(); + const repository = new MemoryAPLRepository(); const apl = new DynamoAPL({ repository }); const result = await apl.isConfigured(); diff --git a/apps/segment/src/modules/configuration/webhooks-status/webhooks-status.tsx b/apps/segment/src/modules/configuration/webhooks-status/webhooks-status.tsx index 3cd4f5a8b8..feccbdae1e 100644 --- a/apps/segment/src/modules/configuration/webhooks-status/webhooks-status.tsx +++ b/apps/segment/src/modules/configuration/webhooks-status/webhooks-status.tsx @@ -16,8 +16,8 @@ export const WebhookStatus = () => { {config.areWebhooksActive ? ( <> - Webhooks are active. For more information about webhooks check Manage app button in - header above. + App webhooks are active. For more information about webhooks check Manage app button + in header above. ACTIVE @@ -26,7 +26,7 @@ export const WebhookStatus = () => { ) : ( <> - Your webhooks were disabled. Most likely, your configuration is invalid. Check your + App webhooks were disabled. Most likely, your configuration is invalid. Check your credentials. diff --git a/apps/segment/src/lib/__tests__/in-memory-apl-repository.ts b/apps/segment/src/modules/db/__tests__/memory-apl-repository.ts similarity index 95% rename from apps/segment/src/lib/__tests__/in-memory-apl-repository.ts rename to apps/segment/src/modules/db/__tests__/memory-apl-repository.ts index a6dff6bd9a..9c0ee7442f 100644 --- a/apps/segment/src/lib/__tests__/in-memory-apl-repository.ts +++ b/apps/segment/src/modules/db/__tests__/memory-apl-repository.ts @@ -4,7 +4,7 @@ import { err, ok, Result } from "neverthrow"; import { BaseError } from "@/errors"; import { APLRepository } from "@/modules/db/types"; -export class InMemoryAPLRepository implements APLRepository { +export class MemoryAPLRepository implements APLRepository { public entries: Record = {}; async getEntry(args: { diff --git a/apps/segment/src/modules/db/segment-apl-mapper.ts b/apps/segment/src/modules/db/dynamo-apl-mapper.ts similarity index 87% rename from apps/segment/src/modules/db/segment-apl-mapper.ts rename to apps/segment/src/modules/db/dynamo-apl-mapper.ts index 9e613aa8a4..6882067efa 100644 --- a/apps/segment/src/modules/db/segment-apl-mapper.ts +++ b/apps/segment/src/modules/db/dynamo-apl-mapper.ts @@ -3,8 +3,8 @@ import { FormattedItem, type PutItemInput } from "dynamodb-toolbox"; import { SegmentAPLEntityType, SegmentMainTable } from "@/modules/db/segment-main-table"; -export class SegmentAPLMapper { - dynamoDBEntityToAuthData(entity: FormattedItem): AuthData { +export class DynamoAPLMapper { + dynamoEntityToAuthData(entity: FormattedItem): AuthData { return { domain: entity.domain, token: entity.token, diff --git a/apps/segment/src/modules/db/dynamo-apl-repository-factory.ts b/apps/segment/src/modules/db/dynamo-apl-repository-factory.ts new file mode 100644 index 0000000000..992737ea96 --- /dev/null +++ b/apps/segment/src/modules/db/dynamo-apl-repository-factory.ts @@ -0,0 +1,18 @@ +import { BaseError } from "@/errors"; + +import { DynamoAPLRepository } from "./dynamo-apl-repository"; + +export class DynamoAPLRepositoryFactory { + static RepositoryCreationError = BaseError.subclass("RepositoryCreationError"); + + static create(): DynamoAPLRepository { + try { + return new DynamoAPLRepository(); + } catch (error) { + throw new DynamoAPLRepositoryFactory.RepositoryCreationError( + "Failed to create DynamoDB APL repository", + { cause: error }, + ); + } + } +} diff --git a/apps/segment/src/modules/db/segment-apl-repository.test.ts b/apps/segment/src/modules/db/dynamo-apl-repository.test.ts similarity index 74% rename from apps/segment/src/modules/db/segment-apl-repository.test.ts rename to apps/segment/src/modules/db/dynamo-apl-repository.test.ts index 335d6f77c4..10b322fbce 100644 --- a/apps/segment/src/modules/db/segment-apl-repository.test.ts +++ b/apps/segment/src/modules/db/dynamo-apl-repository.test.ts @@ -10,20 +10,12 @@ import { mockClient } from "aws-sdk-client-mock"; import { SavedItem } from "dynamodb-toolbox"; import { beforeEach, describe, expect, it } from "vitest"; -import { SegmentAPLRepository } from "./segment-apl-repository"; -import { SegmentMainTable, SegmentMainTableEntityFactory } from "./segment-main-table"; +import { DynamoAPLRepository } from "./dynamo-apl-repository"; +import { SegmentAPLEntityType } from "./segment-main-table"; -describe("SegmentAPLRepository", () => { +describe("DynamoAPLRepository", () => { const mockDocumentClient = mockClient(DynamoDBDocumentClient); - const segmentMainTable = SegmentMainTable.create({ - // @ts-expect-error https://github.com/m-radzikowski/aws-sdk-client-mock/issues/197 - documentClient: mockDocumentClient, - tableName: "segment-test-table", - }); - - const segmentAPLEntity = SegmentMainTableEntityFactory.createAPLEntity(segmentMainTable); - const mockedAuthData: AuthData = { appId: "appId", saleorApiUrl: "saleorApiUrl", @@ -35,7 +27,7 @@ describe("SegmentAPLRepository", () => { }); it("should successfully get AuthData entry from DynamoDB", async () => { - const mockedAPLEntry: SavedItem = { + const mockedAPLEntry: SavedItem = { PK: "saleorApiUrl", SK: "APL", token: "appToken", @@ -50,7 +42,7 @@ describe("SegmentAPLRepository", () => { Item: mockedAPLEntry, }); - const repository = new SegmentAPLRepository({ segmentAPLEntity }); + const repository = new DynamoAPLRepository(); const result = await repository.getEntry({ saleorApiUrl: "saleorApiUrl" }); @@ -68,19 +60,19 @@ describe("SegmentAPLRepository", () => { it("should handle errors when getting AuthData from DynamoDB", async () => { mockDocumentClient.on(GetCommand, {}).rejectsOnce("Exception"); - const repository = new SegmentAPLRepository({ segmentAPLEntity }); + const repository = new DynamoAPLRepository(); const result = await repository.getEntry({ saleorApiUrl: "saleorApiUrl" }); expect(result.isErr()).toBe(true); - expect(result._unsafeUnwrapErr()).toBeInstanceOf(SegmentAPLRepository.ReadEntityError); + expect(result._unsafeUnwrapErr()).toBeInstanceOf(DynamoAPLRepository.ReadEntityError); }); it("should return null if AuthData entry does not exist in DynamoDB", async () => { mockDocumentClient.on(GetCommand, {}).resolvesOnce({}); - const repository = new SegmentAPLRepository({ segmentAPLEntity }); + const repository = new DynamoAPLRepository(); const result = await repository.getEntry({ saleorApiUrl: "saleorApiUrl" }); @@ -92,7 +84,7 @@ describe("SegmentAPLRepository", () => { it("should successfully set AuthData entry in DynamoDB", async () => { mockDocumentClient.on(PutCommand, {}).resolvesOnce({}); - const repository = new SegmentAPLRepository({ segmentAPLEntity }); + const repository = new DynamoAPLRepository(); const result = await repository.setEntry({ authData: mockedAuthData, @@ -106,7 +98,7 @@ describe("SegmentAPLRepository", () => { it("should handle errors when setting AuthData entry DynamoDB", async () => { mockDocumentClient.on(PutCommand, {}).rejectsOnce("Exception"); - const repository = new SegmentAPLRepository({ segmentAPLEntity }); + const repository = new DynamoAPLRepository(); const result = await repository.setEntry({ authData: mockedAuthData, @@ -114,13 +106,13 @@ describe("SegmentAPLRepository", () => { expect(result.isErr()).toBe(true); - expect(result._unsafeUnwrapErr()).toBeInstanceOf(SegmentAPLRepository.WriteEntityError); + expect(result._unsafeUnwrapErr()).toBeInstanceOf(DynamoAPLRepository.WriteEntityError); }); it("should successfully delete AuthData entry from DynamoDB", async () => { mockDocumentClient.on(DeleteCommand, {}).resolvesOnce({}); - const repository = new SegmentAPLRepository({ segmentAPLEntity }); + const repository = new DynamoAPLRepository(); const result = await repository.deleteEntry({ saleorApiUrl: "saleorApiUrl" }); @@ -132,17 +124,17 @@ describe("SegmentAPLRepository", () => { it("should handle errors when deleting AuthData entry from DynamoDB", async () => { mockDocumentClient.on(DeleteCommand, {}).rejectsOnce("Exception"); - const repository = new SegmentAPLRepository({ segmentAPLEntity }); + const repository = new DynamoAPLRepository(); const result = await repository.deleteEntry({ saleorApiUrl: "saleorApiUrl" }); expect(result.isErr()).toBe(true); - expect(result._unsafeUnwrapErr()).toBeInstanceOf(SegmentAPLRepository.DeleteEntityError); + expect(result._unsafeUnwrapErr()).toBeInstanceOf(DynamoAPLRepository.DeleteEntityError); }); it("should successfully get all AuthData entries from DynamoDB", async () => { - const mockedAPLEntries: SavedItem[] = [ + const mockedAPLEntries: SavedItem[] = [ { PK: "saleorApiUrl", SK: "APL", @@ -169,7 +161,7 @@ describe("SegmentAPLRepository", () => { Items: mockedAPLEntries, }); - const repository = new SegmentAPLRepository({ segmentAPLEntity }); + const repository = new DynamoAPLRepository(); const result = await repository.getAllEntries(); @@ -198,7 +190,7 @@ describe("SegmentAPLRepository", () => { Items: [], }); - const repository = new SegmentAPLRepository({ segmentAPLEntity }); + const repository = new DynamoAPLRepository(); const result = await repository.getAllEntries(); @@ -210,12 +202,12 @@ describe("SegmentAPLRepository", () => { it("should handle error when getting all AuthData entries from DynamoDB", async () => { mockDocumentClient.on(ScanCommand, {}).rejectsOnce("Exception"); - const repository = new SegmentAPLRepository({ segmentAPLEntity }); + const repository = new DynamoAPLRepository(); const result = await repository.getAllEntries(); expect(result.isErr()).toBe(true); - expect(result._unsafeUnwrapErr()).toBeInstanceOf(SegmentAPLRepository.ScanEntityError); + expect(result._unsafeUnwrapErr()).toBeInstanceOf(DynamoAPLRepository.ScanEntityError); }); }); diff --git a/apps/segment/src/modules/db/segment-apl-repository.ts b/apps/segment/src/modules/db/dynamo-apl-repository.ts similarity index 75% rename from apps/segment/src/modules/db/segment-apl-repository.ts rename to apps/segment/src/modules/db/dynamo-apl-repository.ts index f4d50b595e..fd559fa0b5 100644 --- a/apps/segment/src/modules/db/segment-apl-repository.ts +++ b/apps/segment/src/modules/db/dynamo-apl-repository.ts @@ -4,30 +4,32 @@ import { err, ok, ResultAsync } from "neverthrow"; import { BaseError } from "@/errors"; import { createLogger } from "@/logger"; -import { SegmentAPLEntityType, SegmentMainTable } from "@/modules/db/segment-main-table"; +import { + SegmentMainTable, + segmentMainTable, + SegmentMainTableEntityFactory, +} from "@/modules/db/segment-main-table"; -import { SegmentAPLMapper } from "./segment-apl-mapper"; +import { DynamoAPLMapper } from "./dynamo-apl-mapper"; import { APLRepository } from "./types"; -export class SegmentAPLRepository implements APLRepository { +export class DynamoAPLRepository implements APLRepository { private logger = createLogger("SegmentAPLRepository"); - private segmentAPLMapper = new SegmentAPLMapper(); + private aplEntity = SegmentMainTableEntityFactory.createAPLEntity(segmentMainTable); + + private segmentAPLMapper = new DynamoAPLMapper(); static ReadEntityError = BaseError.subclass("ReadEntityError"); static WriteEntityError = BaseError.subclass("WriteEntityError"); static DeleteEntityError = BaseError.subclass("DeleteEntityError"); static ScanEntityError = BaseError.subclass("ScanEntityError"); - constructor( - private deps: { - segmentAPLEntity: SegmentAPLEntityType; - }, - ) {} + constructor() {} async getEntry(args: { saleorApiUrl: string }) { const getEntryResult = await ResultAsync.fromPromise( - this.deps.segmentAPLEntity + this.aplEntity .build(GetItemCommand) .key({ PK: SegmentMainTable.getAPLPrimaryKey({ @@ -37,7 +39,7 @@ export class SegmentAPLRepository implements APLRepository { }) .send(), (error) => - new SegmentAPLRepository.ReadEntityError("Failed to read APL entity", { cause: error }), + new DynamoAPLRepository.ReadEntityError("Failed to read APL entity", { cause: error }), ); if (getEntryResult.isErr()) { @@ -54,17 +56,17 @@ export class SegmentAPLRepository implements APLRepository { return ok(null); } - return ok(this.segmentAPLMapper.dynamoDBEntityToAuthData(getEntryResult.value.Item)); + return ok(this.segmentAPLMapper.dynamoEntityToAuthData(getEntryResult.value.Item)); } async setEntry(args: { authData: AuthData }) { const setEntryResult = await ResultAsync.fromPromise( - this.deps.segmentAPLEntity + this.aplEntity .build(PutItemCommand) .item(this.segmentAPLMapper.authDataToDynamoPutEntity(args.authData)) .send(), (error) => - new SegmentAPLRepository.WriteEntityError("Failed to write APL entity", { + new DynamoAPLRepository.WriteEntityError("Failed to write APL entity", { cause: error, }), ); @@ -82,7 +84,7 @@ export class SegmentAPLRepository implements APLRepository { async deleteEntry(args: { saleorApiUrl: string }) { const deleteEntryResult = await ResultAsync.fromPromise( - this.deps.segmentAPLEntity + this.aplEntity .build(DeleteItemCommand) .key({ PK: SegmentMainTable.getAPLPrimaryKey({ @@ -92,7 +94,7 @@ export class SegmentAPLRepository implements APLRepository { }) .send(), (error) => - new SegmentAPLRepository.DeleteEntityError("Failed to delete APL entity", { + new DynamoAPLRepository.DeleteEntityError("Failed to delete APL entity", { cause: error, }), ); @@ -110,16 +112,16 @@ export class SegmentAPLRepository implements APLRepository { async getAllEntries() { const scanEntriesResult = await ResultAsync.fromPromise( - this.deps.segmentAPLEntity.table + this.aplEntity.table .build(ScanCommand) - .entities(this.deps.segmentAPLEntity) + .entities(this.aplEntity) .options({ // keep all the entries in memory - we should introduce pagination in the future maxPages: Infinity, }) .send(), (error) => - new SegmentAPLRepository.ScanEntityError("Failed to scan APL entities", { + new DynamoAPLRepository.ScanEntityError("Failed to scan APL entities", { cause: error, }), ); @@ -135,7 +137,7 @@ export class SegmentAPLRepository implements APLRepository { const possibleItems = scanEntriesResult.value.Items ?? []; if (possibleItems.length > 0) { - return ok(possibleItems.map(this.segmentAPLMapper.dynamoDBEntityToAuthData)); + return ok(possibleItems.map(this.segmentAPLMapper.dynamoEntityToAuthData)); } return ok(null); diff --git a/apps/segment/src/modules/db/dynamo-config-repository.test.ts b/apps/segment/src/modules/db/dynamo-config-repository.test.ts index 300827b791..104f0aa394 100644 --- a/apps/segment/src/modules/db/dynamo-config-repository.test.ts +++ b/apps/segment/src/modules/db/dynamo-config-repository.test.ts @@ -8,25 +8,17 @@ import { env } from "@/env"; import { AppConfig } from "../configuration/app-config"; import { DynamoConfigRepository } from "./dynamo-config-repository"; -import { SegmentMainTable, SegmentMainTableEntityFactory } from "./segment-main-table"; +import { SegmentConfigEntityType } from "./segment-main-table"; describe("DynamoConfigRepository", () => { const mockDocumentClient = mockClient(DynamoDBDocumentClient); - const segmentMainTable = SegmentMainTable.create({ - // @ts-expect-error https://github.com/m-radzikowski/aws-sdk-client-mock/issues/197 - documentClient: mockDocumentClient, - tableName: "segment-test-table", - }); - - const segmentConfigEntity = SegmentMainTableEntityFactory.createConfigEntity(segmentMainTable); - beforeEach(() => { mockDocumentClient.reset(); }); it("should successfully get AppConfig from DynamoDB", async () => { - const mockedConfigEntry: SavedItem = { + const mockedConfigEntry: SavedItem = { PK: "saleorApiUrl#saleorAppId", SK: "APP_CONFIG#configKey", encryptedSegmentWriteKey: encrypt("encryptedKey", env.SECRET_KEY), diff --git a/apps/segment/src/modules/db/segment-apl-repository-factory.ts b/apps/segment/src/modules/db/segment-apl-repository-factory.ts deleted file mode 100644 index b07eb2a96d..0000000000 --- a/apps/segment/src/modules/db/segment-apl-repository-factory.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { BaseError } from "@/errors"; - -import { SegmentAPLRepository } from "./segment-apl-repository"; -import { segmentMainTable, SegmentMainTableEntityFactory } from "./segment-main-table"; - -export class SegmentAPLRepositoryFactory { - static RepositoryCreationError = BaseError.subclass("RepositoryCreationError"); - - static create(): SegmentAPLRepository { - try { - const segmentAPLEntity = SegmentMainTableEntityFactory.createAPLEntity(segmentMainTable); - - return new SegmentAPLRepository({ segmentAPLEntity }); - } catch (error) { - throw new SegmentAPLRepositoryFactory.RepositoryCreationError( - "Failed to create DynamoDB APL repository", - { cause: error }, - ); - } - } -} diff --git a/apps/segment/src/saleor-app.ts b/apps/segment/src/saleor-app.ts index 9eb9303230..c67b5764c3 100644 --- a/apps/segment/src/saleor-app.ts +++ b/apps/segment/src/saleor-app.ts @@ -4,7 +4,7 @@ import { SaleorApp } from "@saleor/app-sdk/saleor-app"; import { env } from "./env"; import { BaseError } from "./errors"; import { DynamoAPL } from "./lib/dynamodb-apl"; -import { SegmentAPLRepositoryFactory } from "./modules/db/segment-apl-repository-factory"; +import { DynamoAPLRepositoryFactory } from "./modules/db/dynamo-apl-repository-factory"; export let apl: APL; @@ -12,7 +12,7 @@ const MisconfiguredSaleorCloudAPLError = BaseError.subclass("MisconfiguredSaleor switch (env.APL) { case "dynamodb": { - const repository = SegmentAPLRepositoryFactory.create(); + const repository = DynamoAPLRepositoryFactory.create(); apl = new DynamoAPL({ repository }); break; From 0ab520d7f4db7c469f9e3e78bab52e9e1156fbaf Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Tue, 21 Jan 2025 13:11:37 +0100 Subject: [PATCH 09/17] Release apps (#1697) Co-authored-by: github-actions[bot] --- .changeset/breezy-buses-greet.md | 5 ----- .changeset/fair-moles-try.md | 5 ----- .changeset/lemon-news-give.md | 5 ----- .changeset/sour-hotels-nail.md | 5 ----- apps/products-feed/CHANGELOG.md | 6 ++++++ apps/products-feed/package.json | 2 +- apps/segment/CHANGELOG.md | 8 ++++++++ apps/segment/package.json | 2 +- 8 files changed, 16 insertions(+), 22 deletions(-) delete mode 100644 .changeset/breezy-buses-greet.md delete mode 100644 .changeset/fair-moles-try.md delete mode 100644 .changeset/lemon-news-give.md delete mode 100644 .changeset/sour-hotels-nail.md diff --git a/.changeset/breezy-buses-greet.md b/.changeset/breezy-buses-greet.md deleted file mode 100644 index 8549331ac5..0000000000 --- a/.changeset/breezy-buses-greet.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"segment": patch ---- - -Added DynamoDB APL. This APL is using DynamoDB as storage. diff --git a/.changeset/fair-moles-try.md b/.changeset/fair-moles-try.md deleted file mode 100644 index 4c41408c43..0000000000 --- a/.changeset/fair-moles-try.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"segment": patch ---- - -Cleanup Segment app - rename of files or fix naming. diff --git a/.changeset/lemon-news-give.md b/.changeset/lemon-news-give.md deleted file mode 100644 index def19b0275..0000000000 --- a/.changeset/lemon-news-give.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"products-feed": minor ---- - -Product feed: prioritize media assigned to a specific product variant. with a fallback mechanism to use product media when no variant-specific media is available. The changes aim to enhance the precision of media selection diff --git a/.changeset/sour-hotels-nail.md b/.changeset/sour-hotels-nail.md deleted file mode 100644 index cb6cbcaf01..0000000000 --- a/.changeset/sour-hotels-nail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"segment": patch ---- - -Store app config in DynamoDB instead of Saleor app metadata. diff --git a/apps/products-feed/CHANGELOG.md b/apps/products-feed/CHANGELOG.md index b75c9f186d..b9e12ba235 100644 --- a/apps/products-feed/CHANGELOG.md +++ b/apps/products-feed/CHANGELOG.md @@ -1,5 +1,11 @@ # saleor-app-products-feed +## 1.20.0 + +### Minor Changes + +- e86926f2: Product feed: prioritize media assigned to a specific product variant. with a fallback mechanism to use product media when no variant-specific media is available. The changes aim to enhance the precision of media selection + ## 1.19.18 ### Patch Changes diff --git a/apps/products-feed/package.json b/apps/products-feed/package.json index 5f8168f0ab..6057f2b3fe 100644 --- a/apps/products-feed/package.json +++ b/apps/products-feed/package.json @@ -1,6 +1,6 @@ { "name": "products-feed", - "version": "1.19.18", + "version": "1.20.0", "scripts": { "build": "next build", "check-types": "tsc --noEmit", diff --git a/apps/segment/CHANGELOG.md b/apps/segment/CHANGELOG.md index 8263903b30..796e5621f4 100644 --- a/apps/segment/CHANGELOG.md +++ b/apps/segment/CHANGELOG.md @@ -1,5 +1,13 @@ # segment +## 2.0.3 + +### Patch Changes + +- b61ce914: Added DynamoDB APL. This APL is using DynamoDB as storage. +- 820f5b90: Cleanup Segment app - rename of files or fix naming. +- 36aea8d9: Store app config in DynamoDB instead of Saleor app metadata. + ## 2.0.2 ### Patch Changes diff --git a/apps/segment/package.json b/apps/segment/package.json index d6133f4a23..a056104330 100644 --- a/apps/segment/package.json +++ b/apps/segment/package.json @@ -1,6 +1,6 @@ { "name": "segment", - "version": "2.0.2", + "version": "2.0.3", "scripts": { "build": "next build", "check-types": "tsc --noEmit", From 0f0bff218509454fe848c5de192743ee0a57922b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:14:18 +0100 Subject: [PATCH 10/17] Move `ThemeSynchronizer` to shared pkgs (#1701) --- .changeset/gorgeous-berries-dress.md | 9 +++++++ apps/avatax/src/pages/_app.tsx | 3 +-- .../src/modules/theme/theme-synchronizer.tsx | 24 ------------------ apps/cms-v2/src/pages/_app.tsx | 6 ++--- .../src/lib/theme-synchronizer.tsx | 25 ------------------- apps/products-feed/src/pages/_app.tsx | 4 +-- apps/segment/src/pages/_app.tsx | 3 +-- apps/smtp/src/lib/theme-synchronizer.tsx | 24 ------------------ apps/smtp/src/pages/_app.tsx | 4 +-- 9 files changed, 15 insertions(+), 87 deletions(-) create mode 100644 .changeset/gorgeous-berries-dress.md delete mode 100644 apps/cms-v2/src/modules/theme/theme-synchronizer.tsx delete mode 100644 apps/products-feed/src/lib/theme-synchronizer.tsx delete mode 100644 apps/smtp/src/lib/theme-synchronizer.tsx diff --git a/.changeset/gorgeous-berries-dress.md b/.changeset/gorgeous-berries-dress.md new file mode 100644 index 0000000000..dbe6233895 --- /dev/null +++ b/.changeset/gorgeous-berries-dress.md @@ -0,0 +1,9 @@ +--- +"products-feed": patch +"segment": patch +"app-avatax": patch +"cms-v2": patch +"smtp": patch +--- + +Move `ThemeSynchronizer` utility to shared packages. diff --git a/apps/avatax/src/pages/_app.tsx b/apps/avatax/src/pages/_app.tsx index 3f7894cf23..573fa15148 100644 --- a/apps/avatax/src/pages/_app.tsx +++ b/apps/avatax/src/pages/_app.tsx @@ -3,11 +3,10 @@ import "@saleor/macaw-ui/style"; import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge"; import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next"; -import { NoSSRWrapper } from "@saleor/apps-shared"; +import { NoSSRWrapper, ThemeSynchronizer } from "@saleor/apps-shared"; import { ThemeProvider } from "@saleor/macaw-ui"; import { AppProps } from "next/app"; -import { ThemeSynchronizer } from "../lib/theme-synchronizer"; import { trpcClient } from "../modules/trpc/trpc-client"; import { AppLayout } from "../modules/ui/app-layout"; import { GraphQLProvider } from "../providers/GraphQLProvider"; diff --git a/apps/cms-v2/src/modules/theme/theme-synchronizer.tsx b/apps/cms-v2/src/modules/theme/theme-synchronizer.tsx deleted file mode 100644 index 339440cb38..0000000000 --- a/apps/cms-v2/src/modules/theme/theme-synchronizer.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { useAppBridge } from "@saleor/app-sdk/app-bridge"; -import { useTheme } from "@saleor/macaw-ui"; -import { useEffect } from "react"; - -export function ThemeSynchronizer() { - const { appBridgeState } = useAppBridge(); - const { setTheme } = useTheme(); - - useEffect(() => { - if (!setTheme || !appBridgeState?.theme) { - return; - } - - if (appBridgeState.theme === "light") { - setTheme("defaultLight"); - } - - if (appBridgeState.theme === "dark") { - setTheme("defaultDark"); - } - }, [appBridgeState?.theme, setTheme]); - - return null; -} diff --git a/apps/cms-v2/src/pages/_app.tsx b/apps/cms-v2/src/pages/_app.tsx index 0c5407b2c5..2ca3b00000 100644 --- a/apps/cms-v2/src/pages/_app.tsx +++ b/apps/cms-v2/src/pages/_app.tsx @@ -1,16 +1,14 @@ -import "@saleor/macaw-ui/style"; import "@/modules/theme/styles.css"; +import "@saleor/macaw-ui/style"; import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge"; import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next"; -import { NoSSRWrapper } from "@saleor/apps-shared"; +import { NoSSRWrapper, ThemeSynchronizer } from "@saleor/apps-shared"; import { Box, ThemeProvider } from "@saleor/macaw-ui"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { AppProps } from "next/app"; -import React, { useEffect } from "react"; import { GraphQLProvider } from "@/modules/graphql/GraphQLProvider"; -import { ThemeSynchronizer } from "@/modules/theme/theme-synchronizer"; import { trpcClient } from "@/modules/trpc/trpc-client"; /** diff --git a/apps/products-feed/src/lib/theme-synchronizer.tsx b/apps/products-feed/src/lib/theme-synchronizer.tsx deleted file mode 100644 index 4d0b289d5e..0000000000 --- a/apps/products-feed/src/lib/theme-synchronizer.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useAppBridge } from "@saleor/app-sdk/app-bridge"; -import { useTheme } from "@saleor/macaw-ui"; -import { useEffect } from "react"; - -// todo move to shared -export function ThemeSynchronizer() { - const { appBridgeState } = useAppBridge(); - const { setTheme } = useTheme(); - - useEffect(() => { - if (!setTheme || !appBridgeState?.theme) { - return; - } - - if (appBridgeState.theme === "light") { - setTheme("defaultLight"); - } - - if (appBridgeState.theme === "dark") { - setTheme("defaultDark"); - } - }, [appBridgeState?.theme, setTheme]); - - return null; -} diff --git a/apps/products-feed/src/pages/_app.tsx b/apps/products-feed/src/pages/_app.tsx index 30796e65bd..42ab121f91 100644 --- a/apps/products-feed/src/pages/_app.tsx +++ b/apps/products-feed/src/pages/_app.tsx @@ -2,13 +2,11 @@ import "@saleor/macaw-ui/style"; import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge"; import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next"; -import { NoSSRWrapper } from "@saleor/apps-shared"; +import { NoSSRWrapper, ThemeSynchronizer } from "@saleor/apps-shared"; import { Box, ThemeProvider } from "@saleor/macaw-ui"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { AppProps } from "next/app"; -import React from "react"; -import { ThemeSynchronizer } from "../lib/theme-synchronizer"; import { trpcClient } from "../modules/trpc/trpc-client"; /** diff --git a/apps/segment/src/pages/_app.tsx b/apps/segment/src/pages/_app.tsx index 0847f0605d..86a73916f7 100644 --- a/apps/segment/src/pages/_app.tsx +++ b/apps/segment/src/pages/_app.tsx @@ -2,11 +2,10 @@ import "@saleor/macaw-ui/style"; import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge"; import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next"; -import { NoSSRWrapper } from "@saleor/apps-shared"; +import { NoSSRWrapper, ThemeSynchronizer } from "@saleor/apps-shared"; import { Box, ThemeProvider } from "@saleor/macaw-ui"; import { AppProps } from "next/app"; -import { ThemeSynchronizer } from "@/lib/theme-synchronizer"; import { trpcClient } from "@/modules/trpc/trpc-client"; import { GraphQLProvider } from "@/providers/GraphQLProvider"; diff --git a/apps/smtp/src/lib/theme-synchronizer.tsx b/apps/smtp/src/lib/theme-synchronizer.tsx deleted file mode 100644 index 339440cb38..0000000000 --- a/apps/smtp/src/lib/theme-synchronizer.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { useAppBridge } from "@saleor/app-sdk/app-bridge"; -import { useTheme } from "@saleor/macaw-ui"; -import { useEffect } from "react"; - -export function ThemeSynchronizer() { - const { appBridgeState } = useAppBridge(); - const { setTheme } = useTheme(); - - useEffect(() => { - if (!setTheme || !appBridgeState?.theme) { - return; - } - - if (appBridgeState.theme === "light") { - setTheme("defaultLight"); - } - - if (appBridgeState.theme === "dark") { - setTheme("defaultDark"); - } - }, [appBridgeState?.theme, setTheme]); - - return null; -} diff --git a/apps/smtp/src/pages/_app.tsx b/apps/smtp/src/pages/_app.tsx index 4e2c64b36f..6d2d248982 100644 --- a/apps/smtp/src/pages/_app.tsx +++ b/apps/smtp/src/pages/_app.tsx @@ -3,12 +3,10 @@ import "../styles/globals.css"; import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge"; import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next"; -import { NoSSRWrapper } from "@saleor/apps-shared"; +import { NoSSRWrapper, ThemeSynchronizer } from "@saleor/apps-shared"; import { ThemeProvider } from "@saleor/macaw-ui"; import { AppProps } from "next/app"; -import React from "react"; -import { ThemeSynchronizer } from "../lib/theme-synchronizer"; import { trpcClient } from "../modules/trpc/trpc-client"; /** From 18a9c3d991fa855eee1601c98f73c126b28fe690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:31:10 +0100 Subject: [PATCH 11/17] Cache TRPC query for logs & implement logs pagination (#1702) --- .changeset/honest-sheep-happen.md | 6 + apps/avatax/next.config.js | 34 +++++ apps/avatax/package.json | 1 + .../modules/client-logs/client-logs.router.ts | 80 ++++++---- .../client-logs/logs-repository.test.ts | 36 +++-- .../modules/client-logs/logs-repository.ts | 59 +++++-- .../src/modules/client-logs/ui/format-date.ts | 41 +++++ .../modules/client-logs/ui/logs-browser.tsx | 129 ++++++++++------ .../ui/use-client-logs-pagination.test.tsx | 54 +++++++ .../ui/use-client-logs-pagination.tsx | 37 +++++ pnpm-lock.yaml | 144 +++++------------- 11 files changed, 420 insertions(+), 201 deletions(-) create mode 100644 .changeset/honest-sheep-happen.md create mode 100644 apps/avatax/src/modules/client-logs/ui/format-date.ts create mode 100644 apps/avatax/src/modules/client-logs/ui/use-client-logs-pagination.test.tsx create mode 100644 apps/avatax/src/modules/client-logs/ui/use-client-logs-pagination.tsx diff --git a/.changeset/honest-sheep-happen.md b/.changeset/honest-sheep-happen.md new file mode 100644 index 0000000000..0fe97b510d --- /dev/null +++ b/.changeset/honest-sheep-happen.md @@ -0,0 +1,6 @@ +--- +"app-avatax": patch +--- + +Implement client logs cache. Right now app will cache request for 1 day and revalidate the cache every 60 seconds. +Added forward / backward pagination to client logs. After this change end user can browse logs that exceeds current pagination limit (first 100). diff --git a/apps/avatax/next.config.js b/apps/avatax/next.config.js index 6b8f76afa0..a4eae482d1 100644 --- a/apps/avatax/next.config.js +++ b/apps/avatax/next.config.js @@ -4,8 +4,42 @@ import withBundleAnalyzerConfig from "@next/bundle-analyzer"; import { withSentryConfig } from "@sentry/nextjs"; +// cache request for 1 day (in seconds) + revalidate once 60 seconds +const cacheValue = "private,s-maxage=60,stale-while-revalidate=86400"; + /** @type {import('next').NextConfig} */ const nextConfig = { + async headers() { + return [ + { + source: "/api/trpc/clientLogs.getByCheckoutOrOrderId", + // Keys based on https://vercel.com/docs/edge-network/headers/cache-control-headers + headers: [ + { + key: "CDN-Cache-Control", + value: cacheValue, + }, + { + key: "Cache-Control", + value: cacheValue, + }, + ], + }, + { + source: "/api/trpc/clientLogs.getByDate", + headers: [ + { + key: "CDN-Cache-Control", + value: cacheValue, + }, + { + key: "Cache-Control", + value: cacheValue, + }, + ], + }, + ]; + }, reactStrictMode: true, transpilePackages: [ "@saleor/apps-otel", diff --git a/apps/avatax/package.json b/apps/avatax/package.json index 361f7ea326..0ec934b3ec 100644 --- a/apps/avatax/package.json +++ b/apps/avatax/package.json @@ -89,6 +89,7 @@ "@graphql-codegen/typescript-urql": "4.0.0", "@graphql-typed-document-node/core": "3.2.0", "@next/bundle-analyzer": "14.1.4", + "@testing-library/react": "^14.0.0", "@total-typescript/ts-reset": "0.6.1", "@types/react": "18.2.5", "@types/react-dom": "18.2.5", diff --git a/apps/avatax/src/modules/client-logs/client-logs.router.ts b/apps/avatax/src/modules/client-logs/client-logs.router.ts index aa1bc72ed8..4838f683ce 100644 --- a/apps/avatax/src/modules/client-logs/client-logs.router.ts +++ b/apps/avatax/src/modules/client-logs/client-logs.router.ts @@ -10,7 +10,7 @@ import { createLogsDocumentClient, createLogsDynamoClient, } from "@/modules/client-logs/dynamo-client"; -import { LogsRepositoryDynamodb } from "@/modules/client-logs/logs-repository"; +import { LastEvaluatedKey, LogsRepositoryDynamodb } from "@/modules/client-logs/logs-repository"; import { protectedClientProcedure } from "@/modules/trpc/protected-client-procedure"; import { router } from "@/modules/trpc/trpc-server"; @@ -86,45 +86,65 @@ export const clientLogsRouter = router({ z.object({ startDate: z.string().datetime(), endDate: z.string().datetime(), + lastEvaluatedKey: z.record(z.unknown()).optional(), }), ) - .query(async ({ input, ctx }): Promise => { - const logsResult = await ctx.logsRepository.getLogsByDate({ - startDate: new Date(input.startDate), - endDate: new Date(input.endDate), - appId: ctx.appId, - saleorApiUrl: ctx.saleorApiUrl, - }); - - if (logsResult.isErr()) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to fetch logs", + .query( + async ({ + input, + ctx, + }): Promise<{ clientLogs: ClientLogValue[]; lastEvaluatedKey: LastEvaluatedKey }> => { + const logsResult = await ctx.logsRepository.getLogsByDate({ + startDate: new Date(input.startDate), + endDate: new Date(input.endDate), + appId: ctx.appId, + saleorApiUrl: ctx.saleorApiUrl, + lastEvaluatedKey: input.lastEvaluatedKey, }); - } - return logsResult.value.map((l) => l.getValue()); - }), + if (logsResult.isErr()) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to fetch logs", + }); + } + + return { + clientLogs: logsResult.value.clientLogs.map((l) => l.getValue()), + lastEvaluatedKey: logsResult.value.lastEvaluatedKey, + }; + }, + ), getByCheckoutOrOrderId: procedureWithFlag .input( z.object({ checkoutOrOrderId: z.string(), + lastEvaluatedKey: z.record(z.unknown()).optional(), }), ) - .query(async ({ input, ctx }): Promise => { - const logsResult = await ctx.logsRepository.getLogsByCheckoutOrOrderId({ - checkoutOrOrderId: input.checkoutOrOrderId, - appId: ctx.appId, - saleorApiUrl: ctx.saleorApiUrl, - }); - - if (logsResult.isErr()) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Failed to fetch logs", + .query( + async ({ + input, + ctx, + }): Promise<{ clientLogs: ClientLogValue[]; lastEvaluatedKey: LastEvaluatedKey }> => { + const logsResult = await ctx.logsRepository.getLogsByCheckoutOrOrderId({ + checkoutOrOrderId: input.checkoutOrOrderId, + appId: ctx.appId, + saleorApiUrl: ctx.saleorApiUrl, + lastEvaluatedKey: input.lastEvaluatedKey, }); - } - return logsResult.value.map((l) => l.getValue()); - }), + if (logsResult.isErr()) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Failed to fetch logs", + }); + } + + return { + clientLogs: logsResult.value.clientLogs.map((l) => l.getValue()), + lastEvaluatedKey: logsResult.value.lastEvaluatedKey, + }; + }, + ), }); diff --git a/apps/avatax/src/modules/client-logs/logs-repository.test.ts b/apps/avatax/src/modules/client-logs/logs-repository.test.ts index 22c6654cb0..a5e7f7aa90 100644 --- a/apps/avatax/src/modules/client-logs/logs-repository.test.ts +++ b/apps/avatax/src/modules/client-logs/logs-repository.test.ts @@ -1,7 +1,7 @@ import { BatchWriteCommand, DynamoDBDocumentClient, QueryCommand } from "@aws-sdk/lib-dynamodb"; import { mockClient } from "aws-sdk-client-mock"; import { type SavedItem } from "dynamodb-toolbox"; -import { beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it } from "vitest"; import { ClientLog, ClientLogStoreRequest } from "@/modules/client-logs/client-log"; @@ -203,13 +203,14 @@ describe("LogsRepositoryDynamodb", () => { startDate: new Date("2023-01-01T00:00:00Z"), endDate: new Date("2023-01-02T00:00:00Z"), appId, + lastEvaluatedKey: undefined, }); expect(result.isOk()).toBe(true); - expect(result._unsafeUnwrap()).toHaveLength(1); + expect(result._unsafeUnwrap().clientLogs).toHaveLength(1); - expect(result._unsafeUnwrap()[0]).toBeInstanceOf(ClientLog); + expect(result._unsafeUnwrap().clientLogs[0]).toBeInstanceOf(ClientLog); }); it("should return an empty array when no items are found in the database", async () => { @@ -230,11 +231,12 @@ describe("LogsRepositoryDynamodb", () => { startDate: new Date("2023-01-01T00:00:00Z"), endDate: new Date("2023-01-02T00:00:00Z"), appId, + lastEvaluatedKey: undefined, }); expect(result.isOk()).toBe(true); - expect(result._unsafeUnwrap()).toEqual([]); + expect(result._unsafeUnwrap()).toStrictEqual({ clientLogs: [], lastEvaluatedKey: undefined }); }); it("should return an error when data cannot be mapped to ClientLog", async () => { @@ -289,6 +291,7 @@ describe("LogsRepositoryDynamodb", () => { startDate: new Date("2023-01-01T00:00:00Z"), endDate: new Date("2023-01-02T00:00:00Z"), appId, + lastEvaluatedKey: undefined, }); expect(result.isErr()).toBe(true); @@ -323,6 +326,7 @@ describe("LogsRepositoryDynamodb", () => { startDate: new Date("2023-01-01T00:00:00Z"), endDate: new Date("2023-01-02T00:00:00Z"), appId, + lastEvaluatedKey: undefined, }); expect(result.isErr()).toBe(true); @@ -344,6 +348,7 @@ describe("LogsRepositoryDynamodb", () => { startDate: new Date("2023-01-01T00:00:00Z"), endDate: new Date("2023-01-02T00:00:00Z"), appId, + lastEvaluatedKey: undefined, }); expect(result.isErr()).toBe(true); @@ -387,13 +392,14 @@ describe("LogsRepositoryDynamodb", () => { saleorApiUrl, checkoutOrOrderId: "test-order-id", appId, + lastEvaluatedKey: undefined, }); expect(result.isOk()).toBe(true); - expect(result._unsafeUnwrap()).toHaveLength(1); + expect(result._unsafeUnwrap().clientLogs).toHaveLength(1); - expect(result._unsafeUnwrap()[0]).toBeInstanceOf(ClientLog); + expect(result._unsafeUnwrap().clientLogs[0]).toBeInstanceOf(ClientLog); }); it("should return an empty array when no items are found in the database", async () => { @@ -413,11 +419,12 @@ describe("LogsRepositoryDynamodb", () => { saleorApiUrl, checkoutOrOrderId: "test-order-id", appId, + lastEvaluatedKey: undefined, }); expect(result.isOk()).toBe(true); - expect(result._unsafeUnwrap()).toEqual([]); + expect(result._unsafeUnwrap()).toStrictEqual({ clientLogs: [], lastEvaluatedKey: undefined }); }); it("should return an error when data cannot be mapped to ClientLog", async () => { @@ -473,6 +480,7 @@ describe("LogsRepositoryDynamodb", () => { saleorApiUrl, checkoutOrOrderId: "test-order-id", appId, + lastEvaluatedKey: undefined, }); expect(result.isErr()).toBe(true); @@ -506,6 +514,7 @@ describe("LogsRepositoryDynamodb", () => { saleorApiUrl, checkoutOrOrderId: "test-order-id", appId, + lastEvaluatedKey: undefined, }); expect(result.isErr()).toBe(true); @@ -526,6 +535,7 @@ describe("LogsRepositoryDynamodb", () => { saleorApiUrl, checkoutOrOrderId: "test-order-id", appId, + lastEvaluatedKey: undefined, }); expect(result.isErr()).toBe(true); @@ -560,9 +570,13 @@ describe("LogsRepositoryMemory", () => { // endDate = startDate + 1h endDate: new Date(new Date().getTime() + 60 * 60 * 1000), appId, + lastEvaluatedKey: undefined, }); - expect(result._unsafeUnwrap()).toEqual([testLog]); + expect(result._unsafeUnwrap()).toStrictEqual({ + clientLogs: [testLog], + lastEvaluatedKey: undefined, + }); }); }); @@ -611,9 +625,13 @@ describe("LogsRepositoryMemory", () => { saleorApiUrl, appId, checkoutOrOrderId: "test-order-id", + lastEvaluatedKey: undefined, }); - expect(result._unsafeUnwrap()).toEqual([testLog]); + expect(result._unsafeUnwrap()).toStrictEqual({ + clientLogs: [testLog], + lastEvaluatedKey: undefined, + }); }); }); }); diff --git a/apps/avatax/src/modules/client-logs/logs-repository.ts b/apps/avatax/src/modules/client-logs/logs-repository.ts index 88bc5149ee..81333cdc16 100644 --- a/apps/avatax/src/modules/client-logs/logs-repository.ts +++ b/apps/avatax/src/modules/client-logs/logs-repository.ts @@ -19,18 +19,20 @@ import { } from "./dynamo-schema"; import { LogsTransformer } from "./log-transformer"; +export type LastEvaluatedKey = Record | undefined; + export interface ILogsRepository { getLogsByDate(args: { saleorApiUrl: string; startDate: Date; endDate: Date; appId: string; - }): Promise>; + }): Promise>; getLogsByCheckoutOrOrderId(args: { saleorApiUrl: string; appId: string; checkoutOrOrderId: string; - }): Promise>; + }): Promise>; writeLog(args: { clientLogRequest: ClientLogStoreRequest; saleorApiUrl: string; @@ -74,14 +76,16 @@ export class LogsRepositoryDynamodb implements ILogsRepository { startDate, endDate, appId, + lastEvaluatedKey, }: { saleorApiUrl: string; startDate: Date; endDate: Date; appId: string; + lastEvaluatedKey: LastEvaluatedKey; }): Promise< Result< - ClientLog[], + { clientLogs: ClientLog[]; lastEvaluatedKey: LastEvaluatedKey }, | InstanceType | InstanceType > @@ -105,7 +109,7 @@ export class LogsRepositoryDynamodb implements ILogsRepository { }, }) .entities(this.logByDateEntity) - .options({ limit: 100, capacity: "TOTAL" }) + .options({ limit: 100, capacity: "TOTAL", exclusiveStartKey: lastEvaluatedKey }) .send(), (err) => new LogsRepositoryDynamodb.LogsFetchError( @@ -133,10 +137,10 @@ export class LogsRepositoryDynamodb implements ILogsRepository { if (!fetchResult.value.Items) { this.logger.info("No logs found for specified dates", { startDate, endDate }); - return ok([]); + return ok({ clientLogs: [], lastEvaluatedKey: undefined }); } - return Result.combine( + const clientLogs = Result.combine( fetchResult.value.Items.map((item) => transformer.fromDynamoEntityToClientLog(item)), ).mapErr((error) => { this.logger.error("Unexpected error while mapping DynamoDB response to ClientLog", { error }); @@ -146,17 +150,33 @@ export class LogsRepositoryDynamodb implements ILogsRepository { { cause: error }, ); }); + + if (clientLogs.isErr()) { + return err(clientLogs.error); + } + + return ok({ + clientLogs: clientLogs.value, + lastEvaluatedKey: fetchResult.value.LastEvaluatedKey, + }); } async getLogsByCheckoutOrOrderId({ saleorApiUrl, appId, checkoutOrOrderId, + lastEvaluatedKey, }: { saleorApiUrl: string; appId: string; checkoutOrOrderId: string; - }): Promise>> { + lastEvaluatedKey: LastEvaluatedKey; + }): Promise< + Result< + { clientLogs: ClientLog[]; lastEvaluatedKey: LastEvaluatedKey }, + InstanceType + > + > { const transformer = new LogsTransformer(); this.logger.debug("Starting fetching logs by checkoutOrOrderId from DynamoDB", { @@ -175,7 +195,7 @@ export class LogsRepositoryDynamodb implements ILogsRepository { }, }) .entities(this.logsByCheckoutOrOrderId) - .options({ limit: 100, capacity: "TOTAL" }) + .options({ limit: 100, capacity: "TOTAL", exclusiveStartKey: lastEvaluatedKey }) .send(), (err) => new LogsRepositoryDynamodb.LogsFetchError( @@ -203,10 +223,10 @@ export class LogsRepositoryDynamodb implements ILogsRepository { if (!fetchResult.value.Items) { this.logger.info("No logs found for checkoutOrOrderId", { checkoutOrOrderId }); - return ok([]); + return ok({ clientLogs: [], lastEvaluatedKey: undefined }); } - return Result.combine( + const clientLogs = Result.combine( fetchResult.value.Items.map((item) => transformer.fromDynamoEntityToClientLog(item)), ).mapErr((error) => { this.logger.error("Unexpected error while mapping DynamoDB response to ClientLog", { error }); @@ -216,6 +236,15 @@ export class LogsRepositoryDynamodb implements ILogsRepository { { cause: error }, ); }); + + if (clientLogs.isErr()) { + return err(clientLogs.error); + } + + return ok({ + clientLogs: clientLogs.value, + lastEvaluatedKey: fetchResult.value.LastEvaluatedKey, + }); } private prepareBatchWriteFromClientLog(args: { @@ -307,8 +336,9 @@ export class LogsRepositoryMemory implements ILogsRepository { startDate: Date; endDate: Date; appId: string; - }): Promise> { - return ok(this.logs); + lastEvaluatedKey: LastEvaluatedKey; + }): Promise> { + return ok({ clientLogs: this.logs, lastEvaluatedKey: undefined }); } static UnexpectedWriteLogError = BaseError.subclass("UnexpectedWriteLogError"); @@ -342,7 +372,8 @@ export class LogsRepositoryMemory implements ILogsRepository { saleorApiUrl: string; appId: string; checkoutOrOrderId: string; - }): Promise> { - return ok(this.logs); + lastEvaluatedKey: LastEvaluatedKey; + }): Promise> { + return ok({ clientLogs: this.logs, lastEvaluatedKey: undefined }); } } diff --git a/apps/avatax/src/modules/client-logs/ui/format-date.ts b/apps/avatax/src/modules/client-logs/ui/format-date.ts new file mode 100644 index 0000000000..fc5c8b567d --- /dev/null +++ b/apps/avatax/src/modules/client-logs/ui/format-date.ts @@ -0,0 +1,41 @@ +/** + * Format date to display in logs table (e.g: 12/3/24, 11:09:56 AM) + */ +export const formatUserFriendlyDate = (date: Date): string => { + const lang = navigator.language ?? "en-GB"; + + return Intl.DateTimeFormat(lang, { + day: "numeric", + month: "numeric", + year: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + }).format(date); +}; + +/** + * Format date for format yyyy-mm-dd T hh:mm -> required by RangePicker + */ +export const formatDateForInput = (d: Date): string => { + const pad = (n: number): string => n.toString().padStart(2, "0"); + + const yyyy = d.getFullYear(); + const MM = pad(d.getMonth() + 1); + const hh = pad(d.getHours()); + const mm = pad(d.getMinutes()); + + return `${yyyy}-${MM}-${pad(d.getDate())}T${hh}:${mm}`; +}; + +/** + * Format date for ISO format with seconds & milliseconds set to 00 -> required by clientLogsRouter.getByDate. + * It allows us to cache the query as seconds won't change between UI changes. + */ +export const formatDateForQuery = (d: Date): string => { + d.setSeconds(0); + + d.setMilliseconds(0); + + return d.toISOString(); +}; diff --git a/apps/avatax/src/modules/client-logs/ui/logs-browser.tsx b/apps/avatax/src/modules/client-logs/ui/logs-browser.tsx index 3a1192772a..40802231a1 100644 --- a/apps/avatax/src/modules/client-logs/ui/logs-browser.tsx +++ b/apps/avatax/src/modules/client-logs/ui/logs-browser.tsx @@ -1,31 +1,31 @@ -import { Box, Input, RangeInput, Skeleton, Switch, Text } from "@saleor/macaw-ui"; +import { + Box, + Button, + ChevronLeftIcon, + ChevronRightIcon, + Input, + RangeInput, + Skeleton, + Switch, + Text, +} from "@saleor/macaw-ui"; import { useState } from "react"; import { type ClientLogValue } from "@/modules/client-logs/client-log"; import { trpcClient } from "@/modules/trpc/trpc-client"; -type LogsTypeSwitchState = "date" | "orderOrCheckoutID"; +import { formatDateForInput, formatDateForQuery, formatUserFriendlyDate } from "./format-date"; +import { useClientLogsPagination } from "./use-client-logs-pagination"; -const formatUserFriendlyDate = (date: Date) => { - const lang = navigator.language ?? "en-GB"; - - return Intl.DateTimeFormat(lang, { - day: "numeric", - month: "numeric", - year: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }).format(date); -}; +type LogsTypeSwitchState = "date" | "orderOrCheckoutID"; const LogsSkeleton = () => { const count = 3; return ( - {new Array(count).fill(null).map((index) => { - return ; + {new Array(count).fill(null).map((_v, i) => { + return ; })} ); @@ -78,21 +78,46 @@ const LogsList = ({ logs }: { logs: Array }) => { ); }; -/** - * Format date for format yyyy-mm-dd T hh:mm -> required by RangePicker - */ -const formatDateForInput = (d: Date) => { - const pad = (n: number) => n.toString().padStart(2, "0"); - - const yyyy = d.getFullYear(); - const MM = pad(d.getMonth() + 1); - const hh = pad(d.getHours()); - const mm = pad(d.getMinutes()); - - return `${yyyy}-${MM}-${pad(d.getDate())}T${hh}:${mm}`; +const LogsPagiation = ({ + onForwardButtonClick, + onBackwardButtonClick, + isForwardButtonDisabled, + isBackwardButtonDisabled, +}: { + onForwardButtonClick: () => void; + onBackwardButtonClick: () => void; + isForwardButtonDisabled: boolean; + isBackwardButtonDisabled: boolean; +}) => { + return ( + + + } + ); }; diff --git a/apps/avatax/src/pages/logs.tsx b/apps/avatax/src/pages/logs.tsx index 344adf0cbb..87a27bad2f 100644 --- a/apps/avatax/src/pages/logs.tsx +++ b/apps/avatax/src/pages/logs.tsx @@ -1,6 +1,5 @@ import { useAppBridge } from "@saleor/app-sdk/app-bridge"; import { Box } from "@saleor/macaw-ui"; -import React from "react"; import { LogsBrowser } from "@/modules/client-logs/ui/logs-browser"; import { AppBreadcrumbs } from "@/modules/ui/app-breadcrumbs"; @@ -8,7 +7,7 @@ import { AppBreadcrumbs } from "@/modules/ui/app-breadcrumbs"; import { Section } from "../modules/ui/app-section"; const Header = () => { - return Check App logs (up to last 100); + return Check App logs; }; const ConfigurationPage = () => { diff --git a/apps/avatax/src/setup-tests.ts b/apps/avatax/src/setup-tests.ts index fff045cbcd..b7dec1f943 100644 --- a/apps/avatax/src/setup-tests.ts +++ b/apps/avatax/src/setup-tests.ts @@ -2,6 +2,10 @@ import { vi } from "vitest"; vi.stubEnv("DYNAMODB_LOGS_ITEM_TTL_IN_DAYS", "7"); vi.stubEnv("SECRET_KEY", "test_secret_key"); +vi.stubEnv("DYNAMODB_LOGS_TABLE_NAME", "test-table"); +vi.stubEnv("AWS_REGION", "test"); +vi.stubEnv("AWS_ACCESS_KEY_ID", "test-id"); +vi.stubEnv("AWS_SECRET_ACCESS_KEY", "test-key"); /** * Add test setup logic here From 989cb68312e22e5f1042cac91348964afa22499c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Thu, 23 Jan 2025 09:35:20 +0100 Subject: [PATCH 13/17] Improve Segment events (#1704) --- .changeset/twenty-berries-occur.md | 7 ++ apps/segment/generated/graphql.ts | 119 ++++++++---------- .../graphql/fragments/order-base.graphql | 37 ++++-- .../subscriptions/order-confirmed.graphql | 12 ++ .../subscriptions/order-created.graphql | 12 -- .../subscriptions/order-fully-paid.graphql | 12 -- apps/segment/package.json | 1 + .../filter-empty-values-from-object.test.ts | 53 ++++++++ .../lib/filter-empty-values-from-object.ts | 47 +++++++ .../tracking-events/__tests__/mocks.ts | 42 +++++-- .../track-event.use-case.test.ts | 44 +++---- .../tracking-events/tracking-events.test.ts | 91 ++++++++------ .../tracking-events/tracking-events.ts | 99 ++++++++++----- .../webhooks/definitions/order-confirmed.ts | 20 +++ .../webhooks/definitions/order-created.ts | 17 --- .../webhooks/definitions/order-fully-paid.ts | 20 --- apps/segment/src/modules/webhooks/webhooks.ts | 6 +- ...order-fully-paid.ts => order-confirmed.ts} | 20 +-- .../src/pages/api/webhooks/order-created.ts | 110 ---------------- pnpm-lock.yaml | 3 + 20 files changed, 415 insertions(+), 357 deletions(-) create mode 100644 .changeset/twenty-berries-occur.md create mode 100644 apps/segment/graphql/subscriptions/order-confirmed.graphql delete mode 100644 apps/segment/graphql/subscriptions/order-created.graphql delete mode 100644 apps/segment/graphql/subscriptions/order-fully-paid.graphql create mode 100644 apps/segment/src/lib/filter-empty-values-from-object.test.ts create mode 100644 apps/segment/src/lib/filter-empty-values-from-object.ts create mode 100644 apps/segment/src/modules/webhooks/definitions/order-confirmed.ts delete mode 100644 apps/segment/src/modules/webhooks/definitions/order-created.ts delete mode 100644 apps/segment/src/modules/webhooks/definitions/order-fully-paid.ts rename apps/segment/src/pages/api/webhooks/{order-fully-paid.ts => order-confirmed.ts} (79%) delete mode 100644 apps/segment/src/pages/api/webhooks/order-created.ts diff --git a/.changeset/twenty-berries-occur.md b/.changeset/twenty-berries-occur.md new file mode 100644 index 0000000000..e66a0a5187 --- /dev/null +++ b/.changeset/twenty-berries-occur.md @@ -0,0 +1,7 @@ +--- +"segment": patch +--- + +- Changed what we sent to Segment to be in sync with their [spec](https://segment.com/docs/connections/spec/ecommerce/v2/) +- Added new Saleor event - `OrderConfirmed` that will be mapped to Segment `OrderCompleted` +- Removed Saleor event - `OrderCreated` - it didn't have respective Segment event diff --git a/apps/segment/generated/graphql.ts b/apps/segment/generated/graphql.ts index fe615826a4..a34dcf4a26 100644 --- a/apps/segment/generated/graphql.ts +++ b/apps/segment/generated/graphql.ts @@ -33098,7 +33098,7 @@ export type _Service = { sdl?: Maybe; }; -export type OrderBaseFragment = { __typename?: 'Order', id: string, userEmail?: string | null, shippingMethodName?: string | null, number: string, user?: { __typename?: 'User', id: string, email: string } | null, channel: { __typename?: 'Channel', id: string, slug: string, name: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, net: { __typename?: 'Money', currency: string, amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, productVariantId?: string | null, productSku?: string | null, variantName: string }> }; +export type OrderBaseFragment = { __typename?: 'Order', id: string, userEmail?: string | null, voucherCode?: string | null, user?: { __typename?: 'User', id: string } | null, channel: { __typename?: 'Channel', id: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, tax: { __typename?: 'Money', amount: number } }, undiscountedTotal: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, shippingPrice: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, quantity: number, voucherCode?: string | null, productSku?: string | null, totalPrice: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, variant?: { __typename?: 'ProductVariant', name: string, product: { __typename?: 'Product', name: string, category?: { __typename?: 'Category', name: string } | null } } | null }> }; export type EnableWebhookMutationVariables = Exact<{ id: Scalars['ID']['input']; @@ -33122,72 +33122,84 @@ export type FetchAppWebhooksQueryVariables = Exact<{ export type FetchAppWebhooksQuery = { __typename?: 'Query', app?: { __typename?: 'App', webhooks?: Array<{ __typename?: 'Webhook', id: string, isActive: boolean }> | null } | null }; -export type OrderCancelledSubscriptionPayloadFragment = { __typename?: 'OrderCancelled', issuedAt?: any | null, order?: { __typename?: 'Order', id: string, userEmail?: string | null, shippingMethodName?: string | null, number: string, user?: { __typename?: 'User', id: string, email: string } | null, channel: { __typename?: 'Channel', id: string, slug: string, name: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, net: { __typename?: 'Money', currency: string, amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, productVariantId?: string | null, productSku?: string | null, variantName: string }> } | null }; +export type OrderCancelledSubscriptionPayloadFragment = { __typename?: 'OrderCancelled', issuedAt?: any | null, order?: { __typename?: 'Order', id: string, userEmail?: string | null, voucherCode?: string | null, user?: { __typename?: 'User', id: string } | null, channel: { __typename?: 'Channel', id: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, tax: { __typename?: 'Money', amount: number } }, undiscountedTotal: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, shippingPrice: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, quantity: number, voucherCode?: string | null, productSku?: string | null, totalPrice: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, variant?: { __typename?: 'ProductVariant', name: string, product: { __typename?: 'Product', name: string, category?: { __typename?: 'Category', name: string } | null } } | null }> } | null }; export type OrderCancelledSubscriptionVariables = Exact<{ [key: string]: never; }>; -export type OrderCancelledSubscription = { __typename?: 'Subscription', event?: { __typename?: 'AccountChangeEmailRequested' } | { __typename?: 'AccountConfirmationRequested' } | { __typename?: 'AccountConfirmed' } | { __typename?: 'AccountDeleteRequested' } | { __typename?: 'AccountDeleted' } | { __typename?: 'AccountEmailChanged' } | { __typename?: 'AccountSetPasswordRequested' } | { __typename?: 'AddressCreated' } | { __typename?: 'AddressDeleted' } | { __typename?: 'AddressUpdated' } | { __typename?: 'AppDeleted' } | { __typename?: 'AppInstalled' } | { __typename?: 'AppStatusChanged' } | { __typename?: 'AppUpdated' } | { __typename?: 'AttributeCreated' } | { __typename?: 'AttributeDeleted' } | { __typename?: 'AttributeUpdated' } | { __typename?: 'AttributeValueCreated' } | { __typename?: 'AttributeValueDeleted' } | { __typename?: 'AttributeValueUpdated' } | { __typename?: 'CalculateTaxes' } | { __typename?: 'CategoryCreated' } | { __typename?: 'CategoryDeleted' } | { __typename?: 'CategoryUpdated' } | { __typename?: 'ChannelCreated' } | { __typename?: 'ChannelDeleted' } | { __typename?: 'ChannelMetadataUpdated' } | { __typename?: 'ChannelStatusChanged' } | { __typename?: 'ChannelUpdated' } | { __typename?: 'CheckoutCreated' } | { __typename?: 'CheckoutFilterShippingMethods' } | { __typename?: 'CheckoutFullyPaid' } | { __typename?: 'CheckoutMetadataUpdated' } | { __typename?: 'CheckoutUpdated' } | { __typename?: 'CollectionCreated' } | { __typename?: 'CollectionDeleted' } | { __typename?: 'CollectionMetadataUpdated' } | { __typename?: 'CollectionUpdated' } | { __typename?: 'CustomerCreated' } | { __typename?: 'CustomerMetadataUpdated' } | { __typename?: 'CustomerUpdated' } | { __typename?: 'DraftOrderCreated' } | { __typename?: 'DraftOrderDeleted' } | { __typename?: 'DraftOrderUpdated' } | { __typename?: 'FulfillmentApproved' } | { __typename?: 'FulfillmentCanceled' } | { __typename?: 'FulfillmentCreated' } | { __typename?: 'FulfillmentMetadataUpdated' } | { __typename?: 'FulfillmentTrackingNumberUpdated' } | { __typename?: 'GiftCardCreated' } | { __typename?: 'GiftCardDeleted' } | { __typename?: 'GiftCardExportCompleted' } | { __typename?: 'GiftCardMetadataUpdated' } | { __typename?: 'GiftCardSent' } | { __typename?: 'GiftCardStatusChanged' } | { __typename?: 'GiftCardUpdated' } | { __typename?: 'InvoiceDeleted' } | { __typename?: 'InvoiceRequested' } | { __typename?: 'InvoiceSent' } | { __typename?: 'ListStoredPaymentMethods' } | { __typename?: 'MenuCreated' } | { __typename?: 'MenuDeleted' } | { __typename?: 'MenuItemCreated' } | { __typename?: 'MenuItemDeleted' } | { __typename?: 'MenuItemUpdated' } | { __typename?: 'MenuUpdated' } | { __typename?: 'OrderBulkCreated' } | { __typename?: 'OrderCancelled', issuedAt?: any | null, order?: { __typename?: 'Order', id: string, userEmail?: string | null, shippingMethodName?: string | null, number: string, user?: { __typename?: 'User', id: string, email: string } | null, channel: { __typename?: 'Channel', id: string, slug: string, name: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, net: { __typename?: 'Money', currency: string, amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, productVariantId?: string | null, productSku?: string | null, variantName: string }> } | null } | { __typename?: 'OrderConfirmed' } | { __typename?: 'OrderCreated' } | { __typename?: 'OrderExpired' } | { __typename?: 'OrderFilterShippingMethods' } | { __typename?: 'OrderFulfilled' } | { __typename?: 'OrderFullyPaid' } | { __typename?: 'OrderFullyRefunded' } | { __typename?: 'OrderMetadataUpdated' } | { __typename?: 'OrderPaid' } | { __typename?: 'OrderRefunded' } | { __typename?: 'OrderUpdated' } | { __typename?: 'PageCreated' } | { __typename?: 'PageDeleted' } | { __typename?: 'PageTypeCreated' } | { __typename?: 'PageTypeDeleted' } | { __typename?: 'PageTypeUpdated' } | { __typename?: 'PageUpdated' } | { __typename?: 'PaymentAuthorize' } | { __typename?: 'PaymentCaptureEvent' } | { __typename?: 'PaymentConfirmEvent' } | { __typename?: 'PaymentGatewayInitializeSession' } | { __typename?: 'PaymentGatewayInitializeTokenizationSession' } | { __typename?: 'PaymentListGateways' } | { __typename?: 'PaymentMethodInitializeTokenizationSession' } | { __typename?: 'PaymentMethodProcessTokenizationSession' } | { __typename?: 'PaymentProcessEvent' } | { __typename?: 'PaymentRefundEvent' } | { __typename?: 'PaymentVoidEvent' } | { __typename?: 'PermissionGroupCreated' } | { __typename?: 'PermissionGroupDeleted' } | { __typename?: 'PermissionGroupUpdated' } | { __typename?: 'ProductCreated' } | { __typename?: 'ProductDeleted' } | { __typename?: 'ProductExportCompleted' } | { __typename?: 'ProductMediaCreated' } | { __typename?: 'ProductMediaDeleted' } | { __typename?: 'ProductMediaUpdated' } | { __typename?: 'ProductMetadataUpdated' } | { __typename?: 'ProductUpdated' } | { __typename?: 'ProductVariantBackInStock' } | { __typename?: 'ProductVariantCreated' } | { __typename?: 'ProductVariantDeleted' } | { __typename?: 'ProductVariantMetadataUpdated' } | { __typename?: 'ProductVariantOutOfStock' } | { __typename?: 'ProductVariantStockUpdated' } | { __typename?: 'ProductVariantUpdated' } | { __typename?: 'PromotionCreated' } | { __typename?: 'PromotionDeleted' } | { __typename?: 'PromotionEnded' } | { __typename?: 'PromotionRuleCreated' } | { __typename?: 'PromotionRuleDeleted' } | { __typename?: 'PromotionRuleUpdated' } | { __typename?: 'PromotionStarted' } | { __typename?: 'PromotionUpdated' } | { __typename?: 'SaleCreated' } | { __typename?: 'SaleDeleted' } | { __typename?: 'SaleToggle' } | { __typename?: 'SaleUpdated' } | { __typename?: 'ShippingListMethodsForCheckout' } | { __typename?: 'ShippingPriceCreated' } | { __typename?: 'ShippingPriceDeleted' } | { __typename?: 'ShippingPriceUpdated' } | { __typename?: 'ShippingZoneCreated' } | { __typename?: 'ShippingZoneDeleted' } | { __typename?: 'ShippingZoneMetadataUpdated' } | { __typename?: 'ShippingZoneUpdated' } | { __typename?: 'ShopMetadataUpdated' } | { __typename?: 'StaffCreated' } | { __typename?: 'StaffDeleted' } | { __typename?: 'StaffSetPasswordRequested' } | { __typename?: 'StaffUpdated' } | { __typename?: 'StoredPaymentMethodDeleteRequested' } | { __typename?: 'ThumbnailCreated' } | { __typename?: 'TransactionCancelationRequested' } | { __typename?: 'TransactionChargeRequested' } | { __typename?: 'TransactionInitializeSession' } | { __typename?: 'TransactionItemMetadataUpdated' } | { __typename?: 'TransactionProcessSession' } | { __typename?: 'TransactionRefundRequested' } | { __typename?: 'TranslationCreated' } | { __typename?: 'TranslationUpdated' } | { __typename?: 'VoucherCodeExportCompleted' } | { __typename?: 'VoucherCodesCreated' } | { __typename?: 'VoucherCodesDeleted' } | { __typename?: 'VoucherCreated' } | { __typename?: 'VoucherDeleted' } | { __typename?: 'VoucherMetadataUpdated' } | { __typename?: 'VoucherUpdated' } | { __typename?: 'WarehouseCreated' } | { __typename?: 'WarehouseDeleted' } | { __typename?: 'WarehouseMetadataUpdated' } | { __typename?: 'WarehouseUpdated' } | null }; +export type OrderCancelledSubscription = { __typename?: 'Subscription', event?: { __typename?: 'AccountChangeEmailRequested' } | { __typename?: 'AccountConfirmationRequested' } | { __typename?: 'AccountConfirmed' } | { __typename?: 'AccountDeleteRequested' } | { __typename?: 'AccountDeleted' } | { __typename?: 'AccountEmailChanged' } | { __typename?: 'AccountSetPasswordRequested' } | { __typename?: 'AddressCreated' } | { __typename?: 'AddressDeleted' } | { __typename?: 'AddressUpdated' } | { __typename?: 'AppDeleted' } | { __typename?: 'AppInstalled' } | { __typename?: 'AppStatusChanged' } | { __typename?: 'AppUpdated' } | { __typename?: 'AttributeCreated' } | { __typename?: 'AttributeDeleted' } | { __typename?: 'AttributeUpdated' } | { __typename?: 'AttributeValueCreated' } | { __typename?: 'AttributeValueDeleted' } | { __typename?: 'AttributeValueUpdated' } | { __typename?: 'CalculateTaxes' } | { __typename?: 'CategoryCreated' } | { __typename?: 'CategoryDeleted' } | { __typename?: 'CategoryUpdated' } | { __typename?: 'ChannelCreated' } | { __typename?: 'ChannelDeleted' } | { __typename?: 'ChannelMetadataUpdated' } | { __typename?: 'ChannelStatusChanged' } | { __typename?: 'ChannelUpdated' } | { __typename?: 'CheckoutCreated' } | { __typename?: 'CheckoutFilterShippingMethods' } | { __typename?: 'CheckoutFullyPaid' } | { __typename?: 'CheckoutMetadataUpdated' } | { __typename?: 'CheckoutUpdated' } | { __typename?: 'CollectionCreated' } | { __typename?: 'CollectionDeleted' } | { __typename?: 'CollectionMetadataUpdated' } | { __typename?: 'CollectionUpdated' } | { __typename?: 'CustomerCreated' } | { __typename?: 'CustomerMetadataUpdated' } | { __typename?: 'CustomerUpdated' } | { __typename?: 'DraftOrderCreated' } | { __typename?: 'DraftOrderDeleted' } | { __typename?: 'DraftOrderUpdated' } | { __typename?: 'FulfillmentApproved' } | { __typename?: 'FulfillmentCanceled' } | { __typename?: 'FulfillmentCreated' } | { __typename?: 'FulfillmentMetadataUpdated' } | { __typename?: 'FulfillmentTrackingNumberUpdated' } | { __typename?: 'GiftCardCreated' } | { __typename?: 'GiftCardDeleted' } | { __typename?: 'GiftCardExportCompleted' } | { __typename?: 'GiftCardMetadataUpdated' } | { __typename?: 'GiftCardSent' } | { __typename?: 'GiftCardStatusChanged' } | { __typename?: 'GiftCardUpdated' } | { __typename?: 'InvoiceDeleted' } | { __typename?: 'InvoiceRequested' } | { __typename?: 'InvoiceSent' } | { __typename?: 'ListStoredPaymentMethods' } | { __typename?: 'MenuCreated' } | { __typename?: 'MenuDeleted' } | { __typename?: 'MenuItemCreated' } | { __typename?: 'MenuItemDeleted' } | { __typename?: 'MenuItemUpdated' } | { __typename?: 'MenuUpdated' } | { __typename?: 'OrderBulkCreated' } | { __typename?: 'OrderCancelled', issuedAt?: any | null, order?: { __typename?: 'Order', id: string, userEmail?: string | null, voucherCode?: string | null, user?: { __typename?: 'User', id: string } | null, channel: { __typename?: 'Channel', id: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, tax: { __typename?: 'Money', amount: number } }, undiscountedTotal: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, shippingPrice: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, quantity: number, voucherCode?: string | null, productSku?: string | null, totalPrice: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, variant?: { __typename?: 'ProductVariant', name: string, product: { __typename?: 'Product', name: string, category?: { __typename?: 'Category', name: string } | null } } | null }> } | null } | { __typename?: 'OrderConfirmed' } | { __typename?: 'OrderCreated' } | { __typename?: 'OrderExpired' } | { __typename?: 'OrderFilterShippingMethods' } | { __typename?: 'OrderFulfilled' } | { __typename?: 'OrderFullyPaid' } | { __typename?: 'OrderFullyRefunded' } | { __typename?: 'OrderMetadataUpdated' } | { __typename?: 'OrderPaid' } | { __typename?: 'OrderRefunded' } | { __typename?: 'OrderUpdated' } | { __typename?: 'PageCreated' } | { __typename?: 'PageDeleted' } | { __typename?: 'PageTypeCreated' } | { __typename?: 'PageTypeDeleted' } | { __typename?: 'PageTypeUpdated' } | { __typename?: 'PageUpdated' } | { __typename?: 'PaymentAuthorize' } | { __typename?: 'PaymentCaptureEvent' } | { __typename?: 'PaymentConfirmEvent' } | { __typename?: 'PaymentGatewayInitializeSession' } | { __typename?: 'PaymentGatewayInitializeTokenizationSession' } | { __typename?: 'PaymentListGateways' } | { __typename?: 'PaymentMethodInitializeTokenizationSession' } | { __typename?: 'PaymentMethodProcessTokenizationSession' } | { __typename?: 'PaymentProcessEvent' } | { __typename?: 'PaymentRefundEvent' } | { __typename?: 'PaymentVoidEvent' } | { __typename?: 'PermissionGroupCreated' } | { __typename?: 'PermissionGroupDeleted' } | { __typename?: 'PermissionGroupUpdated' } | { __typename?: 'ProductCreated' } | { __typename?: 'ProductDeleted' } | { __typename?: 'ProductExportCompleted' } | { __typename?: 'ProductMediaCreated' } | { __typename?: 'ProductMediaDeleted' } | { __typename?: 'ProductMediaUpdated' } | { __typename?: 'ProductMetadataUpdated' } | { __typename?: 'ProductUpdated' } | { __typename?: 'ProductVariantBackInStock' } | { __typename?: 'ProductVariantCreated' } | { __typename?: 'ProductVariantDeleted' } | { __typename?: 'ProductVariantMetadataUpdated' } | { __typename?: 'ProductVariantOutOfStock' } | { __typename?: 'ProductVariantStockUpdated' } | { __typename?: 'ProductVariantUpdated' } | { __typename?: 'PromotionCreated' } | { __typename?: 'PromotionDeleted' } | { __typename?: 'PromotionEnded' } | { __typename?: 'PromotionRuleCreated' } | { __typename?: 'PromotionRuleDeleted' } | { __typename?: 'PromotionRuleUpdated' } | { __typename?: 'PromotionStarted' } | { __typename?: 'PromotionUpdated' } | { __typename?: 'SaleCreated' } | { __typename?: 'SaleDeleted' } | { __typename?: 'SaleToggle' } | { __typename?: 'SaleUpdated' } | { __typename?: 'ShippingListMethodsForCheckout' } | { __typename?: 'ShippingPriceCreated' } | { __typename?: 'ShippingPriceDeleted' } | { __typename?: 'ShippingPriceUpdated' } | { __typename?: 'ShippingZoneCreated' } | { __typename?: 'ShippingZoneDeleted' } | { __typename?: 'ShippingZoneMetadataUpdated' } | { __typename?: 'ShippingZoneUpdated' } | { __typename?: 'ShopMetadataUpdated' } | { __typename?: 'StaffCreated' } | { __typename?: 'StaffDeleted' } | { __typename?: 'StaffSetPasswordRequested' } | { __typename?: 'StaffUpdated' } | { __typename?: 'StoredPaymentMethodDeleteRequested' } | { __typename?: 'ThumbnailCreated' } | { __typename?: 'TransactionCancelationRequested' } | { __typename?: 'TransactionChargeRequested' } | { __typename?: 'TransactionInitializeSession' } | { __typename?: 'TransactionItemMetadataUpdated' } | { __typename?: 'TransactionProcessSession' } | { __typename?: 'TransactionRefundRequested' } | { __typename?: 'TranslationCreated' } | { __typename?: 'TranslationUpdated' } | { __typename?: 'VoucherCodeExportCompleted' } | { __typename?: 'VoucherCodesCreated' } | { __typename?: 'VoucherCodesDeleted' } | { __typename?: 'VoucherCreated' } | { __typename?: 'VoucherDeleted' } | { __typename?: 'VoucherMetadataUpdated' } | { __typename?: 'VoucherUpdated' } | { __typename?: 'WarehouseCreated' } | { __typename?: 'WarehouseDeleted' } | { __typename?: 'WarehouseMetadataUpdated' } | { __typename?: 'WarehouseUpdated' } | null }; -export type OrderCreatedSubscriptionPayloadFragment = { __typename?: 'OrderCreated', issuedAt?: any | null, order?: { __typename?: 'Order', id: string, userEmail?: string | null, shippingMethodName?: string | null, number: string, user?: { __typename?: 'User', id: string, email: string } | null, channel: { __typename?: 'Channel', id: string, slug: string, name: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, net: { __typename?: 'Money', currency: string, amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, productVariantId?: string | null, productSku?: string | null, variantName: string }> } | null }; +export type OrderConfirmedSubscriptionPayloadFragment = { __typename?: 'OrderConfirmed', issuedAt?: any | null, order?: { __typename?: 'Order', id: string, userEmail?: string | null, voucherCode?: string | null, user?: { __typename?: 'User', id: string } | null, channel: { __typename?: 'Channel', id: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, tax: { __typename?: 'Money', amount: number } }, undiscountedTotal: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, shippingPrice: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, quantity: number, voucherCode?: string | null, productSku?: string | null, totalPrice: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, variant?: { __typename?: 'ProductVariant', name: string, product: { __typename?: 'Product', name: string, category?: { __typename?: 'Category', name: string } | null } } | null }> } | null }; -export type OrderCreatedSubscriptionVariables = Exact<{ [key: string]: never; }>; +export type OrderConfirmedSubscriptionVariables = Exact<{ [key: string]: never; }>; -export type OrderCreatedSubscription = { __typename?: 'Subscription', event?: { __typename?: 'AccountChangeEmailRequested' } | { __typename?: 'AccountConfirmationRequested' } | { __typename?: 'AccountConfirmed' } | { __typename?: 'AccountDeleteRequested' } | { __typename?: 'AccountDeleted' } | { __typename?: 'AccountEmailChanged' } | { __typename?: 'AccountSetPasswordRequested' } | { __typename?: 'AddressCreated' } | { __typename?: 'AddressDeleted' } | { __typename?: 'AddressUpdated' } | { __typename?: 'AppDeleted' } | { __typename?: 'AppInstalled' } | { __typename?: 'AppStatusChanged' } | { __typename?: 'AppUpdated' } | { __typename?: 'AttributeCreated' } | { __typename?: 'AttributeDeleted' } | { __typename?: 'AttributeUpdated' } | { __typename?: 'AttributeValueCreated' } | { __typename?: 'AttributeValueDeleted' } | { __typename?: 'AttributeValueUpdated' } | { __typename?: 'CalculateTaxes' } | { __typename?: 'CategoryCreated' } | { __typename?: 'CategoryDeleted' } | { __typename?: 'CategoryUpdated' } | { __typename?: 'ChannelCreated' } | { __typename?: 'ChannelDeleted' } | { __typename?: 'ChannelMetadataUpdated' } | { __typename?: 'ChannelStatusChanged' } | { __typename?: 'ChannelUpdated' } | { __typename?: 'CheckoutCreated' } | { __typename?: 'CheckoutFilterShippingMethods' } | { __typename?: 'CheckoutFullyPaid' } | { __typename?: 'CheckoutMetadataUpdated' } | { __typename?: 'CheckoutUpdated' } | { __typename?: 'CollectionCreated' } | { __typename?: 'CollectionDeleted' } | { __typename?: 'CollectionMetadataUpdated' } | { __typename?: 'CollectionUpdated' } | { __typename?: 'CustomerCreated' } | { __typename?: 'CustomerMetadataUpdated' } | { __typename?: 'CustomerUpdated' } | { __typename?: 'DraftOrderCreated' } | { __typename?: 'DraftOrderDeleted' } | { __typename?: 'DraftOrderUpdated' } | { __typename?: 'FulfillmentApproved' } | { __typename?: 'FulfillmentCanceled' } | { __typename?: 'FulfillmentCreated' } | { __typename?: 'FulfillmentMetadataUpdated' } | { __typename?: 'FulfillmentTrackingNumberUpdated' } | { __typename?: 'GiftCardCreated' } | { __typename?: 'GiftCardDeleted' } | { __typename?: 'GiftCardExportCompleted' } | { __typename?: 'GiftCardMetadataUpdated' } | { __typename?: 'GiftCardSent' } | { __typename?: 'GiftCardStatusChanged' } | { __typename?: 'GiftCardUpdated' } | { __typename?: 'InvoiceDeleted' } | { __typename?: 'InvoiceRequested' } | { __typename?: 'InvoiceSent' } | { __typename?: 'ListStoredPaymentMethods' } | { __typename?: 'MenuCreated' } | { __typename?: 'MenuDeleted' } | { __typename?: 'MenuItemCreated' } | { __typename?: 'MenuItemDeleted' } | { __typename?: 'MenuItemUpdated' } | { __typename?: 'MenuUpdated' } | { __typename?: 'OrderBulkCreated' } | { __typename?: 'OrderCancelled' } | { __typename?: 'OrderConfirmed' } | { __typename?: 'OrderCreated', issuedAt?: any | null, order?: { __typename?: 'Order', id: string, userEmail?: string | null, shippingMethodName?: string | null, number: string, user?: { __typename?: 'User', id: string, email: string } | null, channel: { __typename?: 'Channel', id: string, slug: string, name: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, net: { __typename?: 'Money', currency: string, amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, productVariantId?: string | null, productSku?: string | null, variantName: string }> } | null } | { __typename?: 'OrderExpired' } | { __typename?: 'OrderFilterShippingMethods' } | { __typename?: 'OrderFulfilled' } | { __typename?: 'OrderFullyPaid' } | { __typename?: 'OrderFullyRefunded' } | { __typename?: 'OrderMetadataUpdated' } | { __typename?: 'OrderPaid' } | { __typename?: 'OrderRefunded' } | { __typename?: 'OrderUpdated' } | { __typename?: 'PageCreated' } | { __typename?: 'PageDeleted' } | { __typename?: 'PageTypeCreated' } | { __typename?: 'PageTypeDeleted' } | { __typename?: 'PageTypeUpdated' } | { __typename?: 'PageUpdated' } | { __typename?: 'PaymentAuthorize' } | { __typename?: 'PaymentCaptureEvent' } | { __typename?: 'PaymentConfirmEvent' } | { __typename?: 'PaymentGatewayInitializeSession' } | { __typename?: 'PaymentGatewayInitializeTokenizationSession' } | { __typename?: 'PaymentListGateways' } | { __typename?: 'PaymentMethodInitializeTokenizationSession' } | { __typename?: 'PaymentMethodProcessTokenizationSession' } | { __typename?: 'PaymentProcessEvent' } | { __typename?: 'PaymentRefundEvent' } | { __typename?: 'PaymentVoidEvent' } | { __typename?: 'PermissionGroupCreated' } | { __typename?: 'PermissionGroupDeleted' } | { __typename?: 'PermissionGroupUpdated' } | { __typename?: 'ProductCreated' } | { __typename?: 'ProductDeleted' } | { __typename?: 'ProductExportCompleted' } | { __typename?: 'ProductMediaCreated' } | { __typename?: 'ProductMediaDeleted' } | { __typename?: 'ProductMediaUpdated' } | { __typename?: 'ProductMetadataUpdated' } | { __typename?: 'ProductUpdated' } | { __typename?: 'ProductVariantBackInStock' } | { __typename?: 'ProductVariantCreated' } | { __typename?: 'ProductVariantDeleted' } | { __typename?: 'ProductVariantMetadataUpdated' } | { __typename?: 'ProductVariantOutOfStock' } | { __typename?: 'ProductVariantStockUpdated' } | { __typename?: 'ProductVariantUpdated' } | { __typename?: 'PromotionCreated' } | { __typename?: 'PromotionDeleted' } | { __typename?: 'PromotionEnded' } | { __typename?: 'PromotionRuleCreated' } | { __typename?: 'PromotionRuleDeleted' } | { __typename?: 'PromotionRuleUpdated' } | { __typename?: 'PromotionStarted' } | { __typename?: 'PromotionUpdated' } | { __typename?: 'SaleCreated' } | { __typename?: 'SaleDeleted' } | { __typename?: 'SaleToggle' } | { __typename?: 'SaleUpdated' } | { __typename?: 'ShippingListMethodsForCheckout' } | { __typename?: 'ShippingPriceCreated' } | { __typename?: 'ShippingPriceDeleted' } | { __typename?: 'ShippingPriceUpdated' } | { __typename?: 'ShippingZoneCreated' } | { __typename?: 'ShippingZoneDeleted' } | { __typename?: 'ShippingZoneMetadataUpdated' } | { __typename?: 'ShippingZoneUpdated' } | { __typename?: 'ShopMetadataUpdated' } | { __typename?: 'StaffCreated' } | { __typename?: 'StaffDeleted' } | { __typename?: 'StaffSetPasswordRequested' } | { __typename?: 'StaffUpdated' } | { __typename?: 'StoredPaymentMethodDeleteRequested' } | { __typename?: 'ThumbnailCreated' } | { __typename?: 'TransactionCancelationRequested' } | { __typename?: 'TransactionChargeRequested' } | { __typename?: 'TransactionInitializeSession' } | { __typename?: 'TransactionItemMetadataUpdated' } | { __typename?: 'TransactionProcessSession' } | { __typename?: 'TransactionRefundRequested' } | { __typename?: 'TranslationCreated' } | { __typename?: 'TranslationUpdated' } | { __typename?: 'VoucherCodeExportCompleted' } | { __typename?: 'VoucherCodesCreated' } | { __typename?: 'VoucherCodesDeleted' } | { __typename?: 'VoucherCreated' } | { __typename?: 'VoucherDeleted' } | { __typename?: 'VoucherMetadataUpdated' } | { __typename?: 'VoucherUpdated' } | { __typename?: 'WarehouseCreated' } | { __typename?: 'WarehouseDeleted' } | { __typename?: 'WarehouseMetadataUpdated' } | { __typename?: 'WarehouseUpdated' } | null }; +export type OrderConfirmedSubscription = { __typename?: 'Subscription', event?: { __typename?: 'AccountChangeEmailRequested' } | { __typename?: 'AccountConfirmationRequested' } | { __typename?: 'AccountConfirmed' } | { __typename?: 'AccountDeleteRequested' } | { __typename?: 'AccountDeleted' } | { __typename?: 'AccountEmailChanged' } | { __typename?: 'AccountSetPasswordRequested' } | { __typename?: 'AddressCreated' } | { __typename?: 'AddressDeleted' } | { __typename?: 'AddressUpdated' } | { __typename?: 'AppDeleted' } | { __typename?: 'AppInstalled' } | { __typename?: 'AppStatusChanged' } | { __typename?: 'AppUpdated' } | { __typename?: 'AttributeCreated' } | { __typename?: 'AttributeDeleted' } | { __typename?: 'AttributeUpdated' } | { __typename?: 'AttributeValueCreated' } | { __typename?: 'AttributeValueDeleted' } | { __typename?: 'AttributeValueUpdated' } | { __typename?: 'CalculateTaxes' } | { __typename?: 'CategoryCreated' } | { __typename?: 'CategoryDeleted' } | { __typename?: 'CategoryUpdated' } | { __typename?: 'ChannelCreated' } | { __typename?: 'ChannelDeleted' } | { __typename?: 'ChannelMetadataUpdated' } | { __typename?: 'ChannelStatusChanged' } | { __typename?: 'ChannelUpdated' } | { __typename?: 'CheckoutCreated' } | { __typename?: 'CheckoutFilterShippingMethods' } | { __typename?: 'CheckoutFullyPaid' } | { __typename?: 'CheckoutMetadataUpdated' } | { __typename?: 'CheckoutUpdated' } | { __typename?: 'CollectionCreated' } | { __typename?: 'CollectionDeleted' } | { __typename?: 'CollectionMetadataUpdated' } | { __typename?: 'CollectionUpdated' } | { __typename?: 'CustomerCreated' } | { __typename?: 'CustomerMetadataUpdated' } | { __typename?: 'CustomerUpdated' } | { __typename?: 'DraftOrderCreated' } | { __typename?: 'DraftOrderDeleted' } | { __typename?: 'DraftOrderUpdated' } | { __typename?: 'FulfillmentApproved' } | { __typename?: 'FulfillmentCanceled' } | { __typename?: 'FulfillmentCreated' } | { __typename?: 'FulfillmentMetadataUpdated' } | { __typename?: 'FulfillmentTrackingNumberUpdated' } | { __typename?: 'GiftCardCreated' } | { __typename?: 'GiftCardDeleted' } | { __typename?: 'GiftCardExportCompleted' } | { __typename?: 'GiftCardMetadataUpdated' } | { __typename?: 'GiftCardSent' } | { __typename?: 'GiftCardStatusChanged' } | { __typename?: 'GiftCardUpdated' } | { __typename?: 'InvoiceDeleted' } | { __typename?: 'InvoiceRequested' } | { __typename?: 'InvoiceSent' } | { __typename?: 'ListStoredPaymentMethods' } | { __typename?: 'MenuCreated' } | { __typename?: 'MenuDeleted' } | { __typename?: 'MenuItemCreated' } | { __typename?: 'MenuItemDeleted' } | { __typename?: 'MenuItemUpdated' } | { __typename?: 'MenuUpdated' } | { __typename?: 'OrderBulkCreated' } | { __typename?: 'OrderCancelled' } | { __typename?: 'OrderConfirmed', issuedAt?: any | null, order?: { __typename?: 'Order', id: string, userEmail?: string | null, voucherCode?: string | null, user?: { __typename?: 'User', id: string } | null, channel: { __typename?: 'Channel', id: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, tax: { __typename?: 'Money', amount: number } }, undiscountedTotal: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, shippingPrice: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, quantity: number, voucherCode?: string | null, productSku?: string | null, totalPrice: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, variant?: { __typename?: 'ProductVariant', name: string, product: { __typename?: 'Product', name: string, category?: { __typename?: 'Category', name: string } | null } } | null }> } | null } | { __typename?: 'OrderCreated' } | { __typename?: 'OrderExpired' } | { __typename?: 'OrderFilterShippingMethods' } | { __typename?: 'OrderFulfilled' } | { __typename?: 'OrderFullyPaid' } | { __typename?: 'OrderFullyRefunded' } | { __typename?: 'OrderMetadataUpdated' } | { __typename?: 'OrderPaid' } | { __typename?: 'OrderRefunded' } | { __typename?: 'OrderUpdated' } | { __typename?: 'PageCreated' } | { __typename?: 'PageDeleted' } | { __typename?: 'PageTypeCreated' } | { __typename?: 'PageTypeDeleted' } | { __typename?: 'PageTypeUpdated' } | { __typename?: 'PageUpdated' } | { __typename?: 'PaymentAuthorize' } | { __typename?: 'PaymentCaptureEvent' } | { __typename?: 'PaymentConfirmEvent' } | { __typename?: 'PaymentGatewayInitializeSession' } | { __typename?: 'PaymentGatewayInitializeTokenizationSession' } | { __typename?: 'PaymentListGateways' } | { __typename?: 'PaymentMethodInitializeTokenizationSession' } | { __typename?: 'PaymentMethodProcessTokenizationSession' } | { __typename?: 'PaymentProcessEvent' } | { __typename?: 'PaymentRefundEvent' } | { __typename?: 'PaymentVoidEvent' } | { __typename?: 'PermissionGroupCreated' } | { __typename?: 'PermissionGroupDeleted' } | { __typename?: 'PermissionGroupUpdated' } | { __typename?: 'ProductCreated' } | { __typename?: 'ProductDeleted' } | { __typename?: 'ProductExportCompleted' } | { __typename?: 'ProductMediaCreated' } | { __typename?: 'ProductMediaDeleted' } | { __typename?: 'ProductMediaUpdated' } | { __typename?: 'ProductMetadataUpdated' } | { __typename?: 'ProductUpdated' } | { __typename?: 'ProductVariantBackInStock' } | { __typename?: 'ProductVariantCreated' } | { __typename?: 'ProductVariantDeleted' } | { __typename?: 'ProductVariantMetadataUpdated' } | { __typename?: 'ProductVariantOutOfStock' } | { __typename?: 'ProductVariantStockUpdated' } | { __typename?: 'ProductVariantUpdated' } | { __typename?: 'PromotionCreated' } | { __typename?: 'PromotionDeleted' } | { __typename?: 'PromotionEnded' } | { __typename?: 'PromotionRuleCreated' } | { __typename?: 'PromotionRuleDeleted' } | { __typename?: 'PromotionRuleUpdated' } | { __typename?: 'PromotionStarted' } | { __typename?: 'PromotionUpdated' } | { __typename?: 'SaleCreated' } | { __typename?: 'SaleDeleted' } | { __typename?: 'SaleToggle' } | { __typename?: 'SaleUpdated' } | { __typename?: 'ShippingListMethodsForCheckout' } | { __typename?: 'ShippingPriceCreated' } | { __typename?: 'ShippingPriceDeleted' } | { __typename?: 'ShippingPriceUpdated' } | { __typename?: 'ShippingZoneCreated' } | { __typename?: 'ShippingZoneDeleted' } | { __typename?: 'ShippingZoneMetadataUpdated' } | { __typename?: 'ShippingZoneUpdated' } | { __typename?: 'ShopMetadataUpdated' } | { __typename?: 'StaffCreated' } | { __typename?: 'StaffDeleted' } | { __typename?: 'StaffSetPasswordRequested' } | { __typename?: 'StaffUpdated' } | { __typename?: 'StoredPaymentMethodDeleteRequested' } | { __typename?: 'ThumbnailCreated' } | { __typename?: 'TransactionCancelationRequested' } | { __typename?: 'TransactionChargeRequested' } | { __typename?: 'TransactionInitializeSession' } | { __typename?: 'TransactionItemMetadataUpdated' } | { __typename?: 'TransactionProcessSession' } | { __typename?: 'TransactionRefundRequested' } | { __typename?: 'TranslationCreated' } | { __typename?: 'TranslationUpdated' } | { __typename?: 'VoucherCodeExportCompleted' } | { __typename?: 'VoucherCodesCreated' } | { __typename?: 'VoucherCodesDeleted' } | { __typename?: 'VoucherCreated' } | { __typename?: 'VoucherDeleted' } | { __typename?: 'VoucherMetadataUpdated' } | { __typename?: 'VoucherUpdated' } | { __typename?: 'WarehouseCreated' } | { __typename?: 'WarehouseDeleted' } | { __typename?: 'WarehouseMetadataUpdated' } | { __typename?: 'WarehouseUpdated' } | null }; -export type OrderFullyPaidSubscriptionPayloadFragment = { __typename?: 'OrderFullyPaid', issuedAt?: any | null, order?: { __typename?: 'Order', id: string, userEmail?: string | null, shippingMethodName?: string | null, number: string, user?: { __typename?: 'User', id: string, email: string } | null, channel: { __typename?: 'Channel', id: string, slug: string, name: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, net: { __typename?: 'Money', currency: string, amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, productVariantId?: string | null, productSku?: string | null, variantName: string }> } | null }; - -export type OrderFullyPaidSubscriptionVariables = Exact<{ [key: string]: never; }>; - - -export type OrderFullyPaidSubscription = { __typename?: 'Subscription', event?: { __typename?: 'AccountChangeEmailRequested' } | { __typename?: 'AccountConfirmationRequested' } | { __typename?: 'AccountConfirmed' } | { __typename?: 'AccountDeleteRequested' } | { __typename?: 'AccountDeleted' } | { __typename?: 'AccountEmailChanged' } | { __typename?: 'AccountSetPasswordRequested' } | { __typename?: 'AddressCreated' } | { __typename?: 'AddressDeleted' } | { __typename?: 'AddressUpdated' } | { __typename?: 'AppDeleted' } | { __typename?: 'AppInstalled' } | { __typename?: 'AppStatusChanged' } | { __typename?: 'AppUpdated' } | { __typename?: 'AttributeCreated' } | { __typename?: 'AttributeDeleted' } | { __typename?: 'AttributeUpdated' } | { __typename?: 'AttributeValueCreated' } | { __typename?: 'AttributeValueDeleted' } | { __typename?: 'AttributeValueUpdated' } | { __typename?: 'CalculateTaxes' } | { __typename?: 'CategoryCreated' } | { __typename?: 'CategoryDeleted' } | { __typename?: 'CategoryUpdated' } | { __typename?: 'ChannelCreated' } | { __typename?: 'ChannelDeleted' } | { __typename?: 'ChannelMetadataUpdated' } | { __typename?: 'ChannelStatusChanged' } | { __typename?: 'ChannelUpdated' } | { __typename?: 'CheckoutCreated' } | { __typename?: 'CheckoutFilterShippingMethods' } | { __typename?: 'CheckoutFullyPaid' } | { __typename?: 'CheckoutMetadataUpdated' } | { __typename?: 'CheckoutUpdated' } | { __typename?: 'CollectionCreated' } | { __typename?: 'CollectionDeleted' } | { __typename?: 'CollectionMetadataUpdated' } | { __typename?: 'CollectionUpdated' } | { __typename?: 'CustomerCreated' } | { __typename?: 'CustomerMetadataUpdated' } | { __typename?: 'CustomerUpdated' } | { __typename?: 'DraftOrderCreated' } | { __typename?: 'DraftOrderDeleted' } | { __typename?: 'DraftOrderUpdated' } | { __typename?: 'FulfillmentApproved' } | { __typename?: 'FulfillmentCanceled' } | { __typename?: 'FulfillmentCreated' } | { __typename?: 'FulfillmentMetadataUpdated' } | { __typename?: 'FulfillmentTrackingNumberUpdated' } | { __typename?: 'GiftCardCreated' } | { __typename?: 'GiftCardDeleted' } | { __typename?: 'GiftCardExportCompleted' } | { __typename?: 'GiftCardMetadataUpdated' } | { __typename?: 'GiftCardSent' } | { __typename?: 'GiftCardStatusChanged' } | { __typename?: 'GiftCardUpdated' } | { __typename?: 'InvoiceDeleted' } | { __typename?: 'InvoiceRequested' } | { __typename?: 'InvoiceSent' } | { __typename?: 'ListStoredPaymentMethods' } | { __typename?: 'MenuCreated' } | { __typename?: 'MenuDeleted' } | { __typename?: 'MenuItemCreated' } | { __typename?: 'MenuItemDeleted' } | { __typename?: 'MenuItemUpdated' } | { __typename?: 'MenuUpdated' } | { __typename?: 'OrderBulkCreated' } | { __typename?: 'OrderCancelled' } | { __typename?: 'OrderConfirmed' } | { __typename?: 'OrderCreated' } | { __typename?: 'OrderExpired' } | { __typename?: 'OrderFilterShippingMethods' } | { __typename?: 'OrderFulfilled' } | { __typename?: 'OrderFullyPaid', issuedAt?: any | null, order?: { __typename?: 'Order', id: string, userEmail?: string | null, shippingMethodName?: string | null, number: string, user?: { __typename?: 'User', id: string, email: string } | null, channel: { __typename?: 'Channel', id: string, slug: string, name: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, net: { __typename?: 'Money', currency: string, amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, productVariantId?: string | null, productSku?: string | null, variantName: string }> } | null } | { __typename?: 'OrderFullyRefunded' } | { __typename?: 'OrderMetadataUpdated' } | { __typename?: 'OrderPaid' } | { __typename?: 'OrderRefunded' } | { __typename?: 'OrderUpdated' } | { __typename?: 'PageCreated' } | { __typename?: 'PageDeleted' } | { __typename?: 'PageTypeCreated' } | { __typename?: 'PageTypeDeleted' } | { __typename?: 'PageTypeUpdated' } | { __typename?: 'PageUpdated' } | { __typename?: 'PaymentAuthorize' } | { __typename?: 'PaymentCaptureEvent' } | { __typename?: 'PaymentConfirmEvent' } | { __typename?: 'PaymentGatewayInitializeSession' } | { __typename?: 'PaymentGatewayInitializeTokenizationSession' } | { __typename?: 'PaymentListGateways' } | { __typename?: 'PaymentMethodInitializeTokenizationSession' } | { __typename?: 'PaymentMethodProcessTokenizationSession' } | { __typename?: 'PaymentProcessEvent' } | { __typename?: 'PaymentRefundEvent' } | { __typename?: 'PaymentVoidEvent' } | { __typename?: 'PermissionGroupCreated' } | { __typename?: 'PermissionGroupDeleted' } | { __typename?: 'PermissionGroupUpdated' } | { __typename?: 'ProductCreated' } | { __typename?: 'ProductDeleted' } | { __typename?: 'ProductExportCompleted' } | { __typename?: 'ProductMediaCreated' } | { __typename?: 'ProductMediaDeleted' } | { __typename?: 'ProductMediaUpdated' } | { __typename?: 'ProductMetadataUpdated' } | { __typename?: 'ProductUpdated' } | { __typename?: 'ProductVariantBackInStock' } | { __typename?: 'ProductVariantCreated' } | { __typename?: 'ProductVariantDeleted' } | { __typename?: 'ProductVariantMetadataUpdated' } | { __typename?: 'ProductVariantOutOfStock' } | { __typename?: 'ProductVariantStockUpdated' } | { __typename?: 'ProductVariantUpdated' } | { __typename?: 'PromotionCreated' } | { __typename?: 'PromotionDeleted' } | { __typename?: 'PromotionEnded' } | { __typename?: 'PromotionRuleCreated' } | { __typename?: 'PromotionRuleDeleted' } | { __typename?: 'PromotionRuleUpdated' } | { __typename?: 'PromotionStarted' } | { __typename?: 'PromotionUpdated' } | { __typename?: 'SaleCreated' } | { __typename?: 'SaleDeleted' } | { __typename?: 'SaleToggle' } | { __typename?: 'SaleUpdated' } | { __typename?: 'ShippingListMethodsForCheckout' } | { __typename?: 'ShippingPriceCreated' } | { __typename?: 'ShippingPriceDeleted' } | { __typename?: 'ShippingPriceUpdated' } | { __typename?: 'ShippingZoneCreated' } | { __typename?: 'ShippingZoneDeleted' } | { __typename?: 'ShippingZoneMetadataUpdated' } | { __typename?: 'ShippingZoneUpdated' } | { __typename?: 'ShopMetadataUpdated' } | { __typename?: 'StaffCreated' } | { __typename?: 'StaffDeleted' } | { __typename?: 'StaffSetPasswordRequested' } | { __typename?: 'StaffUpdated' } | { __typename?: 'StoredPaymentMethodDeleteRequested' } | { __typename?: 'ThumbnailCreated' } | { __typename?: 'TransactionCancelationRequested' } | { __typename?: 'TransactionChargeRequested' } | { __typename?: 'TransactionInitializeSession' } | { __typename?: 'TransactionItemMetadataUpdated' } | { __typename?: 'TransactionProcessSession' } | { __typename?: 'TransactionRefundRequested' } | { __typename?: 'TranslationCreated' } | { __typename?: 'TranslationUpdated' } | { __typename?: 'VoucherCodeExportCompleted' } | { __typename?: 'VoucherCodesCreated' } | { __typename?: 'VoucherCodesDeleted' } | { __typename?: 'VoucherCreated' } | { __typename?: 'VoucherDeleted' } | { __typename?: 'VoucherMetadataUpdated' } | { __typename?: 'VoucherUpdated' } | { __typename?: 'WarehouseCreated' } | { __typename?: 'WarehouseDeleted' } | { __typename?: 'WarehouseMetadataUpdated' } | { __typename?: 'WarehouseUpdated' } | null }; - -export type OrderRefundedSubscriptionPayloadFragment = { __typename?: 'OrderRefunded', issuedAt?: any | null, order?: { __typename?: 'Order', id: string, userEmail?: string | null, shippingMethodName?: string | null, number: string, user?: { __typename?: 'User', id: string, email: string } | null, channel: { __typename?: 'Channel', id: string, slug: string, name: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, net: { __typename?: 'Money', currency: string, amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, productVariantId?: string | null, productSku?: string | null, variantName: string }> } | null }; +export type OrderRefundedSubscriptionPayloadFragment = { __typename?: 'OrderRefunded', issuedAt?: any | null, order?: { __typename?: 'Order', id: string, userEmail?: string | null, voucherCode?: string | null, user?: { __typename?: 'User', id: string } | null, channel: { __typename?: 'Channel', id: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, tax: { __typename?: 'Money', amount: number } }, undiscountedTotal: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, shippingPrice: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, quantity: number, voucherCode?: string | null, productSku?: string | null, totalPrice: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, variant?: { __typename?: 'ProductVariant', name: string, product: { __typename?: 'Product', name: string, category?: { __typename?: 'Category', name: string } | null } } | null }> } | null }; export type OrderRefundedSubscriptionVariables = Exact<{ [key: string]: never; }>; -export type OrderRefundedSubscription = { __typename?: 'Subscription', event?: { __typename?: 'AccountChangeEmailRequested' } | { __typename?: 'AccountConfirmationRequested' } | { __typename?: 'AccountConfirmed' } | { __typename?: 'AccountDeleteRequested' } | { __typename?: 'AccountDeleted' } | { __typename?: 'AccountEmailChanged' } | { __typename?: 'AccountSetPasswordRequested' } | { __typename?: 'AddressCreated' } | { __typename?: 'AddressDeleted' } | { __typename?: 'AddressUpdated' } | { __typename?: 'AppDeleted' } | { __typename?: 'AppInstalled' } | { __typename?: 'AppStatusChanged' } | { __typename?: 'AppUpdated' } | { __typename?: 'AttributeCreated' } | { __typename?: 'AttributeDeleted' } | { __typename?: 'AttributeUpdated' } | { __typename?: 'AttributeValueCreated' } | { __typename?: 'AttributeValueDeleted' } | { __typename?: 'AttributeValueUpdated' } | { __typename?: 'CalculateTaxes' } | { __typename?: 'CategoryCreated' } | { __typename?: 'CategoryDeleted' } | { __typename?: 'CategoryUpdated' } | { __typename?: 'ChannelCreated' } | { __typename?: 'ChannelDeleted' } | { __typename?: 'ChannelMetadataUpdated' } | { __typename?: 'ChannelStatusChanged' } | { __typename?: 'ChannelUpdated' } | { __typename?: 'CheckoutCreated' } | { __typename?: 'CheckoutFilterShippingMethods' } | { __typename?: 'CheckoutFullyPaid' } | { __typename?: 'CheckoutMetadataUpdated' } | { __typename?: 'CheckoutUpdated' } | { __typename?: 'CollectionCreated' } | { __typename?: 'CollectionDeleted' } | { __typename?: 'CollectionMetadataUpdated' } | { __typename?: 'CollectionUpdated' } | { __typename?: 'CustomerCreated' } | { __typename?: 'CustomerMetadataUpdated' } | { __typename?: 'CustomerUpdated' } | { __typename?: 'DraftOrderCreated' } | { __typename?: 'DraftOrderDeleted' } | { __typename?: 'DraftOrderUpdated' } | { __typename?: 'FulfillmentApproved' } | { __typename?: 'FulfillmentCanceled' } | { __typename?: 'FulfillmentCreated' } | { __typename?: 'FulfillmentMetadataUpdated' } | { __typename?: 'FulfillmentTrackingNumberUpdated' } | { __typename?: 'GiftCardCreated' } | { __typename?: 'GiftCardDeleted' } | { __typename?: 'GiftCardExportCompleted' } | { __typename?: 'GiftCardMetadataUpdated' } | { __typename?: 'GiftCardSent' } | { __typename?: 'GiftCardStatusChanged' } | { __typename?: 'GiftCardUpdated' } | { __typename?: 'InvoiceDeleted' } | { __typename?: 'InvoiceRequested' } | { __typename?: 'InvoiceSent' } | { __typename?: 'ListStoredPaymentMethods' } | { __typename?: 'MenuCreated' } | { __typename?: 'MenuDeleted' } | { __typename?: 'MenuItemCreated' } | { __typename?: 'MenuItemDeleted' } | { __typename?: 'MenuItemUpdated' } | { __typename?: 'MenuUpdated' } | { __typename?: 'OrderBulkCreated' } | { __typename?: 'OrderCancelled' } | { __typename?: 'OrderConfirmed' } | { __typename?: 'OrderCreated' } | { __typename?: 'OrderExpired' } | { __typename?: 'OrderFilterShippingMethods' } | { __typename?: 'OrderFulfilled' } | { __typename?: 'OrderFullyPaid' } | { __typename?: 'OrderFullyRefunded' } | { __typename?: 'OrderMetadataUpdated' } | { __typename?: 'OrderPaid' } | { __typename?: 'OrderRefunded', issuedAt?: any | null, order?: { __typename?: 'Order', id: string, userEmail?: string | null, shippingMethodName?: string | null, number: string, user?: { __typename?: 'User', id: string, email: string } | null, channel: { __typename?: 'Channel', id: string, slug: string, name: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, net: { __typename?: 'Money', currency: string, amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, productVariantId?: string | null, productSku?: string | null, variantName: string }> } | null } | { __typename?: 'OrderUpdated' } | { __typename?: 'PageCreated' } | { __typename?: 'PageDeleted' } | { __typename?: 'PageTypeCreated' } | { __typename?: 'PageTypeDeleted' } | { __typename?: 'PageTypeUpdated' } | { __typename?: 'PageUpdated' } | { __typename?: 'PaymentAuthorize' } | { __typename?: 'PaymentCaptureEvent' } | { __typename?: 'PaymentConfirmEvent' } | { __typename?: 'PaymentGatewayInitializeSession' } | { __typename?: 'PaymentGatewayInitializeTokenizationSession' } | { __typename?: 'PaymentListGateways' } | { __typename?: 'PaymentMethodInitializeTokenizationSession' } | { __typename?: 'PaymentMethodProcessTokenizationSession' } | { __typename?: 'PaymentProcessEvent' } | { __typename?: 'PaymentRefundEvent' } | { __typename?: 'PaymentVoidEvent' } | { __typename?: 'PermissionGroupCreated' } | { __typename?: 'PermissionGroupDeleted' } | { __typename?: 'PermissionGroupUpdated' } | { __typename?: 'ProductCreated' } | { __typename?: 'ProductDeleted' } | { __typename?: 'ProductExportCompleted' } | { __typename?: 'ProductMediaCreated' } | { __typename?: 'ProductMediaDeleted' } | { __typename?: 'ProductMediaUpdated' } | { __typename?: 'ProductMetadataUpdated' } | { __typename?: 'ProductUpdated' } | { __typename?: 'ProductVariantBackInStock' } | { __typename?: 'ProductVariantCreated' } | { __typename?: 'ProductVariantDeleted' } | { __typename?: 'ProductVariantMetadataUpdated' } | { __typename?: 'ProductVariantOutOfStock' } | { __typename?: 'ProductVariantStockUpdated' } | { __typename?: 'ProductVariantUpdated' } | { __typename?: 'PromotionCreated' } | { __typename?: 'PromotionDeleted' } | { __typename?: 'PromotionEnded' } | { __typename?: 'PromotionRuleCreated' } | { __typename?: 'PromotionRuleDeleted' } | { __typename?: 'PromotionRuleUpdated' } | { __typename?: 'PromotionStarted' } | { __typename?: 'PromotionUpdated' } | { __typename?: 'SaleCreated' } | { __typename?: 'SaleDeleted' } | { __typename?: 'SaleToggle' } | { __typename?: 'SaleUpdated' } | { __typename?: 'ShippingListMethodsForCheckout' } | { __typename?: 'ShippingPriceCreated' } | { __typename?: 'ShippingPriceDeleted' } | { __typename?: 'ShippingPriceUpdated' } | { __typename?: 'ShippingZoneCreated' } | { __typename?: 'ShippingZoneDeleted' } | { __typename?: 'ShippingZoneMetadataUpdated' } | { __typename?: 'ShippingZoneUpdated' } | { __typename?: 'ShopMetadataUpdated' } | { __typename?: 'StaffCreated' } | { __typename?: 'StaffDeleted' } | { __typename?: 'StaffSetPasswordRequested' } | { __typename?: 'StaffUpdated' } | { __typename?: 'StoredPaymentMethodDeleteRequested' } | { __typename?: 'ThumbnailCreated' } | { __typename?: 'TransactionCancelationRequested' } | { __typename?: 'TransactionChargeRequested' } | { __typename?: 'TransactionInitializeSession' } | { __typename?: 'TransactionItemMetadataUpdated' } | { __typename?: 'TransactionProcessSession' } | { __typename?: 'TransactionRefundRequested' } | { __typename?: 'TranslationCreated' } | { __typename?: 'TranslationUpdated' } | { __typename?: 'VoucherCodeExportCompleted' } | { __typename?: 'VoucherCodesCreated' } | { __typename?: 'VoucherCodesDeleted' } | { __typename?: 'VoucherCreated' } | { __typename?: 'VoucherDeleted' } | { __typename?: 'VoucherMetadataUpdated' } | { __typename?: 'VoucherUpdated' } | { __typename?: 'WarehouseCreated' } | { __typename?: 'WarehouseDeleted' } | { __typename?: 'WarehouseMetadataUpdated' } | { __typename?: 'WarehouseUpdated' } | null }; +export type OrderRefundedSubscription = { __typename?: 'Subscription', event?: { __typename?: 'AccountChangeEmailRequested' } | { __typename?: 'AccountConfirmationRequested' } | { __typename?: 'AccountConfirmed' } | { __typename?: 'AccountDeleteRequested' } | { __typename?: 'AccountDeleted' } | { __typename?: 'AccountEmailChanged' } | { __typename?: 'AccountSetPasswordRequested' } | { __typename?: 'AddressCreated' } | { __typename?: 'AddressDeleted' } | { __typename?: 'AddressUpdated' } | { __typename?: 'AppDeleted' } | { __typename?: 'AppInstalled' } | { __typename?: 'AppStatusChanged' } | { __typename?: 'AppUpdated' } | { __typename?: 'AttributeCreated' } | { __typename?: 'AttributeDeleted' } | { __typename?: 'AttributeUpdated' } | { __typename?: 'AttributeValueCreated' } | { __typename?: 'AttributeValueDeleted' } | { __typename?: 'AttributeValueUpdated' } | { __typename?: 'CalculateTaxes' } | { __typename?: 'CategoryCreated' } | { __typename?: 'CategoryDeleted' } | { __typename?: 'CategoryUpdated' } | { __typename?: 'ChannelCreated' } | { __typename?: 'ChannelDeleted' } | { __typename?: 'ChannelMetadataUpdated' } | { __typename?: 'ChannelStatusChanged' } | { __typename?: 'ChannelUpdated' } | { __typename?: 'CheckoutCreated' } | { __typename?: 'CheckoutFilterShippingMethods' } | { __typename?: 'CheckoutFullyPaid' } | { __typename?: 'CheckoutMetadataUpdated' } | { __typename?: 'CheckoutUpdated' } | { __typename?: 'CollectionCreated' } | { __typename?: 'CollectionDeleted' } | { __typename?: 'CollectionMetadataUpdated' } | { __typename?: 'CollectionUpdated' } | { __typename?: 'CustomerCreated' } | { __typename?: 'CustomerMetadataUpdated' } | { __typename?: 'CustomerUpdated' } | { __typename?: 'DraftOrderCreated' } | { __typename?: 'DraftOrderDeleted' } | { __typename?: 'DraftOrderUpdated' } | { __typename?: 'FulfillmentApproved' } | { __typename?: 'FulfillmentCanceled' } | { __typename?: 'FulfillmentCreated' } | { __typename?: 'FulfillmentMetadataUpdated' } | { __typename?: 'FulfillmentTrackingNumberUpdated' } | { __typename?: 'GiftCardCreated' } | { __typename?: 'GiftCardDeleted' } | { __typename?: 'GiftCardExportCompleted' } | { __typename?: 'GiftCardMetadataUpdated' } | { __typename?: 'GiftCardSent' } | { __typename?: 'GiftCardStatusChanged' } | { __typename?: 'GiftCardUpdated' } | { __typename?: 'InvoiceDeleted' } | { __typename?: 'InvoiceRequested' } | { __typename?: 'InvoiceSent' } | { __typename?: 'ListStoredPaymentMethods' } | { __typename?: 'MenuCreated' } | { __typename?: 'MenuDeleted' } | { __typename?: 'MenuItemCreated' } | { __typename?: 'MenuItemDeleted' } | { __typename?: 'MenuItemUpdated' } | { __typename?: 'MenuUpdated' } | { __typename?: 'OrderBulkCreated' } | { __typename?: 'OrderCancelled' } | { __typename?: 'OrderConfirmed' } | { __typename?: 'OrderCreated' } | { __typename?: 'OrderExpired' } | { __typename?: 'OrderFilterShippingMethods' } | { __typename?: 'OrderFulfilled' } | { __typename?: 'OrderFullyPaid' } | { __typename?: 'OrderFullyRefunded' } | { __typename?: 'OrderMetadataUpdated' } | { __typename?: 'OrderPaid' } | { __typename?: 'OrderRefunded', issuedAt?: any | null, order?: { __typename?: 'Order', id: string, userEmail?: string | null, voucherCode?: string | null, user?: { __typename?: 'User', id: string } | null, channel: { __typename?: 'Channel', id: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, tax: { __typename?: 'Money', amount: number } }, undiscountedTotal: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, shippingPrice: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, quantity: number, voucherCode?: string | null, productSku?: string | null, totalPrice: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, variant?: { __typename?: 'ProductVariant', name: string, product: { __typename?: 'Product', name: string, category?: { __typename?: 'Category', name: string } | null } } | null }> } | null } | { __typename?: 'OrderUpdated' } | { __typename?: 'PageCreated' } | { __typename?: 'PageDeleted' } | { __typename?: 'PageTypeCreated' } | { __typename?: 'PageTypeDeleted' } | { __typename?: 'PageTypeUpdated' } | { __typename?: 'PageUpdated' } | { __typename?: 'PaymentAuthorize' } | { __typename?: 'PaymentCaptureEvent' } | { __typename?: 'PaymentConfirmEvent' } | { __typename?: 'PaymentGatewayInitializeSession' } | { __typename?: 'PaymentGatewayInitializeTokenizationSession' } | { __typename?: 'PaymentListGateways' } | { __typename?: 'PaymentMethodInitializeTokenizationSession' } | { __typename?: 'PaymentMethodProcessTokenizationSession' } | { __typename?: 'PaymentProcessEvent' } | { __typename?: 'PaymentRefundEvent' } | { __typename?: 'PaymentVoidEvent' } | { __typename?: 'PermissionGroupCreated' } | { __typename?: 'PermissionGroupDeleted' } | { __typename?: 'PermissionGroupUpdated' } | { __typename?: 'ProductCreated' } | { __typename?: 'ProductDeleted' } | { __typename?: 'ProductExportCompleted' } | { __typename?: 'ProductMediaCreated' } | { __typename?: 'ProductMediaDeleted' } | { __typename?: 'ProductMediaUpdated' } | { __typename?: 'ProductMetadataUpdated' } | { __typename?: 'ProductUpdated' } | { __typename?: 'ProductVariantBackInStock' } | { __typename?: 'ProductVariantCreated' } | { __typename?: 'ProductVariantDeleted' } | { __typename?: 'ProductVariantMetadataUpdated' } | { __typename?: 'ProductVariantOutOfStock' } | { __typename?: 'ProductVariantStockUpdated' } | { __typename?: 'ProductVariantUpdated' } | { __typename?: 'PromotionCreated' } | { __typename?: 'PromotionDeleted' } | { __typename?: 'PromotionEnded' } | { __typename?: 'PromotionRuleCreated' } | { __typename?: 'PromotionRuleDeleted' } | { __typename?: 'PromotionRuleUpdated' } | { __typename?: 'PromotionStarted' } | { __typename?: 'PromotionUpdated' } | { __typename?: 'SaleCreated' } | { __typename?: 'SaleDeleted' } | { __typename?: 'SaleToggle' } | { __typename?: 'SaleUpdated' } | { __typename?: 'ShippingListMethodsForCheckout' } | { __typename?: 'ShippingPriceCreated' } | { __typename?: 'ShippingPriceDeleted' } | { __typename?: 'ShippingPriceUpdated' } | { __typename?: 'ShippingZoneCreated' } | { __typename?: 'ShippingZoneDeleted' } | { __typename?: 'ShippingZoneMetadataUpdated' } | { __typename?: 'ShippingZoneUpdated' } | { __typename?: 'ShopMetadataUpdated' } | { __typename?: 'StaffCreated' } | { __typename?: 'StaffDeleted' } | { __typename?: 'StaffSetPasswordRequested' } | { __typename?: 'StaffUpdated' } | { __typename?: 'StoredPaymentMethodDeleteRequested' } | { __typename?: 'ThumbnailCreated' } | { __typename?: 'TransactionCancelationRequested' } | { __typename?: 'TransactionChargeRequested' } | { __typename?: 'TransactionInitializeSession' } | { __typename?: 'TransactionItemMetadataUpdated' } | { __typename?: 'TransactionProcessSession' } | { __typename?: 'TransactionRefundRequested' } | { __typename?: 'TranslationCreated' } | { __typename?: 'TranslationUpdated' } | { __typename?: 'VoucherCodeExportCompleted' } | { __typename?: 'VoucherCodesCreated' } | { __typename?: 'VoucherCodesDeleted' } | { __typename?: 'VoucherCreated' } | { __typename?: 'VoucherDeleted' } | { __typename?: 'VoucherMetadataUpdated' } | { __typename?: 'VoucherUpdated' } | { __typename?: 'WarehouseCreated' } | { __typename?: 'WarehouseDeleted' } | { __typename?: 'WarehouseMetadataUpdated' } | { __typename?: 'WarehouseUpdated' } | null }; -export type OrderUpdatedSubscriptionPayloadFragment = { __typename?: 'OrderUpdated', issuedAt?: any | null, order?: { __typename?: 'Order', id: string, userEmail?: string | null, shippingMethodName?: string | null, number: string, user?: { __typename?: 'User', id: string, email: string } | null, channel: { __typename?: 'Channel', id: string, slug: string, name: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, net: { __typename?: 'Money', currency: string, amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, productVariantId?: string | null, productSku?: string | null, variantName: string }> } | null }; +export type OrderUpdatedSubscriptionPayloadFragment = { __typename?: 'OrderUpdated', issuedAt?: any | null, order?: { __typename?: 'Order', id: string, userEmail?: string | null, voucherCode?: string | null, user?: { __typename?: 'User', id: string } | null, channel: { __typename?: 'Channel', id: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, tax: { __typename?: 'Money', amount: number } }, undiscountedTotal: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, shippingPrice: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, quantity: number, voucherCode?: string | null, productSku?: string | null, totalPrice: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, variant?: { __typename?: 'ProductVariant', name: string, product: { __typename?: 'Product', name: string, category?: { __typename?: 'Category', name: string } | null } } | null }> } | null }; export type OrderUpdatedSubscriptionVariables = Exact<{ [key: string]: never; }>; -export type OrderUpdatedSubscription = { __typename?: 'Subscription', event?: { __typename?: 'AccountChangeEmailRequested' } | { __typename?: 'AccountConfirmationRequested' } | { __typename?: 'AccountConfirmed' } | { __typename?: 'AccountDeleteRequested' } | { __typename?: 'AccountDeleted' } | { __typename?: 'AccountEmailChanged' } | { __typename?: 'AccountSetPasswordRequested' } | { __typename?: 'AddressCreated' } | { __typename?: 'AddressDeleted' } | { __typename?: 'AddressUpdated' } | { __typename?: 'AppDeleted' } | { __typename?: 'AppInstalled' } | { __typename?: 'AppStatusChanged' } | { __typename?: 'AppUpdated' } | { __typename?: 'AttributeCreated' } | { __typename?: 'AttributeDeleted' } | { __typename?: 'AttributeUpdated' } | { __typename?: 'AttributeValueCreated' } | { __typename?: 'AttributeValueDeleted' } | { __typename?: 'AttributeValueUpdated' } | { __typename?: 'CalculateTaxes' } | { __typename?: 'CategoryCreated' } | { __typename?: 'CategoryDeleted' } | { __typename?: 'CategoryUpdated' } | { __typename?: 'ChannelCreated' } | { __typename?: 'ChannelDeleted' } | { __typename?: 'ChannelMetadataUpdated' } | { __typename?: 'ChannelStatusChanged' } | { __typename?: 'ChannelUpdated' } | { __typename?: 'CheckoutCreated' } | { __typename?: 'CheckoutFilterShippingMethods' } | { __typename?: 'CheckoutFullyPaid' } | { __typename?: 'CheckoutMetadataUpdated' } | { __typename?: 'CheckoutUpdated' } | { __typename?: 'CollectionCreated' } | { __typename?: 'CollectionDeleted' } | { __typename?: 'CollectionMetadataUpdated' } | { __typename?: 'CollectionUpdated' } | { __typename?: 'CustomerCreated' } | { __typename?: 'CustomerMetadataUpdated' } | { __typename?: 'CustomerUpdated' } | { __typename?: 'DraftOrderCreated' } | { __typename?: 'DraftOrderDeleted' } | { __typename?: 'DraftOrderUpdated' } | { __typename?: 'FulfillmentApproved' } | { __typename?: 'FulfillmentCanceled' } | { __typename?: 'FulfillmentCreated' } | { __typename?: 'FulfillmentMetadataUpdated' } | { __typename?: 'FulfillmentTrackingNumberUpdated' } | { __typename?: 'GiftCardCreated' } | { __typename?: 'GiftCardDeleted' } | { __typename?: 'GiftCardExportCompleted' } | { __typename?: 'GiftCardMetadataUpdated' } | { __typename?: 'GiftCardSent' } | { __typename?: 'GiftCardStatusChanged' } | { __typename?: 'GiftCardUpdated' } | { __typename?: 'InvoiceDeleted' } | { __typename?: 'InvoiceRequested' } | { __typename?: 'InvoiceSent' } | { __typename?: 'ListStoredPaymentMethods' } | { __typename?: 'MenuCreated' } | { __typename?: 'MenuDeleted' } | { __typename?: 'MenuItemCreated' } | { __typename?: 'MenuItemDeleted' } | { __typename?: 'MenuItemUpdated' } | { __typename?: 'MenuUpdated' } | { __typename?: 'OrderBulkCreated' } | { __typename?: 'OrderCancelled' } | { __typename?: 'OrderConfirmed' } | { __typename?: 'OrderCreated' } | { __typename?: 'OrderExpired' } | { __typename?: 'OrderFilterShippingMethods' } | { __typename?: 'OrderFulfilled' } | { __typename?: 'OrderFullyPaid' } | { __typename?: 'OrderFullyRefunded' } | { __typename?: 'OrderMetadataUpdated' } | { __typename?: 'OrderPaid' } | { __typename?: 'OrderRefunded' } | { __typename?: 'OrderUpdated', issuedAt?: any | null, order?: { __typename?: 'Order', id: string, userEmail?: string | null, shippingMethodName?: string | null, number: string, user?: { __typename?: 'User', id: string, email: string } | null, channel: { __typename?: 'Channel', id: string, slug: string, name: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, net: { __typename?: 'Money', currency: string, amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, productVariantId?: string | null, productSku?: string | null, variantName: string }> } | null } | { __typename?: 'PageCreated' } | { __typename?: 'PageDeleted' } | { __typename?: 'PageTypeCreated' } | { __typename?: 'PageTypeDeleted' } | { __typename?: 'PageTypeUpdated' } | { __typename?: 'PageUpdated' } | { __typename?: 'PaymentAuthorize' } | { __typename?: 'PaymentCaptureEvent' } | { __typename?: 'PaymentConfirmEvent' } | { __typename?: 'PaymentGatewayInitializeSession' } | { __typename?: 'PaymentGatewayInitializeTokenizationSession' } | { __typename?: 'PaymentListGateways' } | { __typename?: 'PaymentMethodInitializeTokenizationSession' } | { __typename?: 'PaymentMethodProcessTokenizationSession' } | { __typename?: 'PaymentProcessEvent' } | { __typename?: 'PaymentRefundEvent' } | { __typename?: 'PaymentVoidEvent' } | { __typename?: 'PermissionGroupCreated' } | { __typename?: 'PermissionGroupDeleted' } | { __typename?: 'PermissionGroupUpdated' } | { __typename?: 'ProductCreated' } | { __typename?: 'ProductDeleted' } | { __typename?: 'ProductExportCompleted' } | { __typename?: 'ProductMediaCreated' } | { __typename?: 'ProductMediaDeleted' } | { __typename?: 'ProductMediaUpdated' } | { __typename?: 'ProductMetadataUpdated' } | { __typename?: 'ProductUpdated' } | { __typename?: 'ProductVariantBackInStock' } | { __typename?: 'ProductVariantCreated' } | { __typename?: 'ProductVariantDeleted' } | { __typename?: 'ProductVariantMetadataUpdated' } | { __typename?: 'ProductVariantOutOfStock' } | { __typename?: 'ProductVariantStockUpdated' } | { __typename?: 'ProductVariantUpdated' } | { __typename?: 'PromotionCreated' } | { __typename?: 'PromotionDeleted' } | { __typename?: 'PromotionEnded' } | { __typename?: 'PromotionRuleCreated' } | { __typename?: 'PromotionRuleDeleted' } | { __typename?: 'PromotionRuleUpdated' } | { __typename?: 'PromotionStarted' } | { __typename?: 'PromotionUpdated' } | { __typename?: 'SaleCreated' } | { __typename?: 'SaleDeleted' } | { __typename?: 'SaleToggle' } | { __typename?: 'SaleUpdated' } | { __typename?: 'ShippingListMethodsForCheckout' } | { __typename?: 'ShippingPriceCreated' } | { __typename?: 'ShippingPriceDeleted' } | { __typename?: 'ShippingPriceUpdated' } | { __typename?: 'ShippingZoneCreated' } | { __typename?: 'ShippingZoneDeleted' } | { __typename?: 'ShippingZoneMetadataUpdated' } | { __typename?: 'ShippingZoneUpdated' } | { __typename?: 'ShopMetadataUpdated' } | { __typename?: 'StaffCreated' } | { __typename?: 'StaffDeleted' } | { __typename?: 'StaffSetPasswordRequested' } | { __typename?: 'StaffUpdated' } | { __typename?: 'StoredPaymentMethodDeleteRequested' } | { __typename?: 'ThumbnailCreated' } | { __typename?: 'TransactionCancelationRequested' } | { __typename?: 'TransactionChargeRequested' } | { __typename?: 'TransactionInitializeSession' } | { __typename?: 'TransactionItemMetadataUpdated' } | { __typename?: 'TransactionProcessSession' } | { __typename?: 'TransactionRefundRequested' } | { __typename?: 'TranslationCreated' } | { __typename?: 'TranslationUpdated' } | { __typename?: 'VoucherCodeExportCompleted' } | { __typename?: 'VoucherCodesCreated' } | { __typename?: 'VoucherCodesDeleted' } | { __typename?: 'VoucherCreated' } | { __typename?: 'VoucherDeleted' } | { __typename?: 'VoucherMetadataUpdated' } | { __typename?: 'VoucherUpdated' } | { __typename?: 'WarehouseCreated' } | { __typename?: 'WarehouseDeleted' } | { __typename?: 'WarehouseMetadataUpdated' } | { __typename?: 'WarehouseUpdated' } | null }; +export type OrderUpdatedSubscription = { __typename?: 'Subscription', event?: { __typename?: 'AccountChangeEmailRequested' } | { __typename?: 'AccountConfirmationRequested' } | { __typename?: 'AccountConfirmed' } | { __typename?: 'AccountDeleteRequested' } | { __typename?: 'AccountDeleted' } | { __typename?: 'AccountEmailChanged' } | { __typename?: 'AccountSetPasswordRequested' } | { __typename?: 'AddressCreated' } | { __typename?: 'AddressDeleted' } | { __typename?: 'AddressUpdated' } | { __typename?: 'AppDeleted' } | { __typename?: 'AppInstalled' } | { __typename?: 'AppStatusChanged' } | { __typename?: 'AppUpdated' } | { __typename?: 'AttributeCreated' } | { __typename?: 'AttributeDeleted' } | { __typename?: 'AttributeUpdated' } | { __typename?: 'AttributeValueCreated' } | { __typename?: 'AttributeValueDeleted' } | { __typename?: 'AttributeValueUpdated' } | { __typename?: 'CalculateTaxes' } | { __typename?: 'CategoryCreated' } | { __typename?: 'CategoryDeleted' } | { __typename?: 'CategoryUpdated' } | { __typename?: 'ChannelCreated' } | { __typename?: 'ChannelDeleted' } | { __typename?: 'ChannelMetadataUpdated' } | { __typename?: 'ChannelStatusChanged' } | { __typename?: 'ChannelUpdated' } | { __typename?: 'CheckoutCreated' } | { __typename?: 'CheckoutFilterShippingMethods' } | { __typename?: 'CheckoutFullyPaid' } | { __typename?: 'CheckoutMetadataUpdated' } | { __typename?: 'CheckoutUpdated' } | { __typename?: 'CollectionCreated' } | { __typename?: 'CollectionDeleted' } | { __typename?: 'CollectionMetadataUpdated' } | { __typename?: 'CollectionUpdated' } | { __typename?: 'CustomerCreated' } | { __typename?: 'CustomerMetadataUpdated' } | { __typename?: 'CustomerUpdated' } | { __typename?: 'DraftOrderCreated' } | { __typename?: 'DraftOrderDeleted' } | { __typename?: 'DraftOrderUpdated' } | { __typename?: 'FulfillmentApproved' } | { __typename?: 'FulfillmentCanceled' } | { __typename?: 'FulfillmentCreated' } | { __typename?: 'FulfillmentMetadataUpdated' } | { __typename?: 'FulfillmentTrackingNumberUpdated' } | { __typename?: 'GiftCardCreated' } | { __typename?: 'GiftCardDeleted' } | { __typename?: 'GiftCardExportCompleted' } | { __typename?: 'GiftCardMetadataUpdated' } | { __typename?: 'GiftCardSent' } | { __typename?: 'GiftCardStatusChanged' } | { __typename?: 'GiftCardUpdated' } | { __typename?: 'InvoiceDeleted' } | { __typename?: 'InvoiceRequested' } | { __typename?: 'InvoiceSent' } | { __typename?: 'ListStoredPaymentMethods' } | { __typename?: 'MenuCreated' } | { __typename?: 'MenuDeleted' } | { __typename?: 'MenuItemCreated' } | { __typename?: 'MenuItemDeleted' } | { __typename?: 'MenuItemUpdated' } | { __typename?: 'MenuUpdated' } | { __typename?: 'OrderBulkCreated' } | { __typename?: 'OrderCancelled' } | { __typename?: 'OrderConfirmed' } | { __typename?: 'OrderCreated' } | { __typename?: 'OrderExpired' } | { __typename?: 'OrderFilterShippingMethods' } | { __typename?: 'OrderFulfilled' } | { __typename?: 'OrderFullyPaid' } | { __typename?: 'OrderFullyRefunded' } | { __typename?: 'OrderMetadataUpdated' } | { __typename?: 'OrderPaid' } | { __typename?: 'OrderRefunded' } | { __typename?: 'OrderUpdated', issuedAt?: any | null, order?: { __typename?: 'Order', id: string, userEmail?: string | null, voucherCode?: string | null, user?: { __typename?: 'User', id: string } | null, channel: { __typename?: 'Channel', id: string }, total: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number, currency: string }, tax: { __typename?: 'Money', amount: number } }, undiscountedTotal: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, shippingPrice: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, lines: Array<{ __typename?: 'OrderLine', id: string, quantity: number, voucherCode?: string | null, productSku?: string | null, totalPrice: { __typename?: 'TaxedMoney', gross: { __typename?: 'Money', amount: number } }, variant?: { __typename?: 'ProductVariant', name: string, product: { __typename?: 'Product', name: string, category?: { __typename?: 'Category', name: string } | null } } | null }> } | null } | { __typename?: 'PageCreated' } | { __typename?: 'PageDeleted' } | { __typename?: 'PageTypeCreated' } | { __typename?: 'PageTypeDeleted' } | { __typename?: 'PageTypeUpdated' } | { __typename?: 'PageUpdated' } | { __typename?: 'PaymentAuthorize' } | { __typename?: 'PaymentCaptureEvent' } | { __typename?: 'PaymentConfirmEvent' } | { __typename?: 'PaymentGatewayInitializeSession' } | { __typename?: 'PaymentGatewayInitializeTokenizationSession' } | { __typename?: 'PaymentListGateways' } | { __typename?: 'PaymentMethodInitializeTokenizationSession' } | { __typename?: 'PaymentMethodProcessTokenizationSession' } | { __typename?: 'PaymentProcessEvent' } | { __typename?: 'PaymentRefundEvent' } | { __typename?: 'PaymentVoidEvent' } | { __typename?: 'PermissionGroupCreated' } | { __typename?: 'PermissionGroupDeleted' } | { __typename?: 'PermissionGroupUpdated' } | { __typename?: 'ProductCreated' } | { __typename?: 'ProductDeleted' } | { __typename?: 'ProductExportCompleted' } | { __typename?: 'ProductMediaCreated' } | { __typename?: 'ProductMediaDeleted' } | { __typename?: 'ProductMediaUpdated' } | { __typename?: 'ProductMetadataUpdated' } | { __typename?: 'ProductUpdated' } | { __typename?: 'ProductVariantBackInStock' } | { __typename?: 'ProductVariantCreated' } | { __typename?: 'ProductVariantDeleted' } | { __typename?: 'ProductVariantMetadataUpdated' } | { __typename?: 'ProductVariantOutOfStock' } | { __typename?: 'ProductVariantStockUpdated' } | { __typename?: 'ProductVariantUpdated' } | { __typename?: 'PromotionCreated' } | { __typename?: 'PromotionDeleted' } | { __typename?: 'PromotionEnded' } | { __typename?: 'PromotionRuleCreated' } | { __typename?: 'PromotionRuleDeleted' } | { __typename?: 'PromotionRuleUpdated' } | { __typename?: 'PromotionStarted' } | { __typename?: 'PromotionUpdated' } | { __typename?: 'SaleCreated' } | { __typename?: 'SaleDeleted' } | { __typename?: 'SaleToggle' } | { __typename?: 'SaleUpdated' } | { __typename?: 'ShippingListMethodsForCheckout' } | { __typename?: 'ShippingPriceCreated' } | { __typename?: 'ShippingPriceDeleted' } | { __typename?: 'ShippingPriceUpdated' } | { __typename?: 'ShippingZoneCreated' } | { __typename?: 'ShippingZoneDeleted' } | { __typename?: 'ShippingZoneMetadataUpdated' } | { __typename?: 'ShippingZoneUpdated' } | { __typename?: 'ShopMetadataUpdated' } | { __typename?: 'StaffCreated' } | { __typename?: 'StaffDeleted' } | { __typename?: 'StaffSetPasswordRequested' } | { __typename?: 'StaffUpdated' } | { __typename?: 'StoredPaymentMethodDeleteRequested' } | { __typename?: 'ThumbnailCreated' } | { __typename?: 'TransactionCancelationRequested' } | { __typename?: 'TransactionChargeRequested' } | { __typename?: 'TransactionInitializeSession' } | { __typename?: 'TransactionItemMetadataUpdated' } | { __typename?: 'TransactionProcessSession' } | { __typename?: 'TransactionRefundRequested' } | { __typename?: 'TranslationCreated' } | { __typename?: 'TranslationUpdated' } | { __typename?: 'VoucherCodeExportCompleted' } | { __typename?: 'VoucherCodesCreated' } | { __typename?: 'VoucherCodesDeleted' } | { __typename?: 'VoucherCreated' } | { __typename?: 'VoucherDeleted' } | { __typename?: 'VoucherMetadataUpdated' } | { __typename?: 'VoucherUpdated' } | { __typename?: 'WarehouseCreated' } | { __typename?: 'WarehouseDeleted' } | { __typename?: 'WarehouseMetadataUpdated' } | { __typename?: 'WarehouseUpdated' } | null }; export const UntypedOrderBaseFragmentDoc = gql` fragment OrderBase on Order { id user { id - email } channel { id - slug - name } userEmail - shippingMethodName total { gross { amount currency } - net { - currency + tax { + amount + } + } + undiscountedTotal { + gross { amount } } + shippingPrice { + gross { + amount + } + } + voucherCode lines { id - productVariantId + quantity + totalPrice { + gross { + amount + } + } + voucherCode productSku - variantName + variant { + name + product { + name + category { + name + } + } + } } - number } `; export const UntypedOrderCancelledSubscriptionPayloadFragmentDoc = gql` @@ -33198,16 +33210,8 @@ export const UntypedOrderCancelledSubscriptionPayloadFragmentDoc = gql` } } ${UntypedOrderBaseFragmentDoc}`; -export const UntypedOrderCreatedSubscriptionPayloadFragmentDoc = gql` - fragment OrderCreatedSubscriptionPayload on OrderCreated { - issuedAt - order { - ...OrderBase - } -} - ${UntypedOrderBaseFragmentDoc}`; -export const UntypedOrderFullyPaidSubscriptionPayloadFragmentDoc = gql` - fragment OrderFullyPaidSubscriptionPayload on OrderFullyPaid { +export const UntypedOrderConfirmedSubscriptionPayloadFragmentDoc = gql` + fragment OrderConfirmedSubscriptionPayload on OrderConfirmed { issuedAt order { ...OrderBase @@ -33285,27 +33289,16 @@ export const UntypedOrderCancelledDocument = gql` export function useOrderCancelledSubscription(options?: Omit, 'query'>, handler?: Urql.SubscriptionHandler) { return Urql.useSubscription({ query: UntypedOrderCancelledDocument, ...options }, handler); }; -export const UntypedOrderCreatedDocument = gql` - subscription OrderCreated { - event { - ...OrderCreatedSubscriptionPayload - } -} - ${UntypedOrderCreatedSubscriptionPayloadFragmentDoc}`; - -export function useOrderCreatedSubscription(options?: Omit, 'query'>, handler?: Urql.SubscriptionHandler) { - return Urql.useSubscription({ query: UntypedOrderCreatedDocument, ...options }, handler); -}; -export const UntypedOrderFullyPaidDocument = gql` - subscription OrderFullyPaid { +export const UntypedOrderConfirmedDocument = gql` + subscription OrderConfirmed { event { - ...OrderFullyPaidSubscriptionPayload + ...OrderConfirmedSubscriptionPayload } } - ${UntypedOrderFullyPaidSubscriptionPayloadFragmentDoc}`; + ${UntypedOrderConfirmedSubscriptionPayloadFragmentDoc}`; -export function useOrderFullyPaidSubscription(options?: Omit, 'query'>, handler?: Urql.SubscriptionHandler) { - return Urql.useSubscription({ query: UntypedOrderFullyPaidDocument, ...options }, handler); +export function useOrderConfirmedSubscription(options?: Omit, 'query'>, handler?: Urql.SubscriptionHandler) { + return Urql.useSubscription({ query: UntypedOrderConfirmedDocument, ...options }, handler); }; export const UntypedOrderRefundedDocument = gql` subscription OrderRefunded { @@ -33329,17 +33322,15 @@ export const UntypedOrderUpdatedDocument = gql` export function useOrderUpdatedSubscription(options?: Omit, 'query'>, handler?: Urql.SubscriptionHandler) { return Urql.useSubscription({ query: UntypedOrderUpdatedDocument, ...options }, handler); }; -export const OrderBaseFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"shippingMethodName"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"net"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"productVariantId"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variantName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"number"}}]}}]} as unknown as DocumentNode; -export const OrderCancelledSubscriptionPayloadFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderCancelledSubscriptionPayload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderCancelled"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderBase"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"shippingMethodName"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"net"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"productVariantId"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variantName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"number"}}]}}]} as unknown as DocumentNode; -export const OrderCreatedSubscriptionPayloadFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderCreatedSubscriptionPayload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderCreated"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderBase"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"shippingMethodName"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"net"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"productVariantId"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variantName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"number"}}]}}]} as unknown as DocumentNode; -export const OrderFullyPaidSubscriptionPayloadFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderFullyPaidSubscriptionPayload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderFullyPaid"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderBase"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"shippingMethodName"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"net"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"productVariantId"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variantName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"number"}}]}}]} as unknown as DocumentNode; -export const OrderRefundedSubscriptionPayloadFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderRefundedSubscriptionPayload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderRefunded"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderBase"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"shippingMethodName"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"net"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"productVariantId"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variantName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"number"}}]}}]} as unknown as DocumentNode; -export const OrderUpdatedSubscriptionPayloadFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderUpdatedSubscriptionPayload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderUpdated"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderBase"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"shippingMethodName"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"net"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"productVariantId"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variantName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"number"}}]}}]} as unknown as DocumentNode; +export const OrderBaseFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tax"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"undiscountedTotal"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"shippingPrice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"voucherCode"}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"totalPrice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"voucherCode"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"product"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"category"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const OrderCancelledSubscriptionPayloadFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderCancelledSubscriptionPayload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderCancelled"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderBase"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tax"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"undiscountedTotal"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"shippingPrice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"voucherCode"}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"totalPrice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"voucherCode"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"product"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"category"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const OrderConfirmedSubscriptionPayloadFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderConfirmedSubscriptionPayload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderConfirmed"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderBase"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tax"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"undiscountedTotal"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"shippingPrice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"voucherCode"}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"totalPrice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"voucherCode"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"product"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"category"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const OrderRefundedSubscriptionPayloadFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderRefundedSubscriptionPayload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderRefunded"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderBase"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tax"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"undiscountedTotal"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"shippingPrice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"voucherCode"}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"totalPrice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"voucherCode"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"product"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"category"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const OrderUpdatedSubscriptionPayloadFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderUpdatedSubscriptionPayload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderUpdated"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderBase"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tax"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"undiscountedTotal"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"shippingPrice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"voucherCode"}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"totalPrice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"voucherCode"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"product"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"category"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const EnableWebhookDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"EnableWebhook"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"webhookUpdate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"isActive"},"value":{"kind":"BooleanValue","value":true}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"errors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"code"}}]}}]}}]}}]} as unknown as DocumentNode; export const UpdateAppMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateAppMetadata"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MetadataInput"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updatePrivateMetadata"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"item"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"privateMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const FetchAppWebhooksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FetchAppWebhooks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"webhooks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}}]}}]}}]}}]} as unknown as DocumentNode; -export const OrderCancelledDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OrderCancelled"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderCancelledSubscriptionPayload"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"shippingMethodName"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"net"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"productVariantId"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variantName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"number"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderCancelledSubscriptionPayload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderCancelled"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderBase"}}]}}]}}]} as unknown as DocumentNode; -export const OrderCreatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OrderCreated"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderCreatedSubscriptionPayload"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"shippingMethodName"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"net"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"productVariantId"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variantName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"number"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderCreatedSubscriptionPayload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderCreated"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderBase"}}]}}]}}]} as unknown as DocumentNode; -export const OrderFullyPaidDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OrderFullyPaid"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderFullyPaidSubscriptionPayload"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"shippingMethodName"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"net"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"productVariantId"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variantName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"number"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderFullyPaidSubscriptionPayload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderFullyPaid"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderBase"}}]}}]}}]} as unknown as DocumentNode; -export const OrderRefundedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OrderRefunded"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderRefundedSubscriptionPayload"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"shippingMethodName"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"net"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"productVariantId"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variantName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"number"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderRefundedSubscriptionPayload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderRefunded"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderBase"}}]}}]}}]} as unknown as DocumentNode; -export const OrderUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OrderUpdated"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderUpdatedSubscriptionPayload"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"shippingMethodName"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"net"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"productVariantId"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variantName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"number"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderUpdatedSubscriptionPayload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderUpdated"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderBase"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const OrderCancelledDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OrderCancelled"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderCancelledSubscriptionPayload"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tax"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"undiscountedTotal"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"shippingPrice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"voucherCode"}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"totalPrice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"voucherCode"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"product"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"category"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderCancelledSubscriptionPayload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderCancelled"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderBase"}}]}}]}}]} as unknown as DocumentNode; +export const OrderConfirmedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OrderConfirmed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderConfirmedSubscriptionPayload"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tax"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"undiscountedTotal"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"shippingPrice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"voucherCode"}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"totalPrice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"voucherCode"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"product"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"category"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderConfirmedSubscriptionPayload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderConfirmed"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderBase"}}]}}]}}]} as unknown as DocumentNode; +export const OrderRefundedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OrderRefunded"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderRefundedSubscriptionPayload"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tax"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"undiscountedTotal"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"shippingPrice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"voucherCode"}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"totalPrice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"voucherCode"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"product"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"category"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderRefundedSubscriptionPayload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderRefunded"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderBase"}}]}}]}}]} as unknown as DocumentNode; +export const OrderUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OrderUpdated"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderUpdatedSubscriptionPayload"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderBase"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"channel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userEmail"}},{"kind":"Field","name":{"kind":"Name","value":"total"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"currency"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tax"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"undiscountedTotal"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"shippingPrice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"voucherCode"}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"totalPrice"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"gross"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"amount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"voucherCode"}},{"kind":"Field","name":{"kind":"Name","value":"productSku"}},{"kind":"Field","name":{"kind":"Name","value":"variant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"product"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"category"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderUpdatedSubscriptionPayload"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderUpdated"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderBase"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/apps/segment/graphql/fragments/order-base.graphql b/apps/segment/graphql/fragments/order-base.graphql index acb356b236..9b785c908b 100644 --- a/apps/segment/graphql/fragments/order-base.graphql +++ b/apps/segment/graphql/fragments/order-base.graphql @@ -2,30 +2,49 @@ fragment OrderBase on Order { id user { id - email } channel { id - slug - name } userEmail - shippingMethodName total { gross { amount currency } - net { - currency + tax { + amount + } + } + undiscountedTotal { + gross { amount } } + shippingPrice { + gross { + amount + } + } + voucherCode lines { id - productVariantId + quantity + totalPrice { + gross { + amount + } + } + voucherCode productSku - variantName + variant { + name + product { + name + category { + name + } + } + } } - number } diff --git a/apps/segment/graphql/subscriptions/order-confirmed.graphql b/apps/segment/graphql/subscriptions/order-confirmed.graphql new file mode 100644 index 0000000000..6fc7aa840c --- /dev/null +++ b/apps/segment/graphql/subscriptions/order-confirmed.graphql @@ -0,0 +1,12 @@ +fragment OrderConfirmedSubscriptionPayload on OrderConfirmed { + issuedAt + order { + ...OrderBase + } +} + +subscription OrderConfirmed { + event { + ...OrderConfirmedSubscriptionPayload + } +} diff --git a/apps/segment/graphql/subscriptions/order-created.graphql b/apps/segment/graphql/subscriptions/order-created.graphql deleted file mode 100644 index 2f68d12e9b..0000000000 --- a/apps/segment/graphql/subscriptions/order-created.graphql +++ /dev/null @@ -1,12 +0,0 @@ -fragment OrderCreatedSubscriptionPayload on OrderCreated { - issuedAt - order { - ...OrderBase - } -} - -subscription OrderCreated { - event { - ...OrderCreatedSubscriptionPayload - } -} diff --git a/apps/segment/graphql/subscriptions/order-fully-paid.graphql b/apps/segment/graphql/subscriptions/order-fully-paid.graphql deleted file mode 100644 index a2218aad0e..0000000000 --- a/apps/segment/graphql/subscriptions/order-fully-paid.graphql +++ /dev/null @@ -1,12 +0,0 @@ -fragment OrderFullyPaidSubscriptionPayload on OrderFullyPaid { - issuedAt - order { - ...OrderBase - } -} - -subscription OrderFullyPaid { - event { - ...OrderFullyPaidSubscriptionPayload - } -} diff --git a/apps/segment/package.json b/apps/segment/package.json index a056104330..553fbe805e 100644 --- a/apps/segment/package.json +++ b/apps/segment/package.json @@ -52,6 +52,7 @@ "@trpc/server": "10.43.1", "@urql/exchange-auth": "2.1.4", "@vitejs/plugin-react": "4.3.1", + "decimal.js-light": "2.5.1", "dotenv": "16.3.1", "dynamodb-toolbox": "1.8.2", "graphql": "16.7.1", diff --git a/apps/segment/src/lib/filter-empty-values-from-object.test.ts b/apps/segment/src/lib/filter-empty-values-from-object.test.ts new file mode 100644 index 0000000000..1e2f2a2d33 --- /dev/null +++ b/apps/segment/src/lib/filter-empty-values-from-object.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; + +import { filterEmptyValuesFromObject } from "./filter-empty-values-from-object"; + +describe("filterEmptyValues", () => { + it("removes null and undefined values", () => { + const input = { + a: 1, + b: null, + c: undefined, + d: "test", + }; + + expect(filterEmptyValuesFromObject(input)).toMatchInlineSnapshot(` + { + "a": 1, + "d": "test", + } + `); + }); + + it("should remove null or undefined from nested objects", () => { + const input = { + a: 1, + b: { + c: null, + d: undefined, + e: "test", + arry: [ + { + key: undefined, + value: null, + one: 1, + }, + ], + }, + }; + + expect(filterEmptyValuesFromObject(input)).toMatchInlineSnapshot(` + { + "a": 1, + "b": { + "arry": [ + { + "one": 1, + }, + ], + "e": "test", + }, + } + `); + }); +}); diff --git a/apps/segment/src/lib/filter-empty-values-from-object.ts b/apps/segment/src/lib/filter-empty-values-from-object.ts new file mode 100644 index 0000000000..d4d8fa2227 --- /dev/null +++ b/apps/segment/src/lib/filter-empty-values-from-object.ts @@ -0,0 +1,47 @@ +type JsonValue = string | number | boolean | null | undefined | JsonObject | JsonValue[]; +type JsonObject = { [key: string]: JsonValue }; + +/** + * Recursively removes null and undefined values from an object including nested objects and arrays. + * + * @param {T} obj - The object to filter + * @returns {Partial} A new object with null and undefined values removed + * + * @example + * // Simple object + * filterEmptyValuesFromObject({ a: 1, b: null, c: undefined }) + * // Returns: { a: 1 } + * + * @example + * // Nested object with array + * filterEmptyValuesFromObject({ + * items: [ + * { id: 1, value: null }, + * { id: 2, value: 'test' } + * ] + * }) + * // Returns: { items: [{ id: 1 }, { id: 2, value: 'test' }] } + */ +export const filterEmptyValuesFromObject = (obj: T): Partial => { + if (!obj || typeof obj !== "object") return obj; + + return Object.fromEntries( + Object.entries(obj) + .filter(([_, value]) => value !== null && value !== undefined) + .map(([key, value]) => { + if (Array.isArray(value)) { + const filtered = value + .filter((item) => item !== null && item !== undefined) + .map((item) => + typeof item === "object" ? filterEmptyValuesFromObject(item as JsonObject) : item, + ); + + return [key, filtered]; + } + if (typeof value === "object") { + return [key, filterEmptyValuesFromObject(value as JsonObject)]; + } + return [key, value]; + }), + ) as Partial; +}; diff --git a/apps/segment/src/modules/tracking-events/__tests__/mocks.ts b/apps/segment/src/modules/tracking-events/__tests__/mocks.ts index b7c08fc1af..e7137bfad4 100644 --- a/apps/segment/src/modules/tracking-events/__tests__/mocks.ts +++ b/apps/segment/src/modules/tracking-events/__tests__/mocks.ts @@ -2,22 +2,48 @@ import { OrderBaseFragment } from "@/generated/graphql"; export const mockedOrderBase: OrderBaseFragment = { id: "order-id", - userEmail: "user-email", - number: "order-number", channel: { id: "channel-id", - slug: "channel-slug", - name: "channel-name", }, - lines: [], + userEmail: "user-email", total: { gross: { amount: 37, currency: "USD", }, - net: { - amount: 21, - currency: "USD", + tax: { + amount: 0.21, + }, + }, + undiscountedTotal: { + gross: { + amount: 30, }, }, + shippingPrice: { + gross: { + amount: 5, + }, + }, + lines: [ + { + id: "line-id", + quantity: 1, + totalPrice: { + gross: { + amount: 37, + }, + }, + productSku: "sku", + variant: { + name: "variantName", + product: { + name: "productName", + category: { + name: "categoryName", + }, + }, + }, + }, + ], }; diff --git a/apps/segment/src/modules/tracking-events/track-event.use-case.test.ts b/apps/segment/src/modules/tracking-events/track-event.use-case.test.ts index ebe038ba1e..3e80cb6cf4 100644 --- a/apps/segment/src/modules/tracking-events/track-event.use-case.test.ts +++ b/apps/segment/src/modules/tracking-events/track-event.use-case.test.ts @@ -35,7 +35,7 @@ describe("TrackEventUseCase", () => { segmentEventTrackerFactory: mockedSegmentEventTrackerFactory, }); - const event = trackingEventFactory.createOrderCreatedEvent({ + const event = trackingEventFactory.createOrderUpdatedEvent({ orderBase: mockedOrderBase, issuedAt: "2025-01-07", }); @@ -43,27 +43,29 @@ describe("TrackEventUseCase", () => { await useCase.track(event, mockedAppConfig); expect(mockedSegmentClient.track).toHaveBeenCalledWith({ - event: "Saleor Order Created", + event: "Saleor Order Updated", issuedAt: "2025-01-07", properties: { - channel: { - id: "channel-id", - name: "channel-name", - slug: "channel-slug", - }, - id: "order-id", - lines: [], - number: "order-number", - total: { - gross: { - amount: 37, - currency: "USD", + coupon: undefined, + currency: "USD", + discount: 7, + order_id: "order-id", + channel_id: "channel-id", + products: [ + { + category: "categoryName", + coupon: undefined, + name: "productName", + price: 37, + product_id: "line-id", + quantity: 1, + sku: "sku", + variant: "variantName", }, - net: { - amount: 21, - currency: "USD", - }, - }, + ], + shipping: 5, + tax: 0.21, + total: 37, }, user: { id: "user-email", @@ -83,7 +85,7 @@ describe("TrackEventUseCase", () => { segmentEventTrackerFactory: mockedSegmentEventTrackerFactory, }); - const event = trackingEventFactory.createOrderCreatedEvent({ + const event = trackingEventFactory.createOrderUpdatedEvent({ orderBase: mockedOrderBase, issuedAt: "2025-01-07", }); @@ -113,7 +115,7 @@ describe("TrackEventUseCase", () => { segmentEventTrackerFactory: mockedSegmentEventTrackerFactory, }); - const event = trackingEventFactory.createOrderCreatedEvent({ + const event = trackingEventFactory.createOrderUpdatedEvent({ orderBase: mockedOrderBase, issuedAt: "2025-01-07", }); diff --git a/apps/segment/src/modules/tracking-events/tracking-events.test.ts b/apps/segment/src/modules/tracking-events/tracking-events.test.ts index f85e0187d7..ae140d11b6 100644 --- a/apps/segment/src/modules/tracking-events/tracking-events.test.ts +++ b/apps/segment/src/modules/tracking-events/tracking-events.test.ts @@ -4,10 +4,10 @@ import { mockedOrderBase } from "./__tests__/mocks"; import { trackingEventFactory } from "./tracking-events"; describe("trackingEventFactory", () => { - it("should create event for order created with anonymous user if user data in not present", () => { + it("should create event for order updated with anonymous user if user data in not present", () => { vi.mock("uuid", () => ({ v4: () => "2137" })); - const event = trackingEventFactory.createOrderCreatedEvent({ + const event = trackingEventFactory.createOrderUpdatedEvent({ orderBase: { ...mockedOrderBase, userEmail: undefined }, issuedAt: "2025-01-07", }); @@ -16,26 +16,26 @@ describe("trackingEventFactory", () => { { "issuedAt": "2025-01-07", "payload": { - "channel": { - "id": "channel-id", - "name": "channel-name", - "slug": "channel-slug", - }, - "id": "order-id", - "lines": [], - "number": "order-number", - "total": { - "gross": { - "amount": 37, - "currency": "USD", + "channel_id": "channel-id", + "currency": "USD", + "discount": 7, + "order_id": "order-id", + "products": [ + { + "category": "categoryName", + "name": "productName", + "price": 37, + "product_id": "line-id", + "quantity": 1, + "sku": "sku", + "variant": "variantName", }, - "net": { - "amount": 21, - "currency": "USD", - }, - }, + ], + "shipping": 5, + "tax": 0.21, + "total": 37, }, - "type": "Saleor Order Created", + "type": "Saleor Order Updated", "user": { "id": "2137", "type": "anonymous", @@ -44,8 +44,8 @@ describe("trackingEventFactory", () => { `); }); - it("should create event for order created with user email if user information is present", () => { - const event = trackingEventFactory.createOrderCreatedEvent({ + it("should create event for order updated with user email if user information is present", () => { + const event = trackingEventFactory.createOrderUpdatedEvent({ orderBase: mockedOrderBase, issuedAt: "2025-01-07", }); @@ -54,26 +54,26 @@ describe("trackingEventFactory", () => { { "issuedAt": "2025-01-07", "payload": { - "channel": { - "id": "channel-id", - "name": "channel-name", - "slug": "channel-slug", - }, - "id": "order-id", - "lines": [], - "number": "order-number", - "total": { - "gross": { - "amount": 37, - "currency": "USD", - }, - "net": { - "amount": 21, - "currency": "USD", + "channel_id": "channel-id", + "currency": "USD", + "discount": 7, + "order_id": "order-id", + "products": [ + { + "category": "categoryName", + "name": "productName", + "price": 37, + "product_id": "line-id", + "quantity": 1, + "sku": "sku", + "variant": "variantName", }, - }, + ], + "shipping": 5, + "tax": 0.21, + "total": 37, }, - "type": "Saleor Order Created", + "type": "Saleor Order Updated", "user": { "id": "user-email", "type": "logged", @@ -81,4 +81,15 @@ describe("trackingEventFactory", () => { } `); }); + + it("should calculate total discount for order updated based of total & undiscountedTotal", () => { + const event = trackingEventFactory.createOrderUpdatedEvent({ + orderBase: mockedOrderBase, + issuedAt: "2025-01-07", + }); + + expect(event.payload.discount).toBe( + mockedOrderBase.total.gross.amount - mockedOrderBase.undiscountedTotal.gross.amount, + ); + }); }); diff --git a/apps/segment/src/modules/tracking-events/tracking-events.ts b/apps/segment/src/modules/tracking-events/tracking-events.ts index dbddb57766..d4e596df3f 100644 --- a/apps/segment/src/modules/tracking-events/tracking-events.ts +++ b/apps/segment/src/modules/tracking-events/tracking-events.ts @@ -1,7 +1,9 @@ +import Decimal from "decimal.js-light"; import { v4 as uuidv4 } from "uuid"; import { z } from "zod"; import { OrderBaseFragment } from "@/generated/graphql"; +import { filterEmptyValuesFromObject } from "@/lib/filter-empty-values-from-object"; export type TrackingBaseEvent = { type: string; @@ -34,29 +36,28 @@ const getUserInfo = ({ user, userEmail }: OrderBaseFragment) => { } as const; }; +const getProductInfo = (line: OrderBaseFragment["lines"][number]) => ({ + product_id: line.id, + sku: line.productSku, + category: line.variant?.product.category?.name, + name: line.variant?.product.name, + variant: line.variant?.name, + price: line.totalPrice.gross.amount, + quantity: line.quantity, + coupon: line.voucherCode, +}); + +const getDiscount = (args: { + total: OrderBaseFragment["total"]; + undiscountedTotal: OrderBaseFragment["undiscountedTotal"]; +}) => new Decimal(args.total.gross.amount).sub(args.undiscountedTotal.gross.amount).toNumber(); + /** * Semantic events from Segment: * https://segment.com/docs/connections/spec/ecommerce/v2/ */ export const trackingEventFactory = { - createOrderCreatedEvent({ - orderBase, - issuedAt, - }: { - orderBase: OrderBaseFragment; - issuedAt: string | null | undefined; - }): TrackingBaseEvent { - const { user, userEmail, ...order } = orderBase; - - return { - type: "Saleor Order Created", - user: getUserInfo(orderBase), - issuedAt, - payload: { - ...order, - }, - }; - }, + // https://segment.com/docs/connections/spec/ecommerce/v2/#order-updated createOrderUpdatedEvent({ orderBase, issuedAt, @@ -70,11 +71,23 @@ export const trackingEventFactory = { type: "Saleor Order Updated", user: getUserInfo(orderBase), issuedAt, - payload: { - ...order, - }, + payload: filterEmptyValuesFromObject({ + order_id: order.id, + channel_id: order.channel.id, + total: order.total.gross.amount, + shipping: order.shippingPrice.gross.amount, + tax: order.total.tax.amount, + discount: getDiscount({ + total: order.total, + undiscountedTotal: order.undiscountedTotal, + }), + coupon: order.voucherCode, + currency: order.total.gross.currency, + products: order.lines.map(getProductInfo), + }), }; }, + // https://segment.com/docs/connections/spec/ecommerce/v2/#order-cancelled createOrderCancelledEvent({ orderBase, issuedAt, @@ -88,11 +101,23 @@ export const trackingEventFactory = { type: "Saleor Order Cancelled", user: getUserInfo(orderBase), issuedAt, - payload: { - ...order, - }, + payload: filterEmptyValuesFromObject({ + order_id: order.id, + channel_id: order.channel.id, + total: order.total.gross.amount, + shipping: order.shippingPrice?.gross.amount, + tax: order.total.tax.amount, + discount: getDiscount({ + total: order.total, + undiscountedTotal: order.undiscountedTotal, + }), + coupon: order.voucherCode, + currency: order.total.gross.currency, + products: order.lines.map(getProductInfo), + }), }; }, + // https://segment.com/docs/connections/spec/ecommerce/v2/#order-refunded createOrderRefundedEvent({ orderBase, issuedAt, @@ -106,11 +131,14 @@ export const trackingEventFactory = { type: "Saleor Order Refunded", user: getUserInfo(orderBase), issuedAt, - payload: { - ...order, - }, + payload: filterEmptyValuesFromObject({ + order_id: order.id, + channel_id: order.channel.id, + products: order.lines.map(getProductInfo), + }), }; }, + // https://segment.com/docs/connections/spec/ecommerce/v2/#order-completed createOrderCompletedEvent({ orderBase, issuedAt, @@ -124,9 +152,20 @@ export const trackingEventFactory = { type: "Saleor Order Completed", user: getUserInfo(orderBase), issuedAt, - payload: { - ...order, - }, + payload: filterEmptyValuesFromObject({ + order_id: order.id, + channel_id: order.channel.id, + total: order.total.gross.amount, + shipping: order.shippingPrice?.gross.amount, + tax: order.total.tax.amount, + discount: getDiscount({ + total: order.total, + undiscountedTotal: order.undiscountedTotal, + }), + coupon: order.voucherCode, + currency: order.total.gross.currency, + products: order.lines.map(getProductInfo), + }), }; }, }; diff --git a/apps/segment/src/modules/webhooks/definitions/order-confirmed.ts b/apps/segment/src/modules/webhooks/definitions/order-confirmed.ts new file mode 100644 index 0000000000..7df84ece1f --- /dev/null +++ b/apps/segment/src/modules/webhooks/definitions/order-confirmed.ts @@ -0,0 +1,20 @@ +import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; + +import { + OrderConfirmedDocument, + OrderConfirmedSubscriptionPayloadFragment, +} from "@/generated/graphql"; +import { saleorApp } from "@/saleor-app"; + +export const orderConfirmedAsyncWebhook = + new SaleorAsyncWebhook({ + name: "Order Confirmed", + webhookPath: "api/webhooks/order-confirmed", + event: "ORDER_CONFIRMED", + apl: saleorApp.apl, + query: OrderConfirmedDocument, + /** + * Webhook is disabled by default. Will be enabled by the app when configuration succeeds + */ + isActive: false, + }); diff --git a/apps/segment/src/modules/webhooks/definitions/order-created.ts b/apps/segment/src/modules/webhooks/definitions/order-created.ts deleted file mode 100644 index 9e4d143a88..0000000000 --- a/apps/segment/src/modules/webhooks/definitions/order-created.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; - -import { OrderCreatedDocument, OrderCreatedSubscriptionPayloadFragment } from "@/generated/graphql"; -import { saleorApp } from "@/saleor-app"; - -export const orderCreatedAsyncWebhook = - new SaleorAsyncWebhook({ - name: "Order Created", - webhookPath: "api/webhooks/order-created", - event: "ORDER_CREATED", - apl: saleorApp.apl, - query: OrderCreatedDocument, - /** - * Webhook is disabled by default. Will be enabled by the app when configuration succeeds - */ - isActive: false, - }); diff --git a/apps/segment/src/modules/webhooks/definitions/order-fully-paid.ts b/apps/segment/src/modules/webhooks/definitions/order-fully-paid.ts deleted file mode 100644 index 668e62f3a8..0000000000 --- a/apps/segment/src/modules/webhooks/definitions/order-fully-paid.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; - -import { - OrderFullyPaidDocument, - OrderFullyPaidSubscriptionPayloadFragment, -} from "@/generated/graphql"; -import { saleorApp } from "@/saleor-app"; - -export const orderFullyPaidAsyncWebhook = - new SaleorAsyncWebhook({ - name: "Order Fully Paid", - webhookPath: "api/webhooks/order-fully-paid", - event: "ORDER_FULLY_PAID", - apl: saleorApp.apl, - query: OrderFullyPaidDocument, - /** - * Webhook is disabled by default. Will be enabled by the app when configuration succeeds - */ - isActive: false, - }); diff --git a/apps/segment/src/modules/webhooks/webhooks.ts b/apps/segment/src/modules/webhooks/webhooks.ts index e2b74ba8e7..1fc96cd089 100644 --- a/apps/segment/src/modules/webhooks/webhooks.ts +++ b/apps/segment/src/modules/webhooks/webhooks.ts @@ -1,13 +1,11 @@ import { orderCancelledAsyncWebhook } from "./definitions/order-cancelled"; -import { orderCreatedAsyncWebhook } from "./definitions/order-created"; -import { orderFullyPaidAsyncWebhook } from "./definitions/order-fully-paid"; +import { orderConfirmedAsyncWebhook } from "./definitions/order-confirmed"; import { orderRefundedAsyncWebhook } from "./definitions/order-refunded"; import { orderUpdatedAsyncWebhook } from "./definitions/order-updated"; export const appWebhooks = [ orderCancelledAsyncWebhook, - orderCreatedAsyncWebhook, - orderFullyPaidAsyncWebhook, + orderConfirmedAsyncWebhook, orderRefundedAsyncWebhook, orderUpdatedAsyncWebhook, ]; diff --git a/apps/segment/src/pages/api/webhooks/order-fully-paid.ts b/apps/segment/src/pages/api/webhooks/order-confirmed.ts similarity index 79% rename from apps/segment/src/pages/api/webhooks/order-fully-paid.ts rename to apps/segment/src/pages/api/webhooks/order-confirmed.ts index 63d7a48628..b216af9222 100644 --- a/apps/segment/src/pages/api/webhooks/order-fully-paid.ts +++ b/apps/segment/src/pages/api/webhooks/order-confirmed.ts @@ -3,7 +3,7 @@ import { wrapWithLoggerContext } from "@saleor/apps-logger/node"; import { withOtel } from "@saleor/apps-otel"; import { ObservabilityAttributes } from "@saleor/apps-otel/src/lib/observability-attributes"; -import { OrderFullyPaidSubscriptionPayloadFragment } from "@/generated/graphql"; +import { OrderConfirmedSubscriptionPayloadFragment } from "@/generated/graphql"; import { createLogger } from "@/logger"; import { loggerContext } from "@/logger-context"; import { DynamoAppConfigManager } from "@/modules/configuration/dynamo-app-config-manager"; @@ -11,7 +11,7 @@ import { DynamoConfigRepositoryFactory } from "@/modules/db/dynamo-config-factor import { SegmentEventTrackerFactory } from "@/modules/segment/segment-event-tracker-factory"; import { TrackEventUseCase } from "@/modules/tracking-events/track-event.use-case"; import { trackingEventFactory } from "@/modules/tracking-events/tracking-events"; -import { orderFullyPaidAsyncWebhook } from "@/modules/webhooks/definitions/order-fully-paid"; +import { orderConfirmedAsyncWebhook } from "@/modules/webhooks/definitions/order-confirmed"; export const config = { api: { @@ -26,7 +26,7 @@ const configManager = DynamoAppConfigManager.create(configRepository); const segmentEventTrackerFactory = new SegmentEventTrackerFactory(); const useCase = new TrackEventUseCase({ segmentEventTrackerFactory }); -const handler: NextWebhookApiHandler = async ( +const handler: NextWebhookApiHandler = async ( req, res, context, @@ -65,11 +65,11 @@ const handler: NextWebhookApiHandler return useCase.track(event, config).then((result) => { return result.match( () => { - logger.info("Order fully paid event successfully sent to Segment"); + logger.info("Order completed event successfully sent to Segment"); return res .status(200) - .json({ message: "Order fully paid event successfully sent to Segment" }); + .json({ message: "Order completed event successfully sent to Segment" }); }, (error) => { switch (error.constructor) { @@ -85,28 +85,28 @@ const handler: NextWebhookApiHandler } case TrackEventUseCase.TrackEventUseCaseUnknownError: { - logger.error("Unknown error while sending order fully paid event to Segment", { + logger.error("Unknown error while sending order completed event to Segment", { error: error, }); return res .status(500) - .json({ message: "Error while sending order fully paid event to Segment" }); + .json({ message: "Error while sending order completed event to Segment" }); } } }, ); }); } catch (e) { - logger.error("Unhandled error while sending order fully paid event to Segment", { error: e }); + logger.error("Unhandled error while sending order completed event to Segment", { error: e }); return res .status(500) - .json({ message: "Error while sending order fully paid event to Segment" }); + .json({ message: "Error while sending order completed event to Segment" }); } }; export default wrapWithLoggerContext( - withOtel(orderFullyPaidAsyncWebhook.createHandler(handler), "/api/webhooks/order-fully-paid"), + withOtel(orderConfirmedAsyncWebhook.createHandler(handler), "/api/webhooks/order-fully-paid"), loggerContext, ); diff --git a/apps/segment/src/pages/api/webhooks/order-created.ts b/apps/segment/src/pages/api/webhooks/order-created.ts deleted file mode 100644 index d36cabd39c..0000000000 --- a/apps/segment/src/pages/api/webhooks/order-created.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next"; -import { wrapWithLoggerContext } from "@saleor/apps-logger/node"; -import { withOtel } from "@saleor/apps-otel"; -import { ObservabilityAttributes } from "@saleor/apps-otel/src/lib/observability-attributes"; - -import { OrderCreatedSubscriptionPayloadFragment } from "@/generated/graphql"; -import { createLogger } from "@/logger"; -import { loggerContext } from "@/logger-context"; -import { DynamoAppConfigManager } from "@/modules/configuration/dynamo-app-config-manager"; -import { DynamoConfigRepositoryFactory } from "@/modules/db/dynamo-config-factory"; -import { SegmentEventTrackerFactory } from "@/modules/segment/segment-event-tracker-factory"; -import { TrackEventUseCase } from "@/modules/tracking-events/track-event.use-case"; -import { trackingEventFactory } from "@/modules/tracking-events/tracking-events"; -import { orderCreatedAsyncWebhook } from "@/modules/webhooks/definitions/order-created"; - -export const config = { - api: { - bodyParser: false, - }, -}; - -const logger = createLogger("orderCreatedAsyncWebhook"); - -const configRepository = DynamoConfigRepositoryFactory.create(); -const configManager = DynamoAppConfigManager.create(configRepository); -const segmentEventTrackerFactory = new SegmentEventTrackerFactory(); -const useCase = new TrackEventUseCase({ segmentEventTrackerFactory }); - -const handler: NextWebhookApiHandler = async ( - req, - res, - context, -) => { - try { - const { authData, payload } = context; - - const config = await configManager.get({ - saleorApiUrl: authData.saleorApiUrl, - appId: authData.appId, - }); - - if (!config) { - logger.warn("App config not found. Event won't be send to Segment"); - - return res.status(200).json({ - message: "App config not found. Event won't be send to Segment", - }); - } - - if (!payload.order) { - logger.info("Payload does not contain order data. Skipping."); - - return res - .status(200) - .json({ message: "Payload does not contain order data. It will be skipped by app" }); - } - - loggerContext.set(ObservabilityAttributes.ORDER_ID, payload.order.id); - - const event = trackingEventFactory.createOrderCreatedEvent({ - orderBase: payload.order, - issuedAt: payload.issuedAt, - }); - - return useCase.track(event, config).then((result) => { - return result.match( - () => { - logger.info("Order created event successfully sent to Segment"); - - return res - .status(200) - .json({ message: "Order created event successfully sent to Segment" }); - }, - (error) => { - switch (error.constructor) { - case TrackEventUseCase.TrackEventUseCaseSegmentClientError: { - logger.warn("Cannot create Segment Client. Event won't be send to Segment", { - error: error, - }); - - return res.status(200).json({ - message: - "Error during creating connection with Segment. Event won't be send to Segment", - }); - } - - case TrackEventUseCase.TrackEventUseCaseUnknownError: { - logger.error("Unknown error while sending order created event to Segment", { - error: error, - }); - - return res - .status(500) - .json({ message: "Error while sending order created event to Segment" }); - } - } - }, - ); - }); - } catch (e) { - logger.error("Unhandled error while sending order created event to Segment", { error: e }); - - return res.status(500).json({ message: "Error while sending order created event to Segment" }); - } -}; - -export default wrapWithLoggerContext( - withOtel(orderCreatedAsyncWebhook.createHandler(handler), "/api/webhooks/order-created"), - loggerContext, -); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6e8642ec7c..4b9a22dcaf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1236,6 +1236,9 @@ importers: '@vitejs/plugin-react': specifier: 4.3.1 version: 4.3.1(vite@5.3.3(@types/node@20.12.3)(terser@5.18.0)) + decimal.js-light: + specifier: 2.5.1 + version: 2.5.1 dotenv: specifier: 16.3.1 version: 16.3.1 From e3e0d6d23b4d4fed90379091ba23502dc19f6719 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Thu, 23 Jan 2025 10:46:05 +0100 Subject: [PATCH 14/17] extra logs for lines + tests (#1706) --- .changeset/purple-shirts-listen.md | 5 +++ .../avatax-calculate-taxes-adapter.test.ts | 35 +++++++++++++++++++ .../avatax-calculate-taxes-adapter.ts | 22 +++++++++--- ...culate-taxes-response-lines-transformer.ts | 2 ++ 4 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 .changeset/purple-shirts-listen.md create mode 100644 apps/avatax/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.test.ts diff --git a/.changeset/purple-shirts-listen.md b/.changeset/purple-shirts-listen.md new file mode 100644 index 0000000000..1fe618f1d3 --- /dev/null +++ b/.changeset/purple-shirts-listen.md @@ -0,0 +1,5 @@ +--- +"app-avatax": patch +--- + +Added test for suspicious line+tax calculation checker and additional debugging logs diff --git a/apps/avatax/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.test.ts b/apps/avatax/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.test.ts new file mode 100644 index 0000000000..64ae935737 --- /dev/null +++ b/apps/avatax/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; + +import { suspiciousLineCalculationCheck } from "@/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter"; + +describe("suspiciousLineCalculationCheck", () => { + it("Returns false if line is zero", () => { + expect( + suspiciousLineCalculationCheck({ + total_gross_amount: 0, + total_net_amount: 0, + tax_rate: 0.2, // If its zero-line, it doesn matter + }), + ).toBe(false); + }); + + it("Returns true if net & gross is the same, but rate is not: 1.00 + 1.00 + rate 0.08", () => { + expect( + suspiciousLineCalculationCheck({ + total_gross_amount: 1, + total_net_amount: 1, + tax_rate: 0.08, // If its zero-line, it doesn matter + }), + ).toBe(true); + }); + + it("Returns true for small numbers: 0.06 + 0.06 + rate 0.07", () => { + expect( + suspiciousLineCalculationCheck({ + total_gross_amount: 0.06, + total_net_amount: 0.06, + tax_rate: 0.07, // If its zero-line, it doesn matter + }), + ).toBe(true); + }); +}); diff --git a/apps/avatax/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.ts b/apps/avatax/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.ts index 8ecd68b377..9883ae6882 100644 --- a/apps/avatax/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.ts +++ b/apps/avatax/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-adapter.ts @@ -10,6 +10,22 @@ export type AvataxCalculateTaxesResponse = CalculateTaxesResponse; const errorParser = new AvataxErrorsParser(); +export function suspiciousLineCalculationCheck(line: { + total_gross_amount: number; + total_net_amount: number; + tax_rate: number; +}) { + const tax = line.total_gross_amount - line.total_net_amount; + const rate = line.tax_rate; + const lineIsZero = line.total_net_amount === 0 ?? line.total_gross_amount === 0; + + if (tax === 0 && rate !== 0 && !lineIsZero) { + return true; + } + + return false; +} + export class AvataxCalculateTaxesAdapter { private logger = createLogger("AvataxCalculateTaxesAdapter"); @@ -38,11 +54,9 @@ export class AvataxCalculateTaxesAdapter { const transformedResponse = this.avataxCalculateTaxesResponseTransformer.transform(response); transformedResponse.lines.forEach((l) => { - const tax = l.total_gross_amount - l.total_net_amount; - const rate = l.tax_rate; - const lineIsZero = l.total_net_amount === 0 ?? l.total_gross_amount === 0; + const isSuspiciousLine = suspiciousLineCalculationCheck(l); - if (tax === 0 && rate !== 0 && !lineIsZero) { + if (isSuspiciousLine) { this.logger.warn("Non-zero line has zero tax, but rate is not zero", { taxCalculationSummary: response.summary, }); diff --git a/apps/avatax/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-lines-transformer.ts b/apps/avatax/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-lines-transformer.ts index eacfd995e0..1751be821b 100644 --- a/apps/avatax/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-lines-transformer.ts +++ b/apps/avatax/src/modules/avatax/calculate-taxes/avatax-calculate-taxes-response-lines-transformer.ts @@ -76,6 +76,8 @@ export class AvataxCalculateTaxesResponseLinesTransformer { total_net_amount: lineTotalNetAmount, tax_code: line.taxCode, tax_rate: rate, + line_taxable_amount: line.taxableAmount, + line_tax_calculated: line.taxCalculated, }, ); From 969f6919f17fa56caf2bb82e8ac4f33b936425d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Thu, 23 Jan 2025 12:30:25 +0100 Subject: [PATCH 15/17] Fix e2e: add missing env vars (#1707) --- .github/workflows/e2e.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 9595352e15..eaf49ed681 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -55,6 +55,10 @@ jobs: E2E_USER_NAME: "op://Shop-ex/saleor-app-avatax-e2e-${{ env.SALEOR_VERSION }}/E2E_USER_NAME" E2E_USER_PASSWORD: "op://Shop-ex/saleor-app-avatax-e2e-${{ env.SALEOR_VERSION }}/E2E_USER_PASSWORD" SECRET_KEY: "op://Shop-ex/saleor-app-avatax-e2e-${{ env.SALEOR_VERSION }}/SECRET_KEY" + DYNAMODB_LOGS_TABLE_NAME: "op://Shop-ex/saleor-app-avatax-e2e-${{ env.SALEOR_VERSION }}/DYNAMODB_LOGS_TABLE_NAME" + AWS_REGION: "op://Shop-ex/saleor-app-avatax-e2e-${{ env.SALEOR_VERSION }}/AWS_REGION" + AWS_ACCESS_KEY_ID: "op://Shop-ex/saleor-app-avatax-e2e-${{ env.SALEOR_VERSION }}/AWS_ACCESS_KEY_ID" + AWS_SECRET_ACCESS_KEY: "op://Shop-ex/saleor-app-avatax-e2e-${{ env.SALEOR_VERSION }}/AWS_SECRET_ACCESS_KEY" - name: Run e2e tests run: pnpm --filter=app-avatax e2e # TODO: Add HTML report: https://linear.app/saleor/issue/SHOPX-304 From ece2e85a8db38c378e21ee56f8cfd4d3bc18a674 Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Thu, 23 Jan 2025 13:04:35 +0100 Subject: [PATCH 16/17] Release apps (#1703) Co-authored-by: github-actions[bot] --- .changeset/gorgeous-berries-dress.md | 9 --------- .changeset/honest-sheep-happen.md | 6 ------ .changeset/long-cherries-eat.md | 5 ----- .changeset/purple-shirts-listen.md | 5 ----- .changeset/twenty-berries-occur.md | 7 ------- apps/avatax/CHANGELOG.md | 10 ++++++++++ apps/avatax/package.json | 2 +- apps/cms-v2/CHANGELOG.md | 6 ++++++ apps/cms-v2/package.json | 2 +- apps/products-feed/CHANGELOG.md | 6 ++++++ apps/products-feed/package.json | 2 +- apps/segment/CHANGELOG.md | 9 +++++++++ apps/segment/package.json | 2 +- apps/smtp/CHANGELOG.md | 6 ++++++ apps/smtp/package.json | 2 +- 15 files changed, 42 insertions(+), 37 deletions(-) delete mode 100644 .changeset/gorgeous-berries-dress.md delete mode 100644 .changeset/honest-sheep-happen.md delete mode 100644 .changeset/long-cherries-eat.md delete mode 100644 .changeset/purple-shirts-listen.md delete mode 100644 .changeset/twenty-berries-occur.md diff --git a/.changeset/gorgeous-berries-dress.md b/.changeset/gorgeous-berries-dress.md deleted file mode 100644 index dbe6233895..0000000000 --- a/.changeset/gorgeous-berries-dress.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"products-feed": patch -"segment": patch -"app-avatax": patch -"cms-v2": patch -"smtp": patch ---- - -Move `ThemeSynchronizer` utility to shared packages. diff --git a/.changeset/honest-sheep-happen.md b/.changeset/honest-sheep-happen.md deleted file mode 100644 index 0fe97b510d..0000000000 --- a/.changeset/honest-sheep-happen.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"app-avatax": patch ---- - -Implement client logs cache. Right now app will cache request for 1 day and revalidate the cache every 60 seconds. -Added forward / backward pagination to client logs. After this change end user can browse logs that exceeds current pagination limit (first 100). diff --git a/.changeset/long-cherries-eat.md b/.changeset/long-cherries-eat.md deleted file mode 100644 index d302686280..0000000000 --- a/.changeset/long-cherries-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"app-avatax": patch ---- - -Remove feature flag for client logs. After this change logs are enabled by default. diff --git a/.changeset/purple-shirts-listen.md b/.changeset/purple-shirts-listen.md deleted file mode 100644 index 1fe618f1d3..0000000000 --- a/.changeset/purple-shirts-listen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"app-avatax": patch ---- - -Added test for suspicious line+tax calculation checker and additional debugging logs diff --git a/.changeset/twenty-berries-occur.md b/.changeset/twenty-berries-occur.md deleted file mode 100644 index e66a0a5187..0000000000 --- a/.changeset/twenty-berries-occur.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"segment": patch ---- - -- Changed what we sent to Segment to be in sync with their [spec](https://segment.com/docs/connections/spec/ecommerce/v2/) -- Added new Saleor event - `OrderConfirmed` that will be mapped to Segment `OrderCompleted` -- Removed Saleor event - `OrderCreated` - it didn't have respective Segment event diff --git a/apps/avatax/CHANGELOG.md b/apps/avatax/CHANGELOG.md index 2f81e795a1..ec67a12293 100644 --- a/apps/avatax/CHANGELOG.md +++ b/apps/avatax/CHANGELOG.md @@ -1,5 +1,15 @@ # app-avatax +## 1.12.6 + +### Patch Changes + +- 0f0bff21: Move `ThemeSynchronizer` utility to shared packages. +- 18a9c3d9: Implement client logs cache. Right now app will cache request for 1 day and revalidate the cache every 60 seconds. + Added forward / backward pagination to client logs. After this change end user can browse logs that exceeds current pagination limit (first 100). +- e195c8d7: Remove feature flag for client logs. After this change logs are enabled by default. +- e3e0d6d2: Added test for suspicious line+tax calculation checker and additional debugging logs + ## 1.12.5 ### Patch Changes diff --git a/apps/avatax/package.json b/apps/avatax/package.json index 0ec934b3ec..95fbb0b125 100644 --- a/apps/avatax/package.json +++ b/apps/avatax/package.json @@ -1,6 +1,6 @@ { "name": "app-avatax", - "version": "1.12.5", + "version": "1.12.6", "scripts": { "build": " next build", "check-types": "tsc --noEmit", diff --git a/apps/cms-v2/CHANGELOG.md b/apps/cms-v2/CHANGELOG.md index 1b8f0bcc3b..2137fe9e2b 100644 --- a/apps/cms-v2/CHANGELOG.md +++ b/apps/cms-v2/CHANGELOG.md @@ -1,5 +1,11 @@ # saleor-app-cms-v2 +## 2.9.19 + +### Patch Changes + +- 0f0bff21: Move `ThemeSynchronizer` utility to shared packages. + ## 2.9.18 ### Patch Changes diff --git a/apps/cms-v2/package.json b/apps/cms-v2/package.json index c29f724409..cdce0e2969 100644 --- a/apps/cms-v2/package.json +++ b/apps/cms-v2/package.json @@ -1,6 +1,6 @@ { "name": "cms-v2", - "version": "2.9.18", + "version": "2.9.19", "scripts": { "build": "next build", "check-types": "tsc --noEmit", diff --git a/apps/products-feed/CHANGELOG.md b/apps/products-feed/CHANGELOG.md index b9e12ba235..4c2214d2ff 100644 --- a/apps/products-feed/CHANGELOG.md +++ b/apps/products-feed/CHANGELOG.md @@ -1,5 +1,11 @@ # saleor-app-products-feed +## 1.20.1 + +### Patch Changes + +- 0f0bff21: Move `ThemeSynchronizer` utility to shared packages. + ## 1.20.0 ### Minor Changes diff --git a/apps/products-feed/package.json b/apps/products-feed/package.json index 6057f2b3fe..e83c435f23 100644 --- a/apps/products-feed/package.json +++ b/apps/products-feed/package.json @@ -1,6 +1,6 @@ { "name": "products-feed", - "version": "1.20.0", + "version": "1.20.1", "scripts": { "build": "next build", "check-types": "tsc --noEmit", diff --git a/apps/segment/CHANGELOG.md b/apps/segment/CHANGELOG.md index 796e5621f4..6029a7b196 100644 --- a/apps/segment/CHANGELOG.md +++ b/apps/segment/CHANGELOG.md @@ -1,5 +1,14 @@ # segment +## 2.0.4 + +### Patch Changes + +- 0f0bff21: Move `ThemeSynchronizer` utility to shared packages. +- 989cb683: - Changed what we sent to Segment to be in sync with their [spec](https://segment.com/docs/connections/spec/ecommerce/v2/) + - Added new Saleor event - `OrderConfirmed` that will be mapped to Segment `OrderCompleted` + - Removed Saleor event - `OrderCreated` - it didn't have respective Segment event + ## 2.0.3 ### Patch Changes diff --git a/apps/segment/package.json b/apps/segment/package.json index 553fbe805e..a95e60fe9d 100644 --- a/apps/segment/package.json +++ b/apps/segment/package.json @@ -1,6 +1,6 @@ { "name": "segment", - "version": "2.0.3", + "version": "2.0.4", "scripts": { "build": "next build", "check-types": "tsc --noEmit", diff --git a/apps/smtp/CHANGELOG.md b/apps/smtp/CHANGELOG.md index 59bb0d8b40..144ff56525 100644 --- a/apps/smtp/CHANGELOG.md +++ b/apps/smtp/CHANGELOG.md @@ -1,5 +1,11 @@ # smtp +## 1.2.22 + +### Patch Changes + +- 0f0bff21: Move `ThemeSynchronizer` utility to shared packages. + ## 1.2.21 ### Patch Changes diff --git a/apps/smtp/package.json b/apps/smtp/package.json index e15b1a68b8..c123d589ee 100644 --- a/apps/smtp/package.json +++ b/apps/smtp/package.json @@ -1,6 +1,6 @@ { "name": "smtp", - "version": "1.2.21", + "version": "1.2.22", "scripts": { "build": "next build", "check-types": "tsc --noEmit", From 36697b40d157dc39b4e5ebcc62b40d43634cf6f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBuraw?= <9116238+krzysztofzuraw@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:14:07 +0100 Subject: [PATCH 17/17] Change CODEOWNERS to extensibility-team (#1708) --- CODEOWNERS | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 8f5fbf6715..8959763d07 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,10 +1 @@ -* @saleor/apps-guild - -/apps/avatax @saleor/shopex-js -/apps/segment @saleor/shopex-js - -/apps/cms-v2 @saleor/merchant-js -/apps/klaviyo @saleor/merchant-js -/apps/products-feed @saleor/merchant-js -/apps/search @saleor/merchant-js -/apps/smtp @saleor/merchant-js +* @saleor/extensibility-team-js