feat(medusa): Improve performance of Products domain (#3417)
* feat(medusa): Improve product update performances * fix tests and update * update mock repo * improve repo * cleanup * fix * cleanup + bulk emit + unit test fix * improvements * improve * fix unit tests * fix export * fix product update handler * enhance mock repo * fix import integration * fix end point tests * revert mock repo product variant * fix unit * cleanup * cleanup * address feedback * fix quotes in tests * address feedback * Create new-tips-mate.md * use types
This commit is contained in:
1 parent
902ed3c0b2
commit
fa4049cb51
22 files changed
+1041
-425
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"medusa-test-utils": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
"integration-tests-api": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
chores(medusa): Refactoring product update flow to improve handling and performances
|
||||||
@@ -81,7 +81,7 @@ describe("Product import batch job", () => {
|
|||||||
await batchJobSeeder(dbConnection)
|
await batchJobSeeder(dbConnection)
|
||||||
await adminSeeder(dbConnection)
|
await adminSeeder(dbConnection)
|
||||||
await userSeeder(dbConnection)
|
await userSeeder(dbConnection)
|
||||||
await simpleProductCollectionFactory(dbConnection, [
|
await simpleProductCollectionFactory(dbConnection, [
|
||||||
{
|
{
|
||||||
handle: collectionHandle1,
|
handle: collectionHandle1,
|
||||||
},
|
},
|
||||||
@@ -184,7 +184,7 @@ describe("Product import batch job", () => {
|
|||||||
ean: null,
|
ean: null,
|
||||||
upc: null,
|
upc: null,
|
||||||
inventory_quantity: 10,
|
inventory_quantity: 10,
|
||||||
prices: [
|
prices: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
currency_code: "eur",
|
currency_code: "eur",
|
||||||
amount: 100,
|
amount: 100,
|
||||||
@@ -199,7 +199,7 @@ describe("Product import batch job", () => {
|
|||||||
amount: 130,
|
amount: 130,
|
||||||
region_id: "region-product-import-1",
|
region_id: "region-product-import-1",
|
||||||
}),
|
}),
|
||||||
],
|
]),
|
||||||
options: expect.arrayContaining([
|
options: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
value: "option 1 value red",
|
value: "option 1 value red",
|
||||||
@@ -211,24 +211,24 @@ describe("Product import batch job", () => {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
type: null,
|
type: null,
|
||||||
images: [
|
images: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
url: "test-image.png",
|
url: "test-image.png",
|
||||||
}),
|
}),
|
||||||
],
|
]),
|
||||||
options: [
|
options: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: "test-option-1",
|
title: "test-option-1",
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: "test-option-2",
|
title: "test-option-2",
|
||||||
}),
|
}),
|
||||||
],
|
]),
|
||||||
tags: [
|
tags: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
value: "123_1",
|
value: "123_1",
|
||||||
}),
|
}),
|
||||||
],
|
]),
|
||||||
collection: expect.objectContaining({
|
collection: expect.objectContaining({
|
||||||
handle: collectionHandle1,
|
handle: collectionHandle1,
|
||||||
}),
|
}),
|
||||||
@@ -250,7 +250,7 @@ describe("Product import batch job", () => {
|
|||||||
ean: null,
|
ean: null,
|
||||||
upc: null,
|
upc: null,
|
||||||
inventory_quantity: 10,
|
inventory_quantity: 10,
|
||||||
prices: [
|
prices: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
currency_code: "eur",
|
currency_code: "eur",
|
||||||
amount: 100,
|
amount: 100,
|
||||||
@@ -265,7 +265,7 @@ describe("Product import batch job", () => {
|
|||||||
amount: 130,
|
amount: 130,
|
||||||
region_id: "region-product-import-1",
|
region_id: "region-product-import-1",
|
||||||
}),
|
}),
|
||||||
],
|
]),
|
||||||
options: expect.arrayContaining([
|
options: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
value: "option 1 value red",
|
value: "option 1 value red",
|
||||||
@@ -277,19 +277,19 @@ describe("Product import batch job", () => {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
type: null,
|
type: null,
|
||||||
images: [
|
images: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
url: "test-image.png",
|
url: "test-image.png",
|
||||||
}),
|
}),
|
||||||
],
|
]),
|
||||||
options: [
|
options: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: "test-option-1",
|
title: "test-option-1",
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
title: "test-option-2",
|
title: "test-option-2",
|
||||||
}),
|
}),
|
||||||
],
|
]),
|
||||||
tags: [],
|
tags: [],
|
||||||
collection: expect.objectContaining({
|
collection: expect.objectContaining({
|
||||||
handle: collectionHandle1,
|
handle: collectionHandle1,
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getCustomRepository: function (repo) {
|
getCustomRepository: function (repo) {
|
||||||
|
if (repo) {
|
||||||
|
repo["metadata"] = repo["metadata"] ?? {
|
||||||
|
columns: []
|
||||||
|
}
|
||||||
|
}
|
||||||
return repo;
|
return repo;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -13,25 +13,30 @@ class MockRepo {
|
|||||||
findAndCount,
|
findAndCount,
|
||||||
del,
|
del,
|
||||||
count,
|
count,
|
||||||
insertBulk
|
insertBulk,
|
||||||
|
metadata
|
||||||
}) {
|
}) {
|
||||||
this.create_ = create;
|
this.create_ = create
|
||||||
this.update_ = update;
|
this.update_ = update
|
||||||
this.remove_ = remove;
|
this.remove_ = remove
|
||||||
this.delete_ = del;
|
this.delete_ = del
|
||||||
this.softRemove_ = softRemove;
|
this.softRemove_ = softRemove
|
||||||
this.find_ = find;
|
this.find_ = find
|
||||||
this.findDescendantsTree_ = findDescendantsTree;
|
this.findDescendantsTree_ = findDescendantsTree
|
||||||
this.findOne_ = findOne;
|
this.findOne_ = findOne
|
||||||
this.findOneOrFail_ = findOneOrFail;
|
this.findOneOrFail_ = findOneOrFail
|
||||||
this.save_ = save;
|
this.save_ = save
|
||||||
this.findAndCount_ = findAndCount;
|
this.findAndCount_ = findAndCount
|
||||||
this.findOneWithRelations_ = findOneWithRelations;
|
this.findOneWithRelations_ = findOneWithRelations
|
||||||
this.insertBulk_ = insertBulk;
|
this.insertBulk_ = insertBulk
|
||||||
|
|
||||||
|
this.metadata = metadata ?? {
|
||||||
|
columns: []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setFindOne(fn) {
|
setFindOne(fn) {
|
||||||
this.findOne_ = fn;
|
this.findOne_ = fn
|
||||||
}
|
}
|
||||||
|
|
||||||
insertBulk = jest.fn().mockImplementation((...args) => {
|
insertBulk = jest.fn().mockImplementation((...args) => {
|
||||||
@@ -42,83 +47,83 @@ class MockRepo {
|
|||||||
})
|
})
|
||||||
create = jest.fn().mockImplementation((...args) => {
|
create = jest.fn().mockImplementation((...args) => {
|
||||||
if (this.create_) {
|
if (this.create_) {
|
||||||
return this.create_(...args);
|
return this.create_(...args)
|
||||||
}
|
}
|
||||||
return {};
|
return {}
|
||||||
});
|
})
|
||||||
softRemove = jest.fn().mockImplementation((...args) => {
|
softRemove = jest.fn().mockImplementation((...args) => {
|
||||||
if (this.softRemove_) {
|
if (this.softRemove_) {
|
||||||
return this.softRemove_(...args);
|
return this.softRemove_(...args)
|
||||||
}
|
}
|
||||||
return {};
|
return {}
|
||||||
});
|
})
|
||||||
remove = jest.fn().mockImplementation((...args) => {
|
remove = jest.fn().mockImplementation((...args) => {
|
||||||
if (this.remove_) {
|
if (this.remove_) {
|
||||||
return this.remove_(...args);
|
return this.remove_(...args)
|
||||||
}
|
}
|
||||||
return {};
|
return {}
|
||||||
});
|
})
|
||||||
update = jest.fn().mockImplementation((...args) => {
|
update = jest.fn().mockImplementation((...args) => {
|
||||||
if (this.update_) {
|
if (this.update_) {
|
||||||
return this.update_(...args);
|
return this.update_(...args)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
findOneOrFail = jest.fn().mockImplementation((...args) => {
|
findOneOrFail = jest.fn().mockImplementation((...args) => {
|
||||||
if (this.findOneOrFail_) {
|
if (this.findOneOrFail_) {
|
||||||
return this.findOneOrFail_(...args);
|
return this.findOneOrFail_(...args)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
findOneWithRelations = jest.fn().mockImplementation((...args) => {
|
findOneWithRelations = jest.fn().mockImplementation((...args) => {
|
||||||
if (this.findOneWithRelations_) {
|
if (this.findOneWithRelations_) {
|
||||||
return this.findOneWithRelations_(...args);
|
return this.findOneWithRelations_(...args)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
findOne = jest.fn().mockImplementation((...args) => {
|
findOne = jest.fn().mockImplementation((...args) => {
|
||||||
if (this.findOne_) {
|
if (this.findOne_) {
|
||||||
return this.findOne_(...args);
|
return this.findOne_(...args)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
findDescendantsTree = jest.fn().mockImplementation((...args) => {
|
findDescendantsTree = jest.fn().mockImplementation((...args) => {
|
||||||
if (this.findDescendantsTree_) {
|
if (this.findDescendantsTree_) {
|
||||||
return this.findDescendantsTree_(...args);
|
return this.findDescendantsTree_(...args)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
findOneOrFail = jest.fn().mockImplementation((...args) => {
|
findOneOrFail = jest.fn().mockImplementation((...args) => {
|
||||||
if (this.findOneOrFail_) {
|
if (this.findOneOrFail_) {
|
||||||
return this.findOneOrFail_(...args);
|
return this.findOneOrFail_(...args)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
find = jest.fn().mockImplementation((...args) => {
|
find = jest.fn().mockImplementation((...args) => {
|
||||||
if (this.find_) {
|
if (this.find_) {
|
||||||
return this.find_(...args);
|
return this.find_(...args)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
softRemove = jest.fn().mockImplementation((...args) => {
|
softRemove = jest.fn().mockImplementation((...args) => {
|
||||||
if (this.softRemove_) {
|
if (this.softRemove_) {
|
||||||
return this.softRemove_(...args);
|
return this.softRemove_(...args)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
save = jest.fn().mockImplementation((...args) => {
|
save = jest.fn().mockImplementation((...args) => {
|
||||||
if (this.save_) {
|
if (this.save_) {
|
||||||
return this.save_(...args);
|
return this.save_(...args)
|
||||||
}
|
}
|
||||||
return Promise.resolve(...args);
|
return Promise.resolve(...args)
|
||||||
});
|
})
|
||||||
|
|
||||||
findAndCount = jest.fn().mockImplementation((...args) => {
|
findAndCount = jest.fn().mockImplementation((...args) => {
|
||||||
if (this.findAndCount_) {
|
if (this.findAndCount_) {
|
||||||
return this.findAndCount_(...args);
|
return this.findAndCount_(...args)
|
||||||
}
|
}
|
||||||
return {};
|
return {}
|
||||||
});
|
})
|
||||||
delete = jest.fn().mockImplementation((...args) => {
|
delete = jest.fn().mockImplementation((...args) => {
|
||||||
if (this.delete_) {
|
if (this.delete_) {
|
||||||
return this.delete_(...args);
|
return this.delete_(...args)
|
||||||
}
|
}
|
||||||
return {};
|
return {}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (methods = {}) => {
|
export default (methods = {}) => {
|
||||||
return new MockRepo(methods);
|
return new MockRepo(methods)
|
||||||
};
|
}
|
||||||
@@ -2,7 +2,6 @@ import { IdMap } from "medusa-test-utils"
|
|||||||
import { request } from "../../../../../helpers/test-request"
|
import { request } from "../../../../../helpers/test-request"
|
||||||
import { ProductServiceMock } from "../../../../../services/__mocks__/product"
|
import { ProductServiceMock } from "../../../../../services/__mocks__/product"
|
||||||
import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant"
|
import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant"
|
||||||
import { EventBusServiceMock } from "../../../../../services/__mocks__/event-bus"
|
|
||||||
|
|
||||||
describe("POST /admin/products/:id", () => {
|
describe("POST /admin/products/:id", () => {
|
||||||
describe("successfully updates a product", () => {
|
describe("successfully updates a product", () => {
|
||||||
@@ -18,7 +17,7 @@ describe("POST /admin/products/:id", () => {
|
|||||||
description: "Updated test description",
|
description: "Updated test description",
|
||||||
handle: "handle",
|
handle: "handle",
|
||||||
variants: [
|
variants: [
|
||||||
{ id: IdMap.getId("variant_1"), title: "Green" },
|
{ id: IdMap.getId("testVariant"), title: "Green" },
|
||||||
{ title: "Blue" },
|
{ title: "Blue" },
|
||||||
{ title: "Yellow" },
|
{ title: "Yellow" },
|
||||||
],
|
],
|
||||||
@@ -49,7 +48,6 @@ describe("POST /admin/products/:id", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("successfully updates variants and create new ones", async () => {
|
it("successfully updates variants and create new ones", async () => {
|
||||||
expect(ProductVariantServiceMock.delete).toHaveBeenCalledTimes(2)
|
|
||||||
expect(ProductVariantServiceMock.update).toHaveBeenCalledTimes(1)
|
expect(ProductVariantServiceMock.update).toHaveBeenCalledTimes(1)
|
||||||
expect(ProductVariantServiceMock.create).toHaveBeenCalledTimes(2)
|
expect(ProductVariantServiceMock.create).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
@@ -74,7 +72,7 @@ describe("POST /admin/products/:id", () => {
|
|||||||
)
|
)
|
||||||
expect(subject.status).toEqual(404)
|
expect(subject.status).toEqual(404)
|
||||||
expect(subject.error.text).toEqual(
|
expect(subject.error.text).toEqual(
|
||||||
`{"type":"not_found","message":"Variant with id: test_321 is not associated with this product"}`
|
`{"type":"not_found","message":"Variants with id: test_321 are not associated with this product"}`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
ValidateIf,
|
ValidateIf,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from "class-validator"
|
} from "class-validator"
|
||||||
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
|
|
||||||
import {
|
import {
|
||||||
PricingService,
|
PricingService,
|
||||||
ProductService,
|
ProductService,
|
||||||
@@ -19,10 +18,10 @@ import {
|
|||||||
ProductVariantService,
|
ProductVariantService,
|
||||||
} from "../../../../services"
|
} from "../../../../services"
|
||||||
import {
|
import {
|
||||||
|
ProductProductCategoryReq,
|
||||||
ProductSalesChannelReq,
|
ProductSalesChannelReq,
|
||||||
ProductTagReq,
|
ProductTagReq,
|
||||||
ProductTypeReq,
|
ProductTypeReq,
|
||||||
ProductProductCategoryReq,
|
|
||||||
} from "../../../../types/product"
|
} from "../../../../types/product"
|
||||||
|
|
||||||
import { Type } from "class-transformer"
|
import { Type } from "class-transformer"
|
||||||
@@ -32,17 +31,23 @@ import { ProductStatus, ProductVariant } from "../../../../models"
|
|||||||
import {
|
import {
|
||||||
CreateProductVariantInput,
|
CreateProductVariantInput,
|
||||||
ProductVariantPricesUpdateReq,
|
ProductVariantPricesUpdateReq,
|
||||||
|
UpdateProductVariantInput,
|
||||||
} from "../../../../types/product-variant"
|
} from "../../../../types/product-variant"
|
||||||
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
|
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
|
||||||
import { validator } from "../../../../utils/validator"
|
import { validator } from "../../../../utils/validator"
|
||||||
import { MedusaError } from "medusa-core-utils"
|
import { MedusaError } from "medusa-core-utils"
|
||||||
import { DistributedTransaction } from "../../../../utils/transaction"
|
import { DistributedTransaction } from "../../../../utils/transaction"
|
||||||
|
import { IInventoryService } from "../../../../interfaces"
|
||||||
|
import { Logger } from "../../../../types/global"
|
||||||
|
import {
|
||||||
|
defaultAdminProductFields,
|
||||||
|
defaultAdminProductRelations,
|
||||||
|
} from "./index"
|
||||||
|
import { ProductVariantRepository } from "../../../../repositories/product-variant"
|
||||||
import {
|
import {
|
||||||
createVariantTransaction,
|
createVariantTransaction,
|
||||||
revertVariantTransaction,
|
revertVariantTransaction,
|
||||||
} from "./transaction/create-product-variant"
|
} from "./transaction/create-product-variant"
|
||||||
import { IInventoryService } from "../../../../interfaces"
|
|
||||||
import { Logger } from "../../../../types/global"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @oas [post] /products/{id}
|
* @oas [post] /products/{id}
|
||||||
@@ -113,6 +118,9 @@ export default async (req, res) => {
|
|||||||
const validated = await validator(AdminPostProductsProductReq, req.body)
|
const validated = await validator(AdminPostProductsProductReq, req.body)
|
||||||
|
|
||||||
const logger: Logger = req.scope.resolve("logger")
|
const logger: Logger = req.scope.resolve("logger")
|
||||||
|
const productVariantRepo: typeof ProductVariantRepository = req.scope.resolve(
|
||||||
|
"productVariantRepository"
|
||||||
|
)
|
||||||
const productService: ProductService = req.scope.resolve("productService")
|
const productService: ProductService = req.scope.resolve("productService")
|
||||||
const pricingService: PricingService = req.scope.resolve("pricingService")
|
const pricingService: PricingService = req.scope.resolve("pricingService")
|
||||||
const productVariantService: ProductVariantService = req.scope.resolve(
|
const productVariantService: ProductVariantService = req.scope.resolve(
|
||||||
@@ -125,31 +133,74 @@ export default async (req, res) => {
|
|||||||
|
|
||||||
const manager: EntityManager = req.scope.resolve("manager")
|
const manager: EntityManager = req.scope.resolve("manager")
|
||||||
await manager.transaction(async (transactionManager) => {
|
await manager.transaction(async (transactionManager) => {
|
||||||
|
const productServiceTx = productService.withTransaction(transactionManager)
|
||||||
|
|
||||||
const { variants } = validated
|
const { variants } = validated
|
||||||
delete validated.variants
|
delete validated.variants
|
||||||
|
|
||||||
await productService
|
const product = await productServiceTx.update(id, validated)
|
||||||
.withTransaction(transactionManager)
|
|
||||||
.update(id, validated)
|
|
||||||
|
|
||||||
if (!variants) {
|
if (!variants) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const product = await productService
|
const variantRepo = manager.getCustomRepository(productVariantRepo)
|
||||||
|
const productVariants = await productVariantService
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.retrieve(id, {
|
.list(
|
||||||
relations: ["variants"],
|
{ product_id: id },
|
||||||
})
|
{
|
||||||
|
select: variantRepo.metadata.columns.map(
|
||||||
|
(c) => c.propertyName
|
||||||
|
) as (keyof ProductVariant)[],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Iterate product variants and update their properties accordingly
|
const productVariantMap = new Map(productVariants.map((v) => [v.id, v]))
|
||||||
for (const variant of product.variants) {
|
const variantWithIdSet = new Set()
|
||||||
const exists = variants.find((v) => v.id && variant.id === v.id)
|
|
||||||
if (!exists) {
|
const variantIdsNotBelongingToProduct: string[] = []
|
||||||
await productVariantService
|
const variantsToUpdate: {
|
||||||
.withTransaction(transactionManager)
|
variant: ProductVariant
|
||||||
.delete(variant.id)
|
updateData: UpdateProductVariantInput
|
||||||
|
}[] = []
|
||||||
|
const variantsToCreate: ProductVariantReq[] = []
|
||||||
|
|
||||||
|
// Preparing the data step
|
||||||
|
for (const [variantRank, variant] of variants.entries()) {
|
||||||
|
if (!variant.id) {
|
||||||
|
Object.assign(variant, {
|
||||||
|
variant_rank: variantRank,
|
||||||
|
options: variant.options || [],
|
||||||
|
prices: variant.prices || [],
|
||||||
|
})
|
||||||
|
variantsToCreate.push(variant)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Will be used to find the variants that should be removed during the next steps
|
||||||
|
variantWithIdSet.add(variant.id)
|
||||||
|
|
||||||
|
if (!productVariantMap.has(variant.id)) {
|
||||||
|
variantIdsNotBelongingToProduct.push(variant.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const productVariant = productVariantMap.get(variant.id)!
|
||||||
|
Object.assign(variant, {
|
||||||
|
variant_rank: variantRank,
|
||||||
|
product_id: productVariant.product_id,
|
||||||
|
})
|
||||||
|
variantsToUpdate.push({ variant: productVariant, updateData: variant })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variantIdsNotBelongingToProduct.length) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`Variants with id: ${variantIdsNotBelongingToProduct.join(
|
||||||
|
", "
|
||||||
|
)} are not associated with this product`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const allVariantTransactions: DistributedTransaction[] = []
|
const allVariantTransactions: DistributedTransaction[] = []
|
||||||
@@ -160,57 +211,46 @@ export default async (req, res) => {
|
|||||||
productVariantService,
|
productVariantService,
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [index, newVariant] of variants.entries()) {
|
const productVariantServiceTx =
|
||||||
const variantRank = index
|
productVariantService.withTransaction(transactionManager)
|
||||||
|
|
||||||
if (newVariant.id) {
|
// Delete the variant that does not exist anymore from the provided variants
|
||||||
const variant = product.variants.find((v) => v.id === newVariant.id)
|
const variantIdsToDelete = [...productVariantMap.keys()].filter(
|
||||||
|
(variantId) => !variantWithIdSet.has(variantId)
|
||||||
|
)
|
||||||
|
|
||||||
if (!variant) {
|
if (variantIdsToDelete) {
|
||||||
throw new MedusaError(
|
await productVariantServiceTx.delete(variantIdsToDelete)
|
||||||
MedusaError.Types.NOT_FOUND,
|
}
|
||||||
`Variant with id: ${newVariant.id} is not associated with this product`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
await productVariantService
|
if (variantsToUpdate.length) {
|
||||||
.withTransaction(transactionManager)
|
await productVariantServiceTx.update(variantsToUpdate)
|
||||||
.update(variant, {
|
}
|
||||||
...newVariant,
|
|
||||||
variant_rank: variantRank,
|
|
||||||
product_id: variant.product_id,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// If the provided variant does not have an id, we assume that it
|
|
||||||
// should be created
|
|
||||||
|
|
||||||
try {
|
if (variantsToCreate.length) {
|
||||||
const input = {
|
await Promise.all(
|
||||||
...newVariant,
|
variantsToCreate.map(async (data) => {
|
||||||
variant_rank: variantRank,
|
try {
|
||||||
options: newVariant.options || [],
|
const varTransaction = await createVariantTransaction(
|
||||||
prices: newVariant.prices || [],
|
transactionDependencies,
|
||||||
|
product.id,
|
||||||
|
data as CreateProductVariantInput
|
||||||
|
)
|
||||||
|
allVariantTransactions.push(varTransaction)
|
||||||
|
} catch (e) {
|
||||||
|
await Promise.all(
|
||||||
|
allVariantTransactions.map(async (transaction) => {
|
||||||
|
await revertVariantTransaction(
|
||||||
|
transactionDependencies,
|
||||||
|
transaction
|
||||||
|
).catch(() => logger.warn("Transaction couldn't be reverted."))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
|
})
|
||||||
const varTransation = await createVariantTransaction(
|
)
|
||||||
transactionDependencies,
|
|
||||||
product.id,
|
|
||||||
input as CreateProductVariantInput
|
|
||||||
)
|
|
||||||
allVariantTransactions.push(varTransation)
|
|
||||||
} catch (e) {
|
|
||||||
await Promise.all(
|
|
||||||
allVariantTransactions.map(async (transaction) => {
|
|
||||||
await revertVariantTransaction(
|
|
||||||
transactionDependencies,
|
|
||||||
transaction
|
|
||||||
).catch(() => logger.warn("Transaction couldn't be reverted."))
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ import "reflect-metadata"
|
|||||||
import supertest from "supertest"
|
import supertest from "supertest"
|
||||||
import apiLoader from "../loaders/api"
|
import apiLoader from "../loaders/api"
|
||||||
import featureFlagLoader, { featureFlagRouter } from "../loaders/feature-flags"
|
import featureFlagLoader, { featureFlagRouter } from "../loaders/feature-flags"
|
||||||
import { moduleHelper } from "../loaders/module"
|
import moduleLoader, { moduleHelper } from "../loaders/module"
|
||||||
import passportLoader from "../loaders/passport"
|
import passportLoader from "../loaders/passport"
|
||||||
import servicesLoader from "../loaders/services"
|
import servicesLoader from "../loaders/services"
|
||||||
import strategiesLoader from "../loaders/strategies"
|
import strategiesLoader from "../loaders/strategies"
|
||||||
import registerModuleDefinitions from "../loaders/module-definitions"
|
import registerModuleDefinitions from "../loaders/module-definitions"
|
||||||
import moduleLoader from "../loaders/module"
|
import repositories from "../loaders/repositories"
|
||||||
|
import models from "../loaders/models"
|
||||||
|
|
||||||
const adminSessionOpts = {
|
const adminSessionOpts = {
|
||||||
cookieName: "session",
|
cookieName: "session",
|
||||||
@@ -39,8 +40,32 @@ const config = {
|
|||||||
|
|
||||||
const testApp = express()
|
const testApp = express()
|
||||||
|
|
||||||
|
function asArray(resolvers) {
|
||||||
|
return {
|
||||||
|
resolve: (container) =>
|
||||||
|
resolvers.map((resolver) => container.build(resolver)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const container = createContainer()
|
const container = createContainer()
|
||||||
|
|
||||||
|
// TODO: remove once the util is merged in master
|
||||||
|
container.registerAdd = function (name, registration) {
|
||||||
|
const storeKey = name + "_STORE"
|
||||||
|
|
||||||
|
if (this.registrations[storeKey] === undefined) {
|
||||||
|
this.register(storeKey, asValue([]))
|
||||||
|
}
|
||||||
|
const store = this.resolve(storeKey)
|
||||||
|
|
||||||
|
if (this.registrations[name] === undefined) {
|
||||||
|
this.register(name, asArray(store))
|
||||||
|
}
|
||||||
|
store.unshift(registration)
|
||||||
|
|
||||||
|
return this
|
||||||
|
}.bind(container)
|
||||||
|
|
||||||
container.register("featureFlagRouter", asValue(featureFlagRouter))
|
container.register("featureFlagRouter", asValue(featureFlagRouter))
|
||||||
container.register("modulesHelper", asValue(moduleHelper))
|
container.register("modulesHelper", asValue(moduleHelper))
|
||||||
container.register("configModule", asValue(config))
|
container.register("configModule", asValue(config))
|
||||||
@@ -65,6 +90,8 @@ testApp.use((req, res, next) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
featureFlagLoader(config)
|
featureFlagLoader(config)
|
||||||
|
models({ container, configModule: config, isTest: true })
|
||||||
|
repositories({ container, isTest: true })
|
||||||
servicesLoader({ container, configModule: config })
|
servicesLoader({ container, configModule: config })
|
||||||
strategiesLoader({ container, configModule: config })
|
strategiesLoader({ container, configModule: config })
|
||||||
passportLoader({ app: testApp, container, configModule: config })
|
passportLoader({ app: testApp, container, configModule: config })
|
||||||
|
|||||||
@@ -3,16 +3,16 @@ import glob from "glob"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { ClassConstructor, MedusaContainer } from "../types/global"
|
import { ClassConstructor, MedusaContainer } from "../types/global"
|
||||||
import { EntitySchema } from "typeorm"
|
import { EntitySchema } from "typeorm"
|
||||||
import { asClass, asValue, AwilixContainer } from "awilix"
|
import { asClass, asValue } from "awilix"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers all models in the model directory
|
* Registers all models in the model directory
|
||||||
*/
|
*/
|
||||||
export default (
|
export default (
|
||||||
{ container }: { container: MedusaContainer },
|
{ container, isTest }: { container: MedusaContainer; isTest?: boolean },
|
||||||
config = { register: true }
|
config = { register: true }
|
||||||
) => {
|
) => {
|
||||||
const corePath = "../models/*.js"
|
const corePath = isTest ? "../models/*.ts" : "../models/*.js"
|
||||||
const coreFull = path.join(__dirname, corePath)
|
const coreFull = path.join(__dirname, corePath)
|
||||||
|
|
||||||
const models: (ClassConstructor<unknown> | EntitySchema)[] = []
|
const models: (ClassConstructor<unknown> | EntitySchema)[] = []
|
||||||
|
|||||||
@@ -8,8 +8,14 @@ import { ClassConstructor, MedusaContainer } from "../types/global"
|
|||||||
/**
|
/**
|
||||||
* Registers all models in the model directory
|
* Registers all models in the model directory
|
||||||
*/
|
*/
|
||||||
export default ({ container }: { container: MedusaContainer }): void => {
|
export default ({
|
||||||
const corePath = "../repositories/*.js"
|
container,
|
||||||
|
isTest,
|
||||||
|
}: {
|
||||||
|
container: MedusaContainer
|
||||||
|
isTest?: boolean
|
||||||
|
}): void => {
|
||||||
|
const corePath = isTest ? "../repositories/*.ts" : "../repositories/*.js"
|
||||||
const coreFull = path.join(__dirname, corePath)
|
const coreFull = path.join(__dirname, corePath)
|
||||||
|
|
||||||
const core = glob.sync(coreFull, { cwd: __dirname })
|
const core = glob.sync(coreFull, { cwd: __dirname })
|
||||||
|
|||||||
@@ -1,8 +1,25 @@
|
|||||||
import { EntityRepository, In, Repository } from "typeorm"
|
import { EntityRepository, In, Repository } from "typeorm"
|
||||||
import { Image } from "../models/image"
|
import { Image } from "../models"
|
||||||
|
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"
|
||||||
|
|
||||||
@EntityRepository(Image)
|
@EntityRepository(Image)
|
||||||
export class ImageRepository extends Repository<Image> {
|
export class ImageRepository extends Repository<Image> {
|
||||||
|
async insertBulk(data: QueryDeepPartialEntity<Image>[]): Promise<Image[]> {
|
||||||
|
const queryBuilder = this.createQueryBuilder()
|
||||||
|
.insert()
|
||||||
|
.into(Image)
|
||||||
|
.values(data)
|
||||||
|
|
||||||
|
// TODO: remove if statement once this issue is resolved https://github.com/typeorm/typeorm/issues/9850
|
||||||
|
if (!queryBuilder.connection.driver.isReturningSqlSupported("insert")) {
|
||||||
|
const rawImages = await queryBuilder.execute()
|
||||||
|
return rawImages.generatedMaps.map((d) => this.create(d)) as Image[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawImages = await queryBuilder.returning("*").execute()
|
||||||
|
return rawImages.generatedMaps.map((d) => this.create(d))
|
||||||
|
}
|
||||||
|
|
||||||
public async upsertImages(imageUrls: string[]) {
|
public async upsertImages(imageUrls: string[]) {
|
||||||
const existingImages = await this.find({
|
const existingImages = await this.find({
|
||||||
where: {
|
where: {
|
||||||
@@ -14,16 +31,21 @@ export class ImageRepository extends Repository<Image> {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const upsertedImgs: Image[] = []
|
const upsertedImgs: Image[] = []
|
||||||
|
const imageToCreate: QueryDeepPartialEntity<Image>[] = []
|
||||||
|
|
||||||
for (const url of imageUrls) {
|
imageUrls.forEach((url) => {
|
||||||
const aImg = existingImagesMap.get(url)
|
const aImg = existingImagesMap.get(url)
|
||||||
if (aImg) {
|
if (aImg) {
|
||||||
upsertedImgs.push(aImg)
|
upsertedImgs.push(aImg)
|
||||||
} else {
|
} else {
|
||||||
const newImg = this.create({ url })
|
const newImg = this.create({ url })
|
||||||
const savedImg = await this.save(newImg)
|
imageToCreate.push(newImg as QueryDeepPartialEntity<Image>)
|
||||||
upsertedImgs.push(savedImg)
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (imageToCreate.length) {
|
||||||
|
const newImgs = await this.insertBulk(imageToCreate)
|
||||||
|
upsertedImgs.push(...newImgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
return upsertedImgs
|
return upsertedImgs
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ import {
|
|||||||
WhereExpressionBuilder,
|
WhereExpressionBuilder,
|
||||||
} from "typeorm"
|
} from "typeorm"
|
||||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"
|
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"
|
||||||
import { MoneyAmount } from "../models/money-amount"
|
import { MoneyAmount } from "../models"
|
||||||
import {
|
import {
|
||||||
PriceListPriceCreateInput,
|
PriceListPriceCreateInput,
|
||||||
PriceListPriceUpdateInput,
|
PriceListPriceUpdateInput,
|
||||||
} from "../types/price-list"
|
} from "../types/price-list"
|
||||||
|
import { isString } from "../utils"
|
||||||
|
import { ProductVariantPrice } from "../types/product-variant"
|
||||||
|
|
||||||
type Price = Partial<
|
type Price = Partial<
|
||||||
Omit<MoneyAmount, "created_at" | "updated_at" | "deleted_at">
|
Omit<MoneyAmount, "created_at" | "updated_at" | "deleted_at">
|
||||||
@@ -24,6 +26,26 @@ type Price = Partial<
|
|||||||
|
|
||||||
@EntityRepository(MoneyAmount)
|
@EntityRepository(MoneyAmount)
|
||||||
export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
||||||
|
async insertBulk(
|
||||||
|
data: QueryDeepPartialEntity<MoneyAmount>[]
|
||||||
|
): Promise<MoneyAmount[]> {
|
||||||
|
const queryBuilder = this.createQueryBuilder()
|
||||||
|
.insert()
|
||||||
|
.into(MoneyAmount)
|
||||||
|
.values(data)
|
||||||
|
|
||||||
|
// TODO: remove if statement once this issue is resolved https://github.com/typeorm/typeorm/issues/9850
|
||||||
|
if (!queryBuilder.connection.driver.isReturningSqlSupported("insert")) {
|
||||||
|
const rawMoneyAmounts = await queryBuilder.execute()
|
||||||
|
return rawMoneyAmounts.generatedMaps.map((d) =>
|
||||||
|
this.create(d)
|
||||||
|
) as MoneyAmount[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawMoneyAmounts = await queryBuilder.returning("*").execute()
|
||||||
|
return rawMoneyAmounts.generatedMaps.map((d) => this.create(d))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will be removed in a future release.
|
* Will be removed in a future release.
|
||||||
* Use `deleteVariantPricesNotIn` instead.
|
* Use `deleteVariantPricesNotIn` instead.
|
||||||
@@ -50,41 +72,58 @@ export class MoneyAmountRepository extends Repository<MoneyAmount> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async deleteVariantPricesNotIn(
|
public async deleteVariantPricesNotIn(
|
||||||
variantId: string,
|
variantIdOrData:
|
||||||
prices: Price[]
|
| string
|
||||||
|
| { variantId: string; prices: ProductVariantPrice[] }[],
|
||||||
|
prices?: Price[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const where = {
|
const data = isString(variantIdOrData)
|
||||||
variant_id: variantId,
|
? [
|
||||||
price_list_id: IsNull(),
|
|
||||||
}
|
|
||||||
|
|
||||||
const orWhere: ObjectLiteral[] = []
|
|
||||||
|
|
||||||
for (const price of prices) {
|
|
||||||
if (price.currency_code) {
|
|
||||||
orWhere.push(
|
|
||||||
{
|
{
|
||||||
currency_code: Not(price.currency_code),
|
variantId: variantIdOrData,
|
||||||
|
prices: prices!,
|
||||||
},
|
},
|
||||||
{
|
]
|
||||||
region_id: price.region_id ? Not(price.region_id) : Not(IsNull()),
|
: variantIdOrData
|
||||||
currency_code: price.currency_code,
|
|
||||||
}
|
const queryBuilder = this.createQueryBuilder().delete()
|
||||||
)
|
|
||||||
|
for (const data_ of data) {
|
||||||
|
const where = {
|
||||||
|
variant_id: data_.variantId,
|
||||||
|
price_list_id: IsNull(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (price.region_id) {
|
const orWhere: ObjectLiteral[] = []
|
||||||
orWhere.push({
|
|
||||||
region_id: Not(price.region_id),
|
for (const price of data_.prices) {
|
||||||
})
|
if (price.currency_code) {
|
||||||
|
orWhere.push(
|
||||||
|
{
|
||||||
|
currency_code: Not(price.currency_code),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
region_id: price.region_id ? Not(price.region_id) : Not(IsNull()),
|
||||||
|
currency_code: price.currency_code,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (price.region_id) {
|
||||||
|
orWhere.push({
|
||||||
|
region_id: Not(price.region_id),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
queryBuilder.orWhere(
|
||||||
|
new Brackets((localQueryBuild) => {
|
||||||
|
localQueryBuild.where(where).andWhere(orWhere)
|
||||||
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.createQueryBuilder()
|
await queryBuilder.execute()
|
||||||
.delete()
|
|
||||||
.where(where)
|
|
||||||
.andWhere(orWhere)
|
|
||||||
.execute()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async upsertVariantCurrencyPrice(
|
public async upsertVariantCurrencyPrice(
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { EntityRepository, Repository } from "typeorm"
|
import { EntityRepository, Repository } from "typeorm"
|
||||||
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"
|
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"
|
||||||
import { StagedJob } from "../models"
|
import { StagedJob } from "../models"
|
||||||
import { rowSqlResultsToEntityTransformer } from "../utils"
|
|
||||||
|
|
||||||
@EntityRepository(StagedJob)
|
@EntityRepository(StagedJob)
|
||||||
export class StagedJobRepository extends Repository<StagedJob> {
|
export class StagedJobRepository extends Repository<StagedJob> {
|
||||||
@@ -14,15 +13,10 @@ export class StagedJobRepository extends Repository<StagedJob> {
|
|||||||
// TODO: remove if statement once this issue is resolved https://github.com/typeorm/typeorm/issues/9850
|
// TODO: remove if statement once this issue is resolved https://github.com/typeorm/typeorm/issues/9850
|
||||||
if (!queryBuilder.connection.driver.isReturningSqlSupported("insert")) {
|
if (!queryBuilder.connection.driver.isReturningSqlSupported("insert")) {
|
||||||
const rawStagedJobs = await queryBuilder.execute()
|
const rawStagedJobs = await queryBuilder.execute()
|
||||||
return rawStagedJobs.generatedMaps
|
return rawStagedJobs.generatedMaps.map((d) => this.create(d))
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawStagedJobs = await queryBuilder.returning("*").execute()
|
const rawStagedJobs = await queryBuilder.returning("*").execute()
|
||||||
|
return rawStagedJobs.generatedMaps.map((d) => this.create(d))
|
||||||
return rowSqlResultsToEntityTransformer(
|
|
||||||
rawStagedJobs.raw,
|
|
||||||
queryBuilder,
|
|
||||||
this.queryRunner!
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,6 +276,10 @@ describe("ProductVariantService", () => {
|
|||||||
|
|
||||||
const productVariantRepository = MockRepository({
|
const productVariantRepository = MockRepository({
|
||||||
findOne: (query) => Promise.resolve({ id: IdMap.getId("ironman") }),
|
findOne: (query) => Promise.resolve({ id: IdMap.getId("ironman") }),
|
||||||
|
update: (data) => ({
|
||||||
|
generatedMaps: [data],
|
||||||
|
}),
|
||||||
|
create: (data) => data,
|
||||||
})
|
})
|
||||||
|
|
||||||
const moneyAmountRepository = MockRepository({
|
const moneyAmountRepository = MockRepository({
|
||||||
@@ -315,22 +319,54 @@ describe("ProductVariantService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||||
expect(eventBusService.emit).toHaveBeenCalledWith(
|
expect(eventBusService.emit).toHaveBeenCalledWith([
|
||||||
"product-variant.updated",
|
|
||||||
{
|
{
|
||||||
id: IdMap.getId("ironman"),
|
eventName: "product-variant.updated",
|
||||||
fields: ["title"],
|
data: {
|
||||||
|
id: IdMap.getId("ironman"),
|
||||||
|
fields: ["title"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(productVariantRepository.update).toHaveBeenCalledTimes(1)
|
||||||
|
expect(productVariantRepository.update).toHaveBeenCalledWith(
|
||||||
|
{ id: IdMap.getId("ironman") },
|
||||||
|
{
|
||||||
|
title: "new title",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(productVariantRepository.save).toHaveBeenCalledTimes(1)
|
|
||||||
expect(productVariantRepository.save).toHaveBeenCalledWith({
|
|
||||||
id: IdMap.getId("ironman"),
|
|
||||||
title: "new title",
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("successfully updates variant", async () => {
|
it("successfully updates variant", async () => {
|
||||||
|
await productVariantService.update(
|
||||||
|
{ id: IdMap.getId("ironman"), title: "new title" },
|
||||||
|
{
|
||||||
|
title: "new title 2",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||||
|
expect(eventBusService.emit).toHaveBeenCalledWith([
|
||||||
|
{
|
||||||
|
eventName: "product-variant.updated",
|
||||||
|
data: {
|
||||||
|
id: IdMap.getId("ironman"),
|
||||||
|
fields: ["title"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(productVariantRepository.update).toHaveBeenCalledTimes(1)
|
||||||
|
expect(productVariantRepository.update).toHaveBeenCalledWith(
|
||||||
|
{ id: IdMap.getId("ironman") },
|
||||||
|
{
|
||||||
|
title: "new title 2",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully avoid to update variant if the data have not changed", async () => {
|
||||||
await productVariantService.update(
|
await productVariantService.update(
|
||||||
{ id: IdMap.getId("ironman"), title: "new title" },
|
{ id: IdMap.getId("ironman"), title: "new title" },
|
||||||
{
|
{
|
||||||
@@ -338,20 +374,9 @@ describe("ProductVariantService", () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
expect(eventBusService.emit).toHaveBeenCalledTimes(0)
|
||||||
expect(eventBusService.emit).toHaveBeenCalledWith(
|
|
||||||
"product-variant.updated",
|
|
||||||
{
|
|
||||||
id: IdMap.getId("ironman"),
|
|
||||||
fields: ["title"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(productVariantRepository.save).toHaveBeenCalledTimes(1)
|
expect(productVariantRepository.save).toHaveBeenCalledTimes(0)
|
||||||
expect(productVariantRepository.save).toHaveBeenCalledWith({
|
|
||||||
id: IdMap.getId("ironman"),
|
|
||||||
title: "new title",
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("throws if provided variant is missing an id", async () => {
|
it("throws if provided variant is missing an id", async () => {
|
||||||
@@ -376,22 +401,26 @@ describe("ProductVariantService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||||
expect(eventBusService.emit).toHaveBeenCalledWith(
|
expect(eventBusService.emit).toHaveBeenCalledWith([
|
||||||
"product-variant.updated",
|
|
||||||
{
|
{
|
||||||
id: IdMap.getId("ironman"),
|
eventName: "product-variant.updated",
|
||||||
fields: ["title", "metadata"],
|
data: {
|
||||||
|
id: IdMap.getId("ironman"),
|
||||||
|
fields: ["title", "metadata"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(productVariantRepository.update).toHaveBeenCalledTimes(1)
|
||||||
|
expect(productVariantRepository.update).toHaveBeenCalledWith(
|
||||||
|
{ id: IdMap.getId("ironman") },
|
||||||
|
{
|
||||||
|
title: "new title",
|
||||||
|
metadata: {
|
||||||
|
testing: "this",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(productVariantRepository.save).toHaveBeenCalledTimes(1)
|
|
||||||
expect(productVariantRepository.save).toHaveBeenCalledWith({
|
|
||||||
id: IdMap.getId("ironman"),
|
|
||||||
title: "new title",
|
|
||||||
metadata: {
|
|
||||||
testing: "this",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("successfully updates variant inventory_quantity", async () => {
|
it("successfully updates variant inventory_quantity", async () => {
|
||||||
@@ -401,20 +430,24 @@ describe("ProductVariantService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||||
expect(eventBusService.emit).toHaveBeenCalledWith(
|
expect(eventBusService.emit).toHaveBeenCalledWith([
|
||||||
"product-variant.updated",
|
|
||||||
{
|
{
|
||||||
id: IdMap.getId("ironman"),
|
eventName: "product-variant.updated",
|
||||||
fields: ["title", "inventory_quantity"],
|
data: {
|
||||||
|
id: IdMap.getId("ironman"),
|
||||||
|
fields: ["title", "inventory_quantity"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
expect(productVariantRepository.update).toHaveBeenCalledTimes(1)
|
||||||
|
expect(productVariantRepository.update).toHaveBeenCalledWith(
|
||||||
|
{ id: IdMap.getId("ironman") },
|
||||||
|
{
|
||||||
|
inventory_quantity: 98,
|
||||||
|
title: "new title",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(productVariantRepository.save).toHaveBeenCalledTimes(1)
|
|
||||||
expect(productVariantRepository.save).toHaveBeenCalledWith({
|
|
||||||
id: IdMap.getId("ironman"),
|
|
||||||
inventory_quantity: 98,
|
|
||||||
title: "new title",
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("successfully updates variant prices", async () => {
|
it("successfully updates variant prices", async () => {
|
||||||
@@ -429,17 +462,19 @@ describe("ProductVariantService", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
expect(productVariantService.updateVariantPrices).toHaveBeenCalledTimes(1)
|
expect(productVariantService.updateVariantPrices).toHaveBeenCalledTimes(1)
|
||||||
expect(productVariantService.updateVariantPrices).toHaveBeenCalledWith(
|
expect(productVariantService.updateVariantPrices).toHaveBeenCalledWith([
|
||||||
IdMap.getId("ironman"),
|
{
|
||||||
[
|
variantId: IdMap.getId("ironman"),
|
||||||
{
|
prices: [
|
||||||
currency_code: "dkk",
|
{
|
||||||
amount: 1000,
|
currency_code: "dkk",
|
||||||
},
|
amount: 1000,
|
||||||
]
|
},
|
||||||
)
|
],
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
expect(productVariantRepository.save).toHaveBeenCalledTimes(1)
|
expect(productVariantRepository.update).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("successfully updates variant options", async () => {
|
it("successfully updates variant options", async () => {
|
||||||
@@ -460,7 +495,7 @@ describe("ProductVariantService", () => {
|
|||||||
"red"
|
"red"
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(productVariantRepository.save).toHaveBeenCalledTimes(1)
|
expect(productVariantRepository.update).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -628,8 +663,22 @@ describe("ProductVariantService", () => {
|
|||||||
amount: 750,
|
amount: 750,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
find: (query) => {
|
||||||
|
if (query.where.region_id === IdMap.getId("cali")) {
|
||||||
|
return Promise.resolve([])
|
||||||
|
}
|
||||||
|
return Promise.resolve([
|
||||||
|
{
|
||||||
|
id: IdMap.getId("dkk"),
|
||||||
|
variant_id: IdMap.getId("ironman"),
|
||||||
|
currency_code: "dkk",
|
||||||
|
amount: 750,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
},
|
||||||
create: (p) => p,
|
create: (p) => p,
|
||||||
remove: () => Promise.resolve(),
|
remove: () => Promise.resolve(),
|
||||||
|
insertBulk: (data) => data,
|
||||||
})
|
})
|
||||||
|
|
||||||
const oldPrices = [
|
const oldPrices = [
|
||||||
@@ -683,7 +732,7 @@ describe("ProductVariantService", () => {
|
|||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("successfully removes obsolete prices and calls save on new/existing prices", async () => {
|
it("successfully removes obsolete prices and create new prices", async () => {
|
||||||
await productVariantService.updateVariantPrices("ironman", [
|
await productVariantService.updateVariantPrices("ironman", [
|
||||||
{
|
{
|
||||||
currency_code: "usd",
|
currency_code: "usd",
|
||||||
@@ -695,15 +744,14 @@ describe("ProductVariantService", () => {
|
|||||||
moneyAmountRepository.deleteVariantPricesNotIn
|
moneyAmountRepository.deleteVariantPricesNotIn
|
||||||
).toHaveBeenCalledTimes(1)
|
).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
expect(
|
expect(moneyAmountRepository.insertBulk).toHaveBeenCalledTimes(1)
|
||||||
moneyAmountRepository.upsertVariantCurrencyPrice
|
expect(moneyAmountRepository.insertBulk).toHaveBeenCalledWith([
|
||||||
).toHaveBeenCalledTimes(1)
|
{
|
||||||
expect(
|
variant_id: "ironman",
|
||||||
moneyAmountRepository.upsertVariantCurrencyPrice
|
currency_code: "usd",
|
||||||
).toHaveBeenCalledWith("ironman", {
|
amount: 4000,
|
||||||
currency_code: "usd",
|
},
|
||||||
amount: 4000,
|
])
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it("successfully creates new a region price", async () => {
|
it("successfully creates new a region price", async () => {
|
||||||
@@ -718,33 +766,31 @@ describe("ProductVariantService", () => {
|
|||||||
expect(moneyAmountRepository.create).toHaveBeenCalledWith({
|
expect(moneyAmountRepository.create).toHaveBeenCalledWith({
|
||||||
variant_id: IdMap.getId("ironman"),
|
variant_id: IdMap.getId("ironman"),
|
||||||
region_id: IdMap.getId("cali"),
|
region_id: IdMap.getId("cali"),
|
||||||
|
currency_code: "usd",
|
||||||
amount: 100,
|
amount: 100,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(moneyAmountRepository.save).toHaveBeenCalledTimes(1)
|
expect(moneyAmountRepository.insertBulk).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("successfully creates a currency price", async () => {
|
it("successfully updates a currency price", async () => {
|
||||||
await productVariantService.updateVariantPrices(IdMap.getId("ironman"), [
|
await productVariantService.updateVariantPrices(IdMap.getId("ironman"), [
|
||||||
{
|
{
|
||||||
id: IdMap.getId("dkk"),
|
id: IdMap.getId("dkk"),
|
||||||
currency_code: "dkk",
|
currency_code: "dkk",
|
||||||
amount: 750,
|
amount: 850,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
expect(moneyAmountRepository.create).toHaveBeenCalledTimes(0)
|
expect(moneyAmountRepository.create).toHaveBeenCalledTimes(0)
|
||||||
|
|
||||||
expect(
|
expect(moneyAmountRepository.update).toHaveBeenCalledTimes(1)
|
||||||
moneyAmountRepository.upsertVariantCurrencyPrice
|
expect(moneyAmountRepository.update).toHaveBeenCalledWith(
|
||||||
).toHaveBeenCalledTimes(1)
|
{ id: IdMap.getId("dkk") },
|
||||||
expect(
|
{
|
||||||
moneyAmountRepository.upsertVariantCurrencyPrice
|
amount: 850,
|
||||||
).toHaveBeenCalledWith(IdMap.getId("ironman"), {
|
}
|
||||||
id: IdMap.getId("dkk"),
|
)
|
||||||
currency_code: "dkk",
|
|
||||||
amount: 750,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -881,14 +927,16 @@ describe("ProductVariantService", () => {
|
|||||||
|
|
||||||
describe("delete", () => {
|
describe("delete", () => {
|
||||||
const productVariantRepository = MockRepository({
|
const productVariantRepository = MockRepository({
|
||||||
findOne: (query) => {
|
find: (query) => {
|
||||||
if (query.where.id === IdMap.getId("ironmanv2")) {
|
if (query.where.id === IdMap.getId("ironmanv2")) {
|
||||||
return Promise.resolve(undefined)
|
return Promise.resolve([])
|
||||||
}
|
}
|
||||||
return Promise.resolve({
|
return Promise.resolve([
|
||||||
id: IdMap.getId("ironman"),
|
{
|
||||||
product_id: IdMap.getId("product-test"),
|
id: IdMap.getId("ironman"),
|
||||||
})
|
product_id: IdMap.getId("product-test"),
|
||||||
|
},
|
||||||
|
])
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -906,20 +954,22 @@ describe("ProductVariantService", () => {
|
|||||||
await productVariantService.delete(IdMap.getId("ironman"))
|
await productVariantService.delete(IdMap.getId("ironman"))
|
||||||
|
|
||||||
expect(productVariantRepository.softRemove).toBeCalledTimes(1)
|
expect(productVariantRepository.softRemove).toBeCalledTimes(1)
|
||||||
expect(productVariantRepository.softRemove).toBeCalledWith(
|
expect(productVariantRepository.softRemove).toBeCalledWith([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: IdMap.getId("ironman"),
|
id: IdMap.getId("ironman"),
|
||||||
})
|
}),
|
||||||
)
|
])
|
||||||
|
|
||||||
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
|
||||||
expect(eventBusService.emit).toHaveBeenCalledWith(
|
expect(eventBusService.emit).toHaveBeenCalledWith([
|
||||||
"product-variant.deleted",
|
|
||||||
{
|
{
|
||||||
id: IdMap.getId("ironman"),
|
eventName: "product-variant.deleted",
|
||||||
product_id: IdMap.getId("product-test"),
|
data: {
|
||||||
}
|
id: IdMap.getId("ironman"),
|
||||||
)
|
product_id: IdMap.getId("product-test"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it("successfully resolves if variant does not exist", async () => {
|
it("successfully resolves if variant does not exist", async () => {
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||||
import { Brackets, EntityManager, ILike, SelectQueryBuilder } from "typeorm"
|
import {
|
||||||
|
Brackets,
|
||||||
|
EntityManager,
|
||||||
|
ILike,
|
||||||
|
In,
|
||||||
|
IsNull,
|
||||||
|
SelectQueryBuilder,
|
||||||
|
} from "typeorm"
|
||||||
import {
|
import {
|
||||||
IPriceSelectionStrategy,
|
IPriceSelectionStrategy,
|
||||||
PriceSelectionContext,
|
PriceSelectionContext,
|
||||||
@@ -19,17 +26,28 @@ import {
|
|||||||
FindWithRelationsOptions,
|
FindWithRelationsOptions,
|
||||||
ProductVariantRepository,
|
ProductVariantRepository,
|
||||||
} from "../repositories/product-variant"
|
} from "../repositories/product-variant"
|
||||||
import { FindConfig } from "../types/common"
|
import { FindConfig, WithRequiredProperty } from "../types/common"
|
||||||
import {
|
import {
|
||||||
CreateProductVariantInput,
|
CreateProductVariantInput,
|
||||||
FilterableProductVariantProps,
|
FilterableProductVariantProps,
|
||||||
GetRegionPriceContext,
|
GetRegionPriceContext,
|
||||||
ProductVariantPrice,
|
ProductVariantPrice,
|
||||||
|
UpdateProductVariantData,
|
||||||
UpdateProductVariantInput,
|
UpdateProductVariantInput,
|
||||||
|
UpdateVariantCurrencyPriceData,
|
||||||
|
UpdateVariantPricesData,
|
||||||
|
UpdateVariantRegionPriceData,
|
||||||
} from "../types/product-variant"
|
} from "../types/product-variant"
|
||||||
import { buildQuery, setMetadata } from "../utils"
|
import {
|
||||||
|
buildQuery,
|
||||||
|
hasChanges,
|
||||||
|
isObject,
|
||||||
|
isString,
|
||||||
|
setMetadata,
|
||||||
|
} from "../utils"
|
||||||
import EventBusService from "./event-bus"
|
import EventBusService from "./event-bus"
|
||||||
import RegionService from "./region"
|
import RegionService from "./region"
|
||||||
|
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"
|
||||||
|
|
||||||
class ProductVariantService extends TransactionBaseService {
|
class ProductVariantService extends TransactionBaseService {
|
||||||
static Events = {
|
static Events = {
|
||||||
@@ -160,7 +178,7 @@ class ProductVariantService extends TransactionBaseService {
|
|||||||
|
|
||||||
let product = productOrProductId
|
let product = productOrProductId
|
||||||
|
|
||||||
if (typeof product === `string`) {
|
if (isString(product)) {
|
||||||
product = (await productRepo.findOne({
|
product = (await productRepo.findOne({
|
||||||
where: { id: productOrProductId },
|
where: { id: productOrProductId },
|
||||||
relations: ["variants", "variants.options", "options"],
|
relations: ["variants", "variants.options", "options"],
|
||||||
@@ -219,19 +237,12 @@ class ProductVariantService extends TransactionBaseService {
|
|||||||
const result = await variantRepo.save(productVariant)
|
const result = await variantRepo.save(productVariant)
|
||||||
|
|
||||||
if (prices) {
|
if (prices) {
|
||||||
for (const price of prices) {
|
await this.updateVariantPrices([
|
||||||
if (price.region_id) {
|
{
|
||||||
const region = await this.regionService_.retrieve(price.region_id)
|
variantId: result.id,
|
||||||
|
prices,
|
||||||
await this.setRegionPrice(result.id, {
|
},
|
||||||
amount: price.amount,
|
])
|
||||||
region_id: price.region_id,
|
|
||||||
currency_code: region.currency_code,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await this.setCurrencyPrice(result.id, price)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.eventBus_
|
await this.eventBus_
|
||||||
@@ -245,86 +256,186 @@ class ProductVariantService extends TransactionBaseService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a collection of variant.
|
||||||
|
* @param variantData - a collection of variant and the data to update.
|
||||||
|
* @return resolves to the update result.
|
||||||
|
*/
|
||||||
|
async update(
|
||||||
|
variantData: {
|
||||||
|
variant: ProductVariant
|
||||||
|
updateData: UpdateProductVariantInput
|
||||||
|
}[]
|
||||||
|
): Promise<ProductVariant[]>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates a variant.
|
* Updates a variant.
|
||||||
* Price updates should use dedicated methods.
|
* Price updates should use dedicated methods.
|
||||||
* The function will throw, if price updates are attempted.
|
* The function will throw, if price updates are attempted.
|
||||||
* @param variantOrVariantId - variant or id of a variant.
|
* @param variantOrVariantId - variant or id of a variant.
|
||||||
* @param update - an object with the update values.
|
* @param update - an object with the update values.
|
||||||
* @param config - an object with the config values for returning the variant.
|
|
||||||
* @return resolves to the update result.
|
* @return resolves to the update result.
|
||||||
*/
|
*/
|
||||||
async update(
|
async update(
|
||||||
variantOrVariantId: string | Partial<ProductVariant>,
|
variantOrVariantId: string | Partial<ProductVariant>,
|
||||||
update: UpdateProductVariantInput
|
update: UpdateProductVariantInput
|
||||||
): Promise<ProductVariant> {
|
): Promise<ProductVariant>
|
||||||
|
|
||||||
|
async update(
|
||||||
|
variantOrVariantId: string | Partial<ProductVariant>,
|
||||||
|
update: UpdateProductVariantInput
|
||||||
|
): Promise<ProductVariant>
|
||||||
|
|
||||||
|
async update<
|
||||||
|
TInput extends
|
||||||
|
| string
|
||||||
|
| Partial<ProductVariant>
|
||||||
|
| UpdateProductVariantData[],
|
||||||
|
TResult = TInput extends UpdateProductVariantData[]
|
||||||
|
? ProductVariant[]
|
||||||
|
: ProductVariant
|
||||||
|
>(
|
||||||
|
variantOrVariantIdOrData: TInput,
|
||||||
|
updateData?: UpdateProductVariantInput
|
||||||
|
): Promise<TResult> {
|
||||||
|
let data = Array.isArray(variantOrVariantIdOrData)
|
||||||
|
? variantOrVariantIdOrData
|
||||||
|
: ([] as UpdateProductVariantData[])
|
||||||
|
|
||||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||||
const variantRepo = manager.getCustomRepository(
|
const variantRepo = manager.getCustomRepository(
|
||||||
this.productVariantRepository_
|
this.productVariantRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
let variant = variantOrVariantId
|
if (updateData) {
|
||||||
if (typeof variant === `string`) {
|
let variant: Partial<ProductVariant> | null =
|
||||||
const variantRes = await variantRepo.findOne({
|
variantOrVariantIdOrData as Partial<ProductVariant>
|
||||||
where: { id: variantOrVariantId as string },
|
|
||||||
})
|
if (isString(variantOrVariantIdOrData)) {
|
||||||
if (!isDefined(variantRes)) {
|
variant = await this.retrieve(variantOrVariantIdOrData, {
|
||||||
|
select: variantRepo.metadata.columns.map(
|
||||||
|
(c) => c.propertyName
|
||||||
|
) as (keyof ProductVariant)[],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!variant?.id) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.NOT_FOUND,
|
MedusaError.Types.INVALID_DATA,
|
||||||
`Variant with id ${variantOrVariantId} was not found`
|
`Variant id missing`
|
||||||
)
|
|
||||||
} else {
|
|
||||||
variant = variantRes as ProductVariant
|
|
||||||
}
|
|
||||||
} else if (!variant.id) {
|
|
||||||
throw new MedusaError(
|
|
||||||
MedusaError.Types.INVALID_DATA,
|
|
||||||
`Variant id missing`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const { prices, options, metadata, inventory_quantity, ...rest } = update
|
|
||||||
|
|
||||||
if (prices) {
|
|
||||||
await this.updateVariantPrices(variant.id!, prices)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options) {
|
|
||||||
for (const option of options) {
|
|
||||||
await this.updateOptionValue(
|
|
||||||
variant.id!,
|
|
||||||
option.option_id,
|
|
||||||
option.value
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data = [{ variant: variant as ProductVariant, updateData: updateData }]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof metadata === "object") {
|
const result = await this.updateBatch(data)
|
||||||
variant.metadata = setMetadata(variant as ProductVariant, metadata)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof inventory_quantity === "number") {
|
return (Array.isArray(variantOrVariantIdOrData)
|
||||||
variant.inventory_quantity = inventory_quantity as number
|
? result
|
||||||
}
|
: result[0]) as unknown as TResult
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(rest)) {
|
|
||||||
variant[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await variantRepo.save(variant)
|
|
||||||
|
|
||||||
await this.eventBus_
|
|
||||||
.withTransaction(manager)
|
|
||||||
.emit(ProductVariantService.Events.UPDATED, {
|
|
||||||
id: result.id,
|
|
||||||
product_id: result.product_id,
|
|
||||||
fields: Object.keys(update),
|
|
||||||
})
|
|
||||||
|
|
||||||
return result
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async updateBatch(
|
||||||
|
variantData: UpdateProductVariantData[]
|
||||||
|
): Promise<ProductVariant[]> {
|
||||||
|
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||||
|
const variantRepo = manager.getCustomRepository(
|
||||||
|
this.productVariantRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
const variantPriceUpdateData = variantData
|
||||||
|
.filter((data) => isDefined(data.updateData.prices))
|
||||||
|
.map((data) => ({
|
||||||
|
variantId: data.variant.id,
|
||||||
|
prices: data.updateData.prices!,
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (variantPriceUpdateData.length) {
|
||||||
|
await this.updateVariantPrices(variantPriceUpdateData)
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: [ProductVariant, UpdateProductVariantInput, boolean][] =
|
||||||
|
await Promise.all(
|
||||||
|
variantData.map(async ({ variant, updateData }) => {
|
||||||
|
const { prices, options, ...rest } = updateData
|
||||||
|
|
||||||
|
const shouldUpdate = hasChanges(variant, rest)
|
||||||
|
const shouldEmitUpdateEvent =
|
||||||
|
shouldUpdate || !!options?.length || !!prices?.length
|
||||||
|
|
||||||
|
for (const option of options ?? []) {
|
||||||
|
await this.updateOptionValue(
|
||||||
|
variant.id!,
|
||||||
|
option.option_id,
|
||||||
|
option.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toUpdate: QueryDeepPartialEntity<ProductVariant> = {}
|
||||||
|
|
||||||
|
if (isObject(rest.metadata)) {
|
||||||
|
toUpdate["metadata"] = setMetadata(
|
||||||
|
variant as ProductVariant,
|
||||||
|
rest.metadata
|
||||||
|
) as QueryDeepPartialEntity<Record<string, unknown>>
|
||||||
|
delete rest.metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(rest).length) {
|
||||||
|
for (const [key, value] of Object.entries(rest)) {
|
||||||
|
if (variant[key] !== value) {
|
||||||
|
toUpdate[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = variant
|
||||||
|
|
||||||
|
// No need to update if nothing on the variant has changed
|
||||||
|
if (shouldUpdate) {
|
||||||
|
const { id } = variant
|
||||||
|
const rawResult = await variantRepo.update({ id }, toUpdate)
|
||||||
|
result = variantRepo.create({
|
||||||
|
...variant,
|
||||||
|
...rawResult.generatedMaps[0],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return [result, updateData, shouldEmitUpdateEvent]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const events = results
|
||||||
|
.filter(([, , shouldEmitUpdateEvent]) => shouldEmitUpdateEvent)
|
||||||
|
.map(([result, updatedData]) => {
|
||||||
|
return {
|
||||||
|
eventName: ProductVariantService.Events.UPDATED,
|
||||||
|
data: {
|
||||||
|
id: result.id,
|
||||||
|
product_id: result.product_id,
|
||||||
|
fields: Object.keys(updatedData),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (events.length) {
|
||||||
|
await this.eventBus_.withTransaction(manager).emit(events)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.map(([variant]) => variant)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates variant/prices collection.
|
||||||
|
* Deletes any prices that are not in the update object, and is not associated with a price list.
|
||||||
|
* @param data
|
||||||
|
* @returns empty promise
|
||||||
|
*/
|
||||||
|
async updateVariantPrices(data: UpdateVariantPricesData[]): Promise<void>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates a variant's prices.
|
* Updates a variant's prices.
|
||||||
* Deletes any prices that are not in the update object, and is not associated with a price list.
|
* Deletes any prices that are not in the update object, and is not associated with a price list.
|
||||||
@@ -335,6 +446,25 @@ class ProductVariantService extends TransactionBaseService {
|
|||||||
async updateVariantPrices(
|
async updateVariantPrices(
|
||||||
variantId: string,
|
variantId: string,
|
||||||
prices: ProductVariantPrice[]
|
prices: ProductVariantPrice[]
|
||||||
|
): Promise<void>
|
||||||
|
|
||||||
|
async updateVariantPrices(
|
||||||
|
variantIdOrData: string | UpdateVariantPricesData[],
|
||||||
|
prices?: ProductVariantPrice[]
|
||||||
|
): Promise<void> {
|
||||||
|
let data = !isString(variantIdOrData)
|
||||||
|
? variantIdOrData
|
||||||
|
: ([] as UpdateVariantPricesData[])
|
||||||
|
|
||||||
|
if (prices && isString(variantIdOrData)) {
|
||||||
|
data = [{ variantId: variantIdOrData, prices }]
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.updateVariantPricesBatch(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async updateVariantPricesBatch(
|
||||||
|
data: UpdateVariantPricesData[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||||
const moneyAmountRepo = manager.getCustomRepository(
|
const moneyAmountRepo = manager.getCustomRepository(
|
||||||
@@ -342,23 +472,214 @@ class ProductVariantService extends TransactionBaseService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Delete obsolete prices
|
// Delete obsolete prices
|
||||||
await moneyAmountRepo.deleteVariantPricesNotIn(variantId, prices)
|
await moneyAmountRepo.deleteVariantPricesNotIn(data)
|
||||||
|
|
||||||
const regionsServiceTx = this.regionService_.withTransaction(manager)
|
const regionIdsSet: Set<string> = new Set(
|
||||||
|
data
|
||||||
|
.map((data_) =>
|
||||||
|
data_.prices
|
||||||
|
.filter((price) => price.region_id)
|
||||||
|
.map((price) => price.region_id!)
|
||||||
|
)
|
||||||
|
.flat()
|
||||||
|
)
|
||||||
|
|
||||||
for (const price of prices) {
|
const regions = await this.regionService_.withTransaction(manager).list(
|
||||||
if (price.region_id) {
|
{
|
||||||
const region = await regionsServiceTx.retrieve(price.region_id)
|
id: [...regionIdsSet],
|
||||||
|
},
|
||||||
await this.setRegionPrice(variantId, {
|
{
|
||||||
currency_code: region.currency_code,
|
select: ["id", "currency_code"],
|
||||||
region_id: price.region_id,
|
|
||||||
amount: price.amount,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
await this.setCurrencyPrice(variantId, price)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const regionsMap = new Map(regions.map((r) => [r.id, r]))
|
||||||
|
|
||||||
|
const dataRegionPrices: UpdateVariantRegionPriceData[] = []
|
||||||
|
const dataCurrencyPrices: UpdateVariantCurrencyPriceData[] = []
|
||||||
|
|
||||||
|
data.forEach(({ prices, variantId }) => {
|
||||||
|
prices.forEach((price) => {
|
||||||
|
if (price.region_id) {
|
||||||
|
const region = regionsMap.get(price.region_id)!
|
||||||
|
dataRegionPrices.push({
|
||||||
|
variantId,
|
||||||
|
price: {
|
||||||
|
currency_code: region.currency_code,
|
||||||
|
region_id: price.region_id,
|
||||||
|
amount: price.amount,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
dataCurrencyPrices.push({
|
||||||
|
variantId,
|
||||||
|
price: {
|
||||||
|
...price,
|
||||||
|
currency_code: price.currency_code!,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const promises: Promise<any>[] = []
|
||||||
|
|
||||||
|
if (dataRegionPrices.length) {
|
||||||
|
promises.push(this.addOrUpdateRegionPrices(dataRegionPrices))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dataCurrencyPrices.length) {
|
||||||
|
promises.push(this.addOrUpdateCurrencyPrices(dataCurrencyPrices))
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async addOrUpdateRegionPrices(
|
||||||
|
data: UpdateVariantRegionPriceData[]
|
||||||
|
): Promise<void> {
|
||||||
|
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||||
|
const moneyAmountRepo = manager.getCustomRepository(
|
||||||
|
this.moneyAmountRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
const where = data.map((data_) => ({
|
||||||
|
variant_id: data_.variantId,
|
||||||
|
region_id: data_.price.region_id,
|
||||||
|
price_list_id: IsNull(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const moneyAmounts = await moneyAmountRepo.find({
|
||||||
|
where,
|
||||||
|
})
|
||||||
|
|
||||||
|
const moneyAmountsMapToVariantId = new Map()
|
||||||
|
moneyAmounts.map((d) => {
|
||||||
|
const moneyAmounts = moneyAmountsMapToVariantId.get(d.variant_id) ?? []
|
||||||
|
moneyAmounts.push(d)
|
||||||
|
moneyAmountsMapToVariantId.set(d.variant_id, moneyAmounts)
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataToCreate: QueryDeepPartialEntity<MoneyAmount>[] = []
|
||||||
|
const dataToUpdate: QueryDeepPartialEntity<MoneyAmount>[] = []
|
||||||
|
|
||||||
|
data.forEach(({ price, variantId }) => {
|
||||||
|
const variantMoneyAmounts =
|
||||||
|
moneyAmountsMapToVariantId.get(variantId) ?? []
|
||||||
|
|
||||||
|
const moneyAmount: MoneyAmount = variantMoneyAmounts.find(
|
||||||
|
(ma) => ma.region_id === price.region_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (moneyAmount) {
|
||||||
|
// No need to update if the amount is the same
|
||||||
|
if (moneyAmount.amount !== price.amount) {
|
||||||
|
dataToUpdate.push({
|
||||||
|
id: moneyAmount.id,
|
||||||
|
amount: price.amount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dataToCreate.push(
|
||||||
|
moneyAmountRepo.create({
|
||||||
|
...price,
|
||||||
|
variant_id: variantId,
|
||||||
|
}) as QueryDeepPartialEntity<MoneyAmount>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const promises: Promise<any>[] = []
|
||||||
|
|
||||||
|
if (dataToCreate.length) {
|
||||||
|
promises.push(moneyAmountRepo.insertBulk(dataToCreate))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataToUpdate.length) {
|
||||||
|
dataToUpdate.forEach((data) => {
|
||||||
|
const { id, ...rest } = data
|
||||||
|
promises.push(moneyAmountRepo.update({ id: data.id as string }, rest))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async addOrUpdateCurrencyPrices(
|
||||||
|
data: {
|
||||||
|
variantId: string
|
||||||
|
price: WithRequiredProperty<ProductVariantPrice, "currency_code">
|
||||||
|
}[]
|
||||||
|
): Promise<void> {
|
||||||
|
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||||
|
const moneyAmountRepo = manager.getCustomRepository(
|
||||||
|
this.moneyAmountRepository_
|
||||||
|
)
|
||||||
|
|
||||||
|
const where = data.map((data_) => ({
|
||||||
|
variant_id: data_.variantId,
|
||||||
|
currency_code: data_.price.currency_code,
|
||||||
|
region_id: IsNull(),
|
||||||
|
price_list_id: IsNull(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const moneyAmounts = await moneyAmountRepo.find({
|
||||||
|
where,
|
||||||
|
})
|
||||||
|
|
||||||
|
const moneyAmountsMapToVariantId = new Map()
|
||||||
|
moneyAmounts.map((d) => {
|
||||||
|
const moneyAmounts = moneyAmountsMapToVariantId.get(d.variant_id) ?? []
|
||||||
|
moneyAmounts.push(d)
|
||||||
|
moneyAmountsMapToVariantId.set(d.variant_id, moneyAmounts)
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataToCreate: QueryDeepPartialEntity<MoneyAmount>[] = []
|
||||||
|
const dataToUpdate: QueryDeepPartialEntity<MoneyAmount>[] = []
|
||||||
|
|
||||||
|
data.forEach(({ price, variantId }) => {
|
||||||
|
const variantMoneyAmounts =
|
||||||
|
moneyAmountsMapToVariantId.get(variantId) ?? []
|
||||||
|
|
||||||
|
const moneyAmount: MoneyAmount = variantMoneyAmounts.find(
|
||||||
|
(ma) => ma.currency_code === price.currency_code
|
||||||
|
)
|
||||||
|
|
||||||
|
if (moneyAmount) {
|
||||||
|
// No need to update if the amount is the same
|
||||||
|
if (moneyAmount.amount !== price.amount) {
|
||||||
|
dataToUpdate.push({
|
||||||
|
id: moneyAmount.id,
|
||||||
|
amount: price.amount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dataToCreate.push(
|
||||||
|
moneyAmountRepo.create({
|
||||||
|
...price,
|
||||||
|
variant_id: variantId,
|
||||||
|
currency_code: price.currency_code.toLowerCase(),
|
||||||
|
}) as QueryDeepPartialEntity<MoneyAmount>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const promises: Promise<any>[] = []
|
||||||
|
|
||||||
|
if (dataToCreate.length) {
|
||||||
|
promises.push(moneyAmountRepo.insertBulk(dataToCreate))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataToUpdate.length) {
|
||||||
|
dataToUpdate.forEach((data) => {
|
||||||
|
const { id, ...rest } = data
|
||||||
|
promises.push(moneyAmountRepo.update({ id: data.id as string }, rest))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,6 +715,7 @@ class ProductVariantService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @deprecated use addOrUpdateRegionPrices instead
|
||||||
* Sets the default price of a specific region
|
* Sets the default price of a specific region
|
||||||
* @param variantId - the id of the variant to update
|
* @param variantId - the id of the variant to update
|
||||||
* @param price - the price for the variant.
|
* @param price - the price for the variant.
|
||||||
@@ -430,6 +752,7 @@ class ProductVariantService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @deprecated use addOrUpdateCurrencyPrices instead
|
||||||
* Sets the default price for the given currency.
|
* Sets the default price for the given currency.
|
||||||
* @param variantId - the id of the variant to set prices for
|
* @param variantId - the id of the variant to set prices for
|
||||||
* @param price - the price for the variant
|
* @param price - the price for the variant
|
||||||
@@ -641,36 +964,43 @@ class ProductVariantService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes variant.
|
* Deletes variant or variants.
|
||||||
* Will never fail due to delete being idempotent.
|
* Will never fail due to delete being idempotent.
|
||||||
* @param variantId - the id of the variant to delete. Must be
|
* @param variantIds - the id of the variant to delete. Must be
|
||||||
* castable as an ObjectId
|
* castable as an ObjectId
|
||||||
* @return empty promise
|
* @return empty promise
|
||||||
*/
|
*/
|
||||||
async delete(variantId: string): Promise<void> {
|
async delete(variantIds: string | string[]): Promise<void> {
|
||||||
|
const variantIds_ = isString(variantIds) ? [variantIds] : variantIds
|
||||||
|
|
||||||
return await this.atomicPhase_(async (manager: EntityManager) => {
|
return await this.atomicPhase_(async (manager: EntityManager) => {
|
||||||
const variantRepo = manager.getCustomRepository(
|
const variantRepo = manager.getCustomRepository(
|
||||||
this.productVariantRepository_
|
this.productVariantRepository_
|
||||||
)
|
)
|
||||||
|
|
||||||
const variant = await variantRepo.findOne({
|
const variants = await variantRepo.find({
|
||||||
where: { id: variantId },
|
where: { id: In(variantIds_) },
|
||||||
relations: ["prices", "options", "inventory_items"],
|
relations: ["prices", "options", "inventory_items"],
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!variant) {
|
if (!variants.length) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
await variantRepo.softRemove(variant)
|
await variantRepo.softRemove(variants)
|
||||||
|
|
||||||
await this.eventBus_
|
const events = variants.map((variant) => {
|
||||||
.withTransaction(manager)
|
return {
|
||||||
.emit(ProductVariantService.Events.DELETED, {
|
eventName: ProductVariantService.Events.DELETED,
|
||||||
id: variant.id,
|
data: {
|
||||||
product_id: variant.product_id,
|
id: variant.id,
|
||||||
metadata: variant.metadata,
|
product_id: variant.product_id,
|
||||||
})
|
metadata: variant.metadata,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.eventBus_.withTransaction(manager).emit(events)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -495,9 +495,6 @@ class ProductService extends TransactionBaseService {
|
|||||||
): Promise<Product> {
|
): Promise<Product> {
|
||||||
return await this.atomicPhase_(async (manager) => {
|
return await this.atomicPhase_(async (manager) => {
|
||||||
const productRepo = manager.getCustomRepository(this.productRepository_)
|
const productRepo = manager.getCustomRepository(this.productRepository_)
|
||||||
const productVariantRepo = manager.getCustomRepository(
|
|
||||||
this.productVariantRepository_
|
|
||||||
)
|
|
||||||
const productTagRepo = manager.getCustomRepository(
|
const productTagRepo = manager.getCustomRepository(
|
||||||
this.productTagRepository_
|
this.productTagRepository_
|
||||||
)
|
)
|
||||||
@@ -541,20 +538,32 @@ class ProductService extends TransactionBaseService {
|
|||||||
product.thumbnail = images[0]
|
product.thumbnail = images[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const promises: Promise<any>[] = []
|
||||||
|
|
||||||
if (images) {
|
if (images) {
|
||||||
product.images = await imageRepo.upsertImages(images)
|
promises.push(
|
||||||
|
imageRepo
|
||||||
|
.upsertImages(images)
|
||||||
|
.then((image) => (product.images = image))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
product.metadata = setMetadata(product, metadata)
|
product.metadata = setMetadata(product, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof type !== `undefined`) {
|
if (isDefined(type)) {
|
||||||
product.type_id = (await productTypeRepo.upsertType(type))?.id || null
|
promises.push(
|
||||||
|
productTypeRepo
|
||||||
|
.upsertType(type)
|
||||||
|
.then((type) => (product.type_id = type?.id ?? null))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tags) {
|
if (tags) {
|
||||||
product.tags = await productTagRepo.upsertTags(tags)
|
promises.push(
|
||||||
|
productTagRepo.upsertTags(tags).then((tags) => (product.tags = tags))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDefined(categories)) {
|
if (isDefined(categories)) {
|
||||||
@@ -562,11 +571,9 @@ class ProductService extends TransactionBaseService {
|
|||||||
|
|
||||||
if (categories?.length) {
|
if (categories?.length) {
|
||||||
const categoryIds = categories.map((c) => c.id)
|
const categoryIds = categories.map((c) => c.id)
|
||||||
const categoryRecords = categoryIds.map(
|
product.categories = categoryIds.map(
|
||||||
(id) => ({ id } as ProductCategory)
|
(id) => ({ id } as ProductCategory)
|
||||||
)
|
)
|
||||||
|
|
||||||
product.categories = categoryRecords
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,6 +597,8 @@ class ProductService extends TransactionBaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises)
|
||||||
|
|
||||||
const result = await productRepo.save(product)
|
const result = await productRepo.save(product)
|
||||||
|
|
||||||
await this.eventBus_
|
await this.eventBus_
|
||||||
@@ -598,6 +607,7 @@ class ProductService extends TransactionBaseService {
|
|||||||
id: result.id,
|
id: result.id,
|
||||||
fields: Object.keys(update),
|
fields: Object.keys(update),
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,11 @@ import {
|
|||||||
ProductVariantService,
|
ProductVariantService,
|
||||||
RegionService,
|
RegionService,
|
||||||
SalesChannelService,
|
SalesChannelService,
|
||||||
ShippingProfileService
|
ShippingProfileService,
|
||||||
} from "../../../services"
|
} from "../../../services"
|
||||||
import CsvParser from "../../../services/csv-parser"
|
import CsvParser from "../../../services/csv-parser"
|
||||||
import { CreateProductInput } from "../../../types/product"
|
import { CreateProductInput } from "../../../types/product"
|
||||||
import {
|
import { CreateProductVariantInput } from "../../../types/product-variant"
|
||||||
CreateProductVariantInput,
|
|
||||||
UpdateProductVariantInput
|
|
||||||
} from "../../../types/product-variant"
|
|
||||||
import { FlagRouter } from "../../../utils/flag-router"
|
import { FlagRouter } from "../../../utils/flag-router"
|
||||||
import {
|
import {
|
||||||
OperationType,
|
OperationType,
|
||||||
@@ -27,11 +24,11 @@ import {
|
|||||||
ProductImportCsvSchema,
|
ProductImportCsvSchema,
|
||||||
ProductImportInjectedProps,
|
ProductImportInjectedProps,
|
||||||
ProductImportJobContext,
|
ProductImportJobContext,
|
||||||
TParsedProductImportRowData
|
TParsedProductImportRowData,
|
||||||
} from "./types"
|
} from "./types"
|
||||||
import {
|
import {
|
||||||
productImportColumnsDefinition,
|
productImportColumnsDefinition,
|
||||||
productImportSalesChannelsColumnsDefinition
|
productImportSalesChannelsColumnsDefinition,
|
||||||
} from "./types/columns-definition"
|
} from "./types/columns-definition"
|
||||||
import { transformProductData, transformVariantData } from "./utils"
|
import { transformProductData, transformVariantData } from "./utils"
|
||||||
|
|
||||||
@@ -584,12 +581,13 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
|
|||||||
|
|
||||||
await this.prepareVariantOptions(variantOp, product.id)
|
await this.prepareVariantOptions(variantOp, product.id)
|
||||||
|
|
||||||
|
const updateData = transformVariantData(variantOp)
|
||||||
|
delete updateData.product
|
||||||
|
delete updateData["product.handle"]
|
||||||
|
|
||||||
await this.productVariantService_
|
await this.productVariantService_
|
||||||
.withTransaction(transactionManager)
|
.withTransaction(transactionManager)
|
||||||
.update(
|
.update(variantOp["variant.id"] as string, updateData)
|
||||||
variantOp["variant.id"] as string,
|
|
||||||
transformVariantData(variantOp) as UpdateProductVariantInput
|
|
||||||
)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ProductImportStrategy.throwDescriptiveError(variantOp, e.message)
|
ProductImportStrategy.throwDescriptiveError(variantOp, e.message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ import {
|
|||||||
DateComparisonOperator,
|
DateComparisonOperator,
|
||||||
NumericalComparisonOperator,
|
NumericalComparisonOperator,
|
||||||
StringComparisonOperator,
|
StringComparisonOperator,
|
||||||
|
WithRequiredProperty,
|
||||||
} from "./common"
|
} from "./common"
|
||||||
import { XorConstraint } from "./validators/xor"
|
import { XorConstraint } from "./validators/xor"
|
||||||
|
import { ProductVariant } from "../models"
|
||||||
|
|
||||||
export type ProductVariantPrice = {
|
export type ProductVariantPrice = {
|
||||||
id?: string
|
id?: string
|
||||||
@@ -84,6 +86,30 @@ export type UpdateProductVariantInput = {
|
|||||||
metadata?: Record<string, unknown>
|
metadata?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UpdateProductVariantData = {
|
||||||
|
variant: ProductVariant
|
||||||
|
updateData: UpdateProductVariantInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateVariantPricesData = {
|
||||||
|
variantId: string
|
||||||
|
prices: ProductVariantPrice[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateVariantRegionPriceData = {
|
||||||
|
variantId: string
|
||||||
|
price: {
|
||||||
|
currency_code: string
|
||||||
|
region_id: string
|
||||||
|
amount: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UpdateVariantCurrencyPriceData = {
|
||||||
|
variantId: string
|
||||||
|
price: WithRequiredProperty<ProductVariantPrice, "currency_code">
|
||||||
|
}
|
||||||
|
|
||||||
export class FilterableProductVariantProps {
|
export class FilterableProductVariantProps {
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@IsType([String, [String], StringComparisonOperator])
|
@IsType([String, [String], StringComparisonOperator])
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { hasChanges } from "../has-changes"
|
||||||
|
|
||||||
|
describe("hasChanges", function () {
|
||||||
|
it("should return true the data differ and false otherwise", () => {
|
||||||
|
const objToCompareTo = {
|
||||||
|
prop1: "test",
|
||||||
|
prop2: "test",
|
||||||
|
prop3: "test",
|
||||||
|
prop4: {
|
||||||
|
prop4_1: "test",
|
||||||
|
prop4_2: "test",
|
||||||
|
prop4_3: "test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const obj = {
|
||||||
|
prop1: "test",
|
||||||
|
prop2: "test",
|
||||||
|
prop3: "test",
|
||||||
|
prop4: {
|
||||||
|
prop4_1: "test",
|
||||||
|
prop4_2: "test",
|
||||||
|
prop4_3: "test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = hasChanges(objToCompareTo, obj)
|
||||||
|
expect(res).toBeFalsy()
|
||||||
|
|
||||||
|
const obj2 = {
|
||||||
|
...obj,
|
||||||
|
prop3: "tes",
|
||||||
|
}
|
||||||
|
|
||||||
|
res = hasChanges(objToCompareTo, obj2)
|
||||||
|
expect(res).toBeTruthy()
|
||||||
|
|
||||||
|
const obj3 = {
|
||||||
|
...obj,
|
||||||
|
prop4: {
|
||||||
|
prop4_1: "",
|
||||||
|
prop4_2: "test",
|
||||||
|
prop4_3: "test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res = hasChanges(objToCompareTo, obj3)
|
||||||
|
expect(res).toBeTruthy()
|
||||||
|
|
||||||
|
const obj4 = {
|
||||||
|
...obj,
|
||||||
|
}
|
||||||
|
/* @ts-ignore */
|
||||||
|
delete obj4.prop4
|
||||||
|
|
||||||
|
res = hasChanges(objToCompareTo, obj4)
|
||||||
|
expect(res).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { isObject } from "./is-object"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare two objects and return true if there is changes detected from obj2 compared to obj1
|
||||||
|
* @param obj1
|
||||||
|
* @param obj2
|
||||||
|
*/
|
||||||
|
export function hasChanges<T1 extends Object, T2 extends Object>(
|
||||||
|
obj1: T1,
|
||||||
|
obj2: T2
|
||||||
|
): boolean {
|
||||||
|
for (const [key, value] of Object.entries(obj2)) {
|
||||||
|
if (isObject(obj1[key])) {
|
||||||
|
return hasChanges(obj1[key], value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj1[key] !== value) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -8,4 +8,5 @@ export * from "./calculate-price-tax-amount"
|
|||||||
export * from "./csv-cell-content-formatter"
|
export * from "./csv-cell-content-formatter"
|
||||||
export * from "./exception-formatter"
|
export * from "./exception-formatter"
|
||||||
export * from "./db-aware-column"
|
export * from "./db-aware-column"
|
||||||
export * from "./row-sql-results-to-entity-transformer"
|
export * from "./is-object"
|
||||||
|
export * from "./has-changes"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export function isObject(obj: unknown): obj is object {
|
||||||
|
return typeof obj === "object" && !!obj
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { RelationIdLoader } from "typeorm/query-builder/relation-id/RelationIdLoader"
|
|
||||||
import { RawSqlResultsToEntityTransformer } from "typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer"
|
|
||||||
import { QueryBuilder, QueryRunner } from "typeorm"
|
|
||||||
|
|
||||||
export async function rowSqlResultsToEntityTransformer<T>(
|
|
||||||
rows: any[],
|
|
||||||
queryBuilder: QueryBuilder<T>,
|
|
||||||
queryRunner: QueryRunner
|
|
||||||
): Promise<T[]> {
|
|
||||||
const relationIdLoader = new RelationIdLoader(
|
|
||||||
queryBuilder.connection,
|
|
||||||
queryRunner,
|
|
||||||
queryBuilder.expressionMap.relationIdAttributes
|
|
||||||
)
|
|
||||||
const transformer = new RawSqlResultsToEntityTransformer(
|
|
||||||
queryBuilder.expressionMap,
|
|
||||||
queryBuilder.connection.driver,
|
|
||||||
[],
|
|
||||||
[],
|
|
||||||
queryRunner
|
|
||||||
)
|
|
||||||
|
|
||||||
return transformer.transform(
|
|
||||||
rows,
|
|
||||||
queryBuilder.expressionMap.mainAlias!
|
|
||||||
) as T[]
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user