diff --git a/docs/api-handlers.md b/docs/api-handlers.md index 45c3c9e..4435322 100644 --- a/docs/api-handlers.md +++ b/docs/api-handlers.md @@ -73,3 +73,9 @@ export type CreateAppRegisterHandlerOptions = { ``` See [APL](./apl.md) for details what is Auth Persistence Layer in Saleor apps + +### Async Webhook Handler + +App SDK provides a utility that helps building (async) webhook handlers, so app can react on Saleor events. + +Read about it [here](./saleor-async-webhook.md). \ No newline at end of file diff --git a/docs/debugging.md b/docs/debugging.md index 3df6273..f09a59b 100644 --- a/docs/debugging.md +++ b/docs/debugging.md @@ -45,9 +45,10 @@ localStorage.debug = "*"; Use the namespace name to enable debug logs for each module. -| Namespace name | Description | -| --------------------- | -------------------------------------------------- | -| \app-sdk:\* | Enable all | -| app-sdk:AppBridge | Enable [AppBridge](./app-bridge.md) (browser only) | -| app-sdk:Middleware:\* | Enable all middlewares (node only) | -| app-sdk:APL:\* | Enable all APLs (node only) | +| Namespace name | Description | +|-------------------------------|----------------------------------------------------| +| \app-sdk:\* | Enable all | +| app-sdk:AppBridge | Enable [AppBridge](./app-bridge.md) (browser only) | +| app-sdk:Middleware:\* | Enable all middlewares (node only) | +| app-sdk:APL:\* | Enable all APLs (node only) | +| app-sdk:SaleorAsyncWebhook:\* | Enable SaleorAsyncWebhook utility | diff --git a/docs/saleor-async-webhook.md b/docs/saleor-async-webhook.md new file mode 100644 index 0000000..2e54df0 --- /dev/null +++ b/docs/saleor-async-webhook.md @@ -0,0 +1,144 @@ +# Saleor Async Webhook + +Apps are usually connected via webhooks - one App sends a HTTP request to another App, informing about some event or requesting some action to be performed. + +To inform your App about events originated from Saleor, you need to expose a webhook handler, which Saleor will call with POST request. + +To avoid boilerplate, App SDK provides utility that abstracts connection details, allowing developers to focus on business logic. + +Note - this utility works for Saleor Async Webhooks only. Support for Sync webhooks are not yet supported in SDK, but you can write your sync webhook handler +from scratch. + +## Creating async webhook with SaleorAsyncWebhook + +### Creating webhook handler configuration + +To use SaleorAsyncWebhook utility, first create a new instance. It can be created in your API handler file + +```typescript +// pages/api/webhooks/order-created.ts + +/** + * To be type safe, define payload from API. This should be imported from generated graphQL code + */ +type OrderPayload = { + id: string; +}; + +export const orderCreatedWebhook = new SaleorAsyncWebhook({ + /** + * Name of the webhook, not required + */ + name: "Order Created", + /** + * Relative path to the webhook, required + */ + webhookPath: "api/webhooks/order-created", + /** + * Event type, required + */ + asyncEvent: "ORDER_CREATED", + /** + * Decide if webhook created during app installation should be active or not + */ + isActive: true, + /** + * Provide APL, read more below + */ + apl: require("../lib/apl"), + /** + * Subscription query, telling Saleor what payload app expects + */ + query: "TODO", +}); +``` + +- Check available events [here](https://docs.saleor.io/docs/3.x/developer/extending/apps/asynchronous-webhooks#available-webhook-events) +- [Read more about APLs](./apl.md) +- [Subscription query documentation](https://docs.saleor.io/docs/3.x/developer/extending/apps/subscription-webhook-payloads) + +You can consider created `orderCreatedWebhook` a center point of your webhook configuration. Now, you need to create a handler and add it to manifest. + +### Extending app manifest + +Webhooks are created in Saleor when the App is installed. Saleor uses [AppManifest](https://docs.saleor.io/docs/3.x/developer/extending/apps/manifest) to get information about webhooks to create. +`SaleorAsyncWebhook` utility can generate this manifest: + +```typescript +// pages/api/manifest + +import { createManifestHandler } from "@saleor/app-sdk/handlers/next"; + +import { orderCreatedWebhook } from "./order-created.ts"; + +export default createManifestHandler({ + manifestFactory({ appBaseUrl }) { + return { + /** + * Add one or more webhook manifests. + */ + webhooks: [orderCreatedWebhook.getWebhookManifest(appBaseUrl)], + // ...rest of your App's manifest + }; + }, +}); +``` + +Now, try to read your manifest, in default Next.js config it will be `GET localhost:3000/api/manifest`. You should see webhook configuration as part of manifest response. + +### Creating webhook domain logic + +Now, let's create a handler that will process webhook data. Let's back to handler file `pages/api/webhooks/order-created.ts`. + +```typescript +type OrderPayload = { + id: string; +}; + +export const orderCreatedWebhook = new SaleorAsyncWebhook({ + // ... your configuration +}); + +export default orderCreatedWebhook.createHandler((req, res, context) => { + const { baseUrl, event, payload, authData } = context; + + console.log(payload.id); // type is inferred + + // Perform some domain logic + + // End with status 200 + return res.status(200).end(); +}); +``` + +### query vs subscriptionQueryAst + +Subscription query can be specified using plain string or as `ASTNode` object created by `gql` tag. + +If your project does not use any code generation for GraphQL operations, use the string. In case you are using [GraphQL Code Generator](https://the-guild.dev/graphql/codegen), which we highly recommend, you should pass a subscription as GraphQL ASTNode: + +```typescript +/** + * Subscription query, you can define it in the `.ts` file. If you write operations in separate `.graphql` files, codegen will also export them in the generated file. + */ +export const ExampleProductUpdatedSubscription = gql` + ${ProductUpdatedWebhookPayload} + subscription ExampleProductUpdated { + event { + fragment ProductUpdatedWebhookPayload on ProductUpdated { + product { + id + name + } + } + } + } +`; + +export const productUpdatedWebhook = new SaleorAsyncWebhook({ + name: "Example product updated webhook", + webhookPath: "api/webhooks/saleor/product-updated", + asyncEvent: "PRODUCT_UPDATED", + apl: saleorApp.apl, + subscriptionQueryAst: ExampleProductUpdatedSubscription, +}); \ No newline at end of file diff --git a/src/handlers/next/saleor-async-webhook.ts b/src/handlers/next/saleor-async-webhook.ts index 340e129..cfffe23 100644 --- a/src/handlers/next/saleor-async-webhook.ts +++ b/src/handlers/next/saleor-async-webhook.ts @@ -1,11 +1,10 @@ import { ASTNode } from "graphql"; import { NextApiHandler, NextApiRequest, NextApiResponse } from "next"; -import { WebhookManifest } from "../.."; import { APL } from "../../APL"; import { createDebug } from "../../debug"; import { gqlAstToString } from "../../gql-ast-to-string"; -import { WebhookEvent } from "../../types"; +import { WebhookEvent,WebhookManifest } from "../../types"; import { processAsyncSaleorWebhook, SaleorWebhookError, @@ -97,6 +96,8 @@ export class SaleorAsyncWebhook { /** * Returns full URL to the webhook, based on provided baseUrl. * + * TODO: Shouldnt it be private? + * * @param baseUrl Base URL used by your application */ getTargetUrl(baseUrl: string) { diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 592d1d0..0000000 --- a/src/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AppWebhook as WebhookManifest } from "./types"; - -export * from "./const"; -export * from "./headers"; -export * from "./infer-webhooks"; -export * from "./saleor-app"; -export * from "./types"; -export * from "./urls"; -export { WebhookManifest }; diff --git a/src/types.ts b/src/types.ts index 89197be..5a3f138 100644 --- a/src/types.ts +++ b/src/types.ts @@ -55,7 +55,7 @@ export interface AppExtension { url: string; } -export interface AppWebhook { +export interface WebhookManifest { name: string; asyncEvents?: WebhookEvent[]; syncEvents?: WebhookEvent[]; @@ -111,5 +111,5 @@ export interface AppManifest { Be aware that subscription queries are required in manifest sections */ - webhooks?: AppWebhook[]; + webhooks?: WebhookManifest[]; }