Add product search (#64)
This commit is contained in:
@@ -19,6 +19,7 @@ services:
|
|||||||
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
|
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
|
||||||
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
|
||||||
- AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}
|
- AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}
|
||||||
|
- NODE_ENV=development
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ WHERE product_category.product_id = ?
|
|||||||
return this.knex.select('*').from('v_categorytree').cache(MINUTE);
|
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> {
|
async getCategoryById(id: number): Promise<Category> {
|
||||||
return this.knex
|
return this.knex
|
||||||
.select('*')
|
.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>> {
|
async getDesigners(limit: Maybe<number>): Promise<Array<Designer>> {
|
||||||
limit = limit ?? 5000;
|
limit = limit ?? 5000;
|
||||||
return this.knex
|
return this.knex
|
||||||
|
|||||||
@@ -55,6 +55,14 @@ ORDER BY keywords.value
|
|||||||
.then((row) => this.getType(row));
|
.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(
|
async setProductKeywords(
|
||||||
productId: number,
|
productId: number,
|
||||||
keywordIds: Array<number>,
|
keywordIds: Array<number>,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
ProductMaterials,
|
ProductMaterials,
|
||||||
ProductSizeTypes,
|
ProductSizeTypes,
|
||||||
ProductProportionsInput,
|
ProductProportionsInput,
|
||||||
|
Batch,
|
||||||
} from '../types/product-types';
|
} from '../types/product-types';
|
||||||
import { Maybe } from '../types/types';
|
import { Maybe } from '../types/types';
|
||||||
import { booleanStringField, nullOrNumber, roundOrDefaultTo } from './utils';
|
import { booleanStringField, nullOrNumber, roundOrDefaultTo } from './utils';
|
||||||
@@ -179,6 +180,14 @@ export class ProductAPI extends BaseSQLDataSource {
|
|||||||
return res.find(Boolean);
|
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>> {
|
async getCategoryProducts(categoryId: number): Promise<Array<Product>> {
|
||||||
const query = sql.categoryProducts(categoryId);
|
const query = sql.categoryProducts(categoryId);
|
||||||
|
|
||||||
@@ -195,6 +204,45 @@ export class ProductAPI extends BaseSQLDataSource {
|
|||||||
.then((data) => data.rows.map((row) => this.createProductFromRow(row)));
|
.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>> {
|
async getRelatedProducts(id: number): Promise<Array<Product>> {
|
||||||
const ids = await this.knex
|
const ids = await this.knex
|
||||||
.raw(
|
.raw(
|
||||||
|
|||||||
@@ -77,17 +77,18 @@ FROM "product-products" products
|
|||||||
|
|
||||||
export function products(input: ProductsFilterInput) {
|
export function products(input: ProductsFilterInput) {
|
||||||
const limit = input.pagination.limit ?? 100;
|
const limit = input.pagination.limit ?? 100;
|
||||||
const filter = input.filter;
|
const filter = input.filter ?? null;
|
||||||
return (
|
return (
|
||||||
baseQuery +
|
baseQuery +
|
||||||
/* sql */ `
|
/* sql */ `
|
||||||
${
|
${
|
||||||
filter.visible != null
|
filter?.visible != null
|
||||||
? /* sql */ `
|
? /* sql */ `
|
||||||
WHERE products.visible = ${filter?.visible ? '1' : '0'}
|
WHERE products.visible = ${filter?.visible ? '1' : '0'}
|
||||||
AND products.browsable = ${filter?.browsable ? '1' : '0'}`
|
AND products.browsable = ${filter?.browsable ? '1' : '0'}`
|
||||||
: ``
|
: ``
|
||||||
}
|
}
|
||||||
|
ORDER BY products.inserted DESC
|
||||||
LIMIT ${limit}
|
LIMIT ${limit}
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
@@ -98,7 +99,7 @@ export function productsTotal(input: ProductsFilterInput) {
|
|||||||
return /*sql */ `
|
return /*sql */ `
|
||||||
SELECT count(*) FROM "product-products" products
|
SELECT count(*) FROM "product-products" products
|
||||||
${
|
${
|
||||||
filter.visible != null
|
filter?.visible != null
|
||||||
? /* sql */ `
|
? /* sql */ `
|
||||||
WHERE products.visible = ${filter?.visible ? '1' : '0'}
|
WHERE products.visible = ${filter?.visible ? '1' : '0'}
|
||||||
AND products.browsable = ${filter?.browsable ? '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) {
|
export function keywordProducts(keywordId: number) {
|
||||||
return (
|
return (
|
||||||
baseQuery +
|
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 { DesignerAPI } from '../datasources/designer-api';
|
||||||
import { OrderAPI } from '../datasources/order-api';
|
import { OrderAPI } from '../datasources/order-api';
|
||||||
|
import { ProductAPI } from '../datasources/product-api';
|
||||||
import { GeneralInput } from '../types/types';
|
import { GeneralInput } from '../types/types';
|
||||||
|
|
||||||
const Designer = {
|
const Designer = {
|
||||||
async orderRows({ id }, input: GeneralInput, { dataSources }) {
|
async orderRows({ id }, input: GeneralInput, { dataSources }) {
|
||||||
return (<OrderAPI>dataSources.orderApi).getOrderRowsByDesignerId(id, input);
|
return (<OrderAPI>dataSources.orderApi).getOrderRowsByDesignerId(id, input);
|
||||||
},
|
},
|
||||||
|
async products({ id }, _args, { dataSources }) {
|
||||||
|
return (<ProductAPI>dataSources.productApi).getDesignerProducts(id);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getDesigners(_, { limit }, { dataSources }) {
|
async function getDesigners(_, { limit }, { dataSources }) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Product,
|
Product,
|
||||||
ProductListResult,
|
ProductListResult,
|
||||||
ProductsFilterInput,
|
ProductsFilterInput,
|
||||||
|
ProductsSearchResult,
|
||||||
} from '../types/product-types';
|
} from '../types/product-types';
|
||||||
import * as CONFIG from '../config';
|
import * as CONFIG from '../config';
|
||||||
import { CategoryAPI } from '../datasources/category-api';
|
import { CategoryAPI } from '../datasources/category-api';
|
||||||
@@ -14,6 +15,7 @@ import { InteriorAPI } from '../datasources/interior-api';
|
|||||||
import { InteriorsLambdaAPI } from '../datasources/interiors-lambda-api';
|
import { InteriorsLambdaAPI } from '../datasources/interiors-lambda-api';
|
||||||
import { IdNumberResult } from '../types/types';
|
import { IdNumberResult } from '../types/types';
|
||||||
import { moveS3File } from '../s3';
|
import { moveS3File } from '../s3';
|
||||||
|
import { DesignerAPI } from '../datasources/designer-api';
|
||||||
|
|
||||||
const ProductBlacklist = {
|
const ProductBlacklist = {
|
||||||
async market(parent, _args, { dataSources }) {
|
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> {
|
async function getProduct(_, { id }, { dataSources }): Promise<Product> {
|
||||||
return (<ProductAPI>dataSources.productApi).getProduct(id);
|
return (<ProductAPI>dataSources.productApi).getProduct(id);
|
||||||
}
|
}
|
||||||
@@ -79,6 +105,7 @@ export const productTypeDefs = {
|
|||||||
export const productQueryTypeDefs = {
|
export const productQueryTypeDefs = {
|
||||||
products: getProductsResult,
|
products: getProductsResult,
|
||||||
product: getProduct,
|
product: getProduct,
|
||||||
|
productsSearch: getProductsSearchResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
///////////////////
|
///////////////////
|
||||||
|
|||||||
+20
-1
@@ -21,9 +21,10 @@ type Query {
|
|||||||
order(id: ID!): Order
|
order(id: ID!): Order
|
||||||
products(
|
products(
|
||||||
pagination: PaginationInput!
|
pagination: PaginationInput!
|
||||||
filter: ProductsFilterInput!
|
filter: ProductsFilterInput
|
||||||
): ProductsResult
|
): ProductsResult
|
||||||
product(id: Int!): Product
|
product(id: Int!): Product
|
||||||
|
productsSearch(q: String!): ProductsSearchResult
|
||||||
categories: [Category]
|
categories: [Category]
|
||||||
category(id: Int!): Category
|
category(id: Int!): Category
|
||||||
keywords: [Keyword]
|
keywords: [Keyword]
|
||||||
@@ -82,6 +83,18 @@ type ProductsResult {
|
|||||||
items: [Product]
|
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 {
|
input InteriorsFilterInput {
|
||||||
productId: Int!
|
productId: Int!
|
||||||
}
|
}
|
||||||
@@ -245,6 +258,11 @@ type Category {
|
|||||||
products: [Product]
|
products: [Product]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Batch {
|
||||||
|
name: String!
|
||||||
|
products: [Product]
|
||||||
|
}
|
||||||
|
|
||||||
type Product {
|
type Product {
|
||||||
id: ID!
|
id: ID!
|
||||||
stockid: Int
|
stockid: Int
|
||||||
@@ -378,6 +396,7 @@ type Designer {
|
|||||||
name: String
|
name: String
|
||||||
path: String
|
path: String
|
||||||
orderRows(pagination: PaginationInput!, filter: DateFilterInput): [OrderRow]
|
orderRows(pagination: PaginationInput!, filter: DateFilterInput): [OrderRow]
|
||||||
|
products: [Product]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum KeywordType {
|
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';
|
import { Maybe, PaginationInput, PaginationResult } from './types';
|
||||||
|
|
||||||
// Mutation inputs
|
// Mutation inputs
|
||||||
@@ -45,6 +48,14 @@ export interface ProductsFilter {
|
|||||||
browsable?: boolean;
|
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 {
|
export interface ProductListResult {
|
||||||
pagination: PaginationResult;
|
pagination: PaginationResult;
|
||||||
items: Array<Product> | Promise<Array<Product>>;
|
items: Array<Product> | Promise<Array<Product>>;
|
||||||
@@ -112,6 +123,11 @@ export interface PrintProduct {
|
|||||||
updated?: Date;
|
updated?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Batch {
|
||||||
|
name: string;
|
||||||
|
products: Promise<Array<Product>>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Product {
|
export interface Product {
|
||||||
id: number;
|
id: number;
|
||||||
stockid: number;
|
stockid: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user