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:
parent
2c0df91351
commit
a3636f73ef
21 changed files with 4658 additions and 4606 deletions
6
.changeset/old-dingos-hide.md
Normal file
6
.changeset/old-dingos-hide.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
"saleor-app-cms": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix CMS app issues
|
||||||
|
Check if CMS provider instance configuration is working
|
|
@ -62,7 +62,7 @@
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"jsdom": "^20.0.3",
|
"jsdom": "^20.0.3",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.7.1",
|
||||||
"typescript": "4.9",
|
"typescript": "5.0.4",
|
||||||
"vitest": "^0.30.1"
|
"vitest": "^0.30.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,47 @@ import {
|
||||||
ProductVariantUpdatedWebhookPayloadFragment,
|
ProductVariantUpdatedWebhookPayloadFragment,
|
||||||
WebhookProductVariantFragment,
|
WebhookProductVariantFragment,
|
||||||
} from "../../../../generated/graphql";
|
} from "../../../../generated/graphql";
|
||||||
import { CmsClientBatchOperations, CmsClientOperations, ProductResponseSuccess } from "../types";
|
import {
|
||||||
|
BaseResponse,
|
||||||
|
CmsClientBatchOperations,
|
||||||
|
CmsClientOperations,
|
||||||
|
ProductResponseSuccess,
|
||||||
|
} from "../types";
|
||||||
import { getCmsIdFromSaleorItem } from "./metadata";
|
import { getCmsIdFromSaleorItem } from "./metadata";
|
||||||
import { logger as pinoLogger } from "../../logger";
|
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 {
|
interface CmsClientOperationResult {
|
||||||
createdCmsId?: string;
|
createdCmsId?: string;
|
||||||
|
|
|
@ -31,7 +31,7 @@ export const providersConfig = {
|
||||||
name: "token",
|
name: "token",
|
||||||
label: "Token",
|
label: "Token",
|
||||||
helpText:
|
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,
|
required: true,
|
||||||
|
@ -45,14 +45,14 @@ export const providersConfig = {
|
||||||
name: "spaceId",
|
name: "spaceId",
|
||||||
label: "Space ID",
|
label: "Space ID",
|
||||||
helpText:
|
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,
|
required: true,
|
||||||
name: "contentId",
|
name: "contentId",
|
||||||
label: "Content ID",
|
label: "Content ID",
|
||||||
helpText:
|
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,
|
required: true,
|
||||||
|
@ -71,7 +71,7 @@ export const providersConfig = {
|
||||||
name: "apiRequestsPerSecond",
|
name: "apiRequestsPerSecond",
|
||||||
label: "API requests per second",
|
label: "API requests per second",
|
||||||
helpText:
|
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",
|
name: "contentTypeId",
|
||||||
label: "Content Type ID (plural)",
|
label: "Content Type ID (plural)",
|
||||||
helpText:
|
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({
|
export const strapiConfigSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
token: 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),
|
contentTypeId: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -157,15 +157,15 @@ export const contentfulConfigSchema = z.object({
|
||||||
spaceId: z.string().min(1),
|
spaceId: z.string().min(1),
|
||||||
locale: z.string().min(1),
|
locale: z.string().min(1),
|
||||||
contentId: z.string().min(1),
|
contentId: z.string().min(1),
|
||||||
baseUrl: z.string(),
|
baseUrl: z.string().url().optional().or(z.literal("")),
|
||||||
apiRequestsPerSecond: z.string(),
|
apiRequestsPerSecond: z.number().optional().or(z.literal("")),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const datocmsConfigSchema = z.object({
|
export const datocmsConfigSchema = z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
token: z.string().min(1),
|
token: z.string().min(1),
|
||||||
itemTypeId: z.string().min(1),
|
itemTypeId: z.number().min(1),
|
||||||
baseUrl: z.string(),
|
baseUrl: z.string().url().optional().or(z.literal("")),
|
||||||
environment: z.string(),
|
environment: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -101,6 +101,15 @@ const contentfulOperations: CreateOperations<ContentfulConfig> = (config) => {
|
||||||
|
|
||||||
const requestPerSecondLimit = Number(apiRequestsPerSecond || 7);
|
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> => {
|
const createProductInCMS = async (input: ProductInput): Promise<ContentfulResponse> => {
|
||||||
// Contentful API does not auto generate resource ID during creation, it has to be provided.
|
// Contentful API does not auto generate resource ID during creation, it has to be provided.
|
||||||
const resourceId = uuidv4();
|
const resourceId = uuidv4();
|
||||||
|
@ -203,6 +212,12 @@ const contentfulOperations: CreateOperations<ContentfulConfig> = (config) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
ping: async () => {
|
||||||
|
const response = await pingCMS();
|
||||||
|
logger.debug({ response }, "ping response");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
createProduct: async ({ input }) => {
|
createProduct: async ({ input }) => {
|
||||||
const result = await createProductInCMS(input);
|
const result = await createProductInCMS(input);
|
||||||
logger.debug({ result }, "createProduct result");
|
logger.debug({ result }, "createProduct result");
|
||||||
|
|
|
@ -50,10 +50,12 @@ const datocmsOperations: CreateOperations<DatocmsConfig> = (config) => {
|
||||||
|
|
||||||
const client = datocmsClient(config);
|
const client = datocmsClient(config);
|
||||||
|
|
||||||
|
const pingCMS = async () => client.users.findMe();
|
||||||
|
|
||||||
const createProductInCMS = async (input: ProductInput) =>
|
const createProductInCMS = async (input: ProductInput) =>
|
||||||
client.items.create({
|
client.items.create({
|
||||||
item_type: {
|
item_type: {
|
||||||
id: config.itemTypeId,
|
id: String(config.itemTypeId),
|
||||||
type: "item_type",
|
type: "item_type",
|
||||||
},
|
},
|
||||||
saleor_id: input.saleorId,
|
saleor_id: input.saleorId,
|
||||||
|
@ -91,6 +93,20 @@ const datocmsOperations: CreateOperations<DatocmsConfig> = (config) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
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 }) => {
|
createProduct: async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
const item = await createProductInCMS(input);
|
const item = await createProductInCMS(input);
|
||||||
|
|
|
@ -82,6 +82,14 @@ export const strapiOperations: CreateStrapiOperations = (config) => {
|
||||||
|
|
||||||
const { contentTypeId } = 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 createProductInCMS = async (input: ProductInput): Promise<StrapiResponse> => {
|
||||||
const body = transformInputToBody(input);
|
const body = transformInputToBody(input);
|
||||||
const response = await strapiFetch(`/${contentTypeId}`, config, {
|
const response = await strapiFetch(`/${contentTypeId}`, config, {
|
||||||
|
@ -120,6 +128,12 @@ export const strapiOperations: CreateStrapiOperations = (config) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
ping: async () => {
|
||||||
|
const response = await pingCMS();
|
||||||
|
logger.debug({ response }, "ping response");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
createProduct: async ({ input }) => {
|
createProduct: async ({ input }) => {
|
||||||
const result = await createProductInCMS(input);
|
const result = await createProductInCMS(input);
|
||||||
logger.debug({ result }, "createProduct result");
|
logger.debug({ result }, "createProduct result");
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { providersConfig } from "./config";
|
import { ProvidersSchema, providersConfig } from "./config";
|
||||||
|
|
||||||
export type ProductInput = Record<string, any> & {
|
export type ProductInput = Record<string, any> & {
|
||||||
saleorId: string;
|
saleorId: string;
|
||||||
|
@ -11,11 +11,13 @@ export type ProductInput = Record<string, any> & {
|
||||||
image?: string;
|
image?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type BaseResponse = { ok: boolean };
|
||||||
export type ProductResponseSuccess = { ok: true; data: { id: string; saleorId: string } };
|
export type ProductResponseSuccess = { ok: true; data: { id: string; saleorId: string } };
|
||||||
export type ProductResponseError = { ok: false; error: string };
|
export type ProductResponseError = { ok: false; error: string };
|
||||||
export type ProductResponse = ProductResponseSuccess | ProductResponseError;
|
export type ProductResponse = ProductResponseSuccess | ProductResponseError;
|
||||||
|
|
||||||
export type CmsOperations = {
|
export type CmsOperations = {
|
||||||
|
ping: () => Promise<BaseResponse>;
|
||||||
getProduct?: ({ id }: { id: string }) => Promise<Response>;
|
getProduct?: ({ id }: { id: string }) => Promise<Response>;
|
||||||
createProduct: ({ input }: { input: ProductInput }) => Promise<ProductResponse>;
|
createProduct: ({ input }: { input: ProductInput }) => Promise<ProductResponse>;
|
||||||
updateProduct: ({ id, input }: { id: string; input: ProductInput }) => Promise<Response | void>;
|
updateProduct: ({ id, input }: { id: string; input: ProductInput }) => Promise<Response | void>;
|
||||||
|
@ -40,17 +42,14 @@ export type CmsClientBatchOperations = {
|
||||||
operationType: keyof CmsBatchOperations;
|
operationType: keyof CmsBatchOperations;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetProviderTokens<TProviderName extends keyof typeof providersConfig> =
|
|
||||||
(typeof providersConfig)[TProviderName]["tokens"][number];
|
|
||||||
|
|
||||||
export type BaseConfig = {
|
export type BaseConfig = {
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// * Generates the config based on the data supplied in the `providersConfig` variable.
|
// * Generates the config based on the data supplied in the `providersConfig` variable.
|
||||||
export type CreateProviderConfig<TProviderName extends keyof typeof providersConfig> = Record<
|
export type CreateProviderConfig<TProviderName extends keyof typeof providersConfig> = Omit<
|
||||||
GetProviderTokens<TProviderName>["name"],
|
ProvidersSchema[TProviderName],
|
||||||
string
|
"id" | "providerName"
|
||||||
> &
|
> &
|
||||||
BaseConfig;
|
BaseConfig;
|
||||||
|
|
||||||
|
|
|
@ -41,11 +41,15 @@ export const Channels = () => {
|
||||||
}
|
}
|
||||||
}, [channels]);
|
}, [channels]);
|
||||||
|
|
||||||
const handleOnSyncCompleted = (providerInstanceId: string) => {
|
const handleOnSyncCompleted = (providerInstanceId: string, error?: string) => {
|
||||||
if (!activeChannel) {
|
if (!activeChannel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
saveChannel({
|
saveChannel({
|
||||||
...activeChannel,
|
...activeChannel,
|
||||||
requireSyncProviderInstances: activeChannel.requireSyncProviderInstances?.filter(
|
requireSyncProviderInstances: activeChannel.requireSyncProviderInstances?.filter(
|
||||||
|
|
|
@ -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 { SALEOR_API_URL_HEADER, SALEOR_AUTHORIZATION_BEARER_HEADER } from "@saleor/app-sdk/const";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { WebhookProductVariantFragment } from "../../../../generated/graphql";
|
import { WebhookProductVariantFragment } from "../../../../generated/graphql";
|
||||||
|
@ -21,9 +21,9 @@ interface UseProductsVariantsSyncHandlers {
|
||||||
|
|
||||||
export const useProductsVariantsSync = (
|
export const useProductsVariantsSync = (
|
||||||
channelSlug: string | null,
|
channelSlug: string | null,
|
||||||
onSyncCompleted: (providerInstanceId: string) => void
|
onSyncCompleted: (providerInstanceId: string, error?: string) => void
|
||||||
): UseProductsVariantsSyncHandlers => {
|
): UseProductsVariantsSyncHandlers => {
|
||||||
const { appBridgeState } = useAppBridge();
|
const { appBridge, appBridgeState } = useAppBridge();
|
||||||
|
|
||||||
const [startedProviderInstanceId, setStartedProviderInstanceId] = useState<string>();
|
const [startedProviderInstanceId, setStartedProviderInstanceId] = useState<string>();
|
||||||
const [startedOperation, setStartedOperation] = useState<ProductsVariantsSyncOperation>();
|
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(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
products.length <= currentProductIndex &&
|
products.length <= currentProductIndex &&
|
||||||
|
@ -85,13 +112,7 @@ export const useProductsVariantsSync = (
|
||||||
startedProviderInstanceId &&
|
startedProviderInstanceId &&
|
||||||
startedOperation
|
startedOperation
|
||||||
) {
|
) {
|
||||||
const completedProviderInstanceIdSync = startedProviderInstanceId;
|
completeSync(startedProviderInstanceId);
|
||||||
|
|
||||||
setStartedProviderInstanceId(undefined);
|
|
||||||
setStartedOperation(undefined);
|
|
||||||
setCurrentProductIndex(0);
|
|
||||||
|
|
||||||
onSyncCompleted(completedProviderInstanceIdSync);
|
|
||||||
}
|
}
|
||||||
}, [products.length, currentProductIndex, fetchCompleted]);
|
}, [products.length, currentProductIndex, fetchCompleted]);
|
||||||
|
|
||||||
|
@ -112,10 +133,19 @@ export const useProductsVariantsSync = (
|
||||||
const productsBatch = products.slice(productsBatchStartIndex, productsBatchEndIndex);
|
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);
|
// 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);
|
setIsImporting(false);
|
||||||
setCurrentProductIndex(productsBatchEndIndex);
|
|
||||||
})();
|
})();
|
||||||
}, [
|
}, [
|
||||||
startedProviderInstanceId,
|
startedProviderInstanceId,
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
|
@ -12,6 +12,7 @@ import {
|
||||||
} from "../../../lib/cms/config";
|
} from "../../../lib/cms/config";
|
||||||
import { Provider } from "../../providers/config";
|
import { Provider } from "../../providers/config";
|
||||||
import { AppMarkdownText } from "../../ui/app-markdown-text";
|
import { AppMarkdownText } from "../../ui/app-markdown-text";
|
||||||
|
import { ZodNumber } from "zod";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
footer: {
|
footer: {
|
||||||
|
@ -68,8 +69,6 @@ export const ProviderInstanceConfigurationForm = <TProvider extends CMSProviderS
|
||||||
}, [provider, providerInstance]);
|
}, [provider, providerInstance]);
|
||||||
|
|
||||||
const submitHandler = (values: SingleProviderSchema) => {
|
const submitHandler = (values: SingleProviderSchema) => {
|
||||||
console.log(values);
|
|
||||||
|
|
||||||
onSubmit(values);
|
onSubmit(values);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -111,15 +110,19 @@ export const ProviderInstanceConfigurationForm = <TProvider extends CMSProviderS
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
{fields.map((token) => (
|
{fields.map((token) => {
|
||||||
|
const isSecret = token.secret ? { type: "password" } : {};
|
||||||
|
|
||||||
|
return (
|
||||||
<Grid xs={12} item key={token.name}>
|
<Grid xs={12} item key={token.name}>
|
||||||
<TextField
|
<TextField
|
||||||
{...register(token.name as Path<ProvidersSchema[TProvider]>, {
|
{...register(token.name as Path<ProvidersSchema[TProvider]>, {
|
||||||
required: "required" in token && token.required,
|
required: "required" in token && token.required,
|
||||||
|
valueAsNumber:
|
||||||
|
schema.shape[token.name as keyof typeof schema.shape] instanceof ZodNumber,
|
||||||
})}
|
})}
|
||||||
// required={"required" in token && token.required}
|
{...isSecret}
|
||||||
label={token.label}
|
label={token.label}
|
||||||
type={token.secret ? "password" : "text"}
|
|
||||||
name={token.name}
|
name={token.name}
|
||||||
InputLabelProps={{
|
InputLabelProps={{
|
||||||
shrink: !!watch(token.name as Path<ProvidersSchema[TProvider]>),
|
shrink: !!watch(token.name as Path<ProvidersSchema[TProvider]>),
|
||||||
|
@ -139,7 +142,8 @@ export const ProviderInstanceConfigurationForm = <TProvider extends CMSProviderS
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
{providerInstance ? (
|
{providerInstance ? (
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<TextField
|
<TextField
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { ProviderInstanceConfigurationForm } from "./provider-instance-configura
|
||||||
import { Skeleton } from "@material-ui/lab";
|
import { Skeleton } from "@material-ui/lab";
|
||||||
import { ProvidersErrors, ProvidersLoading } from "./types";
|
import { ProvidersErrors, ProvidersLoading } from "./types";
|
||||||
import { getProviderByName, Provider } from "../../providers/config";
|
import { getProviderByName, Provider } from "../../providers/config";
|
||||||
|
import { ProviderInstancePingStatus } from "./hooks/usePingProviderInstance";
|
||||||
|
import { ProviderInstancePing } from "./provider-instance-ping";
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
radioLabel: {
|
radioLabel: {
|
||||||
|
@ -51,6 +53,12 @@ const useStyles = makeStyles((theme) => ({
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: 20,
|
padding: 20,
|
||||||
},
|
},
|
||||||
|
successStatus: {
|
||||||
|
color: theme.palette.type === "dark" ? theme.palette.success.light : theme.palette.success.dark,
|
||||||
|
},
|
||||||
|
errorStatus: {
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const ProviderInstanceConfigurationSkeleton = () => {
|
const ProviderInstanceConfigurationSkeleton = () => {
|
||||||
|
@ -94,6 +102,7 @@ interface ProviderInstanceConfigurationProps {
|
||||||
deleteProviderInstance: (providerInstance: SingleProviderSchema) => any;
|
deleteProviderInstance: (providerInstance: SingleProviderSchema) => any;
|
||||||
loading: ProvidersLoading;
|
loading: ProvidersLoading;
|
||||||
errors: ProvidersErrors;
|
errors: ProvidersErrors;
|
||||||
|
providerInstancePingStatus: ProviderInstancePingStatus | null;
|
||||||
onNewProviderRequest(): void;
|
onNewProviderRequest(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,6 +112,7 @@ export const ProviderInstanceConfiguration = ({
|
||||||
saveProviderInstance,
|
saveProviderInstance,
|
||||||
deleteProviderInstance,
|
deleteProviderInstance,
|
||||||
loading,
|
loading,
|
||||||
|
providerInstancePingStatus,
|
||||||
onNewProviderRequest,
|
onNewProviderRequest,
|
||||||
errors,
|
errors,
|
||||||
}: ProviderInstanceConfigurationProps) => {
|
}: ProviderInstanceConfigurationProps) => {
|
||||||
|
@ -124,6 +134,9 @@ export const ProviderInstanceConfiguration = ({
|
||||||
setSelectedProvider(provider);
|
setSelectedProvider(provider);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isPingStatusLoading =
|
||||||
|
providerInstancePingStatus?.providerInstanceId !== activeProviderInstance?.id;
|
||||||
|
|
||||||
if (loading.fetching || loading.saving) {
|
if (loading.fetching || loading.saving) {
|
||||||
return <ProviderInstanceConfigurationSkeleton />;
|
return <ProviderInstanceConfigurationSkeleton />;
|
||||||
}
|
}
|
||||||
|
@ -196,6 +209,15 @@ export const ProviderInstanceConfiguration = ({
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
{selectedProvider ? (
|
{selectedProvider ? (
|
||||||
<>
|
<>
|
||||||
|
{!newProviderInstance && (
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<ProviderInstancePing
|
||||||
|
loading={isPingStatusLoading}
|
||||||
|
status={providerInstancePingStatus}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<br />
|
<br />
|
||||||
<ProviderInstanceConfigurationForm
|
<ProviderInstanceConfigurationForm
|
||||||
provider={selectedProvider}
|
provider={selectedProvider}
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -8,6 +8,7 @@ import { Button, makeStyles } from "@saleor/macaw-ui";
|
||||||
import { ProviderInstancesSelect } from "./provider-instances-list";
|
import { ProviderInstancesSelect } from "./provider-instances-list";
|
||||||
import { Add } from "@material-ui/icons";
|
import { Add } from "@material-ui/icons";
|
||||||
import { useDashboardNotification } from "@saleor/apps-shared";
|
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||||
|
import { usePingProviderInstance } from "./hooks/usePingProviderInstance";
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
wrapper: {
|
wrapper: {
|
||||||
|
@ -26,6 +27,7 @@ export const ProviderInstances = () => {
|
||||||
const [newProviderInstance, setNewProviderInstance] = useState<SingleProviderSchema | null>(null);
|
const [newProviderInstance, setNewProviderInstance] = useState<SingleProviderSchema | null>(null);
|
||||||
|
|
||||||
const { notifySuccess } = useDashboardNotification();
|
const { notifySuccess } = useDashboardNotification();
|
||||||
|
const pingProviderInstanceOpts = usePingProviderInstance(activeProviderInstanceId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (providerInstances.length && !activeProviderInstanceId) {
|
if (providerInstances.length && !activeProviderInstanceId) {
|
||||||
|
@ -58,6 +60,9 @@ export const ProviderInstances = () => {
|
||||||
if (newProviderInstance && savedProviderInstance) {
|
if (newProviderInstance && savedProviderInstance) {
|
||||||
setActiveProviderInstanceId(savedProviderInstance.id);
|
setActiveProviderInstanceId(savedProviderInstance.id);
|
||||||
}
|
}
|
||||||
|
if (!newProviderInstance) {
|
||||||
|
pingProviderInstanceOpts.refresh();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const handleDeleteProviderInstance = async (providerInstance: SingleProviderSchema) => {
|
const handleDeleteProviderInstance = async (providerInstance: SingleProviderSchema) => {
|
||||||
await deleteProviderInstance(providerInstance);
|
await deleteProviderInstance(providerInstance);
|
||||||
|
@ -94,6 +99,7 @@ export const ProviderInstances = () => {
|
||||||
deleteProviderInstance={handleDeleteProviderInstance}
|
deleteProviderInstance={handleDeleteProviderInstance}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
|
providerInstancePingStatus={pingProviderInstanceOpts.result}
|
||||||
onNewProviderRequest={handleAddNewProviderInstance}
|
onNewProviderRequest={handleAddNewProviderInstance}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
70
apps/cms/src/pages/api/ping-provider-instance.ts
Normal file
70
apps/cms/src/pages/api/ping-provider-instance.ts
Normal 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"]);
|
|
@ -156,14 +156,26 @@ const handler: NextProtectedApiHandler = async (
|
||||||
})) || [],
|
})) || [],
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).json({
|
if (syncResult?.error) {
|
||||||
success: true,
|
logger.error({ error: syncResult.error }, "The sync result error.");
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
data: {
|
data: {
|
||||||
createdCMSIds: syncResult?.createdCmsIds || [],
|
createdCMSIds: syncResult?.createdCmsIds || [],
|
||||||
deletedCMSIds: syncResult?.deletedCmsIds || [],
|
deletedCMSIds: syncResult?.deletedCmsIds || [],
|
||||||
},
|
},
|
||||||
error: syncResult?.error,
|
error: syncResult?.error,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("The sync result success.");
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
createdCMSIds: syncResult?.createdCmsIds || [],
|
||||||
|
deletedCMSIds: syncResult?.deletedCmsIds || [],
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createProtectedHandler(handler, saleorApp.apl, ["MANAGE_APPS"]);
|
export default createProtectedHandler(handler, saleorApp.apl, ["MANAGE_APPS"]);
|
||||||
|
|
12
apps/cms/src/pages/api/webhooks/_utils.ts
Normal file
12
apps/cms/src/pages/api/webhooks/_utils.ts
Normal 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;
|
|
@ -11,6 +11,7 @@ import { createCmsOperations, executeCmsOperations, updateMetadata } from "../..
|
||||||
import { logger as pinoLogger } from "../../../lib/logger";
|
import { logger as pinoLogger } from "../../../lib/logger";
|
||||||
import { createClient } from "../../../lib/graphql";
|
import { createClient } from "../../../lib/graphql";
|
||||||
import { fetchProductVariantMetadata } from "../../../lib/metadata";
|
import { fetchProductVariantMetadata } from "../../../lib/metadata";
|
||||||
|
import { isAppWebhookIssuer } from "./_utils";
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
|
@ -21,6 +22,11 @@ export const config = {
|
||||||
export const ProductUpdatedWebhookPayload = gql`
|
export const ProductUpdatedWebhookPayload = gql`
|
||||||
${UntypedWebhookProductFragmentDoc}
|
${UntypedWebhookProductFragmentDoc}
|
||||||
fragment ProductUpdatedWebhookPayload on ProductUpdated {
|
fragment ProductUpdatedWebhookPayload on ProductUpdated {
|
||||||
|
issuingPrincipal {
|
||||||
|
... on App {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
product {
|
product {
|
||||||
...WebhookProduct
|
...WebhookProduct
|
||||||
}
|
}
|
||||||
|
@ -49,13 +55,19 @@ export const handler: NextWebhookApiHandler<ProductUpdatedWebhookPayloadFragment
|
||||||
res,
|
res,
|
||||||
context
|
context
|
||||||
) => {
|
) => {
|
||||||
const { product } = context.payload;
|
const { product, issuingPrincipal } = context.payload;
|
||||||
const { saleorApiUrl, token } = context.authData;
|
const { saleorApiUrl, token, appId } = context.authData;
|
||||||
|
|
||||||
const logger = pinoLogger.child({
|
const logger = pinoLogger.child({
|
||||||
product,
|
product,
|
||||||
});
|
});
|
||||||
logger.debug("Called webhook PRODUCT_UPDATED");
|
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) {
|
if (!product) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { createCmsOperations, executeCmsOperations, updateMetadata } from "../..
|
||||||
import { logger as pinoLogger } from "../../../lib/logger";
|
import { logger as pinoLogger } from "../../../lib/logger";
|
||||||
import { createClient } from "../../../lib/graphql";
|
import { createClient } from "../../../lib/graphql";
|
||||||
import { fetchProductVariantMetadata } from "../../../lib/metadata";
|
import { fetchProductVariantMetadata } from "../../../lib/metadata";
|
||||||
|
import { isAppWebhookIssuer } from "./_utils";
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
api: {
|
api: {
|
||||||
|
@ -21,6 +22,11 @@ export const config = {
|
||||||
export const ProductVariantUpdatedWebhookPayload = gql`
|
export const ProductVariantUpdatedWebhookPayload = gql`
|
||||||
${UntypedWebhookProductVariantFragmentDoc}
|
${UntypedWebhookProductVariantFragmentDoc}
|
||||||
fragment ProductVariantUpdatedWebhookPayload on ProductVariantUpdated {
|
fragment ProductVariantUpdatedWebhookPayload on ProductVariantUpdated {
|
||||||
|
issuingPrincipal {
|
||||||
|
... on App {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
productVariant {
|
productVariant {
|
||||||
...WebhookProductVariant
|
...WebhookProductVariant
|
||||||
}
|
}
|
||||||
|
@ -50,13 +56,19 @@ export const handler: NextWebhookApiHandler<ProductVariantUpdatedWebhookPayloadF
|
||||||
res,
|
res,
|
||||||
context
|
context
|
||||||
) => {
|
) => {
|
||||||
const { productVariant } = context.payload;
|
const { productVariant, issuingPrincipal } = context.payload;
|
||||||
const { saleorApiUrl, token } = context.authData;
|
const { saleorApiUrl, token, appId } = context.authData;
|
||||||
|
|
||||||
const logger = pinoLogger.child({
|
const logger = pinoLogger.child({
|
||||||
productVariant,
|
productVariant,
|
||||||
});
|
});
|
||||||
logger.debug("Called webhook PRODUCT_VARIANT_UPDATED");
|
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) {
|
if (!productVariant) {
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
|
|
8738
pnpm-lock.yaml
8738
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue