diff --git a/packages/core/types/src/dal/repository-service.ts b/packages/core/types/src/dal/repository-service.ts index 55d4a575c4..b1d5e9ef5a 100644 --- a/packages/core/types/src/dal/repository-service.ts +++ b/packages/core/types/src/dal/repository-service.ts @@ -8,6 +8,15 @@ import { UpsertWithReplaceConfig, } from "./index" +type EntityClassName = string +type EntityValues = { id: string }[] + +export type PerformedActions = { + created: Record + updated: Record + deleted: Record +} + /** * Data access layer (DAL) interface to implements for any repository service. * This layer helps to separate the business logic (service layer) from accessing the @@ -78,7 +87,7 @@ export interface RepositoryService extends BaseRepositoryService { data: any[], config?: UpsertWithReplaceConfig, context?: Context - ): Promise + ): Promise<{ entities: T[]; performedActions: PerformedActions }> } export interface TreeRepositoryService diff --git a/packages/core/types/src/modules-sdk/internal-module-service.ts b/packages/core/types/src/modules-sdk/internal-module-service.ts index 784dea01fe..a16955c388 100644 --- a/packages/core/types/src/modules-sdk/internal-module-service.ts +++ b/packages/core/types/src/modules-sdk/internal-module-service.ts @@ -4,6 +4,7 @@ import { BaseFilterable, FilterQuery as InternalFilterQuery, FilterQuery, + PerformedActions, UpsertWithReplaceConfig, } from "../dal" @@ -88,5 +89,5 @@ export interface InternalModuleService< data: any[], config?: UpsertWithReplaceConfig, sharedContext?: Context - ): Promise + ): Promise<{ entities: TEntity[]; performedActions: PerformedActions }> } diff --git a/packages/core/utils/src/dal/mikro-orm/integration-tests/__tests__/mikro-orm-repository.spec.ts b/packages/core/utils/src/dal/mikro-orm/integration-tests/__tests__/mikro-orm-repository.spec.ts index 20ff7e3d0b..4c2b6d776e 100644 --- a/packages/core/utils/src/dal/mikro-orm/integration-tests/__tests__/mikro-orm-repository.spec.ts +++ b/packages/core/utils/src/dal/mikro-orm/integration-tests/__tests__/mikro-orm-repository.spec.ts @@ -176,7 +176,17 @@ describe("mikroOrmRepository", () => { it("should successfully create a flat entity", async () => { const entity1 = { id: "1", title: "en1", amount: 100 } - const resp = await manager1().upsertWithReplace([entity1]) + const { entities: resp, performedActions } = + await manager1().upsertWithReplace([entity1]) + + expect(performedActions).toEqual({ + created: { + [Entity1.name]: [expect.objectContaining({ id: entity1.id })], + }, + updated: {}, + deleted: {}, + }) + const listedEntities = await manager1().find() expect(listedEntities).toHaveLength(1) @@ -193,9 +203,29 @@ describe("mikroOrmRepository", () => { it("should successfully update a flat entity", async () => { const entity1 = { id: "1", title: "en1" } - await manager1().upsertWithReplace([entity1]) + const { performedActions: performedActions1 } = + await manager1().upsertWithReplace([entity1]) + + expect(performedActions1).toEqual({ + created: { + [Entity1.name]: [expect.objectContaining({ id: entity1.id })], + }, + updated: {}, + deleted: {}, + }) + entity1.title = "newen1" - await manager1().upsertWithReplace([entity1]) + const { performedActions: performedActions2 } = + await manager1().upsertWithReplace([entity1]) + + expect(performedActions2).toEqual({ + created: {}, + updated: { + [Entity1.name]: [expect.objectContaining({ id: entity1.id })], + }, + deleted: {}, + }) + const listedEntities = await manager1().find() expect(listedEntities).toHaveLength(1) @@ -210,9 +240,30 @@ describe("mikroOrmRepository", () => { it("should successfully do a partial update a flat entity", async () => { const entity1 = { id: "1", title: "en1" } - await manager1().upsertWithReplace([entity1]) + const { performedActions: performedActions1 } = + await manager1().upsertWithReplace([entity1]) + + expect(performedActions1).toEqual({ + created: { + [Entity1.name]: [expect.objectContaining({ id: entity1.id })], + }, + updated: {}, + deleted: {}, + }) + entity1.title = undefined as any - await manager1().upsertWithReplace([entity1]) + + const { performedActions: performedActions2 } = + await manager1().upsertWithReplace([entity1]) + + expect(performedActions2).toEqual({ + created: {}, + updated: { + [Entity1.name]: [expect.objectContaining({ id: entity1.id })], + }, + deleted: {}, + }) + const listedEntities = await manager1().find() expect(listedEntities).toHaveLength(1) @@ -240,6 +291,7 @@ describe("mikroOrmRepository", () => { ) }) + // TODO: I believe this should not be allowed it("should successfully create the parent entity of a many-to-one", async () => { const entity2 = { id: "2", @@ -343,9 +395,19 @@ describe("mikroOrmRepository", () => { entity2: [{ title: "en2-1" }, { title: "en2-2" }], } - await manager1().upsertWithReplace([entity1], { - relations: [], + const { performedActions: performedActions1 } = + await manager1().upsertWithReplace([entity1], { + relations: [], + }) + + expect(performedActions1).toEqual({ + created: { + [Entity1.name]: [expect.objectContaining({ id: entity1.id })], + }, + updated: {}, + deleted: {}, }) + const listedEntities = await manager1().find({ where: { id: "1" }, options: { populate: ["entity2"] }, @@ -368,14 +430,29 @@ describe("mikroOrmRepository", () => { entity2: [{ title: "en2-1" }, { title: "en2-2" }], } - await manager1().upsertWithReplace([entity1], { - relations: ["entity2"], - }) + const { performedActions: performedActions1 } = + await manager1().upsertWithReplace([entity1], { + relations: ["entity2"], + }) + const listedEntities = await manager1().find({ where: { id: "1" }, options: { populate: ["entity2"] }, }) + const entities2 = listedEntities.flatMap((entity1) => + entity1.entity2.getItems() + ) + + expect(performedActions1).toEqual({ + created: { + [Entity1.name]: [expect.objectContaining({ id: entity1.id })], + [Entity2.name]: entities2.map((entity2) => ({ id: entity2.id })), + }, + updated: {}, + deleted: {}, + }) + expect(listedEntities).toHaveLength(1) expect(listedEntities[0]).toEqual( expect.objectContaining({ @@ -403,14 +480,26 @@ describe("mikroOrmRepository", () => { entity2: [{ title: "en2-1", entity1: null }], } - await manager1().upsertWithReplace([entity1], { - relations: ["entity2"], - }) + const { performedActions: performedActions1 } = + await manager1().upsertWithReplace([entity1], { + relations: ["entity2"], + }) const listedEntities = await manager1().find({ where: { id: "1" }, options: { populate: ["entity2"] }, }) + expect(performedActions1).toEqual({ + created: { + [Entity1.name]: [expect.objectContaining({ id: entity1.id })], + [Entity2.name]: [ + expect.objectContaining({ id: listedEntities[0].entity2[0].id }), + ], + }, + updated: {}, + deleted: {}, + }) + expect(listedEntities).toHaveLength(1) expect(listedEntities[0]).toEqual( expect.objectContaining({ @@ -435,12 +524,35 @@ describe("mikroOrmRepository", () => { entity2: [{ title: "en2-1" }, { title: "en2-2" }], } - await manager1().upsertWithReplace([entity1], { - relations: ["entity2"], + const { entities: entities1, performedActions: performedActions1 } = + await manager1().upsertWithReplace([entity1], { + relations: ["entity2"], + }) + + expect(performedActions1).toEqual({ + created: { + [Entity1.name]: [expect.objectContaining({ id: entity1.id })], + [Entity2.name]: entities1[0].entity2.map((entity2) => + expect.objectContaining({ id: entity2.id }) + ), + }, + updated: {}, + deleted: {}, }) + entity1.entity2.push({ title: "en2-3" }) - await manager1().upsertWithReplace([entity1], { - relations: [], + + const { performedActions: performedActions2 } = + await manager1().upsertWithReplace([entity1], { + relations: [], + }) + + expect(performedActions2).toEqual({ + created: {}, + updated: { + [Entity1.name]: [expect.objectContaining({ id: entity1.id })], + }, + deleted: {}, }) const listedEntities = await manager1().find({ @@ -478,14 +590,42 @@ describe("mikroOrmRepository", () => { ] as any[], } - await manager1().upsertWithReplace([entity1], { - relations: ["entity2"], + const { entities: entities1, performedActions: performedActions1 } = + await manager1().upsertWithReplace([entity1], { + relations: ["entity2"], + }) + + expect(performedActions1).toEqual({ + created: { + [Entity1.name]: [expect.objectContaining({ id: entity1.id })], + [Entity2.name]: entities1[0].entity2.map((entity2) => + expect.objectContaining({ id: entity2.id }) + ), + }, + updated: {}, + deleted: {}, }) entity1.entity2 = [{ id: "2", title: "newen2-1" }, { title: "en2-3" }] - await manager1().upsertWithReplace([entity1], { - relations: ["entity2"], + const { entities: entities2, performedActions: performedActions2 } = + await manager1().upsertWithReplace([entity1], { + relations: ["entity2"], + }) + + const entity2En23 = entities2[0].entity2.find((e) => e.title === "en2-3")! + + expect(performedActions2).toEqual({ + created: { + [Entity2.name]: [expect.objectContaining({ id: entity2En23.id })], + }, + updated: { + [Entity1.name]: [expect.objectContaining({ id: entity1.id })], + [Entity2.name]: [expect.objectContaining({ id: "2" })], + }, + deleted: { + [Entity2.name]: [expect.objectContaining({ id: "3" })], + }, }) const listedEntities = await manager1().find({ @@ -640,7 +780,7 @@ describe("mikroOrmRepository", () => { ] as any, } - let resp = await manager1().upsertWithReplace([entity1], { + await manager1().upsertWithReplace([entity1], { relations: ["entity3"], }) @@ -648,7 +788,7 @@ describe("mikroOrmRepository", () => { entity1.entity3 = [{ id: "4", title: "newen3-1" }, { title: "en3-4" }] // We don't do many-to-many updates, so id: 4 entity should remain unchanged - resp = await manager1().upsertWithReplace([entity1], { + await manager1().upsertWithReplace([entity1], { relations: ["entity3"], }) @@ -718,9 +858,12 @@ describe("mikroOrmRepository", () => { ] as any, } - const mainEntity = await manager1().upsertWithReplace([entity1], { - relations: ["entity3"], - }) + const { entities: mainEntity } = await manager1().upsertWithReplace( + [entity1], + { + relations: ["entity3"], + } + ) entity1.title = "newen1" entity1.entity3 = [{ id: "4", title: "newen3-1" }, { title: "en3-4" }] @@ -771,11 +914,31 @@ describe("mikroOrmRepository", () => { entity3: [{ title: "en3-1" }, { title: "en3-2" }] as any, } - const [createResp] = await manager1().upsertWithReplace([entity1], { + const { + entities: [createResp], + performedActions: performedActions1, + } = await manager1().upsertWithReplace([entity1], { relations: ["entity2", "entity3"], }) + + expect(performedActions1).toEqual({ + created: { + [Entity1.name]: [expect.objectContaining({ id: createResp.id })], + [Entity2.name]: [ + expect.objectContaining({ id: createResp.entity2[0].id }), + ], + [Entity3.name]: createResp.entity3.map((entity3) => + expect.objectContaining({ id: entity3.id }) + ), + }, + updated: {}, + deleted: {}, + }) + createResp.title = "newen1" - const [updateResp] = await manager1().upsertWithReplace([createResp], { + const { + entities: [updateResp], + } = await manager1().upsertWithReplace([createResp], { relations: ["entity2", "entity3"], }) diff --git a/packages/core/utils/src/dal/mikro-orm/mikro-orm-repository.ts b/packages/core/utils/src/dal/mikro-orm/mikro-orm-repository.ts index 5274ed4445..e0df164607 100644 --- a/packages/core/utils/src/dal/mikro-orm/mikro-orm-repository.ts +++ b/packages/core/utils/src/dal/mikro-orm/mikro-orm-repository.ts @@ -4,6 +4,7 @@ import { DAL, FilterQuery, FilterQuery as InternalFilterQuery, + PerformedActions, RepositoryService, RepositoryTransformOptions, UpsertWithReplaceConfig, @@ -147,7 +148,7 @@ export class MikroOrmBaseRepository relations: [], }, context: Context = {} - ): Promise { + ): Promise<{ entities: T[]; performedActions: PerformedActions }> { throw new Error("Method not implemented.") } @@ -457,10 +458,17 @@ export function mikroOrmBaseRepositoryFactory( relations: [], }, context: Context = {} - ): Promise { - if (!data.length) { - return [] + ): Promise<{ entities: T[]; performedActions: PerformedActions }> { + const performedActions: PerformedActions = { + created: {}, + updated: {}, + deleted: {}, } + + if (!data.length) { + return { entities: [], performedActions } + } + // We want to convert a potential ORM model to a POJO const normalizedData: any[] = await this.serialize(data) @@ -507,11 +515,12 @@ export function mikroOrmBaseRepositoryFactory( return mainEntity }) - const upsertedTopLevelEntities = await this.upsertMany_( - manager, - entity.name, - toUpsert - ) + let { + orderedEntities: upsertedTopLevelEntities, + performedActions: performedActions_, + } = await this.upsertMany_(manager, entity.name, toUpsert) + + this.mergePerformedActions(performedActions, performedActions_) await promiseAll( upsertedTopLevelEntities @@ -534,12 +543,15 @@ export function mikroOrmBaseRepositoryFactory( return } - reconstructedEntry[relationName] = + const { entities, performedActions: performedActions_ } = await this.assignCollectionRelation_( manager, { ...originalEntry, id: (entityEntry as any).id }, relation ) + + this.mergePerformedActions(performedActions, performedActions_) + reconstructedEntry[relationName] = entities return }) }) @@ -551,7 +563,21 @@ export function mikroOrmBaseRepositoryFactory( // manager.create(entity, r, { persist: false }) // ) - return reconstructedResponse + return { entities: reconstructedResponse, performedActions } + } + + private mergePerformedActions( + performedActions: PerformedActions, + newPerformedActions: PerformedActions + ) { + Object.entries(newPerformedActions).forEach(([action, entities]) => { + Object.entries(entities as Record).forEach( + ([entityName, entityData]) => { + performedActions[action][entityName] ??= [] + performedActions[action][entityName].push(...entityData) + } + ) + }) } // FUTURE: We can make this performant by only aggregating the operations, but only executing them at the end. @@ -559,11 +585,18 @@ export function mikroOrmBaseRepositoryFactory( manager: SqlEntityManager, data: T, relation: EntityProperty - ) { + ): Promise<{ entities: any[]; performedActions: PerformedActions }> { const dataForRelation = data[relation.name] + + const performedActions: PerformedActions = { + created: {}, + updated: {}, + deleted: {}, + } + // If the field is not set, we ignore it. Null and empty arrays are a valid input and are handled below if (dataForRelation === undefined) { - return undefined + return { entities: [], performedActions } } // Make sure the data is correctly initialized with IDs before using it @@ -580,10 +613,16 @@ export function mikroOrmBaseRepositoryFactory( [parentPivotColumn]: (data as any).id, }) - return normalizedData + return { entities: normalizedData, performedActions } } - await this.upsertMany_(manager, relation.type, normalizedData, true) + const { performedActions: performedActions_ } = await this.upsertMany_( + manager, + relation.type, + normalizedData, + true + ) + this.mergePerformedActions(performedActions, performedActions_) const pivotData = normalizedData.map((currModel) => { return { @@ -602,7 +641,7 @@ export function mikroOrmBaseRepositoryFactory( }, }) - return normalizedData + return { entities: normalizedData, performedActions } } if (relation.reference === ReferenceType.ONE_TO_MANY) { @@ -622,18 +661,38 @@ export function mikroOrmBaseRepositoryFactory( }) }) - await this.upsertMany_(manager, relation.type, normalizedData) + const { performedActions: performedActions_ } = + await this.upsertMany_(manager, relation.type, normalizedData) + this.mergePerformedActions(performedActions, performedActions_) } + const toDeleteEntities = await manager.find( + relation.type, + { + id: { $nin: normalizedData.map((d: any) => d.id) }, + }, + { + fields: ["id"], + } + ) + const toDeleteIds = toDeleteEntities.map((d: any) => d.id) + await manager.nativeDelete(relation.type, { ...joinColumnsConstraints, - id: { $nin: normalizedData.map((d: any) => d.id) }, + id: { $in: toDeleteIds }, }) - return normalizedData + if (toDeleteEntities.length) { + performedActions.deleted[relation.type] ??= [] + performedActions.deleted[relation.type].push( + ...toDeleteEntities.map((d) => ({ id: d.id })) + ) + } + + return { entities: normalizedData, performedActions } } - return normalizedData + return { entities: normalizedData, performedActions } } protected handleRelationAssignment_( @@ -714,7 +773,7 @@ export function mikroOrmBaseRepositoryFactory( entityName: string, entries: any[], skipUpdate: boolean = false - ) { + ): Promise<{ orderedEntities: any[]; performedActions: PerformedActions }> { const selectQb = manager.qb(entityName) const existingEntities: any[] = await selectQb.select("*").where({ id: { $in: entries.map((d) => d.id) }, @@ -726,6 +785,12 @@ export function mikroOrmBaseRepositoryFactory( const orderedEntities: T[] = [] + const performedActions = { + created: {}, + updated: {}, + deleted: {}, + } + await promiseAll( entries.map(async (data) => { const existingEntity = existingEntitiesMap.get(data.id) @@ -735,19 +800,31 @@ export function mikroOrmBaseRepositoryFactory( return } await manager.nativeUpdate(entityName, { id: data.id }, data) + performedActions.updated[entityName] ??= [] + performedActions.updated[entityName].push({ id: data.id }) } else { const qb = manager.qb(entityName) if (skipUpdate) { - await qb.insert(data).onConflict().ignore().execute() + const res = await qb + .insert(data) + .onConflict() + .ignore() + .execute("all", true) + if (res) { + performedActions.created[entityName] ??= [] + performedActions.created[entityName].push({ id: data.id }) + } } else { await manager.insert(entityName, data) + performedActions.created[entityName] ??= [] + performedActions.created[entityName].push({ id: data.id }) // await manager.insert(entityName, data) } } }) ) - return orderedEntities + return { orderedEntities, performedActions } } async restore( diff --git a/packages/core/utils/src/modules-sdk/internal-module-service-factory.ts b/packages/core/utils/src/modules-sdk/internal-module-service-factory.ts index 302854410a..10f871e49e 100644 --- a/packages/core/utils/src/modules-sdk/internal-module-service-factory.ts +++ b/packages/core/utils/src/modules-sdk/internal-module-service-factory.ts @@ -5,6 +5,7 @@ import { FilterQuery as InternalFilterQuery, FindConfig, ModulesSdkTypes, + PerformedActions, UpsertWithReplaceConfig, } from "@medusajs/types" import { EntitySchema } from "@mikro-orm/core" @@ -493,12 +494,12 @@ export function internalModuleServiceFactory< data: any[], config?: UpsertWithReplaceConfig, sharedContext?: Context - ): Promise + ): Promise<{ entities: TEntity[]; performedActions: PerformedActions }> upsertWithReplace( data: any, config?: UpsertWithReplaceConfig, sharedContext?: Context - ): Promise + ): Promise<{ entities: TEntity; performedActions: PerformedActions }> @InjectTransactionManager(propertyRepositoryName) async upsertWithReplace( @@ -507,14 +508,18 @@ export function internalModuleServiceFactory< relations: [], }, @MedusaContext() sharedContext: Context = {} - ): Promise { + ): Promise<{ + entities: TEntity | TEntity[] + performedActions: PerformedActions + }> { const data_ = Array.isArray(data) ? data : [data] - const entities = await this[propertyRepositoryName].upsertWithReplace( - data_, - config, - sharedContext - ) - return Array.isArray(data) ? entities : entities[0] + const { entities, performedActions } = await this[ + propertyRepositoryName + ].upsertWithReplace(data_, config, sharedContext) + return { + entities: Array.isArray(data) ? entities : entities[0], + performedActions, + } } } diff --git a/packages/modules/pricing/src/services/pricing-module.ts b/packages/modules/pricing/src/services/pricing-module.ts index 67fe6dc687..164e3b657b 100644 --- a/packages/modules/pricing/src/services/pricing-module.ts +++ b/packages/modules/pricing/src/services/pricing-module.ts @@ -498,13 +498,14 @@ export default class PricingModuleService< const normalizedData = await this.normalizeUpdateData(data, sharedContext) const prices = normalizedData.flatMap((priceSet) => priceSet.prices || []) - const upsertedPrices = await this.priceService_.upsertWithReplace( - prices, - { - relations: ["price_rules"], - }, - sharedContext - ) + const { entities: upsertedPrices } = + await this.priceService_.upsertWithReplace( + prices, + { + relations: ["price_rules"], + }, + sharedContext + ) const priceSetsToUpsert = normalizedData.map((priceSet) => { const { prices, ...rest } = priceSet @@ -520,11 +521,14 @@ export default class PricingModuleService< } }) - return await this.priceSetService_.upsertWithReplace( - priceSetsToUpsert, - { relations: ["prices"] }, - sharedContext - ) + const { entities: priceSets } = + await this.priceSetService_.upsertWithReplace( + priceSetsToUpsert, + { relations: ["prices"] }, + sharedContext + ) + + return priceSets } async addRules( diff --git a/packages/modules/product/src/services/product-module-service.ts b/packages/modules/product/src/services/product-module-service.ts index 0e06eb1b7b..471f03e932 100644 --- a/packages/modules/product/src/services/product-module-service.ts +++ b/packages/modules/product/src/services/product-module-service.ts @@ -24,7 +24,9 @@ import { arrayDifference, InjectManager, InjectTransactionManager, + isPresent, isString, + isValidHandle, kebabCase, MedusaContext, MedusaError, @@ -32,9 +34,7 @@ import { ProductStatus, promiseAll, removeUndefined, - isValidHandle, toHandle, - isPresent, } from "@medusajs/utils" import { ProductCategoryEventData, @@ -373,16 +373,19 @@ export default class ProductModuleService< sharedContext ) - return this.productVariantService_.upsertWithReplace( - ProductModuleService.assignOptionsToVariants( - variantsWithProductId, - productOptions - ), - { - relations: ["options"], - }, - sharedContext - ) + const { entities: productVariants } = + await this.productVariantService_.upsertWithReplace( + ProductModuleService.assignOptionsToVariants( + variantsWithProductId, + productOptions + ), + { + relations: ["options"], + }, + sharedContext + ) + + return productVariants } createTags( @@ -811,11 +814,14 @@ export default class ProductModuleService< } as UpdateProductOptionInput }) - return await this.productOptionService_.upsertWithReplace( - normalizedInput, - { relations: ["values"] }, - sharedContext - ) + const { entities: productOptions } = + await this.productOptionService_.upsertWithReplace( + normalizedInput, + { relations: ["values"] }, + sharedContext + ) + + return productOptions } createCollections( @@ -865,11 +871,14 @@ export default class ProductModuleService< // It's safe to use upsertWithReplace here since we only have product IDs and the only operation to do is update the product // with the collection ID - return await this.productCollectionService_.upsertWithReplace( - normalizedInput, - { relations: ["products"] }, - sharedContext - ) + const { entities: productCollections } = + await this.productCollectionService_.upsertWithReplace( + normalizedInput, + { relations: ["products"] }, + sharedContext + ) + + return productCollections } async upsertCollections( @@ -1279,13 +1288,14 @@ export default class ProductModuleService< }) ) - const productData = await this.productService_.upsertWithReplace( - normalizedInput, - { - relations: ["images", "tags", "categories"], - }, - sharedContext - ) + const { entities: productData } = + await this.productService_.upsertWithReplace( + normalizedInput, + { + relations: ["images", "tags", "categories"], + }, + sharedContext + ) await promiseAll( // Note: It's safe to rely on the order here as `upsertWithReplace` preserves the order of the input @@ -1295,7 +1305,7 @@ export default class ProductModuleService< upsertedProduct.variants = [] if (product.options?.length) { - upsertedProduct.options = + const { entities: productOptions } = await this.productOptionService_.upsertWithReplace( product.options?.map((option) => ({ ...option, @@ -1304,10 +1314,11 @@ export default class ProductModuleService< { relations: ["values"] }, sharedContext ) + upsertedProduct.options = productOptions } if (product.variants?.length) { - upsertedProduct.variants = + const { entities: productVariants } = await this.productVariantService_.upsertWithReplace( ProductModuleService.assignOptionsToVariants( product.variants?.map((v) => ({ @@ -1319,6 +1330,7 @@ export default class ProductModuleService< { relations: ["options"] }, sharedContext ) + upsertedProduct.variants = productVariants } }) ) @@ -1342,13 +1354,14 @@ export default class ProductModuleService< }) ) - const productData = await this.productService_.upsertWithReplace( - normalizedInput, - { - relations: ["images", "tags", "categories"], - }, - sharedContext - ) + const { entities: productData } = + await this.productService_.upsertWithReplace( + normalizedInput, + { + relations: ["images", "tags", "categories"], + }, + sharedContext + ) // There is more than 1-level depth of relations here, so we need to handle the options and variants manually await promiseAll( @@ -1358,7 +1371,7 @@ export default class ProductModuleService< let allOptions: any[] = [] if (product.options?.length) { - upsertedProduct.options = + const { entities: productOptions } = await this.productOptionService_.upsertWithReplace( product.options?.map((option) => ({ ...option, @@ -1367,6 +1380,7 @@ export default class ProductModuleService< { relations: ["values"] }, sharedContext ) + upsertedProduct.options = productOptions // Since we handle the options and variants outside of the product upsert, we need to clean up manually await this.productOptionService_.delete( @@ -1391,7 +1405,7 @@ export default class ProductModuleService< } if (product.variants?.length) { - upsertedProduct.variants = + const { entities: productVariants } = await this.productVariantService_.upsertWithReplace( ProductModuleService.assignOptionsToVariants( product.variants?.map((v) => ({ @@ -1403,6 +1417,7 @@ export default class ProductModuleService< { relations: ["options"] }, sharedContext ) + upsertedProduct.variants = productVariants await this.productVariantService_.delete( {