saleor-app-sdk-REDIS_APL/docs/saleor-async-webhook.md
Lukasz Ostrowski ea65d37474
Add better error handling for async webhook factory (#176)
* Add better error handling for async webhook factory

* Add docs, make formatter function async

* Add docs, make formatter function async
2023-02-14 16:19:07 +01:00

5.3 KiB

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

// 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<OrderPayload>({
  /**
   * 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: `
    subscription {
      event {
        ... on OrderCreated {
          order {
            id
          }
        }
      }
    }  
  `,
  /**
   * Optional
   *
   * Read internal errors
   */
  onError(error: WebhookError | Error) {
    // Can be used to e.g. trace errors
    sentry.captureError(error);
  },
  /**
   * Optional
   * Allows to set custom error response. If not provided, default mapping and message will be responsed
   * if Webhook validation fails
   */
  async formatErrorResponse(
    error: WebhookError | Error,
    req: NextApiRequest,
    res: NextApiResponse
  ) {
    return {
      code: 400,
      body: "My custom response",
    };
  },
});

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 to get information about webhooks to create.
SaleorAsyncWebhook utility can generate this manifest:

// 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.

type OrderPayload = {
  id: string;
};

export const orderCreatedWebhook = new SaleorAsyncWebhook<OrderPayload>({
  // ... 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, which we highly recommend, you should pass a subscription as GraphQL ASTNode:

/**
 * 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<ProductUpdatedWebhookPayloadFragment>({
  name: "Example product updated webhook",
  webhookPath: "api/webhooks/saleor/product-updated",
  asyncEvent: "PRODUCT_UPDATED",
  apl: saleorApp.apl,
  subscriptionQueryAst: ExampleProductUpdatedSubscription,
});