fix: product ordering

This commit is contained in:
Sebastian Rindom
2021-10-18 14:39:42 +02:00
parent 700f8c3919
commit 57a6612e84
5 changed files with 67 additions and 62 deletions

View File

@@ -19,7 +19,7 @@ class BaseService {
* Used to build TypeORM queries.
*/
buildQuery_(selector, config = {}) {
const build = obj => {
const build = (obj) => {
const where = Object.entries(obj).reduce((acc, [key, value]) => {
switch (true) {
case value instanceof FindOperator:
@@ -49,11 +49,11 @@ class BaseService {
})
acc[key] = Raw(
a =>
(a) =>
subquery
.map((s, index) => `${a} ${s.operator} :${index}`)
.join(" AND "),
subquery.map(s => s.value)
subquery.map((s) => s.value)
)
break
default:
@@ -149,7 +149,7 @@ class BaseService {
return work(this.transactionManager_)
} else {
const temp = this.manager_
const doWork = async m => {
const doWork = async (m) => {
this.manager_ = m
this.transactionManager_ = m
try {
@@ -167,17 +167,17 @@ class BaseService {
if (isolation) {
let result
try {
result = await this.manager_.transaction(isolation, m => doWork(m))
result = await this.manager_.transaction(isolation, (m) => doWork(m))
return result
} catch (error) {
if (this.shouldRetryTransaction(error)) {
return this.manager_.transaction(isolation, m => doWork(m))
return this.manager_.transaction(isolation, (m) => doWork(m))
} else {
throw error
}
}
}
return this.manager_.transaction(m => doWork(m))
return this.manager_.transaction((m) => doWork(m))
}
}
@@ -230,7 +230,7 @@ class BaseService {
*/
runDecorators_(obj, fields = [], expandFields = []) {
return this.decorators_.reduce(async (acc, next) => {
return acc.then(res => next(res, fields, expandFields)).catch(() => acc)
return acc.then((res) => next(res, fields, expandFields)).catch(() => acc)
}, Promise.resolve(obj))
}
}

View File

@@ -6,12 +6,13 @@ async function loadProductsIntoSearchEngine(container) {
const productService = container.resolve("productService")
const TAKE = 20
let skip = 0
let hasMore = true
let lastSeenId = ""
while (hasMore) {
const products = await productService.list(
{},
{ id: { gt: lastSeenId } },
{
select: [
"id",
@@ -39,8 +40,7 @@ async function loadProductsIntoSearchEngine(container) {
"options",
],
take: TAKE,
skip,
order: { created_at: "ASC" },
order: { id: "ASC" },
}
)
@@ -50,7 +50,7 @@ async function loadProductsIntoSearchEngine(container) {
products,
indexTypes.products
)
skip += products.length
lastSeenId = products[products.length - 1].id
} else {
hasMore = false
}

View File

@@ -34,7 +34,7 @@ export class OrderRepository extends Repository<Order> {
const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id")
return map(entities, e => merge({}, ...entitiesAndRelationsById[e.id]))
return map(entities, (e) => merge({}, ...entitiesAndRelationsById[e.id]))
}
public async findOneWithRelations(

View File

@@ -1,22 +1,35 @@
import { flatten, groupBy, map, merge } from "lodash"
import { EntityRepository, FindManyOptions, Repository } from "typeorm"
import {
OrderByCondition,
EntityRepository,
FindManyOptions,
Repository,
} from "typeorm"
import { Product } from "../models/product"
type DefaultWithoutRelations = Omit<FindManyOptions<Product>, "relations">
type CustomOptions = {
where?: DefaultWithoutRelations["where"] & { tags?: string[] }
order?: OrderByCondition
skip?: number
take?: number
}
type FindWithRelationsOptions = CustomOptions
@EntityRepository(Product)
export class ProductRepository extends Repository<Product> {
public async findWithRelations(
relations: Array<keyof Product> = [],
idsOrOptionsWithoutRelations: Omit<
FindManyOptions<Product>,
"relations"
> = {}
idsOrOptionsWithoutRelations: FindWithRelationsOptions = {}
): Promise<Product[]> {
let entities
let entities: Product[]
if (Array.isArray(idsOrOptionsWithoutRelations)) {
entities = await this.findByIds(idsOrOptionsWithoutRelations)
} 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
// 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
@@ -25,17 +38,15 @@ export class ProductRepository extends Repository<Product> {
.where(idsOrOptionsWithoutRelations.where)
.skip(idsOrOptionsWithoutRelations.skip)
.take(idsOrOptionsWithoutRelations.take)
if (tags) {
.orderBy(idsOrOptionsWithoutRelations.order)
if (tags) {
qb = qb
.leftJoinAndSelect("product.tags", "tags")
.andWhere(
`tags.id IN (:...ids)`, { ids: tags._value}
)
.andWhere(`tags.id IN (:...ids)`, { ids: tags._value })
}
entities = await qb
.getMany()
entities = await qb.getMany()
}
const entitiesIds = entities.map(({ id }) => id)

View File

@@ -128,7 +128,7 @@ class ProductService extends BaseService {
.select(["product.id"])
.where(where)
.andWhere(
new Brackets(qb => {
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}%` })
@@ -140,7 +140,7 @@ class ProductService extends BaseService {
return productRepo.findWithRelations(
rels,
raw.map(i => i.id)
raw.map((i) => i.id)
)
}
@@ -282,7 +282,7 @@ class ProductService extends BaseService {
* @return {Promise} resolves to the creation result.
*/
async create(productObject) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const productRepo = manager.getCustomRepository(this.productRepository_)
const optionRepo = manager.getCustomRepository(
this.productOptionRepository_
@@ -316,7 +316,7 @@ class ProductService extends BaseService {
product = await productRepo.save(product)
product.options = await Promise.all(
options.map(async o => {
options.map(async (o) => {
const res = optionRepo.create({ ...o, product_id: product.id })
await optionRepo.save(res)
return res
@@ -366,7 +366,7 @@ class ProductService extends BaseService {
* @return {Promise} resolves to the update result.
*/
async update(productId, update) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const productRepo = manager.getCustomRepository(this.productRepository_)
const productVariantRepo = manager.getCustomRepository(
this.productVariantRepository_
@@ -376,15 +376,8 @@ class ProductService extends BaseService {
relations: ["variants", "tags", "images"],
})
const {
variants,
metadata,
options,
images,
tags,
type,
...rest
} = update
const { variants, metadata, options, images, tags, type, ...rest } =
update
if (!product.thumbnail && !update.thumbnail && images?.length) {
product.thumbnail = images[0]
@@ -409,7 +402,7 @@ class ProductService extends BaseService {
if (variants) {
// Iterate product variants and update their properties accordingly
for (const variant of product.variants) {
const exists = variants.find(v => v.id && variant.id === v.id)
const exists = variants.find((v) => v.id && variant.id === v.id)
if (!exists) {
await productVariantRepo.remove(variant)
}
@@ -420,7 +413,7 @@ class ProductService extends BaseService {
newVariant.variant_rank = i
if (newVariant.id) {
const variant = product.variants.find(v => v.id === newVariant.id)
const variant = product.variants.find((v) => v.id === newVariant.id)
if (!variant) {
throw new MedusaError(
@@ -471,7 +464,7 @@ class ProductService extends BaseService {
* @return {Promise} empty promise
*/
async delete(productId) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const productRepo = manager.getCustomRepository(this.productRepository_)
// Should not fail, if product does not exist, since delete is idempotent
@@ -500,7 +493,7 @@ class ProductService extends BaseService {
* @return {Promise} the result of the model update operation
*/
async addOption(productId, optionTitle) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const productOptionRepo = manager.getCustomRepository(
this.productOptionRepository_
)
@@ -509,7 +502,7 @@ class ProductService extends BaseService {
relations: ["options", "variants"],
})
if (product.options.find(o => o.title === optionTitle)) {
if (product.options.find((o) => o.title === optionTitle)) {
throw new MedusaError(
MedusaError.Types.DUPLICATE_ERROR,
`An option with the title: ${optionTitle} already exists`
@@ -539,7 +532,7 @@ class ProductService extends BaseService {
}
async reorderVariants(productId, variantOrder) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const productRepo = manager.getCustomRepository(this.productRepository_)
const product = await this.retrieve(productId, {
@@ -553,8 +546,8 @@ class ProductService extends BaseService {
)
}
product.variants = variantOrder.map(vId => {
const variant = product.variants.find(v => v.id === vId)
product.variants = variantOrder.map((vId) => {
const variant = product.variants.find((v) => v.id === vId)
if (!variant) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
@@ -583,7 +576,7 @@ class ProductService extends BaseService {
* @return {Promise} the result of the update operation
*/
async reorderOptions(productId, optionOrder) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const productRepo = manager.getCustomRepository(this.productRepository_)
const product = await this.retrieve(productId, { relations: ["options"] })
@@ -595,8 +588,8 @@ class ProductService extends BaseService {
)
}
product.options = optionOrder.map(oId => {
const option = product.options.find(o => o.id === oId)
product.options = optionOrder.map((oId) => {
const option = product.options.find((o) => o.id === oId)
if (!option) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
@@ -624,7 +617,7 @@ class ProductService extends BaseService {
* @return {Promise} the updated product
*/
async updateOption(productId, optionId, data) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const productOptionRepo = manager.getCustomRepository(
this.productOptionRepository_
)
@@ -634,7 +627,8 @@ class ProductService extends BaseService {
const { title, values } = data
const optionExists = product.options.some(
o => o.title.toUpperCase() === title.toUpperCase() && o.id !== optionId
(o) =>
o.title.toUpperCase() === title.toUpperCase() && o.id !== optionId
)
if (optionExists) {
throw new MedusaError(
@@ -673,7 +667,7 @@ class ProductService extends BaseService {
* @return {Promise} the updated product
*/
async deleteOption(productId, optionId) {
return this.atomicPhase_(async manager => {
return this.atomicPhase_(async (manager) => {
const productOptionRepo = manager.getCustomRepository(
this.productOptionRepository_
)
@@ -701,17 +695,17 @@ class ProductService extends BaseService {
const firstVariant = product.variants[0]
const valueToMatch = firstVariant.options.find(
o => o.option_id === optionId
(o) => o.option_id === optionId
).value
const equalsFirst = await Promise.all(
product.variants.map(async v => {
const option = v.options.find(o => o.option_id === optionId)
product.variants.map(async (v) => {
const option = v.options.find((o) => o.option_id === optionId)
return option.value === valueToMatch
})
)
if (!equalsFirst.every(v => v)) {
if (!equalsFirst.every((v) => v)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`To delete an option, first delete all variants, such that when option is deleted, no duplicate variants will exist.`