diff --git a/cspell.json b/cspell.json index ea3406ee34..5e26ea052f 100644 --- a/cspell.json +++ b/cspell.json @@ -26,7 +26,8 @@ "turborepo", "undiscounted", "żółta", - "noopener" + "noopener", + "Hono" ], "allowCompoundWords": true, "useGitignore": true, diff --git a/docs/developer/extending/apps/developing-apps/app-examples.mdx b/docs/developer/extending/apps/developing-apps/app-examples.mdx index 2ada35d824..6383d1e877 100644 --- a/docs/developer/extending/apps/developing-apps/app-examples.mdx +++ b/docs/developer/extending/apps/developing-apps/app-examples.mdx @@ -17,83 +17,100 @@ import SendgridIcon from "../../../../../static/img/apps/notification-hub.svg" ## Built by Saleor These apps were built by Saleor team using [Saleor App Template](/developer/extending/apps/developing-apps/app-template.mdx), however they are not currently available in the App Store and may not maintained. You are can use them as a reference for building your own apps. -
+
} + icon={} /> } - /> + icon={} + /> } + icon={} /> } + icon={} /> } + icon={} /> } + icon={} /> } + icon={} /> } + icon={} /> } + icon={} /> } + icon={} + /> + + +
## Built by the community These examples can be useful if you want to build a service without the use of the App Template in tech stack other than Next.js. -
+
` is one of the supported platforms, like NextJS or Lambda + Options provided to handler factory: ```typescript type CreateManifestHandlerOptions = { manifestFactory(context: { appBaseUrl: string; - request: NextApiRequest; - schemaVersion: number | null; - }): AppManifest; + request: Request; // Depends on the platform, e.g. Request, NextApiRequest, NextRequest + schemaVersion: [major: number, minor: number] | null; + }): AppManifest; // Ensures response type is valid }; ``` -You can use `NextApiRequest` to read additional parameters from the request. +You can use `request` to read additional parameters from the request. -Field `schemaVersion` can be used to enable some feature based on the Saleor version. It will be `null` if app is being installed in Saleor version below 3.15.0. +Field `schemaVersion` can be used to enable some feature based on the Saleor version. It will be `null` if request doesn't contain `saleor-schema-version` header. +Saleor will automatically attach this header, but the GET request executed e.g. from the browser will not contain this field. -See [`createManifestHandler`](https://github.com/saleor/saleor-app-sdk/blob/5c56cf566d2cc6e4a075c8c619f174fa43aad6c9/src/handlers/next/create-manifest-handler.ts#L18) for more details. See [manifest](https://github.com/saleor/saleor-app-sdk/blob/5c56cf566d2cc6e4a075c8c619f174fa43aad6c9/src/types.ts#L223) too. +Hint: `@saleor/app-sdk` contains documented types attached to the npm package. ### App register handler factory -Example usage of app register handler in Next.js +Following example shows how to use a register handler in Next.js: ```typescript -// pages/api/register.ts +// pages/api/register.ts - next.js route import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next"; -import { UpstashAPL } from "@saleor/app-sdk/APL"; +import { UpstashAPL } from "@saleor/app-sdk/APL/upstash"; // See APL section export default createAppRegisterHandler({ apl: new UpstashAPL({ @@ -79,6 +87,7 @@ export default createAppRegisterHandler({ return respondWithError({ message: "Error, installation will fail" }); }); }, + }); ``` @@ -97,7 +106,7 @@ export type CreateAppRegisterHandlerOptions = { * Run right after Saleor calls this endpoint */ onRequestStart?( - request: Request, + request: Request, // Can be different depending on the platform context: { authToken?: string; saleorDomain?: string; @@ -149,3 +158,8 @@ See [APL](apl) for details on what is Auth Persistence Layer in Saleor apps. App SDK provides a utility that helps build (async) webhook handlers so that the app can react to Saleor events. Read about it [here](saleor-webhook). + +### Protected handler + +To protect endpoint from the outside world and accept only requests from the app's frontend, use the `createProtectedHandler` function. +See more details [here](./protected-handlers.mdx). \ No newline at end of file diff --git a/docs/developer/extending/apps/developing-apps/app-sdk/apl.mdx b/docs/developer/extending/apps/developing-apps/app-sdk/apl.mdx index 049969291f..0a242c48ca 100644 --- a/docs/developer/extending/apps/developing-apps/app-sdk/apl.mdx +++ b/docs/developer/extending/apps/developing-apps/app-sdk/apl.mdx @@ -4,19 +4,15 @@ Auth Persistence Layer (APL) is a technology-agnostic interface for managing aut The following doc contains a JavaScript / TypeScript implementation used by official Saleor apps. -## Available methods - -- `get: (saleorApiUrl: string) => Promise` - If the entry for given saleorApiUrl exists, returns AuthData object. +The idea of APLs is to abstract specific persistent storage (like Redis, Upstash, or even a file) and provide a common interface for CRUD operations. +This way, you can easily switch between different storage solutions without changing the application code. +Additionally, app doesn't have to know any platform-specific details about the storage, like connection strings or endpoints. -- `set: (authData: AuthData) => Promise` - Save auth data. +During development we recommend using `FileAPL` for fast and easy access on the local machine. For production you need to set up some form of database. -- `delete: (saleorApiUrl: string) => Promise` - Remove auth data from the given API URL. +Hint: Specifics of APL is rarely write, but frequent read - it will be checked on every request to the app. We recommend to set up a database with fast read operations, like DynamoDB or Redis. -- `getAll: () => Promise` - Returns all auth data available. - -- `isReady: () => Promise` - Check if the persistence layer behind APL is ready. For example, when a database connection was established. - -- `isConfigured: () => Promise` - Check if the persistence layer behind APL is configured. For example, when an env variable is required by the database connection. +To increase the security, we recommend to store `token` encrypted. ## AuthData @@ -24,19 +20,31 @@ Interface containing data used for communication with the Saleor API: ```ts export interface AuthData { - domain: string; + // Token that Saleor provides to the app during installation. App must store it securely. token: string; + // URL of Saleor GraphQL API, ending with /graphql/. Allows to identify the Saleor instance, especially in multi-tenant apps. saleorApiUrl: string; + // ID of the app stored by Saleor in the database. It's unique for each installation. When app is reinstalled, ID will be different. appId: string; + // Cached JWKS (JSON Web Key Set) used for webhook validation. It's available at https:///.well-known/jwks.json jwks: string; } ``` -- `domain` - Domain of the API -- `token` - Authorization token -- `saleorApiUrl` - Full URL to the Saleor GraphQL API -- `appID` - ID of the app assigned during the installation process -- `jwks` - JSON Web Key Set available at `https:///.well-known/jwks.json`, cached in the APL for the faster webhook validation +## Available methods + +- `get: (saleorApiUrl: string) => Promise` - If the entry for given saleorApiUrl exists, returns AuthData object. + +- `set: (authData: AuthData) => Promise` - Save auth data. + +- `delete: (saleorApiUrl: string) => Promise` - Remove auth data from the given API URL. + +- `getAll: () => Promise` - Returns all auth data available. + +- `isReady?: () => Promise` - Optional: Check if the persistence layer behind APL is ready. For example, when a database connection was established. + +- `isConfigured?: () => Promise` - Optional: Check if the persistence layer behind APL is configured. For example, when an env variable is required by the database connection. + ## AplReadyResult & AplConfiguredResult @@ -62,6 +70,8 @@ type AplConfiguredResult = }; ``` +Implementing these functions is optional, but it can be useful for handling asynchronous operations like database connection. + ## Example implementation Let's create an APL which uses Redis for data storage: @@ -122,6 +132,12 @@ Or access it from the context of API helpers from the SDK: - [Protected API Handlers](./protected-handlers) - [Webhook Handlers](./saleor-webhook) +Hint: You don't need to write RedisAPL on your own, you can import it from the SDK: + +```ts +import { RedisAPL } from "@saleor/app-sdk/APL/redis"; +``` + ### Using different APL depending on the environment Depending on the environment your app is working on, you may want to use a different APL. For example, you may like to use `FileAPL` during local development because it does not require any additional infrastructure. Deployed apps, on the other hand, need a more robust solution. @@ -131,7 +147,8 @@ To handle both scenarios, initialize the proper APLs in your code based on its e ```ts // lib/saleorApp.ts -import { FileAPL, UpstashAPL } from "@saleor/app-sdk/APL"; +import { FileAPL } from "@saleor/app-sdk/APL/file"; +import { UpstashAPL } from "@saleor/app-sdk/APL/upstash"; // Based on the environment variable, the app will use a different APL: // - For local development store auth data in the `.auth-data.json`. @@ -296,3 +313,34 @@ KV_REST_API_READ_ONLY_TOKEN= # A key to the Redis collection. All APL items will be stored inside this collection KV_STORAGE_NAMESPACE= ``` + +### RedisAPL + +RedisAPL requires `redis` client to be installed. + +Similar to VercelKV (which is Redis too), all data is stored in the hash collection. You can provide a custom key for the collection in the constructor + +```typescript +import { createClient } from 'redis'; +import { RedisAPL } from '@saleor/app-sdk/APL/redis'; + +const apl = new RedisAPL({ + client: createClient(), + hashCollectionKey: 'my-key', // optional, by default "saleor_app_auth" +}); + +``` + +See Redis documentation for more details on how to set up the client. + + +### SaleorCloudAPL + +You may see that there is a `SaleorCloudAPL` exported as well. This APL is specific implementation that Saleor Cloud uses when hosting apps on the cloud platform. + +It's not available to use and will not be bundled by your app if not imported. + +## Community APL implementations + +- [Deno KV APL](https://github.com/witoszekdev/saleor-app-hono-deno-template/blob/main/server/deno-kv-apl.ts) +- [Cloudlfare KV APL](https://github.com/witoszekdev/saleor-app-hono-cf-pages-template/blob/main/src/cloudflare-kv-apl.ts) \ No newline at end of file diff --git a/docs/developer/extending/apps/developing-apps/app-sdk/app-bridge.mdx b/docs/developer/extending/apps/developing-apps/app-sdk/app-bridge.mdx index b2a5560360..649c8a0e62 100644 --- a/docs/developer/extending/apps/developing-apps/app-sdk/app-bridge.mdx +++ b/docs/developer/extending/apps/developing-apps/app-sdk/app-bridge.mdx @@ -36,10 +36,21 @@ Available state represents `AppBridgeState`: ```typescript type AppBridgeState = { + /** + * JWT token provided by Dashboard. Represents user's session. + */ token?: string; + /** + * ID of the app + */ id: string; + /** + * Flag if app bridge has properly initialized and authorized + */ ready: boolean; - domain: string; + /** + * Current path on the frontend + */ path: string; theme: ThemeType; locale: LocaleCode; // See src/locales.ts diff --git a/docs/developer/extending/apps/developing-apps/app-sdk/migration-0.x-to-1.x.mdx b/docs/developer/extending/apps/developing-apps/app-sdk/migration-0.x-to-1.x.mdx new file mode 100644 index 0000000000..b912d0bc01 --- /dev/null +++ b/docs/developer/extending/apps/developing-apps/app-sdk/migration-0.x-to-1.x.mdx @@ -0,0 +1,109 @@ +With 1.x.x release in app-sdk we added support for platforms other than Next.js (pages router). Apart of these features we introduced several breaking changes. + +This article shows how to migrate your app from 0.x.x to 1.x.x version of app-sdk. + +To install please run `pnpm install @saleor/sdk@1` + +See [additional changelog](https://github.com/saleor/app-sdk/releases/tag/v1.0.0) + +# New features + +Added handlers for platform: +- `/handlers/aws-lambda` +- `/handlers/fetch-api` +- `/handlers/next` +- `/handlers/next-app-router` + +Exported `parseSchemaVersion` from `/util`. It parses stringified schema version into a tuple `[number, number]` + +# Breaking changes + +## Compatibility changes + +From 1.x.x minimal (officially) supported Saleor version is 3.20 + +Minimal Next.js version is 14. It may work on older versions but it's not officially supported. + +## Schema version + +`schemaVersion` field that was available in Manifest factory and webhook handlers is now parsed into a tuple. + +Before: + +```typescript +schemaVersion: number +``` + +After: + +```typescript +type SchemaVersion = [major: number, minor: number] + +schemaVersion: SchemaVersion +``` + +Now it's secure that `3.20` is not mixed with `3.2` due to a wrong format. + +## APLs export paths + +Now every APL is exported from a separate file. This allows tree-shaking - code will not be evaluated if it's not used. + +```typescript +// Before +import { FileApl, EnvApl } from '@saleor/app-sdk/APL'; + +// After +import { FileApl } from '@saleor/app-sdk/APL/file'; +import { EnvApl } from '@saleor/app-sdk/APL/env'; + +``` + +## Dropping `domain` field + +In `AuthData` we dropped `domain` field. Please use `saleorApiUrl` instead which contains full URL, not only domain + +Additionally `domain` was removed from `AppBridgeState`. The `saleorApiUrl` field is also available there. + +## Moved headers export path + +Previously constant values representing headers were exported from `/const` path. + +Now, `/const` doesn't exist anymore. All headers are exported from `/headers` path. + +## Renamed Nextjs handlers + +In v1 we added Next App Router. To make it clear what is used, we renamed old handlers: + +```typescript +// Before + +import { type NextSyncWebhookHandler } from "@saleor/app-sdk/handlers/next"; + +// After +import { type NextJsSyncWebhookHandler } from "@saleor/app-sdk/handlers/next"; +``` + +## Removed `verify-jwt` path + +You can now import `verifyJwt`, `getJwksUrlFromSaleorApiUrl`, and `verifySignatureWithJwks` from `@saleor/app-sdk/auth` path. + +## Removed `buildResponse` + +`ctx.buildResponse` was removed from Webhook's context. Now to create a typed response from sync webhook, you can use `buildSyncWebhookResponsePayload` function. + + +```typescript +import {buildSyncWebhookResponsePayload} from "@saleor/app-sdk/handlers/shared" + +const typedResponse = buildSyncWebhookResponsePayload<"CHECKOUT_CALCULATE_TAXES">({/* Input will be typed here */}) +``` + +## Other changes + +- `SyncWebhookResponsesMap` is now exported from `@saleor/app-sdk/handlers/shared` +- `asyncEvent` field from `SaleorAsyncWebhook` class constructor was removed. Use `event` instead. +- `requiredEnvVars` parameter no longer is accepted in `SaleorApp` constructor. Please manually validate env variables on the app level. Saleor team recommends [T3 Env](https://env.t3.gg/docs/introduction) +- Removed all exports from `/middlewares`. Use utilities from `/handlers` instead. +- Removed `processSaleorWebhook` and `processProtectedHandler`. Use `./handlers` instead. +- AppBridge now uses `Crypto` API built-in modern browsers. This requires localhost or https to work. +- When creating `MetadataManager`, `deleteMetadata` callback is now required. \ No newline at end of file diff --git a/docs/developer/extending/apps/developing-apps/app-sdk/overview.mdx b/docs/developer/extending/apps/developing-apps/app-sdk/overview.mdx index 1b431e23f7..cf657dfc87 100644 --- a/docs/developer/extending/apps/developing-apps/app-sdk/overview.mdx +++ b/docs/developer/extending/apps/developing-apps/app-sdk/overview.mdx @@ -5,15 +5,10 @@ title: App SDK Overview import CardGrid from "@site/components/CardGrid"; -`@saleor/app-sdk` is a TypeScript-first npm library that serves as a foundation for every Saleor app. It includes helpers built for React and Next.js. +`@saleor/app-sdk` is a TypeScript-first npm library that serves as a foundation for every Saleor app. It includes helpers built for React and serverless functions, like AWS Lambda or Next.JS You can find `@saleor/app-sdk` on [npm](https://www.npmjs.com/package/@saleor/app-sdk) and [GitHub](https://github.com/saleor/saleor-app-sdk). -:::warning - -@saleor/app-sdk only supports Next.js. We can't guarantee it will work with other frameworks. - -::: ## Installation @@ -28,11 +23,25 @@ npm i @saleor/app-sdk yarn add @saleor/app-sdk ``` +## Platforms support + +Utilities for server-side operations support multiple platforms. Currently, we support: +- Next.js (Pages) +- Next.js (App Router) +- AWS Lambda (Serverless functions) +- [FetchAPI](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) (Deno, Cloudflare Workers, etc) + + ## Versioning `@saleor/app-sdk` uses semver. We track its [releases and changelogs on GitHub](https://github.com/saleor/saleor-app-sdk/releases). -Currently, the SDK is in the pre-1.0 phase, meaning the API is unstable. We do our best to minimize breaking changes. +Hint: See [migration from 0.x to 1.x](migration-0.x-to-1.x) for more information. + +## Requirements + +- Saleor support: 3.20 and higher (it may work in older versions, but it's not guaranteed). +- Next.js support: 14 and higher ## Learn more diff --git a/docs/developer/extending/apps/developing-apps/app-sdk/protected-handlers.mdx b/docs/developer/extending/apps/developing-apps/app-sdk/protected-handlers.mdx index cc22c1a103..3ce75961d1 100644 --- a/docs/developer/extending/apps/developing-apps/app-sdk/protected-handlers.mdx +++ b/docs/developer/extending/apps/developing-apps/app-sdk/protected-handlers.mdx @@ -35,19 +35,10 @@ For example purposes, our endpoint will only log the welcome message: ```typescript import { createProtectedHandler, - ProtectedHandlerContext, -} from "@saleor/app-sdk/handlers/next"; -import { NextApiRequest, NextApiResponse } from "next"; -import { saleorApp } from "../../../saleor-app"; - -export const handler = async ( - req: NextApiRequest, - res: NextApiResponse, - ctx: ProtectedHandlerContext -) => { - console.log(`Greetings from ${ctx.authData.domain}`); - res.status(200); -}; +} from "@saleor/app-sdk/handlers/next"; // "next" or other platforms +// See APL documentation for more details +import { apl } from "./apl"; + /** * If any of the requirements is failed, an error response will be returned. @@ -55,14 +46,21 @@ export const handler = async ( * * Last argument is optional array of permissions that will be checked. If user doesn't have them, will return 401 before handler is called */ -export default createProtectedHandler(handler, saleorApp.apl, [ +export default createProtectedHandler(( request, response, ctx) => { + console.log(`Greetings from ${ctx.authData.domain}`); + response.status(200); +}, apl, [ "MANAGE_ORDERS", ]); ``` +Note that argument provided to handler may differ depending on the platform. Next.js pages router will contain `response` but FetchAPI will not. + To make your requests successfully communicate with the backend, `saleor-api-url` and `authorization-bearer` headers are required: ```typescript +import { SALEOR_AUTHORIZATION_BEARER_HEADER, SALEOR_API_URL_HEADER } from "@saleor/app-sdk/headers" + fetch("/api/protected", { headers: { /** @@ -70,8 +68,8 @@ fetch("/api/protected", { * headers the backend will check if the request has enough permissions to * perform the action. */ - "saleor-api-url": saleorApiUrl, - "authorization-bearer": token, + [SALEOR_API_URL_HEADER]: saleorApiUrl, + [SALEOR_AUTHORIZATION_BEARER_HEADER]: token, }, }); ``` diff --git a/docs/developer/extending/apps/developing-apps/app-sdk/saleor-webhook.mdx b/docs/developer/extending/apps/developing-apps/app-sdk/saleor-webhook.mdx index d3cbd7374f..4dde812475 100644 --- a/docs/developer/extending/apps/developing-apps/app-sdk/saleor-webhook.mdx +++ b/docs/developer/extending/apps/developing-apps/app-sdk/saleor-webhook.mdx @@ -1,22 +1,33 @@ # Saleor Webhook Utilities -Apps are usually connected via webhooks - one App sends an HTTP request to another App, informing about some event or requesting some action to be performed. +Apps are usually connected via webhooks - one service sends an HTTP request to another service, informing about some event or requesting some action to be performed. To inform your App about events originating from Saleor, you need to expose a webhook handler, which Saleor will call with a POST request. -The App SDK provides a utility that abstracts connection details and auth, allowing developers to focus on business logic. +The App SDK provides a utility that abstracts connection details and auth, allowing developers to focus on a business logic. Depending on the type of the webhook, you can choose one of the classes: - `SaleorAsyncWebhook` - `SaleorSyncWebhook` +## Platforms support + +app-sdk provides support for multiple platforms. To import specific platform, ensure that you are using the correct import path, for example: +- For AWS Lambda `import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/aws-lambda";` +- For NextJS App Router `import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next-app-router";` + +See [supported platforms](./overview.mdx#platforms-support) + + ## Common configuration Both `SaleorSyncWebhook` and `SaleorAsyncWebhook` contain a similar API with few differences. ### Constructing Webhook instance +Example for NextJS pages router: + In Next.js `pages` directory, create a page, e.g., `pages/api/webhooks/order-created.ts`. We recommend keeping the webhook type in the file name, which Next.js will resolve to route the path. ```typescript @@ -39,6 +50,7 @@ type OrderPayload = { }; export const orderCreatedWebhook = new SaleorAsyncWebhook( + // See below options ); ``` @@ -65,6 +77,7 @@ type CalculateTaxedPayload = { }; export const orderCalculateTaxesWebhook = new SaleorSyncWebhook( + // See below options ); ``` @@ -151,11 +164,11 @@ export const orderCreatedWebhook = new SaleorAsyncWebhook({ isActive: true, apl: require("../lib/apl"), query: ` - subscription { - event { - ... on OrderCreated { - order { - id + subscription { + event { + ... on OrderCreated { + order { + id } } } @@ -297,18 +310,22 @@ export default orderCreatedWebhook.createHandler((req, res, context) => { ### Typed sync webhook response -Sync webhooks need to return a response to Saleor so that the operation can be completed. To achieve that, `SaleorAsyncWebhook` injects an additional context field `buildResponse`. -It infers even from the constructor and provides a typed factory: +Sync webhooks need to return a response to Saleor so that the operation can be completed. To help with that, SDK exposes a helper function that will ensure type safety ```typescript +import { buildSyncWebhookResponsePayload } from "@saleor/app-sdk/handlers/shared"; + const webhook = new SaleorAsyncWebhook({ - event: "ORDER_CALCULATE_TAXES" /* ... rest of config */, + // ... config }); -orderCreatedWebhook.createHandler((req, res, context) => { +webhook.createHandler((req, res, context) => { return res.status(200).send( - context.buildResponse({ - // Fields are typed here + buildSyncWebhookResponsePayload<'ORDER_CALCULATE_TAXES'>({ + lines: [ + // ... Everything is typed here + ], + // ... }) ); }); @@ -353,7 +370,7 @@ export const productUpdatedWebhook = ### Saleor schema version inside context -From version 0.50.0 of `@saleor/app-sdk` have new field: `schemaVersion` inside context for `createHandler`: +For convenience App SDK provides Saleor Schema version (`[major: number, minor: number]`) in the context. However, the source of this field comes from the subscription payload - it must be defined in the subscription query. ```ts export default orderCreatedWebhook.createHandler((req, res, context) => { diff --git a/sidebars/building-apps.js b/sidebars/building-apps.js index 50653344fc..855a918d3c 100644 --- a/sidebars/building-apps.js +++ b/sidebars/building-apps.js @@ -35,6 +35,7 @@ export const buildingApps = [ "developer/extending/apps/developing-apps/app-sdk/saleor-webhook", "developer/extending/apps/developing-apps/app-sdk/settings-manager", "developer/extending/apps/developing-apps/app-sdk/debugging", + "developer/extending/apps/developing-apps/app-sdk/migration-0.x-to-1.x", title("Starter kits"), "developer/extending/apps/developing-apps/app-template",