diff --git a/.changeset/eighty-eels-pretend.md b/.changeset/eighty-eels-pretend.md new file mode 100644 index 0000000000..9ec4c9827e --- /dev/null +++ b/.changeset/eighty-eels-pretend.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): Cart custom query strategy diff --git a/packages/medusa/src/api/routes/store/carts/create-line-item/utils/handler-steps.ts b/packages/medusa/src/api/routes/store/carts/create-line-item/utils/handler-steps.ts index 0dfe65b743..d24fe0fa8b 100644 --- a/packages/medusa/src/api/routes/store/carts/create-line-item/utils/handler-steps.ts +++ b/packages/medusa/src/api/routes/store/carts/create-line-item/utils/handler-steps.ts @@ -1,11 +1,11 @@ import { AwilixContainer } from "awilix" import { EntityManager } from "typeorm" +import { Cart } from "../../../../../../models" import { CartService, LineItemService } from "../../../../../../services" +import { WithRequiredProperty } from "../../../../../../types/common" +import { IdempotencyCallbackResult } from "../../../../../../types/idempotency-key" import { FlagRouter } from "../../../../../../utils/flag-router" import { defaultStoreCartFields, defaultStoreCartRelations } from "../../index" -import { IdempotencyCallbackResult } from "../../../../../../types/idempotency-key" -import { WithRequiredProperty } from "../../../../../../types/common" -import { Cart } from "../../../../../../models" export const CreateLineItemSteps = { STARTED: "started", diff --git a/packages/medusa/src/models/cart.ts b/packages/medusa/src/models/cart.ts index a37a807e02..291a54d56d 100644 --- a/packages/medusa/src/models/cart.ts +++ b/packages/medusa/src/models/cart.ts @@ -216,6 +216,7 @@ */ import { + AfterLoad, BeforeInsert, Column, Entity, @@ -390,6 +391,13 @@ export class Cart extends SoftDeletableEntity { gift_card_total?: number gift_card_tax_total?: number + @AfterLoad() + private afterLoad(): void { + if (this.payment_sessions) { + this.payment_session = this.payment_sessions.find((p) => p.is_selected)! + } + } + @BeforeInsert() private beforeInsert(): void { this.id = generateEntityId(this.id, "cart") diff --git a/packages/medusa/src/repositories/cart.ts b/packages/medusa/src/repositories/cart.ts index 0e53257f86..c70f9b8f04 100644 --- a/packages/medusa/src/repositories/cart.ts +++ b/packages/medusa/src/repositories/cart.ts @@ -1,16 +1,56 @@ -import { ExtendedFindConfig } from "@medusajs/types" +import { objectToStringPath } from "@medusajs/utils" +import { flatten, groupBy, map, merge } from "lodash" +import { FindManyOptions, FindOptionsRelations, In } from "typeorm" import { dataSource } from "../loaders/database" import { Cart } from "../models" export const CartRepository = dataSource.getRepository(Cart).extend({ - async findOne(options: ExtendedFindConfig) { - const [cart] = await this.find(options) + async findWithRelations( + relations: FindOptionsRelations = {}, + optionsWithoutRelations: Omit, "relations"> = {} + ): Promise { + const entities = await this.find(optionsWithoutRelations) + const entitiesIds = entities.map(({ id }) => id) - if (cart?.payment_sessions?.length) { - cart.payment_session = cart.payment_sessions.find((p) => p.is_selected)! + const groupedRelations = {} + for (const rel of objectToStringPath(relations)) { + const [topLevel] = rel.split(".") + if (groupedRelations[topLevel]) { + groupedRelations[topLevel].push(rel) + } else { + groupedRelations[topLevel] = [rel] + } } - return cart + const entitiesIdsWithRelations = await Promise.all( + Object.entries(groupedRelations).map(async ([_, rels]) => { + return this.find({ + where: { id: In(entitiesIds) }, + select: ["id"], + relations: rels as string[], + }) + }) + ).then(flatten) + const entitiesAndRelations = entitiesIdsWithRelations.concat(entities) + + const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id") + return map(entitiesAndRelationsById, (entityAndRelations) => + merge({}, ...entityAndRelations) + ) + }, + + async findOneWithRelations( + relations: FindOptionsRelations = {}, + optionsWithoutRelations: Omit, "relations"> = {} + ): Promise { + // Limit 1 + optionsWithoutRelations.take = 1 + + const result = await this.findWithRelations( + relations, + optionsWithoutRelations + ) + return result[0] }, }) export default CartRepository diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js index b8d17b4af1..0aba7cfc9d 100644 --- a/packages/medusa/src/services/__tests__/cart.js +++ b/packages/medusa/src/services/__tests__/cart.js @@ -65,7 +65,8 @@ describe("CartService", () => { describe("retrieve", () => { let result const cartRepository = MockRepository({ - findOne: () => Promise.resolve({ id: IdMap.getId("emptyCart") }), + findOneWithRelations: () => + Promise.resolve({ id: IdMap.getId("emptyCart") }), }) beforeAll(async () => { jest.clearAllMocks() @@ -81,13 +82,15 @@ describe("CartService", () => { }) it("calls cart model functions", () => { - expect(cartRepository.findOne).toHaveBeenCalledTimes(1) - expect(cartRepository.findOne).toHaveBeenCalledWith({ - relationLoadStrategy: "query", - where: { id: IdMap.getId("emptyCart") }, - select: undefined, - relations: undefined, - }) + expect(cartRepository.findOneWithRelations).toHaveBeenCalledTimes(1) + expect(cartRepository.findOneWithRelations).toHaveBeenCalledWith( + {}, + { + where: { id: IdMap.getId("emptyCart") }, + select: undefined, + relations: undefined, + } + ) }) }) @@ -126,7 +129,9 @@ describe("CartService", () => { ) expect(cartRepository.findOne).toBeCalledTimes(1) - expect(cartRepository.findOne).toBeCalledWith({ where: { id } }) + expect(cartRepository.findOne).toBeCalledWith({ + where: { id }, + }) expect(cartRepository.save).toBeCalledTimes(1) expect(cartRepository.save).toBeCalledWith({ @@ -164,7 +169,7 @@ describe("CartService", () => { const addressRepository = MockRepository({ create: (c) => c, - findOne: (id) => { + findOneWithRelations: (id) => { return { id, first_name: "LeBron", @@ -343,7 +348,7 @@ describe("CartService", () => { } const cartRepository = MockRepository({ - findOne: (q) => { + findOneWithRelations: (rel, q) => { if (q.where.id === IdMap.getId("cartWithLine")) { return Promise.resolve({ id: IdMap.getId("cartWithLine"), @@ -584,7 +589,7 @@ describe("CartService", () => { } const cartRepository = MockRepository({ - findOne: (q) => { + findOneWithRelations: (rel, q) => { if (q.where.id === IdMap.getId("cartWithLine")) { return Promise.resolve({ id: IdMap.getId("cartWithLine"), @@ -670,7 +675,7 @@ describe("CartService", () => { }, } const cartRepository = MockRepository({ - findOne: (q) => { + findOneWithRelations: (rel, q) => { if (q.where.id === IdMap.getId("withShipping")) { return Promise.resolve({ shipping_methods: [ @@ -813,7 +818,7 @@ describe("CartService", () => { describe("update", () => { const cartRepository = MockRepository({ - findOne: (q) => { + findOneWithRelations: (rel, q) => { if (q.where.id === "withpays") { return Promise.resolve({ payment_sessions: [ @@ -844,25 +849,25 @@ describe("CartService", () => { cartService.setPaymentSessions = jest.fn() await cartService.update("withpays", {}) - expect(cartRepository.findOne).toHaveBeenCalledWith( + expect(cartRepository.findOneWithRelations).toHaveBeenCalledWith( expect.objectContaining({ - relations: { - billing_address: true, - customer: true, - discounts: { - rule: true, - }, - gift_cards: true, - items: { - variant: { - product: true, - }, - }, - payment_sessions: true, - region: { countries: true }, - shipping_address: true, - shipping_methods: true, + billing_address: true, + customer: true, + discounts: { + rule: true, }, + gift_cards: true, + items: { + variant: { + product: true, + }, + }, + payment_sessions: true, + region: { countries: true }, + shipping_address: true, + shipping_methods: true, + }), + expect.objectContaining({ select: undefined, where: { id: "withpays", @@ -907,7 +912,7 @@ describe("CartService", () => { } const cartRepository = MockRepository({ - findOne: (q) => { + findOneWithRelations: (rel, q) => { if (q.where.id === IdMap.getId("cannot")) { return Promise.resolve({ items: [ @@ -1019,7 +1024,7 @@ describe("CartService", () => { }, } const cartRepository = MockRepository({ - findOne: () => Promise.resolve({}), + findOneWithRelations: () => Promise.resolve({}), }) const cartService = new CartService({ manager: MockManager, @@ -1091,7 +1096,7 @@ describe("CartService", () => { describe("updateBillingAddress", () => { const cartRepository = MockRepository({ - findOne: () => + findOneWithRelations: () => Promise.resolve({ region: { countries: [{ iso_2: "us" }] }, }), @@ -1153,7 +1158,7 @@ describe("CartService", () => { describe("updateShippingAddress", () => { const cartRepository = MockRepository({ - findOne: () => + findOneWithRelations: () => Promise.resolve({ region: { countries: [{ iso_2: "us" }] }, }), @@ -1288,7 +1293,7 @@ describe("CartService", () => { } const cartRepository = MockRepository({ - findOne: () => + findOneWithRelations: () => Promise.resolve({ items: [ { @@ -1415,7 +1420,7 @@ describe("CartService", () => { describe("setPaymentSession", () => { const cartRepository = MockRepository({ - findOne: (q) => { + findOneWithRelations: (rel, q) => { if (q.where.id === IdMap.getId("cartWithLine")) { return Promise.resolve({ total: 100, @@ -1623,7 +1628,7 @@ describe("CartService", () => { } const cartRepository = MockRepository({ - findOne: (q) => { + findOneWithRelations: (rel, q) => { if (q.where.id === IdMap.getId("cart-to-filter")) { return Promise.resolve(cart3) } @@ -1829,7 +1834,7 @@ describe("CartService", () => { const cartWithCustomSO = buildCart("cart-with-custom-so") const cartRepository = MockRepository({ - findOne: (q) => { + findOneWithRelations: (rel, q) => { switch (q.where.id) { case IdMap.getId("lines"): return Promise.resolve(cart3) @@ -2028,7 +2033,7 @@ describe("CartService", () => { } const cartRepository = MockRepository({ - findOne: (q) => { + findOneWithRelations: (rel, q) => { if (q.where.id === IdMap.getId("with-d")) { return Promise.resolve({ id: IdMap.getId("cart"), @@ -2517,7 +2522,7 @@ describe("CartService", () => { describe("removeDiscount", () => { const cartRepository = MockRepository({ - findOne: (q) => { + findOneWithRelations: (rel, q) => { return Promise.resolve({ id: IdMap.getId("cart"), discounts: [ diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index d2f2a13045..27e2f5f577 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -231,13 +231,15 @@ class CartService extends TransactionBaseService { const cartRepo = this.activeManager_.withRepository(this.cartRepository_) const query = buildQuery({ id: cartId }, options) - query.relationLoadStrategy = "query" if ((options.select || []).length === 0) { query.select = undefined } - const raw = await cartRepo.findOne(query) + const queryRelations = { ...query.relations } + delete query.relations + + const raw = await cartRepo.findOneWithRelations(queryRelations, query) if (!raw) { throw new MedusaError(