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:
parent
a81f061fcf
commit
2a1385bab1
47 changed files with 1145 additions and 918 deletions
5
.changeset/great-trains-judge.md
Normal file
5
.changeset/great-trains-judge.md
Normal 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.
|
5
.changeset/grumpy-hairs-sit.md
Normal file
5
.changeset/grumpy-hairs-sit.md
Normal 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.
|
6
.changeset/metal-brooms-cheer.md
Normal file
6
.changeset/metal-brooms-cheer.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
"saleor-app-products-feed": patch
|
||||
"@saleor/apps-shared": patch
|
||||
---
|
||||
|
||||
Plaintext EditorJS renderer has been moved to the shared package.
|
5
.changeset/moody-frogs-bow.md
Normal file
5
.changeset/moody-frogs-bow.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-app-search": patch
|
||||
---
|
||||
|
||||
Added script responsible for webhook migration.
|
5
.changeset/pretty-apes-float.md
Normal file
5
.changeset/pretty-apes-float.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"saleor-app-search": patch
|
||||
---
|
||||
|
||||
Fixed issue with stale product variants after product deletion or channel visibility settings change.
|
|
@ -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
|
|
@ -2,12 +2,12 @@ import { XMLBuilder } from "fast-xml-parser";
|
|||
import { GoogleFeedProductVariantFragment } from "../../../generated/graphql";
|
||||
import { productToProxy } from "./product-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 { getMappedAttributes } from "./attribute-mapping";
|
||||
import { priceMapping } from "./price-mapping";
|
||||
import { renderHandlebarsTemplate } from "../handlebarsTemplates/render-handlebars-template";
|
||||
import { transformTemplateFormat } from "../handlebarsTemplates/transform-template-format";
|
||||
import { EditorJsPlaintextRenderer } from "@saleor/apps-shared";
|
||||
|
||||
interface GenerateGoogleXmlFeedArgs {
|
||||
productVariants: GoogleFeedProductVariantFragment[];
|
||||
|
|
|
@ -2,6 +2,7 @@ fragment OwnWebhook on Webhook {
|
|||
id
|
||||
isActive
|
||||
name
|
||||
targetUrl
|
||||
asyncEvents {
|
||||
name
|
||||
eventType
|
||||
|
|
|
@ -19,6 +19,7 @@ fragment ProductVariantData on ProductVariant {
|
|||
id
|
||||
channel {
|
||||
slug
|
||||
currencyCode
|
||||
}
|
||||
price {
|
||||
currency
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
query Channels {
|
||||
channels {
|
||||
slug
|
||||
currencyCode
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
query FetchOwnWebhooks($id: ID!) {
|
||||
app(id: $id) {
|
||||
id
|
||||
name
|
||||
isActive
|
||||
webhooks {
|
||||
...OwnWebhook
|
||||
}
|
||||
|
|
|
@ -4,7 +4,13 @@ subscription ProductDeleted {
|
|||
... on ProductDeleted {
|
||||
__typename
|
||||
product {
|
||||
...ProductWebhookPayload
|
||||
id
|
||||
variants{
|
||||
id
|
||||
product{
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
recipient {
|
||||
|
|
|
@ -2,9 +2,11 @@ subscription ProductVariantDeleted {
|
|||
event {
|
||||
__typename
|
||||
... on ProductVariantDeleted {
|
||||
__typename
|
||||
productVariant {
|
||||
...ProductVariantWebhookPayload
|
||||
id
|
||||
product{
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
recipient {
|
||||
|
|
|
@ -22,8 +22,10 @@
|
|||
"@tanstack/react-query": "4.29.19",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@urql/exchange-auth": "^2.1.4",
|
||||
"algoliasearch": "4.14.2",
|
||||
"algoliasearch": "4.19.1",
|
||||
"clsx": "^1.2.1",
|
||||
"debug": "^4.3.4",
|
||||
"dotenv": "^16.3.1",
|
||||
"graphql": "16.7.1",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"next": "13.4.8",
|
||||
|
|
7
apps/search/scripts/migrations/README.md
Normal file
7
apps/search/scripts/migrations/README.md
Normal 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`
|
20
apps/search/scripts/migrations/migration-utils.ts
Normal file
20
apps/search/scripts/migrations/migration-utils.ts
Normal 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();
|
||||
};
|
52
apps/search/scripts/migrations/recreate-webhooks.ts
Normal file
52
apps/search/scripts/migrations/recreate-webhooks.ts
Normal 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);
|
||||
}
|
||||
};
|
|
@ -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();
|
30
apps/search/scripts/migrations/run-webhooks-migration.ts
Normal file
30
apps/search/scripts/migrations/run-webhooks-migration.ts
Normal 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();
|
74
apps/search/src/components/IndicesSettings.tsx
Normal file
74
apps/search/src/components/IndicesSettings.tsx
Normal 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'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>
|
||||
);
|
||||
};
|
|
@ -5,55 +5,93 @@ import {
|
|||
} from "../../../generated/graphql";
|
||||
import { isNotNil } from "../isNotNil";
|
||||
import { SearchProvider } from "../searchProvider";
|
||||
import { createDebug } from "../debug";
|
||||
import {
|
||||
AlgoliaObject,
|
||||
channelListingToAlgoliaIndexId,
|
||||
productAndVariantToAlgolia,
|
||||
productAndVariantToObjectID,
|
||||
} from "./algoliaUtils";
|
||||
import { createLogger } from "@saleor/apps-shared";
|
||||
|
||||
export interface AlgoliaSearchProviderOptions {
|
||||
appId: string;
|
||||
apiKey: string;
|
||||
indexNamePrefix?: string;
|
||||
channels?: Array<{ slug: string; currencyCode: string }>;
|
||||
}
|
||||
|
||||
const debug = createDebug("AlgoliaSearchProvider");
|
||||
const logger = createLogger({ name: "AlgoliaSearchProvider" });
|
||||
|
||||
export class AlgoliaSearchProvider implements SearchProvider {
|
||||
#algolia: SearchClient;
|
||||
#indexNamePrefix?: string | undefined;
|
||||
#indexNames: Array<string>;
|
||||
|
||||
constructor({ appId, apiKey, indexNamePrefix }: AlgoliaSearchProviderOptions) {
|
||||
constructor({ appId, apiKey, indexNamePrefix, channels }: AlgoliaSearchProviderOptions) {
|
||||
this.#algolia = Algoliasearch(appId, apiKey);
|
||||
this.#indexNamePrefix = indexNamePrefix;
|
||||
this.#indexNames =
|
||||
channels?.map((c) => channelListingToAlgoliaIndexId({ channel: c }, this.#indexNamePrefix)) ||
|
||||
[];
|
||||
}
|
||||
|
||||
private async saveGroupedByIndex(groupedByIndex: GroupedByIndex) {
|
||||
debug("saveGroupedByIndex called");
|
||||
logger.debug("saveGroupedByIndex called");
|
||||
return Promise.all(
|
||||
Object.entries(groupedByIndex).map(([indexName, objects]) => {
|
||||
const index = this.#algolia.initIndex(indexName);
|
||||
|
||||
return index.saveObjects(objects);
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async deleteGroupedByIndex(groupedByIndex: GroupedByIndex) {
|
||||
debug("deleteGroupedByIndex called");
|
||||
private async deleteGroupedByIndex(groupedByIndex: IdsGroupedByIndex) {
|
||||
logger.debug("deleteGroupedByIndex called");
|
||||
|
||||
return Promise.all(
|
||||
Object.entries(groupedByIndex).map(([indexName, objects]) => {
|
||||
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[]) {
|
||||
debug(`updatedBatchProducts called`);
|
||||
logger.debug(`updatedBatchProducts called`);
|
||||
|
||||
const groupedByIndex = groupProductsByIndexName(productsBatch, {
|
||||
visibleInListings: true,
|
||||
|
@ -64,41 +102,39 @@ export class AlgoliaSearchProvider implements SearchProvider {
|
|||
}
|
||||
|
||||
async createProduct(product: ProductWebhookPayloadFragment) {
|
||||
debug(`createProduct called`);
|
||||
logger.debug(`createProduct called`);
|
||||
await this.updateProduct(product);
|
||||
}
|
||||
|
||||
async updateProduct(product: ProductWebhookPayloadFragment) {
|
||||
debug(`updateProduct called`);
|
||||
logger.debug(`updateProduct called`);
|
||||
|
||||
if (!product.variants) {
|
||||
debug("Product has no variants - abort");
|
||||
logger.debug("Product has no variants - abort");
|
||||
return;
|
||||
}
|
||||
await Promise.all(product.variants.map((variant) => this.updateProductVariant(variant)));
|
||||
}
|
||||
|
||||
async deleteProduct(product: ProductWebhookPayloadFragment) {
|
||||
debug(`deleteProduct`);
|
||||
if (!product.variants) {
|
||||
debug("Product has no variants - abort");
|
||||
return;
|
||||
}
|
||||
await Promise.all(product.variants.map((variant) => this.deleteProductVariant(variant)));
|
||||
logger.debug(`deleteProduct`);
|
||||
|
||||
await Promise.all(
|
||||
this.#indexNames.map((indexName) => {
|
||||
const index = this.#algolia.initIndex(indexName);
|
||||
|
||||
return index.deleteBy({ filters: `productId:"${product.id}"` });
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async createProductVariant(productVariant: ProductVariantWebhookPayloadFragment) {
|
||||
debug(`createProductVariant called`);
|
||||
logger.debug(`createProductVariant called`);
|
||||
return this.updateProductVariant(productVariant);
|
||||
}
|
||||
|
||||
async updateProductVariant(productVariant: ProductVariantWebhookPayloadFragment) {
|
||||
debug(`updateProductVariant called`);
|
||||
|
||||
if (!productVariant.product.channelListings) {
|
||||
debug("Product has no channelListings - abort");
|
||||
return;
|
||||
}
|
||||
logger.debug(`updateProductVariant called`);
|
||||
|
||||
const groupedByIndexToSave = groupVariantByIndexName(productVariant, {
|
||||
visibleInListings: true,
|
||||
|
@ -109,32 +145,32 @@ export class AlgoliaSearchProvider implements SearchProvider {
|
|||
await this.saveGroupedByIndex(groupedByIndexToSave);
|
||||
}
|
||||
|
||||
const groupedByIndexToDelete = groupVariantByIndexName(productVariant, {
|
||||
visibleInListings: false,
|
||||
indexNamePrefix: this.#indexNamePrefix,
|
||||
});
|
||||
/*
|
||||
* When the variant is removed from a channel, Saleor does not send it's channel listing in the event payload.
|
||||
* 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) {
|
||||
await this.deleteGroupedByIndex(groupedByIndexToDelete);
|
||||
if (staleIndices) {
|
||||
await this.deleteGroupedByIndex(
|
||||
Object.fromEntries(
|
||||
staleIndices.map((index) => [index, [productAndVariantToObjectID(productVariant)]]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteProductVariant(productVariant: ProductVariantWebhookPayloadFragment) {
|
||||
debug(`deleteProductVariant called`);
|
||||
logger.debug(`deleteProductVariant called`);
|
||||
|
||||
if (!productVariant.product.channelListings) {
|
||||
debug("No channel listing data - return");
|
||||
return;
|
||||
}
|
||||
|
||||
const groupedByIndexToDelete = groupVariantByIndexName(productVariant, {
|
||||
visibleInListings: null,
|
||||
indexNamePrefix: this.#indexNamePrefix,
|
||||
});
|
||||
|
||||
if (groupedByIndexToDelete) {
|
||||
await this.deleteGroupedByIndex(groupedByIndexToDelete);
|
||||
}
|
||||
await this.deleteGroupedByIndex(
|
||||
Object.fromEntries(
|
||||
this.#indexNames.map((index) => [index, [productAndVariantToObjectID(productVariant)]]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async ping() {
|
||||
|
@ -150,23 +186,41 @@ export class AlgoliaSearchProvider implements SearchProvider {
|
|||
}
|
||||
|
||||
type GroupedByIndex = Record<string, AlgoliaObject[]>;
|
||||
type IdsGroupedByIndex = Record<string, Array<string>>;
|
||||
|
||||
const groupVariantByIndexName = (
|
||||
productVariant: ProductVariantWebhookPayloadFragment,
|
||||
{
|
||||
visibleInListings,
|
||||
indexNamePrefix,
|
||||
}: { visibleInListings: true | false | null; indexNamePrefix: string | undefined }
|
||||
}: { visibleInListings: true | false | null; indexNamePrefix: string | undefined },
|
||||
) => {
|
||||
if (!productVariant.product.channelListings) {
|
||||
return null;
|
||||
logger.debug("Grouping variants per index name");
|
||||
if (!productVariant.channelListings) {
|
||||
logger.debug("Product variant has no channel listings - abort");
|
||||
return {};
|
||||
}
|
||||
|
||||
const objectsToSaveByIndexName = productVariant.product.channelListings
|
||||
.filter((channelListing) =>
|
||||
const objectsToSaveByIndexName = productVariant.channelListings
|
||||
.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
|
||||
visibleInListings === null ? true : channelListing.visibleInListings === visibleInListings
|
||||
)
|
||||
return visibleInListings === null
|
||||
? true
|
||||
: productChannelListing.visibleInListings === visibleInListings;
|
||||
})
|
||||
.map((channelListing) => {
|
||||
const object = productAndVariantToAlgolia({
|
||||
variant: productVariant,
|
||||
|
@ -192,9 +246,9 @@ const groupProductsByIndexName = (
|
|||
{
|
||||
visibleInListings,
|
||||
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
|
||||
.flatMap((p) => p.variants)
|
||||
.filter(isNotNil)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { EditorJsPlaintextRenderer } from "@saleor/apps-shared";
|
||||
import {
|
||||
ProductAttributesDataFragment,
|
||||
ProductVariantWebhookPayloadFragment,
|
||||
|
@ -127,6 +128,7 @@ export function productAndVariantToAlgolia({
|
|||
attributes,
|
||||
media,
|
||||
description: safeParseJson(product.description),
|
||||
descriptionPlaintext: EditorJsPlaintextRenderer({ stringData: product.description }),
|
||||
slug: product.slug,
|
||||
thumbnail: product.thumbnail?.url,
|
||||
grossPrice: listing?.price?.amount,
|
||||
|
|
26
apps/search/src/lib/useIndicesSetup.tsx
Normal file
26
apps/search/src/lib/useIndicesSetup.tsx
Normal 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,
|
||||
});
|
||||
};
|
|
@ -10,6 +10,7 @@ import { AppConfigurationFields } from "../../domain/configuration";
|
|||
import { AlgoliaSearchProvider } from "../../lib/algolia/algoliaSearchProvider";
|
||||
import { WebhookActivityTogglerService } from "../../domain/WebhookActivityToggler.service";
|
||||
import { createGraphQLClient } from "@saleor/apps-shared";
|
||||
import { ChannelsDocument } from "../../../generated/graphql";
|
||||
|
||||
const logger = createLogger({
|
||||
handler: "api/configuration",
|
||||
|
@ -24,7 +25,7 @@ const sendResponse = async (
|
|||
res: NextApiResponse<SettingsApiResponse>,
|
||||
statusCode: number,
|
||||
settings: SettingsManager,
|
||||
domain: string
|
||||
domain: string,
|
||||
) => {
|
||||
const data = {
|
||||
secretKey: (await settings.get("secretKey", domain)) || "",
|
||||
|
@ -43,7 +44,7 @@ const sendResponse = async (
|
|||
export const handler = async (
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
ctx: ProtectedHandlerContext
|
||||
ctx: ProtectedHandlerContext,
|
||||
) => {
|
||||
const {
|
||||
authData: { token, saleorApiUrl },
|
||||
|
@ -70,10 +71,14 @@ export const handler = async (
|
|||
|
||||
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({
|
||||
appId,
|
||||
apiKey: secretKey,
|
||||
indexNamePrefix: indexNamePrefix,
|
||||
channels,
|
||||
});
|
||||
|
||||
try {
|
||||
|
|
78
apps/search/src/pages/api/setup-indices.ts
Normal file
78
apps/search/src/pages/api/setup-indices.ts
Normal 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,
|
||||
[],
|
||||
);
|
|
@ -1,11 +1,11 @@
|
|||
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||
import { ProductCreated, ProductCreatedDocument } from "../../../../../generated/graphql";
|
||||
import { saleorApp } from "../../../../../saleor-app";
|
||||
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
|
||||
import { ChannelsDocument, ProductCreated } from "../../../../../generated/graphql";
|
||||
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
|
||||
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
|
||||
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
|
||||
import { createLogger } from "../../../../lib/logger";
|
||||
import { createGraphQLClient } from "@saleor/apps-shared";
|
||||
import { webhookProductCreated } from "../../../../webhooks/definitions/product-created";
|
||||
|
||||
export const config = {
|
||||
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({
|
||||
service: "webhookProductCreatedWebhookHandler",
|
||||
});
|
||||
|
@ -32,10 +21,23 @@ export const handler: NextWebhookApiHandler<ProductCreated> = async (req, res, c
|
|||
const { event, authData } = context;
|
||||
|
||||
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 client = createGraphQLClient({
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
token: authData.token,
|
||||
});
|
||||
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
|
||||
const channels = channelsData?.channels || [];
|
||||
|
||||
if (errors?.length || !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,
|
||||
apiKey: settings.secretKey,
|
||||
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) {
|
||||
try {
|
||||
await searchProvider.createProduct(product);
|
||||
} catch (e) {
|
||||
logger.info(e, "Algolia createProduct failed. Webhooks will be disabled");
|
||||
const webhooksToggler = new WebhookActivityTogglerService(
|
||||
authData.appId,
|
||||
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
|
||||
);
|
||||
|
||||
const webhooksToggler = new WebhookActivityTogglerService(
|
||||
authData.appId,
|
||||
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token })
|
||||
);
|
||||
logger.trace("Will disable webhooks");
|
||||
|
||||
logger.trace("Will disable webhooks");
|
||||
await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id));
|
||||
|
||||
await webhooksToggler.disableOwnWebhooks(
|
||||
context.payload.recipient?.webhooks?.map((w) => w.id)
|
||||
);
|
||||
logger.trace("Webhooks disabling operation finished");
|
||||
|
||||
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();
|
||||
return;
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||
import { ProductDeleted, ProductDeletedDocument } from "../../../../../generated/graphql";
|
||||
import { saleorApp } from "../../../../../saleor-app";
|
||||
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
|
||||
import { ChannelsDocument, ProductDeleted } from "../../../../../generated/graphql";
|
||||
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
|
||||
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
|
||||
import { createDebug } from "../../../../lib/debug";
|
||||
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
|
||||
import { createLogger } from "../../../../lib/logger";
|
||||
import { createGraphQLClient } from "@saleor/apps-shared";
|
||||
import { webhookProductDeleted } from "../../../../webhooks/definitions/product-deleted";
|
||||
|
||||
export const config = {
|
||||
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({
|
||||
service: "webhookProductDeletedWebhookHandler",
|
||||
});
|
||||
|
@ -33,10 +21,23 @@ export const handler: NextWebhookApiHandler<ProductDeleted> = async (req, res, c
|
|||
const { event, authData } = context;
|
||||
|
||||
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 client = createGraphQLClient({
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
token: authData.token,
|
||||
});
|
||||
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
|
||||
const channels = channelsData?.channels || [];
|
||||
|
||||
if (errors?.length || !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,
|
||||
apiKey: settings.secretKey,
|
||||
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) {
|
||||
try {
|
||||
await searchProvider.deleteProduct(product);
|
||||
} catch (e) {
|
||||
logger.info(e, "Algolia deleteProduct failed. Webhooks will be disabled");
|
||||
const webhooksToggler = new WebhookActivityTogglerService(
|
||||
authData.appId,
|
||||
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
|
||||
);
|
||||
|
||||
const webhooksToggler = new WebhookActivityTogglerService(
|
||||
authData.appId,
|
||||
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token })
|
||||
);
|
||||
logger.trace("Will disable webhooks");
|
||||
|
||||
logger.trace("Will disable webhooks");
|
||||
await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id));
|
||||
|
||||
await webhooksToggler.disableOwnWebhooks(
|
||||
context.payload.recipient?.webhooks?.map((w) => w.id)
|
||||
);
|
||||
logger.trace("Webhooks disabling operation finished");
|
||||
|
||||
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();
|
||||
return;
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||
import { ProductUpdated, ProductUpdatedDocument } from "../../../../../generated/graphql";
|
||||
import { saleorApp } from "../../../../../saleor-app";
|
||||
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
|
||||
import { ChannelsDocument, ProductUpdated } from "../../../../../generated/graphql";
|
||||
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
|
||||
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
|
||||
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
|
||||
import { createLogger } from "../../../../lib/logger";
|
||||
import { createGraphQLClient } from "@saleor/apps-shared";
|
||||
import { webhookProductUpdated } from "../../../../webhooks/definitions/product-updated";
|
||||
|
||||
export const config = {
|
||||
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({
|
||||
service: "webhookProductUpdatedWebhookHandler",
|
||||
});
|
||||
|
@ -32,10 +21,23 @@ export const handler: NextWebhookApiHandler<ProductUpdated> = async (req, res, c
|
|||
const { event, authData } = context;
|
||||
|
||||
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 client = createGraphQLClient({
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
token: authData.token,
|
||||
});
|
||||
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
|
||||
const channels = channelsData?.channels || [];
|
||||
|
||||
if (errors?.length || !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,
|
||||
apiKey: settings.secretKey,
|
||||
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) {
|
||||
try {
|
||||
await searchProvider.updateProduct(product);
|
||||
} catch (e) {
|
||||
logger.info(e, "Algolia updateProduct failed. Webhooks will be disabled");
|
||||
const webhooksToggler = new WebhookActivityTogglerService(
|
||||
authData.appId,
|
||||
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
|
||||
);
|
||||
|
||||
const webhooksToggler = new WebhookActivityTogglerService(
|
||||
authData.appId,
|
||||
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token })
|
||||
);
|
||||
logger.trace("Will disable webhooks");
|
||||
|
||||
logger.trace("Will disable webhooks");
|
||||
await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id));
|
||||
|
||||
await webhooksToggler.disableOwnWebhooks(
|
||||
context.payload.recipient?.webhooks?.map((w) => w.id)
|
||||
);
|
||||
logger.trace("Webhooks disabling operation finished");
|
||||
|
||||
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();
|
||||
|
|
|
@ -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 {
|
||||
ProductVariantBackInStock,
|
||||
ProductVariantBackInStockDocument,
|
||||
} from "../../../../../generated/graphql";
|
||||
import { saleorApp } from "../../../../../saleor-app";
|
||||
import { ChannelsDocument, ProductVariantBackInStock } from "../../../../../generated/graphql";
|
||||
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
|
||||
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
|
||||
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
|
||||
import { createLogger } from "../../../../lib/logger";
|
||||
import { webhookProductVariantBackInStock } from "../../../../webhooks/definitions/product-variant-back-in-stock";
|
||||
|
||||
export const config = {
|
||||
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({
|
||||
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!`,
|
||||
);
|
||||
|
||||
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 client = createGraphQLClient({
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
token: authData.token,
|
||||
});
|
||||
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
|
||||
const channels = channelsData?.channels || [];
|
||||
|
||||
if (errors?.length || !settings) {
|
||||
logger.warn("Aborting due to lack of settings");
|
||||
|
@ -57,31 +56,26 @@ export const handler: NextWebhookApiHandler<ProductVariantBackInStock> = async (
|
|||
appId: settings.appId,
|
||||
apiKey: settings.secretKey,
|
||||
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) {
|
||||
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 }),
|
||||
);
|
||||
|
||||
const webhooksToggler = new WebhookActivityTogglerService(
|
||||
authData.appId,
|
||||
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
|
||||
);
|
||||
logger.trace("Will disable webhooks");
|
||||
|
||||
logger.trace("Will disable webhooks");
|
||||
await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id));
|
||||
|
||||
await webhooksToggler.disableOwnWebhooks(
|
||||
context.payload.recipient?.webhooks?.map((w) => w.id),
|
||||
);
|
||||
logger.trace("Webhooks disabling operation finished");
|
||||
|
||||
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();
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||
import {
|
||||
ProductVariantCreated,
|
||||
ProductVariantCreatedDocument,
|
||||
} from "../../../../../generated/graphql";
|
||||
import { saleorApp } from "../../../../../saleor-app";
|
||||
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
|
||||
import { ChannelsDocument, ProductVariantCreated } from "../../../../../generated/graphql";
|
||||
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
|
||||
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
|
||||
import { createDebug } from "../../../../lib/debug";
|
||||
import { createLogger } from "../../../../lib/logger";
|
||||
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
|
||||
import { createGraphQLClient } from "@saleor/apps-shared";
|
||||
import { webhookProductVariantCreated } from "../../../../webhooks/definitions/product-variant-created";
|
||||
|
||||
export const config = {
|
||||
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({
|
||||
service: "webhookProductVariantCreatedWebhookHandler",
|
||||
});
|
||||
|
@ -36,10 +21,23 @@ export const handler: NextWebhookApiHandler<ProductVariantCreated> = async (req,
|
|||
const { event, authData } = context;
|
||||
|
||||
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 client = createGraphQLClient({
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
token: authData.token,
|
||||
});
|
||||
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
|
||||
const channels = channelsData?.channels || [];
|
||||
|
||||
if (errors?.length || !settings) {
|
||||
logger.warn("Aborting due to lack of settings");
|
||||
|
@ -53,31 +51,26 @@ export const handler: NextWebhookApiHandler<ProductVariantCreated> = async (req,
|
|||
appId: settings.appId,
|
||||
apiKey: settings.secretKey,
|
||||
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) {
|
||||
try {
|
||||
await searchProvider.createProductVariant(productVariant);
|
||||
} catch (e) {
|
||||
logger.info(e, "Algolia createProductVariant failed. Webhooks will be disabled");
|
||||
const webhooksToggler = new WebhookActivityTogglerService(
|
||||
authData.appId,
|
||||
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
|
||||
);
|
||||
|
||||
const webhooksToggler = new WebhookActivityTogglerService(
|
||||
authData.appId,
|
||||
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token })
|
||||
);
|
||||
logger.trace("Will disable webhooks");
|
||||
|
||||
logger.trace("Will disable webhooks");
|
||||
await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id));
|
||||
|
||||
await webhooksToggler.disableOwnWebhooks(
|
||||
context.payload.recipient?.webhooks?.map((w) => w.id)
|
||||
);
|
||||
logger.trace("Webhooks disabling operation finished");
|
||||
|
||||
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();
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
|
||||
import {
|
||||
ProductVariantDeleted,
|
||||
ProductVariantDeletedDocument,
|
||||
} from "../../../../../generated/graphql";
|
||||
import { saleorApp } from "../../../../../saleor-app";
|
||||
import { NextWebhookApiHandler } from "@saleor/app-sdk/handlers/next";
|
||||
import { ChannelsDocument, ProductVariantDeleted } from "../../../../../generated/graphql";
|
||||
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
|
||||
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
|
||||
import { createDebug } from "../../../../lib/debug";
|
||||
import { createLogger } from "../../../../lib/logger";
|
||||
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
|
||||
import { createGraphQLClient } from "@saleor/apps-shared";
|
||||
import { webhookProductVariantDeleted } from "../../../../webhooks/definitions/product-variant-deleted";
|
||||
|
||||
export const config = {
|
||||
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({
|
||||
service: "webhookProductVariantDeletedWebhookHandler",
|
||||
});
|
||||
|
@ -36,10 +21,23 @@ export const handler: NextWebhookApiHandler<ProductVariantDeleted> = async (req,
|
|||
const { event, authData } = context;
|
||||
|
||||
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 client = createGraphQLClient({
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
token: authData.token,
|
||||
});
|
||||
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
|
||||
const channels = channelsData?.channels || [];
|
||||
|
||||
if (errors?.length || !settings) {
|
||||
logger.warn("Aborting due to lack of settings");
|
||||
|
@ -54,31 +52,26 @@ export const handler: NextWebhookApiHandler<ProductVariantDeleted> = async (req,
|
|||
appId: settings.appId,
|
||||
apiKey: settings.secretKey,
|
||||
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) {
|
||||
try {
|
||||
await searchProvider.deleteProductVariant(productVariant);
|
||||
} catch (e) {
|
||||
logger.info(e, "Algolia deleteProductVariant failed. Webhooks will be disabled");
|
||||
const webhooksToggler = new WebhookActivityTogglerService(
|
||||
authData.appId,
|
||||
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
|
||||
);
|
||||
|
||||
const webhooksToggler = new WebhookActivityTogglerService(
|
||||
authData.appId,
|
||||
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token })
|
||||
);
|
||||
logger.trace("Will disable webhooks");
|
||||
|
||||
logger.trace("Will disable webhooks");
|
||||
await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id));
|
||||
|
||||
await webhooksToggler.disableOwnWebhooks(
|
||||
context.payload.recipient?.webhooks?.map((w) => w.id)
|
||||
);
|
||||
logger.trace("Webhooks disabling operation finished");
|
||||
|
||||
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();
|
||||
|
|
|
@ -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 {
|
||||
ProductVariantOutOfStock,
|
||||
ProductVariantOutOfStockDocument,
|
||||
} from "../../../../../generated/graphql";
|
||||
import { saleorApp } from "../../../../../saleor-app";
|
||||
import { ChannelsDocument, ProductVariantOutOfStock } from "../../../../../generated/graphql";
|
||||
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
|
||||
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
|
||||
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
|
||||
import { createLogger } from "../../../../lib/logger";
|
||||
import { webhookProductVariantOutOfStock } from "../../../../webhooks/definitions/product-variant-out-of-stock";
|
||||
|
||||
export const config = {
|
||||
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({
|
||||
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!`,
|
||||
);
|
||||
|
||||
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 client = createGraphQLClient({
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
token: authData.token,
|
||||
});
|
||||
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
|
||||
const channels = channelsData?.channels || [];
|
||||
|
||||
if (errors?.length || !settings) {
|
||||
logger.warn("Aborting due to lack of settings");
|
||||
|
@ -57,31 +56,26 @@ export const handler: NextWebhookApiHandler<ProductVariantOutOfStock> = async (
|
|||
appId: settings.appId,
|
||||
apiKey: settings.secretKey,
|
||||
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) {
|
||||
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 }),
|
||||
);
|
||||
|
||||
const webhooksToggler = new WebhookActivityTogglerService(
|
||||
authData.appId,
|
||||
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token }),
|
||||
);
|
||||
logger.trace("Will disable webhooks");
|
||||
|
||||
logger.trace("Will disable webhooks");
|
||||
await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id));
|
||||
|
||||
await webhooksToggler.disableOwnWebhooks(
|
||||
context.payload.recipient?.webhooks?.map((w) => w.id),
|
||||
);
|
||||
logger.trace("Webhooks disabling operation finished");
|
||||
|
||||
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();
|
||||
|
|
|
@ -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 {
|
||||
ProductVariantUpdated,
|
||||
ProductVariantUpdatedDocument,
|
||||
} from "../../../../../generated/graphql";
|
||||
import { saleorApp } from "../../../../../saleor-app";
|
||||
import { ChannelsDocument, ProductVariantUpdated } from "../../../../../generated/graphql";
|
||||
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
|
||||
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
|
||||
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
|
||||
import { createLogger } from "../../../../lib/logger";
|
||||
import { webhookProductVariantUpdated } from "../../../../webhooks/definitions/product-variant-updated";
|
||||
|
||||
export const config = {
|
||||
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({
|
||||
service: "webhookProductVariantUpdatedWebhookHandler",
|
||||
});
|
||||
|
@ -35,10 +21,23 @@ export const handler: NextWebhookApiHandler<ProductVariantUpdated> = async (req,
|
|||
const { event, authData } = context;
|
||||
|
||||
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 client = createGraphQLClient({
|
||||
saleorApiUrl: authData.saleorApiUrl,
|
||||
token: authData.token,
|
||||
});
|
||||
const { data: channelsData } = await client.query(ChannelsDocument, {}).toPromise();
|
||||
const channels = channelsData?.channels || [];
|
||||
|
||||
if (errors?.length || !settings) {
|
||||
logger.warn("Aborting due to lack of settings");
|
||||
|
@ -53,31 +52,27 @@ export const handler: NextWebhookApiHandler<ProductVariantUpdated> = async (req,
|
|||
appId: settings.appId,
|
||||
apiKey: settings.secretKey,
|
||||
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) {
|
||||
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 }),
|
||||
);
|
||||
|
||||
const webhooksToggler = new WebhookActivityTogglerService(
|
||||
authData.appId,
|
||||
createGraphQLClient({ saleorApiUrl: authData.saleorApiUrl, token: authData.token })
|
||||
);
|
||||
logger.trace("Will disable webhooks");
|
||||
|
||||
logger.trace("Will disable webhooks");
|
||||
await webhooksToggler.disableOwnWebhooks(context.payload.recipient?.webhooks?.map((w) => w.id));
|
||||
|
||||
await webhooksToggler.disableOwnWebhooks(
|
||||
context.payload.recipient?.webhooks?.map((w) => w.id)
|
||||
);
|
||||
logger.trace("Webhooks disabling operation finished");
|
||||
|
||||
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();
|
||||
|
|
|
@ -6,13 +6,11 @@ import { WebhooksStatus } from "../../components/WebhooksStatus";
|
|||
import { MainInstructions } from "../../components/MainInstructions";
|
||||
import { WebhooksStatusInstructions } from "../../components/WebhooksStatusInstructions";
|
||||
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";
|
||||
|
||||
export const ConfigurationView = () => {
|
||||
const { appBridgeState } = useAppBridge();
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="column" gap={10}>
|
||||
<Box>
|
||||
|
@ -57,6 +55,18 @@ export const ConfigurationView = () => {
|
|||
</Box>
|
||||
}
|
||||
/>
|
||||
|
||||
<AppSection
|
||||
includePadding
|
||||
marginTop={14}
|
||||
heading="Set indices settings"
|
||||
mainContent={<IndicesSettings />}
|
||||
sideContent={
|
||||
<Box>
|
||||
<Text>Sets up indices with recommended settings.</Text>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
14
apps/search/src/webhooks/definitions/product-created.ts
Normal file
14
apps/search/src/webhooks/definitions/product-created.ts
Normal 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,
|
||||
});
|
14
apps/search/src/webhooks/definitions/product-deleted.ts
Normal file
14
apps/search/src/webhooks/definitions/product-deleted.ts
Normal 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,
|
||||
});
|
14
apps/search/src/webhooks/definitions/product-updated.ts
Normal file
14
apps/search/src/webhooks/definitions/product-updated.ts
Normal 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,
|
||||
});
|
|
@ -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,
|
||||
});
|
|
@ -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,
|
||||
});
|
|
@ -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,
|
||||
});
|
|
@ -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,
|
||||
});
|
|
@ -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,
|
||||
});
|
|
@ -1,11 +1,11 @@
|
|||
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";
|
||||
import { webhookProductCreated } from "./src/webhooks/definitions/product-created";
|
||||
import { webhookProductDeleted } from "./src/webhooks/definitions/product-deleted";
|
||||
import { webhookProductUpdated } from "./src/webhooks/definitions/product-updated";
|
||||
import { webhookProductVariantBackInStock } from "./src/webhooks/definitions/product-variant-back-in-stock";
|
||||
import { webhookProductVariantCreated } from "./src/webhooks/definitions/product-variant-created";
|
||||
import { webhookProductVariantDeleted } from "./src/webhooks/definitions/product-variant-deleted";
|
||||
import { webhookProductVariantOutOfStock } from "./src/webhooks/definitions/product-variant-out-of-stock";
|
||||
import { webhookProductVariantUpdated } from "./src/webhooks/definitions/product-variant-updated";
|
||||
|
||||
export const appWebhooks = [
|
||||
webhookProductCreated,
|
||||
|
|
|
@ -6,3 +6,4 @@ export * from "./src/logger";
|
|||
export * from "./src/saleor-version-compatibility-validator";
|
||||
export * from "./src/create-graphql-client";
|
||||
export * from "./src/metadata-manager";
|
||||
export * from "./src/editor-js/editor-js-plaintext-renderer";
|
||||
|
|
|
@ -11,9 +11,9 @@ describe("EditorJsPlaintextRenderer", () => {
|
|||
EditorJsPlaintextRenderer({
|
||||
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"}',
|
||||
})
|
||||
}),
|
||||
).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", () => {
|
||||
|
@ -21,9 +21,9 @@ describe("EditorJsPlaintextRenderer", () => {
|
|||
EditorJsPlaintextRenderer({
|
||||
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"}',
|
||||
})
|
||||
}),
|
||||
).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", () => {
|
||||
|
@ -31,14 +31,14 @@ describe("EditorJsPlaintextRenderer", () => {
|
|||
EditorJsPlaintextRenderer({
|
||||
stringData:
|
||||
'{"time": 1684748620371, "blocks": [{"id": "fw-PCw9s-0", "data": {"text": "Everybody knows that 1 < 2 and 1 > 0."}, "type": "paragraph"}, {"id": "eUK1ih8Wmz", "data": {"text": "This is text heart: <3"}, "type": "paragraph"}], "version": "2.24.3"}',
|
||||
})
|
||||
}),
|
||||
).toBe("Everybody knows that 1 < 2 and 1 > 0.\nThis is text heart: <3");
|
||||
it("Returns numbered list when passed ordered list block", () => {
|
||||
expect(
|
||||
EditorJsPlaintextRenderer({
|
||||
stringData:
|
||||
'{"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");
|
||||
});
|
||||
it("Returns list with dashes when passed unordered list block", () => {
|
||||
|
@ -46,7 +46,7 @@ describe("EditorJsPlaintextRenderer", () => {
|
|||
EditorJsPlaintextRenderer({
|
||||
stringData:
|
||||
'{"time": 1684697984679, "blocks": [{"id": "BNL219JhYr", "data": {"items": ["Apples", "Oranges", "Bananas"], "style": "unordered"}, "type": "list"}], "version": "2.24.3"}',
|
||||
})
|
||||
}),
|
||||
).toBe("- Apples\n- Oranges\n- Bananas");
|
||||
});
|
||||
it("Returns plaintext when header block is passed", () => {
|
||||
|
@ -54,7 +54,7 @@ describe("EditorJsPlaintextRenderer", () => {
|
|||
EditorJsPlaintextRenderer({
|
||||
stringData:
|
||||
'{"time": 1684698075115, "blocks": [{"id": "nC-oNRu-pp", "data": {"text": "Lorem ipsum", "level": 1}, "type": "header"}], "version": "2.24.3"}',
|
||||
})
|
||||
}),
|
||||
).toBe("Lorem ipsum");
|
||||
});
|
||||
it("Returns text additional new line after header, when theres another block passed", () => {
|
||||
|
@ -62,7 +62,7 @@ describe("EditorJsPlaintextRenderer", () => {
|
|||
EditorJsPlaintextRenderer({
|
||||
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"}',
|
||||
})
|
||||
}),
|
||||
).toBe("This is header\n\nThere should be additional new line between header and paragraph");
|
||||
});
|
||||
it("Returns text when passed all types of blocks", () => {
|
||||
|
@ -70,9 +70,9 @@ describe("EditorJsPlaintextRenderer", () => {
|
|||
EditorJsPlaintextRenderer({
|
||||
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"}',
|
||||
})
|
||||
}),
|
||||
).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.",
|
||||
);
|
||||
});
|
||||
});
|
824
pnpm-lock.yaml
824
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue