Search: Add media and stock flag (#874)
* Add media and stock flag * Add UI for updating the queries * Add update info to changelog
This commit is contained in:
parent
3002354c5e
commit
69fe973121
27 changed files with 616 additions and 62 deletions
9
.changeset/eight-oranges-appear.md
Normal file
9
.changeset/eight-oranges-appear.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
"saleor-app-search": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Extended search model with two new fields:
|
||||||
|
- `inStock`: returns true if stock is available. To keep this up to date, the application subscribes to `ProductVariantOutOfStock` and `ProductVariantBackInStock` events
|
||||||
|
- `media`: array of objects containing URL and type of given file (video or image)
|
||||||
|
|
||||||
|
To enable those features, open App configuration page and click on the `Update webhooks` button. To update existing data start a data import.
|
|
@ -1,6 +1,7 @@
|
||||||
fragment OwnWebhook on Webhook {
|
fragment OwnWebhook on Webhook {
|
||||||
id
|
id
|
||||||
isActive
|
isActive
|
||||||
|
name
|
||||||
asyncEvents {
|
asyncEvents {
|
||||||
name
|
name
|
||||||
eventType
|
eventType
|
||||||
|
|
|
@ -24,6 +24,10 @@ fragment ProductData on Product {
|
||||||
thumbnail {
|
thumbnail {
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
media{
|
||||||
|
url
|
||||||
|
type
|
||||||
|
}
|
||||||
attributes {
|
attributes {
|
||||||
...ProductAttributesData
|
...ProductAttributesData
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ fragment ProductVariantData on ProductVariant {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
quantityAvailable
|
||||||
channelListings {
|
channelListings {
|
||||||
id
|
id
|
||||||
channel {
|
channel {
|
||||||
|
|
9
apps/search/graphql/mutations/CreateWebhook.graphql
Normal file
9
apps/search/graphql/mutations/CreateWebhook.graphql
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
mutation CreateWebhook($input: WebhookCreateInput!) {
|
||||||
|
webhookCreate(input:$input){
|
||||||
|
errors{
|
||||||
|
code
|
||||||
|
field
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
apps/search/graphql/mutations/RemoveWebhook.graphql
Normal file
8
apps/search/graphql/mutations/RemoveWebhook.graphql
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
mutation RemoveWebhook($id: ID!) {
|
||||||
|
webhookDelete(id: $id){
|
||||||
|
errors{
|
||||||
|
field
|
||||||
|
message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
subscription ProductVariantBackInStock {
|
||||||
|
event {
|
||||||
|
__typename
|
||||||
|
... on ProductVariantBackInStock {
|
||||||
|
__typename
|
||||||
|
productVariant {
|
||||||
|
...ProductVariantWebhookPayload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recipient {
|
||||||
|
webhooks {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
subscription ProductVariantOutOfStock {
|
||||||
|
event {
|
||||||
|
__typename
|
||||||
|
... on ProductVariantOutOfStock {
|
||||||
|
__typename
|
||||||
|
productVariant {
|
||||||
|
...ProductVariantWebhookPayload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recipient {
|
||||||
|
webhooks {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { AlgoliaSearchProvider } from "../lib/algolia/algoliaSearchProvider";
|
import { AlgoliaSearchProvider } from "../lib/algolia/algoliaSearchProvider";
|
||||||
import { useConfiguration } from "../lib/configuration";
|
import { useConfiguration } from "../lib/configuration";
|
||||||
import { Products, useQueryAllProducts } from "./useQueryAllProducts";
|
import { Products, useQueryAllProducts } from "./useQueryAllProducts";
|
||||||
import { useWebhooksStatus } from "../lib/useWebhooksStatus";
|
|
||||||
|
|
||||||
const BATCH_SIZE = 100;
|
const BATCH_SIZE = 100;
|
||||||
|
|
||||||
|
|
|
@ -14,33 +14,6 @@ export const MainInstructions = ({ children, ...props }: PropsWithBox<{}>) => {
|
||||||
Once the App is configured, you will be able to perform initial index of your existing
|
Once the App is configured, you will be able to perform initial index of your existing
|
||||||
Saleor database.
|
Saleor database.
|
||||||
</Text>
|
</Text>
|
||||||
<Text as="p">
|
|
||||||
The app supports following{" "}
|
|
||||||
<TextLink href={SALEOR_EVENTS_DOCS_URL} newTab>
|
|
||||||
events
|
|
||||||
</TextLink>{" "}
|
|
||||||
that will synchronize Algolia in the background:
|
|
||||||
</Text>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<code>- PRODUCT_CREATED</code>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code>- PRODUCT_UPDATED</code>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code>- PRODUCT_DELETED</code>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code>- PRODUCT_VARIANT_CREATED</code>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code>- PRODUCT_VARIANT_UPDATED</code>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code>- PRODUCT_VARIANT_DELETED</code>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,21 +1,25 @@
|
||||||
import { Accordion, Box, Text } from "@saleor/macaw-ui/next";
|
import { Accordion, Box, Button, Text } from "@saleor/macaw-ui/next";
|
||||||
import { EventDeliveryStatusEnum } from "../../generated/graphql";
|
import { EventDeliveryStatusEnum } from "../../generated/graphql";
|
||||||
import { useWebhooksStatus } from "../lib/useWebhooksStatus";
|
import { useWebhooksStatus } from "../lib/useWebhooksStatus";
|
||||||
import { SemanticChip } from "@saleor/apps-ui";
|
import { SemanticChip } from "@saleor/apps-ui";
|
||||||
|
import { useWebhooksUpdateMutation } from "../lib/useWebhooksUpdate";
|
||||||
|
|
||||||
export const WebhooksStatus = () => {
|
export const WebhooksStatus = () => {
|
||||||
const { data: webhooksData } = useWebhooksStatus();
|
const { data } = useWebhooksStatus();
|
||||||
|
const updateWebhooksMutation = useWebhooksUpdateMutation();
|
||||||
|
|
||||||
if (!webhooksData) {
|
if (!data) {
|
||||||
return <Text>Loading...</Text>;
|
return <Text>Loading...</Text>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const webhooksData = data.webhooks;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Accordion display={"grid"} gap={1.5}>
|
<Accordion display={"grid"} gap={1.5}>
|
||||||
{webhooksData.map((webhook) => {
|
{webhooksData.map((webhook) => {
|
||||||
const failedEventDeliveries = webhook.eventDeliveries?.edges?.filter(
|
const failedEventDeliveries = webhook.eventDeliveries?.edges?.filter(
|
||||||
(e) => e.node.status === EventDeliveryStatusEnum.Failed
|
(e) => e.node.status === EventDeliveryStatusEnum.Failed,
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasFailedDeliveries = failedEventDeliveries && failedEventDeliveries.length > 0;
|
const hasFailedDeliveries = failedEventDeliveries && failedEventDeliveries.length > 0;
|
||||||
|
@ -87,6 +91,24 @@ export const WebhooksStatus = () => {
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
{data.isUpdateNeeded && (
|
||||||
|
<Box
|
||||||
|
borderTopStyle="solid"
|
||||||
|
borderWidth={1}
|
||||||
|
borderColor="neutralDefault"
|
||||||
|
display="flex"
|
||||||
|
gap={4}
|
||||||
|
flexDirection="row"
|
||||||
|
justifyContent="flex-end"
|
||||||
|
alignItems="center"
|
||||||
|
paddingTop={4}
|
||||||
|
>
|
||||||
|
<Text variant="caption">
|
||||||
|
New app version installed. Update the webhooks and run product import again.
|
||||||
|
</Text>
|
||||||
|
<Button onClick={() => updateWebhooksMutation.mutate()}>Update webhooks</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useWebhooksStatus } from "../lib/useWebhooksStatus";
|
||||||
export const WebhooksStatusInstructions = () => {
|
export const WebhooksStatusInstructions = () => {
|
||||||
const { data: webhooksData } = useWebhooksStatus();
|
const { data: webhooksData } = useWebhooksStatus();
|
||||||
|
|
||||||
if (webhooksData && webhooksData.some((w) => !w.isActive)) {
|
if (webhooksData && webhooksData.webhooks.some((w) => !w.isActive)) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Text as={"p"} marginBottom={1.5}>
|
<Text as={"p"} marginBottom={1.5}>
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import { Client, OperationResult } from "urql";
|
import { Client, OperationResult } from "urql";
|
||||||
import {
|
import {
|
||||||
|
CreateWebhookDocument,
|
||||||
|
CreateWebhookMutationVariables,
|
||||||
DisableWebhookDocument,
|
DisableWebhookDocument,
|
||||||
EnableWebhookDocument,
|
EnableWebhookDocument,
|
||||||
FetchOwnWebhooksDocument,
|
FetchOwnWebhooksDocument,
|
||||||
|
RemoveWebhookDocument,
|
||||||
|
WebhookEventTypeEnum,
|
||||||
} from "../../generated/graphql";
|
} from "../../generated/graphql";
|
||||||
import { createLogger } from "../lib/logger";
|
import { createLogger } from "../lib/logger";
|
||||||
|
import { appWebhooks } from "../../webhooks";
|
||||||
|
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
service: "WebhookActivityTogglerService",
|
service: "WebhookActivityTogglerService",
|
||||||
|
@ -14,11 +19,19 @@ export interface IWebhooksActivityClient {
|
||||||
fetchAppWebhooksIDs(id: string): Promise<string[]>;
|
fetchAppWebhooksIDs(id: string): Promise<string[]>;
|
||||||
disableSingleWebhook(id: string): Promise<void>;
|
disableSingleWebhook(id: string): Promise<void>;
|
||||||
enableSingleWebhook(id: string): Promise<void>;
|
enableSingleWebhook(id: string): Promise<void>;
|
||||||
|
removeSingleWebhook(id: string): Promise<void>;
|
||||||
|
createWebhook(input: CreateWebhookMutationVariables["input"]): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IRecreateWebhooksArgs {
|
||||||
|
baseUrl: string;
|
||||||
|
enableWebhooks: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IWebhookActivityTogglerService {
|
export interface IWebhookActivityTogglerService {
|
||||||
disableOwnWebhooks(webhooksIdsParam?: string[]): Promise<void>;
|
disableOwnWebhooks(webhooksIdsParam?: string[]): Promise<void>;
|
||||||
enableOwnWebhooks(): Promise<void>;
|
enableOwnWebhooks(): Promise<void>;
|
||||||
|
recreateOwnWebhooks(args: IRecreateWebhooksArgs): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebhooksActivityClient implements IWebhooksActivityClient {
|
export class WebhooksActivityClient implements IWebhooksActivityClient {
|
||||||
|
@ -30,7 +43,7 @@ export class WebhooksActivityClient implements IWebhooksActivityClient {
|
||||||
{
|
{
|
||||||
error: r.error,
|
error: r.error,
|
||||||
},
|
},
|
||||||
"Error disabling webhook"
|
"Error disabling webhook",
|
||||||
);
|
);
|
||||||
throw new Error("Error disabling webhook");
|
throw new Error("Error disabling webhook");
|
||||||
}
|
}
|
||||||
|
@ -76,6 +89,32 @@ export class WebhooksActivityClient implements IWebhooksActivityClient {
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createWebhook(input: CreateWebhookMutationVariables["input"]): Promise<void> {
|
||||||
|
return this.client
|
||||||
|
.mutation(CreateWebhookDocument, {
|
||||||
|
input,
|
||||||
|
})
|
||||||
|
.toPromise()
|
||||||
|
.then((r) => {
|
||||||
|
this.handleOperationFailure(r);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSingleWebhook(id: string): Promise<void> {
|
||||||
|
return this.client
|
||||||
|
.mutation(RemoveWebhookDocument, {
|
||||||
|
id,
|
||||||
|
})
|
||||||
|
.toPromise()
|
||||||
|
.then((r) => {
|
||||||
|
this.handleOperationFailure(r);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebhookActivityTogglerService implements IWebhookActivityTogglerService {
|
export class WebhookActivityTogglerService implements IWebhookActivityTogglerService {
|
||||||
|
@ -89,7 +128,7 @@ export class WebhookActivityTogglerService implements IWebhookActivityTogglerSer
|
||||||
private client: Pick<Client, "query" | "mutation">,
|
private client: Pick<Client, "query" | "mutation">,
|
||||||
options?: {
|
options?: {
|
||||||
WebhooksClient: IWebhooksActivityClient;
|
WebhooksClient: IWebhooksActivityClient;
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
this.webhooksClient = options?.WebhooksClient ?? new WebhooksActivityClient(this.client);
|
this.webhooksClient = options?.WebhooksClient ?? new WebhooksActivityClient(this.client);
|
||||||
}
|
}
|
||||||
|
@ -121,4 +160,30 @@ export class WebhookActivityTogglerService implements IWebhookActivityTogglerSer
|
||||||
|
|
||||||
await Promise.all(webhooksIds.map((id) => this.webhooksClient.enableSingleWebhook(id)));
|
await Promise.all(webhooksIds.map((id) => this.webhooksClient.enableSingleWebhook(id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async recreateOwnWebhooks({ baseUrl, enableWebhooks }: IRecreateWebhooksArgs) {
|
||||||
|
const webhooksIds = await this.webhooksClient.fetchAppWebhooksIDs(this.ownAppId);
|
||||||
|
|
||||||
|
if (!webhooksIds) {
|
||||||
|
throw new Error("Failed fetching webhooks");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Removing old webhooks");
|
||||||
|
await Promise.all(webhooksIds.map((id) => this.webhooksClient.removeSingleWebhook(id)));
|
||||||
|
logger.debug("Creating new webhooks");
|
||||||
|
await Promise.all(
|
||||||
|
appWebhooks.map((webhook) => {
|
||||||
|
const manifest = webhook.getWebhookManifest(baseUrl);
|
||||||
|
|
||||||
|
return this.webhooksClient.createWebhook({
|
||||||
|
events: manifest.asyncEvents as WebhookEventTypeEnum[],
|
||||||
|
targetUrl: manifest.targetUrl,
|
||||||
|
name: manifest.name,
|
||||||
|
query: manifest.query,
|
||||||
|
isActive: enableWebhooks,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
logger.debug("Done creating new webhooks");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ type PartialChannelListing = {
|
||||||
|
|
||||||
export function channelListingToAlgoliaIndexId(
|
export function channelListingToAlgoliaIndexId(
|
||||||
channelListing: PartialChannelListing,
|
channelListing: PartialChannelListing,
|
||||||
indexNamePrefix: string | undefined
|
indexNamePrefix: string | undefined,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Index name should not start with . (dot)
|
* Index name should not start with . (dot)
|
||||||
|
@ -113,6 +113,10 @@ export function productAndVariantToAlgolia({
|
||||||
|
|
||||||
const listing = variant.channelListings?.find((l) => l.channel.slug === channel);
|
const listing = variant.channelListings?.find((l) => l.channel.slug === channel);
|
||||||
|
|
||||||
|
const inStock = !!variant.quantityAvailable;
|
||||||
|
|
||||||
|
const media = variant.product.media?.map((m) => ({ url: m.url, type: m.type })) || [];
|
||||||
|
|
||||||
const document = {
|
const document = {
|
||||||
objectID: productAndVariantToObjectID(variant),
|
objectID: productAndVariantToObjectID(variant),
|
||||||
productId: product.id,
|
productId: product.id,
|
||||||
|
@ -121,10 +125,12 @@ export function productAndVariantToAlgolia({
|
||||||
productName: product.name,
|
productName: product.name,
|
||||||
variantName: variant.name,
|
variantName: variant.name,
|
||||||
attributes,
|
attributes,
|
||||||
|
media,
|
||||||
description: safeParseJson(product.description),
|
description: safeParseJson(product.description),
|
||||||
slug: product.slug,
|
slug: product.slug,
|
||||||
thumbnail: product.thumbnail?.url,
|
thumbnail: product.thumbnail?.url,
|
||||||
grossPrice: listing?.price?.amount,
|
grossPrice: listing?.price?.amount,
|
||||||
|
inStock,
|
||||||
categories: categoryHierarchicalFacets(variant),
|
categories: categoryHierarchicalFacets(variant),
|
||||||
collections: product.collections?.map((collection) => collection.name) || [],
|
collections: product.collections?.map((collection) => collection.name) || [],
|
||||||
metadata: metadataToAlgoliaAttribute(variant.product.metadata),
|
metadata: metadataToAlgoliaAttribute(variant.product.metadata),
|
||||||
|
|
46
apps/search/src/lib/algolia/is-configured.test.ts
Normal file
46
apps/search/src/lib/algolia/is-configured.test.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { isConfigured } from "./is-configured";
|
||||||
|
|
||||||
|
describe("isConfigured", () => {
|
||||||
|
describe("Valid configurations", () => {
|
||||||
|
it("Returns true, when configuration has all fields filled", () => {
|
||||||
|
expect(
|
||||||
|
isConfigured({
|
||||||
|
configuration: { appId: "appId", indexNamePrefix: "prefix", secretKey: "secretKey" },
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
it("Returns true, when optional index name is not set", () => {
|
||||||
|
expect(
|
||||||
|
isConfigured({
|
||||||
|
configuration: {
|
||||||
|
appId: "appId",
|
||||||
|
indexNamePrefix: undefined,
|
||||||
|
secretKey: "secretKey",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("Invalid configurations", () => {
|
||||||
|
it("Returns false, when empty configuration is used", () => {
|
||||||
|
expect(isConfigured({ configuration: undefined })).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Returns false, when app id is missing", () => {
|
||||||
|
expect(
|
||||||
|
isConfigured({
|
||||||
|
configuration: { appId: undefined, indexNamePrefix: "prefix", secretKey: "secretKey" },
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Returns false, when app id is missing", () => {
|
||||||
|
expect(
|
||||||
|
isConfigured({
|
||||||
|
configuration: { appId: "appId", indexNamePrefix: "prefix", secretKey: undefined },
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
13
apps/search/src/lib/algolia/is-configured.ts
Normal file
13
apps/search/src/lib/algolia/is-configured.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { AppConfigurationFields, AppConfigurationSchema } from "../../domain/configuration";
|
||||||
|
|
||||||
|
interface isConfiguredArgs {
|
||||||
|
configuration: Partial<AppConfigurationFields> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if the app configuration is set up
|
||||||
|
export const isConfigured = ({ configuration }: isConfiguredArgs) => {
|
||||||
|
if (!configuration) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return AppConfigurationSchema.safeParse(configuration).success;
|
||||||
|
};
|
29
apps/search/src/lib/algolia/is-webhook-update-needed.test.ts
Normal file
29
apps/search/src/lib/algolia/is-webhook-update-needed.test.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { isWebhookUpdateNeeded } from "./is-webhook-update-needed";
|
||||||
|
|
||||||
|
describe("isWebhookUpdateNeeded", () => {
|
||||||
|
it("Returns false, when all webhooks are installed", () => {
|
||||||
|
expect(
|
||||||
|
isWebhookUpdateNeeded({
|
||||||
|
existingWebhookNames: [
|
||||||
|
"PRODUCT_CREATED webhook",
|
||||||
|
"PRODUCT_UPDATED webhook",
|
||||||
|
"PRODUCT_DELETED webhook",
|
||||||
|
"PRODUCT_VARIANT_CREATED webhook",
|
||||||
|
"PRODUCT_VARIANT_UPDATED webhook",
|
||||||
|
"PRODUCT_VARIANT_DELETED webhook",
|
||||||
|
"PRODUCT_VARIANT_BACK_IN_STOCK webhook",
|
||||||
|
"PRODUCT_VARIANT_OUT_OF_STOCK webhook",
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Returns true, when passed list of names is not contain all of the hooks", () => {
|
||||||
|
expect(
|
||||||
|
isWebhookUpdateNeeded({
|
||||||
|
existingWebhookNames: [],
|
||||||
|
}),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
11
apps/search/src/lib/algolia/is-webhook-update-needed.ts
Normal file
11
apps/search/src/lib/algolia/is-webhook-update-needed.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { appWebhooks } from "../../../webhooks";
|
||||||
|
|
||||||
|
interface isWebhookUpdateNeededArgs {
|
||||||
|
existingWebhookNames: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isWebhookUpdateNeeded = ({ existingWebhookNames }: isWebhookUpdateNeededArgs) => {
|
||||||
|
const notInstalledWebhooks = appWebhooks.filter((w) => !existingWebhookNames.includes(w.name));
|
||||||
|
|
||||||
|
return !!notInstalledWebhooks.length;
|
||||||
|
};
|
15
apps/search/src/lib/getBaseUrl.ts
Normal file
15
apps/search/src/lib/getBaseUrl.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/**
|
||||||
|
* Extracts the app's url from headers from the response.
|
||||||
|
*/
|
||||||
|
export const getBaseUrl = (headers: { [name: string]: string | string[] | undefined }): string => {
|
||||||
|
const { host, "x-forwarded-proto": xForwardedProto = "http" } = headers;
|
||||||
|
|
||||||
|
const xForwardedProtos = Array.isArray(xForwardedProto)
|
||||||
|
? xForwardedProto.join(",")
|
||||||
|
: xForwardedProto;
|
||||||
|
const protocols = xForwardedProtos.split(",");
|
||||||
|
// prefer https over other protocols
|
||||||
|
const protocol = protocols.find((el) => el === "https") || protocols[0];
|
||||||
|
|
||||||
|
return `${protocol}://${host}`;
|
||||||
|
};
|
|
@ -1,7 +1,7 @@
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAuthenticatedFetch } from "@saleor/app-sdk/app-bridge";
|
import { useAuthenticatedFetch } from "@saleor/app-sdk/app-bridge";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { OwnWebhookFragment } from "../../generated/graphql";
|
import { WebhooksStatusResponse } from "../pages/api/webhooks-status";
|
||||||
|
|
||||||
export const useWebhooksStatus = () => {
|
export const useWebhooksStatus = () => {
|
||||||
const fetch: typeof window.fetch = useAuthenticatedFetch();
|
const fetch: typeof window.fetch = useAuthenticatedFetch();
|
||||||
|
@ -13,7 +13,7 @@ export const useWebhooksStatus = () => {
|
||||||
*/
|
*/
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return useQuery<OwnWebhookFragment[]>({
|
return useQuery<WebhooksStatusResponse>({
|
||||||
queryKey: ["webhooks-status"],
|
queryKey: ["webhooks-status"],
|
||||||
queryFn: fetchFn,
|
queryFn: fetchFn,
|
||||||
});
|
});
|
||||||
|
|
28
apps/search/src/lib/useWebhooksUpdate.tsx
Normal file
28
apps/search/src/lib/useWebhooksUpdate.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useAuthenticatedFetch } from "@saleor/app-sdk/app-bridge";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import { useDashboardNotification } from "@saleor/apps-shared";
|
||||||
|
|
||||||
|
export const useWebhooksUpdateMutation = () => {
|
||||||
|
const fetch: typeof window.fetch = useAuthenticatedFetch();
|
||||||
|
const { notifyError, notifySuccess } = useDashboardNotification();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const mutationFn = useCallback(() => {
|
||||||
|
return fetch("/api/recreate-webhooks", { method: "POST" }).then((resp) => {
|
||||||
|
if (resp.ok) {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["webhooks-status"] });
|
||||||
|
notifySuccess("Webhooks has been updated");
|
||||||
|
} else {
|
||||||
|
notifyError("Webhooks update failed");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/**
|
||||||
|
* fetch from SDK is not wrapped with memo todo
|
||||||
|
*/
|
||||||
|
}, [fetch, notifyError, notifySuccess, queryClient]);
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn,
|
||||||
|
});
|
||||||
|
};
|
|
@ -2,12 +2,7 @@ import { createManifestHandler } from "@saleor/app-sdk/handlers/next";
|
||||||
import { AppManifest } from "@saleor/app-sdk/types";
|
import { AppManifest } from "@saleor/app-sdk/types";
|
||||||
|
|
||||||
import packageJson from "../../../package.json";
|
import packageJson from "../../../package.json";
|
||||||
import { webhookProductCreated } from "./webhooks/saleor/product_created";
|
import { appWebhooks } from "../../../webhooks";
|
||||||
import { webhookProductDeleted } from "./webhooks/saleor/product_deleted";
|
|
||||||
import { webhookProductUpdated } from "./webhooks/saleor/product_updated";
|
|
||||||
import { webhookProductVariantCreated } from "./webhooks/saleor/product_variant_created";
|
|
||||||
import { webhookProductVariantDeleted } from "./webhooks/saleor/product_variant_deleted";
|
|
||||||
import { webhookProductVariantUpdated } from "./webhooks/saleor/product_variant_updated";
|
|
||||||
|
|
||||||
export default createManifestHandler({
|
export default createManifestHandler({
|
||||||
async manifestFactory({ appBaseUrl }) {
|
async manifestFactory({ appBaseUrl }) {
|
||||||
|
@ -44,19 +39,7 @@ export default createManifestHandler({
|
||||||
supportUrl: "https://github.com/saleor/apps/discussions",
|
supportUrl: "https://github.com/saleor/apps/discussions",
|
||||||
tokenTargetUrl: `${apiBaseURL}/api/register`,
|
tokenTargetUrl: `${apiBaseURL}/api/register`,
|
||||||
version: packageJson.version,
|
version: packageJson.version,
|
||||||
webhooks: [
|
webhooks: appWebhooks.map((w) => w.getWebhookManifest(apiBaseURL)),
|
||||||
/**
|
|
||||||
* Configure webhooks here. They will be created in Saleor during installation
|
|
||||||
* Read more
|
|
||||||
* https://docs.saleor.io/docs/3.x/developer/api-reference/objects/webhook
|
|
||||||
*/
|
|
||||||
webhookProductCreated.getWebhookManifest(apiBaseURL),
|
|
||||||
webhookProductDeleted.getWebhookManifest(apiBaseURL),
|
|
||||||
webhookProductUpdated.getWebhookManifest(apiBaseURL),
|
|
||||||
webhookProductVariantCreated.getWebhookManifest(apiBaseURL),
|
|
||||||
webhookProductVariantDeleted.getWebhookManifest(apiBaseURL),
|
|
||||||
webhookProductVariantUpdated.getWebhookManifest(apiBaseURL),
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return manifest;
|
return manifest;
|
||||||
|
|
87
apps/search/src/pages/api/recreate-webhooks.ts
Normal file
87
apps/search/src/pages/api/recreate-webhooks.ts
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import { createProtectedHandler, NextProtectedApiHandler } from "@saleor/app-sdk/handlers/next";
|
||||||
|
import { saleorApp } from "../../../saleor-app";
|
||||||
|
import { createSettingsManager } from "../../lib/metadata";
|
||||||
|
import {
|
||||||
|
IWebhookActivityTogglerService,
|
||||||
|
WebhookActivityTogglerService,
|
||||||
|
} from "../../domain/WebhookActivityToggler.service";
|
||||||
|
import { createLogger } from "../../lib/logger";
|
||||||
|
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
||||||
|
import { createGraphQLClient } from "@saleor/apps-shared";
|
||||||
|
import { Client } from "urql";
|
||||||
|
import { getBaseUrl } from "../../lib/getBaseUrl";
|
||||||
|
import { isConfigured } from "../../lib/algolia/is-configured";
|
||||||
|
|
||||||
|
const logger = createLogger({
|
||||||
|
service: "recreateWebhooksHandler",
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple dependency injection - factory injects all services, in tests everything can be configured without mocks
|
||||||
|
*/
|
||||||
|
type FactoryProps = {
|
||||||
|
settingsManagerFactory: (client: Client) => SettingsManager;
|
||||||
|
webhookActivityTogglerFactory: (appId: string, client: Client) => IWebhookActivityTogglerService;
|
||||||
|
graphqlClientFactory: (saleorApiUrl: string, token: string) => Client;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const recreateWebhooksHandlerFactory =
|
||||||
|
({
|
||||||
|
settingsManagerFactory,
|
||||||
|
webhookActivityTogglerFactory,
|
||||||
|
graphqlClientFactory,
|
||||||
|
}: FactoryProps): NextProtectedApiHandler =>
|
||||||
|
async (req, res, { authData }) => {
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
logger.debug("Request method is different than POST, returning 405");
|
||||||
|
return res.status(405).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Fetching settings");
|
||||||
|
const client = graphqlClientFactory(authData.saleorApiUrl, authData.token);
|
||||||
|
const webhooksToggler = webhookActivityTogglerFactory(authData.appId, client);
|
||||||
|
const settingsManager = settingsManagerFactory(client);
|
||||||
|
|
||||||
|
const domain = new URL(authData.saleorApiUrl).host;
|
||||||
|
|
||||||
|
const [secretKey, appId] = await Promise.all([
|
||||||
|
settingsManager.get("secretKey", domain),
|
||||||
|
settingsManager.get("appId", domain),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const settings = { secretKey, appId };
|
||||||
|
|
||||||
|
logger.debug(settings, "fetched settings");
|
||||||
|
|
||||||
|
const baseUrl = getBaseUrl(req.headers);
|
||||||
|
const enableWebhooks = isConfigured({
|
||||||
|
configuration: {
|
||||||
|
appId: appId,
|
||||||
|
secretKey: secretKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug("Running webhooks recreation");
|
||||||
|
await webhooksToggler.recreateOwnWebhooks({ baseUrl: baseUrl, enableWebhooks });
|
||||||
|
logger.debug("Webhooks recreated");
|
||||||
|
return res.status(200).end();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(e);
|
||||||
|
return res.status(500).end();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createProtectedHandler(
|
||||||
|
recreateWebhooksHandlerFactory({
|
||||||
|
settingsManagerFactory: createSettingsManager,
|
||||||
|
webhookActivityTogglerFactory: function (appId, client) {
|
||||||
|
return new WebhookActivityTogglerService(appId, client);
|
||||||
|
},
|
||||||
|
graphqlClientFactory(saleorApiUrl: string, token: string) {
|
||||||
|
return createGraphQLClient({ saleorApiUrl, token });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
saleorApp.apl,
|
||||||
|
[],
|
||||||
|
);
|
|
@ -1,6 +1,6 @@
|
||||||
import { createProtectedHandler, NextProtectedApiHandler } from "@saleor/app-sdk/handlers/next";
|
import { createProtectedHandler, NextProtectedApiHandler } from "@saleor/app-sdk/handlers/next";
|
||||||
import { saleorApp } from "../../../saleor-app";
|
import { saleorApp } from "../../../saleor-app";
|
||||||
import { FetchOwnWebhooksDocument } from "../../../generated/graphql";
|
import { FetchOwnWebhooksDocument, OwnWebhookFragment } from "../../../generated/graphql";
|
||||||
import { AlgoliaSearchProvider } from "../../lib/algolia/algoliaSearchProvider";
|
import { AlgoliaSearchProvider } from "../../lib/algolia/algoliaSearchProvider";
|
||||||
import { createSettingsManager } from "../../lib/metadata";
|
import { createSettingsManager } from "../../lib/metadata";
|
||||||
import {
|
import {
|
||||||
|
@ -12,6 +12,7 @@ import { SettingsManager } from "@saleor/app-sdk/settings-manager";
|
||||||
import { SearchProvider } from "../../lib/searchProvider";
|
import { SearchProvider } from "../../lib/searchProvider";
|
||||||
import { createGraphQLClient } from "@saleor/apps-shared";
|
import { createGraphQLClient } from "@saleor/apps-shared";
|
||||||
import { Client } from "urql";
|
import { Client } from "urql";
|
||||||
|
import { isWebhookUpdateNeeded } from "../../lib/algolia/is-webhook-update-needed";
|
||||||
|
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
service: "webhooksStatusHandler",
|
service: "webhooksStatusHandler",
|
||||||
|
@ -27,13 +28,18 @@ type FactoryProps = {
|
||||||
graphqlClientFactory: (saleorApiUrl: string, token: string) => Client;
|
graphqlClientFactory: (saleorApiUrl: string, token: string) => Client;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WebhooksStatusResponse = {
|
||||||
|
webhooks: OwnWebhookFragment[];
|
||||||
|
isUpdateNeeded: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export const webhooksStatusHandlerFactory =
|
export const webhooksStatusHandlerFactory =
|
||||||
({
|
({
|
||||||
settingsManagerFactory,
|
settingsManagerFactory,
|
||||||
webhookActivityTogglerFactory,
|
webhookActivityTogglerFactory,
|
||||||
algoliaSearchProviderFactory,
|
algoliaSearchProviderFactory,
|
||||||
graphqlClientFactory,
|
graphqlClientFactory,
|
||||||
}: FactoryProps): NextProtectedApiHandler =>
|
}: FactoryProps): NextProtectedApiHandler<WebhooksStatusResponse> =>
|
||||||
async (req, res, { authData }) => {
|
async (req, res, { authData }) => {
|
||||||
/**
|
/**
|
||||||
* Initialize services
|
* Initialize services
|
||||||
|
@ -93,7 +99,14 @@ export const webhooksStatusHandlerFactory =
|
||||||
return res.status(500).end();
|
return res.status(500).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json(webhooks);
|
const isUpdateNeeded = isWebhookUpdateNeeded({
|
||||||
|
existingWebhookNames: webhooks.map((w) => w.name),
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
webhooks,
|
||||||
|
isUpdateNeeded,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return res.status(500).end();
|
return res.status(500).end();
|
||||||
|
@ -114,5 +127,5 @@ export default createProtectedHandler(
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
saleorApp.apl,
|
saleorApp.apl,
|
||||||
[]
|
[],
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||||
|
import { createGraphQLClient } from "@saleor/apps-shared";
|
||||||
|
import {
|
||||||
|
ProductVariantBackInStock,
|
||||||
|
ProductVariantBackInStockDocument,
|
||||||
|
} from "../../../../../generated/graphql";
|
||||||
|
import { saleorApp } from "../../../../../saleor-app";
|
||||||
|
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
|
||||||
|
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
|
||||||
|
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
|
||||||
|
import { createLogger } from "../../../../lib/logger";
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
api: {
|
||||||
|
bodyParser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const webhookProductVariantBackInStock = new SaleorAsyncWebhook<ProductVariantBackInStock>({
|
||||||
|
webhookPath: "api/webhooks/saleor/product_variant_back_in_stock",
|
||||||
|
event: "PRODUCT_VARIANT_BACK_IN_STOCK",
|
||||||
|
apl: saleorApp.apl,
|
||||||
|
query: ProductVariantBackInStockDocument,
|
||||||
|
/**
|
||||||
|
* Webhook is disabled by default. Will be enabled by the app when configuration succeeds
|
||||||
|
*/
|
||||||
|
isActive: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const logger = createLogger({
|
||||||
|
service: "webhookProductVariantBackInStockWebhookHandler",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const handler: NextWebhookApiHandler<ProductVariantBackInStock> = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
context,
|
||||||
|
) => {
|
||||||
|
const { event, authData } = context;
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`New event ${event} (${context.payload?.__typename}) from the ${authData.domain} domain has been received!`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { settings, errors } = await getAlgoliaConfiguration({ authData });
|
||||||
|
|
||||||
|
if (errors?.length || !settings) {
|
||||||
|
logger.warn("Aborting due to lack of settings");
|
||||||
|
logger.debug(errors);
|
||||||
|
|
||||||
|
return res.status(400).json({
|
||||||
|
message: errors[0].message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchProvider = new AlgoliaSearchProvider({
|
||||||
|
appId: settings.appId,
|
||||||
|
apiKey: settings.secretKey,
|
||||||
|
indexNamePrefix: settings.indexNamePrefix,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { productVariant } = context.payload;
|
||||||
|
|
||||||
|
if (productVariant) {
|
||||||
|
try {
|
||||||
|
await searchProvider.updateProductVariant(productVariant);
|
||||||
|
} catch (e) {
|
||||||
|
logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled");
|
||||||
|
|
||||||
|
const webhooksToggler = new WebhookActivityTogglerService(
|
||||||
|
authData.appId,
|
||||||
|
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.trace("Will disable webhooks");
|
||||||
|
|
||||||
|
await webhooksToggler.disableOwnWebhooks(
|
||||||
|
context.payload.recipient?.webhooks?.map((w) => w.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.trace("Webhooks disabling operation finished");
|
||||||
|
|
||||||
|
return res.status(500).send("Operation failed, webhooks are disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).end();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default webhookProductVariantBackInStock.createHandler(handler);
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||||
|
import { createGraphQLClient } from "@saleor/apps-shared";
|
||||||
|
import {
|
||||||
|
ProductVariantOutOfStock,
|
||||||
|
ProductVariantOutOfStockDocument,
|
||||||
|
} from "../../../../../generated/graphql";
|
||||||
|
import { saleorApp } from "../../../../../saleor-app";
|
||||||
|
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
|
||||||
|
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
|
||||||
|
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
|
||||||
|
import { createLogger } from "../../../../lib/logger";
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
api: {
|
||||||
|
bodyParser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const webhookProductVariantOutOfStock = new SaleorAsyncWebhook<ProductVariantOutOfStock>({
|
||||||
|
webhookPath: "api/webhooks/saleor/product_variant_out_of_stock",
|
||||||
|
event: "PRODUCT_VARIANT_OUT_OF_STOCK",
|
||||||
|
apl: saleorApp.apl,
|
||||||
|
query: ProductVariantOutOfStockDocument,
|
||||||
|
/**
|
||||||
|
* Webhook is disabled by default. Will be enabled by the app when configuration succeeds
|
||||||
|
*/
|
||||||
|
isActive: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const logger = createLogger({
|
||||||
|
service: "webhookProductVariantOutOfStockWebhookHandler",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const handler: NextWebhookApiHandler<ProductVariantOutOfStock> = async (
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
context,
|
||||||
|
) => {
|
||||||
|
const { event, authData } = context;
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`New event ${event} (${context.payload?.__typename}) from the ${authData.domain} domain has been received!`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { settings, errors } = await getAlgoliaConfiguration({ authData });
|
||||||
|
|
||||||
|
if (errors?.length || !settings) {
|
||||||
|
logger.warn("Aborting due to lack of settings");
|
||||||
|
logger.debug(errors);
|
||||||
|
|
||||||
|
return res.status(400).json({
|
||||||
|
message: errors[0].message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchProvider = new AlgoliaSearchProvider({
|
||||||
|
appId: settings.appId,
|
||||||
|
apiKey: settings.secretKey,
|
||||||
|
indexNamePrefix: settings.indexNamePrefix,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { productVariant } = context.payload;
|
||||||
|
|
||||||
|
if (productVariant) {
|
||||||
|
try {
|
||||||
|
await searchProvider.updateProductVariant(productVariant);
|
||||||
|
} catch (e) {
|
||||||
|
logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled");
|
||||||
|
|
||||||
|
const webhooksToggler = new WebhookActivityTogglerService(
|
||||||
|
authData.appId,
|
||||||
|
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.trace("Will disable webhooks");
|
||||||
|
|
||||||
|
await webhooksToggler.disableOwnWebhooks(
|
||||||
|
context.payload.recipient?.webhooks?.map((w) => w.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.trace("Webhooks disabling operation finished");
|
||||||
|
|
||||||
|
return res.status(500).send("Operation failed, webhooks are disabled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).end();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default webhookProductVariantOutOfStock.createHandler(handler);
|
19
apps/search/webhooks.ts
Normal file
19
apps/search/webhooks.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { webhookProductCreated } from "./src/pages/api/webhooks/saleor/product_created";
|
||||||
|
import { webhookProductDeleted } from "./src/pages/api/webhooks/saleor/product_deleted";
|
||||||
|
import { webhookProductUpdated } from "./src/pages/api/webhooks/saleor/product_updated";
|
||||||
|
import { webhookProductVariantCreated } from "./src/pages/api/webhooks/saleor/product_variant_created";
|
||||||
|
import { webhookProductVariantDeleted } from "./src/pages/api/webhooks/saleor/product_variant_deleted";
|
||||||
|
import { webhookProductVariantUpdated } from "./src/pages/api/webhooks/saleor/product_variant_updated";
|
||||||
|
import { webhookProductVariantOutOfStock } from "./src/pages/api/webhooks/saleor/product_variant_out_of_stock";
|
||||||
|
import { webhookProductVariantBackInStock } from "./src/pages/api/webhooks/saleor/product_variant_back_in_stock";
|
||||||
|
|
||||||
|
export const appWebhooks = [
|
||||||
|
webhookProductCreated,
|
||||||
|
webhookProductDeleted,
|
||||||
|
webhookProductUpdated,
|
||||||
|
webhookProductVariantCreated,
|
||||||
|
webhookProductVariantDeleted,
|
||||||
|
webhookProductVariantUpdated,
|
||||||
|
webhookProductVariantOutOfStock,
|
||||||
|
webhookProductVariantBackInStock,
|
||||||
|
];
|
Loading…
Reference in a new issue