Add Handler factories (#61)

* Add createManifestHandler

* Add register handler factory

* Add docs to handlers factory

* Improve tsup configuration (select entry points)

* Reexport symbols from index

* Improve tests
This commit is contained in:
Lukasz Ostrowski 2022-09-21 11:25:01 +02:00 committed by GitHub
parent df504ecd29
commit 0352356fd3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 632 additions and 61 deletions

75
docs/api-handlers.md Normal file
View file

@ -0,0 +1,75 @@
# Api Handlers
Saleor Apps are meant to work in serverless environment, where Cloud Functions are the foundations of server-side code.
Currently, Saleor heavily relies on Next.js, but in the future, other platforms will be supported.
## Required handlers
Saleor requires 2 endpoints to be available for a standalone app:
- Manifest endpoint - Returns JSON object with app properties, like its name or permissions. [Read more](https://docs.saleor.io/docs/3.x/developer/extending/apps/manifest)
- Register endpoint - During the installation process, Saleor sends `POST` request with auth token to this endpoint. [Read more](https://docs.saleor.io/docs/3.x/developer/extending/apps/installing-apps#installation-using-graphql-api)
## Api handlers built-in SDK
To hide Saleor internal logic, app-sdk provides handlers factories. They should work with minimal configuration, leaving
App creators space for domain logic.
### Manifest handler factory
Example usage of manifest handler in Next.js
```typescript
// pages/api/manifest.ts
import { createManifestHandler } from "@app-sdk/handlers/next";
export default createManifestHandler({
manifestFactory(context) {
return {
name: "My Saleor App",
tokenTargetUrl: `${context.appBaseUrl}/api/register`,
appUrl: context.appBaseUrl,
permissions: [],
id: "my-saleor-app",
version: "1",
};
},
});
```
Options provided to handler factory
```typescript
type CreateManifestHandlerOptions = {
manifestFactory(context: { appBaseUrl: string }): AppManifest;
};
```
See [source](./src/handlers/next/create-manifest-handler.ts) for more details. See [manifest](../src/types.ts) too.
### App register handler factory
Example usage of app register handler in Next.js
```typescript
// pages/api/register.ts
import { createAppRegisterHandler } from "@app-sdk/handlers/next";
import { VercelAPL } from "./vercel-apl";
export default createAppRegisterHandler({
apl: new VercelAPL(),
});
```
Options provided to handler factory
```typescript
export type CreateAppRegisterHandlerOptions = {
apl: APL;
};
```
See [APL](./apl.md) for details what is Auth Persistence Layer in Saleor apps

View file

@ -6,8 +6,8 @@
"main": "index.js",
"scripts": {
"prepublishOnly": "pnpm build",
"watch": "tsup-node src/* --format esm,cjs --dts --watch",
"build": "tsup-node src/* --format esm,cjs --dts && clear-package-json package.json -o dist/package.json --fields publishConfig",
"watch": "tsup --watch",
"build": "tsup && clear-package-json package.json -o dist/package.json --fields publishConfig",
"clean": "rm -rf ./dist/*",
"test": "vitest",
"test:ci": "CI=true vitest --coverage",
@ -20,7 +20,8 @@
"license": "ISC",
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
"react-dom": ">=17",
"next": "^12"
},
"dependencies": {
"debug": "^4.3.4",
@ -28,11 +29,13 @@
"graphql": "^16.6.0",
"jose": "^4.9.2",
"node-fetch": "^3.2.10",
"retes": "^0.32.0",
"retes": "^0.33.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"@vitest/coverage-c8": "^0.23.2",
"node-mocks-http": "^1.11.0",
"next": "^12.3.0",
"@testing-library/dom": "^8.17.1",
"@testing-library/react": "^13.4.0",
"@types/debug": "^4.1.7",
@ -98,6 +101,11 @@
"import": "./app-bridge/index.mjs",
"require": "./app-bridge/index.js"
},
"./handlers/next": {
"types": "./handlers/next/index.d.ts",
"import": "./handlers/next/index.mjs",
"require": "./handlers/next/index.js"
},
".": {
"types": "./index.d.ts",
"import": "./index.mjs",

View file

@ -29,12 +29,14 @@ specifiers:
husky: ^8.0.1
jose: ^4.9.2
jsdom: ^20.0.0
next: ^12.3.0
node-fetch: ^3.2.10
node-mocks-http: ^1.11.0
prettier: 2.7.1
react: ^18.2.0
react-dom: 18.2.0
release-it: ^15.4.1
retes: ^0.32.0
retes: ^0.33.0
tsm: ^2.2.2
tsup: ^6.2.3
typescript: ^4.8.2
@ -49,7 +51,7 @@ dependencies:
graphql: 16.6.0
jose: 4.9.2
node-fetch: 3.2.10
retes: 0.32.0
retes: 0.33.0
uuid: 8.3.2
devDependencies:
@ -77,6 +79,8 @@ devDependencies:
eslint-plugin-simple-import-sort: 8.0.0_eslint@8.23.0
husky: 8.0.1
jsdom: 20.0.0
next: 12.3.0_biqbaboplfbrettd7655fr4n2y
node-mocks-http: 1.11.0
prettier: 2.7.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
@ -482,6 +486,127 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
/@next/env/12.3.0:
resolution: {integrity: sha512-PTJpjAFVbzBQ9xXpzMTroShvD5YDIIy46jQ7d4LrWpY+/5a8H90Tm8hE3Hvkc5RBRspVo7kvEOnqQms0A+2Q6w==}
dev: true
/@next/swc-android-arm-eabi/12.3.0:
resolution: {integrity: sha512-/PuirPnAKsYBw93w/7Q9hqy+KGOU9mjYprZ/faxMUJh/dc6v3rYLxkZKNG9nFPIW4QKNTCnhP40xF9hLnxO+xg==}
engines: {node: '>= 10'}
cpu: [arm]
os: [android]
requiresBuild: true
dev: true
optional: true
/@next/swc-android-arm64/12.3.0:
resolution: {integrity: sha512-OaI+FhAM6P9B6Ybwbn0Zl8YwWido0lLwhDBi9WiYCh4RQmIXAyVIoIJPHo4fP05+mXaJ/k1trvDvuURvHOq2qw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
requiresBuild: true
dev: true
optional: true
/@next/swc-darwin-arm64/12.3.0:
resolution: {integrity: sha512-9s4d3Mhii+WFce8o8Jok7WC3Bawkr9wEUU++SJRptjU1L5tsfYJMrSYCACHLhZujziNDLyExe4Hwwsccps1sfg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@next/swc-darwin-x64/12.3.0:
resolution: {integrity: sha512-2scC4MqUTwGwok+wpVxP+zWp7WcCAVOtutki2E1n99rBOTnUOX6qXkgxSy083yBN6GqwuC/dzHeN7hIKjavfRA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@next/swc-freebsd-x64/12.3.0:
resolution: {integrity: sha512-xAlruUREij/bFa+qsE1tmsP28t7vz02N4ZDHt2lh3uJUniE0Ne9idyIDLc1Ed0IF2RjfgOp4ZVunuS3OM0sngw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [freebsd]
requiresBuild: true
dev: true
optional: true
/@next/swc-linux-arm-gnueabihf/12.3.0:
resolution: {integrity: sha512-jin2S4VT/cugc2dSZEUIabhYDJNgrUh7fufbdsaAezgcQzqfdfJqfxl4E9GuafzB4cbRPTaqA0V5uqbp0IyGkQ==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@next/swc-linux-arm64-gnu/12.3.0:
resolution: {integrity: sha512-RqJHDKe0WImeUrdR0kayTkRWgp4vD/MS7g0r6Xuf8+ellOFH7JAAJffDW3ayuVZeMYOa7RvgNFcOoWnrTUl9Nw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@next/swc-linux-arm64-musl/12.3.0:
resolution: {integrity: sha512-nvNWoUieMjvDjpYJ/4SQe9lQs2xMj6ZRs8N+bmTrVu9leY2Fg3WD6W9p/1uU9hGO8u+OdF13wc4iRShu/WYIHg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@next/swc-linux-x64-gnu/12.3.0:
resolution: {integrity: sha512-4ajhIuVU9PeQCMMhdDgZTLrHmjbOUFuIyg6J19hZqwEwDTSqQyrSLkbJs2Nd7IRiM6Ul/XyrtEFCpk4k+xD2+w==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@next/swc-linux-x64-musl/12.3.0:
resolution: {integrity: sha512-U092RBYbaGxoMAwpauePJEu2PuZSEoUCGJBvsptQr2/2XIMwAJDYM4c/M5NfYEsBr+yjvsYNsOpYfeQ88D82Yg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@next/swc-win32-arm64-msvc/12.3.0:
resolution: {integrity: sha512-pzSzaxjDEJe67bUok9Nxf9rykbJfHXW0owICFsPBsqHyc+cr8vpF7g9e2APTCddtVhvjkga9ILoZJ9NxWS7Yiw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@next/swc-win32-ia32-msvc/12.3.0:
resolution: {integrity: sha512-MQGUpMbYhQmTZ06a9e0hPQJnxFMwETo2WtyAotY3GEzbNCQVbCGhsvqEKcl+ZEHgShlHXUWvSffq1ZscY6gK7A==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@next/swc-win32-x64-msvc/12.3.0:
resolution: {integrity: sha512-C/nw6OgQpEULWqs+wgMHXGvlJLguPRFFGqR2TAqWBerQ8J+Sg3z1ZTqwelkSi4FoqStGuZ2UdFHIDN1ySmR1xA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@nodelib/fs.scandir/2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -648,6 +773,12 @@ packages:
engines: {node: '>=14.16'}
dev: true
/@swc/helpers/0.4.11:
resolution: {integrity: sha512-rEUrBSGIoSFuYxwBYtlUFMlE2CwGhmW+w9355/5oduSw8e5h2+Tj4UrAGNNgP9915++wj5vkQo0UuOBqOAq4nw==}
dependencies:
tslib: 2.4.0
dev: true
/@szmarczak/http-timer/5.0.1:
resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==}
engines: {node: '>=14.16'}
@ -976,6 +1107,14 @@ packages:
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
dev: true
/accepts/1.3.8:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
dependencies:
mime-types: 2.1.35
negotiator: 0.6.3
dev: true
/acorn-globals/6.0.0:
resolution: {integrity: sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==}
dependencies:
@ -1520,6 +1659,13 @@ packages:
resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==}
dev: true
/content-disposition/0.5.4:
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
engines: {node: '>= 0.6'}
dependencies:
safe-buffer: 5.2.1
dev: true
/convert-source-map/1.8.0:
resolution: {integrity: sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==}
dependencies:
@ -1702,6 +1848,11 @@ packages:
engines: {node: '>=0.4.0'}
dev: true
/depd/1.1.2:
resolution: {integrity: sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==}
engines: {node: '>= 0.6'}
dev: true
/depd/2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
@ -2801,6 +2952,11 @@ packages:
dependencies:
fetch-blob: 3.2.0
/fresh/0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}
dev: true
/fs-extra/8.1.0:
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
engines: {node: '>=6 <7 || >=8'}
@ -3784,6 +3940,15 @@ packages:
semver: 6.3.0
dev: true
/media-typer/0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
dev: true
/merge-descriptors/1.0.1:
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
dev: true
/merge-stream/2.0.0:
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
dev: true
@ -3792,6 +3957,11 @@ packages:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
/methods/1.1.2:
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
engines: {node: '>= 0.6'}
dev: true
/micromatch/4.0.5:
resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==}
engines: {node: '>=8.6'}
@ -3811,6 +3981,12 @@ packages:
mime-db: 1.52.0
dev: true
/mime/1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
engines: {node: '>=4'}
hasBin: true
dev: true
/mimic-fn/2.1.0:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
@ -3879,6 +4055,11 @@ packages:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: true
/negotiator/0.6.3:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
dev: true
/netmask/2.0.2:
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
engines: {node: '>= 0.4.0'}
@ -3891,6 +4072,51 @@ packages:
type-fest: 2.19.0
dev: true
/next/12.3.0_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-GpzI6me9V1+XYtfK0Ae9WD0mKqHyzQlGq1xH1rzNIYMASo4Tkl4rTe9jSqtBpXFhOS33KohXs9ZY38Akkhdciw==}
engines: {node: '>=12.22.0'}
hasBin: true
peerDependencies:
fibers: '>= 3.1.0'
node-sass: ^6.0.0 || ^7.0.0
react: ^17.0.2 || ^18.0.0-0
react-dom: ^17.0.2 || ^18.0.0-0
sass: ^1.3.0
peerDependenciesMeta:
fibers:
optional: true
node-sass:
optional: true
sass:
optional: true
dependencies:
'@next/env': 12.3.0
'@swc/helpers': 0.4.11
caniuse-lite: 1.0.30001390
postcss: 8.4.14
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
styled-jsx: 5.0.6_react@18.2.0
use-sync-external-store: 1.2.0_react@18.2.0
optionalDependencies:
'@next/swc-android-arm-eabi': 12.3.0
'@next/swc-android-arm64': 12.3.0
'@next/swc-darwin-arm64': 12.3.0
'@next/swc-darwin-x64': 12.3.0
'@next/swc-freebsd-x64': 12.3.0
'@next/swc-linux-arm-gnueabihf': 12.3.0
'@next/swc-linux-arm64-gnu': 12.3.0
'@next/swc-linux-arm64-musl': 12.3.0
'@next/swc-linux-x64-gnu': 12.3.0
'@next/swc-linux-x64-musl': 12.3.0
'@next/swc-win32-arm64-msvc': 12.3.0
'@next/swc-win32-ia32-msvc': 12.3.0
'@next/swc-win32-x64-msvc': 12.3.0
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
dev: true
/node-domexception/1.0.0:
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
engines: {node: '>=10.5.0'}
@ -3915,6 +4141,22 @@ packages:
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
/node-mocks-http/1.11.0:
resolution: {integrity: sha512-jS/WzSOcKbOeGrcgKbenZeNhxUNnP36Yw11+hL4TTxQXErGfqYZ+MaYNNvhaTiGIJlzNSqgQkk9j8dSu1YWSuw==}
engines: {node: '>=0.6'}
dependencies:
accepts: 1.3.8
content-disposition: 0.5.4
depd: 1.1.2
fresh: 0.5.2
merge-descriptors: 1.0.1
methods: 1.1.2
mime: 1.6.0
parseurl: 1.3.3
range-parser: 1.2.1
type-is: 1.6.18
dev: true
/node-releases/2.0.6:
resolution: {integrity: sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==}
dev: true
@ -4176,6 +4418,11 @@ packages:
entities: 4.4.0
dev: true
/parseurl/1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
dev: true
/path-exists/4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@ -4238,6 +4485,15 @@ packages:
yaml: 1.10.2
dev: true
/postcss/8.4.14:
resolution: {integrity: sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==}
engines: {node: ^10 || ^12 || >=14}
dependencies:
nanoid: 3.3.4
picocolors: 1.0.0
source-map-js: 1.0.2
dev: true
/postcss/8.4.16:
resolution: {integrity: sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==}
engines: {node: ^10 || ^12 || >=14}
@ -4355,6 +4611,11 @@ packages:
engines: {node: '>=10'}
dev: true
/range-parser/1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
dev: true
/raw-body/2.5.1:
resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==}
engines: {node: '>= 0.8'}
@ -4561,10 +4822,11 @@ packages:
signal-exit: 3.0.7
dev: true
/retes/0.32.0:
resolution: {integrity: sha512-pCz9SdztBaU4QMX/YA3A5tfEVgHdLwGdc7hJOJHF+XTJzHJksiwQTquZuz32DeJ4KRgo2P8h5sdPnyi+v4AzwQ==}
/retes/0.33.0:
resolution: {integrity: sha512-I6V1G2JkJ2JFIFSVuultNXepf7BW8SCaSUOq5IETM2fDjFim5Dg5F1zU/QbplNW0mqkk8QCw+I722v3nPkpRlA==}
dependencies:
busboy: 1.6.0
zod: 3.19.1
dev: false
/retry/0.13.1:
@ -4867,6 +5129,22 @@ packages:
acorn: 8.8.0
dev: true
/styled-jsx/5.0.6_react@18.2.0:
resolution: {integrity: sha512-xOeROtkK5MGMDimBQ3J6iPId8q0t/BDoG5XN6oKkZClVz9ISF/hihN8OCn2LggMU6N32aXnrXBdn3auSqNS9fA==}
engines: {node: '>= 12.0.0'}
peerDependencies:
'@babel/core': '*'
babel-plugin-macros: '*'
react: '>= 16.8.0 || 17.x.x || ^18.0.0-0'
peerDependenciesMeta:
'@babel/core':
optional: true
babel-plugin-macros:
optional: true
dependencies:
react: 18.2.0
dev: true
/sucrase/3.25.0:
resolution: {integrity: sha512-WxTtwEYXSmZArPGStGBicyRsg5TBEFhT5b7N+tF+zauImP0Acy+CoUK0/byJ8JNPK/5lbpWIVuFagI4+0l85QQ==}
engines: {node: '>=8'}
@ -5141,6 +5419,14 @@ packages:
engines: {node: '>=12.20'}
dev: true
/type-is/1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
dependencies:
media-typer: 0.3.0
mime-types: 2.1.35
dev: true
/typedarray-to-buffer/3.1.5:
resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==}
dependencies:
@ -5237,6 +5523,14 @@ packages:
requires-port: 1.0.0
dev: true
/use-sync-external-store/1.2.0_react@18.2.0:
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
dev: true
/util-deprecate/1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
@ -5603,4 +5897,8 @@ packages:
engines: {node: '>=10'}
dev: true
/zod/3.19.1:
resolution: {integrity: sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA==}
dev: false
publishDirectory: dist

View file

@ -0,0 +1,46 @@
import { createMocks } from "node-mocks-http";
import { describe, expect, it, vi } from "vitest";
import { APL } from "../../APL";
import { createAppRegisterHandler } from "./create-app-register-handler";
describe("create-app-register-handler", () => {
it("Sets auth data for correct request", async () => {
const mockApl: APL = {
get: vi.fn(),
set: vi.fn(),
delete: vi.fn(),
getAll: vi.fn(),
};
const { res, req } = createMocks({
/**
* Use body, instead of params, otherwise - for some reason - param is not accessible in mock request
* Maybe this is a bug https://github.com/howardabrams/node-mocks-http/blob/master/lib/mockRequest.js
*/
body: {
auth_token: "mock-auth-token",
},
headers: {
host: "some-saleor-host.cloud",
"x-forwarded-proto": "https",
"saleor-domain": "https://mock-saleor-domain.saleor.cloud",
},
method: "POST",
});
const handler = createAppRegisterHandler({
apl: mockApl,
});
await handler(req, res);
/**
* It fails -> params.auth_token isn't present
*/
expect(mockApl.set).toHaveBeenCalledWith({
domain: "https://mock-saleor-domain.saleor.cloud",
token: "mock-auth-token",
});
});
});

View file

@ -0,0 +1,43 @@
import type { Handler } from "retes";
import { toNextHandler } from "retes/adapter";
import { withMethod } from "retes/middleware";
import { Response } from "retes/response";
import { APL } from "../../APL";
import { SALEOR_DOMAIN_HEADER } from "../../const";
import { withAuthTokenRequired, withSaleorDomainPresent } from "../../middleware";
export type CreateAppRegisterHandlerOptions = {
apl: APL;
};
/**
* Creates API handler for Next.js. Creates handler called by Saleor that registers app.
* Hides implementation details if possible
* In the future this will be extracted to separate sdk/next package
*/
export const createAppRegisterHandler = ({ apl }: CreateAppRegisterHandlerOptions) => {
const baseHandler: Handler = async (request) => {
const authToken = request.params.auth_token;
const saleorDomain = request.headers[SALEOR_DOMAIN_HEADER] as string;
try {
await apl.set({ domain: saleorDomain, token: authToken });
} catch {
return Response.InternalServerError({
success: false,
error: {
message: "Registration failed: could not save the auth data.",
},
});
}
return Response.OK({ success: true });
};
return toNextHandler([
withMethod("POST"),
withSaleorDomainPresent,
withAuthTokenRequired,
baseHandler,
]);
};

View file

@ -0,0 +1,41 @@
import { createMocks } from "node-mocks-http";
import { describe, expect, it } from "vitest";
import { AppManifest } from "../../types";
import { createManifestHandler } from "./create-manifest-handler";
describe("createManifestHandler", () => {
it("Creates a handler that responds with Manifest", async () => {
const { res, req } = createMocks({
headers: {
host: "some-saleor-host.cloud",
"x-forwarded-proto": "https",
},
method: "GET",
});
const handler = createManifestHandler({
manifestFactory(context: { appBaseUrl: string }): AppManifest {
return {
name: "Mock name",
tokenTargetUrl: `${context.appBaseUrl}/api/register`,
appUrl: context.appBaseUrl,
permissions: [],
id: "app-id",
version: "1",
};
},
});
await handler(req, res);
expect(res._getData()).toEqual({
appUrl: "https://some-saleor-host.cloud",
id: "app-id",
name: "Mock name",
permissions: [],
tokenTargetUrl: "https://some-saleor-host.cloud/api/register",
version: "1",
});
});
});

View file

@ -0,0 +1,29 @@
import { Handler } from "retes";
import { toNextHandler } from "retes/adapter";
import { Response } from "retes/response";
import { withBaseURL } from "../../middleware";
import { AppManifest } from "../../types";
export type CreateManifestHandlerOptions = {
manifestFactory(context: { appBaseUrl: string }): AppManifest;
};
/**
* Creates API handler for Next.js. Helps with Manifest creation, hides
* implementation details if possible
* In the future this will be extracted to separate sdk/next package
*/
export const createManifestHandler = (options: CreateManifestHandlerOptions) => {
const baseHandler: Handler = async (request) => {
const { baseURL } = request.context;
const manifest = options.manifestFactory({
appBaseUrl: baseURL,
});
return Response.OK(manifest);
};
return toNextHandler([withBaseURL, baseHandler]);
};

View file

@ -0,0 +1 @@
export * from "./create-manifest-handler";

View file

@ -0,0 +1,3 @@
Handlers with adapters to Next.js
TODO Extract to separate package

1
src/handlers/readme.md Normal file
View file

@ -0,0 +1 @@
Place for Saleor-related API handlers - without frameworks

View file

@ -1,53 +1,5 @@
import fg from "fast-glob";
import { print } from "graphql/language/printer.js";
import path from "path";
const capitalize = (value: string) => value.charAt(0).toUpperCase() + value.slice(1);
const dropFileExtension = (filename: string) => path.parse(filename).name;
/**
* @deprecated remove magic
*/
export const inferWebhooks = async (
baseURL: string,
webhooksPath: string,
generatedGraphQL: any
) => {
let entries;
if (process.env.NODE_ENV === "production") {
entries = await fg(["*.js"], { cwd: webhooksPath });
} else {
entries = await fg(["*.ts"], { cwd: "pages/api/webhooks" });
}
return entries.map(dropFileExtension).map((name: string) => {
const camelCaseName = name.split("-").map(capitalize).join("");
const eventName = name.toUpperCase().replace(/-/g, "_");
let eventType: string;
if (Object.values(generatedGraphQL.WebhookEventTypeAsyncEnum).includes(eventName)) {
eventType = "asyncEvents";
} else if (Object.values(generatedGraphQL.WebhookEventTypeSyncEnum).includes(eventName)) {
eventType = "syncEvents";
} else {
throw Error("Event type not found.");
}
const statement = `${camelCaseName}SubscriptionDocument`;
let query: string;
if (statement in generatedGraphQL) {
query = print((generatedGraphQL as any)[statement]);
} else {
throw Error("Subscription not found.");
}
return {
name,
[eventType]: [eventName],
query,
targetUrl: `${baseURL}/api/webhooks/${name}`,
};
});
};
export * from "./const";
export * from "./headers";
export * from "./infer-webhooks";
export * from "./types";
export * from "./urls";

53
src/infer-webhooks.ts Normal file
View file

@ -0,0 +1,53 @@
import fg from "fast-glob";
import { print } from "graphql/language/printer";
import path from "path";
const capitalize = (value: string) => value.charAt(0).toUpperCase() + value.slice(1);
const dropFileExtension = (filename: string) => path.parse(filename).name;
/**
* @deprecated remove magic -> change CLI to use static code generation
*/
export const inferWebhooks = async (
baseURL: string,
webhooksPath: string,
generatedGraphQL: any
) => {
let entries;
if (process.env.NODE_ENV === "production") {
entries = await fg(["*.js"], { cwd: webhooksPath });
} else {
entries = await fg(["*.ts"], { cwd: "pages/api/webhooks" });
}
return entries.map(dropFileExtension).map((name: string) => {
const camelCaseName = name.split("-").map(capitalize).join("");
const eventName = name.toUpperCase().replace(/-/g, "_");
let eventType: string;
if (Object.values(generatedGraphQL.WebhookEventTypeAsyncEnum).includes(eventName)) {
eventType = "asyncEvents";
} else if (Object.values(generatedGraphQL.WebhookEventTypeSyncEnum).includes(eventName)) {
eventType = "syncEvents";
} else {
throw Error("Event type not found.");
}
const statement = `${camelCaseName}SubscriptionDocument`;
let query: string;
if (statement in generatedGraphQL) {
query = print((generatedGraphQL as any)[statement]);
} else {
throw Error("Subscription not found.");
}
return {
name,
[eventType]: [eventName],
query,
targetUrl: `${baseURL}/api/webhooks/${name}`,
};
});
};

View file

@ -9,6 +9,7 @@ export const withBaseURL: Middleware = (handler) => async (request) => {
debug("Middleware called with host: %s, protocol %s", host, request.headers["x-forwarded-proto"]);
request.context ??= {};
request.context.baseURL = `${protocol}://${host}`;
debug("context.baseURL resolved to be: \"%s\"", request.context.baseURL);

20
tsup.config.ts Normal file
View file

@ -0,0 +1,20 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: [
/**
* TODO Introduce breaking change and remove exporting from paths, merge everything to index
*/
"src/*",
"src/index.ts",
"src/APL/index.ts",
"src/app-bridge/index.ts",
"src/handlers/next/index.ts",
"src/middleware/index.ts",
],
dts: true,
clean: true,
format: ["esm", "cjs"],
splitting: true,
external: ["**/*.md"],
});