Compare commits
2 commits
main
...
order-expo
Author | SHA1 | Date | |
---|---|---|---|
![]() |
53b67c4505 | ||
![]() |
489e110e74 |
110 changed files with 32287 additions and 898 deletions
5
.changeset/honest-geese-crash.md
Normal file
5
.changeset/honest-geese-crash.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-app-taxes": minor
|
||||
---
|
||||
|
||||
Added logs for AvaTax and TaxJar. Logs are stored in the app metadata. Only the last 100 events are stored. Each provider configuration has its own logs. You can get to them by a new button "Logs" in the provider table.
|
15
.changeset/mighty-tips-hug.md
Normal file
15
.changeset/mighty-tips-hug.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
---
|
||||
"saleor-app-emails-and-messages": patch
|
||||
"saleor-app-data-importer": patch
|
||||
"saleor-app-products-feed": patch
|
||||
"saleor-app-invoices": patch
|
||||
"saleor-app-klaviyo": patch
|
||||
"saleor-app-segment": patch
|
||||
"saleor-app-cms-v2": patch
|
||||
"saleor-app-search": patch
|
||||
"saleor-app-slack": patch
|
||||
"saleor-app-taxes": patch
|
||||
"saleor-app-crm": patch
|
||||
---
|
||||
|
||||
Updated Sentry package
|
5
.changeset/nine-rivers-flow.md
Normal file
5
.changeset/nine-rivers-flow.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-app-klaviyo": patch
|
||||
---
|
||||
|
||||
Fixed error where config couldn't be saved
|
5
.changeset/orange-actors-eat.md
Normal file
5
.changeset/orange-actors-eat.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-app-search": patch
|
||||
---
|
||||
|
||||
Webhook migration scripts has been moved to the shared package.
|
|
@ -1,8 +0,0 @@
|
|||
#changelog
|
||||
---
|
||||
"apps": minor
|
||||
---
|
||||
### Added
|
||||
|
||||
- `apps/emails-and-messages/.env.template`: Described the new environment variable and how it works
|
||||
- `apps/emails-and-messages/src/saleor-app.ts`: Added case "redis" for switch(AplType), which takes advantage of the [RedisAPL PR](https://github.com/saleor/app-sdk/pull/287) I submitted
|
5
.changeset/real-pigs-promise.md
Normal file
5
.changeset/real-pigs-promise.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-app-invoices": minor
|
||||
---
|
||||
|
||||
Replace text "loading" messages with skeletons
|
5
.changeset/silver-windows-accept.md
Normal file
5
.changeset/silver-windows-accept.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@saleor/webhook-utils": patch
|
||||
---
|
||||
|
||||
Introduced a new shared package with helpers used for webhook management.
|
5
.changeset/twelve-pianos-relate.md
Normal file
5
.changeset/twelve-pianos-relate.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-app-invoices": minor
|
||||
---
|
||||
|
||||
Redesigned app layout. Now app uses shared sections as other apps.
|
5
.changeset/wicked-llamas-talk.md
Normal file
5
.changeset/wicked-llamas-talk.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-app-klaviyo": minor
|
||||
---
|
||||
|
||||
Improved app layout to match modern style.
|
9
.github/workflows/assign-pr.yml
vendored
9
.github/workflows/assign-pr.yml
vendored
|
@ -6,12 +6,9 @@ on:
|
|||
|
||||
jobs:
|
||||
assign_creator:
|
||||
if: ${{ github.event.pull_request.user.login != 'dependabot[bot]' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Assign PR to creator
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
CREATOR: ${{ github.event.pull_request.user.login }}
|
||||
run: gh pr edit "$PR_URL" --add-assignee "$CREATOR"
|
||||
uses: thomaseizinger/assign-pr-creator-action@v1.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
31
.github/workflows/changeset-checker.yml
vendored
31
.github/workflows/changeset-checker.yml
vendored
|
@ -1,31 +0,0 @@
|
|||
name: Changesets
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- labeled
|
||||
- edited
|
||||
- synchronize
|
||||
branches-ignore:
|
||||
- 'changeset-release/**'
|
||||
jobs:
|
||||
changeset_check:
|
||||
name: Changeset added to the PR
|
||||
# Adding 'skip changesets' label to the PR will skip this job
|
||||
if: ${{ !contains( github.event.pull_request.labels.*.name, 'skip changeset') }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
# check out full history
|
||||
fetch-depth: 0
|
||||
- uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "pnpm"
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
- name: Changeset added
|
||||
run: npx changeset status --since origin/main
|
2
.github/workflows/check-deps.yml
vendored
2
.github/workflows/check-deps.yml
vendored
|
@ -17,7 +17,7 @@ jobs:
|
|||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
- uses: JamieMason/syncpack-github-action@c145cec44b3731b3fe8e859679e240d6ae011f0f
|
||||
- uses: JamieMason/syncpack-github-action@0.2.2
|
||||
continue-on-error: true
|
||||
with:
|
||||
package-manager: "pnpm"
|
||||
|
|
2
.github/workflows/check-spelling.yml
vendored
2
.github/workflows/check-spelling.yml
vendored
|
@ -7,4 +7,4 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: streetsidesoftware/cspell-action@22e32eb3d70acf30e3fc09bd46edc1d30fb2d6db
|
||||
- uses: streetsidesoftware/cspell-action@v3
|
||||
|
|
4
.github/workflows/prepare-release.yml
vendored
4
.github/workflows/prepare-release.yml
vendored
|
@ -25,11 +25,11 @@ jobs:
|
|||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
- uses: pnpm/action-setup@d882d12c64e032187b2edb46d3a0d003b7a43598
|
||||
- uses: pnpm/action-setup@v2
|
||||
name: Install pnpm
|
||||
- run: pnpm install
|
||||
- name: Create Release Pull Request
|
||||
uses: changesets/action@f13b1baaa620fde937751f5d2c3572b9da32af23
|
||||
uses: changesets/action@v1
|
||||
id: changesets
|
||||
with:
|
||||
title: 🚀 Release apps
|
||||
|
|
2
.github/workflows/unit-tests.yml
vendored
2
.github/workflows/unit-tests.yml
vendored
|
@ -9,7 +9,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd
|
||||
- uses: pnpm/action-setup@v2.2.4
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
|
|
|
@ -1 +1 @@
|
|||
* @saleor/delivery-engineering-js
|
||||
* @saleor/appstore
|
|
@ -1,14 +1,5 @@
|
|||
# saleor-app-cms-v2
|
||||
|
||||
## 2.3.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5d3d81d: Bumped @hookform/resolvers from 2.9.11 to 3.3.1
|
||||
- 5dee65a: Updated dependencies:
|
||||
- @graphql-codegen/cli@5.0.0
|
||||
- 2e29699: Updated Sentry package
|
||||
|
||||
## 2.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "saleor-app-cms-v2",
|
||||
"version": "2.3.3",
|
||||
"version": "2.3.2",
|
||||
"scripts": {
|
||||
"build": "pnpm generate && next build",
|
||||
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
|
||||
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@datocms/cma-client-browser": "2.0.0",
|
||||
"@hookform/resolvers": "^3.3.1",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@saleor/app-sdk": "0.43.1",
|
||||
"@saleor/apps-shared": "workspace:*",
|
||||
"@saleor/apps-ui": "workspace:*",
|
||||
|
@ -48,7 +48,7 @@
|
|||
"zod": "3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "5.0.0",
|
||||
"@graphql-codegen/cli": "4.0.1",
|
||||
"@graphql-codegen/introspection": "4.0.0",
|
||||
"@graphql-codegen/typed-document-node": "5.0.1",
|
||||
"@graphql-codegen/typescript": "4.0.1",
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
# saleor-app-crm
|
||||
|
||||
## 1.7.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5dee65a: Updated dependencies:
|
||||
- @graphql-codegen/cli@5.0.0
|
||||
- 2e29699: Updated Sentry package
|
||||
|
||||
## 1.7.7
|
||||
|
||||
### Patch Changes
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "saleor-app-crm",
|
||||
"version": "1.7.8",
|
||||
"version": "1.7.7",
|
||||
"scripts": {
|
||||
"build": "pnpm generate && next build",
|
||||
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
|
||||
|
@ -42,7 +42,7 @@
|
|||
"zod": "3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "5.0.0",
|
||||
"@graphql-codegen/cli": "4.0.1",
|
||||
"@graphql-codegen/introspection": "4.0.0",
|
||||
"@graphql-codegen/typed-document-node": "5.0.1",
|
||||
"@graphql-codegen/typescript": "4.0.1",
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
# saleor-app-data-importer
|
||||
|
||||
## 1.9.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5dee65a: Updated dependencies:
|
||||
- @graphql-codegen/cli@5.0.0
|
||||
- 2e29699: Updated Sentry package
|
||||
|
||||
## 1.9.6
|
||||
|
||||
### Patch Changes
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "saleor-app-data-importer",
|
||||
"version": "1.9.7",
|
||||
"version": "1.9.6",
|
||||
"scripts": {
|
||||
"build": "pnpm generate && next build",
|
||||
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
|
||||
|
@ -39,7 +39,7 @@
|
|||
"zod": "3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "5.0.0",
|
||||
"@graphql-codegen/cli": "4.0.1",
|
||||
"@graphql-codegen/introspection": "4.0.0",
|
||||
"@graphql-codegen/schema-ast": "4.0.0",
|
||||
"@graphql-codegen/typed-document-node": "5.0.1",
|
||||
|
|
|
@ -7,12 +7,6 @@ APL=
|
|||
REST_APL_ENDPOINT=
|
||||
REST_APL_TOKEN=
|
||||
|
||||
# To use Redis as an APL store, set APP_API_BASE_URL and REDIS_URL.
|
||||
# URL is in format redis[s]://[[username][:password]@][host][:port][/db-number],
|
||||
# so for example redis://alice:foobared@awesome.redis.server:6380
|
||||
# For saleor-platform, thats: `redis://redis:6379/1`
|
||||
REDIS_URL=
|
||||
|
||||
APP_LOG_LEVEL=info
|
||||
|
||||
# Local development variables. When developped locally with Saleor inside docker, these can be set to:
|
||||
|
@ -21,5 +15,4 @@ APP_LOG_LEVEL=info
|
|||
# If developped with tunnels, set this empty, it will fallback to default Next's localhost:3000
|
||||
# https://docs.saleor.io/docs/3.x/developer/extending/apps/local-app-development
|
||||
APP_IFRAME_BASE_URL=
|
||||
APP_API_BASE_URL=
|
||||
|
||||
APP_API_BASE_URL=
|
|
@ -1,14 +1,5 @@
|
|||
# saleor-app-emails-and-messages
|
||||
|
||||
## 1.9.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5d3d81d: Bumped @hookform/resolvers from 2.9.11 to 3.3.1
|
||||
- 5dee65a: Updated dependencies:
|
||||
- @graphql-codegen/cli@5.0.0
|
||||
- 2e29699: Updated Sentry package
|
||||
|
||||
## 1.9.9
|
||||
|
||||
### Patch Changes
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "saleor-app-emails-and-messages",
|
||||
"version": "1.9.10",
|
||||
"version": "1.9.9",
|
||||
"scripts": {
|
||||
"build": "pnpm generate && next build",
|
||||
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
|
||||
|
@ -12,7 +12,7 @@
|
|||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.3.1",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@monaco-editor/react": "^4.4.6",
|
||||
"@saleor/app-sdk": "0.43.1",
|
||||
"@saleor/apps-shared": "workspace:*",
|
||||
|
@ -51,7 +51,7 @@
|
|||
"zod": "3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "5.0.0",
|
||||
"@graphql-codegen/cli": "4.0.1",
|
||||
"@graphql-codegen/introspection": "4.0.0",
|
||||
"@graphql-codegen/schema-ast": "4.0.0",
|
||||
"@graphql-codegen/typed-document-node": "5.0.1",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { APL, FileAPL, RedisAPL, SaleorCloudAPL, UpstashAPL } from "@saleor/app-sdk/APL";
|
||||
import { APL, FileAPL, SaleorCloudAPL, UpstashAPL } from "@saleor/app-sdk/APL";
|
||||
import { SaleorApp } from "@saleor/app-sdk/saleor-app";
|
||||
|
||||
const aplType = process.env.APL ?? "file";
|
||||
|
@ -6,12 +6,6 @@ const aplType = process.env.APL ?? "file";
|
|||
export let apl: APL;
|
||||
|
||||
switch (aplType) {
|
||||
case "redis": {
|
||||
if (!process.env.REDIS_URL) throw new Error("Missing redis url");
|
||||
if (!process.env.APP_API_BASE_URL)
|
||||
throw new Error("Redis relies on APP_API_BASE_URL to store keys, please set env variable");
|
||||
apl = new RedisAPL(new URL(process.env.REDIS_URL), process.env.APP_API_BASE_URL);
|
||||
}
|
||||
case "upstash":
|
||||
apl = new UpstashAPL();
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
{
|
||||
"extends": [
|
||||
"//"
|
||||
],
|
||||
"extends": ["//"],
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"pipeline": {
|
||||
"build": {
|
||||
|
@ -23,8 +21,7 @@
|
|||
"NEXT_PUBLIC_SENTRY_DSN",
|
||||
"SENTRY_ENVIRONMENT",
|
||||
"APP_IFRAME_BASE_URL",
|
||||
"APP_API_BASE_URL",
|
||||
"REDIS_URL"
|
||||
"APP_API_BASE_URL"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,5 @@
|
|||
# saleor-app-invoices
|
||||
|
||||
## 1.16.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 4aee4e1: Replace text "loading" messages with skeletons
|
||||
- 4aee4e1: Redesigned app layout. Now app uses shared sections as other apps.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5d3d81d: Bumped @hookform/resolvers from 2.9.11 to 3.3.1
|
||||
- 5dee65a: Updated dependencies:
|
||||
- @graphql-codegen/cli@5.0.0
|
||||
- 2e29699: Updated Sentry package
|
||||
|
||||
## 1.15.7
|
||||
|
||||
### Patch Changes
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "saleor-app-invoices",
|
||||
"version": "1.16.0",
|
||||
"version": "1.15.7",
|
||||
"scripts": {
|
||||
"build": "pnpm generate && next build",
|
||||
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
|
||||
|
@ -12,7 +12,7 @@
|
|||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.3.1",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@saleor/app-sdk": "0.43.1",
|
||||
"@saleor/apps-shared": "workspace:*",
|
||||
"@saleor/apps-ui": "workspace:*",
|
||||
|
@ -42,7 +42,7 @@
|
|||
"zod": "3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "5.0.0",
|
||||
"@graphql-codegen/cli": "4.0.1",
|
||||
"@graphql-codegen/introspection": "4.0.0",
|
||||
"@graphql-codegen/schema-ast": "4.0.0",
|
||||
"@graphql-codegen/typed-document-node": "5.0.1",
|
||||
|
|
|
@ -1,18 +1,5 @@
|
|||
# saleor-app-klaviyo
|
||||
|
||||
## 1.9.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 4aee4e1: Improved app layout to match modern style.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5dee65a: Updated dependencies:
|
||||
- @graphql-codegen/cli@5.0.0
|
||||
- 2e29699: Updated Sentry package
|
||||
- 4aee4e1: Fixed error where config couldn't be saved
|
||||
|
||||
## 1.8.6
|
||||
|
||||
### Patch Changes
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "saleor-app-klaviyo",
|
||||
"version": "1.9.0",
|
||||
"version": "1.8.6",
|
||||
"scripts": {
|
||||
"build": "pnpm generate && next build",
|
||||
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
|
||||
|
@ -35,7 +35,7 @@
|
|||
"vitest": "0.34.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "5.0.0",
|
||||
"@graphql-codegen/cli": "4.0.1",
|
||||
"@graphql-codegen/introspection": "4.0.0",
|
||||
"@graphql-codegen/schema-ast": "4.0.0",
|
||||
"@graphql-codegen/typed-document-node": "5.0.1",
|
||||
|
|
18
apps/orders-export/.env.example
Normal file
18
apps/orders-export/.env.example
Normal file
|
@ -0,0 +1,18 @@
|
|||
# The key used for metadata encryption. Required for production builds
|
||||
SECRET_KEY=
|
||||
|
||||
# APL Config
|
||||
# https://github.com/saleor/saleor-app-sdk/blob/main/docs/apl.md
|
||||
APL=file
|
||||
REST_APL_ENDPOINT=
|
||||
REST_APL_TOKEN=
|
||||
|
||||
APP_LOG_LEVEL=info
|
||||
|
||||
# Local development variables. When developped locally with Saleor inside docker, these can be set to:
|
||||
# APP_IFRAME_BASE_URL = http://localhost:3000, so Dashboard on host can access iframe
|
||||
# APP_API_BASE_URL=http://host.docker.internal:3000 - so Saleor can reach App running on host, from the container.
|
||||
# If developped with tunnels, set this empty, it will fallback to default Next's localhost:3000
|
||||
# https://docs.saleor.io/docs/3.x/developer/extending/apps/local-app-development
|
||||
APP_IFRAME_BASE_URL=
|
||||
APP_API_BASE_URL=
|
4
apps/orders-export/.eslintrc.json
Normal file
4
apps/orders-export/.eslintrc.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"root": true,
|
||||
"extends": ["saleor"]
|
||||
}
|
19
apps/orders-export/.graphqlrc.yml
Normal file
19
apps/orders-export/.graphqlrc.yml
Normal file
|
@ -0,0 +1,19 @@
|
|||
schema: graphql/schema.graphql
|
||||
documents: [graphql/**/*.graphql, src/**/*.ts, src/**/*.tsx]
|
||||
extensions:
|
||||
codegen:
|
||||
overwrite: true
|
||||
generates:
|
||||
generated/graphql.ts:
|
||||
config:
|
||||
dedupeFragments: true
|
||||
plugins:
|
||||
- typescript
|
||||
- typescript-operations
|
||||
- typescript-urql:
|
||||
documentVariablePrefix: "Untyped"
|
||||
fragmentVariablePrefix: "Untyped"
|
||||
- typed-document-node
|
||||
generated/schema.graphql:
|
||||
plugins:
|
||||
- schema-ast
|
39
apps/orders-export/graphql/fragments/order-base.graphql
Normal file
39
apps/orders-export/graphql/fragments/order-base.graphql
Normal file
|
@ -0,0 +1,39 @@
|
|||
fragment OrderBase on Order {
|
||||
id
|
||||
user {
|
||||
id
|
||||
email
|
||||
}
|
||||
channel {
|
||||
id
|
||||
slug
|
||||
name
|
||||
}
|
||||
userEmail
|
||||
shippingMethodName
|
||||
total {
|
||||
gross {
|
||||
amount
|
||||
currency
|
||||
}
|
||||
net {
|
||||
currency
|
||||
amount
|
||||
}
|
||||
}
|
||||
lines {
|
||||
productVariantId
|
||||
productSku
|
||||
variantName
|
||||
unitPrice {
|
||||
gross {
|
||||
amount
|
||||
}
|
||||
net {
|
||||
amount
|
||||
}
|
||||
}
|
||||
quantity
|
||||
}
|
||||
number
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
mutation UpdateAppMetadata($id: ID!, $input: [MetadataInput!]!) {
|
||||
updatePrivateMetadata(id: $id, input: $input) {
|
||||
item {
|
||||
privateMetadata {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
apps/orders-export/graphql/queries/PaginatedOrders.graphql
Normal file
11
apps/orders-export/graphql/queries/PaginatedOrders.graphql
Normal file
|
@ -0,0 +1,11 @@
|
|||
query PaginatedOrders($after: String, $channel: String!) {
|
||||
orders(first: 100, after: $after, channel: $channel) {
|
||||
edges {
|
||||
node {
|
||||
... on Order {
|
||||
...OrderBase
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
30308
apps/orders-export/graphql/schema.graphql
Normal file
30308
apps/orders-export/graphql/schema.graphql
Normal file
File diff suppressed because it is too large
Load diff
0
apps/orders-export/graphql/subscriptions/.gitkeep
Normal file
0
apps/orders-export/graphql/subscriptions/.gitkeep
Normal file
5
apps/orders-export/next-env.d.ts
vendored
Normal file
5
apps/orders-export/next-env.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
44
apps/orders-export/next.config.js
Normal file
44
apps/orders-export/next.config.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
const { z } = require("zod");
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
|
||||
const RequiredEnvs = z.object({
|
||||
APL: z.string().min(1),
|
||||
});
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = () => {
|
||||
try {
|
||||
RequiredEnvs.parse(process.env);
|
||||
} catch (e) {
|
||||
console.error("🚫 Missing required env variables, see message below");
|
||||
console.error(e.issues);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return {
|
||||
reactStrictMode: true,
|
||||
// TODO Infer names dynamically from disk
|
||||
transpilePackages: ["@saleor/apps-shared", "@saleor/apps-ui", "@saleor/react-hook-form-macaw", "@saleor/trpc"],
|
||||
};
|
||||
};
|
||||
|
||||
const isSentryPropertiesInEnvironment =
|
||||
process.env.SENTRY_AUTH_TOKEN && process.env.SENTRY_PROJECT && process.env.SENTRY_ORG;
|
||||
|
||||
const configWithSentry = withSentryConfig(
|
||||
nextConfig,
|
||||
{
|
||||
silent: true,
|
||||
org: process.env.SENTRY_ORG,
|
||||
project: process.env.SENTRY_PROJECT,
|
||||
},
|
||||
{
|
||||
widenClientFileUpload: true,
|
||||
transpileClientSDK: true,
|
||||
tunnelRoute: "/monitoring",
|
||||
hideSourceMaps: true,
|
||||
disableLogger: true,
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = isSentryPropertiesInEnvironment ? configWithSentry : nextConfig;
|
67
apps/orders-export/package.json
Normal file
67
apps/orders-export/package.json
Normal file
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"name": "saleor-app-orders-export",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"build": "pnpm generate && next build",
|
||||
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
|
||||
"fetch-schema": "curl https://raw.githubusercontent.com/saleor/saleor/${npm_package_saleor_schemaVersion}/saleor/graphql/schema.graphql > graphql/schema.graphql",
|
||||
"generate": "graphql-codegen",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"start": "next start",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@saleor/app-sdk": "0.43.1",
|
||||
"@saleor/apps-shared": "workspace:*",
|
||||
"@saleor/apps-ui": "workspace:*",
|
||||
"@saleor/macaw-ui": "0.8.0-pre.127",
|
||||
"@saleor/react-hook-form-macaw": "workspace:*",
|
||||
"@saleor/trpc": "workspace:*",
|
||||
"@sentry/nextjs": "7.67.0",
|
||||
"@tanstack/react-query": "^4.29.19",
|
||||
"@trpc/client": "10.38.1",
|
||||
"@trpc/next": "10.38.1",
|
||||
"@trpc/react-query": "10.38.1",
|
||||
"@trpc/server": "10.38.1",
|
||||
"@urql/exchange-auth": "^2.1.4",
|
||||
"@vitejs/plugin-react": "4.0.4",
|
||||
"graphql": "16.7.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"jsdom": "^20.0.3",
|
||||
"next": "13.4.8",
|
||||
"pino": "^8.14.1",
|
||||
"pino-pretty": "^10.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-error-boundary": "4.0.10",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"urql": "^4.0.4",
|
||||
"usehooks-ts": "^2.9.1",
|
||||
"vite": "4.4.8",
|
||||
"vitest": "0.34.1",
|
||||
"zod": "3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "4.0.1",
|
||||
"@graphql-codegen/introspection": "4.0.0",
|
||||
"@graphql-codegen/typed-document-node": "5.0.1",
|
||||
"@graphql-codegen/typescript": "4.0.1",
|
||||
"@graphql-codegen/typescript-operations": "4.0.1",
|
||||
"@graphql-codegen/typescript-urql": "3.7.3",
|
||||
"@graphql-typed-document-node/core": "3.2.0",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"@types/react": "18.2.5",
|
||||
"@types/react-dom": "18.2.5",
|
||||
"eslint": "8.46.0",
|
||||
"eslint-config-saleor": "workspace:*",
|
||||
"node-mocks-http": "^1.12.2",
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
"private": true,
|
||||
"saleor": {
|
||||
"schemaVersion": "3.14"
|
||||
}
|
||||
}
|
37
apps/orders-export/sentry.client.config.ts
Normal file
37
apps/orders-export/sentry.client.config.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* This file configures the initialization of Sentry on the client.
|
||||
* The config you add here will be used whenever a users loads a page in their browser.
|
||||
* https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
*/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import pkg from "./package.json";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 0.5,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
|
||||
/*
|
||||
* This sets the sample rate to be 10%. You may want this to be 100% while
|
||||
* in development and sample at a lower rate in production
|
||||
*/
|
||||
replaysSessionSampleRate: 0.1,
|
||||
|
||||
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
|
||||
integrations: [
|
||||
new Sentry.Replay({
|
||||
// Additional Replay configuration goes in here, for example:
|
||||
maskAllText: true,
|
||||
blockAllMedia: true,
|
||||
}),
|
||||
],
|
||||
environment: process.env.SENTRY_ENVIRONMENT,
|
||||
release: `${pkg.name}@${pkg.version}`,
|
||||
});
|
21
apps/orders-export/sentry.edge.config.ts
Normal file
21
apps/orders-export/sentry.edge.config.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
|
||||
* The config you add here will be used whenever one of the edge features is loaded.
|
||||
* Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
|
||||
* https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
*/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import pkg from "./package.json";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 0.5,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
environment: process.env.SENTRY_ENVIRONMENT,
|
||||
release: `${pkg.name}@${pkg.version}`,
|
||||
});
|
20
apps/orders-export/sentry.server.config.ts
Normal file
20
apps/orders-export/sentry.server.config.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* This file configures the initialization of Sentry on the server.
|
||||
* The config you add here will be used whenever the server handles a request.
|
||||
* https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||
*/
|
||||
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import pkg from "./package.json";
|
||||
|
||||
Sentry.init({
|
||||
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
|
||||
|
||||
// Adjust this value in production, or use tracesSampler for greater control
|
||||
tracesSampleRate: 0.5,
|
||||
|
||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||
debug: false,
|
||||
environment: process.env.SENTRY_ENVIRONMENT,
|
||||
release: `${pkg.name}@${pkg.version}`,
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
||||
import { AppConfig } from "./app-config";
|
||||
import { createSettingsManager } from "./metadata-manager";
|
||||
import { createGraphQLClient } from "@saleor/apps-shared";
|
||||
import { AuthData } from "@saleor/app-sdk/APL";
|
||||
|
||||
export class AppConfigMetadataManager {
|
||||
public readonly metadataKey = "app-config-v1";
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
24
apps/orders-export/src/modules/configuration/app-config.ts
Normal file
24
apps/orders-export/src/modules/configuration/app-config.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { z } from "zod";
|
||||
import { RootConfig } from "./schemas/root-config.schema";
|
||||
|
||||
export class AppConfig {
|
||||
private rootData: RootConfig.Shape = {};
|
||||
|
||||
constructor(initialData?: RootConfig.Shape) {
|
||||
if (initialData) {
|
||||
this.rootData = RootConfig.Schema.parse(initialData);
|
||||
}
|
||||
}
|
||||
|
||||
static parse(serializedSchema: string) {
|
||||
return new AppConfig(JSON.parse(serializedSchema));
|
||||
}
|
||||
|
||||
serialize() {
|
||||
return JSON.stringify(this.rootData);
|
||||
}
|
||||
|
||||
getConfig() {
|
||||
return this.rootData;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { createLogger } from "@saleor/apps-shared";
|
||||
import { router } from "../trpc/trpc-server";
|
||||
|
||||
const logger = createLogger({ name: "configuration.router" });
|
||||
|
||||
export const configurationRouter = router({});
|
|
@ -0,0 +1,12 @@
|
|||
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
||||
import { EncryptedMetadataManagerFactory } from "@saleor/apps-shared";
|
||||
import { Client } from "urql";
|
||||
|
||||
export const createSettingsManager = (
|
||||
client: Pick<Client, "query" | "mutation">,
|
||||
appId: string,
|
||||
): SettingsManager => {
|
||||
const metadataManagerFactory = new EncryptedMetadataManagerFactory(process.env.SECRET_KEY!);
|
||||
|
||||
return metadataManagerFactory.create(client, appId);
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export namespace RootConfig {
|
||||
/**
|
||||
* Store entire app config in single file
|
||||
* - Only one request
|
||||
* - Always transactional
|
||||
*/
|
||||
export const Schema = z.object({});
|
||||
|
||||
export type Shape = z.infer<typeof Schema>;
|
||||
}
|
22
apps/orders-export/src/modules/order/order-row.ts
Normal file
22
apps/orders-export/src/modules/order/order-row.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
export interface OrderRowLine {
|
||||
productVariantId: string;
|
||||
productSku: string;
|
||||
variantName: string;
|
||||
quantity: number;
|
||||
unitPriceGrossAmount: number;
|
||||
unitPriceNetAmount: number;
|
||||
}
|
||||
|
||||
// todo add more fields
|
||||
export interface OrderRowFull extends OrderRowLine {
|
||||
id: string;
|
||||
number: string;
|
||||
userId: string;
|
||||
userEmail: string;
|
||||
channelId: string;
|
||||
channelSlug: string;
|
||||
shippingMethodName: string;
|
||||
totalGrossAmount: string;
|
||||
totalNetAmount: string;
|
||||
orderCurrency: string;
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
import { verifyJWT } from "@saleor/app-sdk/verify-jwt";
|
||||
import { middleware, procedure } from "./trpc-server";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { ProtectedHandlerError } from "@saleor/app-sdk/handlers/next";
|
||||
import { saleorApp } from "../../saleor-app";
|
||||
import { createGraphQLClient, logger } from "@saleor/apps-shared";
|
||||
|
||||
const attachAppToken = middleware(async ({ ctx, next }) => {
|
||||
logger.debug("attachAppToken middleware");
|
||||
|
||||
if (!ctx.saleorApiUrl) {
|
||||
logger.debug("ctx.saleorApiUrl not found, throwing");
|
||||
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Missing saleorApiUrl in request",
|
||||
});
|
||||
}
|
||||
|
||||
const authData = await saleorApp.apl.get(ctx.saleorApiUrl);
|
||||
|
||||
if (!authData) {
|
||||
logger.debug("authData not found, throwing 401");
|
||||
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "Missing auth data",
|
||||
});
|
||||
}
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
appToken: authData.token,
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
appId: authData.appId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const validateClientToken = middleware(async ({ ctx, next, meta }) => {
|
||||
logger.debug(
|
||||
{
|
||||
permissions: meta?.requiredClientPermissions,
|
||||
},
|
||||
"Calling validateClientToken middleware with permissions required",
|
||||
);
|
||||
|
||||
if (!ctx.token) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Missing token in request. This middleware can be used only in frontend",
|
||||
});
|
||||
}
|
||||
|
||||
if (!ctx.appId) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Missing appId in request. This middleware can be used after auth is attached",
|
||||
});
|
||||
}
|
||||
|
||||
if (!ctx.saleorApiUrl) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message:
|
||||
"Missing saleorApiUrl in request. This middleware can be used after auth is attached",
|
||||
});
|
||||
}
|
||||
|
||||
if (!ctx.ssr) {
|
||||
try {
|
||||
logger.debug("trying to verify JWT token from frontend");
|
||||
logger.debug({ token: ctx.token ? `${ctx.token[0]}...` : undefined });
|
||||
|
||||
await verifyJWT({
|
||||
appId: ctx.appId,
|
||||
token: ctx.token,
|
||||
saleorApiUrl: ctx.saleorApiUrl,
|
||||
requiredPermissions: meta?.requiredClientPermissions ?? [],
|
||||
});
|
||||
} catch (e) {
|
||||
logger.debug("JWT verification failed, throwing");
|
||||
throw new ProtectedHandlerError("JWT verification failed: ", "JWT_VERIFICATION_FAILED");
|
||||
}
|
||||
}
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
...ctx,
|
||||
saleorApiUrl: ctx.saleorApiUrl,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Construct common graphQL client and attach it to the context
|
||||
*
|
||||
* Can be used only if called from the frontend (react-query),
|
||||
* otherwise jwks validation will fail (if createCaller used)
|
||||
*
|
||||
* TODO Rethink middleware composition to enable safe server-side router calls
|
||||
*/
|
||||
export const protectedClientProcedure = procedure
|
||||
.use(attachAppToken)
|
||||
.use(validateClientToken)
|
||||
.use(async ({ ctx, next }) => {
|
||||
const client = createGraphQLClient({ saleorApiUrl: ctx.saleorApiUrl, token: ctx.appToken });
|
||||
|
||||
return next({
|
||||
ctx: {
|
||||
apiClient: client,
|
||||
appToken: ctx.appToken,
|
||||
saleorApiUrl: ctx.saleorApiUrl,
|
||||
appId: ctx.appId!,
|
||||
},
|
||||
});
|
||||
});
|
8
apps/orders-export/src/modules/trpc/trpc-app-router.ts
Normal file
8
apps/orders-export/src/modules/trpc/trpc-app-router.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { configurationRouter } from "../configuration/configuration.router";
|
||||
import { router } from "./trpc-server";
|
||||
|
||||
export const appRouter = router({
|
||||
configuration: configurationRouter,
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
15
apps/orders-export/src/modules/trpc/trpc-client.ts
Normal file
15
apps/orders-export/src/modules/trpc/trpc-client.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { createTRPCNext } from "@trpc/next";
|
||||
|
||||
import { createHttpBatchLink } from "@saleor/trpc";
|
||||
import { appBridgeInstance } from "../../pages/_app";
|
||||
import { AppRouter } from "./trpc-app-router";
|
||||
|
||||
export const trpcClient = createTRPCNext<AppRouter>({
|
||||
config() {
|
||||
return {
|
||||
links: [createHttpBatchLink(appBridgeInstance)],
|
||||
queryClientConfig: { defaultOptions: { queries: { refetchOnWindowFocus: false } } },
|
||||
};
|
||||
},
|
||||
ssr: false,
|
||||
});
|
31
apps/orders-export/src/modules/trpc/trpc-server.ts
Normal file
31
apps/orders-export/src/modules/trpc/trpc-server.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { initTRPC } from "@trpc/server";
|
||||
import { TrpcContext } from "@saleor/trpc";
|
||||
import { Permission } from "@saleor/app-sdk/types";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
interface Meta {
|
||||
requiredClientPermissions?: Permission[];
|
||||
updateWebhooks?: boolean;
|
||||
}
|
||||
|
||||
const t = initTRPC
|
||||
.context<TrpcContext>()
|
||||
.meta<Meta>()
|
||||
.create({
|
||||
errorFormatter({ shape, error }) {
|
||||
return {
|
||||
...shape,
|
||||
data: {
|
||||
...shape.data,
|
||||
zodError:
|
||||
error.code === "BAD_REQUEST" && error.cause instanceof ZodError
|
||||
? error.cause.flatten()
|
||||
: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const router = t.router;
|
||||
export const procedure = t.procedure;
|
||||
export const middleware = t.middleware;
|
45
apps/orders-export/src/pages/_app.tsx
Normal file
45
apps/orders-export/src/pages/_app.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
import "@saleor/macaw-ui/next/style";
|
||||
import { trpcClient } from "@/modules/trpc/trpc-client";
|
||||
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
|
||||
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
|
||||
import { GraphQLProvider, NoSSRWrapper, ThemeSynchronizer } from "@saleor/apps-shared";
|
||||
import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { AppProps } from "next/app";
|
||||
|
||||
/**
|
||||
* Ensure instance is a singleton.
|
||||
* TODO: This is React 18 issue, consider hiding this workaround inside app-sdk
|
||||
*/
|
||||
export const appBridgeInstance = typeof window !== "undefined" ? new AppBridge() : undefined;
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function NextApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<NoSSRWrapper>
|
||||
<AppBridgeProvider appBridgeInstance={appBridgeInstance}>
|
||||
<GraphQLProvider>
|
||||
<ThemeProvider>
|
||||
<ThemeSynchronizer />
|
||||
<RoutePropagator />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Box padding={10}>
|
||||
<Component {...pageProps} />
|
||||
</Box>
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>
|
||||
</GraphQLProvider>
|
||||
</AppBridgeProvider>
|
||||
</NoSSRWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default trpcClient.withTRPC(NextApp);
|
13
apps/orders-export/src/pages/_document.tsx
Normal file
13
apps/orders-export/src/pages/_document.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Html, Head, Main, NextScript } from "next/document";
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head />
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
45
apps/orders-export/src/pages/api/manifest.ts
Normal file
45
apps/orders-export/src/pages/api/manifest.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { createManifestHandler } from "@saleor/app-sdk/handlers/next";
|
||||
import { AppManifest } from "@saleor/app-sdk/types";
|
||||
|
||||
import packageJson from "../../../package.json";
|
||||
|
||||
export default createManifestHandler({
|
||||
async manifestFactory({ appBaseUrl }) {
|
||||
const iframeBaseUrl = process.env.APP_IFRAME_BASE_URL ?? appBaseUrl;
|
||||
const apiBaseURL = process.env.APP_API_BASE_URL ?? appBaseUrl;
|
||||
|
||||
const manifest: AppManifest = {
|
||||
about: "Export Saleor orders to spreadsheets",
|
||||
appUrl: iframeBaseUrl,
|
||||
author: "Saleor Commerce",
|
||||
/*
|
||||
* brand: {
|
||||
* logo: {
|
||||
* default: `${apiBaseURL}/logo.png`,
|
||||
* },
|
||||
* },
|
||||
*/
|
||||
dataPrivacyUrl: "https://saleor.io/legal/privacy/",
|
||||
extensions: [
|
||||
/**
|
||||
* Optionally, extend Dashboard with custom UIs
|
||||
* https://docs.saleor.io/docs/3.x/developer/extending/apps/extending-dashboard-with-apps
|
||||
*/
|
||||
],
|
||||
homepageUrl: "https://github.com/saleor/apps",
|
||||
id: "saleor.app.orders-export",
|
||||
name: "Orders Export",
|
||||
permissions: ["MANAGE_ORDERS"],
|
||||
requiredSaleorVersion: ">=3.14 <4",
|
||||
supportUrl: "https://github.com/saleor/apps/discussions",
|
||||
tokenTargetUrl: `${apiBaseURL}/api/register`,
|
||||
version: packageJson.version,
|
||||
/*
|
||||
* TODO Add webhooks disabled and enable then when configured
|
||||
*/
|
||||
webhooks: [],
|
||||
};
|
||||
|
||||
return manifest;
|
||||
},
|
||||
});
|
23
apps/orders-export/src/pages/api/register.ts
Normal file
23
apps/orders-export/src/pages/api/register.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { saleorApp } from "@/saleor-app";
|
||||
import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next";
|
||||
|
||||
const allowedUrlsPattern = process.env.ALLOWED_DOMAIN_PATTERN;
|
||||
|
||||
/**
|
||||
* Required endpoint, called by Saleor to install app.
|
||||
* It will exchange tokens with app, so saleorApp.apl will contain token
|
||||
*/
|
||||
export default createAppRegisterHandler({
|
||||
apl: saleorApp.apl,
|
||||
allowedSaleorUrls: [
|
||||
(url) => {
|
||||
if (allowedUrlsPattern) {
|
||||
const regex = new RegExp(allowedUrlsPattern);
|
||||
|
||||
return regex.test(url);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
],
|
||||
});
|
9
apps/orders-export/src/pages/api/trpc/[trpc].ts
Normal file
9
apps/orders-export/src/pages/api/trpc/[trpc].ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import * as trpcNext from "@trpc/server/adapters/next";
|
||||
|
||||
import { appRouter } from "../../../modules/trpc/trpc-app-router";
|
||||
import { createTrpcContext } from "@saleor/trpc";
|
||||
|
||||
export default trpcNext.createNextApiHandler({
|
||||
router: appRouter,
|
||||
createContext: createTrpcContext,
|
||||
});
|
28
apps/orders-export/src/pages/configuration.tsx
Normal file
28
apps/orders-export/src/pages/configuration.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { AppHeader } from "@/modules/ui/app-header";
|
||||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import { Layout } from "@saleor/apps-ui";
|
||||
import { Box, Text } from "@saleor/macaw-ui/next";
|
||||
import { NextPage } from "next";
|
||||
|
||||
const ConfigurationPage: NextPage = () => {
|
||||
const { appBridgeState } = useAppBridge();
|
||||
|
||||
if (!appBridgeState) {
|
||||
throw new Error("AppBridge is not available.");
|
||||
}
|
||||
|
||||
if (appBridgeState.user?.permissions.includes("MANAGE_APPS") === false) {
|
||||
return <Text>You do not have permission to access this page.</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<AppHeader />
|
||||
<Layout.AppSection marginBottom={14} heading="todo">
|
||||
TODO
|
||||
</Layout.AppSection>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigurationPage;
|
30
apps/orders-export/src/pages/index.tsx
Normal file
30
apps/orders-export/src/pages/index.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { NextPage } from "next";
|
||||
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
|
||||
import { useEffect } from "react";
|
||||
import { useIsMounted } from "usehooks-ts";
|
||||
import { useRouter } from "next/router";
|
||||
import { isInIframe } from "@saleor/apps-shared";
|
||||
|
||||
const IndexPage: NextPage = () => {
|
||||
const { appBridgeState } = useAppBridge();
|
||||
const isMounted = useIsMounted();
|
||||
const { replace } = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (isMounted() && appBridgeState?.ready) {
|
||||
replace("/configuration");
|
||||
}
|
||||
}, [isMounted, appBridgeState?.ready, replace]);
|
||||
|
||||
if (isInIframe()) {
|
||||
return <p>Loading</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Saleor App - Orders Export</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndexPage;
|
35
apps/orders-export/src/saleor-app.ts
Normal file
35
apps/orders-export/src/saleor-app.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { APL, FileAPL, SaleorCloudAPL, UpstashAPL } from "@saleor/app-sdk/APL";
|
||||
import { SaleorApp } from "@saleor/app-sdk/saleor-app";
|
||||
|
||||
const aplType = process.env.APL ?? "file";
|
||||
|
||||
export let apl: APL;
|
||||
|
||||
switch (aplType) {
|
||||
case "upstash":
|
||||
apl = new UpstashAPL();
|
||||
|
||||
break;
|
||||
case "file":
|
||||
apl = new FileAPL();
|
||||
|
||||
break;
|
||||
case "saleor-cloud": {
|
||||
if (!process.env.REST_APL_ENDPOINT || !process.env.REST_APL_TOKEN) {
|
||||
throw new Error("Rest APL is not configured - missing env variables. Check saleor-app.ts");
|
||||
}
|
||||
|
||||
apl = new SaleorCloudAPL({
|
||||
resourceUrl: process.env.REST_APL_ENDPOINT,
|
||||
token: process.env.REST_APL_TOKEN,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error("Invalid APL config, ");
|
||||
}
|
||||
}
|
||||
export const saleorApp = new SaleorApp({
|
||||
apl,
|
||||
});
|
1
apps/orders-export/src/setup-tests.ts
Normal file
1
apps/orders-export/src/setup-tests.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export {};
|
23
apps/orders-export/tsconfig.json
Normal file
23
apps/orders-export/tsconfig.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
28
apps/orders-export/turbo.json
Normal file
28
apps/orders-export/turbo.json
Normal file
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"extends": ["//"],
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"env": [
|
||||
"APL",
|
||||
"APP_DEBUG",
|
||||
"NODE_ENV",
|
||||
"SECRET_KEY",
|
||||
"ALLOWED_DOMAIN_PATTERN",
|
||||
"REST_APL_ENDPOINT",
|
||||
"REST_APL_TOKEN",
|
||||
"NEXT_PUBLIC_VERCEL_ENV",
|
||||
"VERCEL_URL",
|
||||
"PORT",
|
||||
"SENTRY_ORG",
|
||||
"SENTRY_PROJECT",
|
||||
"SENTRY_DSN",
|
||||
"SENTRY_AUTH_TOKEN",
|
||||
"NEXT_PUBLIC_SENTRY_DSN",
|
||||
"SENTRY_ENVIRONMENT",
|
||||
"APP_IFRAME_BASE_URL",
|
||||
"APP_API_BASE_URL"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
17
apps/orders-export/vitest.config.ts
Normal file
17
apps/orders-export/vitest.config.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import react from "@vitejs/plugin-react";
|
||||
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
test: {
|
||||
passWithNoTests: true,
|
||||
environment: "jsdom",
|
||||
setupFiles: "./src/setup-tests.ts",
|
||||
css: false,
|
||||
alias: {
|
||||
"@": "./src",
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,21 +1,5 @@
|
|||
# saleor-app-products-feed
|
||||
|
||||
## 1.12.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ae6dbb1: Removed webhooks on product changes used for feed cache due to changed max execution time.
|
||||
- ae6dbb1: Changed Vercel's maximum execution time to be 5 minutes for feed generation. This should help with the previous limits of 60s, that was not enough for feed to be generated.
|
||||
|
||||
## 1.12.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5d3d81d: Bumped @hookform/resolvers from 2.9.11 to 3.3.1
|
||||
- 5dee65a: Updated dependencies:
|
||||
- @graphql-codegen/cli@5.0.0
|
||||
- 2e29699: Updated Sentry package
|
||||
|
||||
## 1.12.0
|
||||
|
||||
### Minor Changes
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
fragment ProductVariantWebhookPayload on ProductVariant {
|
||||
channel
|
||||
channelListings {
|
||||
channel {
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
fragment ProductWebhookPayload on Product {
|
||||
channel
|
||||
channelListings {
|
||||
channel {
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
subscription ProductCreated {
|
||||
event {
|
||||
... on ProductCreated {
|
||||
product {
|
||||
...ProductWebhookPayload
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
subscription ProductDeleted {
|
||||
event {
|
||||
... on ProductDeleted {
|
||||
product {
|
||||
...ProductWebhookPayload
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
subscription ProductUpdated {
|
||||
event {
|
||||
... on ProductUpdated {
|
||||
product {
|
||||
...ProductWebhookPayload
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
subscription ProductVariantCreated {
|
||||
event {
|
||||
... on ProductVariantCreated {
|
||||
productVariant {
|
||||
...ProductVariantWebhookPayload
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
subscription ProductVariantDeleted {
|
||||
event {
|
||||
... on ProductVariantDeleted {
|
||||
productVariant {
|
||||
...ProductVariantWebhookPayload
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
subscription ProductVariantUpdated {
|
||||
event {
|
||||
... on ProductVariantUpdated {
|
||||
productVariant {
|
||||
...ProductVariantWebhookPayload
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,12 +6,7 @@ const isSentryPropertiesInEnvironment =
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
reactStrictMode: true,
|
||||
transpilePackages: [
|
||||
"@saleor/apps-shared",
|
||||
"@saleor/apps-ui",
|
||||
"@saleor/react-hook-form-macaw",
|
||||
"@saleor/webhook-utils",
|
||||
],
|
||||
transpilePackages: ["@saleor/apps-shared", "@saleor/apps-ui", "@saleor/react-hook-form-macaw"],
|
||||
};
|
||||
|
||||
const configWithSentry = withSentryConfig(
|
||||
|
@ -27,7 +22,7 @@ const configWithSentry = withSentryConfig(
|
|||
tunnelRoute: "/monitoring",
|
||||
hideSourceMaps: true,
|
||||
disableLogger: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = isSentryPropertiesInEnvironment ? configWithSentry : nextConfig;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "saleor-app-products-feed",
|
||||
"version": "1.12.2",
|
||||
"version": "1.12.0",
|
||||
"scripts": {
|
||||
"build": "pnpm generate && next build",
|
||||
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
|
||||
|
@ -13,13 +13,12 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.332.0",
|
||||
"@hookform/resolvers": "^3.3.1",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@saleor/app-sdk": "0.43.1",
|
||||
"@saleor/apps-shared": "workspace:*",
|
||||
"@saleor/apps-ui": "workspace:*",
|
||||
"@saleor/macaw-ui": "0.8.0-pre.127",
|
||||
"@saleor/react-hook-form-macaw": "workspace:*",
|
||||
"@saleor/webhook-utils": "workspace:*",
|
||||
"@sentry/nextjs": "7.67.0",
|
||||
"@tanstack/react-query": "4.29.19",
|
||||
"@trpc/client": "10.38.1",
|
||||
|
@ -28,7 +27,6 @@
|
|||
"@trpc/server": "10.38.1",
|
||||
"@urql/exchange-auth": "^2.1.4",
|
||||
"@vitejs/plugin-react": "4.0.4",
|
||||
"dotenv": "^16.3.1",
|
||||
"fast-xml-parser": "^4.0.15",
|
||||
"graphql": "16.7.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
|
@ -48,7 +46,7 @@
|
|||
"zod": "3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "5.0.0",
|
||||
"@graphql-codegen/cli": "4.0.1",
|
||||
"@graphql-codegen/introspection": "4.0.0",
|
||||
"@graphql-codegen/schema-ast": "4.0.0",
|
||||
"@graphql-codegen/typed-document-node": "5.0.1",
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
# Webhook migration scripts
|
||||
|
||||
Test migration with dry run, operation will not modify any data:
|
||||
`npx tsx scripts/migrations/run-webhooks-migration-dry-run.ts`
|
||||
|
||||
To start the migration run command:
|
||||
`npx tsx scripts/migrations/run-webhooks-migration.ts`
|
|
@ -1,20 +0,0 @@
|
|||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
|
||||
import { SaleorCloudAPL } from "@saleor/app-sdk/APL";
|
||||
|
||||
export const verifyRequiredEnvs = () => {
|
||||
const requiredEnvs = ["SALEOR_CLOUD_TOKEN", "SALEOR_CLOUD_RESOURCE_URL"];
|
||||
|
||||
if (!requiredEnvs.every((env) => process.env[env])) {
|
||||
throw new Error(`Missing envs: ${requiredEnvs.join(" | ")}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchCloudAplEnvs = () => {
|
||||
const saleorAPL = new SaleorCloudAPL({
|
||||
token: process.env.SALEOR_CLOUD_TOKEN!,
|
||||
resourceUrl: process.env.SALEOR_CLOUD_RESOURCE_URL!,
|
||||
});
|
||||
|
||||
return saleorAPL.getAll();
|
||||
};
|
|
@ -1,30 +0,0 @@
|
|||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
|
||||
import * as dotenv from "dotenv";
|
||||
import { fetchCloudAplEnvs, verifyRequiredEnvs } from "./migration-utils";
|
||||
import { updateWebhooksScript } from "./update-webhooks";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const runMigration = async () => {
|
||||
console.log("Starting webhooks migration (dry run)");
|
||||
|
||||
verifyRequiredEnvs();
|
||||
|
||||
console.log("Envs verified, fetching envs");
|
||||
|
||||
const allEnvs = await fetchCloudAplEnvs().catch((r) => {
|
||||
console.error("Could not fetch instances from the APL");
|
||||
console.error(r);
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
for (const env of allEnvs) {
|
||||
await updateWebhooksScript({ authData: env, dryRun: true });
|
||||
}
|
||||
|
||||
console.log("Migration dry run complete");
|
||||
};
|
||||
|
||||
runMigration();
|
|
@ -1,30 +0,0 @@
|
|||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
|
||||
import * as dotenv from "dotenv";
|
||||
import { fetchCloudAplEnvs, verifyRequiredEnvs } from "./migration-utils";
|
||||
import { updateWebhooksScript } from "./update-webhooks";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const runMigration = async () => {
|
||||
console.log("Starting running migration");
|
||||
|
||||
verifyRequiredEnvs();
|
||||
|
||||
console.log("Envs verified, fetching envs");
|
||||
|
||||
const allEnvs = await fetchCloudAplEnvs().catch((r) => {
|
||||
console.error("Could not fetch instances from the APL");
|
||||
console.error(r);
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
for (const env of allEnvs) {
|
||||
await updateWebhooksScript({ authData: env, dryRun: false });
|
||||
}
|
||||
|
||||
console.log("Migration complete");
|
||||
};
|
||||
|
||||
runMigration();
|
|
@ -1,29 +0,0 @@
|
|||
/* eslint-disable turbo/no-undeclared-env-vars */
|
||||
|
||||
import { createGraphQLClient } from "@saleor/apps-shared";
|
||||
import { AuthData } from "@saleor/app-sdk/APL";
|
||||
import { webhookMigrationRunner } from "@saleor/webhook-utils";
|
||||
|
||||
export const updateWebhooksScript = async ({
|
||||
authData,
|
||||
dryRun,
|
||||
}: {
|
||||
authData: AuthData;
|
||||
dryRun: boolean;
|
||||
}) => {
|
||||
console.log("Working on env: ", authData.saleorApiUrl);
|
||||
|
||||
const client = createGraphQLClient({
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
token: authData.token,
|
||||
});
|
||||
|
||||
await webhookMigrationRunner({
|
||||
client,
|
||||
dryRun,
|
||||
getManifests: async ({ appDetails }) => {
|
||||
// Products feed application has currently no webhooks, so we return empty array
|
||||
return [];
|
||||
},
|
||||
});
|
||||
};
|
|
@ -2,6 +2,7 @@ import { router } from "../trpc/trpc-server";
|
|||
import { protectedClientProcedure } from "../trpc/protected-client-procedure";
|
||||
import { createLogger } from "@saleor/apps-shared";
|
||||
|
||||
import { updateCacheForConfigurations } from "../metadata-cache/update-cache-for-configurations";
|
||||
import { AppConfigSchema, imageSizeInputSchema, titleTemplateInputSchema } from "./app-config";
|
||||
import { z } from "zod";
|
||||
import { createS3ClientFromConfiguration } from "../file-storage/s3/create-s3-client-from-configuration";
|
||||
|
@ -105,6 +106,17 @@ export const appConfigurationRouter = router({
|
|||
}) => {
|
||||
const config = await getConfig();
|
||||
|
||||
/**
|
||||
* TODO Check if this has to run, once its cached, it should be invalidated by webhooks only.
|
||||
*
|
||||
* But this operation isn't expensive and users will not continuously save this form
|
||||
*/
|
||||
await updateCacheForConfigurations({
|
||||
client: apiClient,
|
||||
channelsSlugs: [input.channelSlug],
|
||||
saleorApiUrl: saleorApiUrl,
|
||||
});
|
||||
|
||||
logger.debug({ channel: input.channelSlug }, "Updated cache for channel");
|
||||
|
||||
config.setChannelUrls(input.channelSlug, input.urls);
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
||||
|
||||
export class CacheConfigurator {
|
||||
private metadataKeyPrefix = "cursor-cache-";
|
||||
|
||||
constructor(private metadataManager: SettingsManager, private saleorApiUrl: string) {}
|
||||
|
||||
private constructKey(channel: string) {
|
||||
return this.metadataKeyPrefix + channel;
|
||||
}
|
||||
|
||||
get({ channel }: { channel: string }): Promise<string[] | undefined> {
|
||||
return this.metadataManager.get(this.constructKey(channel), this.saleorApiUrl).then((data) => {
|
||||
if (!data) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
throw new Error("Invalid metadata value, can't be parsed");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
set({ channel, value }: { channel: string; value: string[] }): Promise<void> {
|
||||
return this.metadataManager.set({
|
||||
key: this.constructKey(channel),
|
||||
value: JSON.stringify(value),
|
||||
domain: this.saleorApiUrl,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import { createLogger } from "@saleor/apps-shared";
|
||||
import { CacheConfigurator } from "./cache-configurator";
|
||||
import { createSettingsManager } from "../../lib/metadata-manager";
|
||||
import { getCursors } from "../google-feed/fetch-product-data";
|
||||
import { Client } from "urql";
|
||||
|
||||
interface UpdateCacheForConfigurationsArgs {
|
||||
client: Client;
|
||||
saleorApiUrl: string;
|
||||
channelsSlugs: string[];
|
||||
}
|
||||
|
||||
export const updateCacheForConfigurations = async ({
|
||||
client,
|
||||
channelsSlugs,
|
||||
saleorApiUrl,
|
||||
}: UpdateCacheForConfigurationsArgs) => {
|
||||
const logger = createLogger({ saleorApiUrl: saleorApiUrl });
|
||||
|
||||
logger.debug("Updating the cursor cache");
|
||||
|
||||
const cache = new CacheConfigurator(createSettingsManager(client), saleorApiUrl);
|
||||
|
||||
const cacheUpdatePromises = channelsSlugs.map(async (channel) => {
|
||||
const cursors = await getCursors({ client, channel });
|
||||
|
||||
await cache.set({ channel, value: cursors });
|
||||
});
|
||||
|
||||
await Promise.all(cacheUpdatePromises);
|
||||
|
||||
logger.debug("Cursor cache updated");
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
import { GraphqlClientFactory } from "../../lib/create-graphql-client";
|
||||
import { updateCacheForConfigurations } from "./update-cache-for-configurations";
|
||||
import { AuthData } from "@saleor/app-sdk/APL";
|
||||
import {
|
||||
ProductVariantWebhookPayloadFragment,
|
||||
ProductWebhookPayloadFragment,
|
||||
} from "../../../generated/graphql";
|
||||
import { NextApiResponse } from "next";
|
||||
|
||||
type ChannelFragment =
|
||||
| Pick<ProductWebhookPayloadFragment, "channel" | "channelListings">
|
||||
| Pick<ProductVariantWebhookPayloadFragment, "channel" | "channelListings">;
|
||||
|
||||
export const updateCacheOnWebhook = async ({
|
||||
channels,
|
||||
authData,
|
||||
res,
|
||||
}: {
|
||||
authData: AuthData;
|
||||
channels: ChannelFragment;
|
||||
res: NextApiResponse;
|
||||
}) => {
|
||||
const client = GraphqlClientFactory.fromAuthData(authData);
|
||||
|
||||
const channelsSlugs = [
|
||||
channels.channel,
|
||||
...(channels.channelListings?.map((cl) => cl.channel.slug) ?? []),
|
||||
].filter((c) => c) as string[];
|
||||
|
||||
if (channelsSlugs.length === 0) {
|
||||
return res.status(200).end();
|
||||
}
|
||||
|
||||
await updateCacheForConfigurations({
|
||||
channelsSlugs,
|
||||
client,
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
});
|
||||
|
||||
return res.status(200).end();
|
||||
};
|
|
@ -6,6 +6,9 @@ import { fetchProductData } from "../../../../../modules/google-feed/fetch-produ
|
|||
import { GoogleFeedSettingsFetcher } from "../../../../../modules/google-feed/get-google-feed-settings";
|
||||
import { generateGoogleXmlFeed } from "../../../../../modules/google-feed/generate-google-xml-feed";
|
||||
import { fetchShopData } from "../../../../../modules/google-feed/fetch-shop-data";
|
||||
import { CacheConfigurator } from "../../../../../modules/metadata-cache/cache-configurator";
|
||||
import { createSettingsManager } from "../../../../../lib/metadata-manager";
|
||||
import { GraphqlClientFactory } from "../../../../../lib/create-graphql-client";
|
||||
import { uploadFile } from "../../../../../modules/file-storage/s3/upload-file";
|
||||
import { createS3ClientFromConfiguration } from "../../../../../modules/file-storage/s3/create-s3-client-from-configuration";
|
||||
import { getFileDetails } from "../../../../../modules/file-storage/s3/get-file-details";
|
||||
|
@ -13,10 +16,6 @@ import { getDownloadUrl, getFileName } from "../../../../../modules/file-storage
|
|||
import { RootConfig } from "../../../../../modules/app-configuration/app-config";
|
||||
import { z, ZodError } from "zod";
|
||||
|
||||
export const config = {
|
||||
maxDuration: 5 * 60, // 5 minutes
|
||||
};
|
||||
|
||||
// By default we cache the feed for 5 minutes. This can be changed by setting the FEED_CACHE_MAX_AGE
|
||||
const FEED_CACHE_MAX_AGE = process.env.FEED_CACHE_MAX_AGE
|
||||
? parseInt(process.env.FEED_CACHE_MAX_AGE, 10)
|
||||
|
@ -158,10 +157,23 @@ export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
|
||||
logger.debug("Generating a new feed");
|
||||
|
||||
const cacheClient = GraphqlClientFactory.fromAuthData(authData);
|
||||
|
||||
if (!cacheClient) {
|
||||
logger.error("Can't create the gql client");
|
||||
return res.status(500).end();
|
||||
}
|
||||
|
||||
// get cached cursors
|
||||
const cache = new CacheConfigurator(createSettingsManager(cacheClient), authData.saleorApiUrl);
|
||||
|
||||
const cursors = await cache.get({ channel });
|
||||
|
||||
// TODO: instead of separate variants, use group id https://support.google.com/merchants/answer/6324507?hl=en
|
||||
let productVariants: GoogleFeedProductVariantFragment[] = [];
|
||||
|
||||
try {
|
||||
productVariants = await fetchProductData({ client, channel, imageSize });
|
||||
productVariants = await fetchProductData({ client, channel, cursors, imageSize });
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return res.status(400).end();
|
||||
|
|
|
@ -2,6 +2,11 @@ import { createManifestHandler } from "@saleor/app-sdk/handlers/next";
|
|||
import { AppManifest } from "@saleor/app-sdk/types";
|
||||
|
||||
import packageJson from "../../../package.json";
|
||||
import { webhookProductCreated } from "./webhooks/product_created";
|
||||
import { webhookProductDeleted } from "./webhooks/product_deleted";
|
||||
import { webhookProductVariantCreated } from "./webhooks/product_variant_created";
|
||||
import { webhookProductVariantDeleted } from "./webhooks/product_variant_deleted";
|
||||
import { webhookProductVariantUpdated } from "./webhooks/product_variant_updated";
|
||||
|
||||
export default createManifestHandler({
|
||||
async manifestFactory({ appBaseUrl }) {
|
||||
|
@ -26,7 +31,13 @@ export default createManifestHandler({
|
|||
supportUrl: "https://github.com/saleor/apps/discussions",
|
||||
tokenTargetUrl: `${apiBaseURL}/api/register`,
|
||||
version: packageJson.version,
|
||||
webhooks: [],
|
||||
webhooks: [
|
||||
webhookProductCreated.getWebhookManifest(apiBaseURL),
|
||||
webhookProductDeleted.getWebhookManifest(apiBaseURL),
|
||||
webhookProductVariantCreated.getWebhookManifest(apiBaseURL),
|
||||
webhookProductVariantDeleted.getWebhookManifest(apiBaseURL),
|
||||
webhookProductVariantUpdated.getWebhookManifest(apiBaseURL),
|
||||
],
|
||||
};
|
||||
|
||||
return manifest;
|
||||
|
|
41
apps/products-feed/src/pages/api/webhooks/product_created.ts
Normal file
41
apps/products-feed/src/pages/api/webhooks/product_created.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||
import { createLogger } from "@saleor/apps-shared";
|
||||
import { saleorApp } from "../../../saleor-app";
|
||||
import {
|
||||
ProductCreatedDocument,
|
||||
ProductWebhookPayloadFragment,
|
||||
} from "../../../../generated/graphql";
|
||||
import { updateCacheOnWebhook } from "../../../modules/metadata-cache/update-cache-on-webhook";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const webhookProductCreated = new SaleorAsyncWebhook<ProductWebhookPayloadFragment>({
|
||||
webhookPath: "api/webhooks/product_created",
|
||||
event: "PRODUCT_CREATED",
|
||||
apl: saleorApp.apl,
|
||||
query: ProductCreatedDocument,
|
||||
// todo make it disabled by default, enable when app is configured
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
const logger = createLogger({
|
||||
service: "webhook-product_created",
|
||||
});
|
||||
|
||||
export const handler: NextWebhookApiHandler<ProductWebhookPayloadFragment> = async (
|
||||
req,
|
||||
res,
|
||||
context
|
||||
) => {
|
||||
await updateCacheOnWebhook({
|
||||
authData: context.authData,
|
||||
channels: context.payload,
|
||||
res,
|
||||
});
|
||||
};
|
||||
|
||||
export default webhookProductCreated.createHandler(handler);
|
40
apps/products-feed/src/pages/api/webhooks/product_deleted.ts
Normal file
40
apps/products-feed/src/pages/api/webhooks/product_deleted.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||
import {
|
||||
ProductDeletedDocument,
|
||||
ProductWebhookPayloadFragment,
|
||||
} from "../../../../generated/graphql";
|
||||
import { saleorApp } from "../../../saleor-app";
|
||||
import { createLogger } from "@saleor/apps-shared";
|
||||
import { updateCacheOnWebhook } from "../../../modules/metadata-cache/update-cache-on-webhook";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const webhookProductDeleted = new SaleorAsyncWebhook<ProductWebhookPayloadFragment>({
|
||||
webhookPath: "api/webhooks/product_deleted",
|
||||
event: "PRODUCT_DELETED",
|
||||
apl: saleorApp.apl,
|
||||
query: ProductDeletedDocument,
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
const logger = createLogger({
|
||||
service: "webhook_product_deleted",
|
||||
});
|
||||
|
||||
export const handler: NextWebhookApiHandler<ProductWebhookPayloadFragment> = async (
|
||||
req,
|
||||
res,
|
||||
context
|
||||
) => {
|
||||
await updateCacheOnWebhook({
|
||||
authData: context.authData,
|
||||
channels: context.payload,
|
||||
res,
|
||||
});
|
||||
};
|
||||
|
||||
export default webhookProductDeleted.createHandler(handler);
|
40
apps/products-feed/src/pages/api/webhooks/product_updated.ts
Normal file
40
apps/products-feed/src/pages/api/webhooks/product_updated.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||
import {
|
||||
ProductUpdatedDocument,
|
||||
ProductWebhookPayloadFragment,
|
||||
} from "../../../../generated/graphql";
|
||||
import { saleorApp } from "../../../saleor-app";
|
||||
import { createLogger } from "@saleor/apps-shared";
|
||||
import { updateCacheOnWebhook } from "../../../modules/metadata-cache/update-cache-on-webhook";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const webhookProductUpdated = new SaleorAsyncWebhook<ProductWebhookPayloadFragment>({
|
||||
webhookPath: "api/webhooks/product_updated",
|
||||
event: "PRODUCT_UPDATED",
|
||||
apl: saleorApp.apl,
|
||||
query: ProductUpdatedDocument,
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
const logger = createLogger({
|
||||
service: "webhookProductUpdatedWebhookHandler",
|
||||
});
|
||||
|
||||
export const handler: NextWebhookApiHandler<ProductWebhookPayloadFragment> = async (
|
||||
req,
|
||||
res,
|
||||
context
|
||||
) => {
|
||||
await updateCacheOnWebhook({
|
||||
authData: context.authData,
|
||||
channels: context.payload,
|
||||
res,
|
||||
});
|
||||
};
|
||||
|
||||
export default webhookProductUpdated.createHandler(handler);
|
|
@ -0,0 +1,41 @@
|
|||
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||
import {
|
||||
ProductVariantCreatedDocument,
|
||||
ProductVariantWebhookPayloadFragment,
|
||||
} from "../../../../generated/graphql";
|
||||
import { saleorApp } from "../../../saleor-app";
|
||||
import { createLogger } from "@saleor/apps-shared";
|
||||
import { updateCacheOnWebhook } from "../../../modules/metadata-cache/update-cache-on-webhook";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const webhookProductVariantCreated =
|
||||
new SaleorAsyncWebhook<ProductVariantWebhookPayloadFragment>({
|
||||
webhookPath: "api/webhooks/product_variant_created",
|
||||
event: "PRODUCT_VARIANT_CREATED",
|
||||
apl: saleorApp.apl,
|
||||
query: ProductVariantCreatedDocument,
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
const logger = createLogger({
|
||||
service: "PRODUCT_VARIANT_CREATED webhook",
|
||||
});
|
||||
|
||||
export const handler: NextWebhookApiHandler<ProductVariantWebhookPayloadFragment> = async (
|
||||
req,
|
||||
res,
|
||||
context
|
||||
) => {
|
||||
await updateCacheOnWebhook({
|
||||
authData: context.authData,
|
||||
channels: context.payload,
|
||||
res,
|
||||
});
|
||||
};
|
||||
|
||||
export default webhookProductVariantCreated.createHandler(handler);
|
|
@ -0,0 +1,41 @@
|
|||
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||
import { createLogger } from "@saleor/apps-shared";
|
||||
import {
|
||||
ProductVariantDeletedDocument,
|
||||
ProductVariantWebhookPayloadFragment,
|
||||
} from "../../../../generated/graphql";
|
||||
import { saleorApp } from "../../../saleor-app";
|
||||
import { updateCacheOnWebhook } from "../../../modules/metadata-cache/update-cache-on-webhook";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const webhookProductVariantDeleted =
|
||||
new SaleorAsyncWebhook<ProductVariantWebhookPayloadFragment>({
|
||||
webhookPath: "api/webhooks/product_variant_deleted",
|
||||
event: "PRODUCT_VARIANT_DELETED",
|
||||
apl: saleorApp.apl,
|
||||
query: ProductVariantDeletedDocument,
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
const logger = createLogger({
|
||||
service: "PRODUCT_VARIANT_DELETED",
|
||||
});
|
||||
|
||||
export const handler: NextWebhookApiHandler<ProductVariantWebhookPayloadFragment> = async (
|
||||
req,
|
||||
res,
|
||||
context
|
||||
) => {
|
||||
await updateCacheOnWebhook({
|
||||
authData: context.authData,
|
||||
channels: context.payload,
|
||||
res,
|
||||
});
|
||||
};
|
||||
|
||||
export default webhookProductVariantDeleted.createHandler(handler);
|
|
@ -0,0 +1,41 @@
|
|||
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||
import { createLogger } from "@saleor/apps-shared";
|
||||
import {
|
||||
ProductVariantUpdatedDocument,
|
||||
ProductVariantWebhookPayloadFragment,
|
||||
} from "../../../../generated/graphql";
|
||||
import { saleorApp } from "../../../saleor-app";
|
||||
import { updateCacheOnWebhook } from "../../../modules/metadata-cache/update-cache-on-webhook";
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const webhookProductVariantUpdated =
|
||||
new SaleorAsyncWebhook<ProductVariantWebhookPayloadFragment>({
|
||||
webhookPath: "api/webhooks/product_variant_updated",
|
||||
event: "PRODUCT_VARIANT_UPDATED",
|
||||
apl: saleorApp.apl,
|
||||
query: ProductVariantUpdatedDocument,
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
const logger = createLogger({
|
||||
service: "webhookProductVariantUpdatedWebhookHandler",
|
||||
});
|
||||
|
||||
export const handler: NextWebhookApiHandler<ProductVariantWebhookPayloadFragment> = async (
|
||||
req,
|
||||
res,
|
||||
context
|
||||
) => {
|
||||
await updateCacheOnWebhook({
|
||||
authData: context.authData,
|
||||
channels: context.payload,
|
||||
res,
|
||||
});
|
||||
};
|
||||
|
||||
export default webhookProductVariantUpdated.createHandler(handler);
|
|
@ -1,50 +1,5 @@
|
|||
# saleor-app-search
|
||||
|
||||
## 1.16.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 1982d81: Added a new `pricing` field to the Algolia object representation. It now passes variant pricing representation from GraphQL:
|
||||
|
||||
```graphQL
|
||||
price {
|
||||
gross {
|
||||
amount
|
||||
}
|
||||
net {
|
||||
amount
|
||||
}
|
||||
}
|
||||
discount {
|
||||
gross {
|
||||
amount
|
||||
}
|
||||
net {
|
||||
amount
|
||||
}
|
||||
}
|
||||
onSale
|
||||
priceUndiscounted {
|
||||
gross {
|
||||
amount
|
||||
}
|
||||
net {
|
||||
amount
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 5d3d81d: Bumped @hookform/resolvers from 2.9.11 to 3.3.1
|
||||
- 5dee65a: Updated dependencies:
|
||||
- @graphql-codegen/cli@5.0.0
|
||||
- 2e29699: Updated Sentry package
|
||||
- 7e0755e: Webhook migration scripts has been moved to the shared package.
|
||||
- Updated dependencies [5dee65a]
|
||||
- Updated dependencies [7e0755e]
|
||||
- @saleor/webhook-utils@0.0.1
|
||||
|
||||
## 1.15.0
|
||||
|
||||
### Minor Changes
|
||||
|
|
|
@ -10,26 +10,7 @@ fragment ProductVariantData on ProductVariant {
|
|||
price {
|
||||
gross {
|
||||
amount
|
||||
}
|
||||
net {
|
||||
amount
|
||||
}
|
||||
}
|
||||
discount {
|
||||
gross {
|
||||
amount
|
||||
}
|
||||
net {
|
||||
amount
|
||||
}
|
||||
}
|
||||
onSale
|
||||
priceUndiscounted {
|
||||
gross {
|
||||
amount
|
||||
}
|
||||
net {
|
||||
amount
|
||||
currency
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "saleor-app-search",
|
||||
"version": "1.16.0",
|
||||
"version": "1.15.0",
|
||||
"scripts": {
|
||||
"build": "pnpm generate && next build",
|
||||
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
|
||||
|
@ -12,7 +12,7 @@
|
|||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.3.1",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@saleor/app-sdk": "0.43.1",
|
||||
"@saleor/apps-shared": "workspace:*",
|
||||
"@saleor/apps-ui": "workspace:*",
|
||||
|
@ -44,7 +44,7 @@
|
|||
"zod": "3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "5.0.0",
|
||||
"@graphql-codegen/cli": "4.0.1",
|
||||
"@graphql-codegen/introspection": "4.0.0",
|
||||
"@graphql-codegen/schema-ast": "4.0.0",
|
||||
"@graphql-codegen/typed-document-node": "5.0.1",
|
||||
|
|
|
@ -78,15 +78,9 @@ export class AlgoliaSearchProvider implements SearchProvider {
|
|||
"productId",
|
||||
"inStock",
|
||||
"categories",
|
||||
"grossPrice",
|
||||
"attributes",
|
||||
"collections",
|
||||
"pricing.price.net",
|
||||
"pricing.price.gross",
|
||||
"pricing.discount.net",
|
||||
"pricing.discount.gross",
|
||||
"pricing.priceUndiscounted.net",
|
||||
"pricing.priceUndiscounted.gross",
|
||||
"pricing.onSale",
|
||||
],
|
||||
attributeForDistinct: "productId",
|
||||
numericAttributesForFiltering: ["grossPrice"],
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue