From 0352356fd3a2ea0aacc1f8b6438a2f21742a9f8f Mon Sep 17 00:00:00 2001 From: Lukasz Ostrowski Date: Wed, 21 Sep 2022 11:25:01 +0200 Subject: [PATCH] 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 --- docs/api-handlers.md | 75 +++++ package.json | 16 +- pnpm-lock.yaml | 306 +++++++++++++++++- .../next/create-app-register-handler.test.ts | 46 +++ .../next/create-app-register-handler.ts | 43 +++ .../next/create-manifest-handler.test.ts | 41 +++ src/handlers/next/create-manifest-handler.ts | 29 ++ src/handlers/next/index.ts | 1 + src/handlers/next/readme.md | 3 + src/handlers/readme.md | 1 + src/index.ts | 58 +--- src/infer-webhooks.ts | 53 +++ src/middleware/with-base-url.ts | 1 + tsup.config.ts | 20 ++ 14 files changed, 632 insertions(+), 61 deletions(-) create mode 100644 docs/api-handlers.md create mode 100644 src/handlers/next/create-app-register-handler.test.ts create mode 100644 src/handlers/next/create-app-register-handler.ts create mode 100644 src/handlers/next/create-manifest-handler.test.ts create mode 100644 src/handlers/next/create-manifest-handler.ts create mode 100644 src/handlers/next/index.ts create mode 100644 src/handlers/next/readme.md create mode 100644 src/handlers/readme.md create mode 100644 src/infer-webhooks.ts create mode 100644 tsup.config.ts diff --git a/docs/api-handlers.md b/docs/api-handlers.md new file mode 100644 index 0000000..68c0eaa --- /dev/null +++ b/docs/api-handlers.md @@ -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 diff --git a/package.json b/package.json index c73c032..9293f3f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a94de07..b7a7987 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/handlers/next/create-app-register-handler.test.ts b/src/handlers/next/create-app-register-handler.test.ts new file mode 100644 index 0000000..7d5d661 --- /dev/null +++ b/src/handlers/next/create-app-register-handler.test.ts @@ -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", + }); + }); +}); diff --git a/src/handlers/next/create-app-register-handler.ts b/src/handlers/next/create-app-register-handler.ts new file mode 100644 index 0000000..d71fa9e --- /dev/null +++ b/src/handlers/next/create-app-register-handler.ts @@ -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, + ]); +}; diff --git a/src/handlers/next/create-manifest-handler.test.ts b/src/handlers/next/create-manifest-handler.test.ts new file mode 100644 index 0000000..8455797 --- /dev/null +++ b/src/handlers/next/create-manifest-handler.test.ts @@ -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", + }); + }); +}); diff --git a/src/handlers/next/create-manifest-handler.ts b/src/handlers/next/create-manifest-handler.ts new file mode 100644 index 0000000..40aa2dc --- /dev/null +++ b/src/handlers/next/create-manifest-handler.ts @@ -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]); +}; diff --git a/src/handlers/next/index.ts b/src/handlers/next/index.ts new file mode 100644 index 0000000..037eb05 --- /dev/null +++ b/src/handlers/next/index.ts @@ -0,0 +1 @@ +export * from "./create-manifest-handler"; diff --git a/src/handlers/next/readme.md b/src/handlers/next/readme.md new file mode 100644 index 0000000..e6cfad4 --- /dev/null +++ b/src/handlers/next/readme.md @@ -0,0 +1,3 @@ +Handlers with adapters to Next.js + +TODO Extract to separate package diff --git a/src/handlers/readme.md b/src/handlers/readme.md new file mode 100644 index 0000000..1ebfb2f --- /dev/null +++ b/src/handlers/readme.md @@ -0,0 +1 @@ +Place for Saleor-related API handlers - without frameworks diff --git a/src/index.ts b/src/index.ts index 27de774..1fdc678 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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"; diff --git a/src/infer-webhooks.ts b/src/infer-webhooks.ts new file mode 100644 index 0000000..f097c11 --- /dev/null +++ b/src/infer-webhooks.ts @@ -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}`, + }; + }); +}; diff --git a/src/middleware/with-base-url.ts b/src/middleware/with-base-url.ts index 227c07e..d4d804f 100644 --- a/src/middleware/with-base-url.ts +++ b/src/middleware/with-base-url.ts @@ -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); diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..015ced5 --- /dev/null +++ b/tsup.config.ts @@ -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"], +});