Improve Search/Algolia mappings (#827)

* parse metadata before sending to algolia

* variants mapping

* extract metadata mapping

* Changeset

* rename field
This commit is contained in:
Lukasz Ostrowski 2023-07-31 12:08:05 +02:00 committed by GitHub
parent fa65735571
commit 2cb7e5edee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 105 additions and 9 deletions

View file

@ -0,0 +1,9 @@
---
"saleor-app-search": minor
---
Improved attributes mapping. Now Algolia will receive better products data:
- Added `otherVariants` field which is *an array of variant id strings*. It will only contain other variants, so if the array is empty, it means the variant is the only one. It can be quickly used to count alternative variants or to reference them by ID.
- Added `variantMetadata` field. Now `metadata` contains data for product parent and `variantMetadata` for each variant.
- Improved JSON fields mapping. Now json-like fields will be sent to Algolia as structure jsons, not strings. This include: `description`, `metadata`, `variantMetadata`, `otherVariants`.

View file

@ -1,4 +1,7 @@
fragment ProductData on Product {
variants {
id
}
id
name
description

View file

@ -1,4 +1,8 @@
fragment ProductVariantData on ProductVariant {
metadata {
key
value
}
id
name
sku

View file

@ -3,6 +3,8 @@ import {
ProductVariantWebhookPayloadFragment,
} from "../../../generated/graphql";
import { isNotNil } from "../isNotNil";
import { safeParseJson } from "../safe-parse-json";
import { metadataToAlgoliaAttribute } from "./metadata-to-algolia-attribute";
type PartialChannelListing = {
channel: {
@ -58,10 +60,6 @@ export function categoryHierarchicalFacets({ product }: ProductVariantWebhookPay
return categoryLvlMapping;
}
export function formatMetadata({ product }: ProductVariantWebhookPayloadFragment) {
return Object.fromEntries(product.metadata?.map(({ key, value }) => [key, value]) || []);
}
export type AlgoliaObject = ReturnType<typeof productAndVariantToAlgolia>;
/**
@ -123,13 +121,15 @@ export function productAndVariantToAlgolia({
productName: product.name,
variantName: variant.name,
attributes,
description: product.description,
description: safeParseJson(product.description),
slug: product.slug,
thumbnail: product.thumbnail?.url,
grossPrice: listing?.price?.amount,
categories: categoryHierarchicalFacets(variant),
collections: product.collections?.map((collection) => collection.name) || [],
metadata: formatMetadata(variant),
metadata: metadataToAlgoliaAttribute(variant.product.metadata),
variantMetadata: metadataToAlgoliaAttribute(variant.metadata),
otherVariants: variant.product.variants?.map((v) => v.id).filter((v) => v !== variant.id) || [],
};
return document;

View file

@ -0,0 +1,64 @@
import { describe, it, expect } from "vitest";
import { metadataToAlgoliaAttribute } from "./metadata-to-algolia-attribute";
import { map } from "zod";
describe("metadataToAlgoliaAttribute", () => {
it("Maps string attribute", () => {
expect(
metadataToAlgoliaAttribute([
{
key: "foo",
value: "bar",
},
{
key: "foobar",
value: "baz",
},
])
).toEqual({
foo: "bar",
foobar: "baz",
});
});
it("Maps json attribute to nested json", () => {
expect(
metadataToAlgoliaAttribute([
{
key: "foo",
value: `{"bar": "baz"}`,
},
{
key: "foobar",
value: `["a", "b", "c"]`,
},
])
).toEqual({
foo: {
bar: "baz",
},
foobar: ["a", "b", "c"],
});
});
it("Maps invalid json attribute to string", () => {
const invalidJson = `{"bar": "baz"`;
expect(metadataToAlgoliaAttribute([{ key: "invalidJson", value: invalidJson }])).toEqual({
invalidJson: `{"bar": "baz"`,
});
});
it("Maps empty value", () => {
expect(
metadataToAlgoliaAttribute([
{
key: "foo",
value: "",
},
])
).toEqual({
foo: null,
});
});
});

View file

@ -0,0 +1,6 @@
import { MetadataItem } from "../../../generated/graphql";
import { safeParseJson } from "../safe-parse-json";
export function metadataToAlgoliaAttribute(metadata: MetadataItem[]) {
return Object.fromEntries(metadata?.map(({ key, value }) => [key, safeParseJson(value)]) || []);
}

View file

@ -0,0 +1,11 @@
export const safeParseJson = (jsonString: string) => {
if (!jsonString) {
return null;
}
try {
return JSON.parse(jsonString);
} catch (e) {
return jsonString;
}
};

View file

@ -1,15 +1,14 @@
import { NextWebhookApiHandler, SaleorAsyncWebhook } from "@saleor/app-sdk/handlers/next";
import { createGraphQLClient } from "@saleor/apps-shared";
import {
ProductVariantUpdated,
ProductVariantUpdatedDocument,
} from "../../../../../generated/graphql";
import { saleorApp } from "../../../../../saleor-app";
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
import { AlgoliaSearchProvider } from "../../../../lib/algolia/algoliaSearchProvider";
import { getAlgoliaConfiguration } from "../../../../lib/algolia/getAlgoliaConfiguration";
import { createDebug } from "../../../../lib/debug";
import { createLogger } from "../../../../lib/logger";
import { WebhookActivityTogglerService } from "../../../../domain/WebhookActivityToggler.service";
import { createGraphQLClient } from "@saleor/apps-shared";
export const config = {
api: {