📧 Dynamic webhook management (#646)
* Make channels section expandable based on override setting * Revert "Make channels section expandable based on override setting" This reverts commit e107c5e990b4110156043ed494fb0054bd936654. * Add status component * Remove no longer used component * Remove no longer used component * Removed webhook creation during App installation * Extend tRPC meta to contain webhook sync flag * Add app baseUrl to the context * Webhook management service * Add changeset
This commit is contained in:
parent
ec68ed53a3
commit
82dfc3fa6f
16 changed files with 1042 additions and 159 deletions
7
.changeset/two-dingos-notice.md
Normal file
7
.changeset/two-dingos-notice.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
"saleor-app-emails-and-messages": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Webhooks are no longer created during the app registration. Instead, the app will subscribe events based on it's configuration, after change has been detected.
|
||||||
|
|
||||||
|
This change does not have negative impact on existing app installations - webhooks will be removed during next change of the provider configuration.
|
|
@ -1,39 +0,0 @@
|
||||||
import { Client, gql } from "urql";
|
|
||||||
import { WebhookCreateMutationDocument, WebhookEventTypeEnum } from "../../generated/graphql";
|
|
||||||
import { notifyWebhook } from "../pages/api/webhooks/notify";
|
|
||||||
|
|
||||||
const webhookCreateMutation = gql`
|
|
||||||
mutation webhookCreateMutation($input: WebhookCreateInput!) {
|
|
||||||
webhookCreate(input: $input) {
|
|
||||||
webhook {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
isActive
|
|
||||||
}
|
|
||||||
errors {
|
|
||||||
field
|
|
||||||
message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface RegisterNotifyWebhookArgs {
|
|
||||||
client: Client;
|
|
||||||
baseUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const registerNotifyWebhook = async ({ client, baseUrl }: RegisterNotifyWebhookArgs) => {
|
|
||||||
const manifest = notifyWebhook.getWebhookManifest(baseUrl);
|
|
||||||
|
|
||||||
return await client
|
|
||||||
.mutation(WebhookCreateMutationDocument, {
|
|
||||||
input: {
|
|
||||||
name: manifest.name,
|
|
||||||
targetUrl: manifest.targetUrl,
|
|
||||||
events: [WebhookEventTypeEnum.NotifyUser],
|
|
||||||
isActive: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.toPromise();
|
|
||||||
};
|
|
|
@ -10,18 +10,13 @@ import {
|
||||||
sendgridUpdateEventSchema,
|
sendgridUpdateEventSchema,
|
||||||
sendgridUpdateSenderSchema,
|
sendgridUpdateSenderSchema,
|
||||||
} from "./sendgrid-config-input-schema";
|
} from "./sendgrid-config-input-schema";
|
||||||
import {
|
import { SendgridConfigurationServiceError } from "./sendgrid-configuration.service";
|
||||||
SendgridConfigurationService,
|
|
||||||
SendgridConfigurationServiceError,
|
|
||||||
} from "./sendgrid-configuration.service";
|
|
||||||
import { router } from "../../trpc/trpc-server";
|
import { router } from "../../trpc/trpc-server";
|
||||||
import { protectedClientProcedure } from "../../trpc/protected-client-procedure";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { fetchSenders } from "../sendgrid-api";
|
import { fetchSenders } from "../sendgrid-api";
|
||||||
import { updateChannelsInputSchema } from "../../channels/channel-configuration-schema";
|
import { updateChannelsInputSchema } from "../../channels/channel-configuration-schema";
|
||||||
import { SendgridPrivateMetadataManager } from "./sendgrid-metadata-manager";
|
|
||||||
import { createSettingsManager } from "../../../lib/metadata-manager";
|
|
||||||
import { sendgridDefaultEmptyConfigurations } from "./sendgrid-default-empty-configurations";
|
import { sendgridDefaultEmptyConfigurations } from "./sendgrid-default-empty-configurations";
|
||||||
|
import { protectedWithConfigurationServices } from "../../trpc/protected-client-procedure-with-services";
|
||||||
|
|
||||||
export const throwTrpcErrorFromConfigurationServiceError = (
|
export const throwTrpcErrorFromConfigurationServiceError = (
|
||||||
error: SendgridConfigurationServiceError | unknown
|
error: SendgridConfigurationServiceError | unknown
|
||||||
|
@ -51,32 +46,14 @@ export const throwTrpcErrorFromConfigurationServiceError = (
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
* Allow access only for the dashboard users and attaches the
|
|
||||||
* configuration service to the context
|
|
||||||
*/
|
|
||||||
const protectedWithConfigurationService = protectedClientProcedure.use(({ next, ctx }) =>
|
|
||||||
next({
|
|
||||||
ctx: {
|
|
||||||
...ctx,
|
|
||||||
sendgridConfigurationService: new SendgridConfigurationService({
|
|
||||||
metadataManager: new SendgridPrivateMetadataManager(
|
|
||||||
createSettingsManager(ctx.apiClient, ctx.appId!),
|
|
||||||
ctx.saleorApiUrl
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const sendgridConfigurationRouter = router({
|
export const sendgridConfigurationRouter = router({
|
||||||
fetch: protectedWithConfigurationService.query(async ({ ctx }) => {
|
fetch: protectedWithConfigurationServices.query(async ({ ctx }) => {
|
||||||
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
|
||||||
logger.debug("sendgridConfigurationRouter.fetch called");
|
logger.debug("sendgridConfigurationRouter.fetch called");
|
||||||
return ctx.sendgridConfigurationService.getConfigurationRoot();
|
return ctx.sendgridConfigurationService.getConfigurationRoot();
|
||||||
}),
|
}),
|
||||||
getConfiguration: protectedWithConfigurationService
|
getConfiguration: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
.input(sendgridConfigurationIdInputSchema)
|
.input(sendgridConfigurationIdInputSchema)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
|
@ -89,7 +66,7 @@ export const sendgridConfigurationRouter = router({
|
||||||
throwTrpcErrorFromConfigurationServiceError(e);
|
throwTrpcErrorFromConfigurationServiceError(e);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
getConfigurations: protectedWithConfigurationService
|
getConfigurations: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
.input(sendgridGetConfigurationsInputSchema)
|
.input(sendgridGetConfigurationsInputSchema)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
|
@ -102,8 +79,8 @@ export const sendgridConfigurationRouter = router({
|
||||||
throwTrpcErrorFromConfigurationServiceError(e);
|
throwTrpcErrorFromConfigurationServiceError(e);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
createConfiguration: protectedWithConfigurationService
|
createConfiguration: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
|
||||||
.input(sendgridCreateConfigurationInputSchema)
|
.input(sendgridCreateConfigurationInputSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
@ -116,8 +93,8 @@ export const sendgridConfigurationRouter = router({
|
||||||
|
|
||||||
return await ctx.sendgridConfigurationService.createConfiguration(newConfiguration);
|
return await ctx.sendgridConfigurationService.createConfiguration(newConfiguration);
|
||||||
}),
|
}),
|
||||||
deleteConfiguration: protectedWithConfigurationService
|
deleteConfiguration: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
|
||||||
.input(sendgridConfigurationIdInputSchema)
|
.input(sendgridConfigurationIdInputSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
@ -130,7 +107,7 @@ export const sendgridConfigurationRouter = router({
|
||||||
throwTrpcErrorFromConfigurationServiceError(e);
|
throwTrpcErrorFromConfigurationServiceError(e);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
getEventConfiguration: protectedWithConfigurationService
|
getEventConfiguration: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
.input(sendgridGetEventConfigurationInputSchema)
|
.input(sendgridGetEventConfigurationInputSchema)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
|
@ -147,8 +124,8 @@ export const sendgridConfigurationRouter = router({
|
||||||
throwTrpcErrorFromConfigurationServiceError(e);
|
throwTrpcErrorFromConfigurationServiceError(e);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
updateBasicInformation: protectedWithConfigurationService
|
updateBasicInformation: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
|
||||||
.input(sendgridUpdateBasicInformationSchema)
|
.input(sendgridUpdateBasicInformationSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
@ -161,7 +138,7 @@ export const sendgridConfigurationRouter = router({
|
||||||
throwTrpcErrorFromConfigurationServiceError(e);
|
throwTrpcErrorFromConfigurationServiceError(e);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
updateApiConnection: protectedWithConfigurationService
|
updateApiConnection: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
.input(sendgridUpdateApiConnectionSchema)
|
.input(sendgridUpdateApiConnectionSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
@ -176,7 +153,7 @@ export const sendgridConfigurationRouter = router({
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateSender: protectedWithConfigurationService
|
updateSender: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
.input(sendgridUpdateSenderSchema)
|
.input(sendgridUpdateSenderSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
@ -213,7 +190,7 @@ export const sendgridConfigurationRouter = router({
|
||||||
throwTrpcErrorFromConfigurationServiceError(e);
|
throwTrpcErrorFromConfigurationServiceError(e);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
updateChannels: protectedWithConfigurationService
|
updateChannels: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
.input(updateChannelsInputSchema)
|
.input(updateChannelsInputSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
@ -235,8 +212,8 @@ export const sendgridConfigurationRouter = router({
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateEvent: protectedWithConfigurationService
|
updateEvent: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
|
||||||
.input(sendgridUpdateEventSchema)
|
.input(sendgridUpdateEventSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
@ -256,8 +233,8 @@ export const sendgridConfigurationRouter = router({
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateEventArray: protectedWithConfigurationService
|
updateEventArray: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
|
||||||
.input(sendgridUpdateEventArraySchema)
|
.input(sendgridUpdateEventArraySchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import { createLogger } from "@saleor/apps-shared";
|
import { createLogger } from "@saleor/apps-shared";
|
||||||
import {
|
import { SmtpConfigurationServiceError } from "./smtp-configuration.service";
|
||||||
SmtpConfigurationService,
|
|
||||||
SmtpConfigurationServiceError,
|
|
||||||
} from "./smtp-configuration.service";
|
|
||||||
import { router } from "../../trpc/trpc-server";
|
import { router } from "../../trpc/trpc-server";
|
||||||
import { protectedClientProcedure } from "../../trpc/protected-client-procedure";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { compileMjml } from "../compile-mjml";
|
import { compileMjml } from "../compile-mjml";
|
||||||
import Handlebars from "handlebars";
|
import Handlebars from "handlebars";
|
||||||
|
@ -22,8 +18,7 @@ import {
|
||||||
smtpUpdateSmtpSchema,
|
smtpUpdateSmtpSchema,
|
||||||
} from "./smtp-config-input-schema";
|
} from "./smtp-config-input-schema";
|
||||||
import { updateChannelsInputSchema } from "../../channels/channel-configuration-schema";
|
import { updateChannelsInputSchema } from "../../channels/channel-configuration-schema";
|
||||||
import { SmtpPrivateMetadataManager } from "./smtp-metadata-manager";
|
import { protectedWithConfigurationServices } from "../../trpc/protected-client-procedure-with-services";
|
||||||
import { createSettingsManager } from "../../../lib/metadata-manager";
|
|
||||||
import { smtpDefaultEmptyConfigurations } from "./smtp-default-empty-configurations";
|
import { smtpDefaultEmptyConfigurations } from "./smtp-default-empty-configurations";
|
||||||
|
|
||||||
export const throwTrpcErrorFromConfigurationServiceError = (
|
export const throwTrpcErrorFromConfigurationServiceError = (
|
||||||
|
@ -54,32 +49,14 @@ export const throwTrpcErrorFromConfigurationServiceError = (
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
* Allow access only for the dashboard users and attaches the
|
|
||||||
* configuration service to the context
|
|
||||||
*/
|
|
||||||
const protectedWithConfigurationService = protectedClientProcedure.use(({ next, ctx }) =>
|
|
||||||
next({
|
|
||||||
ctx: {
|
|
||||||
...ctx,
|
|
||||||
smtpConfigurationService: new SmtpConfigurationService({
|
|
||||||
metadataManager: new SmtpPrivateMetadataManager(
|
|
||||||
createSettingsManager(ctx.apiClient, ctx.appId!),
|
|
||||||
ctx.saleorApiUrl
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const smtpConfigurationRouter = router({
|
export const smtpConfigurationRouter = router({
|
||||||
fetch: protectedWithConfigurationService.query(async ({ ctx }) => {
|
fetch: protectedWithConfigurationServices.query(async ({ ctx }) => {
|
||||||
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
|
||||||
logger.debug("smtpConfigurationRouter.fetch called");
|
logger.debug("smtpConfigurationRouter.fetch called");
|
||||||
return ctx.smtpConfigurationService.getConfigurationRoot();
|
return ctx.smtpConfigurationService.getConfigurationRoot();
|
||||||
}),
|
}),
|
||||||
getConfiguration: protectedWithConfigurationService
|
getConfiguration: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
.input(smtpConfigurationIdInputSchema)
|
.input(smtpConfigurationIdInputSchema)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
|
@ -93,7 +70,7 @@ export const smtpConfigurationRouter = router({
|
||||||
throwTrpcErrorFromConfigurationServiceError(e);
|
throwTrpcErrorFromConfigurationServiceError(e);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
getConfigurations: protectedWithConfigurationService
|
getConfigurations: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
.input(smtpGetConfigurationsInputSchema)
|
.input(smtpGetConfigurationsInputSchema)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
|
@ -106,8 +83,8 @@ export const smtpConfigurationRouter = router({
|
||||||
throwTrpcErrorFromConfigurationServiceError(e);
|
throwTrpcErrorFromConfigurationServiceError(e);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
createConfiguration: protectedWithConfigurationService
|
createConfiguration: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
|
||||||
.input(smtpCreateConfigurationInputSchema)
|
.input(smtpCreateConfigurationInputSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
@ -120,8 +97,8 @@ export const smtpConfigurationRouter = router({
|
||||||
|
|
||||||
return await ctx.smtpConfigurationService.createConfiguration(newConfiguration);
|
return await ctx.smtpConfigurationService.createConfiguration(newConfiguration);
|
||||||
}),
|
}),
|
||||||
deleteConfiguration: protectedWithConfigurationService
|
deleteConfiguration: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
|
||||||
.input(smtpConfigurationIdInputSchema)
|
.input(smtpConfigurationIdInputSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
@ -134,7 +111,7 @@ export const smtpConfigurationRouter = router({
|
||||||
throwTrpcErrorFromConfigurationServiceError(e);
|
throwTrpcErrorFromConfigurationServiceError(e);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
getEventConfiguration: protectedWithConfigurationService
|
getEventConfiguration: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
.input(smtpGetEventConfigurationInputSchema)
|
.input(smtpGetEventConfigurationInputSchema)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
|
@ -152,7 +129,7 @@ export const smtpConfigurationRouter = router({
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
renderTemplate: protectedWithConfigurationService
|
renderTemplate: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
@ -196,8 +173,8 @@ export const smtpConfigurationRouter = router({
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateBasicInformation: protectedWithConfigurationService
|
updateBasicInformation: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
|
||||||
.input(smtpUpdateBasicInformationSchema)
|
.input(smtpUpdateBasicInformationSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
@ -211,8 +188,8 @@ export const smtpConfigurationRouter = router({
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateSmtp: protectedWithConfigurationService
|
updateSmtp: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
|
||||||
.input(smtpUpdateSmtpSchema)
|
.input(smtpUpdateSmtpSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
@ -226,7 +203,7 @@ export const smtpConfigurationRouter = router({
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateSender: protectedWithConfigurationService
|
updateSender: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
.input(smtpUpdateSenderSchema)
|
.input(smtpUpdateSenderSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
@ -241,7 +218,7 @@ export const smtpConfigurationRouter = router({
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateChannels: protectedWithConfigurationService
|
updateChannels: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
||||||
.input(updateChannelsInputSchema)
|
.input(updateChannelsInputSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
@ -263,8 +240,8 @@ export const smtpConfigurationRouter = router({
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
updateEvent: protectedWithConfigurationService
|
updateEvent: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
|
||||||
.input(smtpUpdateEventSchema)
|
.input(smtpUpdateEventSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
@ -283,8 +260,8 @@ export const smtpConfigurationRouter = router({
|
||||||
throwTrpcErrorFromConfigurationServiceError(e);
|
throwTrpcErrorFromConfigurationServiceError(e);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
updateEventActiveStatus: protectedWithConfigurationService
|
updateEventActiveStatus: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
|
||||||
.input(smtpUpdateEventActiveStatusInputSchema)
|
.input(smtpUpdateEventActiveStatusInputSchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
@ -302,8 +279,8 @@ export const smtpConfigurationRouter = router({
|
||||||
throwTrpcErrorFromConfigurationServiceError(e);
|
throwTrpcErrorFromConfigurationServiceError(e);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
updateEventArray: protectedWithConfigurationService
|
updateEventArray: protectedWithConfigurationServices
|
||||||
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
|
.meta({ requiredClientPermissions: ["MANAGE_APPS"], updateWebhooks: true })
|
||||||
.input(smtpUpdateEventArraySchema)
|
.input(smtpUpdateEventArraySchema)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
const logger = createLogger({ saleorApiUrl: ctx.saleorApiUrl });
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { createLogger } from "@saleor/apps-shared";
|
||||||
|
import { createSettingsManager } from "../../lib/metadata-manager";
|
||||||
|
import { SendgridConfigurationService } from "../sendgrid/configuration/sendgrid-configuration.service";
|
||||||
|
import { SendgridPrivateMetadataManager } from "../sendgrid/configuration/sendgrid-metadata-manager";
|
||||||
|
import { SmtpConfigurationService } from "../smtp/configuration/smtp-configuration.service";
|
||||||
|
import { SmtpPrivateMetadataManager } from "../smtp/configuration/smtp-metadata-manager";
|
||||||
|
import { syncWebhookStatus } from "../webhook-management/sync-webhook-status";
|
||||||
|
import { protectedClientProcedure } from "./protected-client-procedure";
|
||||||
|
import { WebhookManagementService } from "../webhook-management/webhook-management-service";
|
||||||
|
|
||||||
|
const logger = createLogger({ name: "protectedWithConfigurationServices middleware" });
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Allow access only for the dashboard users and attaches the
|
||||||
|
* configuration service to the context.
|
||||||
|
* The services do not fetch data from the API unless they are used.
|
||||||
|
* If meta key updateWebhooks is set to true, additional calls to the API will be made
|
||||||
|
* to create or remove webhooks.
|
||||||
|
*/
|
||||||
|
export const protectedWithConfigurationServices = protectedClientProcedure.use(
|
||||||
|
async ({ next, ctx, meta }) => {
|
||||||
|
const smtpConfigurationService = new SmtpConfigurationService({
|
||||||
|
metadataManager: new SmtpPrivateMetadataManager(
|
||||||
|
createSettingsManager(ctx.apiClient, ctx.appId!),
|
||||||
|
ctx.saleorApiUrl
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendgridConfigurationService = new SendgridConfigurationService({
|
||||||
|
metadataManager: new SendgridPrivateMetadataManager(
|
||||||
|
createSettingsManager(ctx.apiClient, ctx.appId!),
|
||||||
|
ctx.saleorApiUrl
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await next({
|
||||||
|
ctx: {
|
||||||
|
smtpConfigurationService,
|
||||||
|
sendgridConfigurationService,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (meta?.updateWebhooks) {
|
||||||
|
logger.debug("Updating webhooks");
|
||||||
|
|
||||||
|
const webhookManagementService = new WebhookManagementService(ctx.baseUrl, ctx.apiClient);
|
||||||
|
|
||||||
|
await syncWebhookStatus({
|
||||||
|
sendgridConfigurationService,
|
||||||
|
smtpConfigurationService,
|
||||||
|
webhookManagementService,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
);
|
|
@ -1,13 +1,17 @@
|
||||||
import * as trpcNext from "@trpc/server/adapters/next";
|
import * as trpcNext from "@trpc/server/adapters/next";
|
||||||
import { SALEOR_AUTHORIZATION_BEARER_HEADER, SALEOR_API_URL_HEADER } from "@saleor/app-sdk/const";
|
import { SALEOR_AUTHORIZATION_BEARER_HEADER, SALEOR_API_URL_HEADER } from "@saleor/app-sdk/const";
|
||||||
import { inferAsyncReturnType } from "@trpc/server";
|
import { inferAsyncReturnType } from "@trpc/server";
|
||||||
|
import { getBaseUrl } from "../../lib/get-base-url";
|
||||||
|
|
||||||
export const createTrpcContext = async ({ res, req }: trpcNext.CreateNextContextOptions) => {
|
export const createTrpcContext = async ({ res, req }: trpcNext.CreateNextContextOptions) => {
|
||||||
|
const baseUrl = getBaseUrl(req.headers);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token: req.headers[SALEOR_AUTHORIZATION_BEARER_HEADER] as string | undefined,
|
token: req.headers[SALEOR_AUTHORIZATION_BEARER_HEADER] as string | undefined,
|
||||||
saleorApiUrl: req.headers[SALEOR_API_URL_HEADER] as string | undefined,
|
saleorApiUrl: req.headers[SALEOR_API_URL_HEADER] as string | undefined,
|
||||||
appId: undefined as undefined | string,
|
appId: undefined as undefined | string,
|
||||||
ssr: undefined as undefined | boolean,
|
ssr: undefined as undefined | boolean,
|
||||||
|
baseUrl,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { ZodError } from "zod";
|
||||||
|
|
||||||
interface Meta {
|
interface Meta {
|
||||||
requiredClientPermissions?: Permission[];
|
requiredClientPermissions?: Permission[];
|
||||||
|
updateWebhooks?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = initTRPC
|
const t = initTRPC
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { Client, gql } from "urql";
|
||||||
|
import {
|
||||||
|
AppWebhooksDocument,
|
||||||
|
CreateAppWebhookDocument,
|
||||||
|
CreateAppWebhookMutationVariables,
|
||||||
|
DeleteAppWebhookDocument,
|
||||||
|
} from "../../../generated/graphql";
|
||||||
|
|
||||||
|
gql`
|
||||||
|
fragment WebhookDetails on Webhook {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
asyncEvents {
|
||||||
|
name
|
||||||
|
eventType
|
||||||
|
}
|
||||||
|
isActive
|
||||||
|
}
|
||||||
|
|
||||||
|
query AppWebhooks {
|
||||||
|
app {
|
||||||
|
webhooks {
|
||||||
|
...WebhookDetails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation CreateAppWebhook($input: WebhookCreateInput!) {
|
||||||
|
webhookCreate(input: $input) {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
message
|
||||||
|
}
|
||||||
|
webhook {
|
||||||
|
...WebhookDetails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation DeleteAppWebhook($id: ID!) {
|
||||||
|
webhookDelete(id: $id) {
|
||||||
|
errors {
|
||||||
|
field
|
||||||
|
message
|
||||||
|
}
|
||||||
|
webhook {
|
||||||
|
...WebhookDetails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const fetchAppWebhooks = ({ client }: { client: Client }) =>
|
||||||
|
client
|
||||||
|
.query(AppWebhooksDocument, {})
|
||||||
|
.toPromise()
|
||||||
|
.then((response) => {
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(response.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const appData = response.data?.app;
|
||||||
|
|
||||||
|
if (!appData) {
|
||||||
|
throw new Error(
|
||||||
|
"App data not found in the response. The token can be invalid or the app has been uninstalled."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return appData.webhooks || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createAppWebhook = ({
|
||||||
|
client,
|
||||||
|
variables,
|
||||||
|
}: {
|
||||||
|
client: Client;
|
||||||
|
variables: CreateAppWebhookMutationVariables["input"];
|
||||||
|
}) =>
|
||||||
|
client
|
||||||
|
.mutation(CreateAppWebhookDocument, { input: variables })
|
||||||
|
.toPromise()
|
||||||
|
.then((response) => {
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(response.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("create wh", response.data?.webhookCreate?.errors);
|
||||||
|
const webhookCreateData = response.data?.webhookCreate?.webhook;
|
||||||
|
|
||||||
|
if (!webhookCreateData) {
|
||||||
|
throw new Error("Webhook Creation did not return any data nor error.");
|
||||||
|
}
|
||||||
|
return webhookCreateData;
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteAppWebhook = ({ client, id }: { client: Client; id: string }) =>
|
||||||
|
client
|
||||||
|
.mutation(DeleteAppWebhookDocument, { id })
|
||||||
|
.toPromise()
|
||||||
|
.then((response) => {
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(response.error.message);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
});
|
|
@ -0,0 +1,287 @@
|
||||||
|
import { expect, describe, it } from "vitest";
|
||||||
|
import { SmtpConfiguration } from "../smtp/configuration/smtp-config-schema";
|
||||||
|
import { getWebhookStatusesFromConfigurations } from "./get-webhook-statuses-from-configurations";
|
||||||
|
import { SendgridConfiguration } from "../sendgrid/configuration/sendgrid-config-schema";
|
||||||
|
|
||||||
|
export const nonActiveSmtpConfiguration: SmtpConfiguration = {
|
||||||
|
id: "1685343953413npk9p",
|
||||||
|
active: false,
|
||||||
|
name: "Best name",
|
||||||
|
smtpHost: "smtpHost",
|
||||||
|
smtpPort: "1337",
|
||||||
|
encryption: "NONE",
|
||||||
|
channels: {
|
||||||
|
override: false,
|
||||||
|
channels: [],
|
||||||
|
mode: "restrict",
|
||||||
|
},
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ORDER_CREATED",
|
||||||
|
template: "template",
|
||||||
|
subject: "Order {{ order.number }} has been created!!",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ORDER_FULFILLED",
|
||||||
|
template: "template",
|
||||||
|
subject: "Order {{ order.number }} has been fulfilled",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ORDER_CONFIRMED",
|
||||||
|
template: "template",
|
||||||
|
subject: "Order {{ order.number }} has been confirmed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ORDER_CANCELLED",
|
||||||
|
template: "template",
|
||||||
|
subject: "Order {{ order.number }} has been cancelled",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ORDER_FULLY_PAID",
|
||||||
|
template: "template",
|
||||||
|
subject: "Order {{ order.number }} has been fully paid",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "INVOICE_SENT",
|
||||||
|
template: "template",
|
||||||
|
subject: "New invoice has been created",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ACCOUNT_CONFIRMATION",
|
||||||
|
template: "template",
|
||||||
|
subject: "Account activation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ACCOUNT_PASSWORD_RESET",
|
||||||
|
template: "template",
|
||||||
|
subject: "Password reset request",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ACCOUNT_CHANGE_EMAIL_REQUEST",
|
||||||
|
template: "template",
|
||||||
|
subject: "Email change request",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ACCOUNT_CHANGE_EMAIL_CONFIRM",
|
||||||
|
template: "template",
|
||||||
|
subject: "Email change confirmation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ACCOUNT_DELETE",
|
||||||
|
template: "template",
|
||||||
|
subject: "Account deletion",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
smtpUser: "John",
|
||||||
|
smtpPassword: "securepassword",
|
||||||
|
senderEmail: "no-reply@example.com",
|
||||||
|
senderName: "Sender Name",
|
||||||
|
};
|
||||||
|
|
||||||
|
const nonActiveSendgridConfiguration: SendgridConfiguration = {
|
||||||
|
id: "1685343953413npk9p",
|
||||||
|
active: false,
|
||||||
|
name: "Best name",
|
||||||
|
sandboxMode: false,
|
||||||
|
apiKey: "SG.123",
|
||||||
|
channels: {
|
||||||
|
override: false,
|
||||||
|
channels: [],
|
||||||
|
mode: "restrict",
|
||||||
|
},
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ORDER_CREATED",
|
||||||
|
template: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ORDER_FULFILLED",
|
||||||
|
template: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ORDER_CONFIRMED",
|
||||||
|
template: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ORDER_CANCELLED",
|
||||||
|
template: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ORDER_FULLY_PAID",
|
||||||
|
template: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "INVOICE_SENT",
|
||||||
|
template: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ACCOUNT_CONFIRMATION",
|
||||||
|
template: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ACCOUNT_PASSWORD_RESET",
|
||||||
|
template: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ACCOUNT_CHANGE_EMAIL_REQUEST",
|
||||||
|
template: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ACCOUNT_CHANGE_EMAIL_CONFIRM",
|
||||||
|
template: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ACCOUNT_DELETE",
|
||||||
|
template: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sender: "1",
|
||||||
|
senderEmail: "no-reply@example.com",
|
||||||
|
senderName: "Sender Name",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("getWebhookStatusesFromConfigurations", function () {
|
||||||
|
it("Statuses should be set to false, when no configurations passed", async () => {
|
||||||
|
expect(
|
||||||
|
getWebhookStatusesFromConfigurations({
|
||||||
|
smtpConfigurations: [],
|
||||||
|
sendgridConfigurations: [],
|
||||||
|
})
|
||||||
|
).toStrictEqual({
|
||||||
|
invoiceSentWebhook: false,
|
||||||
|
notifyWebhook: false,
|
||||||
|
orderCancelledWebhook: false,
|
||||||
|
orderConfirmedWebhook: false,
|
||||||
|
orderFulfilledWebhook: false,
|
||||||
|
orderCreatedWebhook: false,
|
||||||
|
orderFullyPaidWebhook: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Statuses should be set to false, when no active configurations passed", async () => {
|
||||||
|
expect(
|
||||||
|
getWebhookStatusesFromConfigurations({
|
||||||
|
smtpConfigurations: [nonActiveSmtpConfiguration],
|
||||||
|
sendgridConfigurations: [nonActiveSendgridConfiguration],
|
||||||
|
})
|
||||||
|
).toStrictEqual({
|
||||||
|
invoiceSentWebhook: false,
|
||||||
|
notifyWebhook: false,
|
||||||
|
orderCancelledWebhook: false,
|
||||||
|
orderConfirmedWebhook: false,
|
||||||
|
orderFulfilledWebhook: false,
|
||||||
|
orderCreatedWebhook: false,
|
||||||
|
orderFullyPaidWebhook: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Statuses should be set to false, when configuration is not active even if events were activated", async () => {
|
||||||
|
const smtpConfiguration = {
|
||||||
|
...nonActiveSmtpConfiguration,
|
||||||
|
events: nonActiveSmtpConfiguration.events.map((event) => ({ ...event, active: true })),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getWebhookStatusesFromConfigurations({
|
||||||
|
smtpConfigurations: [smtpConfiguration],
|
||||||
|
sendgridConfigurations: [nonActiveSendgridConfiguration],
|
||||||
|
})
|
||||||
|
).toStrictEqual({
|
||||||
|
invoiceSentWebhook: false,
|
||||||
|
notifyWebhook: false,
|
||||||
|
orderCancelledWebhook: false,
|
||||||
|
orderConfirmedWebhook: false,
|
||||||
|
orderFulfilledWebhook: false,
|
||||||
|
orderCreatedWebhook: false,
|
||||||
|
orderFullyPaidWebhook: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Status of the event should be set to true, when at least one active configuration has activated it", async () => {
|
||||||
|
const smtpConfiguration: SmtpConfiguration = {
|
||||||
|
...nonActiveSmtpConfiguration,
|
||||||
|
active: true,
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
eventType: "INVOICE_SENT",
|
||||||
|
subject: "",
|
||||||
|
template: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getWebhookStatusesFromConfigurations({
|
||||||
|
smtpConfigurations: [nonActiveSmtpConfiguration, smtpConfiguration],
|
||||||
|
sendgridConfigurations: [nonActiveSendgridConfiguration],
|
||||||
|
})
|
||||||
|
).toStrictEqual({
|
||||||
|
invoiceSentWebhook: true,
|
||||||
|
notifyWebhook: false,
|
||||||
|
orderCancelledWebhook: false,
|
||||||
|
orderConfirmedWebhook: false,
|
||||||
|
orderFulfilledWebhook: false,
|
||||||
|
orderCreatedWebhook: false,
|
||||||
|
orderFullyPaidWebhook: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Status of the NOTIFY webhooks should be set to true, when at least one active configuration has activated one of its related events", async () => {
|
||||||
|
const smtpConfiguration: SmtpConfiguration = {
|
||||||
|
...nonActiveSmtpConfiguration,
|
||||||
|
active: true,
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
eventType: "ACCOUNT_CHANGE_EMAIL_CONFIRM",
|
||||||
|
subject: "",
|
||||||
|
template: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
active: true,
|
||||||
|
eventType: "ACCOUNT_CHANGE_EMAIL_REQUEST",
|
||||||
|
subject: "",
|
||||||
|
template: "",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
getWebhookStatusesFromConfigurations({
|
||||||
|
smtpConfigurations: [nonActiveSmtpConfiguration, smtpConfiguration],
|
||||||
|
sendgridConfigurations: [nonActiveSendgridConfiguration],
|
||||||
|
})
|
||||||
|
).toStrictEqual({
|
||||||
|
invoiceSentWebhook: false,
|
||||||
|
notifyWebhook: true,
|
||||||
|
orderCancelledWebhook: false,
|
||||||
|
orderConfirmedWebhook: false,
|
||||||
|
orderFulfilledWebhook: false,
|
||||||
|
orderCreatedWebhook: false,
|
||||||
|
orderFullyPaidWebhook: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { SendgridConfiguration } from "../sendgrid/configuration/sendgrid-config-schema";
|
||||||
|
import { SmtpConfiguration } from "../smtp/configuration/smtp-config-schema";
|
||||||
|
import { AppWebhook, eventToWebhookMapping } from "./webhook-management-service";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns dictionary of webhook statuses based on passed configurations.
|
||||||
|
* Webhook is marked as active (true) if at least one event related event is marked as active.
|
||||||
|
*/
|
||||||
|
export const getWebhookStatusesFromConfigurations = ({
|
||||||
|
smtpConfigurations,
|
||||||
|
sendgridConfigurations,
|
||||||
|
}: {
|
||||||
|
smtpConfigurations: SmtpConfiguration[];
|
||||||
|
sendgridConfigurations: SendgridConfiguration[];
|
||||||
|
}) => {
|
||||||
|
const statuses: Record<AppWebhook, boolean> = {
|
||||||
|
invoiceSentWebhook: false,
|
||||||
|
notifyWebhook: false,
|
||||||
|
orderCancelledWebhook: false,
|
||||||
|
orderConfirmedWebhook: false,
|
||||||
|
orderFulfilledWebhook: false,
|
||||||
|
orderCreatedWebhook: false,
|
||||||
|
orderFullyPaidWebhook: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
smtpConfigurations.forEach(async (config) => {
|
||||||
|
if (!config.active) {
|
||||||
|
// Configuration has to be active to enable webhook
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config.events.forEach(async (event) => {
|
||||||
|
if (event.active) {
|
||||||
|
/*
|
||||||
|
* Mapping is mandatory since multiple events can be mapped to one webhook,
|
||||||
|
* as in case of NOTIFY
|
||||||
|
*/
|
||||||
|
statuses[eventToWebhookMapping[event.eventType]] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
sendgridConfigurations.forEach(async (config) => {
|
||||||
|
if (!config.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
config.events.forEach(async (event) => {
|
||||||
|
if (event.active) {
|
||||||
|
statuses[eventToWebhookMapping[event.eventType]] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return statuses;
|
||||||
|
};
|
|
@ -0,0 +1,159 @@
|
||||||
|
import { vi, expect, describe, it, afterEach } from "vitest";
|
||||||
|
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
||||||
|
import { SmtpPrivateMetadataManager } from "../smtp/configuration/smtp-metadata-manager";
|
||||||
|
import { SmtpConfigurationService } from "../smtp/configuration/smtp-configuration.service";
|
||||||
|
import { syncWebhookStatus } from "./sync-webhook-status";
|
||||||
|
import { SendgridPrivateMetadataManager } from "../sendgrid/configuration/sendgrid-metadata-manager";
|
||||||
|
import { SendgridConfigurationService } from "../sendgrid/configuration/sendgrid-configuration.service";
|
||||||
|
import { WebhookManagementService } from "./webhook-management-service";
|
||||||
|
import { Client } from "urql";
|
||||||
|
import * as statusesExports from "./get-webhook-statuses-from-configurations";
|
||||||
|
|
||||||
|
const mockSaleorApiUrl = "https://demo.saleor.io/graphql/";
|
||||||
|
|
||||||
|
describe("syncWebhookStatus", function () {
|
||||||
|
const createMockedClient = () => ({} as Client);
|
||||||
|
|
||||||
|
const webhookManagementService = new WebhookManagementService(
|
||||||
|
mockSaleorApiUrl,
|
||||||
|
createMockedClient()
|
||||||
|
);
|
||||||
|
|
||||||
|
const createWebhookMock = vi
|
||||||
|
.spyOn(webhookManagementService, "createWebhook")
|
||||||
|
.mockImplementation((_) => Promise.resolve());
|
||||||
|
|
||||||
|
const deleteWebhookMock = vi
|
||||||
|
.spyOn(webhookManagementService, "deleteWebhook")
|
||||||
|
.mockImplementation((_) => Promise.resolve());
|
||||||
|
|
||||||
|
const smtpConfigurator = new SmtpPrivateMetadataManager(
|
||||||
|
null as unknown as SettingsManager,
|
||||||
|
mockSaleorApiUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
const smtpConfigurationService = new SmtpConfigurationService({
|
||||||
|
metadataManager: smtpConfigurator,
|
||||||
|
initialData: {
|
||||||
|
configurations: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendgridConfigurator = new SendgridPrivateMetadataManager(
|
||||||
|
null as unknown as SettingsManager,
|
||||||
|
mockSaleorApiUrl
|
||||||
|
);
|
||||||
|
|
||||||
|
const sendgridConfigurationService = new SendgridConfigurationService({
|
||||||
|
metadataManager: sendgridConfigurator,
|
||||||
|
initialData: {
|
||||||
|
configurations: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("No webhook should be created or deleted, when both API and configurations don't use any", async () => {
|
||||||
|
vi.spyOn(statusesExports, "getWebhookStatusesFromConfigurations").mockReturnValue({
|
||||||
|
invoiceSentWebhook: false,
|
||||||
|
notifyWebhook: false,
|
||||||
|
orderCancelledWebhook: false,
|
||||||
|
orderConfirmedWebhook: false,
|
||||||
|
orderFulfilledWebhook: false,
|
||||||
|
orderCreatedWebhook: false,
|
||||||
|
orderFullyPaidWebhook: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getWebhooksStatusMock = vi
|
||||||
|
.spyOn(webhookManagementService, "getWebhooksStatus")
|
||||||
|
.mockResolvedValue({
|
||||||
|
invoiceSentWebhook: false,
|
||||||
|
notifyWebhook: false,
|
||||||
|
orderCancelledWebhook: false,
|
||||||
|
orderConfirmedWebhook: false,
|
||||||
|
orderFulfilledWebhook: false,
|
||||||
|
orderCreatedWebhook: false,
|
||||||
|
orderFullyPaidWebhook: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await syncWebhookStatus({
|
||||||
|
smtpConfigurationService,
|
||||||
|
sendgridConfigurationService,
|
||||||
|
webhookManagementService,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getWebhooksStatusMock).toHaveBeenCalled();
|
||||||
|
expect(deleteWebhookMock).not.toHaveBeenCalled();
|
||||||
|
expect(createWebhookMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Webhooks should be deleted from API, when configurations no longer use them", async () => {
|
||||||
|
vi.spyOn(statusesExports, "getWebhookStatusesFromConfigurations").mockReturnValue({
|
||||||
|
invoiceSentWebhook: false,
|
||||||
|
notifyWebhook: false,
|
||||||
|
orderCancelledWebhook: false,
|
||||||
|
orderConfirmedWebhook: false,
|
||||||
|
orderFulfilledWebhook: false,
|
||||||
|
orderCreatedWebhook: false,
|
||||||
|
orderFullyPaidWebhook: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getWebhooksStatusMock = vi
|
||||||
|
.spyOn(webhookManagementService, "getWebhooksStatus")
|
||||||
|
.mockResolvedValue({
|
||||||
|
invoiceSentWebhook: true,
|
||||||
|
notifyWebhook: true,
|
||||||
|
orderCancelledWebhook: false,
|
||||||
|
orderConfirmedWebhook: false,
|
||||||
|
orderFulfilledWebhook: false,
|
||||||
|
orderCreatedWebhook: false,
|
||||||
|
orderFullyPaidWebhook: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await syncWebhookStatus({
|
||||||
|
smtpConfigurationService,
|
||||||
|
sendgridConfigurationService,
|
||||||
|
webhookManagementService,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getWebhooksStatusMock).toHaveBeenCalled();
|
||||||
|
expect(createWebhookMock).not.toHaveBeenCalled();
|
||||||
|
expect(deleteWebhookMock).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Webhooks should be created using API, when new configurations use them", async () => {
|
||||||
|
vi.spyOn(statusesExports, "getWebhookStatusesFromConfigurations").mockReturnValue({
|
||||||
|
invoiceSentWebhook: true,
|
||||||
|
notifyWebhook: true,
|
||||||
|
orderCancelledWebhook: false,
|
||||||
|
orderConfirmedWebhook: false,
|
||||||
|
orderFulfilledWebhook: false,
|
||||||
|
orderCreatedWebhook: false,
|
||||||
|
orderFullyPaidWebhook: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getWebhooksStatusMock = vi
|
||||||
|
.spyOn(webhookManagementService, "getWebhooksStatus")
|
||||||
|
.mockResolvedValue({
|
||||||
|
invoiceSentWebhook: false,
|
||||||
|
notifyWebhook: false,
|
||||||
|
orderCancelledWebhook: false,
|
||||||
|
orderConfirmedWebhook: false,
|
||||||
|
orderFulfilledWebhook: false,
|
||||||
|
orderCreatedWebhook: false,
|
||||||
|
orderFullyPaidWebhook: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await syncWebhookStatus({
|
||||||
|
smtpConfigurationService,
|
||||||
|
sendgridConfigurationService,
|
||||||
|
webhookManagementService,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getWebhooksStatusMock).toHaveBeenCalled();
|
||||||
|
expect(createWebhookMock).toHaveBeenCalledTimes(2);
|
||||||
|
expect(deleteWebhookMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { SmtpConfigurationService } from "../smtp/configuration/smtp-configuration.service";
|
||||||
|
import { SendgridConfigurationService } from "../sendgrid/configuration/sendgrid-configuration.service";
|
||||||
|
import { WebhookManagementService, AppWebhook } from "./webhook-management-service";
|
||||||
|
import { createLogger } from "@saleor/apps-shared";
|
||||||
|
import { getWebhookStatusesFromConfigurations } from "./get-webhook-statuses-from-configurations";
|
||||||
|
|
||||||
|
const logger = createLogger({ name: "SyncWebhooksStatus" });
|
||||||
|
|
||||||
|
interface SyncWebhooksStatusArgs {
|
||||||
|
smtpConfigurationService: SmtpConfigurationService;
|
||||||
|
sendgridConfigurationService: SendgridConfigurationService;
|
||||||
|
webhookManagementService: WebhookManagementService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks active events in configurations and updates webhooks in the API if needed.
|
||||||
|
*/
|
||||||
|
export const syncWebhookStatus = async ({
|
||||||
|
smtpConfigurationService,
|
||||||
|
sendgridConfigurationService,
|
||||||
|
webhookManagementService,
|
||||||
|
}: SyncWebhooksStatusArgs) => {
|
||||||
|
logger.debug("Pulling current webhook status from the API");
|
||||||
|
const oldStatuses = await webhookManagementService.getWebhooksStatus();
|
||||||
|
|
||||||
|
logger.debug("Generate expected webhook status based on current configurations");
|
||||||
|
|
||||||
|
// API requests can be triggered if not cached yet
|
||||||
|
const [activeSmtpConfigurations, activeSendgridConfigurations] = await Promise.all([
|
||||||
|
smtpConfigurationService.getConfigurations(),
|
||||||
|
sendgridConfigurationService.getConfigurations(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const newStatuses = getWebhookStatusesFromConfigurations({
|
||||||
|
smtpConfigurations: activeSmtpConfigurations,
|
||||||
|
sendgridConfigurations: activeSendgridConfigurations,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug("Update webhooks in the API if needed");
|
||||||
|
const apiMutationPromises = Object.keys(newStatuses).map(async (key) => {
|
||||||
|
const webhook = key as AppWebhook;
|
||||||
|
|
||||||
|
if (newStatuses[webhook] === oldStatuses[webhook]) {
|
||||||
|
// Webhook status is already up to date
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newStatuses[webhook]) {
|
||||||
|
logger.debug(`Creating webhook ${webhook}`);
|
||||||
|
return webhookManagementService.createWebhook({ webhook });
|
||||||
|
} else {
|
||||||
|
logger.debug(`Deleting webhook ${webhook}`);
|
||||||
|
return webhookManagementService.deleteWebhook({ webhook });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(apiMutationPromises);
|
||||||
|
|
||||||
|
logger.debug("Webhooks status synchronized");
|
||||||
|
};
|
|
@ -0,0 +1,153 @@
|
||||||
|
import { vi, expect, describe, it, afterEach } from "vitest";
|
||||||
|
import { WebhookManagementService } from "./webhook-management-service";
|
||||||
|
import { Client } from "urql";
|
||||||
|
import * as operationExports from "./api-operations";
|
||||||
|
import { WebhookEventTypeAsyncEnum } from "../../../generated/graphql";
|
||||||
|
import { invoiceSentWebhook } from "../../pages/api/webhooks/invoice-sent";
|
||||||
|
import { orderCancelledWebhook } from "../../pages/api/webhooks/order-cancelled";
|
||||||
|
|
||||||
|
const mockSaleorApiUrl = "https://demo.saleor.io/graphql/";
|
||||||
|
|
||||||
|
describe("WebhookManagementService", function () {
|
||||||
|
const mockedClient = {} as Client;
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("API should be called, when getWebhooks is used", async () => {
|
||||||
|
const webhookManagementService = new WebhookManagementService(
|
||||||
|
"https://example.com",
|
||||||
|
mockedClient
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchAppWebhooksMock = vi.spyOn(operationExports, "fetchAppWebhooks").mockResolvedValue([
|
||||||
|
{
|
||||||
|
asyncEvents: [{ eventType: WebhookEventTypeAsyncEnum.InvoiceSent, name: "Invoice sent" }],
|
||||||
|
id: "1",
|
||||||
|
isActive: true,
|
||||||
|
name: invoiceSentWebhook.name,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const webhookData = await webhookManagementService.getWebhooks();
|
||||||
|
|
||||||
|
expect(webhookData).toStrictEqual([
|
||||||
|
{
|
||||||
|
asyncEvents: [{ eventType: WebhookEventTypeAsyncEnum.InvoiceSent, name: "Invoice sent" }],
|
||||||
|
id: "1",
|
||||||
|
isActive: true,
|
||||||
|
name: invoiceSentWebhook.name,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(fetchAppWebhooksMock).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Webhook statuses should be active, when whists in API and active", async () => {
|
||||||
|
const webhookManagementService = new WebhookManagementService(
|
||||||
|
"https://example.com",
|
||||||
|
mockedClient
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchAppWebhooksMock = vi.spyOn(operationExports, "fetchAppWebhooks").mockResolvedValue([
|
||||||
|
{
|
||||||
|
asyncEvents: [{ eventType: WebhookEventTypeAsyncEnum.InvoiceSent, name: "Invoice sent" }],
|
||||||
|
id: "1",
|
||||||
|
isActive: true,
|
||||||
|
name: invoiceSentWebhook.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
asyncEvents: [
|
||||||
|
{ eventType: WebhookEventTypeAsyncEnum.OrderCancelled, name: "Order cancelled" },
|
||||||
|
],
|
||||||
|
id: "2",
|
||||||
|
isActive: false,
|
||||||
|
name: orderCancelledWebhook.name,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const statuses = await webhookManagementService.getWebhooksStatus();
|
||||||
|
|
||||||
|
expect(statuses).toStrictEqual({
|
||||||
|
invoiceSentWebhook: true,
|
||||||
|
notifyWebhook: false,
|
||||||
|
orderCancelledWebhook: false,
|
||||||
|
orderConfirmedWebhook: false,
|
||||||
|
orderCreatedWebhook: false,
|
||||||
|
orderFulfilledWebhook: false,
|
||||||
|
orderFullyPaidWebhook: false,
|
||||||
|
});
|
||||||
|
expect(fetchAppWebhooksMock).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Webhook should be created using the API, when requested", async () => {
|
||||||
|
const webhookManagementService = new WebhookManagementService(
|
||||||
|
"https://example.com",
|
||||||
|
mockedClient
|
||||||
|
);
|
||||||
|
|
||||||
|
const createAppWebhookMock = vi.spyOn(operationExports, "createAppWebhook").mockResolvedValue({
|
||||||
|
id: "1",
|
||||||
|
isActive: true,
|
||||||
|
name: invoiceSentWebhook.name,
|
||||||
|
asyncEvents: [{ eventType: WebhookEventTypeAsyncEnum.InvoiceSent, name: "Invoice sent" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await webhookManagementService.createWebhook({
|
||||||
|
webhook: "invoiceSentWebhook",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createAppWebhookMock).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
// Values are taken from webhook definition
|
||||||
|
expect(createAppWebhookMock).toBeCalledWith({
|
||||||
|
client: mockedClient,
|
||||||
|
variables: {
|
||||||
|
asyncEvents: ["INVOICE_SENT"],
|
||||||
|
isActive: true,
|
||||||
|
name: invoiceSentWebhook.name,
|
||||||
|
targetUrl: "https://example.com/api/webhooks/invoice-sent",
|
||||||
|
query:
|
||||||
|
"subscription InvoiceSent { event { ...InvoiceSentWebhookPayload }}fragment InvoiceSentWebhookPayload on InvoiceSent { invoice { id message externalUrl url order { id } } order { ...OrderDetails }}fragment OrderDetails on Order { id number userEmail channel { slug } user { email firstName lastName } billingAddress { streetAddress1 city postalCode country { country } } shippingAddress { streetAddress1 city postalCode country { country } } lines { id productName variantName quantity thumbnail { url alt } unitPrice { gross { currency amount } } totalPrice { gross { currency amount } } } subtotal { gross { amount currency } } shippingPrice { gross { amount currency } } total { gross { amount currency } }}",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Webhook should be deleted using the API, when requested", async () => {
|
||||||
|
const webhookManagementService = new WebhookManagementService(
|
||||||
|
"https://example.com",
|
||||||
|
mockedClient
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.spyOn(operationExports, "fetchAppWebhooks").mockResolvedValue([
|
||||||
|
{
|
||||||
|
asyncEvents: [{ eventType: WebhookEventTypeAsyncEnum.InvoiceSent, name: "Invoice sent" }],
|
||||||
|
id: "1",
|
||||||
|
isActive: true,
|
||||||
|
name: invoiceSentWebhook.name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
asyncEvents: [
|
||||||
|
{ eventType: WebhookEventTypeAsyncEnum.OrderCancelled, name: "Order cancelled" },
|
||||||
|
],
|
||||||
|
id: "2",
|
||||||
|
isActive: false,
|
||||||
|
name: orderCancelledWebhook.name,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const deleteAppWebhookMock = vi.spyOn(operationExports, "deleteAppWebhook").mockResolvedValue();
|
||||||
|
|
||||||
|
await webhookManagementService.deleteWebhook({
|
||||||
|
webhook: "invoiceSentWebhook",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(deleteAppWebhookMock).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
// Values are taken from webhook definition
|
||||||
|
expect(deleteAppWebhookMock).toBeCalledWith({
|
||||||
|
client: mockedClient,
|
||||||
|
id: "1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { invoiceSentWebhook } from "../../pages/api/webhooks/invoice-sent";
|
||||||
|
import { orderCancelledWebhook } from "../../pages/api/webhooks/order-cancelled";
|
||||||
|
import { orderConfirmedWebhook } from "../../pages/api/webhooks/order-confirmed";
|
||||||
|
import { orderCreatedWebhook } from "../../pages/api/webhooks/order-created";
|
||||||
|
import { orderFulfilledWebhook } from "../../pages/api/webhooks/order-fulfilled";
|
||||||
|
import { orderFullyPaidWebhook } from "../../pages/api/webhooks/order-fully-paid";
|
||||||
|
import { Client } from "urql";
|
||||||
|
import { createAppWebhook, deleteAppWebhook, fetchAppWebhooks } from "./api-operations";
|
||||||
|
import { notifyWebhook } from "../../pages/api/webhooks/notify";
|
||||||
|
import { MessageEventTypes } from "../event-handlers/message-event-types";
|
||||||
|
import { createLogger } from "@saleor/apps-shared";
|
||||||
|
import { WebhookEventTypeAsyncEnum } from "../../../generated/graphql";
|
||||||
|
|
||||||
|
export const AppWebhooks = {
|
||||||
|
orderCreatedWebhook,
|
||||||
|
orderFulfilledWebhook,
|
||||||
|
orderConfirmedWebhook,
|
||||||
|
orderCancelledWebhook,
|
||||||
|
orderFullyPaidWebhook,
|
||||||
|
invoiceSentWebhook,
|
||||||
|
notifyWebhook,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AppWebhook = keyof typeof AppWebhooks;
|
||||||
|
|
||||||
|
export const eventToWebhookMapping: Record<MessageEventTypes, AppWebhook> = {
|
||||||
|
ACCOUNT_CONFIRMATION: "notifyWebhook",
|
||||||
|
ACCOUNT_DELETE: "notifyWebhook",
|
||||||
|
ACCOUNT_PASSWORD_RESET: "notifyWebhook",
|
||||||
|
ACCOUNT_CHANGE_EMAIL_REQUEST: "notifyWebhook",
|
||||||
|
ACCOUNT_CHANGE_EMAIL_CONFIRM: "notifyWebhook",
|
||||||
|
INVOICE_SENT: "invoiceSentWebhook",
|
||||||
|
ORDER_CANCELLED: "orderCancelledWebhook",
|
||||||
|
ORDER_CONFIRMED: "orderConfirmedWebhook",
|
||||||
|
ORDER_CREATED: "orderCreatedWebhook",
|
||||||
|
ORDER_FULFILLED: "orderFulfilledWebhook",
|
||||||
|
ORDER_FULLY_PAID: "orderFullyPaidWebhook",
|
||||||
|
};
|
||||||
|
|
||||||
|
const logger = createLogger({
|
||||||
|
name: "WebhookManagementService",
|
||||||
|
});
|
||||||
|
|
||||||
|
export class WebhookManagementService {
|
||||||
|
constructor(private appBaseUrl: string, private client: Client) {}
|
||||||
|
|
||||||
|
// Returns list of webhooks registered for the App in the Saleor instance
|
||||||
|
public async getWebhooks() {
|
||||||
|
logger.debug("Fetching webhooks");
|
||||||
|
return await fetchAppWebhooks({ client: this.client });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a dictionary with webhooks status.
|
||||||
|
* Status equal to true means that webhook is created and active.
|
||||||
|
*/
|
||||||
|
public async getWebhooksStatus() {
|
||||||
|
logger.debug("Fetching webhooks status");
|
||||||
|
const webhooks = await this.getWebhooks();
|
||||||
|
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.keys(AppWebhooks).map((webhook) => {
|
||||||
|
const webhookData = webhooks.find(
|
||||||
|
(w) => w.name === AppWebhooks[webhook as AppWebhook].name
|
||||||
|
);
|
||||||
|
|
||||||
|
return [webhook as AppWebhook, Boolean(webhookData?.isActive)];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createWebhook({ webhook }: { webhook: AppWebhook }) {
|
||||||
|
const webhookManifest = AppWebhooks[webhook].getWebhookManifest(this.appBaseUrl);
|
||||||
|
|
||||||
|
const asyncWebhooks = webhookManifest.asyncEvents;
|
||||||
|
|
||||||
|
if (!asyncWebhooks?.length) {
|
||||||
|
logger.warn(`Webhook ${webhook} has no async events`);
|
||||||
|
throw new Error("Only the webhooks with async events can be registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
await createAppWebhook({
|
||||||
|
client: this.client,
|
||||||
|
variables: {
|
||||||
|
asyncEvents: asyncWebhooks as WebhookEventTypeAsyncEnum[],
|
||||||
|
isActive: true,
|
||||||
|
name: webhookManifest.name,
|
||||||
|
targetUrl: webhookManifest.targetUrl,
|
||||||
|
// Override empty queries to handle NOTIFY webhook
|
||||||
|
query: webhookManifest.query === "{}" ? undefined : webhookManifest.query,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteWebhook({ webhook }: { webhook: AppWebhook }): Promise<void> {
|
||||||
|
logger.debug(`Deleting webhook ${webhook}`);
|
||||||
|
logger.debug(`Fetching existing webhooks`);
|
||||||
|
const webhookData = await this.getWebhooks();
|
||||||
|
|
||||||
|
const id = webhookData.find((w) => w.name === AppWebhooks[webhook].name)?.id;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
logger.error(`Webhook ${AppWebhooks[webhook].name} was not registered yet`);
|
||||||
|
throw new Error(`Webhook ${AppWebhooks[webhook].name} was not registered yet`);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(`Running delete mutation`);
|
||||||
|
await deleteAppWebhook({
|
||||||
|
client: this.client,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,12 +2,6 @@ import { createManifestHandler } from "@saleor/app-sdk/handlers/next";
|
||||||
import { AppManifest } from "@saleor/app-sdk/types";
|
import { AppManifest } from "@saleor/app-sdk/types";
|
||||||
|
|
||||||
import packageJson from "../../../package.json";
|
import packageJson from "../../../package.json";
|
||||||
import { orderCreatedWebhook } from "./webhooks/order-created";
|
|
||||||
import { orderFulfilledWebhook } from "./webhooks/order-fulfilled";
|
|
||||||
import { orderConfirmedWebhook } from "./webhooks/order-confirmed";
|
|
||||||
import { orderCancelledWebhook } from "./webhooks/order-cancelled";
|
|
||||||
import { orderFullyPaidWebhook } from "./webhooks/order-fully-paid";
|
|
||||||
import { invoiceSentWebhook } from "./webhooks/invoice-sent";
|
|
||||||
|
|
||||||
export default createManifestHandler({
|
export default createManifestHandler({
|
||||||
async manifestFactory(context) {
|
async manifestFactory(context) {
|
||||||
|
@ -18,14 +12,6 @@ export default createManifestHandler({
|
||||||
permissions: ["MANAGE_ORDERS", "MANAGE_USERS"],
|
permissions: ["MANAGE_ORDERS", "MANAGE_USERS"],
|
||||||
id: "saleor.app.emails-and-messages",
|
id: "saleor.app.emails-and-messages",
|
||||||
version: packageJson.version,
|
version: packageJson.version,
|
||||||
webhooks: [
|
|
||||||
orderCreatedWebhook.getWebhookManifest(context.appBaseUrl),
|
|
||||||
orderFulfilledWebhook.getWebhookManifest(context.appBaseUrl),
|
|
||||||
orderConfirmedWebhook.getWebhookManifest(context.appBaseUrl),
|
|
||||||
orderCancelledWebhook.getWebhookManifest(context.appBaseUrl),
|
|
||||||
orderFullyPaidWebhook.getWebhookManifest(context.appBaseUrl),
|
|
||||||
invoiceSentWebhook.getWebhookManifest(context.appBaseUrl),
|
|
||||||
],
|
|
||||||
extensions: [
|
extensions: [
|
||||||
/**
|
/**
|
||||||
* Optionally, extend Dashboard with custom UIs
|
* Optionally, extend Dashboard with custom UIs
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next";
|
import { createAppRegisterHandler } from "@saleor/app-sdk/handlers/next";
|
||||||
|
|
||||||
import { saleorApp } from "../../saleor-app";
|
import { saleorApp } from "../../saleor-app";
|
||||||
import { logger, createGraphQLClient } from "@saleor/apps-shared";
|
|
||||||
import { getBaseUrl } from "../../lib/get-base-url";
|
|
||||||
import { registerNotifyWebhook } from "../../lib/register-notify-webhook";
|
|
||||||
|
|
||||||
const allowedUrlsPattern = process.env.ALLOWED_DOMAIN_PATTERN;
|
const allowedUrlsPattern = process.env.ALLOWED_DOMAIN_PATTERN;
|
||||||
|
|
||||||
|
@ -24,19 +21,4 @@ export default createAppRegisterHandler({
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onAuthAplSaved: async (request, ctx) => {
|
|
||||||
// Subscribe to Notify using the mutation since it does not use subscriptions and can't be subscribed via manifest
|
|
||||||
logger.debug("onAuthAplSaved executing");
|
|
||||||
const baseUrl = getBaseUrl(request.headers);
|
|
||||||
const client = createGraphQLClient({
|
|
||||||
saleorApiUrl: ctx.authData.saleorApiUrl,
|
|
||||||
token: ctx.authData.token,
|
|
||||||
});
|
|
||||||
|
|
||||||
await registerNotifyWebhook({
|
|
||||||
client: client,
|
|
||||||
baseUrl: baseUrl,
|
|
||||||
});
|
|
||||||
logger.debug("Webhook registered");
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue