Search: Fix stale variants after product removal, add indices setup and product descriptions (#932)

* Move plaintext renderer to shared package and use it for Algolia documents

* Fix stale variants after product removal, add indices setup

* Search: webhooks migration script (#936)

* Add webhook recreation script

* Add changeset
This commit is contained in:
Krzysztof Wolski 2023-08-29 13:19:31 +02:00 committed by GitHub
parent a81f061fcf
commit 2a1385bab1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 1145 additions and 918 deletions

View file

@ -0,0 +1,5 @@
---
"saleor-app-search": minor
---
Added new field to products document: descriptionPlaintext. It contains the product description converted to plaintext format which can be used to full text search.

View file

@ -0,0 +1,5 @@
---
"saleor-app-search": minor
---
Added Algolia index configuration helper, which sets up recommended settings: distinct results based on Product ID, faceting filters and searchable attributes.

View file

@ -0,0 +1,6 @@
---
"saleor-app-products-feed": patch
"@saleor/apps-shared": patch
---
Plaintext EditorJS renderer has been moved to the shared package.

View file

@ -0,0 +1,5 @@
---
"saleor-app-search": patch
---
Added script responsible for webhook migration.

View file

@ -0,0 +1,5 @@
---
"saleor-app-search": patch
---
Fixed issue with stale product variants after product deletion or channel visibility settings change.

View file

@ -1,3 +0,0 @@
Editor.js is used in Dashboard for product description. Feed description should be rendered to standard text.
This module contains logic that transforms Editor.js to plain text

View file

@ -2,12 +2,12 @@ import { XMLBuilder } from "fast-xml-parser";
import { GoogleFeedProductVariantFragment } from "../../../generated/graphql"; import { GoogleFeedProductVariantFragment } from "../../../generated/graphql";
import { productToProxy } from "./product-to-proxy"; import { productToProxy } from "./product-to-proxy";
import { shopDetailsToProxy } from "./shop-details-to-proxy"; import { shopDetailsToProxy } from "./shop-details-to-proxy";
import { EditorJsPlaintextRenderer } from "../editor-js/editor-js-plaintext-renderer";
import { RootConfig } from "../app-configuration/app-config"; import { RootConfig } from "../app-configuration/app-config";
import { getMappedAttributes } from "./attribute-mapping"; import { getMappedAttributes } from "./attribute-mapping";
import { priceMapping } from "./price-mapping"; import { priceMapping } from "./price-mapping";
import { renderHandlebarsTemplate } from "../handlebarsTemplates/render-handlebars-template"; import { renderHandlebarsTemplate } from "../handlebarsTemplates/render-handlebars-template";
import { transformTemplateFormat } from "../handlebarsTemplates/transform-template-format"; import { transformTemplateFormat } from "../handlebarsTemplates/transform-template-format";
import { EditorJsPlaintextRenderer } from "@saleor/apps-shared";
interface GenerateGoogleXmlFeedArgs { interface GenerateGoogleXmlFeedArgs {
productVariants: GoogleFeedProductVariantFragment[]; productVariants: GoogleFeedProductVariantFragment[];

View file

@ -2,6 +2,7 @@ fragment OwnWebhook on Webhook {
id id
isActive isActive
name name
targetUrl
asyncEvents { asyncEvents {
name name
eventType eventType

View file

@ -19,6 +19,7 @@ fragment ProductVariantData on ProductVariant {
id id
channel { channel {
slug slug
currencyCode
} }
price { price {
currency currency

View file

@ -1,5 +1,6 @@
query Channels { query Channels {
channels { channels {
slug slug
currencyCode
} }
} }

View file

@ -1,5 +1,8 @@
query FetchOwnWebhooks($id: ID!) { query FetchOwnWebhooks($id: ID!) {
app(id: $id) { app(id: $id) {
id
name
isActive
webhooks { webhooks {
...OwnWebhook ...OwnWebhook
} }

View file

@ -4,7 +4,13 @@ subscription ProductDeleted {
... on ProductDeleted { ... on ProductDeleted {
__typename __typename
product { product {
...ProductWebhookPayload id
variants{
id
product{
id
}
}
} }
} }
recipient { recipient {

View file

@ -2,9 +2,11 @@ subscription ProductVariantDeleted {
event { event {
__typename __typename
... on ProductVariantDeleted { ... on ProductVariantDeleted {
__typename
productVariant { productVariant {
...ProductVariantWebhookPayload id
product{
id
}
} }
} }
recipient { recipient {

View file

@ -22,8 +22,10 @@
"@tanstack/react-query": "4.29.19", "@tanstack/react-query": "4.29.19",
"@types/debug": "^4.1.7", "@types/debug": "^4.1.7",
"@urql/exchange-auth": "^2.1.4", "@urql/exchange-auth": "^2.1.4",
"algoliasearch": "4.14.2", "algoliasearch": "4.19.1",
"clsx": "^1.2.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"dotenv": "^16.3.1",
"graphql": "16.7.1", "graphql": "16.7.1",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",
"next": "13.4.8", "next": "13.4.8",

View file

@ -0,0 +1,7 @@
# Webhook migration scripts
Test migration with dry run, operation will not modify any data:
`npx tsx scripts/migrations/run-webhooks-migration-dry-run.ts`
To start the migration run command:
`npx tsx scripts/migrations/run-webhooks-migration.ts`

View file

@ -0,0 +1,20 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { SaleorCloudAPL } from "@saleor/app-sdk/APL";
export const verifyRequiredEnvs = () => {
const requiredEnvs = ["SALEOR_CLOUD_TOKEN", "SALEOR_CLOUD_RESOURCE_URL"];
if (!requiredEnvs.every((env) => process.env[env])) {
throw new Error(`Missing envs: ${requiredEnvs.join(" | ")}`);
}
};
export const fetchCloudAplEnvs = () => {
const saleorAPL = new SaleorCloudAPL({
token: process.env.SALEOR_CLOUD_TOKEN!,
resourceUrl: process.env.SALEOR_CLOUD_RESOURCE_URL!,
});
return saleorAPL.getAll();
};

View file

@ -0,0 +1,52 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import { createGraphQLClient } from "@saleor/apps-shared";
import { WebhookActivityTogglerService } from "../../src/domain/WebhookActivityToggler.service";
import { FetchOwnWebhooksDocument } from "../../generated/graphql";
import { AuthData } from "@saleor/app-sdk/APL";
export const recreateWebhooks = async ({
authData,
dryRun,
}: {
authData: AuthData;
dryRun: boolean;
}) => {
console.log("Working on env: ", authData.saleorApiUrl);
const client = createGraphQLClient({
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
const webhooks = await client
.query(FetchOwnWebhooksDocument, {
id: authData.appId,
})
.toPromise()
.then((r) => r.data?.app?.webhooks);
if (!webhooks?.length) {
console.error("The environment does not have any webhooks, skipping");
return;
}
// Use currently existing webhook data to determine a proper baseUrl and enabled state
const targetUrl = webhooks[0].targetUrl;
const enabled = webhooks[0].isActive;
const baseUrl = new URL(targetUrl).origin;
if (dryRun) {
console.log("Necessary data gathered, skipping recreation of webhooks due to dry run mode");
return;
}
const webhookService = new WebhookActivityTogglerService(authData.appId, client);
try {
await webhookService.recreateOwnWebhooks({ baseUrl, enableWebhooks: enabled });
console.log("✅ Webhooks recreated successfully");
} catch (e) {
console.error("🛑 Failed to recreate webhooks: ", e);
}
};

View file

@ -0,0 +1,30 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import * as dotenv from "dotenv";
import { fetchCloudAplEnvs, verifyRequiredEnvs } from "./migration-utils";
import { recreateWebhooks } from "./recreate-webhooks";
dotenv.config();
const runMigration = async () => {
console.log("Starting webhooks migration (dry run)");
verifyRequiredEnvs();
console.log("Envs verified, fetching envs");
const allEnvs = await fetchCloudAplEnvs().catch((r) => {
console.error("Could not fetch instances from the APL");
console.error(r);
process.exit(1);
});
for (const env of allEnvs) {
await recreateWebhooks({ authData: env, dryRun: true });
}
console.log("Migration dry run complete");
};
runMigration();

View file

@ -0,0 +1,30 @@
/* eslint-disable turbo/no-undeclared-env-vars */
import * as dotenv from "dotenv";
import { fetchCloudAplEnvs, verifyRequiredEnvs } from "./migration-utils";
import { recreateWebhooks } from "./recreate-webhooks";
dotenv.config();
const runMigration = async () => {
console.log("Starting running migration");
verifyRequiredEnvs();
console.log("Envs verified, fetching envs");
const allEnvs = await fetchCloudAplEnvs().catch((r) => {
console.error("Could not fetch instances from the APL");
console.error(r);
process.exit(1);
});
for (const env of allEnvs) {
await recreateWebhooks({ authData: env, dryRun: false });
}
console.log("Migration complete");
};
runMigration();

View file

@ -0,0 +1,74 @@
import { Box, Button, Text } from "@saleor/macaw-ui/next";
import React from "react";
import { useConfiguration } from "../lib/configuration";
import { TextLink } from "@saleor/apps-ui";
import { useIndicesSetupMutation } from "../lib/useIndicesSetup";
export const IndicesSettings = () => {
const algoliaConfiguration = useConfiguration();
const updateWebhooksMutation = useIndicesSetupMutation();
const isConfigured = algoliaConfiguration.data?.appId && algoliaConfiguration.data?.secretKey;
return (
<Box>
<Box>
<Text variant={"heading"} as={"p"} marginBottom={1.5}>
Performing this operation will update indices to use recommended settings:
</Text>
<ul>
<li>
<Text variant="body">
Distinct and grouping. According to{" "}
<TextLink
href="https://www.algolia.com/doc/guides/managing-results/refine-results/faceting/"
newTab
>
Algolia&apos;s recommendations
</TextLink>
, product variants are sent as separate entries. The distinct feature will group
results based on product ID.
</Text>
</li>
<li>
<Text variant="body">
Mark which attributes should be used as{" "}
<TextLink
href="https://www.algolia.com/doc/guides/managing-results/must-do/searchable-attributes/"
newTab
>
searchable
</TextLink>
. This includes: name, description, category, collections.
</Text>
</li>
<li>
<Text variant="body">
Set up attributes{" "}
<TextLink
href="https://www.algolia.com/doc/guides/managing-results/refine-results/faceting/"
newTab
>
faceting
</TextLink>
. This includes: category, collections, price, product type, stock, product attributes
</Text>
</li>
</ul>
<Text variant={"heading"} as={"p"} color={"iconCriticalSubdued"} marginBottom={1.5}>
Please note - if indices are already configured, this operation will overwrite settings
mentioned above.
</Text>
<Box display={"flex"} justifyContent={"flex-end"}>
<Button
disabled={!isConfigured}
onClick={() => updateWebhooksMutation.mutate()}
variant="primary"
>
Update indices configuration
</Button>
</Box>
</Box>
</Box>
);
};

View file

@ -5,55 +5,93 @@ import {
} from "../../../generated/graphql"; } from "../../../generated/graphql";
import { isNotNil } from "../isNotNil"; import { isNotNil } from "../isNotNil";
import { SearchProvider } from "../searchProvider"; import { SearchProvider } from "../searchProvider";
import { createDebug } from "../debug";
import { import {
AlgoliaObject, AlgoliaObject,
channelListingToAlgoliaIndexId, channelListingToAlgoliaIndexId,
productAndVariantToAlgolia, productAndVariantToAlgolia,
productAndVariantToObjectID,
} from "./algoliaUtils"; } from "./algoliaUtils";
import { createLogger } from "@saleor/apps-shared";
export interface AlgoliaSearchProviderOptions { export interface AlgoliaSearchProviderOptions {
appId: string; appId: string;
apiKey: string; apiKey: string;
indexNamePrefix?: string; indexNamePrefix?: string;
channels?: Array<{ slug: string; currencyCode: string }>;
} }
const debug = createDebug("AlgoliaSearchProvider"); const logger = createLogger({ name: "AlgoliaSearchProvider" });
export class AlgoliaSearchProvider implements SearchProvider { export class AlgoliaSearchProvider implements SearchProvider {
#algolia: SearchClient; #algolia: SearchClient;
#indexNamePrefix?: string | undefined; #indexNamePrefix?: string | undefined;
#indexNames: Array<string>;
constructor({ appId, apiKey, indexNamePrefix }: AlgoliaSearchProviderOptions) { constructor({ appId, apiKey, indexNamePrefix, channels }: AlgoliaSearchProviderOptions) {
this.#algolia = Algoliasearch(appId, apiKey); this.#algolia = Algoliasearch(appId, apiKey);
this.#indexNamePrefix = indexNamePrefix; this.#indexNamePrefix = indexNamePrefix;
this.#indexNames =
channels?.map((c) => channelListingToAlgoliaIndexId({ channel: c }, this.#indexNamePrefix)) ||
[];
} }
private async saveGroupedByIndex(groupedByIndex: GroupedByIndex) { private async saveGroupedByIndex(groupedByIndex: GroupedByIndex) {
debug("saveGroupedByIndex called"); logger.debug("saveGroupedByIndex called");
return Promise.all( return Promise.all(
Object.entries(groupedByIndex).map(([indexName, objects]) => { Object.entries(groupedByIndex).map(([indexName, objects]) => {
const index = this.#algolia.initIndex(indexName); const index = this.#algolia.initIndex(indexName);
return index.saveObjects(objects); return index.saveObjects(objects);
}) }),
); );
} }
private async deleteGroupedByIndex(groupedByIndex: GroupedByIndex) { private async deleteGroupedByIndex(groupedByIndex: IdsGroupedByIndex) {
debug("deleteGroupedByIndex called"); logger.debug("deleteGroupedByIndex called");
return Promise.all( return Promise.all(
Object.entries(groupedByIndex).map(([indexName, objects]) => { Object.entries(groupedByIndex).map(([indexName, objects]) => {
const index = this.#algolia.initIndex(indexName); const index = this.#algolia.initIndex(indexName);
return index.deleteObjects(objects.map((o) => o.objectID)); return index.deleteObjects(objects);
}) }),
);
}
async updateIndicesSettings() {
logger.debug(`updateIndicesSettings called`);
await Promise.all(
this.#indexNames.map(async (indexName) => {
const index = this.#algolia.initIndex(indexName);
return index.setSettings({
attributesForFaceting: [
"productId",
"inStock",
"categories",
"grossPrice",
"attributes",
"collections",
],
attributeForDistinct: "productId",
numericAttributesForFiltering: ["grossPrice"],
distinct: true,
searchableAttributes: [
"name",
"productName",
"variantName",
"productType",
"category",
"descriptionPlaintext",
"collections",
],
});
}),
); );
} }
async updatedBatchProducts(productsBatch: ProductWebhookPayloadFragment[]) { async updatedBatchProducts(productsBatch: ProductWebhookPayloadFragment[]) {
debug(`updatedBatchProducts called`); logger.debug(`updatedBatchProducts called`);
const groupedByIndex = groupProductsByIndexName(productsBatch, { const groupedByIndex = groupProductsByIndexName(productsBatch, {
visibleInListings: true, visibleInListings: true,
@ -64,41 +102,39 @@ export class AlgoliaSearchProvider implements SearchProvider {
} }
async createProduct(product: ProductWebhookPayloadFragment) { async createProduct(product: ProductWebhookPayloadFragment) {
debug(`createProduct called`); logger.debug(`createProduct called`);
await this.updateProduct(product); await this.updateProduct(product);
} }
async updateProduct(product: ProductWebhookPayloadFragment) { async updateProduct(product: ProductWebhookPayloadFragment) {
debug(`updateProduct called`); logger.debug(`updateProduct called`);
if (!product.variants) { if (!product.variants) {
debug("Product has no variants - abort"); logger.debug("Product has no variants - abort");
return; return;
} }
await Promise.all(product.variants.map((variant) => this.updateProductVariant(variant))); await Promise.all(product.variants.map((variant) => this.updateProductVariant(variant)));
} }
async deleteProduct(product: ProductWebhookPayloadFragment) { async deleteProduct(product: ProductWebhookPayloadFragment) {
debug(`deleteProduct`); logger.debug(`deleteProduct`);
if (!product.variants) {
debug("Product has no variants - abort"); await Promise.all(
return; this.#indexNames.map((indexName) => {
} const index = this.#algolia.initIndex(indexName);
await Promise.all(product.variants.map((variant) => this.deleteProductVariant(variant)));
return index.deleteBy({ filters: `productId:"${product.id}"` });
}),
);
} }
async createProductVariant(productVariant: ProductVariantWebhookPayloadFragment) { async createProductVariant(productVariant: ProductVariantWebhookPayloadFragment) {
debug(`createProductVariant called`); logger.debug(`createProductVariant called`);
return this.updateProductVariant(productVariant); return this.updateProductVariant(productVariant);
} }
async updateProductVariant(productVariant: ProductVariantWebhookPayloadFragment) { async updateProductVariant(productVariant: ProductVariantWebhookPayloadFragment) {
debug(`updateProductVariant called`); logger.debug(`updateProductVariant called`);
if (!productVariant.product.channelListings) {
debug("Product has no channelListings - abort");
return;
}
const groupedByIndexToSave = groupVariantByIndexName(productVariant, { const groupedByIndexToSave = groupVariantByIndexName(productVariant, {
visibleInListings: true, visibleInListings: true,
@ -109,32 +145,32 @@ export class AlgoliaSearchProvider implements SearchProvider {
await this.saveGroupedByIndex(groupedByIndexToSave); await this.saveGroupedByIndex(groupedByIndexToSave);
} }
const groupedByIndexToDelete = groupVariantByIndexName(productVariant, { /*
visibleInListings: false, * When the variant is removed from a channel, Saleor does not send it's channel listing in the event payload.
indexNamePrefix: this.#indexNamePrefix, * If it was created previously, we have to remove it.
}); * To achieve that we call delete operation for every index which wasn't updated.
*/
const staleIndices = this.#indexNames.filter(
(name) => !Object.keys(groupedByIndexToSave || {}).includes(name),
);
if (groupedByIndexToDelete && !!Object.keys(groupedByIndexToDelete).length) { if (staleIndices) {
await this.deleteGroupedByIndex(groupedByIndexToDelete); await this.deleteGroupedByIndex(
Object.fromEntries(
staleIndices.map((index) => [index, [productAndVariantToObjectID(productVariant)]]),
),
);
} }
} }
async deleteProductVariant(productVariant: ProductVariantWebhookPayloadFragment) { async deleteProductVariant(productVariant: ProductVariantWebhookPayloadFragment) {
debug(`deleteProductVariant called`); logger.debug(`deleteProductVariant called`);
if (!productVariant.product.channelListings) { await this.deleteGroupedByIndex(
debug("No channel listing data - return"); Object.fromEntries(
return; this.#indexNames.map((index) => [index, [productAndVariantToObjectID(productVariant)]]),
} ),
);
const groupedByIndexToDelete = groupVariantByIndexName(productVariant, {
visibleInListings: null,
indexNamePrefix: this.#indexNamePrefix,
});
if (groupedByIndexToDelete) {
await this.deleteGroupedByIndex(groupedByIndexToDelete);
}
} }
async ping() { async ping() {
@ -150,23 +186,41 @@ export class AlgoliaSearchProvider implements SearchProvider {
} }
type GroupedByIndex = Record<string, AlgoliaObject[]>; type GroupedByIndex = Record<string, AlgoliaObject[]>;
type IdsGroupedByIndex = Record<string, Array<string>>;
const groupVariantByIndexName = ( const groupVariantByIndexName = (
productVariant: ProductVariantWebhookPayloadFragment, productVariant: ProductVariantWebhookPayloadFragment,
{ {
visibleInListings, visibleInListings,
indexNamePrefix, indexNamePrefix,
}: { visibleInListings: true | false | null; indexNamePrefix: string | undefined } }: { visibleInListings: true | false | null; indexNamePrefix: string | undefined },
) => { ) => {
if (!productVariant.product.channelListings) { logger.debug("Grouping variants per index name");
return null; if (!productVariant.channelListings) {
logger.debug("Product variant has no channel listings - abort");
return {};
} }
const objectsToSaveByIndexName = productVariant.product.channelListings const objectsToSaveByIndexName = productVariant.channelListings
.filter((channelListing) => .filter((channelListing) => {
const productChannelListing = productVariant.product.channelListings?.find(
(productChannelListing) =>
productChannelListing.channel.slug === channelListing.channel.slug,
);
if (!productChannelListing) {
logger.debug(
{ var: channelListing, prod: productChannelListing },
"no product channel listing found - abort",
);
return false;
}
// don't filter if `visibleInListings` is null // don't filter if `visibleInListings` is null
visibleInListings === null ? true : channelListing.visibleInListings === visibleInListings return visibleInListings === null
) ? true
: productChannelListing.visibleInListings === visibleInListings;
})
.map((channelListing) => { .map((channelListing) => {
const object = productAndVariantToAlgolia({ const object = productAndVariantToAlgolia({
variant: productVariant, variant: productVariant,
@ -192,9 +246,9 @@ const groupProductsByIndexName = (
{ {
visibleInListings, visibleInListings,
indexNamePrefix, indexNamePrefix,
}: { visibleInListings: true | false | null; indexNamePrefix: string | undefined } }: { visibleInListings: true | false | null; indexNamePrefix: string | undefined },
) => { ) => {
debug(`groupProductsByIndexName called`); logger.debug(`groupProductsByIndexName called`);
const batchesAndIndices = productsBatch const batchesAndIndices = productsBatch
.flatMap((p) => p.variants) .flatMap((p) => p.variants)
.filter(isNotNil) .filter(isNotNil)

View file

@ -1,3 +1,4 @@
import { EditorJsPlaintextRenderer } from "@saleor/apps-shared";
import { import {
ProductAttributesDataFragment, ProductAttributesDataFragment,
ProductVariantWebhookPayloadFragment, ProductVariantWebhookPayloadFragment,
@ -127,6 +128,7 @@ export function productAndVariantToAlgolia({
attributes, attributes,
media, media,
description: safeParseJson(product.description), description: safeParseJson(product.description),
descriptionPlaintext: EditorJsPlaintextRenderer({ stringData: product.description }),
slug: product.slug, slug: product.slug,
thumbnail: product.thumbnail?.url, thumbnail: product.thumbnail?.url,
grossPrice: listing?.price?.amount, grossPrice: listing?.price?.amount,

View file

@ -0,0 +1,26 @@
import { useMutation } from "@tanstack/react-query";
import { useAuthenticatedFetch } from "@saleor/app-sdk/app-bridge";
import { useCallback } from "react";
import { useDashboardNotification } from "@saleor/apps-shared";
export const useIndicesSetupMutation = () => {
const fetch: typeof window.fetch = useAuthenticatedFetch();
const { notifyError, notifySuccess } = useDashboardNotification();
const mutationFn = useCallback(() => {
return fetch("/api/setup-indices", { method: "POST" }).then((resp) => {
if (resp.ok) {
notifySuccess("Settings has been updated");
} else {
notifyError("Settings update failed");
}
});
/**
* fetch from SDK is not wrapped with memo todo
*/
}, [fetch, notifyError, notifySuccess]);
return useMutation({
mutationFn,
});
};

View file

@ -10,6 +10,7 @@ import { AppConfigurationFields } from "../../domain/configuration";
import { AlgoliaSearchProvider } from "../../lib/algolia/algoliaSearchProvider"; import { AlgoliaSearchProvider } from "../../lib/algolia/algoliaSearchProvider";
import { WebhookActivityTogglerService } from "../../domain/WebhookActivityToggler.service"; import { WebhookActivityTogglerService } from "../../domain/WebhookActivityToggler.service";
import { createGraphQLClient } from "@saleor/apps-shared"; import { createGraphQLClient } from "@saleor/apps-shared";
import { ChannelsDocument } from "../../../generated/graphql";
const logger = createLogger({ const logger = createLogger({
handler: "api/configuration", handler: "api/configuration",
@ -24,7 +25,7 @@ const sendResponse = async (
res: NextApiResponse<SettingsApiResponse>, res: NextApiResponse<SettingsApiResponse>,
statusCode: number, statusCode: number,
settings: SettingsManager, settings: SettingsManager,
domain: string domain: string,
) => { ) => {
const data = { const data = {
secretKey: (await settings.get("secretKey", domain)) || "", secretKey: (await settings.get("secretKey", domain)) || "",
@ -43,7 +44,7 @@ const sendResponse = async (
export const handler = async ( export const handler = async (
req: NextApiRequest, req: NextApiRequest,
res: NextApiResponse, res: NextApiResponse,
ctx: ProtectedHandlerContext ctx: ProtectedHandlerContext,
) => { ) => {
const { const {
authData: { token, saleorApiUrl }, authData: { token, saleorApiUrl },
@ -70,10 +71,14 @@ export const handler = async (
const { appId, secretKey, indexNamePrefix } = JSON.parse(req.body) as AppConfigurationFields; const { appId, secretKey, indexNamePrefix } = JSON.parse(req.body) as AppConfigurationFields;
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
const channels = channelsData?.channels || [];
const algoliaClient = new AlgoliaSearchProvider({ const algoliaClient = new AlgoliaSearchProvider({
appId, appId,
apiKey: secretKey, apiKey: secretKey,
indexNamePrefix: indexNamePrefix, indexNamePrefix: indexNamePrefix,
channels,
}); });
try { try {

View file

@ -0,0 +1,78 @@
import { createProtectedHandler, NextProtectedApiHandler } from "@saleor/app-sdk/handlers/next";
import { saleorApp } from "../../../saleor-app";
import { createSettingsManager } from "../../lib/metadata";
import { createLogger } from "../../lib/logger";
import { SettingsManager } from "@saleor/app-sdk/settings-manager";
import { createGraphQLClient } from "@saleor/apps-shared";
import { Client } from "urql";
import { ChannelsDocument } from "../../../generated/graphql";
import { AlgoliaSearchProvider } from "../../lib/algolia/algoliaSearchProvider";
const logger = createLogger({
service: "setupIndicesHandler",
});
/**
* Simple dependency injection - factory injects all services, in tests everything can be configured without mocks
*/
type FactoryProps = {
settingsManagerFactory: (client: Client) => SettingsManager;
graphqlClientFactory: (saleorApiUrl: string, token: string) => Client;
};
export const setupIndicesHandlerFactory =
({ settingsManagerFactory, 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 settingsManager = settingsManagerFactory(client);
const domain = new URL(authData.saleorApiUrl).host;
const [secretKey, appId, indexNamePrefix, channelsRequest] = await Promise.all([
settingsManager.get("secretKey", domain),
settingsManager.get("appId", domain),
settingsManager.get("indexNamePrefix", domain),
client.query(ChannelsDocument, {}).toPromise(),
]);
if (!secretKey || !appId) {
logger.debug("Missing secretKey or appId, returning 400");
return res.status(400).end();
}
const channels = channelsRequest.data?.channels || [];
const algoliaClient = new AlgoliaSearchProvider({
appId,
apiKey: secretKey,
indexNamePrefix: indexNamePrefix,
channels,
});
try {
logger.debug("Running indices update");
await algoliaClient.updateIndicesSettings();
logger.debug("Indices updated");
return res.status(200).end();
} catch (e) {
logger.error(e);
return res.status(500).end();
}
};
export default createProtectedHandler(
setupIndicesHandlerFactory({
settingsManagerFactory: createSettingsManager,
graphqlClientFactory(saleorApiUrl: string, token: string) {
return createGraphQLClient({ saleorApiUrl, token });
},
}),
saleorApp.apl,
[],
);

View file

@ -1,11 +1,11 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { ProductCreated, ProductCreatedDocument } from "../../../../../generated/graphql"; import { ChannelsDocument, ProductCreated } from "../../../../../generated/graphql";
import { saleorApp } from "../../../../../saleor-app";
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider"; import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration"; import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
import { createLogger } from "../../../../lib/logger"; import { createLogger } from "../../../../lib/logger";
import { createGraphQLClient } from "@saleor/apps-shared"; import { createGraphQLClient } from "@saleor/apps-shared";
import { webhookProductCreated } from "../../../../webhooks/definitions/product-created";
export const config = { export const config = {
api: { api: {
@ -13,17 +13,6 @@ export const config = {
}, },
}; };
export const webhookProductCreated = new SaleorAsyncWebhook<ProductCreated>({
webhookPath: "api/webhooks/saleor/product_created",
event: "PRODUCT_CREATED",
apl: saleorApp.apl,
query: ProductCreatedDocument,
/**
* Webhook is disabled by default. Will be enabled by the app when configuration succeeds
*/
isActive: false,
});
const logger = createLogger({ const logger = createLogger({
service: "webhookProductCreatedWebhookHandler", service: "webhookProductCreatedWebhookHandler",
}); });
@ -32,10 +21,23 @@ export const handler: NextWebhookApiHandler<ProductCreated> = async (req, res, c
const { event, authData } = context; const { event, authData } = context;
logger.debug( logger.debug(
`New event ${event} (${context.payload?.__typename}) from the ${authData.domain} domain has been received!` `New event ${event} (${context.payload?.__typename}) from the ${authData.domain} domain has been received!`,
); );
const { product } = context.payload;
if (!product) {
logger.error("Webhook did not received expected product data in the payload.");
return res.status(200).end();
}
const { settings, errors } = await getAlgoliaConfiguration({ authData }); const { settings, errors } = await getAlgoliaConfiguration({ authData });
const client = createGraphQLClient({
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
const channels = channelsData?.channels || [];
if (errors?.length || !settings) { if (errors?.length || !settings) {
logger.warn("Aborting due to lack of settings"); logger.warn("Aborting due to lack of settings");
@ -50,31 +52,26 @@ export const handler: NextWebhookApiHandler<ProductCreated> = async (req, res, c
appId: settings.appId, appId: settings.appId,
apiKey: settings.secretKey, apiKey: settings.secretKey,
indexNamePrefix: settings.indexNamePrefix, indexNamePrefix: settings.indexNamePrefix,
channels,
}); });
const { product } = context.payload; try {
await searchProvider.createProduct(product);
} catch (e) {
logger.info(e, "Algolia createProduct failed. Webhooks will be disabled");
if (product) { const webhooksToggler = new WebhookActivityTogglerService(
try { authData.appId,
await searchProvider.createProduct(product); createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
} catch (e) { );
logger.info(e, "Algolia createProduct failed. Webhooks will be disabled");
const webhooksToggler = new WebhookActivityTogglerService( logger.trace("Will disable webhooks");
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));
await webhooksToggler.disableOwnWebhooks( logger.trace("Webhooks disabling operation finished");
context.payload.recipient?.webhooks?.map((w) => w.id)
);
logger.trace("Webhooks disabling operation finished"); return res.status(500).send("Operation failed, webhooks are disabled");
return res.status(500).send("Operation failed, webhooks are disabled");
}
} }
res.status(200).end(); res.status(200).end();
return; return;

View file

@ -1,12 +1,11 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { ProductDeleted, ProductDeletedDocument } from "../../../../../generated/graphql"; import { ChannelsDocument, ProductDeleted } from "../../../../../generated/graphql";
import { saleorApp } from "../../../../../saleor-app";
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider"; import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration"; import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
import { createDebug } from "../../../../lib/debug";
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
import { createLogger } from "../../../../lib/logger"; import { createLogger } from "../../../../lib/logger";
import { createGraphQLClient } from "@saleor/apps-shared"; import { createGraphQLClient } from "@saleor/apps-shared";
import { webhookProductDeleted } from "../../../../webhooks/definitions/product-deleted";
export const config = { export const config = {
api: { api: {
@ -14,17 +13,6 @@ export const config = {
}, },
}; };
export const webhookProductDeleted = new SaleorAsyncWebhook<ProductDeleted>({
webhookPath: "api/webhooks/saleor/product_deleted",
event: "PRODUCT_DELETED",
apl: saleorApp.apl,
query: ProductDeletedDocument,
/**
* Webhook is disabled by default. Will be enabled by the app when configuration succeeds
*/
isActive: false,
});
const logger = createLogger({ const logger = createLogger({
service: "webhookProductDeletedWebhookHandler", service: "webhookProductDeletedWebhookHandler",
}); });
@ -33,10 +21,23 @@ export const handler: NextWebhookApiHandler<ProductDeleted> = async (req, res, c
const { event, authData } = context; const { event, authData } = context;
logger.debug( logger.debug(
`New event ${event} (${context.payload?.__typename}) from the ${authData.domain} domain has been received!` `New event ${event} (${context.payload?.__typename}) from the ${authData.domain} domain has been received!`,
); );
const { product } = context.payload;
if (!product) {
logger.error("Webhook did not received expected product data in the payload.");
return res.status(200).end();
}
const { settings, errors } = await getAlgoliaConfiguration({ authData }); const { settings, errors } = await getAlgoliaConfiguration({ authData });
const client = createGraphQLClient({
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
const channels = channelsData?.channels || [];
if (errors?.length || !settings) { if (errors?.length || !settings) {
logger.warn("Aborting due to lack of settings"); logger.warn("Aborting due to lack of settings");
@ -51,31 +52,26 @@ export const handler: NextWebhookApiHandler<ProductDeleted> = async (req, res, c
appId: settings.appId, appId: settings.appId,
apiKey: settings.secretKey, apiKey: settings.secretKey,
indexNamePrefix: settings.indexNamePrefix, indexNamePrefix: settings.indexNamePrefix,
channels,
}); });
const { product } = context.payload; try {
await searchProvider.deleteProduct(product);
} catch (e) {
logger.info(e, "Algolia deleteProduct failed. Webhooks will be disabled");
if (product) { const webhooksToggler = new WebhookActivityTogglerService(
try { authData.appId,
await searchProvider.deleteProduct(product); createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
} catch (e) { );
logger.info(e, "Algolia deleteProduct failed. Webhooks will be disabled");
const webhooksToggler = new WebhookActivityTogglerService( logger.trace("Will disable webhooks");
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));
await webhooksToggler.disableOwnWebhooks( logger.trace("Webhooks disabling operation finished");
context.payload.recipient?.webhooks?.map((w) => w.id)
);
logger.trace("Webhooks disabling operation finished"); return res.status(500).send("Operation failed, webhooks are disabled");
return res.status(500).send("Operation failed, webhooks are disabled");
}
} }
res.status(200).end(); res.status(200).end();
return; return;

View file

@ -1,11 +1,11 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { ProductUpdated, ProductUpdatedDocument } from "../../../../../generated/graphql"; import { ChannelsDocument, ProductUpdated } from "../../../../../generated/graphql";
import { saleorApp } from "../../../../../saleor-app";
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider"; import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration"; import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
import { createLogger } from "../../../../lib/logger"; import { createLogger } from "../../../../lib/logger";
import { createGraphQLClient } from "@saleor/apps-shared"; import { createGraphQLClient } from "@saleor/apps-shared";
import { webhookProductUpdated } from "../../../../webhooks/definitions/product-updated";
export const config = { export const config = {
api: { api: {
@ -13,17 +13,6 @@ export const config = {
}, },
}; };
export const webhookProductUpdated = new SaleorAsyncWebhook<ProductUpdated>({
webhookPath: "api/webhooks/saleor/product_updated",
event: "PRODUCT_UPDATED",
apl: saleorApp.apl,
query: ProductUpdatedDocument,
/**
* Webhook is disabled by default. Will be enabled by the app when configuration succeeds
*/
isActive: false,
});
const logger = createLogger({ const logger = createLogger({
service: "webhookProductUpdatedWebhookHandler", service: "webhookProductUpdatedWebhookHandler",
}); });
@ -32,10 +21,23 @@ export const handler: NextWebhookApiHandler<ProductUpdated> = async (req, res, c
const { event, authData } = context; const { event, authData } = context;
logger.debug( logger.debug(
`New event ${event} (${context.payload?.__typename}) from the ${authData.domain} domain has been received!` `New event ${event} (${context.payload?.__typename}) from the ${authData.domain} domain has been received!`,
); );
const { product } = context.payload;
if (!product) {
logger.error("Webhook did not received expected product data in the payload.");
return res.status(200).end();
}
const { settings, errors } = await getAlgoliaConfiguration({ authData }); const { settings, errors } = await getAlgoliaConfiguration({ authData });
const client = createGraphQLClient({
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
const channels = channelsData?.channels || [];
if (errors?.length || !settings) { if (errors?.length || !settings) {
logger.warn("Aborting due to lack of settings"); logger.warn("Aborting due to lack of settings");
@ -50,31 +52,26 @@ export const handler: NextWebhookApiHandler<ProductUpdated> = async (req, res, c
appId: settings.appId, appId: settings.appId,
apiKey: settings.secretKey, apiKey: settings.secretKey,
indexNamePrefix: settings.indexNamePrefix, indexNamePrefix: settings.indexNamePrefix,
channels,
}); });
const { product } = context.payload; try {
await searchProvider.updateProduct(product);
} catch (e) {
logger.info(e, "Algolia updateProduct failed. Webhooks will be disabled");
if (product) { const webhooksToggler = new WebhookActivityTogglerService(
try { authData.appId,
await searchProvider.updateProduct(product); createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
} catch (e) { );
logger.info(e, "Algolia updateProduct failed. Webhooks will be disabled");
const webhooksToggler = new WebhookActivityTogglerService( logger.trace("Will disable webhooks");
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));
await webhooksToggler.disableOwnWebhooks( logger.trace("Webhooks disabling operation finished");
context.payload.recipient?.webhooks?.map((w) => w.id)
);
logger.trace("Webhooks disabling operation finished"); return res.status(500).send("Operation failed, webhooks are disabled");
return res.status(500).send("Operation failed, webhooks are disabled");
}
} }
res.status(200).end(); res.status(200).end();

View file

@ -1,14 +1,11 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { createGraphQLClient } from "@saleor/apps-shared"; import { createGraphQLClient } from "@saleor/apps-shared";
import { import { ChannelsDocument, ProductVariantBackInStock } from "../../../../../generated/graphql";
ProductVariantBackInStock,
ProductVariantBackInStockDocument,
} from "../../../../../generated/graphql";
import { saleorApp } from "../../../../../saleor-app";
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider"; import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration"; import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
import { createLogger } from "../../../../lib/logger"; import { createLogger } from "../../../../lib/logger";
import { webhookProductVariantBackInStock } from "../../../../webhooks/definitions/product-variant-back-in-stock";
export const config = { export const config = {
api: { api: {
@ -16,17 +13,6 @@ export const config = {
}, },
}; };
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({ const logger = createLogger({
service: "webhookProductVariantBackInStockWebhookHandler", service: "webhookProductVariantBackInStockWebhookHandler",
}); });
@ -42,7 +28,20 @@ export const handler: NextWebhookApiHandler<ProductVariantBackInStock> = async (
`New event ${event} (${context.payload?.__typename}) from the ${authData.domain} domain has been received!`, `New event ${event} (${context.payload?.__typename}) from the ${authData.domain} domain has been received!`,
); );
const { productVariant } = context.payload;
if (!productVariant) {
logger.error("Webhook did not received expected product data in the payload.");
return res.status(200).end();
}
const { settings, errors } = await getAlgoliaConfiguration({ authData }); const { settings, errors } = await getAlgoliaConfiguration({ authData });
const client = createGraphQLClient({
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
const channels = channelsData?.channels || [];
if (errors?.length || !settings) { if (errors?.length || !settings) {
logger.warn("Aborting due to lack of settings"); logger.warn("Aborting due to lack of settings");
@ -57,31 +56,26 @@ export const handler: NextWebhookApiHandler<ProductVariantBackInStock> = async (
appId: settings.appId, appId: settings.appId,
apiKey: settings.secretKey, apiKey: settings.secretKey,
indexNamePrefix: settings.indexNamePrefix, indexNamePrefix: settings.indexNamePrefix,
channels,
}); });
const { productVariant } = context.payload; try {
await searchProvider.updateProductVariant(productVariant);
} catch (e) {
logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled");
if (productVariant) { const webhooksToggler = new WebhookActivityTogglerService(
try { authData.appId,
await searchProvider.updateProductVariant(productVariant); createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
} catch (e) { );
logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled");
const webhooksToggler = new WebhookActivityTogglerService( logger.trace("Will disable webhooks");
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));
await webhooksToggler.disableOwnWebhooks( logger.trace("Webhooks disabling operation finished");
context.payload.recipient?.webhooks?.map((w) => w.id),
);
logger.trace("Webhooks disabling operation finished"); return res.status(500).send("Operation failed, webhooks are disabled");
return res.status(500).send("Operation failed, webhooks are disabled");
}
} }
res.status(200).end(); res.status(200).end();

View file

@ -1,15 +1,11 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { import { ChannelsDocument, ProductVariantCreated } from "../../../../../generated/graphql";
ProductVariantCreated,
ProductVariantCreatedDocument,
} from "../../../../../generated/graphql";
import { saleorApp } from "../../../../../saleor-app";
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider"; import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration"; import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
import { createDebug } from "../../../../lib/debug";
import { createLogger } from "../../../../lib/logger"; import { createLogger } from "../../../../lib/logger";
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
import { createGraphQLClient } from "@saleor/apps-shared"; import { createGraphQLClient } from "@saleor/apps-shared";
import { webhookProductVariantCreated } from "../../../../webhooks/definitions/product-variant-created";
export const config = { export const config = {
api: { api: {
@ -17,17 +13,6 @@ export const config = {
}, },
}; };
export const webhookProductVariantCreated = new SaleorAsyncWebhook<ProductVariantCreated>({
webhookPath: "api/webhooks/saleor/product_variant_created",
event: "PRODUCT_VARIANT_CREATED",
apl: saleorApp.apl,
query: ProductVariantCreatedDocument,
/**
* Webhook is disabled by default. Will be enabled by the app when configuration succeeds
*/
isActive: false,
});
const logger = createLogger({ const logger = createLogger({
service: "webhookProductVariantCreatedWebhookHandler", service: "webhookProductVariantCreatedWebhookHandler",
}); });
@ -36,10 +21,23 @@ export const handler: NextWebhookApiHandler<ProductVariantCreated> = async (req,
const { event, authData } = context; const { event, authData } = context;
logger.debug( logger.debug(
`New event ${event} (${context.payload?.__typename}) from the ${authData.domain} domain has been received!` `New event ${event} (${context.payload?.__typename}) from the ${authData.domain} domain has been received!`,
); );
const { productVariant } = context.payload;
if (!productVariant) {
logger.error("Webhook did not received expected product data in the payload.");
return res.status(200).end();
}
const { settings, errors } = await getAlgoliaConfiguration({ authData }); const { settings, errors } = await getAlgoliaConfiguration({ authData });
const client = createGraphQLClient({
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
const channels = channelsData?.channels || [];
if (errors?.length || !settings) { if (errors?.length || !settings) {
logger.warn("Aborting due to lack of settings"); logger.warn("Aborting due to lack of settings");
@ -53,31 +51,26 @@ export const handler: NextWebhookApiHandler<ProductVariantCreated> = async (req,
appId: settings.appId, appId: settings.appId,
apiKey: settings.secretKey, apiKey: settings.secretKey,
indexNamePrefix: settings.indexNamePrefix, indexNamePrefix: settings.indexNamePrefix,
channels,
}); });
const { productVariant } = context.payload; try {
await searchProvider.createProductVariant(productVariant);
} catch (e) {
logger.info(e, "Algolia createProductVariant failed. Webhooks will be disabled");
if (productVariant) { const webhooksToggler = new WebhookActivityTogglerService(
try { authData.appId,
await searchProvider.createProductVariant(productVariant); createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
} catch (e) { );
logger.info(e, "Algolia createProductVariant failed. Webhooks will be disabled");
const webhooksToggler = new WebhookActivityTogglerService( logger.trace("Will disable webhooks");
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));
await webhooksToggler.disableOwnWebhooks( logger.trace("Webhooks disabling operation finished");
context.payload.recipient?.webhooks?.map((w) => w.id)
);
logger.trace("Webhooks disabling operation finished"); return res.status(500).send("Operation failed, webhooks are disabled");
return res.status(500).send("Operation failed, webhooks are disabled");
}
} }
res.status(200).end(); res.status(200).end();

View file

@ -1,15 +1,11 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { import { ChannelsDocument, ProductVariantDeleted } from "../../../../../generated/graphql";
ProductVariantDeleted,
ProductVariantDeletedDocument,
} from "../../../../../generated/graphql";
import { saleorApp } from "../../../../../saleor-app";
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider"; import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration"; import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
import { createDebug } from "../../../../lib/debug";
import { createLogger } from "../../../../lib/logger"; import { createLogger } from "../../../../lib/logger";
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
import { createGraphQLClient } from "@saleor/apps-shared"; import { createGraphQLClient } from "@saleor/apps-shared";
import { webhookProductVariantDeleted } from "../../../../webhooks/definitions/product-variant-deleted";
export const config = { export const config = {
api: { api: {
@ -17,17 +13,6 @@ export const config = {
}, },
}; };
export const webhookProductVariantDeleted = new SaleorAsyncWebhook<ProductVariantDeleted>({
webhookPath: "api/webhooks/saleor/product_variant_deleted",
event: "PRODUCT_VARIANT_DELETED",
apl: saleorApp.apl,
query: ProductVariantDeletedDocument,
/**
* Webhook is disabled by default. Will be enabled by the app when configuration succeeds
*/
isActive: false,
});
const logger = createLogger({ const logger = createLogger({
service: "webhookProductVariantDeletedWebhookHandler", service: "webhookProductVariantDeletedWebhookHandler",
}); });
@ -36,10 +21,23 @@ export const handler: NextWebhookApiHandler<ProductVariantDeleted> = async (req,
const { event, authData } = context; const { event, authData } = context;
logger.debug( logger.debug(
`New event ${event} (${context.payload?.__typename}) from the ${authData.domain} domain has been received!` `New event ${event} (${context.payload?.__typename}) from the ${authData.domain} domain has been received!`,
); );
const { productVariant } = context.payload;
if (!productVariant) {
logger.error("Webhook did not received expected product data in the payload.");
return res.status(200).end();
}
const { settings, errors } = await getAlgoliaConfiguration({ authData }); const { settings, errors } = await getAlgoliaConfiguration({ authData });
const client = createGraphQLClient({
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
const channels = channelsData?.channels || [];
if (errors?.length || !settings) { if (errors?.length || !settings) {
logger.warn("Aborting due to lack of settings"); logger.warn("Aborting due to lack of settings");
@ -54,31 +52,26 @@ export const handler: NextWebhookApiHandler<ProductVariantDeleted> = async (req,
appId: settings.appId, appId: settings.appId,
apiKey: settings.secretKey, apiKey: settings.secretKey,
indexNamePrefix: settings.indexNamePrefix, indexNamePrefix: settings.indexNamePrefix,
channels,
}); });
const { productVariant } = context.payload; try {
await searchProvider.deleteProductVariant(productVariant);
} catch (e) {
logger.info(e, "Algolia deleteProductVariant failed. Webhooks will be disabled");
if (productVariant) { const webhooksToggler = new WebhookActivityTogglerService(
try { authData.appId,
await searchProvider.deleteProductVariant(productVariant); createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
} catch (e) { );
logger.info(e, "Algolia deleteProductVariant failed. Webhooks will be disabled");
const webhooksToggler = new WebhookActivityTogglerService( logger.trace("Will disable webhooks");
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));
await webhooksToggler.disableOwnWebhooks( logger.trace("Webhooks disabling operation finished");
context.payload.recipient?.webhooks?.map((w) => w.id)
);
logger.trace("Webhooks disabling operation finished"); return res.status(500).send("Operation failed, webhooks are disabled");
return res.status(500).send("Operation failed, webhooks are disabled");
}
} }
res.status(200).end(); res.status(200).end();

View file

@ -1,14 +1,11 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { createGraphQLClient } from "@saleor/apps-shared"; import { createGraphQLClient } from "@saleor/apps-shared";
import { import { ChannelsDocument, ProductVariantOutOfStock } from "../../../../../generated/graphql";
ProductVariantOutOfStock,
ProductVariantOutOfStockDocument,
} from "../../../../../generated/graphql";
import { saleorApp } from "../../../../../saleor-app";
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider"; import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration"; import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
import { createLogger } from "../../../../lib/logger"; import { createLogger } from "../../../../lib/logger";
import { webhookProductVariantOutOfStock } from "../../../../webhooks/definitions/product-variant-out-of-stock";
export const config = { export const config = {
api: { api: {
@ -16,17 +13,6 @@ export const config = {
}, },
}; };
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({ const logger = createLogger({
service: "webhookProductVariantOutOfStockWebhookHandler", service: "webhookProductVariantOutOfStockWebhookHandler",
}); });
@ -42,7 +28,20 @@ export const handler: NextWebhookApiHandler<ProductVariantOutOfStock> = async (
`New event ${event} (${context.payload?.__typename}) from the ${authData.domain} domain has been received!`, `New event ${event} (${context.payload?.__typename}) from the ${authData.domain} domain has been received!`,
); );
const { productVariant } = context.payload;
if (!productVariant) {
logger.error("Webhook did not received expected product data in the payload.");
return res.status(200).end();
}
const { settings, errors } = await getAlgoliaConfiguration({ authData }); const { settings, errors } = await getAlgoliaConfiguration({ authData });
const client = createGraphQLClient({
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
const channels = channelsData?.channels || [];
if (errors?.length || !settings) { if (errors?.length || !settings) {
logger.warn("Aborting due to lack of settings"); logger.warn("Aborting due to lack of settings");
@ -57,31 +56,26 @@ export const handler: NextWebhookApiHandler<ProductVariantOutOfStock> = async (
appId: settings.appId, appId: settings.appId,
apiKey: settings.secretKey, apiKey: settings.secretKey,
indexNamePrefix: settings.indexNamePrefix, indexNamePrefix: settings.indexNamePrefix,
channels,
}); });
const { productVariant } = context.payload; try {
await searchProvider.updateProductVariant(productVariant);
} catch (e) {
logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled");
if (productVariant) { const webhooksToggler = new WebhookActivityTogglerService(
try { authData.appId,
await searchProvider.updateProductVariant(productVariant); createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
} catch (e) { );
logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled");
const webhooksToggler = new WebhookActivityTogglerService( logger.trace("Will disable webhooks");
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));
await webhooksToggler.disableOwnWebhooks( logger.trace("Webhooks disabling operation finished");
context.payload.recipient?.webhooks?.map((w) => w.id),
);
logger.trace("Webhooks disabling operation finished"); return res.status(500).send("Operation failed, webhooks are disabled");
return res.status(500).send("Operation failed, webhooks are disabled");
}
} }
res.status(200).end(); res.status(200).end();

View file

@ -1,14 +1,11 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next"; import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
import { createGraphQLClient } from "@saleor/apps-shared"; import { createGraphQLClient } from "@saleor/apps-shared";
import { import { ChannelsDocument, ProductVariantUpdated } from "../../../../../generated/graphql";
ProductVariantUpdated,
ProductVariantUpdatedDocument,
} from "../../../../../generated/graphql";
import { saleorApp } from "../../../../../saleor-app";
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service"; import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider"; import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration"; import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
import { createLogger } from "../../../../lib/logger"; import { createLogger } from "../../../../lib/logger";
import { webhookProductVariantUpdated } from "../../../../webhooks/definitions/product-variant-updated";
export const config = { export const config = {
api: { api: {
@ -16,17 +13,6 @@ export const config = {
}, },
}; };
export const webhookProductVariantUpdated = new SaleorAsyncWebhook<ProductVariantUpdated>({
webhookPath: "api/webhooks/saleor/product_variant_updated",
event: "PRODUCT_VARIANT_UPDATED",
apl: saleorApp.apl,
query: ProductVariantUpdatedDocument,
/**
* Webhook is disabled by default. Will be enabled by the app when configuration succeeds
*/
isActive: false,
});
const logger = createLogger({ const logger = createLogger({
service: "webhookProductVariantUpdatedWebhookHandler", service: "webhookProductVariantUpdatedWebhookHandler",
}); });
@ -35,10 +21,23 @@ export const handler: NextWebhookApiHandler<ProductVariantUpdated> = async (req,
const { event, authData } = context; const { event, authData } = context;
logger.debug( logger.debug(
`New event ${event} (${context.payload?.__typename}) from the ${authData.domain} domain has been received!` `New event ${event} (${context.payload?.__typename}) from the ${authData.domain} domain has been received!`,
); );
const { productVariant } = context.payload;
if (!productVariant) {
logger.error("Webhook did not received expected product data in the payload.");
return res.status(200).end();
}
const { settings, errors } = await getAlgoliaConfiguration({ authData }); const { settings, errors } = await getAlgoliaConfiguration({ authData });
const client = createGraphQLClient({
saleorApiUrl: authData.saleorApiUrl,
token: authData.token,
});
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
const channels = channelsData?.channels || [];
if (errors?.length || !settings) { if (errors?.length || !settings) {
logger.warn("Aborting due to lack of settings"); logger.warn("Aborting due to lack of settings");
@ -53,31 +52,27 @@ export const handler: NextWebhookApiHandler<ProductVariantUpdated> = async (req,
appId: settings.appId, appId: settings.appId,
apiKey: settings.secretKey, apiKey: settings.secretKey,
indexNamePrefix: settings.indexNamePrefix, indexNamePrefix: settings.indexNamePrefix,
channels,
}); });
const { productVariant } = context.payload; try {
logger.debug("Updating variant");
await searchProvider.updateProductVariant(productVariant);
} catch (e) {
logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled");
if (productVariant) { const webhooksToggler = new WebhookActivityTogglerService(
try { authData.appId,
await searchProvider.updateProductVariant(productVariant); createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
} catch (e) { );
logger.info(e, "Algolia updateProductVariant failed. Webhooks will be disabled");
const webhooksToggler = new WebhookActivityTogglerService( logger.trace("Will disable webhooks");
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));
await webhooksToggler.disableOwnWebhooks( logger.trace("Webhooks disabling operation finished");
context.payload.recipient?.webhooks?.map((w) => w.id)
);
logger.trace("Webhooks disabling operation finished"); return res.status(500).send("Operation failed, webhooks are disabled");
return res.status(500).send("Operation failed, webhooks are disabled");
}
} }
res.status(200).end(); res.status(200).end();

View file

@ -6,13 +6,11 @@ import { WebhooksStatus } from "../../components/WebhooksStatus";
import { MainInstructions } from "../../components/MainInstructions"; import { MainInstructions } from "../../components/MainInstructions";
import { WebhooksStatusInstructions } from "../../components/WebhooksStatusInstructions"; import { WebhooksStatusInstructions } from "../../components/WebhooksStatusInstructions";
import { TextLink } from "@saleor/apps-ui"; import { TextLink } from "@saleor/apps-ui";
import { useAppBridge } from "@saleor/app-sdk/app-bridge"; import { IndicesSettings } from "../../components/IndicesSettings";
const ALGOLIA_DASHBOARD_TOKENS_URL = "https://www.algolia.com/account/api-keys/all"; const ALGOLIA_DASHBOARD_TOKENS_URL = "https://www.algolia.com/account/api-keys/all";
export const ConfigurationView = () => { export const ConfigurationView = () => {
const { appBridgeState } = useAppBridge();
return ( return (
<Box display="flex" flexDirection="column" gap={10}> <Box display="flex" flexDirection="column" gap={10}>
<Box> <Box>
@ -57,6 +55,18 @@ export const ConfigurationView = () => {
</Box> </Box>
} }
/> />
<AppSection
includePadding
marginTop={14}
heading="Set indices settings"
mainContent={<IndicesSettings />}
sideContent={
<Box>
<Text>Sets up indices with recommended settings.</Text>
</Box>
}
/>
</Box> </Box>
); );
}; };

View file

@ -0,0 +1,14 @@
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { ProductCreated, ProductCreatedDocument } from "../../../generated/graphql";
import { saleorApp } from "../../../saleor-app";
export const webhookProductCreated = new SaleorAsyncWebhook<ProductCreated>({
webhookPath: "api/webhooks/saleor/product_created",
event: "PRODUCT_CREATED",
apl: saleorApp.apl,
query: ProductCreatedDocument,
/**
* Webhook is disabled by default. Will be enabled by the app when configuration succeeds
*/
isActive: false,
});

View file

@ -0,0 +1,14 @@
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { ProductDeleted, ProductDeletedDocument } from "../../../generated/graphql";
import { saleorApp } from "../../../saleor-app";
export const webhookProductDeleted = new SaleorAsyncWebhook<ProductDeleted>({
webhookPath: "api/webhooks/saleor/product_deleted",
event: "PRODUCT_DELETED",
apl: saleorApp.apl,
query: ProductDeletedDocument,
/**
* Webhook is disabled by default. Will be enabled by the app when configuration succeeds
*/
isActive: false,
});

View file

@ -0,0 +1,14 @@
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { ProductUpdated, ProductUpdatedDocument } from "../../../generated/graphql";
import { saleorApp } from "../../../saleor-app";
export const webhookProductUpdated = new SaleorAsyncWebhook<ProductUpdated>({
webhookPath: "api/webhooks/saleor/product_updated",
event: "PRODUCT_UPDATED",
apl: saleorApp.apl,
query: ProductUpdatedDocument,
/**
* Webhook is disabled by default. Will be enabled by the app when configuration succeeds
*/
isActive: false,
});

View file

@ -0,0 +1,17 @@
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import {
ProductVariantBackInStock,
ProductVariantBackInStockDocument,
} from "../../../generated/graphql";
import { saleorApp } from "../../../saleor-app";
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,
});

View file

@ -0,0 +1,14 @@
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { ProductVariantCreated, ProductVariantCreatedDocument } from "../../../generated/graphql";
import { saleorApp } from "../../../saleor-app";
export const webhookProductVariantCreated = new SaleorAsyncWebhook<ProductVariantCreated>({
webhookPath: "api/webhooks/saleor/product_variant_created",
event: "PRODUCT_VARIANT_CREATED",
apl: saleorApp.apl,
query: ProductVariantCreatedDocument,
/**
* Webhook is disabled by default. Will be enabled by the app when configuration succeeds
*/
isActive: false,
});

View file

@ -0,0 +1,14 @@
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { ProductVariantDeleted, ProductVariantDeletedDocument } from "../../../generated/graphql";
import { saleorApp } from "../../../saleor-app";
export const webhookProductVariantDeleted = new SaleorAsyncWebhook<ProductVariantDeleted>({
webhookPath: "api/webhooks/saleor/product_variant_deleted",
event: "PRODUCT_VARIANT_DELETED",
apl: saleorApp.apl,
query: ProductVariantDeletedDocument,
/**
* Webhook is disabled by default. Will be enabled by the app when configuration succeeds
*/
isActive: false,
});

View file

@ -0,0 +1,17 @@
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import {
ProductVariantOutOfStock,
ProductVariantOutOfStockDocument,
} from "../../../generated/graphql";
import { saleorApp } from "../../../saleor-app";
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,
});

View file

@ -0,0 +1,14 @@
import { SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { ProductVariantUpdated, ProductVariantUpdatedDocument } from "../../../generated/graphql";
import { saleorApp } from "../../../saleor-app";
export const webhookProductVariantUpdated = new SaleorAsyncWebhook<ProductVariantUpdated>({
webhookPath: "api/webhooks/saleor/product_variant_updated",
event: "PRODUCT_VARIANT_UPDATED",
apl: saleorApp.apl,
query: ProductVariantUpdatedDocument,
/**
* Webhook is disabled by default. Will be enabled by the app when configuration succeeds
*/
isActive: false,
});

View file

@ -1,11 +1,11 @@
import { webhookProductCreated } from "./src/pages/api/webhooks/saleor/product_created"; import { webhookProductCreated } from "./src/webhooks/definitions/product-created";
import { webhookProductDeleted } from "./src/pages/api/webhooks/saleor/product_deleted"; import { webhookProductDeleted } from "./src/webhooks/definitions/product-deleted";
import { webhookProductUpdated } from "./src/pages/api/webhooks/saleor/product_updated"; import { webhookProductUpdated } from "./src/webhooks/definitions/product-updated";
import { webhookProductVariantCreated } from "./src/pages/api/webhooks/saleor/product_variant_created"; import { webhookProductVariantBackInStock } from "./src/webhooks/definitions/product-variant-back-in-stock";
import { webhookProductVariantDeleted } from "./src/pages/api/webhooks/saleor/product_variant_deleted"; import { webhookProductVariantCreated } from "./src/webhooks/definitions/product-variant-created";
import { webhookProductVariantUpdated } from "./src/pages/api/webhooks/saleor/product_variant_updated"; import { webhookProductVariantDeleted } from "./src/webhooks/definitions/product-variant-deleted";
import { webhookProductVariantOutOfStock } from "./src/pages/api/webhooks/saleor/product_variant_out_of_stock"; import { webhookProductVariantOutOfStock } from "./src/webhooks/definitions/product-variant-out-of-stock";
import { webhookProductVariantBackInStock } from "./src/pages/api/webhooks/saleor/product_variant_back_in_stock"; import { webhookProductVariantUpdated } from "./src/webhooks/definitions/product-variant-updated";
export const appWebhooks = [ export const appWebhooks = [
webhookProductCreated, webhookProductCreated,

View file

@ -6,3 +6,4 @@ export * from "./src/logger";
export * from "./src/saleor-version-compatibility-validator"; export * from "./src/saleor-version-compatibility-validator";
export * from "./src/create-graphql-client"; export * from "./src/create-graphql-client";
export * from "./src/metadata-manager"; export * from "./src/metadata-manager";
export * from "./src/editor-js/editor-js-plaintext-renderer";

View file

@ -11,9 +11,9 @@ describe("EditorJsPlaintextRenderer", () => {
EditorJsPlaintextRenderer({ EditorJsPlaintextRenderer({
stringData: stringData:
'{"time": 1684697732024, "blocks": [{"id": "HVJ8gMNIXY", "data": {"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris nibh lacus, dignissim at aliquet et, gravida sed velit. Suspendisse at volutpat erat. Lorem ipsum dolor sit amet, consectetur adipiscing elit."}, "type": "paragraph"}], "version": "2.24.3"}', '{"time": 1684697732024, "blocks": [{"id": "HVJ8gMNIXY", "data": {"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris nibh lacus, dignissim at aliquet et, gravida sed velit. Suspendisse at volutpat erat. Lorem ipsum dolor sit amet, consectetur adipiscing elit."}, "type": "paragraph"}], "version": "2.24.3"}',
}) }),
).toBe( ).toBe(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris nibh lacus, dignissim at aliquet et, gravida sed velit. Suspendisse at volutpat erat. Lorem ipsum dolor sit amet, consectetur adipiscing elit." "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris nibh lacus, dignissim at aliquet et, gravida sed velit. Suspendisse at volutpat erat. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
); );
}); });
it("Returns plaintext with no formatting when passed paragraph block with additional styles", () => { it("Returns plaintext with no formatting when passed paragraph block with additional styles", () => {
@ -21,9 +21,9 @@ describe("EditorJsPlaintextRenderer", () => {
EditorJsPlaintextRenderer({ EditorJsPlaintextRenderer({
stringData: stringData:
'{"time": 1684697809104, "blocks": [{"id": "HVJ8gMNIXY", "data": {"text": "Lorem ipsum dolor sit <b>amet</b>, consectetur adipiscing elit. Mauris <s>nibh lacus</s>, dignissim at aliquet et, gravida sed velit. Suspendisse at volutpat erat. <i>Lorem ipsum </i>dolor sit amet, consectetur adipiscing elit."}, "type": "paragraph"}], "version": "2.24.3"}', '{"time": 1684697809104, "blocks": [{"id": "HVJ8gMNIXY", "data": {"text": "Lorem ipsum dolor sit <b>amet</b>, consectetur adipiscing elit. Mauris <s>nibh lacus</s>, dignissim at aliquet et, gravida sed velit. Suspendisse at volutpat erat. <i>Lorem ipsum </i>dolor sit amet, consectetur adipiscing elit."}, "type": "paragraph"}], "version": "2.24.3"}',
}) }),
).toBe( ).toBe(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris nibh lacus, dignissim at aliquet et, gravida sed velit. Suspendisse at volutpat erat. Lorem ipsum dolor sit amet, consectetur adipiscing elit." "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris nibh lacus, dignissim at aliquet et, gravida sed velit. Suspendisse at volutpat erat. Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
); );
}); });
it("Returns text containing angle brackets, when passed block without the style tags", () => { it("Returns text containing angle brackets, when passed block without the style tags", () => {
@ -31,14 +31,14 @@ describe("EditorJsPlaintextRenderer", () => {
EditorJsPlaintextRenderer({ EditorJsPlaintextRenderer({
stringData: stringData:
'{"time": 1684748620371, "blocks": [{"id": "fw-PCw9s-0", "data": {"text": "Everybody knows that 1 &lt; 2 and 1 &gt; 0."}, "type": "paragraph"}, {"id": "eUK1ih8Wmz", "data": {"text": "This is text heart: &lt;3"}, "type": "paragraph"}], "version": "2.24.3"}', '{"time": 1684748620371, "blocks": [{"id": "fw-PCw9s-0", "data": {"text": "Everybody knows that 1 &lt; 2 and 1 &gt; 0."}, "type": "paragraph"}, {"id": "eUK1ih8Wmz", "data": {"text": "This is text heart: &lt;3"}, "type": "paragraph"}], "version": "2.24.3"}',
}) }),
).toBe("Everybody knows that 1 &lt; 2 and 1 &gt; 0.\nThis is text heart: &lt;3"); ).toBe("Everybody knows that 1 &lt; 2 and 1 &gt; 0.\nThis is text heart: &lt;3");
it("Returns numbered list when passed ordered list block", () => { it("Returns numbered list when passed ordered list block", () => {
expect( expect(
EditorJsPlaintextRenderer({ EditorJsPlaintextRenderer({
stringData: stringData:
'{"time": 1684697916091, "blocks": [{"id": "BNL219JhYr", "data": {"items": ["Apples", "Oranges", "Bananas"], "style": "ordered"}, "type": "list"}], "version": "2.24.3"}', '{"time": 1684697916091, "blocks": [{"id": "BNL219JhYr", "data": {"items": ["Apples", "Oranges", "Bananas"], "style": "ordered"}, "type": "list"}], "version": "2.24.3"}',
}) }),
).toBe("1. Apples\n2. Oranges\n3. Bananas"); ).toBe("1. Apples\n2. Oranges\n3. Bananas");
}); });
it("Returns list with dashes when passed unordered list block", () => { it("Returns list with dashes when passed unordered list block", () => {
@ -46,7 +46,7 @@ describe("EditorJsPlaintextRenderer", () => {
EditorJsPlaintextRenderer({ EditorJsPlaintextRenderer({
stringData: stringData:
'{"time": 1684697984679, "blocks": [{"id": "BNL219JhYr", "data": {"items": ["Apples", "Oranges", "Bananas"], "style": "unordered"}, "type": "list"}], "version": "2.24.3"}', '{"time": 1684697984679, "blocks": [{"id": "BNL219JhYr", "data": {"items": ["Apples", "Oranges", "Bananas"], "style": "unordered"}, "type": "list"}], "version": "2.24.3"}',
}) }),
).toBe("- Apples\n- Oranges\n- Bananas"); ).toBe("- Apples\n- Oranges\n- Bananas");
}); });
it("Returns plaintext when header block is passed", () => { it("Returns plaintext when header block is passed", () => {
@ -54,7 +54,7 @@ describe("EditorJsPlaintextRenderer", () => {
EditorJsPlaintextRenderer({ EditorJsPlaintextRenderer({
stringData: stringData:
'{"time": 1684698075115, "blocks": [{"id": "nC-oNRu-pp", "data": {"text": "Lorem ipsum", "level": 1}, "type": "header"}], "version": "2.24.3"}', '{"time": 1684698075115, "blocks": [{"id": "nC-oNRu-pp", "data": {"text": "Lorem ipsum", "level": 1}, "type": "header"}], "version": "2.24.3"}',
}) }),
).toBe("Lorem ipsum"); ).toBe("Lorem ipsum");
}); });
it("Returns text additional new line after header, when theres another block passed", () => { it("Returns text additional new line after header, when theres another block passed", () => {
@ -62,7 +62,7 @@ describe("EditorJsPlaintextRenderer", () => {
EditorJsPlaintextRenderer({ EditorJsPlaintextRenderer({
stringData: stringData:
'{"time": 1684748016130, "blocks": [{"id": "nC-oNRu-pp", "data": {"text": "This is header", "level": 1}, "type": "header"}, {"id": "fw-PCw9s-0", "data": {"text": "There should be additional new line between header and paragraph"}, "type": "paragraph"}], "version": "2.24.3"}', '{"time": 1684748016130, "blocks": [{"id": "nC-oNRu-pp", "data": {"text": "This is header", "level": 1}, "type": "header"}, {"id": "fw-PCw9s-0", "data": {"text": "There should be additional new line between header and paragraph"}, "type": "paragraph"}], "version": "2.24.3"}',
}) }),
).toBe("This is header\n\nThere should be additional new line between header and paragraph"); ).toBe("This is header\n\nThere should be additional new line between header and paragraph");
}); });
it("Returns text when passed all types of blocks", () => { it("Returns text when passed all types of blocks", () => {
@ -70,9 +70,9 @@ describe("EditorJsPlaintextRenderer", () => {
EditorJsPlaintextRenderer({ EditorJsPlaintextRenderer({
stringData: stringData:
'{"time": 1684698250098, "blocks": [{"id": "nC-oNRu-pp", "data": {"text": "Lorem ipsum", "level": 1}, "type": "header"}, {"id": "1ADVi9cvw8", "data": {"text": "This is <b>introduction</b> to the list of things"}, "type": "paragraph"}, {"id": "7OFi_vE_hc", "data": {"items": ["Red", "Blue"], "style": "ordered"}, "type": "list"}, {"id": "PYLABJ1KWZ", "data": {"text": "Closing thoughts."}, "type": "paragraph"}], "version": "2.24.3"}', '{"time": 1684698250098, "blocks": [{"id": "nC-oNRu-pp", "data": {"text": "Lorem ipsum", "level": 1}, "type": "header"}, {"id": "1ADVi9cvw8", "data": {"text": "This is <b>introduction</b> to the list of things"}, "type": "paragraph"}, {"id": "7OFi_vE_hc", "data": {"items": ["Red", "Blue"], "style": "ordered"}, "type": "list"}, {"id": "PYLABJ1KWZ", "data": {"text": "Closing thoughts."}, "type": "paragraph"}], "version": "2.24.3"}',
}) }),
).toBe( ).toBe(
"Lorem ipsum\n\nThis is introduction to the list of things\n1. Red\n2. Blue\nClosing thoughts." "Lorem ipsum\n\nThis is introduction to the list of things\n1. Red\n2. Blue\nClosing thoughts.",
); );
}); });
}); });

File diff suppressed because it is too large Load diff