Skip to content

Commit f430bdc

Browse files
lkostrowskiclaude
andauthored
Map multi-value attributes as arrays in Algolia indexing (#2095)
* Map multi-value attributes as arrays in Algolia indexing Previously, multi-select attribute values were joined as comma-separated strings. Now they are properly mapped as arrays of strings, enabling better faceting and filtering in Algolia. Also updates AGENTS.md with changeset workflow documentation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * fix single attr * fix agents --------- Co-authored-by: Claude <[email protected]>
1 parent 6c890f6 commit f430bdc

File tree

4 files changed

+115
-3
lines changed

4 files changed

+115
-3
lines changed

.changeset/purple-rocks-build.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"saleor-app-search": patch
3+
---
4+
5+
Map multi-value attributes as arrays instead of comma-separated strings in Algolia indexing. They should be properly represented in Algolia now

AGENTS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ static ValidationError = BaseError.subclass("ValidationError", {
112112
3. **Type Safety**: All apps use strict TypeScript - ensure no `any` types
113113
4. **Testing**: Write unit tests alongside features, E2E tests for critical workflows
114114
5. **Linting**: Code must pass ESLint rules including custom app-specific rules like `n/no-process-env`
115+
6. **Changeset**: Functional changes, like new features or fixes should have changeset attached. Do not attach it if code changes do not have visible impact to the user, like refactor. To run changeset:
116+
- Execute `pnpm changeset add` from root directory
117+
- Select affected app(s) or package(s)
118+
- If many changes applied in single commit, create multiple changesets
119+
- Ensure changeset has a good value, describing what was the actual change. It should be less technical than the commit. Best if it has before/after described.
115120

116121
## App-Specific Notes
117122

apps/search/src/lib/algolia/algoliaUtils.test.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,106 @@ describe("algoliaUtils", function () {
128128
expect(mappedEntity.attributes["booleanFalse"]).toBe(false);
129129
});
130130

131+
it("Maps single-value non-boolean attribute as string", () => {
132+
const mappedEntity = productAndVariantToAlgolia({
133+
channel: "test",
134+
enabledKeys: ["attributes"],
135+
variant: {
136+
id: "id",
137+
attributes: [
138+
{
139+
attribute: {
140+
name: "size",
141+
},
142+
values: [
143+
{
144+
name: "Large",
145+
inputType: "DROPDOWN",
146+
boolean: null,
147+
},
148+
],
149+
},
150+
],
151+
name: "product name",
152+
metadata: [],
153+
product: {
154+
__typename: undefined,
155+
id: "",
156+
name: "",
157+
description: undefined,
158+
slug: "",
159+
variants: undefined,
160+
category: undefined,
161+
thumbnail: undefined,
162+
media: undefined,
163+
attributes: [],
164+
channelListings: undefined,
165+
collections: undefined,
166+
metadata: [],
167+
},
168+
},
169+
});
170+
171+
// @ts-expect-error - record is not typed (attributes are dynamic keys)
172+
expect(mappedEntity.attributes["size"]).toBe("Large");
173+
});
174+
175+
it("Maps multi-value attributes as array of strings", () => {
176+
const mappedEntity = productAndVariantToAlgolia({
177+
channel: "test",
178+
enabledKeys: ["attributes"],
179+
variant: {
180+
id: "id",
181+
attributes: [
182+
{
183+
attribute: {
184+
name: "colors",
185+
},
186+
values: [
187+
{
188+
name: "Red",
189+
inputType: "MULTISELECT",
190+
boolean: null,
191+
},
192+
{
193+
name: "Blue",
194+
inputType: "MULTISELECT",
195+
boolean: null,
196+
},
197+
{
198+
name: "Green",
199+
inputType: "MULTISELECT",
200+
boolean: null,
201+
},
202+
],
203+
},
204+
],
205+
name: "product name",
206+
metadata: [],
207+
product: {
208+
__typename: undefined,
209+
id: "",
210+
name: "",
211+
description: undefined,
212+
slug: "",
213+
variants: undefined,
214+
category: undefined,
215+
thumbnail: undefined,
216+
media: undefined,
217+
attributes: [],
218+
channelListings: undefined,
219+
collections: undefined,
220+
metadata: [],
221+
},
222+
},
223+
});
224+
225+
// @ts-expect-error - record is not typed (attributes are dynamic keys)
226+
expect(mappedEntity.attributes["colors"]).toStrictEqual(["Red", "Blue", "Green"]);
227+
// @ts-expect-error - record is not typed (attributes are dynamic keys)
228+
expect(Array.isArray(mappedEntity.attributes["colors"])).toBe(true);
229+
});
230+
131231
it("Filters out inactive variants from otherVariants", () => {
132232
const currentChannel = "channel-1";
133233
const mappedEntity = productAndVariantToAlgolia({

apps/search/src/lib/algolia/algoliaUtils.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ const mapSelectedAttributesToRecord = (attr: ProductAttributesDataFragment) => {
9292
*/
9393
const filteredValues = attr.values.filter((v) => !!v.name?.length);
9494

95-
let value: string | boolean;
95+
let value: string | boolean | string[];
9696

9797
/**
9898
* Strategy for boolean type only
@@ -101,18 +101,20 @@ const mapSelectedAttributesToRecord = (attr: ProductAttributesDataFragment) => {
101101
*/
102102
if (isAttributeValueBooleanType(filteredValues)) {
103103
value = filteredValues[0].boolean;
104+
} else if (filteredValues.length === 1 && filteredValues[0].name) {
105+
value = filteredValues[0].name;
104106
} else {
105107
/**
106108
* Fallback to initial/previous behavior
107109
* TODO: Its not correct to use "name" field always. E.g. for plaintext field more accurate is "plainText",
108110
* for "date" field there are date and dateTime fields. "Name" can work on the frontend but doesn't fit for faceting
109111
*/
110-
value = filteredValues.map((v) => v.name).join(", ") || "";
112+
value = filteredValues.map((v) => v.name).filter(isNotNil);
111113
}
112114

113115
return {
114116
[attr.attribute.name]: value,
115-
} as Record<string, string | boolean>;
117+
} as Record<string, string | boolean | string[]>;
116118
};
117119

118120
export function productAndVariantToAlgolia({

0 commit comments

Comments
 (0)