Add product search (#64)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -32,6 +32,13 @@ WHERE product_category.product_id = ?
|
||||
return this.knex.select('*').from('v_categorytree').cache(MINUTE);
|
||||
}
|
||||
|
||||
async searchCategories(name: string): Promise<Array<Category>> {
|
||||
return this.knex
|
||||
.select('*')
|
||||
.from('v_categorytree')
|
||||
.whereRaw(`LOWER(name) LIKE ?`, [`${name}`]);
|
||||
}
|
||||
|
||||
async getCategoryById(id: number): Promise<Category> {
|
||||
return this.knex
|
||||
.select('*')
|
||||
|
||||
@@ -23,6 +23,21 @@ export class DesignerAPI extends BaseSQLDataSource {
|
||||
});
|
||||
}
|
||||
|
||||
async searchDesigners(name: string): Promise<Array<Designer>> {
|
||||
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<number>): Promise<Array<Designer>> {
|
||||
limit = limit ?? 5000;
|
||||
return this.knex
|
||||
|
||||
@@ -55,6 +55,14 @@ ORDER BY keywords.value
|
||||
.then((row) => this.getType(row));
|
||||
}
|
||||
|
||||
async searchKeywords(name: string): Promise<Array<Keyword>> {
|
||||
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<number>,
|
||||
|
||||
@@ -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<Array<Product>> {
|
||||
const query = sql.designerProducts();
|
||||
|
||||
return this.knex
|
||||
.raw(query, designerId)
|
||||
.then((data) => data.rows.map((row) => this.createProductFromRow(row)));
|
||||
}
|
||||
|
||||
async getCategoryProducts(categoryId: number): Promise<Array<Product>> {
|
||||
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<Array<Product>> {
|
||||
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<Array<Product>> {
|
||||
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<Array<Batch>> {
|
||||
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 <Batch>{
|
||||
name: key,
|
||||
products: batches[key],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getRelatedProducts(id: number): Promise<Array<Product>> {
|
||||
const ids = await this.knex
|
||||
.raw(
|
||||
|
||||
@@ -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
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (<OrderAPI>dataSources.orderApi).getOrderRowsByDesignerId(id, input);
|
||||
},
|
||||
async products({ id }, _args, { dataSources }) {
|
||||
return (<ProductAPI>dataSources.productApi).getDesignerProducts(id);
|
||||
},
|
||||
};
|
||||
|
||||
async function getDesigners(_, { limit }, { dataSources }) {
|
||||
|
||||
@@ -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<ProductsSearchResult> {
|
||||
const prodApi = <ProductAPI>dataSources.productApi;
|
||||
const catApi = <CategoryAPI>dataSources.categoryApi;
|
||||
const keywApi = <KeywordAPI>dataSources.keywordApi;
|
||||
const designerApi = <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<Product> {
|
||||
return (<ProductAPI>dataSources.productApi).getProduct(id);
|
||||
}
|
||||
@@ -79,6 +105,7 @@ export const productTypeDefs = {
|
||||
export const productQueryTypeDefs = {
|
||||
products: getProductsResult,
|
||||
product: getProduct,
|
||||
productsSearch: getProductsSearchResult,
|
||||
};
|
||||
|
||||
///////////////////
|
||||
|
||||
+20
-1
@@ -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 {
|
||||
|
||||
@@ -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<Array<Product>>;
|
||||
batches: Promise<Array<Batch>>;
|
||||
keywords: Promise<Array<Keyword>>;
|
||||
categories: Promise<Array<Category>>;
|
||||
designers: Promise<Array<Designer>>;
|
||||
}
|
||||
export interface ProductListResult {
|
||||
pagination: PaginationResult;
|
||||
items: Array<Product> | Promise<Array<Product>>;
|
||||
@@ -112,6 +123,11 @@ export interface PrintProduct {
|
||||
updated?: Date;
|
||||
}
|
||||
|
||||
export interface Batch {
|
||||
name: string;
|
||||
products: Promise<Array<Product>>;
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: number;
|
||||
stockid: number;
|
||||
|
||||
Reference in New Issue
Block a user