Use configuration service

This commit is contained in:
Krzysztof Wolski 2023-03-07 22:02:37 +01:00
parent d23e85a850
commit eb9bd700ca
23 changed files with 446 additions and 390 deletions

View file

@ -9,3 +9,12 @@ export const appConfigInputSchema = z.object({
}) })
), ),
}); });
export const appChannelConfigurationInputSchema = z.object({
channel: z.string(),
configuration: z.object({
active: z.boolean(),
mjmlConfigurationId: z.string().optional(),
sendgridConfigurationId: z.string().optional(),
}),
});

View file

@ -1,23 +1,57 @@
import { PrivateMetadataAppConfigurator } from "./app-configurator";
import { createSettingsManager } from "./metadata-manager";
import { logger as pinoLogger } from "../../lib/logger"; import { logger as pinoLogger } from "../../lib/logger";
import { appConfigInputSchema } from "./app-config-input-schema"; import {
import { GetAppConfigurationService } from "./get-app-configuration.service"; appChannelConfigurationInputSchema,
appConfigInputSchema,
} from "./app-config-input-schema";
import { AppConfigurationService } from "./get-app-configuration.service";
import { router } from "../trpc/trpc-server"; import { router } from "../trpc/trpc-server";
import { protectedClientProcedure } from "../trpc/protected-client-procedure"; import { protectedClientProcedure } from "../trpc/protected-client-procedure";
import { z } from "zod";
// Allow access only for the dashboard users and attaches the
// configuration service to the context
const protectedWithConfigurationService = protectedClientProcedure.use(({ next, ctx }) =>
next({
ctx: {
...ctx,
configurationService: new AppConfigurationService({
apiClient: ctx.apiClient,
saleorApiUrl: ctx.saleorApiUrl,
}),
},
})
);
export const appConfigurationRouter = router({ export const appConfigurationRouter = router({
fetch: protectedClientProcedure.query(async ({ ctx, input }) => { getChannelConfiguration: protectedWithConfigurationService
.input(z.object({ channelSlug: z.string() }))
.query(async ({ ctx, input }) => {
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
logger.debug("Get Channel Configuration called");
return await ctx.configurationService.getChannelConfiguration(input.channelSlug);
}),
setChannelConfiguration: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(appChannelConfigurationInputSchema)
.mutation(async ({ ctx, input }) => {
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
logger.debug("Set channel configuration called");
await ctx.configurationService.setChannelConfiguration(input);
}),
fetch: protectedWithConfigurationService.query(async ({ ctx, input }) => {
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl }); const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
logger.debug("appConfigurationRouter.fetch called"); logger.debug("appConfigurationRouter.fetch called");
return new GetAppConfigurationService({ return new AppConfigurationService({
apiClient: ctx.apiClient, apiClient: ctx.apiClient,
saleorApiUrl: ctx.saleorApiUrl, saleorApiUrl: ctx.saleorApiUrl,
}).getConfiguration(); }).getConfiguration();
}), }),
setAndReplace: protectedClientProcedure setAndReplace: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] }) .meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(appConfigInputSchema) .input(appConfigInputSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
@ -25,12 +59,7 @@ export const appConfigurationRouter = router({
logger.debug(input, "appConfigurationRouter.setAndReplace called with input"); logger.debug(input, "appConfigurationRouter.setAndReplace called with input");
const appConfigurator = new PrivateMetadataAppConfigurator( await ctx.configurationService.setConfigurationRoot(input);
createSettingsManager(ctx.apiClient),
ctx.saleorApiUrl
);
await appConfigurator.setConfig(input);
return null; return null;
}), }),

View file

@ -1,65 +1,91 @@
import { PrivateMetadataAppConfigurator } from "./app-configurator"; import { PrivateMetadataAppConfigurator } from "./app-configurator";
import { createSettingsManager } from "./metadata-manager"; import { createSettingsManager } from "./metadata-manager";
import { ChannelsFetcher } from "../channels/channels-fetcher";
import { ShopInfoFetcher } from "../shop-info/shop-info-fetcher";
import { FallbackAppConfig } from "./fallback-app-config";
import { Client } from "urql"; import { Client } from "urql";
import { logger as pinoLogger } from "../../lib/logger"; import { logger as pinoLogger } from "../../lib/logger";
import { AppConfig, AppConfigurationPerChannel } from "./app-config";
import { getDefaultEmptyAppConfiguration } from "./app-config-container";
// todo test const logger = pinoLogger.child({
export class GetAppConfigurationService { service: "AppConfigurationService",
constructor( });
private settings: {
apiClient: Client; export class AppConfigurationService {
saleorApiUrl: string; private configurationData?: AppConfig;
} private metadataConfigurator: PrivateMetadataAppConfigurator;
) {}
constructor(args: { apiClient: Client; saleorApiUrl: string; initialData?: AppConfig }) {
this.metadataConfigurator = new PrivateMetadataAppConfigurator(
createSettingsManager(args.apiClient),
args.saleorApiUrl
);
}
// Fetch configuration from Saleor API and cache it
private async pullConfiguration() {
logger.debug("Fetch configuration from Saleor API");
const config = await this.metadataConfigurator.getConfig();
this.configurationData = config;
}
// Push configuration to Saleor API
private async pushConfiguration() {
logger.debug("Push configuration to Saleor API");
await this.metadataConfigurator.setConfig(this.configurationData!);
}
async getConfiguration() { async getConfiguration() {
const logger = pinoLogger.child({ logger.debug("Get configuration");
service: "GetAppConfigurationService",
saleorApiUrl: this.settings.saleorApiUrl,
});
const { saleorApiUrl, apiClient } = this.settings; if (!this.configurationData) {
logger.debug("No configuration found in cache. Will fetch it from Saleor API");
await this.pullConfiguration();
}
const appConfigurator = new PrivateMetadataAppConfigurator( const savedAppConfig = this.configurationData ?? null;
createSettingsManager(apiClient),
saleorApiUrl
);
const savedAppConfig = (await appConfigurator.getConfig()) ?? null;
logger.debug(savedAppConfig, "Retrieved app config from Metadata. Will return it"); logger.debug(savedAppConfig, "Retrieved app config from Metadata. Will return it");
if (savedAppConfig) { if (savedAppConfig) {
return savedAppConfig; return savedAppConfig;
} }
}
logger.info("App config not found in metadata. Will create default config now."); // Saves configuration to Saleor API and cache it
async setConfigurationRoot(config: AppConfig) {
logger.debug("Set configuration");
const channelsFetcher = new ChannelsFetcher(apiClient); this.configurationData = config;
const shopInfoFetcher = new ShopInfoFetcher(apiClient); await this.pushConfiguration();
}
const [channels, shopAppConfiguration] = await Promise.all([ // Returns channel configuration if existing. Otherwise returns default empty one
channelsFetcher.fetchChannels(), async getChannelConfiguration(channel: string) {
shopInfoFetcher.fetchShopInfo(), logger.debug("Get channel configuration");
]); const configurations = await this.getConfiguration();
if (!configurations) {
return getDefaultEmptyAppConfiguration();
}
logger.debug(channels, "Fetched channels"); const channelConfiguration = configurations.configurationsPerChannel[channel];
logger.debug(shopAppConfiguration, "Fetched shop app configuration"); return channelConfiguration || getDefaultEmptyAppConfiguration();
}
const appConfig = FallbackAppConfig.createFallbackConfigFromExistingShopAndChannels( async setChannelConfiguration({
channels ?? [], channel,
shopAppConfiguration configuration,
); }: {
channel: string;
configuration: AppConfigurationPerChannel;
}) {
logger.debug("Set channel configuration");
let configurations = await this.getConfiguration();
if (!configurations) {
configurations = { configurationsPerChannel: {} };
}
logger.debug(appConfig, "Created a fallback AppConfig. Will save it."); configurations.configurationsPerChannel[channel] = configuration;
await this.setConfigurationRoot(configurations);
await appConfigurator.setConfig(appConfig);
logger.info("Saved initial AppConfig");
return appConfig;
} }
} }

View file

@ -83,7 +83,7 @@ export const AppConfigurationForm = (props: AppConfigurationFormProps) => {
<SwitchSelectorButton <SwitchSelectorButton
value={button.value.toString()} value={button.value.toString()}
onClick={() => onChange(button.value)} onClick={() => onChange(button.value)}
activeTab={value.toString()} activeTab={value?.toString() || "false"}
key={button.label} key={button.label}
> >
{button.label} {button.label}

View file

@ -1,6 +1,5 @@
import React, { useEffect, useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { EditIcon, IconButton, makeStyles } from "@saleor/macaw-ui"; import { EditIcon, IconButton, makeStyles } from "@saleor/macaw-ui";
import { AppConfigContainer } from "../app-config-container";
import { AppConfigurationForm } from "./app-configuration-form"; import { AppConfigurationForm } from "./app-configuration-form";
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge"; import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
import { AppColumnsLayout } from "../../ui/app-columns-layout"; import { AppColumnsLayout } from "../../ui/app-columns-layout";
@ -28,96 +27,88 @@ const useStyles = makeStyles((theme) => {
export const ChannelsConfigurationTab = () => { export const ChannelsConfigurationTab = () => {
const styles = useStyles(); const styles = useStyles();
const { appBridge } = useAppBridge(); const { appBridge } = useAppBridge();
const [mjmlConfigurationsListData, setMjmlConfigurationsListData] = useState<
{ label: string; value: string }[]
>([]);
const [sendgridConfigurationsListData, setSendgridConfigurationsListData] = useState<
{ label: string; value: string }[]
>([]);
const { data: configurationData, refetch: refetchConfig } =
trpcClient.appConfiguration.fetch.useQuery();
trpcClient.mjmlConfiguration.getConfigurations.useQuery(
{},
{
onSuccess(data) {
setMjmlConfigurationsListData(
data.map((configuration) => ({
value: configuration.id,
label: configuration.configurationName,
}))
);
},
}
);
trpcClient.sendgridConfiguration.fetch.useQuery(undefined, {
onSuccess(data) {
const keys = Object.keys(data.availableConfigurations);
setSendgridConfigurationsListData(
keys.map((key) => ({
value: key,
label: data.availableConfigurations[key].configurationName,
}))
);
},
});
const {
data: channels,
isLoading: isChannelsLoading,
isSuccess: isChannelsFetchSuccess,
} = trpcClient.channels.fetch.useQuery();
const { mutate, error: saveError } = trpcClient.appConfiguration.setAndReplace.useMutation({
onSuccess() {
refetchConfig();
appBridge?.dispatch(
actions.Notification({
title: "Success",
text: "Saved app configuration",
status: "success",
})
);
},
});
const [activeChannelSlug, setActiveChannelSlug] = useState<string | null>(null); const [activeChannelSlug, setActiveChannelSlug] = useState<string | null>(null);
useEffect(() => { const { data: channelsData, isLoading: isChannelsDataLoading } =
if (isChannelsFetchSuccess) { trpcClient.channels.fetch.useQuery(undefined, {
setActiveChannelSlug(channels[0].slug ?? null); onSuccess: (data) => {
} if (data?.length) {
}, [isChannelsFetchSuccess, channels]); setActiveChannelSlug(data[0].slug);
}
},
});
const activeChannel = useMemo(() => { const {
try { data: configurationData,
return channels!.find((c) => c.slug === activeChannelSlug)!; refetch: refetchConfig,
} catch (e) { isLoading: isConfigurationDataLoading,
return null; } = trpcClient.appConfiguration.getChannelConfiguration.useQuery(
} {
}, [channels, activeChannelSlug]); channelSlug: activeChannelSlug!,
},
{ enabled: !!activeChannelSlug }
);
if (isChannelsLoading) { const { data: mjmlConfigurations, isLoading: isMjmlQueryLoading } =
trpcClient.mjmlConfiguration.getConfigurations.useQuery({});
const mjmlConfigurationsListData = useMemo(() => {
return (
mjmlConfigurations?.map((configuration) => ({
value: configuration.id,
label: configuration.configurationName,
})) ?? []
);
}, [mjmlConfigurations]);
const { data: sendgridConfigurations, isLoading: isSendgridQueryLoading } =
trpcClient.sendgridConfiguration.fetch.useQuery();
const sendgridConfigurationsListData = useMemo(() => {
if (!sendgridConfigurations) {
return [];
}
const keys = Object.keys(sendgridConfigurations.availableConfigurations ?? {}) || [];
return (
keys.map((key) => ({
value: key,
label: sendgridConfigurations.availableConfigurations[key].configurationName,
})) ?? []
);
}, [sendgridConfigurations]);
const { mutate: mutateAppChannelConfiguration, error: saveError } =
trpcClient.appConfiguration.setChannelConfiguration.useMutation({
onSuccess() {
refetchConfig();
appBridge?.dispatch(
actions.Notification({
title: "Success",
text: "Saved app configuration",
status: "success",
})
);
},
});
const activeChannel = channelsData?.find((c) => c.slug === activeChannelSlug);
if (isChannelsDataLoading) {
return <LoadingIndicator />; return <LoadingIndicator />;
} }
if (!channelsData?.length) {
if (!channels?.length) {
return <div>NO CHANNELS</div>; return <div>NO CHANNELS</div>;
} }
if (!activeChannel) { const isFormDataLoading =
return <div>Error. No channel available</div>; isConfigurationDataLoading || isMjmlQueryLoading || isSendgridQueryLoading;
}
return ( return (
<AppColumnsLayout> <AppColumnsLayout>
<SideMenu <SideMenu
title="Channels" title="Channels"
selectedItemId={activeChannel.slug} selectedItemId={activeChannel?.slug}
headerToolbar={ headerToolbar={
<IconButton <IconButton
variant="secondary" variant="secondary"
@ -133,31 +124,32 @@ export const ChannelsConfigurationTab = () => {
</IconButton> </IconButton>
} }
onClick={(id) => setActiveChannelSlug(id)} onClick={(id) => setActiveChannelSlug(id)}
items={channels.map((c) => ({ label: c.name, id: c.slug })) || []} items={channelsData.map((c) => ({ label: c.name, id: c.slug })) || []}
/> />
{activeChannel ? ( <div className={styles.configurationColumn}>
<div className={styles.configurationColumn}> {!activeChannel || isFormDataLoading ? (
<AppConfigurationForm <LoadingIndicator />
channelID={activeChannel.id} ) : (
key={activeChannelSlug} <>
channelSlug={activeChannel.slug} <AppConfigurationForm
mjmlConfigurationChoices={mjmlConfigurationsListData} channelID={activeChannel.id}
sendgridConfigurationChoices={sendgridConfigurationsListData} key={activeChannelSlug}
onSubmit={async (data) => { channelSlug={activeChannel.slug}
const newConfig = AppConfigContainer.setChannelAppConfiguration(configurationData)( mjmlConfigurationChoices={mjmlConfigurationsListData}
activeChannel.slug sendgridConfigurationChoices={sendgridConfigurationsListData}
)(data); onSubmit={async (data) => {
mutateAppChannelConfiguration({
mutate(newConfig); channel: activeChannel.slug,
}} configuration: data,
initialData={AppConfigContainer.getChannelAppConfiguration(configurationData)( });
activeChannel.slug }}
)} initialData={configurationData}
channelName={activeChannel?.name ?? activeChannelSlug} channelName={activeChannel?.name ?? activeChannelSlug}
/> />
{saveError && <span>{saveError.message}</span>} {saveError && <span>{saveError.message}</span>}
</div> </>
) : null} )}
</div>
</AppColumnsLayout> </AppColumnsLayout>
); );
}; };

View file

@ -1,17 +1,16 @@
import { ChannelsFetcher } from "./channels-fetcher"; import { ChannelsFetcher } from "./channels-fetcher";
import { ChannelFragment } from "../../../generated/graphql"; import { createClient } from "../../lib/create-graphql-client";
import { createClient } from "../../lib/create-graphq-client";
import { router } from "../trpc/trpc-server"; import { router } from "../trpc/trpc-server";
import { protectedClientProcedure } from "../trpc/protected-client-procedure"; import { protectedClientProcedure } from "../trpc/protected-client-procedure";
export const channelsRouter = router({ export const channelsRouter = router({
fetch: protectedClientProcedure.query(async ({ ctx, input }): Promise<ChannelFragment[]> => { fetch: protectedClientProcedure.query(async ({ ctx }) => {
const client = createClient(ctx.saleorApiUrl, async () => const client = createClient(ctx.saleorApiUrl, async () =>
Promise.resolve({ token: ctx.appToken }) Promise.resolve({ token: ctx.appToken })
); );
const fetcher = new ChannelsFetcher(client); const fetcher = new ChannelsFetcher(client);
return fetcher.fetchChannels().then((channels) => channels ?? []); return await fetcher.fetchChannels().then((channels) => channels ?? []);
}), }),
}); });

View file

@ -1,8 +1,10 @@
import { AuthData } from "@saleor/app-sdk/APL"; import { AuthData } from "@saleor/app-sdk/APL";
import { Client } from "urql";
import { logger as pinoLogger } from "../../lib/logger"; import { logger as pinoLogger } from "../../lib/logger";
import { AppConfigurationService } from "../app-configuration/get-app-configuration.service";
import { MjmlConfigurationService } from "../mjml/configuration/get-mjml-configuration.service";
import { sendMjml } from "../mjml/send-mjml"; import { sendMjml } from "../mjml/send-mjml";
import { sendSendgrid } from "../sendgrid/send-sendgrid"; import { sendSendgrid } from "../sendgrid/send-sendgrid";
import { appRouter } from "../trpc/trpc-app-router";
import { MessageEventTypes } from "./message-event-types"; import { MessageEventTypes } from "./message-event-types";
interface SendEventMessagesArgs { interface SendEventMessagesArgs {
@ -11,6 +13,7 @@ interface SendEventMessagesArgs {
event: MessageEventTypes; event: MessageEventTypes;
authData: AuthData; authData: AuthData;
payload: any; payload: any;
client: Client;
} }
export const sendEventMessages = async ({ export const sendEventMessages = async ({
@ -19,6 +22,7 @@ export const sendEventMessages = async ({
event, event,
authData, authData,
payload, payload,
client,
}: SendEventMessagesArgs) => { }: SendEventMessagesArgs) => {
const logger = pinoLogger.child({ const logger = pinoLogger.child({
fn: "sendEventMessages", fn: "sendEventMessages",
@ -26,16 +30,12 @@ export const sendEventMessages = async ({
logger.debug("Function called"); logger.debug("Function called");
// get app configuration const appConfigurationService = new AppConfigurationService({
const caller = appRouter.createCaller({ apiClient: client,
appId: authData.appId,
saleorApiUrl: authData.saleorApiUrl, saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
ssr: true,
}); });
const appConfigurations = await caller.appConfiguration.fetch(); const channelAppConfiguration = await appConfigurationService.getChannelConfiguration(channel);
const channelAppConfiguration = appConfigurations.configurationsPerChannel[channel];
if (!channelAppConfiguration) { if (!channelAppConfiguration) {
logger.warn("App has no configuration for this channel"); logger.warn("App has no configuration for this channel");
@ -50,18 +50,27 @@ export const sendEventMessages = async ({
if (channelAppConfiguration.mjmlConfigurationId) { if (channelAppConfiguration.mjmlConfigurationId) {
logger.debug("Channel has assigned MJML configuration"); logger.debug("Channel has assigned MJML configuration");
const mjmlStatus = await sendMjml({
authData, const mjmlConfigurationService = new MjmlConfigurationService({
channel, apiClient: client,
event, saleorApiUrl: authData.saleorApiUrl,
payload,
recipientEmail,
mjmlConfigurationId: channelAppConfiguration.mjmlConfigurationId,
}); });
if (mjmlStatus?.errors.length) { const mjmlConfiguration = await mjmlConfigurationService.getConfiguration({
logger.error("MJML errors"); id: channelAppConfiguration.mjmlConfigurationId,
logger.error(mjmlStatus?.errors); });
if (mjmlConfiguration) {
const mjmlStatus = await sendMjml({
event,
payload,
recipientEmail,
mjmlConfiguration,
});
if (mjmlStatus?.errors.length) {
logger.error("MJML errors");
logger.error(mjmlStatus?.errors);
}
} }
} }
const sendgridStatus = await sendSendgrid({ const sendgridStatus = await sendSendgrid({

View file

@ -1,36 +1,107 @@
import { PrivateMetadataMjmlConfigurator } from "./mjml-configurator"; import { MjmlConfigurator, PrivateMetadataMjmlConfigurator } from "./mjml-configurator";
import { Client } from "urql"; import { Client } from "urql";
import { logger as pinoLogger } from "../../../lib/logger"; import { logger as pinoLogger } from "../../../lib/logger";
import { createSettingsManager } from "../../app-configuration/metadata-manager"; import { createSettingsManager } from "../../app-configuration/metadata-manager";
import { MjmlConfig, MjmlConfiguration } from "./mjml-config";
import { FilterConfigurationsArgs, MjmlConfigContainer } from "./mjml-config-container";
// todo test const logger = pinoLogger.child({
export class GetMjmlConfigurationService { service: "MjmlConfigurationService",
constructor( });
private settings: {
apiClient: Client;
saleorApiUrl: string;
}
) {}
async getConfiguration() { export class MjmlConfigurationService {
const logger = pinoLogger.child({ private configurationData?: MjmlConfig;
service: "GetMjmlConfigurationService", private metadataConfigurator: MjmlConfigurator;
saleorApiUrl: this.settings.saleorApiUrl,
});
const { saleorApiUrl, apiClient } = this.settings; constructor(args: { apiClient: Client; saleorApiUrl: string; initialData?: MjmlConfig }) {
this.metadataConfigurator = new PrivateMetadataMjmlConfigurator(
const mjmlConfigurator = new PrivateMetadataMjmlConfigurator( createSettingsManager(args.apiClient),
createSettingsManager(apiClient), args.saleorApiUrl
saleorApiUrl
); );
const savedMjmlConfig = (await mjmlConfigurator.getConfig()) ?? null; if (args.initialData) {
this.configurationData = args.initialData;
logger.debug(savedMjmlConfig, "Retrieved MJML config from Metadata. Will return it");
if (savedMjmlConfig) {
return savedMjmlConfig;
} }
} }
// Fetch configuration from Saleor API and cache it
private async pullConfiguration() {
logger.debug("Fetch configuration from Saleor API");
const config = await this.metadataConfigurator.getConfig();
this.configurationData = config;
}
// Push configuration to Saleor API
private async pushConfiguration() {
logger.debug("Push configuration to Saleor API");
await this.metadataConfigurator.setConfig(this.configurationData!);
}
// Returns configuration from cache or fetches it from Saleor API
async getConfigurationRoot() {
logger.debug("Get configuration root");
if (this.configurationData) {
logger.debug("Using cached configuration");
return this.configurationData;
}
// No cached data, fetch it from Saleor API
await this.pullConfiguration();
if (!this.configurationData) {
logger.warn("No configuration found in Saleor API");
return;
}
return this.configurationData;
}
// Saves configuration to Saleor API and cache it
async setConfigurationRoot(config: MjmlConfig) {
logger.debug("Set configuration root");
this.configurationData = config;
await this.pushConfiguration();
}
async getConfiguration({ id }: { id: string }) {
logger.debug("Get configuration");
return MjmlConfigContainer.getConfiguration(await this.getConfigurationRoot())({ id });
}
async getConfigurations(filter: FilterConfigurationsArgs) {
logger.debug("Get configuration");
return MjmlConfigContainer.getConfigurations(await this.getConfigurationRoot())(filter);
}
async createConfiguration(config: Omit<MjmlConfiguration, "id" | "events">) {
logger.debug("Create configuration");
const updatedConfigurationRoot = MjmlConfigContainer.createConfiguration(
await this.getConfigurationRoot()
)(config);
await this.setConfigurationRoot(updatedConfigurationRoot);
return updatedConfigurationRoot.configurations[
updatedConfigurationRoot.configurations.length - 1
];
}
async updateConfiguration(config: MjmlConfiguration) {
logger.debug("Update configuration");
const updatedConfigurationRoot = MjmlConfigContainer.updateConfiguration(
await this.getConfigurationRoot()
)(config);
this.setConfigurationRoot(updatedConfigurationRoot);
}
async deleteConfiguration({ id }: { id: string }) {
logger.debug("Delete configuration");
const updatedConfigurationRoot = MjmlConfigContainer.deleteConfiguration(
await this.getConfigurationRoot()
)({ id });
this.setConfigurationRoot(updatedConfigurationRoot);
}
} }

View file

@ -2,7 +2,9 @@ import { messageEventTypes } from "../../event-handlers/message-event-types";
import { MjmlConfig as MjmlConfigurationRoot, MjmlConfiguration } from "./mjml-config"; import { MjmlConfig as MjmlConfigurationRoot, MjmlConfiguration } from "./mjml-config";
import { defaultMjmlTemplates, defaultMjmlSubjectTemplates } from "../default-templates"; import { defaultMjmlTemplates, defaultMjmlSubjectTemplates } from "../default-templates";
const getDefaultEventsConfiguration = (): MjmlConfiguration["events"] => export const generateMjmlConfigurationId = () => Date.now().toString();
export const getDefaultEventsConfiguration = (): MjmlConfiguration["events"] =>
messageEventTypes.map((eventType) => ({ messageEventTypes.map((eventType) => ({
active: true, active: true,
eventType: eventType, eventType: eventType,
@ -41,7 +43,7 @@ const getConfiguration =
return mjmlConfigRoot.configurations.find((c) => c.id === id); return mjmlConfigRoot.configurations.find((c) => c.id === id);
}; };
interface FilterConfigurationsArgs { export interface FilterConfigurationsArgs {
ids?: string[]; ids?: string[];
active?: boolean; active?: boolean;
} }
@ -74,7 +76,7 @@ const createConfiguration =
// for creating a new configurations, the ID has to be generated // for creating a new configurations, the ID has to be generated
const newConfiguration = { const newConfiguration = {
...mjmlConfiguration, ...mjmlConfiguration,
id: Date.now().toString(), id: generateMjmlConfigurationId(),
events: getDefaultEventsConfiguration(), events: getDefaultEventsConfiguration(),
}; };
mjmlConfigNormalized.configurations.unshift(newConfiguration); mjmlConfigNormalized.configurations.unshift(newConfiguration);

View file

@ -1,4 +1,3 @@
import { PrivateMetadataMjmlConfigurator } from "./mjml-configurator";
import { logger as pinoLogger } from "../../../lib/logger"; import { logger as pinoLogger } from "../../../lib/logger";
import { import {
mjmlCreateConfigurationSchema, mjmlCreateConfigurationSchema,
@ -9,134 +8,96 @@ import {
mjmlUpdateEventConfigurationInputSchema, mjmlUpdateEventConfigurationInputSchema,
mjmlUpdateOrCreateConfigurationSchema, mjmlUpdateOrCreateConfigurationSchema,
} from "./mjml-config-input-schema"; } from "./mjml-config-input-schema";
import { GetMjmlConfigurationService } from "./get-mjml-configuration.service"; import { MjmlConfigurationService } from "./get-mjml-configuration.service";
import { router } from "../../trpc/trpc-server"; import { router } from "../../trpc/trpc-server";
import { protectedClientProcedure } from "../../trpc/protected-client-procedure"; import { protectedClientProcedure } from "../../trpc/protected-client-procedure";
import { createSettingsManager } from "../../app-configuration/metadata-manager";
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";
import { MjmlConfigContainer } from "./mjml-config-container";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
// Allow access only for the dashboard users and attaches the
// configuration service to the context
const protectedWithConfigurationService = protectedClientProcedure.use(({ next, ctx }) =>
next({
ctx: {
...ctx,
configurationService: new MjmlConfigurationService({
apiClient: ctx.apiClient,
saleorApiUrl: ctx.saleorApiUrl,
}),
},
})
);
export const mjmlConfigurationRouter = router({ export const mjmlConfigurationRouter = router({
fetch: protectedClientProcedure.query(async ({ ctx }) => { fetch: protectedWithConfigurationService.query(async ({ ctx }) => {
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl }); const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
logger.debug("mjmlConfigurationRouter.fetch called"); logger.debug("mjmlConfigurationRouter.fetch called");
return ctx.configurationService.getConfigurationRoot();
return new GetMjmlConfigurationService({
apiClient: ctx.apiClient,
saleorApiUrl: ctx.saleorApiUrl,
}).getConfiguration();
}), }),
getConfiguration: protectedClientProcedure getConfiguration: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] }) .meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(mjmlGetConfigurationInputSchema) .input(mjmlGetConfigurationInputSchema)
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl }); const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
logger.debug(input, "mjmlConfigurationRouter.get called"); logger.debug(input, "mjmlConfigurationRouter.get called");
return ctx.configurationService.getConfiguration(input);
const mjmlConfigurator = new PrivateMetadataMjmlConfigurator(
createSettingsManager(ctx.apiClient),
ctx.saleorApiUrl
);
const configRoot = await mjmlConfigurator.getConfig();
return MjmlConfigContainer.getConfiguration(configRoot)(input);
}), }),
getConfigurations: protectedClientProcedure getConfigurations: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] }) .meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(mjmlGetConfigurationsInputSchema) .input(mjmlGetConfigurationsInputSchema)
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl }); const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
logger.debug(input, "mjmlConfigurationRouter.getConfigurations called"); logger.debug(input, "mjmlConfigurationRouter.getConfigurations called");
const mjmlConfigurator = new PrivateMetadataMjmlConfigurator( return ctx.configurationService.getConfigurations(input);
createSettingsManager(ctx.apiClient),
ctx.saleorApiUrl
);
const configRoot = await mjmlConfigurator.getConfig();
return MjmlConfigContainer.getConfigurations(configRoot)(input);
}), }),
createConfiguration: protectedClientProcedure createConfiguration: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] }) .meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(mjmlCreateConfigurationSchema) .input(mjmlCreateConfigurationSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl }); const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
logger.debug(input, "mjmlConfigurationRouter.create called"); logger.debug(input, "mjmlConfigurationRouter.create called");
const mjmlConfigurator = new PrivateMetadataMjmlConfigurator( return await ctx.configurationService.createConfiguration(input);
createSettingsManager(ctx.apiClient),
ctx.saleorApiUrl
);
const configRoot = await mjmlConfigurator.getConfig();
const newConfigurationRoot = MjmlConfigContainer.createConfiguration(configRoot)(input);
await mjmlConfigurator.setConfig(newConfigurationRoot);
return null;
}), }),
deleteConfiguration: protectedClientProcedure deleteConfiguration: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] }) .meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(mjmlDeleteConfigurationInputSchema) .input(mjmlDeleteConfigurationInputSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl }); const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
logger.debug(input, "mjmlConfigurationRouter.delete called"); logger.debug(input, "mjmlConfigurationRouter.delete called");
const mjmlConfigurator = new PrivateMetadataMjmlConfigurator(
createSettingsManager(ctx.apiClient),
ctx.saleorApiUrl
);
const configRoot = await mjmlConfigurator.getConfig();
const newConfigurationRoot = MjmlConfigContainer.deleteConfiguration(configRoot)(input);
await mjmlConfigurator.setConfig(newConfigurationRoot);
await ctx.configurationService.deleteConfiguration(input);
return null; return null;
}), }),
updateOrCreateConfiguration: protectedClientProcedure updateOrCreateConfiguration: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] }) .meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(mjmlUpdateOrCreateConfigurationSchema) .input(mjmlUpdateOrCreateConfigurationSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl }); const logger = pinoLogger.child({ saleorApiUrl: ctx.saleorApiUrl });
logger.debug(input, "mjmlConfigurationRouter.update or create called"); logger.debug(input, "mjmlConfigurationRouter.update or create called");
const mjmlConfigurator = new PrivateMetadataMjmlConfigurator(
createSettingsManager(ctx.apiClient),
ctx.saleorApiUrl
);
const configRoot = await mjmlConfigurator.getConfig();
const { id } = input; const { id } = input;
if (!!id) { if (!id) {
const existingConfiguration = MjmlConfigContainer.getConfiguration(configRoot)({ id }); return await ctx.configurationService.createConfiguration(input);
} else {
const existingConfiguration = await ctx.configurationService.getConfiguration({ id });
if (!existingConfiguration) { if (!existingConfiguration) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Configuration not found", message: "Configuration not found",
}); });
} }
// checking typeof id is not enough to satisfy typescript, so need to override id field issue
const configuration = { const configuration = {
id, id,
...input, ...input,
events: existingConfiguration.events, events: existingConfiguration.events,
}; };
await ctx.configurationService.updateConfiguration(configuration);
const newConfigurationRoot =
MjmlConfigContainer.updateConfiguration(configRoot)(configuration);
await mjmlConfigurator.setConfig(newConfigurationRoot);
return configuration; return configuration;
} else {
const newConfigurationRoot = MjmlConfigContainer.createConfiguration(configRoot)(input);
await mjmlConfigurator.setConfig(newConfigurationRoot);
return newConfigurationRoot.configurations[newConfigurationRoot.configurations.length - 1];
} }
}), }),
getEventConfiguration: protectedClientProcedure getEventConfiguration: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] }) .meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(mjmlGetEventConfigurationInputSchema) .input(mjmlGetEventConfigurationInputSchema)
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
@ -144,22 +105,17 @@ export const mjmlConfigurationRouter = router({
logger.debug(input, "mjmlConfigurationRouter.getEventConfiguration or create called"); logger.debug(input, "mjmlConfigurationRouter.getEventConfiguration or create called");
const mjmlConfigurator = new PrivateMetadataMjmlConfigurator( const configuration = await ctx.configurationService.getConfiguration({
createSettingsManager(ctx.apiClient),
ctx.saleorApiUrl
);
const configRoot = await mjmlConfigurator.getConfig();
const configuration = MjmlConfigContainer.getConfiguration(configRoot)({
id: input.configurationId, id: input.configurationId,
}); });
if (!configuration) { if (!configuration) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Configuration not found", message: "Configuration not found",
}); });
} }
const event = configuration.events.find((e) => e.eventType === input.eventType); const event = configuration.events.find((e) => e.eventType === input.eventType);
if (!event) { if (!event) {
throw new TRPCError({ throw new TRPCError({
@ -169,7 +125,7 @@ export const mjmlConfigurationRouter = router({
} }
return event; return event;
}), }),
updateEventConfiguration: protectedClientProcedure updateEventConfiguration: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] }) .meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input(mjmlUpdateEventConfigurationInputSchema) .input(mjmlUpdateEventConfigurationInputSchema)
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
@ -177,22 +133,17 @@ export const mjmlConfigurationRouter = router({
logger.debug(input, "mjmlConfigurationRouter.updateEventConfiguration or create called"); logger.debug(input, "mjmlConfigurationRouter.updateEventConfiguration or create called");
const mjmlConfigurator = new PrivateMetadataMjmlConfigurator( const configuration = await ctx.configurationService.getConfiguration({
createSettingsManager(ctx.apiClient),
ctx.saleorApiUrl
);
const configRoot = await mjmlConfigurator.getConfig();
const configuration = MjmlConfigContainer.getConfiguration(configRoot)({
id: input.configurationId, id: input.configurationId,
}); });
if (!configuration) { if (!configuration) {
throw new TRPCError({ throw new TRPCError({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: "Configuration not found", message: "Configuration not found",
}); });
} }
const eventIndex = configuration.events.findIndex((e) => e.eventType === input.eventType); const eventIndex = configuration.events.findIndex((e) => e.eventType === input.eventType);
configuration.events[eventIndex] = { configuration.events[eventIndex] = {
active: input.active, active: input.active,
@ -200,13 +151,11 @@ export const mjmlConfigurationRouter = router({
template: input.template, template: input.template,
subject: input.subject, subject: input.subject,
}; };
const newConfigurationRoot = await ctx.configurationService.updateConfiguration(configuration);
MjmlConfigContainer.updateConfiguration(configRoot)(configuration);
await mjmlConfigurator.setConfig(newConfigurationRoot);
return configuration; return configuration;
}), }),
renderTemplate: protectedClientProcedure renderTemplate: protectedWithConfigurationService
.meta({ requiredClientPermissions: ["MANAGE_APPS"] }) .meta({ requiredClientPermissions: ["MANAGE_APPS"] })
.input( .input(
z.object({ z.object({

View file

@ -116,19 +116,25 @@ export const MjmlConfigurationTab = ({ configurationId }: MjmlConfigurationTabPr
items={configurations?.map((c) => ({ label: c.configurationName, id: c.id })) || []} items={configurations?.map((c) => ({ label: c.configurationName, id: c.id })) || []}
/> />
<div className={styles.configurationColumn}> <div className={styles.configurationColumn}>
<MjmlConfigurationForm {configurationsIsLoading ? (
onConfigurationSaved={() => refetchConfigurations()} <LoadingIndicator />
initialData={configuration || getDefaultEmptyConfiguration()} ) : (
configurationId={configurationId} <>
/> <MjmlConfigurationForm
{!!configurationId && !!configuration && ( onConfigurationSaved={() => refetchConfigurations()}
<MjmlTemplatesCard initialData={configuration || getDefaultEmptyConfiguration()}
configurationId={configurationId} configurationId={configurationId}
configuration={configuration} />
onEventChanged={() => { {!!configurationId && !!configuration && (
refetchConfigurations(); <MjmlTemplatesCard
}} configurationId={configurationId}
/> configuration={configuration}
onEventChanged={() => {
refetchConfigurations();
}}
/>
)}
</>
)} )}
</div> </div>
</AppColumnsLayout> </AppColumnsLayout>

View file

@ -1,43 +0,0 @@
import { AuthData } from "@saleor/app-sdk/APL";
import { appRouter } from "../trpc/trpc-app-router";
import { logger as pinoLogger } from "../../lib/logger";
interface GetMjmlSettingsArgs {
authData: AuthData;
channel: string;
configurationId: string;
}
export const getActiveMjmlSettings = async ({
authData,
channel,
configurationId,
}: GetMjmlSettingsArgs) => {
const logger = pinoLogger.child({
fn: "getMjmlSettings",
channel,
});
const caller = appRouter.createCaller({
appId: authData.appId,
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
ssr: true,
});
const configuration = await caller.mjmlConfiguration.getConfiguration({
id: configurationId,
});
if (!configuration) {
logger.warn(`The MJML configuration with id ${configurationId} does not exist`);
return;
}
if (!configuration.active) {
logger.warn(`The MJML configuration ${configuration.configurationName} is not active`);
return;
}
return configuration;
};

View file

@ -1,16 +1,13 @@
import { logger as pinoLogger } from "../../lib/logger"; import { logger as pinoLogger } from "../../lib/logger";
import { AuthData } from "@saleor/app-sdk/APL";
import { getActiveMjmlSettings } from "./get-active-mjml-settings";
import { compileMjml } from "./compile-mjml"; import { compileMjml } from "./compile-mjml";
import { compileHandlebarsTemplate } from "./compile-handlebars-template"; import { compileHandlebarsTemplate } from "./compile-handlebars-template";
import { sendEmailWithSmtp } from "./send-email-with-smtp"; import { sendEmailWithSmtp } from "./send-email-with-smtp";
import { MessageEventTypes } from "../event-handlers/message-event-types"; import { MessageEventTypes } from "../event-handlers/message-event-types";
import { htmlToPlaintext } from "./html-to-plaintext"; import { htmlToPlaintext } from "./html-to-plaintext";
import { MjmlConfiguration } from "./configuration/mjml-config";
interface SendMjmlArgs { interface SendMjmlArgs {
authData: AuthData; mjmlConfiguration: MjmlConfiguration;
mjmlConfigurationId: string;
channel: string;
recipientEmail: string; recipientEmail: string;
event: MessageEventTypes; event: MessageEventTypes;
payload: any; payload: any;
@ -24,36 +21,17 @@ export interface EmailServiceResponse {
} }
export const sendMjml = async ({ export const sendMjml = async ({
authData,
channel,
payload, payload,
recipientEmail, recipientEmail,
event, event,
mjmlConfigurationId, mjmlConfiguration,
}: SendMjmlArgs) => { }: SendMjmlArgs) => {
const logger = pinoLogger.child({ const logger = pinoLogger.child({
fn: "sendMjml", fn: "sendMjml",
event, event,
}); });
const settings = await getActiveMjmlSettings({ const eventSettings = mjmlConfiguration.events.find((e) => e.eventType === event);
authData,
channel,
configurationId: mjmlConfigurationId,
});
if (!settings) {
logger.debug("No active settings, skipping");
return {
errors: [
{
message: "No active settings",
},
],
};
}
const eventSettings = settings.events.find((e) => e.eventType === event);
if (!eventSettings) { if (!eventSettings) {
logger.debug("No active settings for this event, skipping"); logger.debug("No active settings for this event, skipping");
return { return {
@ -154,13 +132,13 @@ export const sendMjml = async ({
mailData: { mailData: {
text: emailBodyPlaintext, text: emailBodyPlaintext,
html: emailBodyHtml, html: emailBodyHtml,
from: `${settings.senderName} <${settings.senderEmail}>`, from: `${mjmlConfiguration.senderName} <${mjmlConfiguration.senderEmail}>`,
to: recipientEmail, to: recipientEmail,
subject: emailSubject, subject: emailSubject,
}, },
smtpSettings: { smtpSettings: {
host: settings.smtpHost, host: mjmlConfiguration.smtpHost,
port: parseInt(settings.smtpPort, 10), port: parseInt(mjmlConfiguration.smtpPort, 10),
}, },
}); });

View file

@ -4,7 +4,7 @@ import { TRPCError } from "@trpc/server";
import { ProtectedHandlerError } from "@saleor/app-sdk/handlers/next"; import { ProtectedHandlerError } from "@saleor/app-sdk/handlers/next";
import { saleorApp } from "../../saleor-app"; import { saleorApp } from "../../saleor-app";
import { logger } from "../../lib/logger"; import { logger } from "../../lib/logger";
import { createClient } from "../../lib/create-graphq-client"; import { createClient } from "../../lib/create-graphql-client";
const attachAppToken = middleware(async ({ ctx, next }) => { const attachAppToken = middleware(async ({ ctx, next }) => {
logger.debug("attachAppToken middleware"); logger.debug("attachAppToken middleware");

View file

@ -21,7 +21,6 @@ export const CodeEditor = ({ initialTemplate, onChange, value, language }: Props
const handleOnChange = useCallback( const handleOnChange = useCallback(
(value?: string) => { (value?: string) => {
console.log("ON CHANGE");
onChange(value ?? ""); onChange(value ?? "");
}, },
[value] [value]

View file

@ -7,6 +7,7 @@ import {
OrderDetailsFragmentDoc, OrderDetailsFragmentDoc,
} from "../../../../generated/graphql"; } from "../../../../generated/graphql";
import { sendEventMessages } from "../../../modules/event-handlers/send-event-messages"; import { sendEventMessages } from "../../../modules/event-handlers/send-event-messages";
import { createClient } from "../../../lib/create-graphql-client";
const InvoiceSentWebhookPayload = gql` const InvoiceSentWebhookPayload = gql`
${OrderDetailsFragmentDoc} ${OrderDetailsFragmentDoc}
@ -71,10 +72,14 @@ const handler: NextWebhookApiHandler<InvoiceSentWebhookPayloadFragment> = async
} }
const channel = order.channel.slug; const channel = order.channel.slug;
const client = createClient(authData.saleorApiUrl, async () =>
Promise.resolve({ token: authData.token })
);
await sendEventMessages({ await sendEventMessages({
authData, authData,
channel, channel,
client,
event: "INVOICE_SENT", event: "INVOICE_SENT",
payload: { order: payload.order }, payload: { order: payload.order },
recipientEmail, recipientEmail,

View file

@ -7,6 +7,7 @@ import {
OrderDetailsFragmentDoc, OrderDetailsFragmentDoc,
} from "../../../../generated/graphql"; } from "../../../../generated/graphql";
import { sendEventMessages } from "../../../modules/event-handlers/send-event-messages"; import { sendEventMessages } from "../../../modules/event-handlers/send-event-messages";
import { createClient } from "../../../lib/create-graphql-client";
const OrderCancelledWebhookPayload = gql` const OrderCancelledWebhookPayload = gql`
${OrderDetailsFragmentDoc} ${OrderDetailsFragmentDoc}
@ -62,10 +63,14 @@ const handler: NextWebhookApiHandler<OrderCancelledWebhookPayloadFragment> = asy
} }
const channel = order.channel.slug; const channel = order.channel.slug;
const client = createClient(authData.saleorApiUrl, async () =>
Promise.resolve({ token: authData.token })
);
await sendEventMessages({ await sendEventMessages({
authData, authData,
channel, channel,
client,
event: "ORDER_CANCELLED", event: "ORDER_CANCELLED",
payload: { order: payload.order }, payload: { order: payload.order },
recipientEmail, recipientEmail,

View file

@ -7,6 +7,7 @@ import {
OrderDetailsFragmentDoc, OrderDetailsFragmentDoc,
} from "../../../../generated/graphql"; } from "../../../../generated/graphql";
import { sendEventMessages } from "../../../modules/event-handlers/send-event-messages"; import { sendEventMessages } from "../../../modules/event-handlers/send-event-messages";
import { createClient } from "../../../lib/create-graphql-client";
const OrderConfirmedWebhookPayload = gql` const OrderConfirmedWebhookPayload = gql`
${OrderDetailsFragmentDoc} ${OrderDetailsFragmentDoc}
@ -63,10 +64,14 @@ const handler: NextWebhookApiHandler<OrderConfirmedWebhookPayloadFragment> = asy
} }
const channel = order.channel.slug; const channel = order.channel.slug;
const client = createClient(authData.saleorApiUrl, async () =>
Promise.resolve({ token: authData.token })
);
await sendEventMessages({ await sendEventMessages({
authData, authData,
channel, channel,
client,
event: "ORDER_CONFIRMED", event: "ORDER_CONFIRMED",
payload: { order: payload.order }, payload: { order: payload.order },
recipientEmail, recipientEmail,

View file

@ -1,10 +1,11 @@
import { OrderDetailsFragment, OrderDetailsFragmentDoc } from "./../../../../generated/graphql"; import { OrderDetailsFragmentDoc } from "./../../../../generated/graphql";
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { gql } from "urql"; import { gql } from "urql";
import { saleorApp } from "../../../saleor-app"; import { saleorApp } from "../../../saleor-app";
import { logger as pinoLogger } from "../../../lib/logger"; import { logger as pinoLogger } from "../../../lib/logger";
import { OrderCreatedWebhookPayloadFragment } from "../../../../generated/graphql"; import { OrderCreatedWebhookPayloadFragment } from "../../../../generated/graphql";
import { sendEventMessages } from "../../../modules/event-handlers/send-event-messages"; import { sendEventMessages } from "../../../modules/event-handlers/send-event-messages";
import { createClient } from "../../../lib/create-graphql-client";
const OrderCreatedWebhookPayload = gql` const OrderCreatedWebhookPayload = gql`
${OrderDetailsFragmentDoc} ${OrderDetailsFragmentDoc}
@ -60,10 +61,14 @@ const handler: NextWebhookApiHandler<OrderCreatedWebhookPayloadFragment> = async
} }
const channel = order.channel.slug; const channel = order.channel.slug;
const client = createClient(authData.saleorApiUrl, async () =>
Promise.resolve({ token: authData.token })
);
await sendEventMessages({ await sendEventMessages({
authData, authData,
channel, channel,
client,
event: "ORDER_CREATED", event: "ORDER_CREATED",
payload: { order: payload.order }, payload: { order: payload.order },
recipientEmail, recipientEmail,

View file

@ -7,6 +7,7 @@ import {
OrderFulfilledWebhookPayloadFragment, OrderFulfilledWebhookPayloadFragment,
} from "../../../../generated/graphql"; } from "../../../../generated/graphql";
import { sendEventMessages } from "../../../modules/event-handlers/send-event-messages"; import { sendEventMessages } from "../../../modules/event-handlers/send-event-messages";
import { createClient } from "../../../lib/create-graphql-client";
const OrderFulfilledWebhookPayload = gql` const OrderFulfilledWebhookPayload = gql`
${OrderDetailsFragmentDoc} ${OrderDetailsFragmentDoc}
@ -63,9 +64,13 @@ const handler: NextWebhookApiHandler<OrderFulfilledWebhookPayloadFragment> = asy
} }
const channel = order.channel.slug; const channel = order.channel.slug;
const client = createClient(authData.saleorApiUrl, async () =>
Promise.resolve({ token: authData.token })
);
await sendEventMessages({ await sendEventMessages({
authData, authData,
client,
channel, channel,
event: "ORDER_FULFILLED", event: "ORDER_FULFILLED",
payload: { order: payload.order }, payload: { order: payload.order },

View file

@ -7,6 +7,7 @@ import {
OrderFullyPaidWebhookPayloadFragment, OrderFullyPaidWebhookPayloadFragment,
} from "../../../../generated/graphql"; } from "../../../../generated/graphql";
import { sendEventMessages } from "../../../modules/event-handlers/send-event-messages"; import { sendEventMessages } from "../../../modules/event-handlers/send-event-messages";
import { createClient } from "../../../lib/create-graphql-client";
const OrderFullyPaidWebhookPayload = gql` const OrderFullyPaidWebhookPayload = gql`
${OrderDetailsFragmentDoc} ${OrderDetailsFragmentDoc}
@ -63,10 +64,14 @@ const handler: NextWebhookApiHandler<OrderFullyPaidWebhookPayloadFragment> = asy
} }
const channel = order.channel.slug; const channel = order.channel.slug;
const client = createClient(authData.saleorApiUrl, async () =>
Promise.resolve({ token: authData.token })
);
await sendEventMessages({ await sendEventMessages({
authData, authData,
channel, channel,
client,
event: "ORDER_FULLY_PAID", event: "ORDER_FULLY_PAID",
payload: { order: payload.order }, payload: { order: payload.order },
recipientEmail, recipientEmail,

View file

@ -3,7 +3,7 @@ import React from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { trpcClient } from "../../../../../modules/trpc/trpc-client"; import { trpcClient } from "../../../../../modules/trpc/trpc-client";
import { checkMessageEventType } from "../../../../../modules/event-handlers/check-message-event-type"; import { parseMessageEventType } from "../../../../../modules/event-handlers/parse-message-event-type";
import { ConfigurationPageBaseLayout } from "../../../../../modules/ui/configuration-page-base-layout"; import { ConfigurationPageBaseLayout } from "../../../../../modules/ui/configuration-page-base-layout";
import { EventConfigurationForm } from "../../../../../modules/mjml/configuration/ui/mjml-event-configuration-form"; import { EventConfigurationForm } from "../../../../../modules/mjml/configuration/ui/mjml-event-configuration-form";
import { LoadingIndicator } from "../../../../../modules/ui/loading-indicator"; import { LoadingIndicator } from "../../../../../modules/ui/loading-indicator";
@ -13,7 +13,7 @@ const EventConfigurationPage: NextPage = () => {
const configurationId = router.query.configurationId as string; const configurationId = router.query.configurationId as string;
const eventTypeFromQuery = router.query.eventType as string | undefined; const eventTypeFromQuery = router.query.eventType as string | undefined;
const eventType = checkMessageEventType(eventTypeFromQuery); const eventType = parseMessageEventType(eventTypeFromQuery);
const { const {
data: configuration, data: configuration,