chore: Merge master to develop and manage conflict (#3570)

This commit is contained in:
Adrien de Peretti
2023-03-29 10:11:32 +02:00
committed by GitHub
parent 0695ff642b
commit 7f120e576b
25 changed files with 1033 additions and 383 deletions
@@ -183,7 +183,7 @@ describe("Product import batch job", () => {
ean: null,
upc: null,
inventory_quantity: 10,
prices: [
prices: expect.arrayContaining([
expect.objectContaining({
currency_code: "eur",
amount: 100,
@@ -198,7 +198,7 @@ describe("Product import batch job", () => {
amount: 130,
region_id: "region-product-import-1",
}),
],
]),
options: expect.arrayContaining([
expect.objectContaining({
value: "option 1 value red",
@@ -210,24 +210,24 @@ describe("Product import batch job", () => {
}),
],
type: null,
images: [
images: expect.arrayContaining([
expect.objectContaining({
url: "test-image.png",
}),
],
options: [
]),
options: expect.arrayContaining([
expect.objectContaining({
title: "test-option-1",
}),
expect.objectContaining({
title: "test-option-2",
}),
],
tags: [
]),
tags: expect.arrayContaining([
expect.objectContaining({
value: "123_1",
}),
],
]),
collection: expect.objectContaining({
handle: collectionHandle1,
}),
@@ -249,7 +249,7 @@ describe("Product import batch job", () => {
ean: null,
upc: null,
inventory_quantity: 10,
prices: [
prices: expect.arrayContaining([
expect.objectContaining({
currency_code: "eur",
amount: 100,
@@ -264,7 +264,7 @@ describe("Product import batch job", () => {
amount: 130,
region_id: "region-product-import-1",
}),
],
]),
options: expect.arrayContaining([
expect.objectContaining({
value: "option 1 value red",
@@ -276,19 +276,19 @@ describe("Product import batch job", () => {
}),
],
type: null,
images: [
images: expect.arrayContaining([
expect.objectContaining({
url: "test-image.png",
}),
],
options: [
]),
options: expect.arrayContaining([
expect.objectContaining({
title: "test-option-1",
}),
expect.objectContaining({
title: "test-option-2",
}),
],
]),
tags: [],
collection: expect.objectContaining({
handle: collectionHandle1,
@@ -1,9 +1,22 @@
export default {
connection: {
getMetadata: (target) => {
return target["metadata"] ?? {
columns: []
}
}
},
getRepository: function (repo) {
return repo
},
withRepository: function (repo) {
if (repo) {
return Object.assign(repo, { manager: this })
}
return repo
},
@@ -14,6 +14,7 @@ class MockRepo {
del,
count,
insertBulk,
metadata
}) {
this.create_ = create
this.update_ = update
@@ -28,6 +29,10 @@ class MockRepo {
this.findAndCount_ = findAndCount
this.findOneWithRelations_ = findOneWithRelations
this.insertBulk_ = insertBulk
this.metadata = metadata ?? {
columns: []
}
}
setFindOne(fn) {
@@ -83,11 +88,22 @@ class MockRepo {
return this.findDescendantsTree_(...args)
}
})
findOneOrFail = jest.fn().mockImplementation((...args) => {
if (this.findOneOrFail_) {
return this.findOneOrFail_(...args)
}
})
find = jest.fn().mockImplementation((...args) => {
if (this.find_) {
return this.find_(...args)
}
})
softRemove = jest.fn().mockImplementation((...args) => {
if (this.softRemove_) {
return this.softRemove_(...args)
}
})
save = jest.fn().mockImplementation((...args) => {
if (this.save_) {
return this.save_(...args)
@@ -107,6 +123,7 @@ class MockRepo {
}
return {}
})
delete = jest.fn().mockImplementation((...args) => {
if (this.delete_) {
return this.delete_(...args)
@@ -17,7 +17,7 @@ describe("POST /admin/products/:id", () => {
description: "Updated test description",
handle: "handle",
variants: [
{ id: IdMap.getId("variant_1"), title: "Green" },
{ id: IdMap.getId("testVariant"), title: "Green" },
{ title: "Blue" },
{ title: "Yellow" },
],
@@ -48,7 +48,6 @@ describe("POST /admin/products/:id", () => {
})
it("successfully updates variants and create new ones", async () => {
expect(ProductVariantServiceMock.delete).toHaveBeenCalledTimes(2)
expect(ProductVariantServiceMock.update).toHaveBeenCalledTimes(1)
expect(ProductVariantServiceMock.create).toHaveBeenCalledTimes(2)
})
@@ -73,7 +72,7 @@ describe("POST /admin/products/:id", () => {
)
expect(subject.status).toEqual(404)
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"}`
)
})
})
@@ -17,7 +17,7 @@ import {
import { EntityManager } from "typeorm"
import { defaultAdminProductFields, defaultAdminProductRelations } from "."
import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels"
import { ProductStatus } from "../../../../models"
import { ProductStatus, ProductVariant } from "../../../../models"
import {
PricingService,
ProductService,
@@ -34,10 +34,12 @@ import {
import {
CreateProductVariantInput,
ProductVariantPricesUpdateReq,
UpdateProductVariantInput,
} from "../../../../types/product-variant"
import { FeatureFlagDecorators } from "../../../../utils/feature-flag-decorators"
import { DistributedTransaction } from "../../../../utils/transaction"
import { validator } from "../../../../utils/validator"
import { ProductVariantRepository } from "../../../../repositories/product-variant"
import {
createVariantTransaction,
revertVariantTransaction,
@@ -112,6 +114,9 @@ export default async (req, res) => {
const validated = await validator(AdminPostProductsProductReq, req.body)
const logger: Logger = req.scope.resolve("logger")
const productVariantRepo: typeof ProductVariantRepository = req.scope.resolve(
"productVariantRepository"
)
const productService: ProductService = req.scope.resolve("productService")
const pricingService: PricingService = req.scope.resolve("pricingService")
const productVariantService: ProductVariantService = req.scope.resolve(
@@ -124,31 +129,74 @@ export default async (req, res) => {
const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => {
const productServiceTx = productService.withTransaction(transactionManager)
const { variants } = validated
delete validated.variants
await productService
.withTransaction(transactionManager)
.update(id, validated)
const product = await productServiceTx.update(id, validated)
if (!variants) {
return
}
const product = await productService
const variantRepo = manager.withRepository(productVariantRepo)
const productVariants = await productVariantService
.withTransaction(transactionManager)
.retrieve(id, {
relations: ["variants"],
})
// Iterate product variants and update their properties accordingly
for (const variant of product.variants) {
const exists = variants.find((v) => v.id && variant.id === v.id)
if (!exists) {
await productVariantService
.withTransaction(transactionManager)
.delete(variant.id)
.list(
{ product_id: id },
{
select: variantRepo.metadata.columns.map(
(c) => c.propertyName
) as (keyof ProductVariant)[],
}
)
const productVariantMap = new Map(productVariants.map((v) => [v.id, v]))
const variantWithIdSet = new Set()
const variantIdsNotBelongingToProduct: string[] = []
const variantsToUpdate: {
variant: ProductVariant
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[] = []
@@ -159,44 +207,32 @@ export default async (req, res) => {
productVariantService,
}
for (const [index, newVariant] of variants.entries()) {
const variantRank = index
const productVariantServiceTx =
productVariantService.withTransaction(transactionManager)
if (newVariant.id) {
const variant = product.variants.find((v) => v.id === newVariant.id)
if (!variant) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Variant with id: ${newVariant.id} is not associated with this product`
// Delete the variant that does not exist anymore from the provided variants
const variantIdsToDelete = [...productVariantMap.keys()].filter(
(variantId) => !variantWithIdSet.has(variantId)
)
if (variantIdsToDelete) {
await productVariantServiceTx.delete(variantIdsToDelete)
}
await productVariantService
.withTransaction(transactionManager)
.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
if (variantsToUpdate.length) {
await productVariantServiceTx.update(variantsToUpdate)
}
if (variantsToCreate.length) {
await Promise.all(
variantsToCreate.map(async (data) => {
try {
const input = {
...newVariant,
variant_rank: variantRank,
options: newVariant.options || [],
prices: newVariant.prices || [],
}
const varTransation = await createVariantTransaction(
const varTransaction = await createVariantTransaction(
transactionDependencies,
product.id,
input as CreateProductVariantInput
data as CreateProductVariantInput
)
allVariantTransactions.push(varTransation)
allVariantTransactions.push(varTransaction)
} catch (e) {
await Promise.all(
allVariantTransactions.map(async (transaction) => {
@@ -209,7 +245,8 @@ export default async (req, res) => {
throw e
}
}
})
)
}
})
+33 -5
View File
@@ -1,8 +1,3 @@
import {
moduleHelper,
moduleLoader,
registerModules,
} from "@medusajs/modules-sdk"
import { asValue, createContainer } from "awilix"
import express from "express"
import jwt from "jsonwebtoken"
@@ -15,6 +10,13 @@ import featureFlagLoader, { featureFlagRouter } from "../loaders/feature-flags"
import passportLoader from "../loaders/passport"
import servicesLoader from "../loaders/services"
import strategiesLoader from "../loaders/strategies"
import {
moduleHelper,
moduleLoader,
registerModules,
} from "@medusajs/modules-sdk"
import repositories from "../loaders/repositories"
import models from "../loaders/models"
const adminSessionOpts = {
cookieName: "session",
@@ -40,8 +42,32 @@ const config = {
const testApp = express()
function asArray(resolvers) {
return {
resolve: (container) =>
resolvers.map((resolver) => container.build(resolver)),
}
}
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("modulesHelper", asValue(moduleHelper))
container.register("configModule", asValue(config))
@@ -66,6 +92,8 @@ testApp.use((req, res, next) => {
})
featureFlagLoader(config)
models({ container, configModule: config, isTest: true })
repositories({ container, isTest: true })
servicesLoader({ container, configModule: config })
strategiesLoader({ container, configModule: config })
passportLoader({ app: testApp, container, configModule: config })
+3 -3
View File
@@ -3,16 +3,16 @@ import glob from "glob"
import path from "path"
import { ClassConstructor, MedusaContainer } from "../types/global"
import { EntitySchema } from "typeorm"
import { asClass, asValue, AwilixContainer } from "awilix"
import { asClass, asValue } from "awilix"
/**
* Registers all models in the model directory
*/
export default (
{ container }: { container: MedusaContainer },
{ container, isTest }: { container: MedusaContainer; isTest?: boolean },
config = { register: true }
) => {
const corePath = "../models/*.js"
const corePath = isTest ? "../models/*.ts" : "../models/*.js"
const coreFull = path.join(__dirname, corePath)
const models: (ClassConstructor<unknown> | EntitySchema)[] = []
+8 -2
View File
@@ -8,8 +8,14 @@ import { asValue } from "awilix"
/**
* Registers all models in the model directory
*/
export default ({ container }: { container: MedusaContainer }): void => {
const corePath = "../repositories/*.js"
export default ({
container,
isTest,
}: {
container: MedusaContainer
isTest?: boolean
}): void => {
const corePath = isTest ? "../repositories/*.ts" : "../repositories/*.js"
const coreFull = path.join(__dirname, corePath)
const core = glob.sync(coreFull, { cwd: __dirname })
+26 -3
View File
@@ -1,8 +1,26 @@
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"
import { Image } from "../models"
import { dataSource } from "../loaders/database"
import { In } from "typeorm"
export const ImageRepository = dataSource.getRepository(Image).extend({
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))
},
async upsertImages(imageUrls: string[]) {
const existingImages = await this.find({
where: {
@@ -14,16 +32,21 @@ export const ImageRepository = dataSource.getRepository(Image).extend({
)
const upsertedImgs: Image[] = []
const imageToCreate: QueryDeepPartialEntity<Image>[] = []
for (const url of imageUrls) {
imageUrls.forEach((url) => {
const aImg = existingImagesMap.get(url)
if (aImg) {
upsertedImgs.push(aImg)
} else {
const newImg = this.create({ url })
const savedImg = await this.save(newImg)
upsertedImgs.push(savedImg)
imageToCreate.push(newImg as QueryDeepPartialEntity<Image>)
}
})
if (imageToCreate.length) {
const newImgs = await this.insertBulk(imageToCreate)
upsertedImgs.push(...newImgs)
}
return upsertedImgs
@@ -7,13 +7,15 @@ import {
ObjectLiteral,
WhereExpressionBuilder,
} from "typeorm"
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"
import { MoneyAmount } from "../models"
import {
PriceListPriceCreateInput,
PriceListPriceUpdateInput,
} from "../types/price-list"
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"
import { dataSource } from "../loaders/database"
import { ProductVariantPrice } from "../types/product-variant"
import { isString } from "../utils"
type Price = Partial<
Omit<MoneyAmount, "created_at" | "updated_at" | "deleted_at">
@@ -24,6 +26,31 @@ type Price = Partial<
export const MoneyAmountRepository = dataSource
.getRepository(MoneyAmount)
.extend({
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.
* Use `deleteVariantPricesNotIn` instead.
* @deprecated
*/
async findVariantPricesNotIn(
variantId: string,
prices: Price[]
@@ -44,6 +71,63 @@ export const MoneyAmountRepository = dataSource
return pricesNotInPricesPayload
},
async deleteVariantPricesNotIn(
variantIdOrData:
| string
| { variantId: string; prices: ProductVariantPrice[] }[],
prices?: Price[]
): Promise<void> {
const data = isString(variantIdOrData)
? [
{
variantId: variantIdOrData,
prices: prices!,
},
]
: variantIdOrData
const queryBuilder = this.createQueryBuilder().delete()
for (const data_ of data) {
const where = {
variant_id: data_.variantId,
price_list_id: IsNull(),
}
const orWhere: ObjectLiteral[] = []
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 queryBuilder.execute()
},
async upsertVariantCurrencyPrice(
variantId: string,
price: Price
@@ -70,44 +154,6 @@ export const MoneyAmountRepository = dataSource
return await this.save(moneyAmount)
},
async deleteVariantPricesNotIn(
variantId: string,
prices: Price[]
): Promise<void> {
const where = {
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),
},
{
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),
})
}
}
await this.createQueryBuilder()
.delete()
.where(where)
.andWhere(orWhere)
.execute()
},
async addPriceListPrices(
priceListId: string,
prices: PriceListPriceCreateInput[],
@@ -12,9 +12,7 @@ export const StagedJobRepository = dataSource.getRepository(StagedJob).extend({
// TODO: remove if statement once this issue is resolved https://github.com/typeorm/typeorm/issues/9850
if (!queryBuilder.connection.driver.isReturningSqlSupported("insert")) {
const rawStagedJobs = await queryBuilder.execute()
return rawStagedJobs.generatedMaps.map((d) =>
this.create(d)
) as StagedJob[]
return rawStagedJobs.generatedMaps.map((d) => this.create(d))
}
const rawStagedJobs = await queryBuilder.returning("*").execute()
@@ -276,6 +276,10 @@ describe("ProductVariantService", () => {
const productVariantRepository = MockRepository({
findOne: (query) => Promise.resolve({ id: IdMap.getId("ironman") }),
update: (data) => ({
generatedMaps: [data],
}),
create: (data) => data,
})
const moneyAmountRepository = MockRepository({
@@ -315,22 +319,56 @@ describe("ProductVariantService", () => {
})
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
expect(eventBusService.emit).toHaveBeenCalledWith(
"product-variant.updated",
expect(eventBusService.emit).toHaveBeenCalledWith([
{
eventName: "product-variant.updated",
data: {
id: IdMap.getId("ironman"),
fields: ["title"],
}
)
},
},
])
expect(productVariantRepository.save).toHaveBeenCalledTimes(1)
expect(productVariantRepository.save).toHaveBeenCalledWith({
expect(productVariantRepository.update).toHaveBeenCalledTimes(1)
expect(productVariantRepository.update).toHaveBeenCalledWith(
{ id: IdMap.getId("ironman") },
{
id: IdMap.getId("ironman"),
title: "new title",
})
}
)
})
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") },
{
id: IdMap.getId("ironman"),
title: "new title 2",
}
)
})
it("successfully avoid to update variant if the data have not changed", async () => {
await productVariantService.update(
{ id: IdMap.getId("ironman"), title: "new title" },
{
@@ -338,20 +376,9 @@ describe("ProductVariantService", () => {
}
)
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
expect(eventBusService.emit).toHaveBeenCalledWith(
"product-variant.updated",
{
id: IdMap.getId("ironman"),
fields: ["title"],
}
)
expect(eventBusService.emit).toHaveBeenCalledTimes(0)
expect(productVariantRepository.save).toHaveBeenCalledTimes(1)
expect(productVariantRepository.save).toHaveBeenCalledWith({
id: IdMap.getId("ironman"),
title: "new title",
})
expect(productVariantRepository.save).toHaveBeenCalledTimes(0)
})
it("throws if provided variant is missing an id", async () => {
@@ -376,22 +403,27 @@ describe("ProductVariantService", () => {
})
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
expect(eventBusService.emit).toHaveBeenCalledWith(
"product-variant.updated",
expect(eventBusService.emit).toHaveBeenCalledWith([
{
eventName: "product-variant.updated",
data: {
id: IdMap.getId("ironman"),
fields: ["title", "metadata"],
}
)
},
},
])
expect(productVariantRepository.save).toHaveBeenCalledTimes(1)
expect(productVariantRepository.save).toHaveBeenCalledWith({
expect(productVariantRepository.update).toHaveBeenCalledTimes(1)
expect(productVariantRepository.update).toHaveBeenCalledWith(
{ id: IdMap.getId("ironman") },
{
id: IdMap.getId("ironman"),
title: "new title",
metadata: {
testing: "this",
},
})
}
)
})
it("successfully updates variant inventory_quantity", async () => {
@@ -401,20 +433,25 @@ describe("ProductVariantService", () => {
})
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
expect(eventBusService.emit).toHaveBeenCalledWith(
"product-variant.updated",
expect(eventBusService.emit).toHaveBeenCalledWith([
{
eventName: "product-variant.updated",
data: {
id: IdMap.getId("ironman"),
fields: ["title", "inventory_quantity"],
}
)
},
},
])
expect(productVariantRepository.save).toHaveBeenCalledTimes(1)
expect(productVariantRepository.save).toHaveBeenCalledWith({
expect(productVariantRepository.update).toHaveBeenCalledTimes(1)
expect(productVariantRepository.update).toHaveBeenCalledWith(
{ id: IdMap.getId("ironman") },
{
id: IdMap.getId("ironman"),
inventory_quantity: 98,
title: "new title",
})
}
)
})
it("successfully updates variant prices", async () => {
@@ -429,17 +466,19 @@ describe("ProductVariantService", () => {
})
expect(productVariantService.updateVariantPrices).toHaveBeenCalledTimes(1)
expect(productVariantService.updateVariantPrices).toHaveBeenCalledWith(
IdMap.getId("ironman"),
[
expect(productVariantService.updateVariantPrices).toHaveBeenCalledWith([
{
variantId: IdMap.getId("ironman"),
prices: [
{
currency_code: "dkk",
amount: 1000,
},
]
)
],
},
])
expect(productVariantRepository.save).toHaveBeenCalledTimes(1)
expect(productVariantRepository.update).toHaveBeenCalledTimes(1)
})
it("successfully updates variant options", async () => {
@@ -460,7 +499,7 @@ describe("ProductVariantService", () => {
"red"
)
expect(productVariantRepository.save).toHaveBeenCalledTimes(1)
expect(productVariantRepository.update).toHaveBeenCalledTimes(1)
})
})
@@ -628,8 +667,22 @@ describe("ProductVariantService", () => {
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,
remove: () => Promise.resolve(),
insertBulk: (data) => data,
})
const oldPrices = [
@@ -691,7 +744,7 @@ describe("ProductVariantService", () => {
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", [
{
currency_code: "usd",
@@ -703,15 +756,14 @@ describe("ProductVariantService", () => {
moneyAmountRepository.deleteVariantPricesNotIn
).toHaveBeenCalledTimes(1)
expect(
moneyAmountRepository.upsertVariantCurrencyPrice
).toHaveBeenCalledTimes(1)
expect(
moneyAmountRepository.upsertVariantCurrencyPrice
).toHaveBeenCalledWith("ironman", {
expect(moneyAmountRepository.insertBulk).toHaveBeenCalledTimes(1)
expect(moneyAmountRepository.insertBulk).toHaveBeenCalledWith([
{
variant_id: "ironman",
currency_code: "usd",
amount: 4000,
})
},
])
})
it("successfully creates new a region price", async () => {
@@ -726,33 +778,31 @@ describe("ProductVariantService", () => {
expect(moneyAmountRepository.create).toHaveBeenCalledWith({
variant_id: IdMap.getId("ironman"),
region_id: IdMap.getId("cali"),
currency_code: "usd",
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"), [
{
id: IdMap.getId("dkk"),
currency_code: "dkk",
amount: 750,
amount: 850,
},
])
expect(moneyAmountRepository.create).toHaveBeenCalledTimes(0)
expect(
moneyAmountRepository.upsertVariantCurrencyPrice
).toHaveBeenCalledTimes(1)
expect(
moneyAmountRepository.upsertVariantCurrencyPrice
).toHaveBeenCalledWith(IdMap.getId("ironman"), {
id: IdMap.getId("dkk"),
currency_code: "dkk",
amount: 750,
})
expect(moneyAmountRepository.update).toHaveBeenCalledTimes(1)
expect(moneyAmountRepository.update).toHaveBeenCalledWith(
{ id: IdMap.getId("dkk") },
{
amount: 850,
}
)
})
})
@@ -889,14 +939,16 @@ describe("ProductVariantService", () => {
describe("delete", () => {
const productVariantRepository = MockRepository({
findOne: (query) => {
find: (query) => {
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"),
})
},
])
},
})
@@ -914,20 +966,22 @@ describe("ProductVariantService", () => {
await productVariantService.delete(IdMap.getId("ironman"))
expect(productVariantRepository.softRemove).toBeCalledTimes(1)
expect(productVariantRepository.softRemove).toBeCalledWith(
expect(productVariantRepository.softRemove).toBeCalledWith([
expect.objectContaining({
id: IdMap.getId("ironman"),
})
)
}),
])
expect(eventBusService.emit).toHaveBeenCalledTimes(1)
expect(eventBusService.emit).toHaveBeenCalledWith(
"product-variant.deleted",
expect(eventBusService.emit).toHaveBeenCalledWith([
{
eventName: "product-variant.deleted",
data: {
id: IdMap.getId("ironman"),
product_id: IdMap.getId("product-test"),
}
)
},
},
])
})
it("successfully resolves if variant does not exist", async () => {
+4 -6
View File
@@ -619,12 +619,9 @@ class DiscountService extends TransactionBaseService {
})
let fullItemPrice = lineItem.unit_price * lineItem.quantity
if (
this.featureFlagRouter_.isFeatureEnabled(
TaxInclusivePricingFeatureFlag.key
) &&
lineItem.includes_tax
) {
const includesTax = this.featureFlagRouter_.isFeatureEnabled(TaxInclusivePricingFeatureFlag.key) && lineItem.includes_tax
if (includesTax) {
const lineItemTotals = await this.newTotalsService_
.withTransaction(transactionManager)
.getLineItemTotals([lineItem], {
@@ -649,6 +646,7 @@ class DiscountService extends TransactionBaseService {
const totals = await this.newTotalsService_.getLineItemTotals(
discountedItems,
{
includeTax: includesTax,
calculationContext,
}
)
+1 -1
View File
@@ -156,7 +156,7 @@ export default class EventBusService
*
* If we are in a long-running transaction, the ACID properties of a
* transaction ensure, that events are kept invisible to the enqueuer
* until the trasaction has commited.
* until the transaction has committed.
*
* This patterns also gives us at-least-once delivery of events, as events
* are only removed from the database, if they are successfully delivered.
+408 -70
View File
@@ -7,6 +7,7 @@ import {
FindOptionsSelect,
FindOptionsWhere,
ILike,
In,
IsNull,
SelectQueryBuilder,
} from "typeorm"
@@ -25,22 +26,34 @@ import {
FindWithRelationsOptions,
ProductVariantRepository,
} from "../repositories/product-variant"
import { FindConfig, WithRequiredProperty } from "../types/common"
import {
CreateProductVariantInput,
FilterableProductVariantProps,
GetRegionPriceContext,
ProductVariantPrice,
UpdateProductVariantData,
UpdateProductVariantInput,
UpdateVariantCurrencyPriceData,
UpdateVariantPricesData,
UpdateVariantRegionPriceData,
} from "../types/product-variant"
import { buildQuery, buildRelations, setMetadata } from "../utils"
import {
buildQuery,
buildRelations,
hasChanges,
isObject,
isString,
setMetadata,
} from "../utils"
import { CartRepository } from "../repositories/cart"
import { MoneyAmountRepository } from "../repositories/money-amount"
import { ProductRepository } from "../repositories/product"
import { ProductOptionValueRepository } from "../repositories/product-option-value"
import { FindConfig } from "../types/common"
import EventBusService from "./event-bus"
import RegionService from "./region"
import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"
class ProductVariantService extends TransactionBaseService {
static Events = {
@@ -164,7 +177,7 @@ class ProductVariantService extends TransactionBaseService {
let product = productOrProductId
if (typeof product === `string`) {
if (isString(product)) {
product = (await productRepo.findOne({
where: { id: productOrProductId as string },
relations: buildRelations([
@@ -229,19 +242,12 @@ class ProductVariantService extends TransactionBaseService {
const result = await variantRepo.save(productVariant)
if (prices) {
for (const price of prices) {
if (price.region_id) {
const region = await this.regionService_.retrieve(price.region_id)
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.updateVariantPrices([
{
variantId: result.id,
prices,
},
])
}
await this.eventBus_
@@ -255,84 +261,185 @@ 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.
* Price updates should use dedicated methods.
* The function will throw, if price updates are attempted.
* @param variantOrVariantId - variant or id of a variant.
* @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.
*/
async update(
variantOrVariantId: string | Partial<ProductVariant>,
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) => {
const variantRepo = manager.withRepository(this.productVariantRepository_)
let variant = variantOrVariantId
if (typeof variant === `string`) {
const variantRes = await variantRepo.findOne({
where: { id: variantOrVariantId as string },
if (updateData) {
let variant: Partial<ProductVariant> | null =
variantOrVariantIdOrData as Partial<ProductVariant>
if (isString(variantOrVariantIdOrData)) {
variant = await this.retrieve(variantOrVariantIdOrData, {
select: variantRepo.metadata.columns.map(
(c) => c.propertyName
) as (keyof ProductVariant)[],
})
if (!isDefined(variantRes)) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Variant with id ${variantOrVariantId} was not found`
)
} else {
variant = variantRes as ProductVariant
}
} else if (!variant.id) {
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)
data = [{ variant: variant as ProductVariant, updateData: updateData }]
}
if (options) {
for (const option of options) {
const result = await this.updateBatch(data)
return (Array.isArray(variantOrVariantIdOrData)
? result
: result[0]) as unknown as TResult
})
}
protected async updateBatch(
variantData: UpdateProductVariantData[]
): Promise<ProductVariant[]> {
return await this.atomicPhase_(async (manager: EntityManager) => {
const variantRepo = manager.withRepository(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 (typeof metadata === "object") {
variant.metadata = setMetadata(variant as ProductVariant, metadata)
}
if (typeof inventory_quantity === "number") {
variant.inventory_quantity = inventory_quantity as number
}
if (Object.keys(rest).length) {
for (const [key, value] of Object.entries(rest)) {
variant[key] = value
if (variant[key] !== value) {
toUpdate[key] = value
}
}
}
const result = await variantRepo.save(variant)
let result = variant
await this.eventBus_
.withTransaction(manager)
.emit(ProductVariantService.Events.UPDATED, {
// No need to update if nothing on the variant has changed
if (shouldUpdate) {
const { id } = variant
const rawResult = await variantRepo.update(
{ id },
{ 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(update),
fields: Object.keys(updatedData),
},
}
})
return result
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.
* Deletes any prices that are not in the update object, and is not associated with a price list.
@@ -343,6 +450,25 @@ class ProductVariantService extends TransactionBaseService {
async updateVariantPrices(
variantId: string,
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> {
return await this.atomicPhase_(async (manager: EntityManager) => {
const moneyAmountRepo = manager.withRepository(
@@ -350,27 +476,230 @@ class ProductVariantService extends TransactionBaseService {
)
// 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(
{
id: [...regionIdsSet],
},
{
select: ["id", "currency_code"],
}
)
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 = await regionsServiceTx.retrieve(price.region_id)
await this.setRegionPrice(variantId, {
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 {
await this.setCurrencyPrice(variantId, price)
dataCurrencyPrices.push({
variantId,
price: {
...price,
currency_code: price.currency_code!,
},
})
}
})
})
const promises: Promise<any>[] = []
if (dataRegionPrices.length) {
promises.push(this.upsertRegionPrices(dataRegionPrices))
}
await this.priceSelectionStrategy_
if (dataCurrencyPrices.length) {
promises.push(this.upsertCurrencyPrices(dataCurrencyPrices))
}
await Promise.all(promises)
})
}
async upsertRegionPrices(
data: UpdateVariantRegionPriceData[]
): Promise<void> {
return await this.atomicPhase_(async (manager: EntityManager) => {
const moneyAmountRepo = manager.withRepository(
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))
})
}
if (dataToCreate.length || dataToUpdate.length) {
promises.push(
this.priceSelectionStrategy_
.withTransaction(manager)
.onVariantsPricesUpdate([variantId])
.onVariantsPricesUpdate(data.map((d) => d.variantId))
)
}
await Promise.all(promises)
})
}
async upsertCurrencyPrices(
data: {
variantId: string
price: WithRequiredProperty<ProductVariantPrice, "currency_code">
}[]
): Promise<void> {
return await this.atomicPhase_(async (manager: EntityManager) => {
const moneyAmountRepo = manager.withRepository(
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))
})
}
if (dataToCreate.length || dataToUpdate.length) {
promises.push(
this.priceSelectionStrategy_
.withTransaction(manager)
.onVariantsPricesUpdate(data.map((d) => d.variantId))
)
}
await Promise.all(promises)
})
}
@@ -406,6 +735,7 @@ class ProductVariantService extends TransactionBaseService {
}
/**
* @deprecated use addOrUpdateRegionPrices instead
* Sets the default price of a specific region
* @param variantId - the id of the variant to update
* @param price - the price for the variant.
@@ -442,6 +772,7 @@ class ProductVariantService extends TransactionBaseService {
}
/**
* @deprecated use addOrUpdateCurrencyPrices instead
* Sets the default price for the given currency.
* @param variantId - the id of the variant to set prices for
* @param price - the price for the variant
@@ -670,34 +1001,41 @@ class ProductVariantService extends TransactionBaseService {
}
/**
* Deletes variant.
* Deletes variant or variants.
* 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
* @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) => {
const variantRepo = manager.withRepository(this.productVariantRepository_)
const variant = await variantRepo.findOne({
where: { id: variantId },
const variants = await variantRepo.find({
where: { id: In(variantIds_) },
relations: ["prices", "options", "inventory_items"],
})
if (!variant) {
if (!variants.length) {
return Promise.resolve()
}
await variantRepo.softRemove(variant)
await variantRepo.softRemove(variants)
await this.eventBus_
.withTransaction(manager)
.emit(ProductVariantService.Events.DELETED, {
const events = variants.map((variant) => {
return {
eventName: ProductVariantService.Events.DELETED,
data: {
id: variant.id,
product_id: variant.product_id,
metadata: variant.metadata,
},
}
})
await this.eventBus_.withTransaction(manager).emit(events)
})
}
+20 -7
View File
@@ -517,20 +517,32 @@ class ProductService extends TransactionBaseService {
product.thumbnail = images[0]
}
const promises: Promise<any>[] = []
if (images) {
product.images = await imageRepo.upsertImages(images)
promises.push(
imageRepo
.upsertImages(images)
.then((image) => (product.images = image))
)
}
if (metadata) {
product.metadata = setMetadata(product, metadata)
}
if (typeof type !== `undefined`) {
product.type_id = (await productTypeRepo.upsertType(type))?.id || null
if (isDefined(type)) {
promises.push(
productTypeRepo
.upsertType(type)
.then((type) => (product.type_id = type?.id ?? null))
)
}
if (tags) {
product.tags = await productTagRepo.upsertTags(tags)
promises.push(
productTagRepo.upsertTags(tags).then((tags) => (product.tags = tags))
)
}
if (isDefined(categories)) {
@@ -538,11 +550,9 @@ class ProductService extends TransactionBaseService {
if (categories?.length) {
const categoryIds = categories.map((c) => c.id)
const categoryRecords = categoryIds.map(
product.categories = categoryIds.map(
(id) => ({ id } as ProductCategory)
)
product.categories = categoryRecords
}
}
@@ -566,6 +576,8 @@ class ProductService extends TransactionBaseService {
}
}
await Promise.all(promises)
const result = await productRepo.save(product)
await this.eventBus_
@@ -574,6 +586,7 @@ class ProductService extends TransactionBaseService {
id: result.id,
fields: Object.keys(update),
})
return result
})
}
@@ -12,14 +12,11 @@ import {
ProductVariantService,
RegionService,
SalesChannelService,
ShippingProfileService
ShippingProfileService,
} from "../../../services"
import CsvParser from "../../../services/csv-parser"
import { CreateProductInput } from "../../../types/product"
import {
CreateProductVariantInput,
UpdateProductVariantInput
} from "../../../types/product-variant"
import { CreateProductVariantInput } from "../../../types/product-variant"
import { FlagRouter } from "../../../utils/flag-router"
import {
OperationType,
@@ -27,11 +24,11 @@ import {
ProductImportCsvSchema,
ProductImportInjectedProps,
ProductImportJobContext,
TParsedProductImportRowData
TParsedProductImportRowData,
} from "./types"
import {
productImportColumnsDefinition,
productImportSalesChannelsColumnsDefinition
productImportSalesChannelsColumnsDefinition,
} from "./types/columns-definition"
import { transformProductData, transformVariantData } from "./utils"
@@ -584,12 +581,13 @@ class ProductImportStrategy extends AbstractBatchJobStrategy {
await this.prepareVariantOptions(variantOp, product.id)
const updateData = transformVariantData(variantOp)
delete updateData.product
delete updateData["product.handle"]
await this.productVariantService_
.withTransaction(transactionManager)
.update(
variantOp["variant.id"] as string,
transformVariantData(variantOp) as UpdateProductVariantInput
)
.update(variantOp["variant.id"] as string, updateData)
} catch (e) {
ProductImportStrategy.throwDescriptiveError(variantOp, e.message)
}
@@ -12,8 +12,10 @@ import {
DateComparisonOperator,
NumericalComparisonOperator,
StringComparisonOperator,
WithRequiredProperty,
} from "./common"
import { XorConstraint } from "./validators/xor"
import { ProductVariant } from "../models"
export type ProductVariantPrice = {
id?: string
@@ -84,6 +86,30 @@ export type UpdateProductVariantInput = {
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 {
@ValidateNested()
@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()
})
})
+23
View File
@@ -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
}
+2 -1
View File
@@ -9,6 +9,7 @@ export * from "./is-object"
export * from "./is-string"
export * from "./product-category"
export * from "./remove-undefined-properties"
export * from "./row-sql-results-to-entity-transformer"
export * from "./set-metadata"
export * from "./validate-id"
export * from "./is-object"
export * from "./has-changes"
@@ -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[]
}