Fix CMS app issues (#386)

* Fix CMS app visual issues

* Add provider configuration instance ping status

* Skip update webhooks processing if issuing principal is this CMS app

* Fix provider configuration form validation

* Create old-dingos-hide.md
This commit is contained in:
Dawid 2023-04-18 18:46:28 +02:00 committed by GitHub
parent 2c0df91351
commit a3636f73ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 4658 additions and 4606 deletions

View file

@ -0,0 +1,6 @@
---
"saleor-app-cms": patch
---
Fix CMS app issues
Check if CMS provider instance configuration is working

View file

@ -62,7 +62,7 @@
"eslint-config-prettier": "^8.5.0",
"jsdom": "^20.0.3",
"prettier": "^2.7.1",
"typescript": "4.9",
"typescript": "5.0.4",
"vitest": "^0.30.1"
}
}

View file

@ -2,9 +2,47 @@ import {
ProductVariantUpdatedWebhookPayloadFragment,
WebhookProductVariantFragment,
} from "../../../../generated/graphql";
import { CmsClientBatchOperations, CmsClientOperations, ProductResponseSuccess } from "../types";
import {
BaseResponse,
CmsClientBatchOperations,
CmsClientOperations,
ProductResponseSuccess,
} from "../types";
import { getCmsIdFromSaleorItem } from "./metadata";
import { logger as pinoLogger } from "../../logger";
import { CMSProvider, cmsProviders } from "../providers";
import { ProviderInstanceSchema, providersSchemaSet } from "../config";
export const pingProviderInstance = async (
providerInstanceSettings: ProviderInstanceSchema
): Promise<BaseResponse> => {
const logger = pinoLogger.child({ providerInstanceSettings });
logger.debug("Ping provider instance called");
const provider = cmsProviders[
providerInstanceSettings.providerName as CMSProvider
] as (typeof cmsProviders)[keyof typeof cmsProviders];
const validation =
providersSchemaSet[providerInstanceSettings.providerName as CMSProvider].safeParse(
providerInstanceSettings
);
if (!validation.success) {
logger.error("The provider instance settings validation failed.", {
error: validation.error.message,
});
return { ok: false };
}
const config = validation.data;
const client = provider.create(config as any); // config without validation = providerInstanceSettings as any
const pingResult = await client.ping();
return pingResult;
};
interface CmsClientOperationResult {
createdCmsId?: string;

View file

@ -31,7 +31,7 @@ export const providersConfig = {
name: "token",
label: "Token",
helpText:
'You can find this in your Contentful project, go to Settings > API keys > Content management tokens > Generate personal token. More instructions at [Contentful "Authentication" documentation](https://www.contentful.com/developers/docs/references/authentication/).',
'You can find this in your Contentful project, go to Settings > API Keys > Content Management Tokens > Generate Personal Token. More instructions at [Contentful "Authentication" documentation](https://www.contentful.com/developers/docs/references/authentication/).',
},
{
required: true,
@ -45,14 +45,14 @@ export const providersConfig = {
name: "spaceId",
label: "Space ID",
helpText:
"You can find this in your Contentful project, go to settings > general settings.",
"You can find this in your Contentful project, go to Settings > General Settings.",
},
{
required: true,
name: "contentId",
label: "Content ID",
helpText:
"You can find this in your Contentful project, go to Content model > select model > Content type id.",
"You can find this in your Contentful project, go to Content Model > select Model > Content Type ID.",
},
{
required: true,
@ -71,7 +71,7 @@ export const providersConfig = {
name: "apiRequestsPerSecond",
label: "API requests per second",
helpText:
"API rate limits. Default 7. Used in bulk products variants sync. Higher rate limits may speed up a little products variants bulk sync. Higher rate limit may apply depending on different Contentful plan, learn more at https://www.contentful.com/developers/docs/references/content-management-api/#/introduction/api-rate-limits.",
"API rate limits. The default is 7. Used in bulk products variants sync. Higher rate limits may speed up a little products variants bulk sync. Higher rate limit may apply depending on different Contentful plan, learn more at https://www.contentful.com/developers/docs/references/content-management-api/#/introduction/api-rate-limits.",
},
],
},
@ -99,7 +99,7 @@ export const providersConfig = {
name: "contentTypeId",
label: "Content Type ID (plural)",
helpText:
'You can find this in your Strapi project, go to Content-Type Builder > select content type > click Edit > use API ID (Plural). More instructions at [Strapi "Editing content types" documentation](https://docs.strapi.io/user-docs/content-type-builder/managing-content-types#editing-content-types).',
'You can find this in your Strapi project, go to Content-Type Builder > select Content Type > click Edit > Use API ID (Plural). More instructions at [Strapi "Editing content types" documentation](https://docs.strapi.io/user-docs/content-type-builder/managing-content-types#editing-content-types).',
},
],
},
@ -146,7 +146,7 @@ export type DatocmsConfig = CreateProviderConfig<"datocms">;
export const strapiConfigSchema = z.object({
name: z.string().min(1),
token: z.string().min(1),
baseUrl: z.string().min(1),
baseUrl: z.string().url().min(1),
contentTypeId: z.string().min(1),
});
@ -157,15 +157,15 @@ export const contentfulConfigSchema = z.object({
spaceId: z.string().min(1),
locale: z.string().min(1),
contentId: z.string().min(1),
baseUrl: z.string(),
apiRequestsPerSecond: z.string(),
baseUrl: z.string().url().optional().or(z.literal("")),
apiRequestsPerSecond: z.number().optional().or(z.literal("")),
});
export const datocmsConfigSchema = z.object({
name: z.string().min(1),
token: z.string().min(1),
itemTypeId: z.string().min(1),
baseUrl: z.string(),
itemTypeId: z.number().min(1),
baseUrl: z.string().url().optional().or(z.literal("")),
environment: z.string(),
});

View file

@ -101,6 +101,15 @@ const contentfulOperations: CreateOperations<ContentfulConfig> = (config) => {
const requestPerSecondLimit = Number(apiRequestsPerSecond || 7);
const pingCMS = async () => {
const endpoint = `/spaces/${spaceId}`;
const response = await contentfulFetch(endpoint, config, { method: "GET" });
logger.debug({ response }, "pingCMS response");
return {
ok: response.ok,
};
};
const createProductInCMS = async (input: ProductInput): Promise<ContentfulResponse> => {
// Contentful API does not auto generate resource ID during creation, it has to be provided.
const resourceId = uuidv4();
@ -203,6 +212,12 @@ const contentfulOperations: CreateOperations<ContentfulConfig> = (config) => {
};
return {
ping: async () => {
const response = await pingCMS();
logger.debug({ response }, "ping response");
return response;
},
createProduct: async ({ input }) => {
const result = await createProductInCMS(input);
logger.debug({ result }, "createProduct result");

View file

@ -50,10 +50,12 @@ const datocmsOperations: CreateOperations<DatocmsConfig> = (config) => {
const client = datocmsClient(config);
const pingCMS = async () => client.users.findMe();
const createProductInCMS = async (input: ProductInput) =>
client.items.create({
item_type: {
id: config.itemTypeId,
id: String(config.itemTypeId),
type: "item_type",
},
saleor_id: input.saleorId,
@ -91,6 +93,20 @@ const datocmsOperations: CreateOperations<DatocmsConfig> = (config) => {
});
return {
ping: async () => {
try {
const response = await pingCMS();
logger.debug({ response }, "ping response");
if (!response.id) {
throw new Error();
}
return { ok: true };
} catch (error) {
return { ok: false };
}
},
createProduct: async ({ input }) => {
try {
const item = await createProductInCMS(input);

View file

@ -82,6 +82,14 @@ export const strapiOperations: CreateStrapiOperations = (config) => {
const { contentTypeId } = config;
const pingCMS = async () => {
const response = await strapiFetch(`/${contentTypeId}`, config, {
method: "GET",
});
logger.debug({ response }, "pingCMS response");
return { ok: response.ok };
};
const createProductInCMS = async (input: ProductInput): Promise<StrapiResponse> => {
const body = transformInputToBody(input);
const response = await strapiFetch(`/${contentTypeId}`, config, {
@ -120,6 +128,12 @@ export const strapiOperations: CreateStrapiOperations = (config) => {
};
return {
ping: async () => {
const response = await pingCMS();
logger.debug({ response }, "ping response");
return response;
},
createProduct: async ({ input }) => {
const result = await createProductInCMS(input);
logger.debug({ result }, "createProduct result");

View file

@ -1,5 +1,5 @@
import { z } from "zod";
import { providersConfig } from "./config";
import { ProvidersSchema, providersConfig } from "./config";
export type ProductInput = Record<string, any> & {
saleorId: string;
@ -11,11 +11,13 @@ export type ProductInput = Record<string, any> & {
image?: string;
};
export type BaseResponse = { ok: boolean };
export type ProductResponseSuccess = { ok: true; data: { id: string; saleorId: string } };
export type ProductResponseError = { ok: false; error: string };
export type ProductResponse = ProductResponseSuccess | ProductResponseError;
export type CmsOperations = {
ping: () => Promise<BaseResponse>;
getProduct?: ({ id }: { id: string }) => Promise<Response>;
createProduct: ({ input }: { input: ProductInput }) => Promise<ProductResponse>;
updateProduct: ({ id, input }: { id: string; input: ProductInput }) => Promise<Response | void>;
@ -40,17 +42,14 @@ export type CmsClientBatchOperations = {
operationType: keyof CmsBatchOperations;
};
export type GetProviderTokens<TProviderName extends keyof typeof providersConfig> =
(typeof providersConfig)[TProviderName]["tokens"][number];
export type BaseConfig = {
name: string;
};
// * Generates the config based on the data supplied in the `providersConfig` variable.
export type CreateProviderConfig<TProviderName extends keyof typeof providersConfig> = Record<
GetProviderTokens<TProviderName>["name"],
string
export type CreateProviderConfig<TProviderName extends keyof typeof providersConfig> = Omit<
ProvidersSchema[TProviderName],
"id" | "providerName"
> &
BaseConfig;

View file

@ -41,11 +41,15 @@ export const Channels = () => {
}
}, [channels]);
const handleOnSyncCompleted = (providerInstanceId: string) => {
const handleOnSyncCompleted = (providerInstanceId: string, error?: string) => {
if (!activeChannel) {
return;
}
if (error) {
return;
}
saveChannel({
...activeChannel,
requireSyncProviderInstances: activeChannel.requireSyncProviderInstances?.filter(

View file

@ -1,4 +1,4 @@
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const";
import { useCallback, useEffect, useState } from "react";
import { WebhookProductVariantFragment } from "../../../../generated/graphql";
@ -21,9 +21,9 @@ interface UseProductsVariantsSyncHandlers {
export const useProductsVariantsSync = (
channelSlug: string | null,
onSyncCompleted: (providerInstanceId: string) => void
onSyncCompleted: (providerInstanceId: string, error?: string) => void
): UseProductsVariantsSyncHandlers => {
const { appBridgeState } = useAppBridge();
const { appBridge, appBridgeState } = useAppBridge();
const [startedProviderInstanceId, setStartedProviderInstanceId] = useState<string>();
const [startedOperation, setStartedOperation] = useState<ProductsVariantsSyncOperation>();
@ -78,6 +78,33 @@ export const useProductsVariantsSync = (
}
};
const completeSync = (completedProviderInstanceIdSync: string, error?: string) => {
setStartedProviderInstanceId(undefined);
setStartedOperation(undefined);
setCurrentProductIndex(0);
onSyncCompleted(completedProviderInstanceIdSync, error);
if (error) {
appBridge?.dispatch(
actions.Notification({
title: "Error",
status: "error",
text: "Error syncing products variants",
apiMessage: error,
})
);
} else {
appBridge?.dispatch(
actions.Notification({
title: "Success",
status: "success",
text: "Products variants sync completed successfully",
})
);
}
};
useEffect(() => {
if (
products.length <= currentProductIndex &&
@ -85,13 +112,7 @@ export const useProductsVariantsSync = (
startedProviderInstanceId &&
startedOperation
) {
const completedProviderInstanceIdSync = startedProviderInstanceId;
setStartedProviderInstanceId(undefined);
setStartedOperation(undefined);
setCurrentProductIndex(0);
onSyncCompleted(completedProviderInstanceIdSync);
completeSync(startedProviderInstanceId);
}
}, [products.length, currentProductIndex, fetchCompleted]);
@ -112,10 +133,19 @@ export const useProductsVariantsSync = (
const productsBatch = products.slice(productsBatchStartIndex, productsBatchEndIndex);
// temporary solution, cannot use directly backend methods without fetch, due to non-browser Node dependency, like await cmsProvider.updatedBatchProducts(productsBatch);
await syncFetch(startedProviderInstanceId, startedOperation, productsBatch);
const syncResult = await syncFetch(
startedProviderInstanceId,
startedOperation,
productsBatch
);
if (syncResult.error) {
completeSync(startedProviderInstanceId, syncResult.error);
} else {
setCurrentProductIndex(productsBatchEndIndex);
}
setIsImporting(false);
setCurrentProductIndex(productsBatchEndIndex);
})();
}, [
startedProviderInstanceId,

View file

@ -0,0 +1,70 @@
import { useEffect, useState } from "react";
import { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const";
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
const getCurrentTime = () => new Date().toLocaleTimeString();
export interface ProviderInstancePingStatus {
providerInstanceId: string;
success: boolean;
time: string;
}
export interface PingProviderInstanceOpts {
result: ProviderInstancePingStatus | null;
refresh: () => void;
}
export const usePingProviderInstance = (providerInstanceId: string | null) => {
const { appBridgeState } = useAppBridge();
const [result, setResult] = useState<null | ProviderInstancePingStatus>(null);
const ping = async (providerInstanceId: string): Promise<ProviderInstancePingStatus> => {
try {
const pingResponse = await fetch("/api/ping-provider-instance", {
method: "POST",
headers: [
["content-type", "application/json"],
[SALEOR_API_URL_HEADER, appBridgeState?.saleorApiUrl!],
[SALEOR_AUTHORIZATION_BEARER_HEADER, appBridgeState?.token!],
],
body: JSON.stringify({
providerInstanceId,
}),
});
const pingResult = await pingResponse.json();
return {
providerInstanceId,
success: pingResult.success,
time: getCurrentTime(),
};
} catch (error) {
console.error("useProductsVariantsSync syncFetch error", error);
return {
providerInstanceId,
success: false,
time: getCurrentTime(),
};
}
};
const refresh = () => {
setResult(null);
if (providerInstanceId) {
ping(providerInstanceId).then((result) => setResult(result));
}
};
useEffect(() => {
refresh();
}, [providerInstanceId]);
return {
result,
refresh,
};
};

View file

@ -12,6 +12,7 @@ import {
} from "../../../lib/cms/config";
import { Provider } from "../../providers/config";
import { AppMarkdownText } from "../../ui/app-markdown-text";
import { ZodNumber } from "zod";
const useStyles = makeStyles((theme) => ({
footer: {
@ -68,8 +69,6 @@ export const ProviderInstanceConfigurationForm = <TProvider extends CMSProviderS
}, [provider, providerInstance]);
const submitHandler = (values: SingleProviderSchema) => {
console.log(values);
onSubmit(values);
};
@ -111,35 +110,40 @@ export const ProviderInstanceConfigurationForm = <TProvider extends CMSProviderS
}
/>
</Grid>
{fields.map((token) => (
<Grid xs={12} item key={token.name}>
<TextField
{...register(token.name as Path<ProvidersSchema[TProvider]>, {
required: "required" in token && token.required,
})}
// required={"required" in token && token.required}
label={token.label}
type={token.secret ? "password" : "text"}
name={token.name}
InputLabelProps={{
shrink: !!watch(token.name as Path<ProvidersSchema[TProvider]>),
}}
fullWidth
// @ts-ignore TODO: fix errors typing
error={!!errors[token.name as Path<ProvidersSchema[TProvider]>]}
helperText={
<>
{errors[token.name as Path<ProvidersSchema[TProvider]>]?.message ||
("helpText" in token && (
<AppMarkdownText>{`${getOptionalText(token)}${
token.helpText
}`}</AppMarkdownText>
))}
</>
}
/>
</Grid>
))}
{fields.map((token) => {
const isSecret = token.secret ? { type: "password" } : {};
return (
<Grid xs={12} item key={token.name}>
<TextField
{...register(token.name as Path<ProvidersSchema[TProvider]>, {
required: "required" in token && token.required,
valueAsNumber:
schema.shape[token.name as keyof typeof schema.shape] instanceof ZodNumber,
})}
{...isSecret}
label={token.label}
name={token.name}
InputLabelProps={{
shrink: !!watch(token.name as Path<ProvidersSchema[TProvider]>),
}}
fullWidth
// @ts-ignore TODO: fix errors typing
error={!!errors[token.name as Path<ProvidersSchema[TProvider]>]}
helperText={
<>
{errors[token.name as Path<ProvidersSchema[TProvider]>]?.message ||
("helpText" in token && (
<AppMarkdownText>{`${getOptionalText(token)}${
token.helpText
}`}</AppMarkdownText>
))}
</>
}
/>
</Grid>
);
})}
{providerInstance ? (
<Grid item xs={12}>
<TextField

View file

@ -8,6 +8,8 @@ import { ProviderInstanceConfigurationForm } from "./provider-instance-configura
import { Skeleton } from "@material-ui/lab";
import { ProvidersErrors, ProvidersLoading } from "./types";
import { getProviderByName, Provider } from "../../providers/config";
import { ProviderInstancePingStatus } from "./hooks/usePingProviderInstance";
import { ProviderInstancePing } from "./provider-instance-ping";
const useStyles = makeStyles((theme) => ({
radioLabel: {
@ -51,6 +53,12 @@ const useStyles = makeStyles((theme) => ({
borderRadius: 8,
padding: 20,
},
successStatus: {
color: theme.palette.type === "dark" ? theme.palette.success.light : theme.palette.success.dark,
},
errorStatus: {
color: theme.palette.error.main,
},
}));
const ProviderInstanceConfigurationSkeleton = () => {
@ -94,6 +102,7 @@ interface ProviderInstanceConfigurationProps {
deleteProviderInstance: (providerInstance: SingleProviderSchema) => any;
loading: ProvidersLoading;
errors: ProvidersErrors;
providerInstancePingStatus: ProviderInstancePingStatus | null;
onNewProviderRequest(): void;
}
@ -103,6 +112,7 @@ export const ProviderInstanceConfiguration = ({
saveProviderInstance,
deleteProviderInstance,
loading,
providerInstancePingStatus,
onNewProviderRequest,
errors,
}: ProviderInstanceConfigurationProps) => {
@ -124,6 +134,9 @@ export const ProviderInstanceConfiguration = ({
setSelectedProvider(provider);
};
const isPingStatusLoading =
providerInstancePingStatus?.providerInstanceId !== activeProviderInstance?.id;
if (loading.fetching || loading.saving) {
return <ProviderInstanceConfigurationSkeleton />;
}
@ -196,6 +209,15 @@ export const ProviderInstanceConfiguration = ({
</RadioGroup>
{selectedProvider ? (
<>
{!newProviderInstance && (
<>
<br />
<ProviderInstancePing
loading={isPingStatusLoading}
status={providerInstancePingStatus}
/>
</>
)}
<br />
<ProviderInstanceConfigurationForm
provider={selectedProvider}

View file

@ -0,0 +1,46 @@
import { Typography } from "@material-ui/core";
import { Skeleton } from "@material-ui/lab";
import { makeStyles } from "@saleor/macaw-ui";
import { ProviderInstancePingStatus } from "./hooks/usePingProviderInstance";
import clsx from "clsx";
const useStyles = makeStyles((theme) => ({
successStatus: {
color: theme.palette.type === "dark" ? theme.palette.success.light : theme.palette.success.dark,
},
errorStatus: {
color: theme.palette.error.main,
},
}));
interface ProviderInstancePingStatusProps {
loading: boolean;
status: ProviderInstancePingStatus | null;
}
export const ProviderInstancePing = ({ loading, status }: ProviderInstancePingStatusProps) => {
const styles = useStyles();
const parseProviderInstanceStatus = () => {
const statusText = status?.success ? "Ok" : "Error";
const checkTime = `(check time ${status?.time})`;
return `Configuration connection: ${statusText} ${checkTime}`;
};
if (loading) {
return <Skeleton />;
}
return (
<Typography
variant="body1"
className={clsx({
[styles.successStatus]: status?.success,
[styles.errorStatus]: !status?.success,
})}
>
{parseProviderInstanceStatus()}
</Typography>
);
};

View file

@ -8,6 +8,7 @@ import { Button, makeStyles } from "@saleor/macaw-ui";
import { ProviderInstancesSelect } from "./provider-instances-list";
import { Add } from "@material-ui/icons";
import { useDashboardNotification } from "@saleor/apps-shared";
import { usePingProviderInstance } from "./hooks/usePingProviderInstance";
const useStyles = makeStyles({
wrapper: {
@ -26,6 +27,7 @@ export const ProviderInstances = () => {
const [newProviderInstance, setNewProviderInstance] = useState<SingleProviderSchema | null>(null);
const { notifySuccess } = useDashboardNotification();
const pingProviderInstanceOpts = usePingProviderInstance(activeProviderInstanceId);
useEffect(() => {
if (providerInstances.length && !activeProviderInstanceId) {
@ -58,6 +60,9 @@ export const ProviderInstances = () => {
if (newProviderInstance && savedProviderInstance) {
setActiveProviderInstanceId(savedProviderInstance.id);
}
if (!newProviderInstance) {
pingProviderInstanceOpts.refresh();
}
};
const handleDeleteProviderInstance = async (providerInstance: SingleProviderSchema) => {
await deleteProviderInstance(providerInstance);
@ -94,6 +99,7 @@ export const ProviderInstances = () => {
deleteProviderInstance={handleDeleteProviderInstance}
loading={loading}
errors={errors}
providerInstancePingStatus={pingProviderInstanceOpts.result}
onNewProviderRequest={handleAddNewProviderInstance}
/>
</div>

View file

@ -0,0 +1,70 @@
import { NextProtectedApiHandler, createProtectedHandler } from "@saleor/app-sdk/handlers/next";
import { saleorApp } from "../../../saleor-app";
import type { NextApiRequest, NextApiResponse } from "next";
import { logger as pinoLogger } from "../../lib/logger";
import { createClient } from "../../lib/graphql";
import { createSettingsManager } from "../../lib/metadata";
import { getProviderInstancesSettings } from "../../lib/cms/client/settings";
import { pingProviderInstance } from "../../lib/cms/client/clients-execution";
export interface ProviderInstancePingApiPayload {
providerInstanceId: string;
}
export interface ProviderInstancePingApiResponse {
success: boolean;
}
const handler: NextProtectedApiHandler = async (
req: NextApiRequest,
res: NextApiResponse<ProviderInstancePingApiResponse>,
context
) => {
const { authData } = context;
const { providerInstanceId } = req.body as ProviderInstancePingApiPayload;
const logger = pinoLogger.child({
endpoint: "ping-provider-instance",
});
logger.debug({ providerInstanceId }, "Called endpoint ping-provider-instance");
if (req.method !== "POST") {
return res.status(405).json({
success: false,
});
}
if (!providerInstanceId) {
return res.status(400).json({
success: false,
});
}
const client = createClient(authData.saleorApiUrl, async () => ({
token: authData.token,
}));
const settingsManager = createSettingsManager(client);
const providerInstancesSettingsParsed = await getProviderInstancesSettings(settingsManager);
const providerInstanceSettings = providerInstancesSettingsParsed[providerInstanceId];
if (!providerInstanceSettings) {
return res.status(400).json({
success: false,
});
}
const pingResult = await pingProviderInstance(providerInstanceSettings);
if (!pingResult.ok) {
return res.status(400).json({
success: false,
});
}
return res.status(200).json({
success: true,
});
};
export default createProtectedHandler(handler, saleorApp.apl, ["MANAGE_APPS"]);

View file

@ -156,13 +156,25 @@ const handler: NextProtectedApiHandler = async (
})) || [],
});
if (syncResult?.error) {
logger.error({ error: syncResult.error }, "The sync result error.");
return res.status(500).json({
success: false,
data: {
createdCMSIds: syncResult?.createdCmsIds || [],
deletedCMSIds: syncResult?.deletedCmsIds || [],
},
error: syncResult?.error,
});
}
logger.debug("The sync result success.");
return res.status(200).json({
success: true,
data: {
createdCMSIds: syncResult?.createdCmsIds || [],
deletedCMSIds: syncResult?.deletedCmsIds || [],
},
error: syncResult?.error,
});
};

View file

@ -0,0 +1,12 @@
interface UnknownIssuingPrincipal {
__typename?: string;
}
export const isAppWebhookIssuer = <T extends UnknownIssuingPrincipal | null | undefined>(
issuingPrincipal: T,
appId: string
) =>
issuingPrincipal &&
(!issuingPrincipal.__typename || issuingPrincipal.__typename === "App") &&
"id" in issuingPrincipal &&
issuingPrincipal.id === appId;

View file

@ -11,6 +11,7 @@ import { createCmsOperations, executeCmsOperations, updateMetadata } from "../..
import { logger as pinoLogger } from "../../../lib/logger";
import { createClient } from "../../../lib/graphql";
import { fetchProductVariantMetadata } from "../../../lib/metadata";
import { isAppWebhookIssuer } from "./_utils";
export const config = {
api: {
@ -21,6 +22,11 @@ export const config = {
export const ProductUpdatedWebhookPayload = gql`
${UntypedWebhookProductFragmentDoc}
fragment ProductUpdatedWebhookPayload on ProductUpdated {
issuingPrincipal {
... on App {
id
}
}
product {
...WebhookProduct
}
@ -49,13 +55,19 @@ export const handler: NextWebhookApiHandler<ProductUpdatedWebhookPayloadFragment
res,
context
) => {
const { product } = context.payload;
const { saleorApiUrl, token } = context.authData;
const { product, issuingPrincipal } = context.payload;
const { saleorApiUrl, token, appId } = context.authData;
const logger = pinoLogger.child({
product,
});
logger.debug("Called webhook PRODUCT_UPDATED");
logger.debug({ issuingPrincipal }, "Issuing principal");
if (isAppWebhookIssuer(issuingPrincipal, appId)) {
logger.debug("Issuing principal is the same as the app. Skipping webhook processing.");
return res.status(200).end();
}
if (!product) {
return res.status(500).json({

View file

@ -11,6 +11,7 @@ import { createCmsOperations, executeCmsOperations, updateMetadata } from "../..
import { logger as pinoLogger } from "../../../lib/logger";
import { createClient } from "../../../lib/graphql";
import { fetchProductVariantMetadata } from "../../../lib/metadata";
import { isAppWebhookIssuer } from "./_utils";
export const config = {
api: {
@ -21,6 +22,11 @@ export const config = {
export const ProductVariantUpdatedWebhookPayload = gql`
${UntypedWebhookProductVariantFragmentDoc}
fragment ProductVariantUpdatedWebhookPayload on ProductVariantUpdated {
issuingPrincipal {
... on App {
id
}
}
productVariant {
...WebhookProductVariant
}
@ -50,13 +56,19 @@ export const handler: NextWebhookApiHandler<ProductVariantUpdatedWebhookPayloadF
res,
context
) => {
const { productVariant } = context.payload;
const { saleorApiUrl, token } = context.authData;
const { productVariant, issuingPrincipal } = context.payload;
const { saleorApiUrl, token, appId } = context.authData;
const logger = pinoLogger.child({
productVariant,
});
logger.debug("Called webhook PRODUCT_VARIANT_UPDATED");
logger.debug({ issuingPrincipal }, "Issuing principal");
if (isAppWebhookIssuer(issuingPrincipal, appId)) {
logger.debug("Issuing principal is the same as the app. Skipping webhook processing.");
return res.status(200).end();
}
if (!productVariant) {
return res.status(500).json({

File diff suppressed because it is too large Load diff