From 08149c4a87c0ac2f24cd1fb1cdd7d2b32d55dbb3 Mon Sep 17 00:00:00 2001 From: Niklas Fondberg Date: Mon, 11 Oct 2021 11:20:48 +0200 Subject: [PATCH] Add product search (#64) --- docker-compose.yml | 1 + src/datasources/category-api.ts | 7 +++++ src/datasources/designer-api.ts | 15 +++++++++ src/datasources/keyword-api.ts | 8 +++++ src/datasources/product-api.ts | 48 +++++++++++++++++++++++++++++ src/datasources/sql/products-sql.ts | 25 +++++++++++++-- src/resolvers/designers-resolver.ts | 4 +++ src/resolvers/products-resolver.ts | 27 ++++++++++++++++ src/schema.graphql | 21 ++++++++++++- src/types/product-types.ts | 16 ++++++++++ 10 files changed, 168 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 834a651..0c36ba3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,7 @@ services: - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION} + - NODE_ENV=development networks: default: diff --git a/src/datasources/category-api.ts b/src/datasources/category-api.ts index 57b7d90..11d81b9 100644 --- a/src/datasources/category-api.ts +++ b/src/datasources/category-api.ts @@ -32,6 +32,13 @@ WHERE product_category.product_id = ? return this.knex.select('*').from('v_categorytree').cache(MINUTE); } + async searchCategories(name: string): Promise> { + return this.knex + .select('*') + .from('v_categorytree') + .whereRaw(`LOWER(name) LIKE ?`, [`${name}`]); + } + async getCategoryById(id: number): Promise { return this.knex .select('*') diff --git a/src/datasources/designer-api.ts b/src/datasources/designer-api.ts index fb37045..feb9d17 100644 --- a/src/datasources/designer-api.ts +++ b/src/datasources/designer-api.ts @@ -23,6 +23,21 @@ export class DesignerAPI extends BaseSQLDataSource { }); } + async searchDesigners(name: string): Promise> { + return this.knex + .select('*') + .from('designers') + .whereRaw(`LOWER(name) LIKE ?`, [`${name}`]) + .then((rows) => { + return rows.map((row) => { + return { + ...row, + id: row.designerid, + }; + }); + }); + } + async getDesigners(limit: Maybe): Promise> { limit = limit ?? 5000; return this.knex diff --git a/src/datasources/keyword-api.ts b/src/datasources/keyword-api.ts index 22510f8..f9754eb 100644 --- a/src/datasources/keyword-api.ts +++ b/src/datasources/keyword-api.ts @@ -55,6 +55,14 @@ ORDER BY keywords.value .then((row) => this.getType(row)); } + async searchKeywords(name: string): Promise> { + return this.knex + .select('*') + .from('keywords') + .leftJoin('keyword_type', 'keyword_type.keyword_id', 'keywords.id') + .whereRaw(`LOWER(value) LIKE ?`, [`${name}`]); + } + async setProductKeywords( productId: number, keywordIds: Array, diff --git a/src/datasources/product-api.ts b/src/datasources/product-api.ts index 2138e3d..bbae813 100644 --- a/src/datasources/product-api.ts +++ b/src/datasources/product-api.ts @@ -17,6 +17,7 @@ import { ProductMaterials, ProductSizeTypes, ProductProportionsInput, + Batch, } from '../types/product-types'; import { Maybe } from '../types/types'; import { booleanStringField, nullOrNumber, roundOrDefaultTo } from './utils'; @@ -179,6 +180,14 @@ export class ProductAPI extends BaseSQLDataSource { return res.find(Boolean); } + async getDesignerProducts(designerId: number): Promise> { + const query = sql.designerProducts(); + + return this.knex + .raw(query, designerId) + .then((data) => data.rows.map((row) => this.createProductFromRow(row))); + } + async getCategoryProducts(categoryId: number): Promise> { const query = sql.categoryProducts(categoryId); @@ -195,6 +204,45 @@ export class ProductAPI extends BaseSQLDataSource { .then((data) => data.rows.map((row) => this.createProductFromRow(row))); } + async searchByName(name: string): Promise> { + const query = sql.searchByFieldValue(2); // name + + return this.knex + .raw(query, name) + .then((data) => data.rows.map((row) => this.createProductFromRow(row))); + } + + async searchByArtNo(name: string): Promise> { + const query = sql.searchByFieldValue(1); // artNo + + return this.knex + .raw(query, name) + .then((data) => data.rows.map((row) => this.createProductFromRow(row))); + } + + async searchByBatch(name: string): Promise> { + const query = sql.searchByFieldValue(36); // batch + + const batchRes = await this.knex + .raw(query, name) + .then((data) => data.rows.map((row) => this.createProductFromRow(row))); + + const batches = {}; + batchRes.forEach((p) => { + if (!batches[p.fields.batch]) { + batches[p.fields.batch] = []; + } + batches[p.fields.batch].push(p); + }); + + return Object.keys(batches).map((key) => { + return { + name: key, + products: batches[key], + }; + }); + } + async getRelatedProducts(id: number): Promise> { const ids = await this.knex .raw( diff --git a/src/datasources/sql/products-sql.ts b/src/datasources/sql/products-sql.ts index 6fc275a..8325618 100644 --- a/src/datasources/sql/products-sql.ts +++ b/src/datasources/sql/products-sql.ts @@ -77,17 +77,18 @@ FROM "product-products" products export function products(input: ProductsFilterInput) { const limit = input.pagination.limit ?? 100; - const filter = input.filter; + const filter = input.filter ?? null; return ( baseQuery + /* sql */ ` ${ - filter.visible != null + filter?.visible != null ? /* sql */ ` WHERE products.visible = ${filter?.visible ? '1' : '0'} AND products.browsable = ${filter?.browsable ? '1' : '0'}` : `` } +ORDER BY products.inserted DESC LIMIT ${limit} ` ); @@ -98,7 +99,7 @@ export function productsTotal(input: ProductsFilterInput) { return /*sql */ ` SELECT count(*) FROM "product-products" products ${ - filter.visible != null + filter?.visible != null ? /* sql */ ` WHERE products.visible = ${filter?.visible ? '1' : '0'} AND products.browsable = ${filter?.browsable ? '1' : '0'}` @@ -125,6 +126,15 @@ export function categoryProducts(categoryId: number) { ); } +export function designerProducts() { + return ( + baseQuery + + /* sql */ ` + WHERE products.designerid = ?; +` + ); +} + export function keywordProducts(keywordId: number) { return ( baseQuery + @@ -133,3 +143,12 @@ export function keywordProducts(keywordId: number) { ` ); } + +export function searchByFieldValue(fieldid: number) { + return ( + baseQuery + + /* sql */ ` + WHERE products.productid IN (SELECT productid FROM "product-products_fields" WHERE fieldid = ${fieldid} AND LOWER(value) LIKE ? LIMIT 5000); -- limit to 5000 results +` + ); +} diff --git a/src/resolvers/designers-resolver.ts b/src/resolvers/designers-resolver.ts index ee59dcf..294ef34 100644 --- a/src/resolvers/designers-resolver.ts +++ b/src/resolvers/designers-resolver.ts @@ -1,11 +1,15 @@ import { DesignerAPI } from '../datasources/designer-api'; import { OrderAPI } from '../datasources/order-api'; +import { ProductAPI } from '../datasources/product-api'; import { GeneralInput } from '../types/types'; const Designer = { async orderRows({ id }, input: GeneralInput, { dataSources }) { return (dataSources.orderApi).getOrderRowsByDesignerId(id, input); }, + async products({ id }, _args, { dataSources }) { + return (dataSources.productApi).getDesignerProducts(id); + }, }; async function getDesigners(_, { limit }, { dataSources }) { diff --git a/src/resolvers/products-resolver.ts b/src/resolvers/products-resolver.ts index a979654..ae27bd5 100644 --- a/src/resolvers/products-resolver.ts +++ b/src/resolvers/products-resolver.ts @@ -3,6 +3,7 @@ import { Product, ProductListResult, ProductsFilterInput, + ProductsSearchResult, } from '../types/product-types'; import * as CONFIG from '../config'; import { CategoryAPI } from '../datasources/category-api'; @@ -14,6 +15,7 @@ import { InteriorAPI } from '../datasources/interior-api'; import { InteriorsLambdaAPI } from '../datasources/interiors-lambda-api'; import { IdNumberResult } from '../types/types'; import { moveS3File } from '../s3'; +import { DesignerAPI } from '../datasources/designer-api'; const ProductBlacklist = { async market(parent, _args, { dataSources }) { @@ -66,6 +68,30 @@ async function getProductsResult( }; } +async function getProductsSearchResult( + _, + { q }, + { dataSources }, +): Promise { + const prodApi = dataSources.productApi; + const catApi = dataSources.categoryApi; + const keywApi = dataSources.keywordApi; + const designerApi = dataSources.designerApi; + const prods = Promise.all([ + prodApi.searchByName(q), + prodApi.searchByArtNo(q), + ]).then((d) => d.flat()); + + return { + q: q, + products: prods, + batches: prodApi.searchByBatch(q), + keywords: keywApi.searchKeywords(q), + categories: catApi.searchCategories(q), + designers: designerApi.searchDesigners(q), + }; +} + async function getProduct(_, { id }, { dataSources }): Promise { return (dataSources.productApi).getProduct(id); } @@ -79,6 +105,7 @@ export const productTypeDefs = { export const productQueryTypeDefs = { products: getProductsResult, product: getProduct, + productsSearch: getProductsSearchResult, }; /////////////////// diff --git a/src/schema.graphql b/src/schema.graphql index 5788a7c..c765090 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -21,9 +21,10 @@ type Query { order(id: ID!): Order products( pagination: PaginationInput! - filter: ProductsFilterInput! + filter: ProductsFilterInput ): ProductsResult product(id: Int!): Product + productsSearch(q: String!): ProductsSearchResult categories: [Category] category(id: Int!): Category keywords: [Keyword] @@ -82,6 +83,18 @@ type ProductsResult { items: [Product] } +type ProductsSearchResult { + q: String! + """ + products contains matches for `name` and `artNo` + """ + products: [Product] + batches: [Batch] + keywords: [Keyword] + categories: [Category] + designers: [Designer] +} + input InteriorsFilterInput { productId: Int! } @@ -245,6 +258,11 @@ type Category { products: [Product] } +type Batch { + name: String! + products: [Product] +} + type Product { id: ID! stockid: Int @@ -378,6 +396,7 @@ type Designer { name: String path: String orderRows(pagination: PaginationInput!, filter: DateFilterInput): [OrderRow] + products: [Product] } enum KeywordType { diff --git a/src/types/product-types.ts b/src/types/product-types.ts index 7cd1061..a440ce8 100644 --- a/src/types/product-types.ts +++ b/src/types/product-types.ts @@ -1,3 +1,6 @@ +import { Category } from './category-types'; +import { Designer } from './designer-types'; +import { Keyword } from './keyword-types'; import { Maybe, PaginationInput, PaginationResult } from './types'; // Mutation inputs @@ -45,6 +48,14 @@ export interface ProductsFilter { browsable?: boolean; } +export interface ProductsSearchResult { + q: string; + products: Promise>; + batches: Promise>; + keywords: Promise>; + categories: Promise>; + designers: Promise>; +} export interface ProductListResult { pagination: PaginationResult; items: Array | Promise>; @@ -112,6 +123,11 @@ export interface PrintProduct { updated?: Date; } +export interface Batch { + name: string; + products: Promise>; +} + export interface Product { id: number; stockid: number;