fix(medusa): add total count for list queries in product (#710)
This commit is contained in:
1 parent
075864f24c
commit
109d400720
5 files changed
+259
-99
No files matched your search
@@ -21,14 +21,12 @@ describe("GET /admin/products", () => {
|
|||||||
|
|
||||||
it("returns 200 and decorated products", () => {
|
it("returns 200 and decorated products", () => {
|
||||||
expect(subject.status).toEqual(200)
|
expect(subject.status).toEqual(200)
|
||||||
expect(subject.body.products[0]._id).toEqual(products.product1._id)
|
expect(subject.body.products[0].id).toEqual(products.product1.id)
|
||||||
expect(subject.body.products[0].decorated).toEqual(true)
|
expect(subject.body.products[1].id).toEqual(products.product2.id)
|
||||||
expect(subject.body.products[1]._id).toEqual(products.product2._id)
|
|
||||||
expect(subject.body.products[1].decorated).toEqual(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("calls update", () => {
|
it("calls update", () => {
|
||||||
expect(ProductServiceMock.list).toHaveBeenCalledTimes(1)
|
expect(ProductServiceMock.listAndCount).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -82,7 +82,10 @@ export default async (req, res) => {
|
|||||||
take: limit,
|
take: limit,
|
||||||
}
|
}
|
||||||
|
|
||||||
const products = await productService.list(selector, listConfig)
|
const [products, count] = await productService.listAndCount(
|
||||||
|
selector,
|
||||||
|
listConfig
|
||||||
|
)
|
||||||
|
|
||||||
res.json({ products, count: products.length, offset, limit })
|
res.json({ products, count, offset, limit })
|
||||||
}
|
}
|
||||||
@@ -20,45 +20,50 @@ type FindWithRelationsOptions = CustomOptions
|
|||||||
|
|
||||||
@EntityRepository(Product)
|
@EntityRepository(Product)
|
||||||
export class ProductRepository extends Repository<Product> {
|
export class ProductRepository extends Repository<Product> {
|
||||||
public async findWithRelations(
|
private mergeEntitiesWithRelations(
|
||||||
relations: Array<keyof Product> = [],
|
entitiesAndRelations: Array<Partial<Product>>
|
||||||
idsOrOptionsWithoutRelations: FindWithRelationsOptions = {}
|
): Product[] {
|
||||||
): Promise<Product[]> {
|
const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id")
|
||||||
|
return map(entitiesAndRelationsById, (entityAndRelations) =>
|
||||||
|
merge({}, ...entityAndRelations)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async queryProducts(
|
||||||
|
optionsWithoutRelations: FindWithRelationsOptions,
|
||||||
|
shouldCount: boolean = false
|
||||||
|
): Promise<[Product[], number]> {
|
||||||
|
const tags = optionsWithoutRelations.where.tags
|
||||||
|
delete optionsWithoutRelations.where.tags
|
||||||
|
let qb = this.createQueryBuilder("product")
|
||||||
|
.select(["product.id"])
|
||||||
|
.where(optionsWithoutRelations.where)
|
||||||
|
.skip(optionsWithoutRelations.skip)
|
||||||
|
.take(optionsWithoutRelations.take)
|
||||||
|
.orderBy(optionsWithoutRelations.order)
|
||||||
|
|
||||||
|
if (tags) {
|
||||||
|
qb = qb
|
||||||
|
.leftJoinAndSelect("product.tags", "tags")
|
||||||
|
.andWhere(`tags.id IN (:...ids)`, { ids: tags._value })
|
||||||
|
}
|
||||||
|
|
||||||
let entities: Product[]
|
let entities: Product[]
|
||||||
if (Array.isArray(idsOrOptionsWithoutRelations)) {
|
let count = null
|
||||||
entities = await this.findByIds(idsOrOptionsWithoutRelations)
|
if (shouldCount) {
|
||||||
|
const result = await qb.getManyAndCount()
|
||||||
|
entities = result[0]
|
||||||
|
count = result[1]
|
||||||
} else {
|
} else {
|
||||||
// Since tags are in a one-to-many realtion they cant be included in a
|
|
||||||
// regular query, to solve this add the join on tags seperately if
|
|
||||||
// the query exists
|
|
||||||
const tags = idsOrOptionsWithoutRelations.where.tags
|
|
||||||
delete idsOrOptionsWithoutRelations.where.tags
|
|
||||||
let qb = this.createQueryBuilder("product")
|
|
||||||
.select(["product.id"])
|
|
||||||
.where(idsOrOptionsWithoutRelations.where)
|
|
||||||
.skip(idsOrOptionsWithoutRelations.skip)
|
|
||||||
.take(idsOrOptionsWithoutRelations.take)
|
|
||||||
.orderBy(idsOrOptionsWithoutRelations.order)
|
|
||||||
|
|
||||||
if (tags) {
|
|
||||||
qb = qb
|
|
||||||
.leftJoinAndSelect("product.tags", "tags")
|
|
||||||
.andWhere(`tags.id IN (:...ids)`, { ids: tags._value })
|
|
||||||
}
|
|
||||||
|
|
||||||
entities = await qb.getMany()
|
entities = await qb.getMany()
|
||||||
}
|
}
|
||||||
const entitiesIds = entities.map(({ id }) => id)
|
|
||||||
|
|
||||||
if (entitiesIds.length === 0) {
|
return [entities, count]
|
||||||
// no need to continue
|
}
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
if (relations.length === 0) {
|
|
||||||
return this.findByIds(entitiesIds, idsOrOptionsWithoutRelations)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
private getGroupedRelations(relations: Array<keyof Product>): {
|
||||||
|
[toplevel: string]: string[]
|
||||||
|
} {
|
||||||
const groupedRelations: { [toplevel: string]: string[] } = {}
|
const groupedRelations: { [toplevel: string]: string[] } = {}
|
||||||
for (const rel of relations) {
|
for (const rel of relations) {
|
||||||
const [topLevel] = rel.split(".")
|
const [topLevel] = rel.split(".")
|
||||||
@@ -69,6 +74,13 @@ export class ProductRepository extends Repository<Product> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return groupedRelations
|
||||||
|
}
|
||||||
|
|
||||||
|
private async queryProductsWithIds(
|
||||||
|
entityIds: string[],
|
||||||
|
groupedRelations: { [toplevel: string]: string[] }
|
||||||
|
): Promise<Product[]> {
|
||||||
const entitiesIdsWithRelations = await Promise.all(
|
const entitiesIdsWithRelations = await Promise.all(
|
||||||
Object.entries(groupedRelations).map(([toplevel, rels]) => {
|
Object.entries(groupedRelations).map(([toplevel, rels]) => {
|
||||||
let querybuilder = this.createQueryBuilder("products")
|
let querybuilder = this.createQueryBuilder("products")
|
||||||
@@ -105,18 +117,94 @@ export class ProductRepository extends Repository<Product> {
|
|||||||
return querybuilder
|
return querybuilder
|
||||||
.where(
|
.where(
|
||||||
"products.deleted_at IS NULL AND products.id IN (:...entitiesIds)",
|
"products.deleted_at IS NULL AND products.id IN (:...entitiesIds)",
|
||||||
{ entitiesIds }
|
{ entitiesIds: entityIds }
|
||||||
)
|
)
|
||||||
.getMany()
|
.getMany()
|
||||||
})
|
})
|
||||||
).then(flatten)
|
).then(flatten)
|
||||||
|
|
||||||
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
|
return entitiesIdsWithRelations
|
||||||
|
}
|
||||||
|
|
||||||
const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id")
|
public async findWithRelationsAndCount(
|
||||||
return map(entitiesAndRelationsById, (entityAndRelations) =>
|
relations: Array<keyof Product> = [],
|
||||||
merge({}, ...entityAndRelations)
|
idsOrOptionsWithoutRelations: FindWithRelationsOptions = {}
|
||||||
|
): Promise<[Product[], number]> {
|
||||||
|
let count: number
|
||||||
|
let entities: Product[]
|
||||||
|
if (Array.isArray(idsOrOptionsWithoutRelations)) {
|
||||||
|
entities = await this.findByIds(idsOrOptionsWithoutRelations)
|
||||||
|
count = entities.length
|
||||||
|
} else {
|
||||||
|
const result = await this.queryProducts(
|
||||||
|
idsOrOptionsWithoutRelations,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
entities = result[0]
|
||||||
|
count = result[1]
|
||||||
|
}
|
||||||
|
const entitiesIds = entities.map(({ id }) => id)
|
||||||
|
|
||||||
|
if (entitiesIds.length === 0) {
|
||||||
|
// no need to continue
|
||||||
|
return [[], count]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relations.length === 0) {
|
||||||
|
const toReturn = await this.findByIds(
|
||||||
|
entitiesIds,
|
||||||
|
idsOrOptionsWithoutRelations
|
||||||
|
)
|
||||||
|
return [toReturn, toReturn.length]
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupedRelations = this.getGroupedRelations(relations)
|
||||||
|
const entitiesIdsWithRelations = await this.queryProductsWithIds(
|
||||||
|
entitiesIds,
|
||||||
|
groupedRelations
|
||||||
)
|
)
|
||||||
|
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
|
||||||
|
const entitiesToReturn =
|
||||||
|
this.mergeEntitiesWithRelations(entitiesAndRelations)
|
||||||
|
|
||||||
|
return [entitiesToReturn, count]
|
||||||
|
}
|
||||||
|
|
||||||
|
public async findWithRelations(
|
||||||
|
relations: Array<keyof Product> = [],
|
||||||
|
idsOrOptionsWithoutRelations: FindWithRelationsOptions = {}
|
||||||
|
): Promise<Product[]> {
|
||||||
|
let entities: Product[]
|
||||||
|
if (Array.isArray(idsOrOptionsWithoutRelations)) {
|
||||||
|
entities = await this.findByIds(idsOrOptionsWithoutRelations)
|
||||||
|
} else {
|
||||||
|
const result = await this.queryProducts(
|
||||||
|
idsOrOptionsWithoutRelations,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
entities = result[0]
|
||||||
|
}
|
||||||
|
const entitiesIds = entities.map(({ id }) => id)
|
||||||
|
|
||||||
|
if (entitiesIds.length === 0) {
|
||||||
|
// no need to continue
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relations.length === 0) {
|
||||||
|
return await this.findByIds(entitiesIds, idsOrOptionsWithoutRelations)
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupedRelations = this.getGroupedRelations(relations)
|
||||||
|
const entitiesIdsWithRelations = await this.queryProductsWithIds(
|
||||||
|
entitiesIds,
|
||||||
|
groupedRelations
|
||||||
|
)
|
||||||
|
const entitiesAndRelations = entitiesIdsWithRelations.concat(entities)
|
||||||
|
const entitiesToReturn =
|
||||||
|
this.mergeEntitiesWithRelations(entitiesAndRelations)
|
||||||
|
|
||||||
|
return entitiesToReturn
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findOneWithRelations(
|
public async findOneWithRelations(
|
||||||
|
|||||||
@@ -63,10 +63,10 @@ export const products = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ProductServiceMock = {
|
export const ProductServiceMock = {
|
||||||
withTransaction: function() {
|
withTransaction: function () {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
create: jest.fn().mockImplementation(data => {
|
create: jest.fn().mockImplementation((data) => {
|
||||||
if (data.title === "Test Product") {
|
if (data.title === "Test Product") {
|
||||||
return Promise.resolve(products.product1)
|
return Promise.resolve(products.product1)
|
||||||
}
|
}
|
||||||
@@ -76,14 +76,14 @@ export const ProductServiceMock = {
|
|||||||
return Promise.resolve({ ...data })
|
return Promise.resolve({ ...data })
|
||||||
}),
|
}),
|
||||||
count: jest.fn().mockReturnValue(4),
|
count: jest.fn().mockReturnValue(4),
|
||||||
publish: jest.fn().mockImplementation(_ => {
|
publish: jest.fn().mockImplementation((_) => {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
id: IdMap.getId("publish"),
|
id: IdMap.getId("publish"),
|
||||||
name: "Product 1",
|
name: "Product 1",
|
||||||
published: true,
|
published: true,
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
delete: jest.fn().mockImplementation(_ => {
|
delete: jest.fn().mockImplementation((_) => {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}),
|
}),
|
||||||
createVariant: jest.fn().mockImplementation((productId, value) => {
|
createVariant: jest.fn().mockImplementation((productId, value) => {
|
||||||
@@ -111,7 +111,7 @@ export const ProductServiceMock = {
|
|||||||
.mockReturnValue(
|
.mockReturnValue(
|
||||||
Promise.resolve([{ id: IdMap.getId("1") }, { id: IdMap.getId("2") }])
|
Promise.resolve([{ id: IdMap.getId("1") }, { id: IdMap.getId("2") }])
|
||||||
),
|
),
|
||||||
retrieve: jest.fn().mockImplementation(productId => {
|
retrieve: jest.fn().mockImplementation((productId) => {
|
||||||
if (productId === IdMap.getId("product1")) {
|
if (productId === IdMap.getId("product1")) {
|
||||||
return Promise.resolve(products.product1)
|
return Promise.resolve(products.product1)
|
||||||
}
|
}
|
||||||
@@ -135,7 +135,10 @@ export const ProductServiceMock = {
|
|||||||
update: jest.fn().mockImplementation((product, data) => {
|
update: jest.fn().mockImplementation((product, data) => {
|
||||||
return Promise.resolve(products.product1)
|
return Promise.resolve(products.product1)
|
||||||
}),
|
}),
|
||||||
list: jest.fn().mockImplementation(data => {
|
listAndCount: jest.fn().mockImplementation((data) => {
|
||||||
|
return Promise.resolve([[products.product1, products.product2], 2])
|
||||||
|
}),
|
||||||
|
list: jest.fn().mockImplementation((data) => {
|
||||||
// Used to retrieve a product based on a variant id see
|
// Used to retrieve a product based on a variant id see
|
||||||
// ProductVariantService.addOptionValue
|
// ProductVariantService.addOptionValue
|
||||||
if (data.variants === IdMap.getId("giftCardVar")) {
|
if (data.variants === IdMap.getId("giftCardVar")) {
|
||||||
|
|||||||
@@ -87,82 +87,86 @@ class ProductService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {object} selector - selector for query
|
* Lists products based on the provided parameters.
|
||||||
* @param {Object} config - config for query object for find
|
* @param {object} selector - an object that defines rules to filter products
|
||||||
* @return {Promise} the result of the find operation
|
* by
|
||||||
|
* @param {object} config - object that defines the scope for what should be
|
||||||
|
* returned
|
||||||
|
* @return {Promise<Product[]>} the result of the find operation
|
||||||
*/
|
*/
|
||||||
async list(selector = {}, config = { relations: [], skip: 0, take: 20 }) {
|
async list(selector = {}, config = { relations: [], skip: 0, take: 20 }) {
|
||||||
const productRepo = this.manager_.getCustomRepository(
|
const productRepo = this.manager_.getCustomRepository(
|
||||||
this.productRepository_
|
this.productRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
let q
|
const { q, query, relations } = this.prepareListQuery_(selector, config)
|
||||||
if ("q" in selector) {
|
|
||||||
q = selector.q
|
|
||||||
delete selector.q
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = this.buildQuery_(selector, config)
|
|
||||||
|
|
||||||
if (config.relations && config.relations.length > 0) {
|
|
||||||
query.relations = config.relations
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.select && config.select.length > 0) {
|
|
||||||
query.select = config.select
|
|
||||||
}
|
|
||||||
|
|
||||||
const rels = query.relations
|
|
||||||
delete query.relations
|
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
const where = query.where
|
const qb = this.getFreeTextQueryBuilder_(productRepo, query, q)
|
||||||
|
const raw = await qb.getMany()
|
||||||
delete where.description
|
|
||||||
delete where.title
|
|
||||||
|
|
||||||
const raw = await productRepo
|
|
||||||
.createQueryBuilder("product")
|
|
||||||
.leftJoinAndSelect("product.variants", "variant")
|
|
||||||
.leftJoinAndSelect("product.collection", "collection")
|
|
||||||
.select(["product.id"])
|
|
||||||
.where(where)
|
|
||||||
.andWhere(
|
|
||||||
new Brackets((qb) => {
|
|
||||||
qb.where(`product.description ILIKE :q`, { q: `%${q}%` })
|
|
||||||
.orWhere(`product.title ILIKE :q`, { q: `%${q}%` })
|
|
||||||
.orWhere(`variant.title ILIKE :q`, { q: `%${q}%` })
|
|
||||||
.orWhere(`variant.sku ILIKE :q`, { q: `%${q}%` })
|
|
||||||
.orWhere(`collection.title ILIKE :q`, { q: `%${q}%` })
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.getMany()
|
|
||||||
|
|
||||||
return productRepo.findWithRelations(
|
return productRepo.findWithRelations(
|
||||||
rels,
|
relations,
|
||||||
raw.map((i) => i.id)
|
raw.map((i) => i.id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return productRepo.findWithRelations(rels, query)
|
return productRepo.findWithRelations(relations, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists products based on the provided parameters and includes the count of
|
||||||
|
* products that match the query.
|
||||||
|
* @param {object} selector - an object that defines rules to filter products
|
||||||
|
* by
|
||||||
|
* @param {object} config - object that defines the scope for what should be
|
||||||
|
* returned
|
||||||
|
* @return {[Promise<Product[]>, number]} an array containing the products as
|
||||||
|
* the first element and the total count of products that matches the query
|
||||||
|
* as the second element.
|
||||||
|
*/
|
||||||
|
async listAndCount(
|
||||||
|
selector = {},
|
||||||
|
config = { relations: [], skip: 0, take: 20 }
|
||||||
|
) {
|
||||||
|
const productRepo = this.manager_.getCustomRepository(
|
||||||
|
this.productRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
const { q, query, relations } = this.prepareListQuery_(selector, config)
|
||||||
|
|
||||||
|
if (q) {
|
||||||
|
const qb = this.getFreeTextQueryBuilder_(productRepo, query, q)
|
||||||
|
const [raw, count] = await qb.getManyAndCount()
|
||||||
|
|
||||||
|
const products = await productRepo.findWithRelations(
|
||||||
|
relations,
|
||||||
|
raw.map((i) => i.id)
|
||||||
|
)
|
||||||
|
return [products, count]
|
||||||
|
}
|
||||||
|
|
||||||
|
return await productRepo.findWithRelationsAndCount(relations, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the total number of documents in database
|
* Return the total number of documents in database
|
||||||
|
* @param {object} selector - the selector to choose products by
|
||||||
* @return {Promise} the result of the count operation
|
* @return {Promise} the result of the count operation
|
||||||
*/
|
*/
|
||||||
count() {
|
count(selector = {}) {
|
||||||
const productRepo = this.manager_.getCustomRepository(
|
const productRepo = this.manager_.getCustomRepository(
|
||||||
this.productRepository_
|
this.productRepository_
|
||||||
)
|
)
|
||||||
return productRepo.count()
|
const query = this.buildQuery_(selector)
|
||||||
|
return productRepo.count(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a product by id.
|
* Gets a product by id.
|
||||||
* Throws in case of DB Error and if product was not found.
|
* Throws in case of DB Error and if product was not found.
|
||||||
* @param {string} productId - id of the product to get.
|
* @param {string} productId - id of the product to get.
|
||||||
* @param {object} config - config of the product to get.
|
* @param {object} config - object that defines what should be included in the
|
||||||
|
* query response
|
||||||
* @return {Promise<Product>} the result of the find one operation.
|
* @return {Promise<Product>} the result of the find one operation.
|
||||||
*/
|
*/
|
||||||
async retrieve(productId, config = {}) {
|
async retrieve(productId, config = {}) {
|
||||||
@@ -747,6 +751,70 @@ class ProductService extends BaseService {
|
|||||||
// const final = await this.runDecorators_(decorated)
|
// const final = await this.runDecorators_(decorated)
|
||||||
return product
|
return product
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a query object to be used for list queries.
|
||||||
|
* @param {object} selector - the selector to create the query from
|
||||||
|
* @param {object} config - the config to use for the query
|
||||||
|
* @return {object} an object containing the query, relations and free-text
|
||||||
|
* search param.
|
||||||
|
*/
|
||||||
|
prepareListQuery_(selector, config) {
|
||||||
|
let q
|
||||||
|
if ("q" in selector) {
|
||||||
|
q = selector.q
|
||||||
|
delete selector.q
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = this.buildQuery_(selector, config)
|
||||||
|
|
||||||
|
if (config.relations && config.relations.length > 0) {
|
||||||
|
query.relations = config.relations
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.select && config.select.length > 0) {
|
||||||
|
query.select = config.select
|
||||||
|
}
|
||||||
|
|
||||||
|
const rels = query.relations
|
||||||
|
delete query.relations
|
||||||
|
|
||||||
|
return {
|
||||||
|
query,
|
||||||
|
relations: rels,
|
||||||
|
q,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a QueryBuilder that can fetch products based on free text.
|
||||||
|
* @param {ProductRepository} productRepo - an instance of a ProductRepositry
|
||||||
|
* @param {FindOptions<Product>} query - the query to get products by
|
||||||
|
* @param {string} q - the text to perform free text search from
|
||||||
|
* @return {QueryBuilder<Product>} a query builder that can fetch products
|
||||||
|
*/
|
||||||
|
getFreeTextQueryBuilder_(productRepo, query, q) {
|
||||||
|
const where = query.where
|
||||||
|
|
||||||
|
delete where.description
|
||||||
|
delete where.title
|
||||||
|
|
||||||
|
return productRepo
|
||||||
|
.createQueryBuilder("product")
|
||||||
|
.leftJoinAndSelect("product.variants", "variant")
|
||||||
|
.leftJoinAndSelect("product.collection", "collection")
|
||||||
|
.select(["product.id"])
|
||||||
|
.where(where)
|
||||||
|
.andWhere(
|
||||||
|
new Brackets((qb) => {
|
||||||
|
qb.where(`product.description ILIKE :q`, { q: `%${q}%` })
|
||||||
|
.orWhere(`product.title ILIKE :q`, { q: `%${q}%` })
|
||||||
|
.orWhere(`variant.title ILIKE :q`, { q: `%${q}%` })
|
||||||
|
.orWhere(`variant.sku ILIKE :q`, { q: `%${q}%` })
|
||||||
|
.orWhere(`collection.title ILIKE :q`, { q: `%${q}%` })
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProductService
|
export default ProductService
|
||||||
Reference in new issue
Block a user