fix: product ordering
This commit is contained in:
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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.`
|
||||||
|
|||||||
Reference in New Issue
Block a user