Refactors: webhooks, config (#941)

This commit is contained in:
Lukasz Ostrowski 2023-08-30 12:17:44 +02:00 committed by GitHub
parent 056209486c
commit ed30a818e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 407 additions and 417 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-app-search": patch
---
Refactor: Migrated private metadata of the app (used for settings) - previously each setting was saved in a dedicated metadata key. Now entire config is kept as JSON in a single field. This is non-breaking change - app will fallback to old config if needed.

View file

@ -0,0 +1,5 @@
---
"saleor-app-search": patch
---
Refactor: Extracted shared webhook logic, like clients creation and settings fetching to a shared function that creates entire context. Webhook handlers LoC decreased by half

View file

@ -0,0 +1,5 @@
---
"saleor-app-search": minor
---
Added "Saleor Commerce" as an author in the Manifest

View file

@ -7,7 +7,10 @@ import { Input } from "@saleor/react-hook-form-macaw";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { AppConfigurationFields, AppConfigurationSchema } from "../domain/configuration"; import {
AppConfigurationFields,
AppConfigurationSchema,
} from "../modules/configuration/configuration";
import { AlgoliaSearchProvider } from "../lib/algolia/algoliaSearchProvider"; import { AlgoliaSearchProvider } from "../lib/algolia/algoliaSearchProvider";
import { trpcClient } from "../modules/trpc/trpc-client"; import { trpcClient } from "../modules/trpc/trpc-client";

View file

@ -1,9 +0,0 @@
import { z } from "zod";
export const AppConfigurationSchema = z.object({
appId: z.string().min(3),
indexNamePrefix: z.string().optional(),
secretKey: z.string().min(3),
});
export type AppConfigurationFields = z.infer<typeof AppConfigurationSchema>;

View file

@ -2,12 +2,14 @@ import { AuthData } from "@saleor/app-sdk/APL";
import { createDebug } from "../debug"; import { createDebug } from "../debug";
import { createSettingsManager } from "../metadata"; import { createSettingsManager } from "../metadata";
import { createGraphQLClient } from "@saleor/apps-shared"; import { createGraphQLClient } from "@saleor/apps-shared";
import { AppConfigMetadataManager } from "../../modules/configuration/app-config-metadata-manager";
import { createLogger } from "../logger";
interface GetAlgoliaConfigurationArgs { interface GetAlgoliaConfigurationArgs {
authData: AuthData; authData: AuthData;
} }
const debug = createDebug("getAlgoliaConfiguration"); const logger = createLogger({ name: "getAlgoliaConfiguration" });
export const getAlgoliaConfiguration = async ({ authData }: GetAlgoliaConfigurationArgs) => { export const getAlgoliaConfiguration = async ({ authData }: GetAlgoliaConfigurationArgs) => {
const client = createGraphQLClient({ const client = createGraphQLClient({
@ -16,50 +18,33 @@ export const getAlgoliaConfiguration = async ({ authData }: GetAlgoliaConfigurat
}); });
const settings = createSettingsManager(client, authData.appId); const settings = createSettingsManager(client, authData.appId);
const configManager = new AppConfigMetadataManager(settings);
try { try {
const secretKey = await settings.get("secretKey", authData.domain); const config = await configManager.get(authData.saleorApiUrl);
if (!secretKey?.length) { if (config.getConfig()) {
return {
settings: config.getConfig(),
};
} else {
return { return {
errors: [ errors: [
{ {
message: message: "App is not configued. Please configue the app first",
"Missing secret key to the Algolia API. Please, configure the application first.",
}, },
], ],
}; };
} }
} catch (e) {
logger.error("Failed to fetch configuration from metadata");
const appId = await settings.get("appId", authData.domain);
if (!appId?.length) {
return { return {
errors: [ errors: [
{ {
message: "Missing App ID to the Algolia API. Please, configure the application first.", message: "Failed to load configuration",
}, },
], ],
}; };
} }
const indexNamePrefix = (await settings.get("indexNamePrefix", authData.domain)) || "";
debug("Configuration fetched");
return {
settings: {
appId,
secretKey,
indexNamePrefix,
},
};
} catch (error) {
debug("Unexpected error during fetching the configuration");
if (error instanceof Error) {
debug(error.message);
}
return {
errors: [{ message: "Couldn't fetch the settings from the API" }],
};
}
}; };

View file

@ -1,4 +1,7 @@
import { AppConfigurationFields, AppConfigurationSchema } from "../../domain/configuration"; import {
AppConfigurationFields,
AppConfigurationSchema,
} from "../../modules/configuration/configuration";
interface isConfiguredArgs { interface isConfiguredArgs {
configuration: Partial<AppConfigurationFields> | undefined; configuration: Partial<AppConfigurationFields> | undefined;

View file

@ -0,0 +1,38 @@
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { createGraphQLClient } from "@saleor/apps-shared";
import { AuthData } from "@saleor/app-sdk/APL";
import { AppConfig } from "./configuration";
import { createSettingsManager } from "../../lib/metadata";
export class AppConfigMetadataManager {
public readonly metadataKey = "app-config-v2";
constructor(private mm: SettingsManager) {}
async get(saleorApiUrl?: string) {
const metadata = await this.mm.get(this.metadataKey, saleorApiUrl);
return metadata ? AppConfig.parse(metadata) : new AppConfig();
}
set(config: AppConfig, saleorApiUrl?: string) {
return this.mm.set({
key: this.metadataKey,
value: config.serialize(),
domain: saleorApiUrl,
});
}
static createFromAuthData(authData: AuthData): AppConfigMetadataManager {
const settingsManager = createSettingsManager(
createGraphQLClient({
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
}),
authData.appId,
);
return new AppConfigMetadataManager(settingsManager);
}
}

View file

@ -2,11 +2,13 @@ import { createLogger } from "@saleor/apps-shared";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { ChannelsDocument } from "../../../generated/graphql"; import { ChannelsDocument } from "../../../generated/graphql";
import { WebhookActivityTogglerService } from "../../domain/WebhookActivityToggler.service"; import { WebhookActivityTogglerService } from "../../domain/WebhookActivityToggler.service";
import { AppConfigurationFields, AppConfigurationSchema } from "../../domain/configuration"; import { AppConfigurationFields, AppConfigurationSchema } from "./configuration";
import { AlgoliaSearchProvider } from "../../lib/algolia/algoliaSearchProvider"; import { AlgoliaSearchProvider } from "../../lib/algolia/algoliaSearchProvider";
import { createSettingsManager } from "../../lib/metadata"; import { createSettingsManager } from "../../lib/metadata";
import { protectedClientProcedure } from "../trpc/protected-client-procedure"; import { protectedClientProcedure } from "../trpc/protected-client-procedure";
import { router } from "../trpc/trpc-server"; import { router } from "../trpc/trpc-server";
import { fetchLegacyConfiguration } from "./legacy-configuration";
import { AppConfigMetadataManager } from "./app-config-metadata-manager";
const logger = createLogger({ name: "configuration.router" }); const logger = createLogger({ name: "configuration.router" });
@ -19,18 +21,23 @@ export const configurationRouter = router({
*/ */
const domain = new URL(ctx.saleorApiUrl).host; const domain = new URL(ctx.saleorApiUrl).host;
const config = await new AppConfigMetadataManager(settingsManager).get(ctx.saleorApiUrl);
/** /**
* TODO - refactor to single config in one key * Verify if config is filled with data - by default its null
*/ */
const data: AppConfigurationFields = { if (config.getConfig()) {
secretKey: (await settingsManager.get("secretKey", domain)) || "", return config.getConfig();
appId: (await settingsManager.get("appId", domain)) || "", } else {
indexNamePrefix: (await settingsManager.get("indexNamePrefix", domain)) || "", /**
}; * Otherwise fetch legacy config from old metadata keys
*/
const data = await fetchLegacyConfiguration(settingsManager, domain);
logger.debug("Will return config"); config.setAlgoliaSettings(data);
return data; return config.getConfig();
}
}), }),
setConfig: protectedClientProcedure setConfig: protectedClientProcedure
.meta({ requiredClientPermissions: ["MANAGE_APPS"] }) .meta({ requiredClientPermissions: ["MANAGE_APPS"] })
@ -48,22 +55,19 @@ export const configurationRouter = router({
const settingsManager = createSettingsManager(ctx.apiClient, ctx.appId); const settingsManager = createSettingsManager(ctx.apiClient, ctx.appId);
/** const configManager = new AppConfigMetadataManager(settingsManager);
* Backwards compatbitility
*/ const config = await configManager.get(ctx.saleorApiUrl);
const domain = new URL(ctx.saleorApiUrl).host;
try { try {
logger.debug("Will ping Algolia"); logger.trace("Will ping Algolia");
await algoliaClient.ping(); await algoliaClient.ping();
logger.debug("Algolia connection is ok. Will save settings"); logger.trace("Algolia connection is ok. Will save settings");
await settingsManager.set([ config.setAlgoliaSettings(input);
{ key: "secretKey", value: input.secretKey || "", domain },
{ key: "appId", value: input.appId || "", domain }, await configManager.set(config, ctx.saleorApiUrl);
{ key: "indexNamePrefix", value: input.indexNamePrefix || "", domain },
]);
logger.debug("Settings set successfully"); logger.debug("Settings set successfully");

View file

@ -0,0 +1,40 @@
import { z } from "zod";
export const AppConfigurationSchema = z.object({
appId: z.string().min(3),
indexNamePrefix: z.string().optional(),
secretKey: z.string().min(3),
});
export type AppConfigurationFields = z.infer<typeof AppConfigurationSchema>;
export const AppConfigRootSchema = AppConfigurationSchema.nullable();
export type AppConfigRootSchemaFields = z.infer<typeof AppConfigRootSchema>;
export class AppConfig {
private rootData: AppConfigRootSchemaFields = null;
constructor(initialData?: AppConfigRootSchemaFields) {
if (initialData) {
this.rootData = AppConfigurationSchema.parse(initialData);
}
}
static parse(serializedSchema: string) {
return new AppConfig(JSON.parse(serializedSchema));
}
serialize() {
return JSON.stringify(this.rootData);
}
setAlgoliaSettings(settings: AppConfigurationFields) {
this.rootData = AppConfigurationSchema.parse(settings);
return this;
}
getConfig() {
return this.rootData;
}
}

View file

@ -0,0 +1,18 @@
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { AppConfigurationFields } from "./configuration";
/**
* Before single-key configuration was introduced, this was a shape of settings.
*/
export const fetchLegacyConfiguration = async (
settingsManager: SettingsManager,
domain: string,
) => {
const data: AppConfigurationFields = {
secretKey: (await settingsManager.get("secretKey", domain)) || "",
appId: (await settingsManager.get("appId", domain)) || "",
indexNamePrefix: (await settingsManager.get("indexNamePrefix", domain)) || "",
};
return data;
};

View file

@ -40,6 +40,7 @@ export default createManifestHandler({
tokenTargetUrl: `${apiBaseURL}/api/register`, tokenTargetUrl: `${apiBaseURL}/api/register`,
version: packageJson.version, version: packageJson.version,
webhooks: appWebhooks.map((w) => w.getWebhookManifest(apiBaseURL)), webhooks: appWebhooks.map((w) => w.getWebhookManifest(apiBaseURL)),
author: "Saleor Commerce",
}; };
return manifest; return manifest;

View file

@ -1,11 +1,9 @@
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next"; import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { ChannelsDocument, ProductCreated } from "../../../../../generated/graphql"; import { ProductCreated } from "../../../../../generated/graphql";
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
import { createLogger } from "../../../../lib/logger"; import { createLogger } from "../../../../lib/logger";
import { createGraphQLClient } from "@saleor/apps-shared";
import { webhookProductCreated } from "../../../../webhooks/definitions/product-created"; import { webhookProductCreated } from "../../../../webhooks/definitions/product-created";
import { createWebhookContext } from "../../../../webhooks/webhook-context";
export const config = { export const config = {
api: { api: {
@ -31,50 +29,34 @@ export const handler: NextWebhookApiHandler<ProductCreated> = async (req, res, c
return res.status(200).end(); return res.status(200).end();
} }
const { settings, errors } = await getAlgoliaConfiguration({ authData }); try {
const client = createGraphQLClient({ const { algoliaClient, apiClient } = await createWebhookContext({ authData });
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
const channels = channelsData?.channels || [];
if (errors?.length || !settings) {
logger.warn("Aborting due to lack of settings");
logger.debug(errors);
return res.status(400).json({
message: errors[0].message,
});
}
const searchProvider = new AlgoliaSearchProvider({
appId: settings.appId,
apiKey: settings.secretKey,
indexNamePrefix: settings.indexNamePrefix,
channels,
});
try { try {
await searchProvider.createProduct(product); await algoliaClient.createProduct(product);
res.status(200).end();
return;
} catch (e) { } catch (e) {
logger.info(e, "Algolia createProduct failed. Webhooks will be disabled"); logger.info(e, "Algolia createProduct failed. Webhooks will be disabled");
const webhooksToggler = new WebhookActivityTogglerService( const webhooksToggler = new WebhookActivityTogglerService(authData.appId, apiClient);
authData.appId,
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
);
logger.trace("Will disable webhooks"); logger.trace("Will disable webhooks");
await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id)); await webhooksToggler.disableOwnWebhooks(
context.payload.recipient?.webhooks?.map((w) => w.id),
);
logger.trace("Webhooks disabling operation finished"); logger.trace("Webhooks disabling operation finished");
return res.status(500).send("Operation failed, webhooks are disabled"); return res.status(500).send("Operation failed, webhooks are disabled");
} }
res.status(200).end(); } catch (e) {
return; return res.status(400).json({
message: (e as Error).message,
});
}
}; };
export default webhookProductCreated.createHandler(handler); export default webhookProductCreated.createHandler(handler);

View file

@ -1,11 +1,9 @@
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next"; import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { ChannelsDocument, ProductDeleted } from "../../../../../generated/graphql"; import { ProductDeleted } from "../../../../../generated/graphql";
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
import { createLogger } from "../../../../lib/logger"; import { createLogger } from "../../../../lib/logger";
import { createGraphQLClient } from "@saleor/apps-shared";
import { webhookProductDeleted } from "../../../../webhooks/definitions/product-deleted"; import { webhookProductDeleted } from "../../../../webhooks/definitions/product-deleted";
import { createWebhookContext } from "../../../../webhooks/webhook-context";
export const config = { export const config = {
api: { api: {
@ -31,50 +29,34 @@ export const handler: NextWebhookApiHandler<ProductDeleted> = async (req, res, c
return res.status(200).end(); return res.status(200).end();
} }
const { settings, errors } = await getAlgoliaConfiguration({ authData }); try {
const client = createGraphQLClient({ const { algoliaClient, apiClient } = await createWebhookContext({ authData });
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
const channels = channelsData?.channels || [];
if (errors?.length || !settings) {
logger.warn("Aborting due to lack of settings");
logger.debug(errors);
return res.status(400).json({
message: errors[0].message,
});
}
const searchProvider = new AlgoliaSearchProvider({
appId: settings.appId,
apiKey: settings.secretKey,
indexNamePrefix: settings.indexNamePrefix,
channels,
});
try { try {
await searchProvider.deleteProduct(product); await algoliaClient.deleteProduct(product);
res.status(200).end();
return;
} catch (e) { } catch (e) {
logger.info(e, "Algolia deleteProduct failed. Webhooks will be disabled"); logger.info(e, "Algolia deleteProduct failed. Webhooks will be disabled");
const webhooksToggler = new WebhookActivityTogglerService( const webhooksToggler = new WebhookActivityTogglerService(authData.appId, apiClient);
authData.appId,
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
);
logger.trace("Will disable webhooks"); logger.trace("Will disable webhooks");
await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id)); await webhooksToggler.disableOwnWebhooks(
context.payload.recipient?.webhooks?.map((w) => w.id),
);
logger.trace("Webhooks disabling operation finished"); logger.trace("Webhooks disabling operation finished");
return res.status(500).send("Operation failed, webhooks are disabled"); return res.status(500).send("Operation failed, webhooks are disabled");
} }
res.status(200).end(); } catch (e) {
return; return res.status(400).json({
message: (e as Error).message,
});
}
}; };
export default webhookProductDeleted.createHandler(handler); export default webhookProductDeleted.createHandler(handler);

View file

@ -1,11 +1,9 @@
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next"; import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { ChannelsDocument, ProductUpdated } from "../../../../../generated/graphql"; import { ProductUpdated } from "../../../../../generated/graphql";
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
import { createLogger } from "../../../../lib/logger"; import { createLogger } from "../../../../lib/logger";
import { createGraphQLClient } from "@saleor/apps-shared";
import { webhookProductUpdated } from "../../../../webhooks/definitions/product-updated"; import { webhookProductUpdated } from "../../../../webhooks/definitions/product-updated";
import { createWebhookContext } from "../../../../webhooks/webhook-context";
export const config = { export const config = {
api: { api: {
@ -31,51 +29,34 @@ export const handler: NextWebhookApiHandler<ProductUpdated> = async (req, res, c
return res.status(200).end(); return res.status(200).end();
} }
const { settings, errors } = await getAlgoliaConfiguration({ authData }); try {
const client = createGraphQLClient({ const { algoliaClient, apiClient } = await createWebhookContext({ authData });
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
const channels = channelsData?.channels || [];
if (errors?.length || !settings) {
logger.warn("Aborting due to lack of settings");
logger.debug(errors);
return res.status(400).json({
message: errors[0].message,
});
}
const searchProvider = new AlgoliaSearchProvider({
appId: settings.appId,
apiKey: settings.secretKey,
indexNamePrefix: settings.indexNamePrefix,
channels,
});
try { try {
await searchProvider.updateProduct(product); await algoliaClient.updateProduct(product);
res.status(200).end();
return;
} catch (e) { } catch (e) {
logger.info(e, "Algolia updateProduct failed. Webhooks will be disabled"); logger.info(e, "Algolia updateProduct failed. Webhooks will be disabled");
const webhooksToggler = new WebhookActivityTogglerService( const webhooksToggler = new WebhookActivityTogglerService(authData.appId, apiClient);
authData.appId,
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
);
logger.trace("Will disable webhooks"); logger.trace("Will disable webhooks");
await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id)); await webhooksToggler.disableOwnWebhooks(
context.payload.recipient?.webhooks?.map((w) => w.id),
);
logger.trace("Webhooks disabling operation finished"); logger.trace("Webhooks disabling operation finished");
return res.status(500).send("Operation failed, webhooks are disabled"); return res.status(500).send("Operation failed, webhooks are disabled");
} }
} catch (e) {
res.status(200).end(); return res.status(400).json({
return; message: (e as Error).message,
});
}
}; };
export default webhookProductUpdated.createHandler(handler); export default webhookProductUpdated.createHandler(handler);

View file

@ -1,11 +1,9 @@
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next"; import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { createGraphQLClient } from "@saleor/apps-shared"; import { ProductVariantBackInStock } from "../../../../../generated/graphql";
import { ChannelsDocument, ProductVariantBackInStock } from "../../../../../generated/graphql";
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
import { createLogger } from "../../../../lib/logger"; import { createLogger } from "../../../../lib/logger";
import { webhookProductVariantBackInStock } from "../../../../webhooks/definitions/product-variant-back-in-stock"; import { webhookProductVariantBackInStock } from "../../../../webhooks/definitions/product-variant-back-in-stock";
import { createWebhookContext } from "../../../../webhooks/webhook-context";
export const config = { export const config = {
api: { api: {
@ -35,51 +33,34 @@ export const handler: NextWebhookApiHandler<ProductVariantBackInStock> = async (
return res.status(200).end(); return res.status(200).end();
} }
const { settings, errors } = await getAlgoliaConfiguration({ authData }); try {
const client = createGraphQLClient({ const { algoliaClient, apiClient } = await createWebhookContext({ authData });
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
const channels = channelsData?.channels || [];
if (errors?.length || !settings) {
logger.warn("Aborting due to lack of settings");
logger.debug(errors);
return res.status(400).json({
message: errors[0].message,
});
}
const searchProvider = new AlgoliaSearchProvider({
appId: settings.appId,
apiKey: settings.secretKey,
indexNamePrefix: settings.indexNamePrefix,
channels,
});
try { try {
await searchProvider.updateProductVariant(productVariant); await algoliaClient.updateProductVariant(productVariant);
res.status(200).end();
return;
} catch (e) { } catch (e) {
logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled"); logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled");
const webhooksToggler = new WebhookActivityTogglerService( const webhooksToggler = new WebhookActivityTogglerService(authData.appId, apiClient);
authData.appId,
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
);
logger.trace("Will disable webhooks"); logger.trace("Will disable webhooks");
await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id)); await webhooksToggler.disableOwnWebhooks(
context.payload.recipient?.webhooks?.map((w) => w.id),
);
logger.trace("Webhooks disabling operation finished"); logger.trace("Webhooks disabling operation finished");
return res.status(500).send("Operation failed, webhooks are disabled"); return res.status(500).send("Operation failed, webhooks are disabled");
} }
} catch (e) {
res.status(200).end(); return res.status(400).json({
return; message: (e as Error).message,
});
}
}; };
export default webhookProductVariantBackInStock.createHandler(handler); export default webhookProductVariantBackInStock.createHandler(handler);

View file

@ -1,11 +1,9 @@
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next"; import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { ChannelsDocument, ProductVariantCreated } from "../../../../../generated/graphql"; import { ProductVariantCreated } from "../../../../../generated/graphql";
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
import { createLogger } from "../../../../lib/logger";
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
import { createGraphQLClient } from "@saleor/apps-shared"; import { createLogger } from "../../../../lib/logger";
import { webhookProductVariantCreated } from "../../../../webhooks/definitions/product-variant-created"; import { webhookProductVariantCreated } from "../../../../webhooks/definitions/product-variant-created";
import { createWebhookContext } from "../../../../webhooks/webhook-context";
export const config = { export const config = {
api: { api: {
@ -31,50 +29,34 @@ export const handler: NextWebhookApiHandler<ProductVariantCreated> = async (req,
return res.status(200).end(); return res.status(200).end();
} }
const { settings, errors } = await getAlgoliaConfiguration({ authData }); try {
const client = createGraphQLClient({ const { algoliaClient, apiClient } = await createWebhookContext({ authData });
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
const channels = channelsData?.channels || [];
if (errors?.length || !settings) {
logger.warn("Aborting due to lack of settings");
logger.debug(errors);
return res.status(400).json({
message: errors[0].message,
});
}
const searchProvider = new AlgoliaSearchProvider({
appId: settings.appId,
apiKey: settings.secretKey,
indexNamePrefix: settings.indexNamePrefix,
channels,
});
try { try {
await searchProvider.createProductVariant(productVariant); await algoliaClient.createProductVariant(productVariant);
res.status(200).end();
return;
} catch (e) { } catch (e) {
logger.info(e, "Algolia createProductVariant failed. Webhooks will be disabled"); logger.info(e, "Algolia createProductVariant failed. Webhooks will be disabled");
const webhooksToggler = new WebhookActivityTogglerService( const webhooksToggler = new WebhookActivityTogglerService(authData.appId, apiClient);
authData.appId,
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
);
logger.trace("Will disable webhooks"); logger.trace("Will disable webhooks");
await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id)); await webhooksToggler.disableOwnWebhooks(
context.payload.recipient?.webhooks?.map((w) => w.id),
);
logger.trace("Webhooks disabling operation finished"); logger.trace("Webhooks disabling operation finished");
return res.status(500).send("Operation failed, webhooks are disabled"); return res.status(500).send("Operation failed, webhooks are disabled");
} }
} catch (e) {
res.status(200).end(); return res.status(400).json({
return; message: (e as Error).message,
});
}
}; };
export default webhookProductVariantCreated.createHandler(handler); export default webhookProductVariantCreated.createHandler(handler);

View file

@ -1,11 +1,9 @@
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next"; import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { ChannelsDocument, ProductVariantDeleted } from "../../../../../generated/graphql"; import { ProductVariantDeleted } from "../../../../../generated/graphql";
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
import { createLogger } from "../../../../lib/logger";
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
import { createGraphQLClient } from "@saleor/apps-shared"; import { createLogger } from "../../../../lib/logger";
import { webhookProductVariantDeleted } from "../../../../webhooks/definitions/product-variant-deleted"; import { webhookProductVariantDeleted } from "../../../../webhooks/definitions/product-variant-deleted";
import { createWebhookContext } from "../../../../webhooks/webhook-context";
export const config = { export const config = {
api: { api: {
@ -31,51 +29,34 @@ export const handler: NextWebhookApiHandler<ProductVariantDeleted> = async (req,
return res.status(200).end(); return res.status(200).end();
} }
const { settings, errors } = await getAlgoliaConfiguration({ authData }); try {
const client = createGraphQLClient({ const { algoliaClient, apiClient } = await createWebhookContext({ authData });
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
const channels = channelsData?.channels || [];
if (errors?.length || !settings) {
logger.warn("Aborting due to lack of settings");
logger.debug(errors);
return res.status(400).json({
message: errors[0].message,
});
}
const searchProvider = new AlgoliaSearchProvider({
appId: settings.appId,
apiKey: settings.secretKey,
indexNamePrefix: settings.indexNamePrefix,
channels,
});
try { try {
await searchProvider.deleteProductVariant(productVariant); await algoliaClient.deleteProductVariant(productVariant);
res.status(200).end();
return;
} catch (e) { } catch (e) {
logger.info(e, "Algolia deleteProductVariant failed. Webhooks will be disabled"); logger.info(e, "Algolia deleteProductVariant failed. Webhooks will be disabled");
const webhooksToggler = new WebhookActivityTogglerService( const webhooksToggler = new WebhookActivityTogglerService(authData.appId, apiClient);
authData.appId,
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
);
logger.trace("Will disable webhooks"); logger.trace("Will disable webhooks");
await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id)); await webhooksToggler.disableOwnWebhooks(
context.payload.recipient?.webhooks?.map((w) => w.id),
);
logger.trace("Webhooks disabling operation finished"); logger.trace("Webhooks disabling operation finished");
return res.status(500).send("Operation failed, webhooks are disabled"); return res.status(500).send("Operation failed, webhooks are disabled");
} }
} catch (e) {
res.status(200).end(); return res.status(400).json({
return; message: (e as Error).message,
});
}
}; };
export default webhookProductVariantDeleted.createHandler(handler); export default webhookProductVariantDeleted.createHandler(handler);

View file

@ -1,11 +1,9 @@
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next"; import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { createGraphQLClient } from "@saleor/apps-shared"; import { ProductVariantOutOfStock } from "../../../../../generated/graphql";
import { ChannelsDocument, ProductVariantOutOfStock } from "../../../../../generated/graphql";
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
import { createLogger } from "../../../../lib/logger"; import { createLogger } from "../../../../lib/logger";
import { webhookProductVariantOutOfStock } from "../../../../webhooks/definitions/product-variant-out-of-stock"; import { webhookProductVariantOutOfStock } from "../../../../webhooks/definitions/product-variant-out-of-stock";
import { createWebhookContext } from "../../../../webhooks/webhook-context";
export const config = { export const config = {
api: { api: {
@ -35,51 +33,34 @@ export const handler: NextWebhookApiHandler<ProductVariantOutOfStock> = async (
return res.status(200).end(); return res.status(200).end();
} }
const { settings, errors } = await getAlgoliaConfiguration({ authData }); try {
const client = createGraphQLClient({ const { algoliaClient, apiClient } = await createWebhookContext({ authData });
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
const channels = channelsData?.channels || [];
if (errors?.length || !settings) {
logger.warn("Aborting due to lack of settings");
logger.debug(errors);
return res.status(400).json({
message: errors[0].message,
});
}
const searchProvider = new AlgoliaSearchProvider({
appId: settings.appId,
apiKey: settings.secretKey,
indexNamePrefix: settings.indexNamePrefix,
channels,
});
try { try {
await searchProvider.updateProductVariant(productVariant); await algoliaClient.updateProductVariant(productVariant);
res.status(200).end();
return;
} catch (e) { } catch (e) {
logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled"); logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled");
const webhooksToggler = new WebhookActivityTogglerService( const webhooksToggler = new WebhookActivityTogglerService(authData.appId, apiClient);
authData.appId,
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
);
logger.trace("Will disable webhooks"); logger.trace("Will disable webhooks");
await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id)); await webhooksToggler.disableOwnWebhooks(
context.payload.recipient?.webhooks?.map((w) => w.id),
);
logger.trace("Webhooks disabling operation finished"); logger.trace("Webhooks disabling operation finished");
return res.status(500).send("Operation failed, webhooks are disabled"); return res.status(500).send("Operation failed, webhooks are disabled");
} }
} catch (e) {
res.status(200).end(); return res.status(400).json({
return; message: (e as Error).message,
});
}
}; };
export default webhookProductVariantOutOfStock.createHandler(handler); export default webhookProductVariantOutOfStock.createHandler(handler);

View file

@ -1,11 +1,9 @@
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next"; import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { createGraphQLClient } from "@saleor/apps-shared"; import { ProductVariantUpdated } from "../../../../../generated/graphql";
import { ChannelsDocument, ProductVariantUpdated } from "../../../../../generated/graphql";
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
import { createLogger } from "../../../../lib/logger"; import { createLogger } from "../../../../lib/logger";
import { webhookProductVariantUpdated } from "../../../../webhooks/definitions/product-variant-updated"; import { webhookProductVariantUpdated } from "../../../../webhooks/definitions/product-variant-updated";
import { createWebhookContext } from "../../../../webhooks/webhook-context";
export const config = { export const config = {
api: { api: {
@ -31,52 +29,34 @@ export const handler: NextWebhookApiHandler<ProductVariantUpdated> = async (req,
return res.status(200).end(); return res.status(200).end();
} }
const { settings, errors } = await getAlgoliaConfiguration({ authData }); try {
const client = createGraphQLClient({ const { algoliaClient, apiClient } = await createWebhookContext({ authData });
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
const channels = channelsData?.channels || [];
if (errors?.length || !settings) {
logger.warn("Aborting due to lack of settings");
logger.debug(errors);
return res.status(400).json({
message: errors[0].message,
});
}
const searchProvider = new AlgoliaSearchProvider({
appId: settings.appId,
apiKey: settings.secretKey,
indexNamePrefix: settings.indexNamePrefix,
channels,
});
try { try {
logger.debug("Updating variant"); await algoliaClient.updateProductVariant(productVariant);
await searchProvider.updateProductVariant(productVariant);
res.status(200).end();
return;
} catch (e) { } catch (e) {
logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled"); logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled");
const webhooksToggler = new WebhookActivityTogglerService( const webhooksToggler = new WebhookActivityTogglerService(authData.appId, apiClient);
authData.appId,
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
);
logger.trace("Will disable webhooks"); logger.trace("Will disable webhooks");
await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id)); await webhooksToggler.disableOwnWebhooks(
context.payload.recipient?.webhooks?.map((w) => w.id),
);
logger.trace("Webhooks disabling operation finished"); logger.trace("Webhooks disabling operation finished");
return res.status(500).send("Operation failed, webhooks are disabled"); return res.status(500).send("Operation failed, webhooks are disabled");
} }
} catch (e) {
res.status(200).end(); return res.status(400).json({
return; message: (e as Error).message,
});
}
}; };
export default webhookProductVariantUpdated.createHandler(handler); export default webhookProductVariantUpdated.createHandler(handler);

View file

@ -0,0 +1,42 @@
import { AuthData } from "@saleor/app-sdk/APL";
import { getAlgoliaConfiguration } from "../lib/algolia/getAlgoliaConfiguration";
import { createGraphQLClient } from "@saleor/apps-shared";
import { ChannelsDocument } from "../../generated/graphql";
import { AlgoliaSearchProvider } from "../lib/algolia/algoliaSearchProvider";
/**
* Fetches and creates all shared entities required by webhook to proceed
*/
export const createWebhookContext = async ({ authData }: { authData: AuthData }) => {
const { settings, errors } = await getAlgoliaConfiguration({ authData });
const apiClient = createGraphQLClient({
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
const { data: channelsData } = await apiClient.query(ChannelsDocument, {}).toPromise();
const channels = channelsData?.channels || [];
if (!settings || errors) {
let errorMessage = "Error fetching settings";
if (errors && errors.length > 0 && errors[0].message) {
errorMessage = errors[0].message;
}
throw new Error(errorMessage);
}
const algoliaClient = new AlgoliaSearchProvider({
appId: settings.appId,
apiKey: settings.secretKey,
indexNamePrefix: settings.indexNamePrefix,
channels,
});
return {
apiClient,
channels,
settings,
algoliaClient,
};
};