Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse saleor schema version and add it to context #339

Merged
merged 5 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/large-weeks-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@saleor/app-sdk": minor
---

Parse the `saleor-schema-version` header and include the parsed version in the contexts of `createHandler` and `webhookFactory`. If the header is absent (Saleor version below 3.15), the version will default to `null`. This parsed version enables supporting multiple schemas in a single app, as outlined in the [RFC](https://github.com/saleor/apps/issues/1213).
1 change: 1 addition & 0 deletions src/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export const SALEOR_EVENT_HEADER = "saleor-event";
export const SALEOR_SIGNATURE_HEADER = "saleor-signature";
export const SALEOR_AUTHORIZATION_BEARER_HEADER = "authorization-bearer";
export const SALEOR_API_URL_HEADER = "saleor-api-url";
export const SALEOR_SCHEMA_VERSION = "saleor-schema-version";

export * from "./locales";
6 changes: 5 additions & 1 deletion src/handlers/next/create-manifest-handler.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { NextApiHandler, NextApiRequest } from "next";

import { getBaseUrl } from "../../headers";
import { getBaseUrl, getSaleorHeaders } from "../../headers";
import { AppManifest } from "../../types";

export type CreateManifestHandlerOptions = {
manifestFactory(context: {
appBaseUrl: string;
request: NextApiRequest;
/** For Saleor < 3.15 it will be null. */
schemaVersion: number | null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non blocking; you can add comment (jsdoc) mentioning here that null means <3.15

IDEs are using comments for hints

}): AppManifest | Promise<AppManifest>;
};

Expand All @@ -18,11 +20,13 @@ export type CreateManifestHandlerOptions = {
export const createManifestHandler =
(options: CreateManifestHandlerOptions): NextApiHandler =>
async (request, response) => {
const { schemaVersion } = getSaleorHeaders(request.headers);
const baseURL = getBaseUrl(request.headers);

const manifest = await options.manifestFactory({
appBaseUrl: baseURL,
request,
schemaVersion,
});

return response.status(200).json(manifest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ describe("processAsyncSaleorWebhook", () => {
"saleor-api-url": mockAPL.workingSaleorApiUrl,
"saleor-event": "product_updated",
"saleor-signature": "mocked_signature",
"content-length": "0", // is ignored by mocked raw-body
"content-length": "0", // is ignored by mocked raw-body.
"saleor-schema-version": "3.19",
},
method: "POST",
// body can be skipped because we mock it with raw-body
Expand Down Expand Up @@ -149,4 +150,49 @@ describe("processAsyncSaleorWebhook", () => {
})
).rejects.toThrow("Request signature check failed");
});

it("Fallback to null if saleor-schema-version header is missing", async () => {
delete mockRequest.headers["saleor-schema-version"];
await expect(
processSaleorWebhook({
req: mockRequest,
apl: mockAPL,
allowedEvent: "PRODUCT_UPDATED",
})
).resolves.toStrictEqual({
authData: {
appId: "mock-app-id",
domain: "example.com",
jwks: "{}",
saleorApiUrl: "https://example.com/graphql/",
token: "mock-token",
},
baseUrl: "https://some-saleor-host.cloud",
event: "product_updated",
payload: {},
schemaVersion: null,
});
});

it("Return schema version if saleor-schema-version header is present", async () => {
await expect(
processSaleorWebhook({
req: mockRequest,
apl: mockAPL,
allowedEvent: "PRODUCT_UPDATED",
})
).resolves.toStrictEqual({
authData: {
appId: "mock-app-id",
domain: "example.com",
jwks: "{}",
saleorApiUrl: "https://example.com/graphql/",
token: "mock-token",
},
baseUrl: "https://some-saleor-host.cloud",
event: "product_updated",
payload: {},
schemaVersion: 3.19,
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export type WebhookContext<T> = {
event: string;
payload: T;
authData: AuthData;
/** For Saleor < 3.15 it will be null. */
schemaVersion: number | null;
};

interface ProcessSaleorWebhookArgs {
Expand Down Expand Up @@ -87,7 +89,7 @@ export const processSaleorWebhook: ProcessSaleorWebhook = async <T>({
throw new WebhookError("Wrong request method, only POST allowed", "WRONG_METHOD");
}

const { event, signature, saleorApiUrl } = getSaleorHeaders(req.headers);
const { event, signature, saleorApiUrl, schemaVersion } = getSaleorHeaders(req.headers);
const baseUrl = getBaseUrl(req.headers);

if (!baseUrl) {
Expand All @@ -105,6 +107,10 @@ export const processSaleorWebhook: ProcessSaleorWebhook = async <T>({
throw new WebhookError("Missing saleor-event header", "MISSING_EVENT_HEADER");
}

if (!schemaVersion) {
debug("Missing saleor-schema-version header");
}

const expected = allowedEvent.toLowerCase();

if (event !== expected) {
Expand Down Expand Up @@ -209,6 +215,7 @@ export const processSaleorWebhook: ProcessSaleorWebhook = async <T>({
event,
payload: parsedBody as T,
authData,
schemaVersion,
};
} catch (err) {
const message = (err as Error)?.message ?? "Unknown error";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ describe("SaleorAsyncWebhook", () => {
baseUrl: "example.com",
event: "product_updated",
payload: { data: "test_payload" },
schemaVersion: 3.19,
authData: {
domain: "example.com",
token: "token",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ describe("SaleorSyncWebhook", () => {
baseUrl: "example.com",
event: "CHECKOUT_CALCULATE_TAXES",
payload: { data: "test_payload" },
schemaVersion: 3.19,
authData: {
domain: mockApl.workingSaleorDomain,
token: mockApl.mockToken,
Expand Down
5 changes: 5 additions & 0 deletions src/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import {
SALEOR_AUTHORIZATION_BEARER_HEADER,
SALEOR_DOMAIN_HEADER,
SALEOR_EVENT_HEADER,
SALEOR_SCHEMA_VERSION,
SALEOR_SIGNATURE_HEADER,
} from "./const";

const toStringOrUndefined = (value: string | string[] | undefined) =>
value ? value.toString() : undefined;

const toFloatOrNull = (value: string | string[] | undefined) =>
value ? parseFloat(value.toString()) : null;

/**
* Extracts Saleor-specific headers from the response.
*/
Expand All @@ -18,6 +22,7 @@ export const getSaleorHeaders = (headers: { [name: string]: string | string[] |
signature: toStringOrUndefined(headers[SALEOR_SIGNATURE_HEADER]),
event: toStringOrUndefined(headers[SALEOR_EVENT_HEADER]),
saleorApiUrl: toStringOrUndefined(headers[SALEOR_API_URL_HEADER]),
schemaVersion: toFloatOrNull(headers[SALEOR_SCHEMA_VERSION]),
});

/**
Expand Down
Loading