diff --git a/.changeset/happy-colts-cough.md b/.changeset/happy-colts-cough.md new file mode 100644 index 0000000..ba06340 --- /dev/null +++ b/.changeset/happy-colts-cough.md @@ -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. diff --git a/.changeset/kind-wombats-beg.md b/.changeset/kind-wombats-beg.md new file mode 100644 index 0000000..21ecb41 --- /dev/null +++ b/.changeset/kind-wombats-beg.md @@ -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 diff --git a/.changeset/metal-worms-double.md b/.changeset/metal-worms-double.md new file mode 100644 index 0000000..0578a0b --- /dev/null +++ b/.changeset/metal-worms-double.md @@ -0,0 +1,5 @@ +--- +"saleor-app-search": minor +--- + +Added "Saleor Commerce" as an author in the Manifest diff --git a/apps/search/src/components/AlgoliaConfigurationForm.tsx b/apps/search/src/components/AlgoliaConfigurationForm.tsx index ce2f525..7d1c7ee 100644 --- a/apps/search/src/components/AlgoliaConfigurationForm.tsx +++ b/apps/search/src/components/AlgoliaConfigurationForm.tsx @@ -7,7 +7,10 @@ import { Input } from "@saleor/react-hook-form-macaw"; import { useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; 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 { trpcClient } from "../modules/trpc/trpc-client"; diff --git a/apps/search/src/domain/configuration.ts b/apps/search/src/domain/configuration.ts deleted file mode 100644 index e25125e..0000000 --- a/apps/search/src/domain/configuration.ts +++ /dev/null @@ -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; diff --git a/apps/search/src/lib/algolia/getAlgoliaConfiguration.ts b/apps/search/src/lib/algolia/getAlgoliaConfiguration.ts index e7a8b9b..d68cb25 100644 --- a/apps/search/src/lib/algolia/getAlgoliaConfiguration.ts +++ b/apps/search/src/lib/algolia/getAlgoliaConfiguration.ts @@ -2,12 +2,14 @@ import { AuthData } from "@saleor/app-sdk/APL"; import { createDebug } from "../debug"; import { createSettingsManager } from "../metadata"; import { createGraphQLClient } from "@saleor/apps-shared"; +import { AppConfigMetadataManager } from "../../modules/configuration/app-config-metadata-manager"; +import { createLogger } from "../logger"; interface GetAlgoliaConfigurationArgs { authData: AuthData; } -const debug = createDebug("getAlgoliaConfiguration"); +const logger = createLogger({ name: "getAlgoliaConfiguration" }); export const getAlgoliaConfiguration = async ({ authData }: GetAlgoliaConfigurationArgs) => { const client = createGraphQLClient({ @@ -16,50 +18,33 @@ export const getAlgoliaConfiguration = async ({ authData }: GetAlgoliaConfigurat }); const settings = createSettingsManager(client, authData.appId); + const configManager = new AppConfigMetadataManager(settings); 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 { errors: [ { - message: - "Missing secret key to the Algolia API. Please, configure the application first.", + message: "App is not configued. Please configue the app first", }, ], }; } + } catch (e) { + logger.error("Failed to fetch configuration from metadata"); - const appId = await settings.get("appId", authData.domain); - - if (!appId?.length) { - return { - errors: [ - { - message: "Missing App ID to the Algolia API. Please, configure the application first.", - }, - ], - }; - } - - 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" }], + errors: [ + { + message: "Failed to load configuration", + }, + ], }; } }; diff --git a/apps/search/src/lib/algolia/is-configured.ts b/apps/search/src/lib/algolia/is-configured.ts index 63776db..04550dc 100644 --- a/apps/search/src/lib/algolia/is-configured.ts +++ b/apps/search/src/lib/algolia/is-configured.ts @@ -1,4 +1,7 @@ -import { AppConfigurationFields, AppConfigurationSchema } from "../../domain/configuration"; +import { + AppConfigurationFields, + AppConfigurationSchema, +} from "../../modules/configuration/configuration"; interface isConfiguredArgs { configuration: Partial | undefined; diff --git a/apps/search/src/modules/configuration/app-config-metadata-manager.ts b/apps/search/src/modules/configuration/app-config-metadata-manager.ts new file mode 100644 index 0000000..2cb5fc9 --- /dev/null +++ b/apps/search/src/modules/configuration/app-config-metadata-manager.ts @@ -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); + } +} diff --git a/apps/search/src/modules/configuration/configuration.router.ts b/apps/search/src/modules/configuration/configuration.router.ts index 2ba5628..45a4d12 100644 --- a/apps/search/src/modules/configuration/configuration.router.ts +++ b/apps/search/src/modules/configuration/configuration.router.ts @@ -2,11 +2,13 @@ import { createLogger } from "@saleor/apps-shared"; import { TRPCError } from "@trpc/server"; import { ChannelsDocument } from "../../../generated/graphql"; import { WebhookActivityTogglerService } from "../../domain/WebhookActivityToggler.service"; -import { AppConfigurationFields, AppConfigurationSchema } from "../../domain/configuration"; +import { AppConfigurationFields, AppConfigurationSchema } from "./configuration"; import { AlgoliaSearchProvider } from "../../lib/algolia/algoliaSearchProvider"; import { createSettingsManager } from "../../lib/metadata"; import { protectedClientProcedure } from "../trpc/protected-client-procedure"; import { router } from "../trpc/trpc-server"; +import { fetchLegacyConfiguration } from "./legacy-configuration"; +import { AppConfigMetadataManager } from "./app-config-metadata-manager"; const logger = createLogger({ name: "configuration.router" }); @@ -19,18 +21,23 @@ export const configurationRouter = router({ */ 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 = { - secretKey: (await settingsManager.get("secretKey", domain)) || "", - appId: (await settingsManager.get("appId", domain)) || "", - indexNamePrefix: (await settingsManager.get("indexNamePrefix", domain)) || "", - }; + if (config.getConfig()) { + return config.getConfig(); + } else { + /** + * 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 .meta({ requiredClientPermissions: ["MANAGE_APPS"] }) @@ -48,22 +55,19 @@ export const configurationRouter = router({ const settingsManager = createSettingsManager(ctx.apiClient, ctx.appId); - /** - * Backwards compatbitility - */ - const domain = new URL(ctx.saleorApiUrl).host; + const configManager = new AppConfigMetadataManager(settingsManager); + + const config = await configManager.get(ctx.saleorApiUrl); try { - logger.debug("Will ping Algolia"); + logger.trace("Will ping Algolia"); await algoliaClient.ping(); - logger.debug("Algolia connection is ok. Will save settings"); + logger.trace("Algolia connection is ok. Will save settings"); - await settingsManager.set([ - { key: "secretKey", value: input.secretKey || "", domain }, - { key: "appId", value: input.appId || "", domain }, - { key: "indexNamePrefix", value: input.indexNamePrefix || "", domain }, - ]); + config.setAlgoliaSettings(input); + + await configManager.set(config, ctx.saleorApiUrl); logger.debug("Settings set successfully"); diff --git a/apps/search/src/modules/configuration/configuration.ts b/apps/search/src/modules/configuration/configuration.ts new file mode 100644 index 0000000..482b148 --- /dev/null +++ b/apps/search/src/modules/configuration/configuration.ts @@ -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; + +export const AppConfigRootSchema = AppConfigurationSchema.nullable(); +export type AppConfigRootSchemaFields = z.infer; + +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; + } +} diff --git a/apps/search/src/modules/configuration/legacy-configuration.ts b/apps/search/src/modules/configuration/legacy-configuration.ts new file mode 100644 index 0000000..d6f360b --- /dev/null +++ b/apps/search/src/modules/configuration/legacy-configuration.ts @@ -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; +}; diff --git a/apps/search/src/pages/api/manifest.ts b/apps/search/src/pages/api/manifest.ts index 94d4b5e..288f90c 100644 --- a/apps/search/src/pages/api/manifest.ts +++ b/apps/search/src/pages/api/manifest.ts @@ -40,6 +40,7 @@ export default createManifestHandler({ tokenTargetUrl: `${apiBaseURL}/api/register`, version: packageJson.version, webhooks: appWebhooks.map((w) => w.getWebhookManifest(apiBaseURL)), + author: "Saleor Commerce", }; return manifest; diff --git a/apps/search/src/pages/api/webhooks/saleor/product_created.ts b/apps/search/src/pages/api/webhooks/saleor/product_created.ts index cfbacca..4079033 100644 --- a/apps/search/src/pages/api/webhooks/saleor/product_created.ts +++ b/apps/search/src/pages/api/webhooks/saleor/product_created.ts @@ -1,11 +1,9 @@ import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next"; -import { ChannelsDocument, ProductCreated } from "../../../../../generated/graphql"; -import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider"; -import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration"; +import { ProductCreated } from "../../../../../generated/graphql"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { createLogger } from "../../../../lib/logger"; -import { createGraphQLClient } from "@saleor/apps-shared"; import { webhookProductCreated } from "../../../../webhooks/definitions/product-created"; +import { createWebhookContext } from "../../../../webhooks/webhook-context"; export const config = { api: { @@ -31,50 +29,34 @@ export const handler: NextWebhookApiHandler = async (req, res, c return res.status(200).end(); } - const { settings, errors } = await getAlgoliaConfiguration({ authData }); - const client = createGraphQLClient({ - saleorApiUrl: authData.saleorApiUrl, - token: authData.token, - }); - const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise(); - const channels = channelsData?.channels || []; + try { + const { algoliaClient, apiClient } = await createWebhookContext({ authData }); - if (errors?.length || !settings) { - logger.warn("Aborting due to lack of settings"); - logger.debug(errors); + try { + await algoliaClient.createProduct(product); + res.status(200).end(); + return; + } catch (e) { + logger.info(e, "Algolia createProduct failed. Webhooks will be disabled"); + + const webhooksToggler = new WebhookActivityTogglerService(authData.appId, apiClient); + + logger.trace("Will disable webhooks"); + + await webhooksToggler.disableOwnWebhooks( + context.payload.recipient?.webhooks?.map((w) => w.id), + ); + + logger.trace("Webhooks disabling operation finished"); + + return res.status(500).send("Operation failed, webhooks are disabled"); + } + } catch (e) { return res.status(400).json({ - message: errors[0].message, + message: (e as Error).message, }); } - - const searchProvider = new AlgoliaSearchProvider({ - appId: settings.appId, - apiKey: settings.secretKey, - indexNamePrefix: settings.indexNamePrefix, - channels, - }); - - try { - await searchProvider.createProduct(product); - } catch (e) { - logger.info(e, "Algolia createProduct failed. Webhooks will be disabled"); - - const webhooksToggler = new WebhookActivityTogglerService( - authData.appId, - createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }), - ); - - logger.trace("Will disable webhooks"); - - await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id)); - - logger.trace("Webhooks disabling operation finished"); - - return res.status(500).send("Operation failed, webhooks are disabled"); - } - res.status(200).end(); - return; }; export default webhookProductCreated.createHandler(handler); diff --git a/apps/search/src/pages/api/webhooks/saleor/product_deleted.ts b/apps/search/src/pages/api/webhooks/saleor/product_deleted.ts index be79fa7..4634694 100644 --- a/apps/search/src/pages/api/webhooks/saleor/product_deleted.ts +++ b/apps/search/src/pages/api/webhooks/saleor/product_deleted.ts @@ -1,11 +1,9 @@ import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next"; -import { ChannelsDocument, ProductDeleted } from "../../../../../generated/graphql"; -import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider"; -import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration"; +import { ProductDeleted } from "../../../../../generated/graphql"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { createLogger } from "../../../../lib/logger"; -import { createGraphQLClient } from "@saleor/apps-shared"; import { webhookProductDeleted } from "../../../../webhooks/definitions/product-deleted"; +import { createWebhookContext } from "../../../../webhooks/webhook-context"; export const config = { api: { @@ -31,50 +29,34 @@ export const handler: NextWebhookApiHandler = async (req, res, c return res.status(200).end(); } - const { settings, errors } = await getAlgoliaConfiguration({ authData }); - const client = createGraphQLClient({ - saleorApiUrl: authData.saleorApiUrl, - token: authData.token, - }); - const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise(); - const channels = channelsData?.channels || []; + try { + const { algoliaClient, apiClient } = await createWebhookContext({ authData }); - if (errors?.length || !settings) { - logger.warn("Aborting due to lack of settings"); - logger.debug(errors); + try { + await algoliaClient.deleteProduct(product); + res.status(200).end(); + return; + } catch (e) { + logger.info(e, "Algolia deleteProduct failed. Webhooks will be disabled"); + + const webhooksToggler = new WebhookActivityTogglerService(authData.appId, apiClient); + + logger.trace("Will disable webhooks"); + + await webhooksToggler.disableOwnWebhooks( + context.payload.recipient?.webhooks?.map((w) => w.id), + ); + + logger.trace("Webhooks disabling operation finished"); + + return res.status(500).send("Operation failed, webhooks are disabled"); + } + } catch (e) { return res.status(400).json({ - message: errors[0].message, + message: (e as Error).message, }); } - - const searchProvider = new AlgoliaSearchProvider({ - appId: settings.appId, - apiKey: settings.secretKey, - indexNamePrefix: settings.indexNamePrefix, - channels, - }); - - try { - await searchProvider.deleteProduct(product); - } catch (e) { - logger.info(e, "Algolia deleteProduct failed. Webhooks will be disabled"); - - const webhooksToggler = new WebhookActivityTogglerService( - authData.appId, - createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }), - ); - - logger.trace("Will disable webhooks"); - - await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id)); - - logger.trace("Webhooks disabling operation finished"); - - return res.status(500).send("Operation failed, webhooks are disabled"); - } - res.status(200).end(); - return; }; export default webhookProductDeleted.createHandler(handler); diff --git a/apps/search/src/pages/api/webhooks/saleor/product_updated.ts b/apps/search/src/pages/api/webhooks/saleor/product_updated.ts index 1624608..c4f723b 100644 --- a/apps/search/src/pages/api/webhooks/saleor/product_updated.ts +++ b/apps/search/src/pages/api/webhooks/saleor/product_updated.ts @@ -1,11 +1,9 @@ import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next"; -import { ChannelsDocument, ProductUpdated } from "../../../../../generated/graphql"; -import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider"; -import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration"; +import { ProductUpdated } from "../../../../../generated/graphql"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { createLogger } from "../../../../lib/logger"; -import { createGraphQLClient } from "@saleor/apps-shared"; import { webhookProductUpdated } from "../../../../webhooks/definitions/product-updated"; +import { createWebhookContext } from "../../../../webhooks/webhook-context"; export const config = { api: { @@ -31,51 +29,34 @@ export const handler: NextWebhookApiHandler = async (req, res, c return res.status(200).end(); } - const { settings, errors } = await getAlgoliaConfiguration({ authData }); - const client = createGraphQLClient({ - saleorApiUrl: authData.saleorApiUrl, - token: authData.token, - }); - const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise(); - const channels = channelsData?.channels || []; + try { + const { algoliaClient, apiClient } = await createWebhookContext({ authData }); - if (errors?.length || !settings) { - logger.warn("Aborting due to lack of settings"); - logger.debug(errors); + try { + await algoliaClient.updateProduct(product); + res.status(200).end(); + return; + } catch (e) { + logger.info(e, "Algolia updateProduct failed. Webhooks will be disabled"); + + const webhooksToggler = new WebhookActivityTogglerService(authData.appId, apiClient); + + logger.trace("Will disable webhooks"); + + await webhooksToggler.disableOwnWebhooks( + context.payload.recipient?.webhooks?.map((w) => w.id), + ); + + logger.trace("Webhooks disabling operation finished"); + + return res.status(500).send("Operation failed, webhooks are disabled"); + } + } catch (e) { return res.status(400).json({ - message: errors[0].message, + message: (e as Error).message, }); } - - const searchProvider = new AlgoliaSearchProvider({ - appId: settings.appId, - apiKey: settings.secretKey, - indexNamePrefix: settings.indexNamePrefix, - channels, - }); - - try { - await searchProvider.updateProduct(product); - } catch (e) { - logger.info(e, "Algolia updateProduct failed. Webhooks will be disabled"); - - const webhooksToggler = new WebhookActivityTogglerService( - authData.appId, - createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }), - ); - - logger.trace("Will disable webhooks"); - - await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id)); - - logger.trace("Webhooks disabling operation finished"); - - return res.status(500).send("Operation failed, webhooks are disabled"); - } - - res.status(200).end(); - return; }; export default webhookProductUpdated.createHandler(handler); diff --git a/apps/search/src/pages/api/webhooks/saleor/product_variant_back_in_stock.ts b/apps/search/src/pages/api/webhooks/saleor/product_variant_back_in_stock.ts index 1808111..2c0491e 100644 --- a/apps/search/src/pages/api/webhooks/saleor/product_variant_back_in_stock.ts +++ b/apps/search/src/pages/api/webhooks/saleor/product_variant_back_in_stock.ts @@ -1,11 +1,9 @@ import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next"; -import { createGraphQLClient } from "@saleor/apps-shared"; -import { ChannelsDocument, ProductVariantBackInStock } from "../../../../../generated/graphql"; +import { ProductVariantBackInStock } from "../../../../../generated/graphql"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; -import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider"; -import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration"; import { createLogger } from "../../../../lib/logger"; import { webhookProductVariantBackInStock } from "../../../../webhooks/definitions/product-variant-back-in-stock"; +import { createWebhookContext } from "../../../../webhooks/webhook-context"; export const config = { api: { @@ -35,51 +33,34 @@ export const handler: NextWebhookApiHandler = async ( return res.status(200).end(); } - const { settings, errors } = await getAlgoliaConfiguration({ authData }); - const client = createGraphQLClient({ - saleorApiUrl: authData.saleorApiUrl, - token: authData.token, - }); - const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise(); - const channels = channelsData?.channels || []; + try { + const { algoliaClient, apiClient } = await createWebhookContext({ authData }); - if (errors?.length || !settings) { - logger.warn("Aborting due to lack of settings"); - logger.debug(errors); + try { + await algoliaClient.updateProductVariant(productVariant); + res.status(200).end(); + return; + } catch (e) { + logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled"); + + const webhooksToggler = new WebhookActivityTogglerService(authData.appId, apiClient); + + logger.trace("Will disable webhooks"); + + await webhooksToggler.disableOwnWebhooks( + context.payload.recipient?.webhooks?.map((w) => w.id), + ); + + logger.trace("Webhooks disabling operation finished"); + + return res.status(500).send("Operation failed, webhooks are disabled"); + } + } catch (e) { return res.status(400).json({ - message: errors[0].message, + message: (e as Error).message, }); } - - const searchProvider = new AlgoliaSearchProvider({ - appId: settings.appId, - apiKey: settings.secretKey, - indexNamePrefix: settings.indexNamePrefix, - channels, - }); - - try { - await searchProvider.updateProductVariant(productVariant); - } catch (e) { - logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled"); - - const webhooksToggler = new WebhookActivityTogglerService( - authData.appId, - createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }), - ); - - logger.trace("Will disable webhooks"); - - await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id)); - - logger.trace("Webhooks disabling operation finished"); - - return res.status(500).send("Operation failed, webhooks are disabled"); - } - - res.status(200).end(); - return; }; export default webhookProductVariantBackInStock.createHandler(handler); diff --git a/apps/search/src/pages/api/webhooks/saleor/product_variant_created.ts b/apps/search/src/pages/api/webhooks/saleor/product_variant_created.ts index dbce796..ec2414d 100644 --- a/apps/search/src/pages/api/webhooks/saleor/product_variant_created.ts +++ b/apps/search/src/pages/api/webhooks/saleor/product_variant_created.ts @@ -1,11 +1,9 @@ import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next"; -import { ChannelsDocument, ProductVariantCreated } from "../../../../../generated/graphql"; -import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider"; -import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration"; -import { createLogger } from "../../../../lib/logger"; +import { ProductVariantCreated } from "../../../../../generated/graphql"; 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 { createWebhookContext } from "../../../../webhooks/webhook-context"; export const config = { api: { @@ -31,50 +29,34 @@ export const handler: NextWebhookApiHandler = async (req, return res.status(200).end(); } - const { settings, errors } = await getAlgoliaConfiguration({ authData }); - const client = createGraphQLClient({ - saleorApiUrl: authData.saleorApiUrl, - token: authData.token, - }); - const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise(); - const channels = channelsData?.channels || []; + try { + const { algoliaClient, apiClient } = await createWebhookContext({ authData }); - if (errors?.length || !settings) { - logger.warn("Aborting due to lack of settings"); - logger.debug(errors); + try { + await algoliaClient.createProductVariant(productVariant); + + res.status(200).end(); + return; + } catch (e) { + logger.info(e, "Algolia createProductVariant failed. Webhooks will be disabled"); + + const webhooksToggler = new WebhookActivityTogglerService(authData.appId, apiClient); + + logger.trace("Will disable webhooks"); + + await webhooksToggler.disableOwnWebhooks( + context.payload.recipient?.webhooks?.map((w) => w.id), + ); + + logger.trace("Webhooks disabling operation finished"); + + return res.status(500).send("Operation failed, webhooks are disabled"); + } + } catch (e) { return res.status(400).json({ - message: errors[0].message, + message: (e as Error).message, }); } - - const searchProvider = new AlgoliaSearchProvider({ - appId: settings.appId, - apiKey: settings.secretKey, - indexNamePrefix: settings.indexNamePrefix, - channels, - }); - - try { - await searchProvider.createProductVariant(productVariant); - } catch (e) { - logger.info(e, "Algolia createProductVariant failed. Webhooks will be disabled"); - - const webhooksToggler = new WebhookActivityTogglerService( - authData.appId, - createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }), - ); - - logger.trace("Will disable webhooks"); - - await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id)); - - logger.trace("Webhooks disabling operation finished"); - - return res.status(500).send("Operation failed, webhooks are disabled"); - } - - res.status(200).end(); - return; }; export default webhookProductVariantCreated.createHandler(handler); diff --git a/apps/search/src/pages/api/webhooks/saleor/product_variant_deleted.ts b/apps/search/src/pages/api/webhooks/saleor/product_variant_deleted.ts index 44c6ce6..3c09f6f 100644 --- a/apps/search/src/pages/api/webhooks/saleor/product_variant_deleted.ts +++ b/apps/search/src/pages/api/webhooks/saleor/product_variant_deleted.ts @@ -1,11 +1,9 @@ import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next"; -import { ChannelsDocument, ProductVariantDeleted } from "../../../../../generated/graphql"; -import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider"; -import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration"; -import { createLogger } from "../../../../lib/logger"; +import { ProductVariantDeleted } from "../../../../../generated/graphql"; 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 { createWebhookContext } from "../../../../webhooks/webhook-context"; export const config = { api: { @@ -31,51 +29,34 @@ export const handler: NextWebhookApiHandler = async (req, return res.status(200).end(); } - const { settings, errors } = await getAlgoliaConfiguration({ authData }); - const client = createGraphQLClient({ - saleorApiUrl: authData.saleorApiUrl, - token: authData.token, - }); - const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise(); - const channels = channelsData?.channels || []; + try { + const { algoliaClient, apiClient } = await createWebhookContext({ authData }); - if (errors?.length || !settings) { - logger.warn("Aborting due to lack of settings"); - logger.debug(errors); + try { + await algoliaClient.deleteProductVariant(productVariant); + res.status(200).end(); + return; + } catch (e) { + logger.info(e, "Algolia deleteProductVariant failed. Webhooks will be disabled"); + + const webhooksToggler = new WebhookActivityTogglerService(authData.appId, apiClient); + + logger.trace("Will disable webhooks"); + + await webhooksToggler.disableOwnWebhooks( + context.payload.recipient?.webhooks?.map((w) => w.id), + ); + + logger.trace("Webhooks disabling operation finished"); + + return res.status(500).send("Operation failed, webhooks are disabled"); + } + } catch (e) { return res.status(400).json({ - message: errors[0].message, + message: (e as Error).message, }); } - - const searchProvider = new AlgoliaSearchProvider({ - appId: settings.appId, - apiKey: settings.secretKey, - indexNamePrefix: settings.indexNamePrefix, - channels, - }); - - try { - await searchProvider.deleteProductVariant(productVariant); - } catch (e) { - logger.info(e, "Algolia deleteProductVariant failed. Webhooks will be disabled"); - - const webhooksToggler = new WebhookActivityTogglerService( - authData.appId, - createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }), - ); - - logger.trace("Will disable webhooks"); - - await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id)); - - logger.trace("Webhooks disabling operation finished"); - - return res.status(500).send("Operation failed, webhooks are disabled"); - } - - res.status(200).end(); - return; }; export default webhookProductVariantDeleted.createHandler(handler); diff --git a/apps/search/src/pages/api/webhooks/saleor/product_variant_out_of_stock.ts b/apps/search/src/pages/api/webhooks/saleor/product_variant_out_of_stock.ts index 3426ee6..8115200 100644 --- a/apps/search/src/pages/api/webhooks/saleor/product_variant_out_of_stock.ts +++ b/apps/search/src/pages/api/webhooks/saleor/product_variant_out_of_stock.ts @@ -1,11 +1,9 @@ import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next"; -import { createGraphQLClient } from "@saleor/apps-shared"; -import { ChannelsDocument, ProductVariantOutOfStock } from "../../../../../generated/graphql"; +import { ProductVariantOutOfStock } from "../../../../../generated/graphql"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; -import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider"; -import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration"; import { createLogger } from "../../../../lib/logger"; import { webhookProductVariantOutOfStock } from "../../../../webhooks/definitions/product-variant-out-of-stock"; +import { createWebhookContext } from "../../../../webhooks/webhook-context"; export const config = { api: { @@ -35,51 +33,34 @@ export const handler: NextWebhookApiHandler = async ( return res.status(200).end(); } - const { settings, errors } = await getAlgoliaConfiguration({ authData }); - const client = createGraphQLClient({ - saleorApiUrl: authData.saleorApiUrl, - token: authData.token, - }); - const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise(); - const channels = channelsData?.channels || []; + try { + const { algoliaClient, apiClient } = await createWebhookContext({ authData }); - if (errors?.length || !settings) { - logger.warn("Aborting due to lack of settings"); - logger.debug(errors); + try { + await algoliaClient.updateProductVariant(productVariant); + res.status(200).end(); + return; + } catch (e) { + logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled"); + + const webhooksToggler = new WebhookActivityTogglerService(authData.appId, apiClient); + + logger.trace("Will disable webhooks"); + + await webhooksToggler.disableOwnWebhooks( + context.payload.recipient?.webhooks?.map((w) => w.id), + ); + + logger.trace("Webhooks disabling operation finished"); + + return res.status(500).send("Operation failed, webhooks are disabled"); + } + } catch (e) { return res.status(400).json({ - message: errors[0].message, + message: (e as Error).message, }); } - - const searchProvider = new AlgoliaSearchProvider({ - appId: settings.appId, - apiKey: settings.secretKey, - indexNamePrefix: settings.indexNamePrefix, - channels, - }); - - try { - await searchProvider.updateProductVariant(productVariant); - } catch (e) { - logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled"); - - const webhooksToggler = new WebhookActivityTogglerService( - authData.appId, - createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }), - ); - - logger.trace("Will disable webhooks"); - - await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id)); - - logger.trace("Webhooks disabling operation finished"); - - return res.status(500).send("Operation failed, webhooks are disabled"); - } - - res.status(200).end(); - return; }; export default webhookProductVariantOutOfStock.createHandler(handler); diff --git a/apps/search/src/pages/api/webhooks/saleor/product_variant_updated.ts b/apps/search/src/pages/api/webhooks/saleor/product_variant_updated.ts index 8ae0c37..be6d781 100644 --- a/apps/search/src/pages/api/webhooks/saleor/product_variant_updated.ts +++ b/apps/search/src/pages/api/webhooks/saleor/product_variant_updated.ts @@ -1,11 +1,9 @@ import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next"; -import { createGraphQLClient } from "@saleor/apps-shared"; -import { ChannelsDocument, ProductVariantUpdated } from "../../../../../generated/graphql"; +import { ProductVariantUpdated } from "../../../../../generated/graphql"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; -import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider"; -import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration"; import { createLogger } from "../../../../lib/logger"; import { webhookProductVariantUpdated } from "../../../../webhooks/definitions/product-variant-updated"; +import { createWebhookContext } from "../../../../webhooks/webhook-context"; export const config = { api: { @@ -31,52 +29,34 @@ export const handler: NextWebhookApiHandler = async (req, return res.status(200).end(); } - const { settings, errors } = await getAlgoliaConfiguration({ authData }); - const client = createGraphQLClient({ - saleorApiUrl: authData.saleorApiUrl, - token: authData.token, - }); - const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise(); - const channels = channelsData?.channels || []; + try { + const { algoliaClient, apiClient } = await createWebhookContext({ authData }); - if (errors?.length || !settings) { - logger.warn("Aborting due to lack of settings"); - logger.debug(errors); + try { + await algoliaClient.updateProductVariant(productVariant); + res.status(200).end(); + return; + } catch (e) { + logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled"); + + const webhooksToggler = new WebhookActivityTogglerService(authData.appId, apiClient); + + logger.trace("Will disable webhooks"); + + await webhooksToggler.disableOwnWebhooks( + context.payload.recipient?.webhooks?.map((w) => w.id), + ); + + logger.trace("Webhooks disabling operation finished"); + + return res.status(500).send("Operation failed, webhooks are disabled"); + } + } catch (e) { return res.status(400).json({ - message: errors[0].message, + message: (e as Error).message, }); } - - const searchProvider = new AlgoliaSearchProvider({ - appId: settings.appId, - apiKey: settings.secretKey, - indexNamePrefix: settings.indexNamePrefix, - channels, - }); - - try { - logger.debug("Updating variant"); - await searchProvider.updateProductVariant(productVariant); - } catch (e) { - logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled"); - - const webhooksToggler = new WebhookActivityTogglerService( - authData.appId, - createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }), - ); - - logger.trace("Will disable webhooks"); - - await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id)); - - logger.trace("Webhooks disabling operation finished"); - - return res.status(500).send("Operation failed, webhooks are disabled"); - } - - res.status(200).end(); - return; }; export default webhookProductVariantUpdated.createHandler(handler); diff --git a/apps/search/src/webhooks/webhook-context.ts b/apps/search/src/webhooks/webhook-context.ts new file mode 100644 index 0000000..48e1825 --- /dev/null +++ b/apps/search/src/webhooks/webhook-context.ts @@ -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, + }; +};