From c475c1c86097b58ac4539eab5aa529117469bf36 Mon Sep 17 00:00:00 2001 From: Anders Gustafsson <34234789+anders-photowall@users.noreply.github.com> Date: Tue, 28 Feb 2023 10:28:58 +0100 Subject: [PATCH] 3367 add facets subquery to product (#146) --- src/datasources/interiors-lambda-api.ts | 10 ++----- src/datasources/keyword-api.ts | 35 +++++++++++++++++++---- src/datasources/product-api.ts | 17 ++---------- src/datasources/utils.ts | 27 ++++++++++++++++++ src/resolvers/products-resolver.ts | 37 +++++++++++++++++++++++++ src/schema.graphql | 6 ++++ src/types/keyword-types.ts | 2 +- src/types/product-types.ts | 6 ++++ 8 files changed, 112 insertions(+), 28 deletions(-) diff --git a/src/datasources/interiors-lambda-api.ts b/src/datasources/interiors-lambda-api.ts index 08a8035..ffa4da2 100644 --- a/src/datasources/interiors-lambda-api.ts +++ b/src/datasources/interiors-lambda-api.ts @@ -1,7 +1,8 @@ import { RESTDataSource } from '@apollo/datasource-rest'; import { ScopeAccess, Scopes } from '../cognito/access-control'; -import { Product, ProductGroup } from '../types/product-types'; +import { Product } from '../types/product-types'; import { DataSourceOptions, User } from '../types/types'; +import { isRepeatingPattern } from './utils'; export class InteriorsLambdaAPI extends RESTDataSource { override baseURL = process.env.INTERIORS_URL; @@ -23,11 +24,6 @@ export class InteriorsLambdaAPI extends RESTDataSource { async generateNewInteriors(product: Product): Promise { ScopeAccess.validate(this.user).all([Scopes.INTERIORS_WRITE]); - const found = product.printProducts.find( - (pr) => pr.groupId === ProductGroup.WALLPAPER, - ); - const isRepeating = found !== undefined; - const body = { token: process.env.INTERIORS_TOKEN, image: `products/${product.id}.jpg`, @@ -38,7 +34,7 @@ export class InteriorsLambdaAPI extends RESTDataSource { autodetect: true, }; - if (isRepeating) { + if (isRepeatingPattern(product)) { body['print_file_height'] = product.fields.printFileHeight; body['print_file_dpi'] = product.fields.printFileDpi; } diff --git a/src/datasources/keyword-api.ts b/src/datasources/keyword-api.ts index e65d74d..5353fae 100644 --- a/src/datasources/keyword-api.ts +++ b/src/datasources/keyword-api.ts @@ -1,11 +1,16 @@ +import DataLoader from 'dataloader'; import { SQLDataSource } from 'datasource-sql'; import { ScopeAccess, Scopes } from '../cognito/access-control'; import { Keyword, KeywordType } from '../types/keyword-types'; import { DataSourceOptions, User } from '../types/types'; import { TextsAPI } from './texts-api'; -import { MINUTE } from './utils'; +import { createQuestionMarksFromList, MINUTE } from './utils'; +interface KeywordWithProductId extends Keyword { + product_id: number; +} export class KeywordAPI extends SQLDataSource { + private productKeywordsLoader: DataLoader; textsApi: TextsAPI; user: User; constructor(options: DataSourceOptions, config, textsApi) { @@ -13,6 +18,20 @@ export class KeywordAPI extends SQLDataSource { this.user = options.user; this.initialize({ cache: options.cache, context: null }); this.textsApi = textsApi; + + this.productKeywordsLoader = new DataLoader(async (ids: number[]) => { + const results = await this.getProductKeywordsBatch(ids); + const indexed = {} as Record; + results.forEach((keywordRow) => { + if (!indexed[keywordRow.product_id]) { + indexed[keywordRow.product_id] = [keywordRow]; + } else { + indexed[keywordRow.product_id].push(keywordRow); + } + }); + const output = ids.map((id) => indexed[id] ?? []); + return output; + }); } // ------------------------------- @@ -36,18 +55,24 @@ export class KeywordAPI extends SQLDataSource { // Functions used in resolvers or other datasources // ---------------------------- - async getProductKeywords(productId: number): Promise> { + async getProductKeywords(productId: number): Promise { + return this.productKeywordsLoader.load(productId); + } + + async getProductKeywordsBatch( + productIds: number[], + ): Promise { ScopeAccess.validate(this.user).all([Scopes.KEYWORDS_READ]); const query = /* sql */ ` -SELECT keywords.*, keyword_type.type_id FROM keywords +SELECT product_keyword.product_id,keywords.*, keyword_type.type_id FROM keywords LEFT JOIN keyword_type ON keyword_type.keyword_id = keywords.id JOIN product_keyword ON product_keyword.keyword_id = keywords.id -WHERE product_keyword.product_id = ? +WHERE product_keyword.product_id IN (${createQuestionMarksFromList(productIds)}) ORDER BY keywords.value `; return this.knex - .raw(query, productId) + .raw(query, productIds) .then((data) => data.rows.map((row) => this.getType(row))); } diff --git a/src/datasources/product-api.ts b/src/datasources/product-api.ts index a374af3..2f86775 100644 --- a/src/datasources/product-api.ts +++ b/src/datasources/product-api.ts @@ -11,7 +11,6 @@ import { ProductBlacklist, ProductWallpaperType, ProductFields, - Orientation, ProductsFilterInput, ProductInfoInput, ProductBlacklistMarketInput, @@ -25,6 +24,7 @@ import { DataSourceOptions, Maybe } from '../types/types'; import { booleanStringField, createQuestionMarksFromList, + getOrientationString, isInt, nullOrNumber, roundOrDefaultTo, @@ -153,24 +153,11 @@ export class ProductAPI extends BaseSQLDataSource { return fields; } - getOrientation(fields: ProductFields): string { - if (!fields || !fields.width || !fields.height) { - return Orientation[Orientation.UNKNOWN]; - } - if (fields.width > fields.height) { - return Orientation[Orientation.LANDSCAPE]; - } - if (fields.width < fields.height) { - return Orientation[Orientation.STANDING]; - } - return Orientation[Orientation.SQUARE]; - } - createProductFromRow(row: any) { row.type = this.getProductTypes(row); row.wallpaperTypes = this.getWallpaperTypes(row); row.designerId = row.designerid; - row.orientation = this.getOrientation(row.fields); + row.orientation = getOrientationString(row.fields); row.publishingDate = row.publishing_date; row.printProducts = this.getPrintProducts(row); row.blacklisting = this.getBlacklisting(row); diff --git a/src/datasources/utils.ts b/src/datasources/utils.ts index 4b7d236..b6925cf 100644 --- a/src/datasources/utils.ts +++ b/src/datasources/utils.ts @@ -1,3 +1,10 @@ +import { + Orientation, + Product, + ProductFields, + ProductGroup, +} from '../types/product-types'; + type InputType = string | undefined | null; export const MINUTE = 60; @@ -74,3 +81,23 @@ export function roundOrDefaultTo( export const createQuestionMarksFromList = (arr: unknown[]): string => { return '?,'.repeat(arr.length).slice(0, -1); }; + +export const isRepeatingPattern = (product: Product): boolean => { + const found = product.printProducts.find( + (pr) => pr.groupId === ProductGroup.WALLPAPER, + ); + return found !== undefined; +}; + +export const getOrientationString = (fields: ProductFields): string => { + if (!fields || !fields.width || !fields.height) { + return Orientation[Orientation.UNKNOWN]; + } + if (fields.width > fields.height) { + return Orientation[Orientation.LANDSCAPE]; + } + if (fields.width < fields.height) { + return Orientation[Orientation.STANDING]; + } + return Orientation[Orientation.SQUARE]; +}; diff --git a/src/resolvers/products-resolver.ts b/src/resolvers/products-resolver.ts index ca06440..15701d2 100644 --- a/src/resolvers/products-resolver.ts +++ b/src/resolvers/products-resolver.ts @@ -1,5 +1,6 @@ import { ProductAPI } from '../datasources/product-api'; import { + Orientation, PrintProduct, Product, ProductListResult, @@ -20,6 +21,7 @@ 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'; const ProductBlacklist = { async market(parent, _args, { dataSources }) { @@ -45,6 +47,41 @@ const Product = { async related({ id }, _args, { dataSources }) { return (dataSources.productApi).getRelatedProducts(id); }, + async facets(product: Product, _args, { dataSources }) { + const keywords = await (( + dataSources.keywordApi + )).getProductKeywords(product.id); + const colors = []; + const types = []; + keywords.forEach((keyword) => { + if (keyword.type === 'COLOR') { + colors.push(keyword.value); + } else if (keyword.value === 'typeillustration') { + types.push('illustration'); + } else if (keyword.value === 'typephotography') { + types.push('photograph'); + } + }); + + const isRepeating = isRepeatingPattern(product); + if (isRepeating) { + types.push('repeating-pattern'); + } + + const orientations = isRepeating + ? [ + Orientation[Orientation.LANDSCAPE], + Orientation[Orientation.STANDING], + Orientation[Orientation.SQUARE], + ] + : [getOrientationString(product.fields)]; + + return [ + { attribute: 'color', values: colors }, + { attribute: 'orientation', values: orientations }, + { attribute: 'type', values: types }, + ]; + }, }; const PrintProduct = { diff --git a/src/schema.graphql b/src/schema.graphql index a120351..6eaf938 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -239,6 +239,11 @@ type Copyright { products: [Product] } +type Facet { + attribute: String, + values: [String] +} + type Product { id: ID! stockid: Int @@ -261,6 +266,7 @@ type Product { wallpaperTypes: [ProductWallpaperType]! fields: ProductFields orientation: Orientation + facets: [Facet] """ Will be single values return in later release """ diff --git a/src/types/keyword-types.ts b/src/types/keyword-types.ts index d5303ab..8d8d655 100644 --- a/src/types/keyword-types.ts +++ b/src/types/keyword-types.ts @@ -6,5 +6,5 @@ export enum KeywordType { export interface Keyword { id: number; value: string; - type: KeywordType; + type: string; } diff --git a/src/types/product-types.ts b/src/types/product-types.ts index ad4abf5..924c6f7 100644 --- a/src/types/product-types.ts +++ b/src/types/product-types.ts @@ -136,6 +136,11 @@ export interface Copyright { products: Promise>; } +export interface Facet { + attribute: string; + value: string[]; +} + export interface Product { id: number; stockid: number; @@ -152,6 +157,7 @@ export interface Product { blacklisting: [ProductBlacklist]; type: [ProductType]; fields: ProductFields; + facets: Facet[]; } // TODO: test stock products to see what props can be mandatory