3367 add facets subquery to product (#146)

This commit is contained in:
Anders Gustafsson
2023-02-28 10:28:58 +01:00
committed by GitHub
parent 5bc4e92c92
commit c475c1c860
8 changed files with 112 additions and 28 deletions
+3 -7
View File
@@ -1,7 +1,8 @@
import { RESTDataSource } from '@apollo/datasource-rest'; import { RESTDataSource } from '@apollo/datasource-rest';
import { ScopeAccess, Scopes } from '../cognito/access-control'; 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 { DataSourceOptions, User } from '../types/types';
import { isRepeatingPattern } from './utils';
export class InteriorsLambdaAPI extends RESTDataSource { export class InteriorsLambdaAPI extends RESTDataSource {
override baseURL = process.env.INTERIORS_URL; override baseURL = process.env.INTERIORS_URL;
@@ -23,11 +24,6 @@ export class InteriorsLambdaAPI extends RESTDataSource {
async generateNewInteriors(product: Product): Promise<any> { async generateNewInteriors(product: Product): Promise<any> {
ScopeAccess.validate(this.user).all([Scopes.INTERIORS_WRITE]); ScopeAccess.validate(this.user).all([Scopes.INTERIORS_WRITE]);
const found = product.printProducts.find(
(pr) => pr.groupId === ProductGroup.WALLPAPER,
);
const isRepeating = found !== undefined;
const body = { const body = {
token: process.env.INTERIORS_TOKEN, token: process.env.INTERIORS_TOKEN,
image: `products/${product.id}.jpg`, image: `products/${product.id}.jpg`,
@@ -38,7 +34,7 @@ export class InteriorsLambdaAPI extends RESTDataSource {
autodetect: true, autodetect: true,
}; };
if (isRepeating) { if (isRepeatingPattern(product)) {
body['print_file_height'] = product.fields.printFileHeight; body['print_file_height'] = product.fields.printFileHeight;
body['print_file_dpi'] = product.fields.printFileDpi; body['print_file_dpi'] = product.fields.printFileDpi;
} }
+30 -5
View File
@@ -1,11 +1,16 @@
import DataLoader from 'dataloader';
import { SQLDataSource } from 'datasource-sql'; import { SQLDataSource } from 'datasource-sql';
import { ScopeAccess, Scopes } from '../cognito/access-control'; import { ScopeAccess, Scopes } from '../cognito/access-control';
import { Keyword, KeywordType } from '../types/keyword-types'; import { Keyword, KeywordType } from '../types/keyword-types';
import { DataSourceOptions, User } from '../types/types'; import { DataSourceOptions, User } from '../types/types';
import { TextsAPI } from './texts-api'; 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 { export class KeywordAPI extends SQLDataSource {
private productKeywordsLoader: DataLoader<number, Keyword[]>;
textsApi: TextsAPI; textsApi: TextsAPI;
user: User; user: User;
constructor(options: DataSourceOptions, config, textsApi) { constructor(options: DataSourceOptions, config, textsApi) {
@@ -13,6 +18,20 @@ export class KeywordAPI extends SQLDataSource {
this.user = options.user; this.user = options.user;
this.initialize({ cache: options.cache, context: null }); this.initialize({ cache: options.cache, context: null });
this.textsApi = textsApi; this.textsApi = textsApi;
this.productKeywordsLoader = new DataLoader(async (ids: number[]) => {
const results = await this.getProductKeywordsBatch(ids);
const indexed = {} as Record<string, KeywordWithProductId[]>;
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 // Functions used in resolvers or other datasources
// ---------------------------- // ----------------------------
async getProductKeywords(productId: number): Promise<Array<Keyword>> { async getProductKeywords(productId: number): Promise<Keyword[]> {
return this.productKeywordsLoader.load(productId);
}
async getProductKeywordsBatch(
productIds: number[],
): Promise<KeywordWithProductId[]> {
ScopeAccess.validate(this.user).all([Scopes.KEYWORDS_READ]); ScopeAccess.validate(this.user).all([Scopes.KEYWORDS_READ]);
const query = /* sql */ ` 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 LEFT JOIN keyword_type ON keyword_type.keyword_id = keywords.id
JOIN product_keyword ON product_keyword.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 ORDER BY keywords.value
`; `;
return this.knex return this.knex
.raw(query, productId) .raw(query, productIds)
.then((data) => data.rows.map((row) => this.getType(row))); .then((data) => data.rows.map((row) => this.getType(row)));
} }
+2 -15
View File
@@ -11,7 +11,6 @@ import {
ProductBlacklist, ProductBlacklist,
ProductWallpaperType, ProductWallpaperType,
ProductFields, ProductFields,
Orientation,
ProductsFilterInput, ProductsFilterInput,
ProductInfoInput, ProductInfoInput,
ProductBlacklistMarketInput, ProductBlacklistMarketInput,
@@ -25,6 +24,7 @@ import { DataSourceOptions, Maybe } from '../types/types';
import { import {
booleanStringField, booleanStringField,
createQuestionMarksFromList, createQuestionMarksFromList,
getOrientationString,
isInt, isInt,
nullOrNumber, nullOrNumber,
roundOrDefaultTo, roundOrDefaultTo,
@@ -153,24 +153,11 @@ export class ProductAPI extends BaseSQLDataSource {
return <ProductFields>fields; return <ProductFields>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) { createProductFromRow(row: any) {
row.type = this.getProductTypes(row); row.type = this.getProductTypes(row);
row.wallpaperTypes = this.getWallpaperTypes(row); row.wallpaperTypes = this.getWallpaperTypes(row);
row.designerId = row.designerid; row.designerId = row.designerid;
row.orientation = this.getOrientation(row.fields); row.orientation = getOrientationString(row.fields);
row.publishingDate = row.publishing_date; row.publishingDate = row.publishing_date;
row.printProducts = this.getPrintProducts(row); row.printProducts = this.getPrintProducts(row);
row.blacklisting = this.getBlacklisting(row); row.blacklisting = this.getBlacklisting(row);
+27
View File
@@ -1,3 +1,10 @@
import {
Orientation,
Product,
ProductFields,
ProductGroup,
} from '../types/product-types';
type InputType = string | undefined | null; type InputType = string | undefined | null;
export const MINUTE = 60; export const MINUTE = 60;
@@ -74,3 +81,23 @@ export function roundOrDefaultTo(
export const createQuestionMarksFromList = (arr: unknown[]): string => { export const createQuestionMarksFromList = (arr: unknown[]): string => {
return '?,'.repeat(arr.length).slice(0, -1); 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];
};
+37
View File
@@ -1,5 +1,6 @@
import { ProductAPI } from '../datasources/product-api'; import { ProductAPI } from '../datasources/product-api';
import { import {
Orientation,
PrintProduct, PrintProduct,
Product, Product,
ProductListResult, ProductListResult,
@@ -20,6 +21,7 @@ import { DesignerAPI } from '../datasources/designer-api';
import { PrintProductDefaultsAPI } from '../datasources/printproduct-defaults-api'; import { PrintProductDefaultsAPI } from '../datasources/printproduct-defaults-api';
import { PrintProductDefaults } from '../types/printproduct-defaults-types'; import { PrintProductDefaults } from '../types/printproduct-defaults-types';
import { GraphQLError } from 'graphql'; import { GraphQLError } from 'graphql';
import { getOrientationString, isRepeatingPattern } from '../datasources/utils';
const ProductBlacklist = { const ProductBlacklist = {
async market(parent, _args, { dataSources }) { async market(parent, _args, { dataSources }) {
@@ -45,6 +47,41 @@ const Product = {
async related({ id }, _args, { dataSources }) { async related({ id }, _args, { dataSources }) {
return (<ProductAPI>dataSources.productApi).getRelatedProducts(id); return (<ProductAPI>dataSources.productApi).getRelatedProducts(id);
}, },
async facets(product: Product, _args, { dataSources }) {
const keywords = await (<KeywordAPI>(
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 = { const PrintProduct = {
+6
View File
@@ -239,6 +239,11 @@ type Copyright {
products: [Product] products: [Product]
} }
type Facet {
attribute: String,
values: [String]
}
type Product { type Product {
id: ID! id: ID!
stockid: Int stockid: Int
@@ -261,6 +266,7 @@ type Product {
wallpaperTypes: [ProductWallpaperType]! wallpaperTypes: [ProductWallpaperType]!
fields: ProductFields fields: ProductFields
orientation: Orientation orientation: Orientation
facets: [Facet]
""" """
Will be single values return in later release Will be single values return in later release
""" """
+1 -1
View File
@@ -6,5 +6,5 @@ export enum KeywordType {
export interface Keyword { export interface Keyword {
id: number; id: number;
value: string; value: string;
type: KeywordType; type: string;
} }
+6
View File
@@ -136,6 +136,11 @@ export interface Copyright {
products: Promise<Array<Product>>; products: Promise<Array<Product>>;
} }
export interface Facet {
attribute: string;
value: string[];
}
export interface Product { export interface Product {
id: number; id: number;
stockid: number; stockid: number;
@@ -152,6 +157,7 @@ export interface Product {
blacklisting: [ProductBlacklist]; blacklisting: [ProductBlacklist];
type: [ProductType]; type: [ProductType];
fields: ProductFields; fields: ProductFields;
facets: Facet[];
} }
// TODO: test stock products to see what props can be mandatory // TODO: test stock products to see what props can be mandatory