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
@@ -19,7 +19,7 @@ class BaseService {
* Used to build TypeORM queries. * Used to build TypeORM queries.
*/ */
buildQuery_(selector, config = {}) { buildQuery_(selector, config = {}) {
const build = obj => { const build = (obj) => {
const where = Object.entries(obj).reduce((acc, [key, value]) => { const where = Object.entries(obj).reduce((acc, [key, value]) => {
switch (true) { switch (true) {
case value instanceof FindOperator: case value instanceof FindOperator:
@@ -49,11 +49,11 @@ class BaseService {
}) })
acc[key] = Raw( acc[key] = Raw(
a => (a) =>
subquery subquery
.map((s, index) => `${a} ${s.operator} :${index}`) .map((s, index) => `${a} ${s.operator} :${index}`)
.join(" AND "), .join(" AND "),
subquery.map(s => s.value) subquery.map((s) => s.value)
) )
break break
default: default:
@@ -149,7 +149,7 @@ class BaseService {
return work(this.transactionManager_) return work(this.transactionManager_)
} else { } else {
const temp = this.manager_ const temp = this.manager_
const doWork = async m => { const doWork = async (m) => {
this.manager_ = m this.manager_ = m
this.transactionManager_ = m this.transactionManager_ = m
try { try {
@@ -167,17 +167,17 @@ class BaseService {
if (isolation) { if (isolation) {
let result let result
try { try {
result = await this.manager_.transaction(isolation, m => doWork(m)) result = await this.manager_.transaction(isolation, (m) => doWork(m))
return result return result
} catch (error) { } catch (error) {
if (this.shouldRetryTransaction(error)) { if (this.shouldRetryTransaction(error)) {
return this.manager_.transaction(isolation, m => doWork(m)) return this.manager_.transaction(isolation, (m) => doWork(m))
} else { } else {
throw error 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 = []) { runDecorators_(obj, fields = [], expandFields = []) {
return this.decorators_.reduce(async (acc, next) => { 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)) }, Promise.resolve(obj))
} }
} }
+5 -5
View File
@@ -6,12 +6,13 @@ async function loadProductsIntoSearchEngine(container) {
const productService = container.resolve("productService") const productService = container.resolve("productService")
const TAKE = 20 const TAKE = 20
let skip = 0
let hasMore = true let hasMore = true
let lastSeenId = ""
while (hasMore) { while (hasMore) {
const products = await productService.list( const products = await productService.list(
{}, { id: { gt: lastSeenId } },
{ {
select: [ select: [
"id", "id",
@@ -39,8 +40,7 @@ async function loadProductsIntoSearchEngine(container) {
"options", "options",
], ],
take: TAKE, take: TAKE,
skip, order: { id: "ASC" },
order: { created_at: "ASC" },
} }
) )
@@ -50,7 +50,7 @@ async function loadProductsIntoSearchEngine(container) {
products, products,
indexTypes.products indexTypes.products
) )
skip += products.length lastSeenId = products[products.length - 1].id
} else { } else {
hasMore = false hasMore = false
} }
+1 -1
View File
@@ -34,7 +34,7 @@ export class OrderRepository extends Repository<Order> {
const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id") const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id")
return map(entities, e => merge({}, ...entitiesAndRelationsById[e.id])) return map(entities, (e) => merge({}, ...entitiesAndRelationsById[e.id]))
} }
public async findOneWithRelations( public async findOneWithRelations(
+27 -16
View File
@@ -1,22 +1,35 @@
import { flatten, groupBy, map, merge } from "lodash" 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" 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) @EntityRepository(Product)
export class ProductRepository extends Repository<Product> { export class ProductRepository extends Repository<Product> {
public async findWithRelations( public async findWithRelations(
relations: Array<keyof Product> = [], relations: Array<keyof Product> = [],
idsOrOptionsWithoutRelations: Omit< idsOrOptionsWithoutRelations: FindWithRelationsOptions = {}
FindManyOptions<Product>,
"relations"
> = {}
): Promise<Product[]> { ): Promise<Product[]> {
let entities let entities: Product[]
if (Array.isArray(idsOrOptionsWithoutRelations)) { if (Array.isArray(idsOrOptionsWithoutRelations)) {
entities = await this.findByIds(idsOrOptionsWithoutRelations) entities = await this.findByIds(idsOrOptionsWithoutRelations)
} else { } else {
// Since tags are in a one-to-many realtion they cant be included in a // 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 // regular query, to solve this add the join on tags seperately if
// the query exists // the query exists
const tags = idsOrOptionsWithoutRelations.where.tags const tags = idsOrOptionsWithoutRelations.where.tags
delete idsOrOptionsWithoutRelations.where.tags delete idsOrOptionsWithoutRelations.where.tags
@@ -25,17 +38,15 @@ export class ProductRepository extends Repository<Product> {
.where(idsOrOptionsWithoutRelations.where) .where(idsOrOptionsWithoutRelations.where)
.skip(idsOrOptionsWithoutRelations.skip) .skip(idsOrOptionsWithoutRelations.skip)
.take(idsOrOptionsWithoutRelations.take) .take(idsOrOptionsWithoutRelations.take)
.orderBy(idsOrOptionsWithoutRelations.order)
if (tags) {
if (tags) {
qb = qb qb = qb
.leftJoinAndSelect("product.tags", "tags") .leftJoinAndSelect("product.tags", "tags")
.andWhere( .andWhere(`tags.id IN (:...ids)`, { ids: tags._value })
`tags.id IN (:...ids)`, { ids: tags._value}
)
} }
entities = await qb entities = await qb.getMany()
.getMany()
} }
const entitiesIds = entities.map(({ id }) => id) const entitiesIds = entities.map(({ id }) => id)
+26 -32
View File
@@ -128,7 +128,7 @@ class ProductService extends BaseService {
.select(["product.id"]) .select(["product.id"])
.where(where) .where(where)
.andWhere( .andWhere(
new Brackets(qb => { new Brackets((qb) => {
qb.where(`product.description ILIKE :q`, { q: `%${q}%` }) qb.where(`product.description ILIKE :q`, { q: `%${q}%` })
.orWhere(`product.title ILIKE :q`, { q: `%${q}%` }) .orWhere(`product.title ILIKE :q`, { q: `%${q}%` })
.orWhere(`variant.title ILIKE :q`, { q: `%${q}%` }) .orWhere(`variant.title ILIKE :q`, { q: `%${q}%` })
@@ -140,7 +140,7 @@ class ProductService extends BaseService {
return productRepo.findWithRelations( return productRepo.findWithRelations(
rels, 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. * @return {Promise} resolves to the creation result.
*/ */
async create(productObject) { async create(productObject) {
return this.atomicPhase_(async manager => { return this.atomicPhase_(async (manager) => {
const productRepo = manager.getCustomRepository(this.productRepository_) const productRepo = manager.getCustomRepository(this.productRepository_)
const optionRepo = manager.getCustomRepository( const optionRepo = manager.getCustomRepository(
this.productOptionRepository_ this.productOptionRepository_
@@ -316,7 +316,7 @@ class ProductService extends BaseService {
product = await productRepo.save(product) product = await productRepo.save(product)
product.options = await Promise.all( product.options = await Promise.all(
options.map(async o => { options.map(async (o) => {
const res = optionRepo.create({ ...o, product_id: product.id }) const res = optionRepo.create({ ...o, product_id: product.id })
await optionRepo.save(res) await optionRepo.save(res)
return res return res
@@ -366,7 +366,7 @@ class ProductService extends BaseService {
* @return {Promise} resolves to the update result. * @return {Promise} resolves to the update result.
*/ */
async update(productId, update) { async update(productId, update) {
return this.atomicPhase_(async manager => { return this.atomicPhase_(async (manager) => {
const productRepo = manager.getCustomRepository(this.productRepository_) const productRepo = manager.getCustomRepository(this.productRepository_)
const productVariantRepo = manager.getCustomRepository( const productVariantRepo = manager.getCustomRepository(
this.productVariantRepository_ this.productVariantRepository_
@@ -376,15 +376,8 @@ class ProductService extends BaseService {
relations: ["variants", "tags", "images"], relations: ["variants", "tags", "images"],
}) })
const { const { variants, metadata, options, images, tags, type, ...rest } =
variants, update
metadata,
options,
images,
tags,
type,
...rest
} = update
if (!product.thumbnail && !update.thumbnail && images?.length) { if (!product.thumbnail && !update.thumbnail && images?.length) {
product.thumbnail = images[0] product.thumbnail = images[0]
@@ -409,7 +402,7 @@ class ProductService extends BaseService {
if (variants) { if (variants) {
// Iterate product variants and update their properties accordingly // Iterate product variants and update their properties accordingly
for (const variant of product.variants) { 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) { if (!exists) {
await productVariantRepo.remove(variant) await productVariantRepo.remove(variant)
} }
@@ -420,7 +413,7 @@ class ProductService extends BaseService {
newVariant.variant_rank = i newVariant.variant_rank = i
if (newVariant.id) { 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) { if (!variant) {
throw new MedusaError( throw new MedusaError(
@@ -471,7 +464,7 @@ class ProductService extends BaseService {
* @return {Promise} empty promise * @return {Promise} empty promise
*/ */
async delete(productId) { async delete(productId) {
return this.atomicPhase_(async manager => { return this.atomicPhase_(async (manager) => {
const productRepo = manager.getCustomRepository(this.productRepository_) const productRepo = manager.getCustomRepository(this.productRepository_)
// Should not fail, if product does not exist, since delete is idempotent // 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 * @return {Promise} the result of the model update operation
*/ */
async addOption(productId, optionTitle) { async addOption(productId, optionTitle) {
return this.atomicPhase_(async manager => { return this.atomicPhase_(async (manager) => {
const productOptionRepo = manager.getCustomRepository( const productOptionRepo = manager.getCustomRepository(
this.productOptionRepository_ this.productOptionRepository_
) )
@@ -509,7 +502,7 @@ class ProductService extends BaseService {
relations: ["options", "variants"], relations: ["options", "variants"],
}) })
if (product.options.find(o => o.title === optionTitle)) { if (product.options.find((o) => o.title === optionTitle)) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.DUPLICATE_ERROR, MedusaError.Types.DUPLICATE_ERROR,
`An option with the title: ${optionTitle} already exists` `An option with the title: ${optionTitle} already exists`
@@ -539,7 +532,7 @@ class ProductService extends BaseService {
} }
async reorderVariants(productId, variantOrder) { async reorderVariants(productId, variantOrder) {
return this.atomicPhase_(async manager => { return this.atomicPhase_(async (manager) => {
const productRepo = manager.getCustomRepository(this.productRepository_) const productRepo = manager.getCustomRepository(this.productRepository_)
const product = await this.retrieve(productId, { const product = await this.retrieve(productId, {
@@ -553,8 +546,8 @@ class ProductService extends BaseService {
) )
} }
product.variants = variantOrder.map(vId => { product.variants = variantOrder.map((vId) => {
const variant = product.variants.find(v => v.id === vId) const variant = product.variants.find((v) => v.id === vId)
if (!variant) { if (!variant) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.INVALID_DATA, MedusaError.Types.INVALID_DATA,
@@ -583,7 +576,7 @@ class ProductService extends BaseService {
* @return {Promise} the result of the update operation * @return {Promise} the result of the update operation
*/ */
async reorderOptions(productId, optionOrder) { async reorderOptions(productId, optionOrder) {
return this.atomicPhase_(async manager => { return this.atomicPhase_(async (manager) => {
const productRepo = manager.getCustomRepository(this.productRepository_) const productRepo = manager.getCustomRepository(this.productRepository_)
const product = await this.retrieve(productId, { relations: ["options"] }) const product = await this.retrieve(productId, { relations: ["options"] })
@@ -595,8 +588,8 @@ class ProductService extends BaseService {
) )
} }
product.options = optionOrder.map(oId => { product.options = optionOrder.map((oId) => {
const option = product.options.find(o => o.id === oId) const option = product.options.find((o) => o.id === oId)
if (!option) { if (!option) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.INVALID_DATA, MedusaError.Types.INVALID_DATA,
@@ -624,7 +617,7 @@ class ProductService extends BaseService {
* @return {Promise} the updated product * @return {Promise} the updated product
*/ */
async updateOption(productId, optionId, data) { async updateOption(productId, optionId, data) {
return this.atomicPhase_(async manager => { return this.atomicPhase_(async (manager) => {
const productOptionRepo = manager.getCustomRepository( const productOptionRepo = manager.getCustomRepository(
this.productOptionRepository_ this.productOptionRepository_
) )
@@ -634,7 +627,8 @@ class ProductService extends BaseService {
const { title, values } = data const { title, values } = data
const optionExists = product.options.some( 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) { if (optionExists) {
throw new MedusaError( throw new MedusaError(
@@ -673,7 +667,7 @@ class ProductService extends BaseService {
* @return {Promise} the updated product * @return {Promise} the updated product
*/ */
async deleteOption(productId, optionId) { async deleteOption(productId, optionId) {
return this.atomicPhase_(async manager => { return this.atomicPhase_(async (manager) => {
const productOptionRepo = manager.getCustomRepository( const productOptionRepo = manager.getCustomRepository(
this.productOptionRepository_ this.productOptionRepository_
) )
@@ -701,17 +695,17 @@ class ProductService extends BaseService {
const firstVariant = product.variants[0] const firstVariant = product.variants[0]
const valueToMatch = firstVariant.options.find( const valueToMatch = firstVariant.options.find(
o => o.option_id === optionId (o) => o.option_id === optionId
).value ).value
const equalsFirst = await Promise.all( const equalsFirst = await Promise.all(
product.variants.map(async v => { product.variants.map(async (v) => {
const option = v.options.find(o => o.option_id === optionId) const option = v.options.find((o) => o.option_id === optionId)
return option.value === valueToMatch return option.value === valueToMatch
}) })
) )
if (!equalsFirst.every(v => v)) { if (!equalsFirst.every((v) => v)) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.INVALID_DATA, MedusaError.Types.INVALID_DATA,
`To delete an option, first delete all variants, such that when option is deleted, no duplicate variants will exist.` `To delete an option, first delete all variants, such that when option is deleted, no duplicate variants will exist.`