From c996cf53e0bc03140eb52de80a42ea27b87f833f Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Mon, 4 May 2026 09:55:51 +0200 Subject: [PATCH] 9101: Added new filters for motifs (#193) --- src/datasources/category-api.ts | 122 ++++++++++++++--------------- src/datasources/utils.ts | 4 + src/resolvers/products-resolver.ts | 33 +++++--- 3 files changed, 85 insertions(+), 74 deletions(-) diff --git a/src/datasources/category-api.ts b/src/datasources/category-api.ts index afbade5..28844a0 100644 --- a/src/datasources/category-api.ts +++ b/src/datasources/category-api.ts @@ -3,7 +3,18 @@ import { ScopeAccess, Scopes } from '../cognito/access-control'; import { Category, CategoryLocaleData } from '../types/category-types'; import { DataSourceOptions } from '../types/types'; import { BaseSQLDataSource } from './BaseSQLDataSource'; -import { createQuestionMarksFromList, MINUTE } from './utils'; +import { createQuestionMarksFromList, HOUR, MINUTE } from './utils'; + +const ROOMS_PARENT_ID = 270; +const COLORS_PARENT_ID = 1665; +const STYLES_PARENT_ID = 1814; +const PATTERNS_PARENT_ID = 1414; +const KNOWN_FACET_PARENT_IDS = [ + ROOMS_PARENT_ID, + COLORS_PARENT_ID, + STYLES_PARENT_ID, + PATTERNS_PARENT_ID, +]; export class CategoryAPI extends BaseSQLDataSource { private categoryLoader: DataLoader; @@ -86,108 +97,93 @@ WHERE product_category.product_id = ? return res; } + async getMotifRootIds(): Promise { + const { rows } = await this.cachedRaw( + /* sql */ ` + SELECT id + FROM v_categorytree_v2 + WHERE depth = 1 + AND id NOT IN (${createQuestionMarksFromList(KNOWN_FACET_PARENT_IDS)}) + `, + KNOWN_FACET_PARENT_IDS, + ) + // @ts-ignore + .cache(HOUR); + + return rows.map((row) => row.id); + } + async getCategoryBasedFacets(productId: number): Promise<{ room: string[]; color: string[]; style: string[]; pattern: string[]; + motif: string[]; }> { ScopeAccess.validate(this.user).some([ Scopes.CATEGORIES_READ, Scopes.CATEGORIES_PUBLIC_READ, ]); - const ROOMS_PARENT_ID = 270; - const COLORS_PARENT_ID = 1665; - const STYLES_PARENT_ID = 1814; - const PATTERNS_PARENT_ID = 1414; + const motifRootIds = await this.getMotifRootIds(); + const allFacetParentIds = [...KNOWN_FACET_PARENT_IDS, ...motifRootIds]; const { rows } = await this.cachedRaw( - ` - WITH rooms AS ( - SELECT lft, rgt FROM categories_v2_lft_rgt WHERE id = ? - ), - colors AS ( - SELECT lft, rgt FROM categories_v2_lft_rgt WHERE id = ? - ), - styles AS ( - SELECT lft, rgt FROM categories_v2_lft_rgt WHERE id = ? - ), - patterns AS ( - SELECT lft, rgt FROM categories_v2_lft_rgt WHERE id = ? + /* sql */ ` + WITH facet_parents AS ( + SELECT id, lft, rgt, name, + CASE + WHEN id = ? THEN 'room' + WHEN id = ? THEN 'color' + WHEN id = ? THEN 'style' + WHEN id = ? THEN 'pattern' + ELSE 'motif' + END AS facet + FROM categories_v2_lft_rgt + WHERE id IN (${createQuestionMarksFromList(allFacetParentIds)}) ) SELECT - array_remove( - array_agg( - DISTINCT CASE - WHEN c.lft > rooms.lft AND c.rgt < rooms.rgt THEN c.name - END - ), - NULL - ) AS room, - array_remove( - array_agg( - DISTINCT CASE - WHEN c.lft > colors.lft AND c.rgt < colors.rgt THEN c.name - END - ), - NULL - ) AS color, - array_remove( - array_agg( - DISTINCT CASE - WHEN c.lft > styles.lft AND c.rgt < styles.rgt THEN c.name - END - ), - NULL - ) AS style, - array_remove( - array_agg( - DISTINCT CASE - WHEN c.lft > patterns.lft AND c.rgt < patterns.rgt THEN c.name - END - ), - NULL - ) AS pattern + array_agg(DISTINCT c.name) FILTER (WHERE fp.facet = 'room') AS room, + array_agg(DISTINCT c.name) FILTER (WHERE fp.facet = 'color') AS color, + array_agg(DISTINCT c.name) FILTER (WHERE fp.facet = 'style') AS style, + array_agg(DISTINCT c.name) FILTER (WHERE fp.facet = 'pattern') AS pattern, + array_agg(DISTINCT fp.name) FILTER (WHERE fp.facet = 'motif') AS motif FROM product_category pc JOIN categories_v2_lft_rgt c ON c.id = pc.category_id - CROSS JOIN rooms - CROSS JOIN colors - CROSS JOIN styles - CROSS JOIN patterns + JOIN facet_parents fp ON c.lft > fp.lft AND c.rgt < fp.rgt WHERE pc.product_id = ? - AND ( - (c.lft > rooms.lft AND c.rgt < rooms.rgt) - OR - (c.lft > colors.lft AND c.rgt < colors.rgt) - OR - (c.lft > styles.lft AND c.rgt < styles.rgt) - OR - (c.lft > patterns.lft AND c.rgt < patterns.rgt) - ) `, [ ROOMS_PARENT_ID, COLORS_PARENT_ID, STYLES_PARENT_ID, PATTERNS_PARENT_ID, + ...allFacetParentIds, productId, ], ) // @ts-ignore .cache(MINUTE * 5); - const facetRow = rows[0] ?? { room: [], color: [], style: [], pattern: [] }; + const facetRow = rows[0] ?? { + room: [], + color: [], + style: [], + pattern: [], + motif: [], + }; const roomCategories: string[] = facetRow.room ?? []; const colorCategories: string[] = facetRow.color ?? []; const styleCategories: string[] = facetRow.style ?? []; const patternCategories: string[] = facetRow.pattern ?? []; + const motifCategories: string[] = facetRow.motif ?? []; return { room: roomCategories, color: colorCategories, style: styleCategories, pattern: patternCategories, + motif: motifCategories, }; } diff --git a/src/datasources/utils.ts b/src/datasources/utils.ts index b6925cf..37c3c0b 100644 --- a/src/datasources/utils.ts +++ b/src/datasources/utils.ts @@ -8,6 +8,7 @@ import { type InputType = string | undefined | null; export const MINUTE = 60; +export const HOUR = 3600; function isNumeric(n: InputType) { return !isNaN(parseFloat(n)); @@ -82,6 +83,9 @@ export const createQuestionMarksFromList = (arr: unknown[]): string => { return '?,'.repeat(arr.length).slice(0, -1); }; +export const sanitizeFacetValues = (values: string[]): string[] => + values.map((v) => v.toLowerCase().replace(/,/g, '')); + export const isRepeatingPattern = (product: Product): boolean => { const found = product.printProducts.find( (pr) => pr.groupId === ProductGroup.WALLPAPER, diff --git a/src/resolvers/products-resolver.ts b/src/resolvers/products-resolver.ts index 3013a4d..60db56d 100644 --- a/src/resolvers/products-resolver.ts +++ b/src/resolvers/products-resolver.ts @@ -21,7 +21,11 @@ import { DesignerAPI } from '../datasources/designer-api'; import { PrintProductDefaultsAPI } from '../datasources/printproduct-defaults-api'; import { PrintProductDefaults } from '../types/printproduct-defaults-types'; import { GraphQLError } from 'graphql'; -import { getOrientationString, isRepeatingPattern } from '../datasources/utils'; +import { + getOrientationString, + isRepeatingPattern, + sanitizeFacetValues, +} from '../datasources/utils'; import { sendMessage } from '../aws/sqs'; import { sendProductUpdatedEvent } from '../bernard/client'; @@ -117,16 +121,13 @@ const Product = { }), ]); - const patterns = product.printProducts.some( - (pp) => pp.groupId === ProductGroup.WALLPAPER, - ) - ? categoryBasedFacets.pattern - : []; + const patterns = isRepeating ? categoryBasedFacets.pattern : []; + const motifs = isRepeating ? [] : categoryBasedFacets.motif; return [ { attribute: 'color', - values: [...colors], + values: sanitizeFacetValues([...colors]), }, { attribute: 'orientation', @@ -141,10 +142,20 @@ const Product = { return value.toLowerCase(); }), }, - { attribute: 'type', values: types }, - { attribute: 'room', values: categoryBasedFacets.room }, - { attribute: 'style', values: categoryBasedFacets.style }, - { attribute: 'pattern', values: patterns }, + { attribute: 'type', values: sanitizeFacetValues(types) }, + { + attribute: 'room', + values: sanitizeFacetValues(categoryBasedFacets.room), + }, + { + attribute: 'style', + values: sanitizeFacetValues(categoryBasedFacets.style), + }, + { attribute: 'pattern', values: sanitizeFacetValues(patterns) }, + { + attribute: 'motif', + values: sanitizeFacetValues(motifs), + }, ]; }, };