saleor-app-sdk-REDIS_APL/docs/saleor-webhook.md
Lukasz Ostrowski 9420209a2d
Add sync webhook (#202)
* Change process-async-saleor-webhook to process-saleor-webhook and add better debug logs

* Change processWebhook name

* wip

* Add base class structure

* refactor

* add sync payload builder

* wip

* wip

* Add some missing webhook responses

* Fix tests for Async version

* Fix tests

* Add MockAPL test util

* Refactor tests to use MockAPL

* Add test to sync webhook

* Restore legacy subscriptionQueryAst field for compatibility

* CR Fixes - TS type & event -> eventType

* Changelog, docs and remove old payment webhooks

* Apply suggestions from code review

Co-authored-by: Krzysztof Wolski <krzysztof.k.wolski@gmail.com>

* Update src/handlers/next/saleor-webhooks/process-saleor-webhook.ts

Co-authored-by: Krzysztof Wolski <krzysztof.k.wolski@gmail.com>

---------

Co-authored-by: Krzysztof Wolski <krzysztof.k.wolski@gmail.com>
2023-03-06 15:01:22 +01:00

305 lines
8.4 KiB
Markdown

# 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.
To inform your App about events originated from Saleor, you need to expose a webhook handler, which Saleor will call with POST request.
The App SDK provides a utility that abstracts connection details and auth, allowing developers to focus on business logic.
Depending on the type of the webhook, you can choose one of the classes:
- `SaleorAsyncWebhook`
- `SaleorSyncWebhook`
## Common configuration
Both `SaleorSyncWebhook` and `SaleorAsyncWebhook` contain similar API with little differences.
### Constructing Webhook instance
In Next.js pages create a page, e.g. `pages/api/webhooks/order-created.ts`. We recommend to keep webhook type in file name, which will be resolved by Next.js to route path.
```typescript
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
/**
* 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>(options);
```
For `SaleorSyncWebhook` it will be similar. Create e.g. `order-calculate-taxes.ts` page and create a new instance:
```typescript
import { SaleorSyncWebhook } from "@saleor/app-sdk/handlers/next";
/**
* To be type-safe, define payload from API. This should be imported from generated GraphQL code
*/
type CalculateTaxedPayload = {
// taxes payload from subscription
};
export const orderCalculateTaxesWebhook = new SaleorSyncWebhook<OrderPayload>(options);
```
### Configuring Webhook instance
`options` in constructor must be specified. Here are all options:
```typescript
type Options = {
/**
* Additional webhook name, optional.
*/
name?: string;
/**
* Path to webhook. Should represent relative path from base app URL. In Next.js it will start with `api/`, e.g. `api/webhooks/order-created`.
*/
webhookPath: string;
/**
* Valid Async or Sync webhook. Constructor is statically typed so only valid Sync/Async webhooks will be allowed
*/
event: Event;
/**
* Should event be active after installation, enabled by default
*/
isActive?: boolean;
/**
* APL instance - see docs/apl/md
*/
apl: APL;
/**
* Optional callback that allows to inject custom error handling and take control of the response
*/
onError?(error: WebhookError | Error, req: NextApiRequest, res: NextApiResponse): void;
/**
* Optional callback that allows to format error message. Useful to control how much details should be returned in the response
*/
formatErrorResponse?(
error: WebhookError | Error,
req: NextApiRequest,
res: NextApiResponse
): Promise<{
code: number;
body: object | string;
}>;
/**
* Required subscription query. Can be raw GraphQL string or the instance of query wrapped in `gql` tags
*/
query: string | ASTNode;
};
```
#### Configuration examples
##### SaleorAsyncWebhook configuration example
```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<OrderPayload>({
name: "Order Created",
webhookPath: "api/webhooks/order-created",
event: "ORDER_CREATED",
isActive: true,
apl: require("../lib/apl"),
query: `
subscription {
event {
... on OrderCreated {
order {
id
}
}
}
}
`,
onError(error: WebhookError | Error) {
// Can be used to e.g. trace errors
sentry.captureError(error);
},
async formatErrorResponse(
error: WebhookError | Error,
req: NextApiRequest,
res: NextApiResponse
) {
return {
code: 400,
body: "My custom response",
};
},
});
```
##### SyncAsyncWebhook configuration example
```typescript
// pages/api/webhooks/order-created.ts
/**
* To be type safe, define payload from API. This should be imported from generated graphQL code
*/
type Payload = {
taxBase: {
currency: string;
};
};
export const orderCalculateTaxesWebhook = new SaleorAsyncWebhook<Payload>({
name: "Order Calculate Taxes",
webhookPath: "api/webhooks/order-created",
event: "ORDER_CALCULATE_TAXES",
isActive: true,
apl: require("../lib/apl"),
query: `
subscription {
event {
... on CalculateTaxes {
taxBase {
currency
}
}
}
}
`,
onError(error: WebhookError | Error) {
sentry.captureError(error);
},
async formatErrorResponse(
error: WebhookError | Error,
req: NextApiRequest,
res: NextApiResponse
) {
return {
code: 400,
body: "My custom response",
};
},
});
```
### Resources
- 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)
### 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.
`SaleorSyncWebhook` & `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<OrderPayload>({
// ... your configuration
});
/**
* Handler has to be a default export so the Next.js will be able to use it
*/
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();
});
```
### Typed sync webhook response
Sync webhooks need to return response to Saleor, so operation can be completed. To achieve that, `SaleorAsyncWebhook` injects additional context field `buildResponse`.
It infers even from constructor and provides typed factory:
```typescript
const webhook = new SaleorAsyncWebhook({ event: "ORDER_CALCULATE_TAXES" /* ... rest of config */ });
orderCreatedWebhook.createHandler((req, res, context) => {
return res.status(200).send(
context.buildResponse({
// Fields are typed here
})
);
});
```
### 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<ProductUpdatedWebhookPayloadFragment>({
name: "Example product updated webhook",
webhookPath: "api/webhooks/saleor/product-updated",
event: "PRODUCT_UPDATED",
apl: saleorApp.apl,
query: ExampleProductUpdatedSubscription, // Or use plain string
});
```