From 76332ca6c153a786acc07d3f06ff45c3b9346fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Fri, 22 Dec 2023 13:05:36 +0100 Subject: [PATCH] feat(medusa, link-modules): sales channel <> cart link (#5459) * feat: sales channel joiner config * feat: product sales channel link config, SC list method * feat: migration * fix: refactor list SC * refactor: SC repo api * chore: changeset * feat: add dedicated FF * wip: cart<>sc link and migration * chore: changeset * fix: update migration with the cart table constraints * feat: populate the pivot table * chore: remove relation from joiner config * fix: constraint name * fix: filter out link relations when calling internal services * feat: product<> sc join entity * fix: update case * fix: add FF on in the repository, fix tests * fix: assign id when FF is on * fix: target table * feat: product service - fetch SC with RQ * feat: admin list products & SC with isolated product domain * feat: get admin product * feat: store endpoints * fix: remove duplicate import * fix: remove "name" prop * feat: typeorm entity changes * feat: pivot table, entity, on cart create changes * feat: update carts' SC * feat: cart - getValidatedSalesChannel with RQ * feat: refactor * wip: changes to create cart workflow * fix: remove join table entity due to migrations failing * fix: product seeder if FF is on * feat: attach SC handler and test * fix: env * feat: workflow compensation, cart service retrieve with RQ * fix: remote joiner implode map * chore: update changesets * fix: remove methods from SC service/repo * feat: use remote link in handlers * fix: remove SC service calls * fix: link params * fix: migration add constraint to make link upsert pass * refactor: workflow product handlers to handle remote links * fix: condition * fix: use correct method * fix: build * wip: update FF * fix: update FF in the handlers * chore: migrate to medusav2 FF * chore: uncomment test * fix: product factory * fix: unlinking SC and product * fix: use module name variable * refactor: cleanup query definitions * fix: add constraint * wip: migrate FF * fix: comments * feat: cart entity callbacks, fix tests * fix: only create SC in test * wip: services updates, changes to models * chore: rename prop * fix: add hook * fix: address comments * fix: temp sc filtering * fix: use RQ to filter by SC * fix: relations on retrieve * feat: migration sync data, remove FF * fix: revert order of queries * fix: alter migration, relations in service * fix: revert id * fix: migrations * fix: make expand work * fix: remote link method call * fix: try making tests work without id in the pivot table * test: use remote link * test: relations changes * fix: preserve channel id column * fix: seeder and factory * fix: remove sales_channels from response * feat: support feature flag arrays * fix: cover everything with correct FF * fix: remove verbose * fix: unit and plugin tests * chore: comments * fix: reenable workflow handler, add comments, split cart create workflow tests * chore: reenable link in the create mehod, update changesets * fix: address feedback * fix: revert migration * fix: change the migration to follow link module * fix: migration syntax * fix: merge conflicts * fix: typo * feat: remove store sales channel foreign key * fix: merge migrations * fix: FF keys * refactor: cart service * refactor: FF missing key * fix: comments * fix: address PR comments * fix: new changesets * fix: revert flag router changes * chore: refactor `isFeatureEnabled` --------- Co-authored-by: Carlos R. L. Rodrigues Co-authored-by: Riqwan Thamir --- .changeset/two-chefs-complain.md | 9 ++ .../factories/simple-cart-factory.ts | 18 ++- .../__tests__/cart/store/ff-medusa-v2.ts | 96 +++++++++++++ .../plugins/__tests__/cart/store/index.ts | 2 +- .../__tests__/inventory/order/order.js | 6 +- .../src/definition/cart/create-cart.ts | 48 ++++++- .../cart/attach-cart-to-sales-channel.ts | 42 ++++++ .../src/handlers/cart/create-cart.ts | 4 +- .../cart/detach-cart-from-sales-channel.ts | 42 ++++++ .../core-flows/src/handlers/cart/index.ts | 2 + .../src/definitions/cart-sales-channel.ts | 65 +++++++++ .../link-modules/src/definitions/index.ts | 1 + packages/link-modules/src/links.ts | 6 + .../src/api/routes/store/carts/index.ts | 7 +- .../medusa/src/joiner-configs/cart-service.ts | 11 +- ...3273-add-table-product-shipping-profile.ts | 2 +- .../1698160215000-cart-sales-channels-link.ts | 47 +++++++ .../medusa/src/models/cart-sales-channel.ts | 29 ++++ packages/medusa/src/models/cart.ts | 75 ++++++++-- packages/medusa/src/models/sales-channel.ts | 23 +++- .../medusa/src/services/__tests__/cart.js | 1 + packages/medusa/src/services/cart.ts | 128 +++++++++++++++--- .../src/services/sales-channel-location.ts | 4 +- .../src/utils/feature-flag-decorators.ts | 4 +- .../src/utils/remote-query-fetch-data.ts | 12 ++ .../orchestration/src/joiner/remote-joiner.ts | 45 +++--- packages/utils/src/common/is-object.ts | 4 +- .../src/feature-flags/utils/flag-router.ts | 18 +-- 28 files changed, 662 insertions(+), 89 deletions(-) create mode 100644 .changeset/two-chefs-complain.md create mode 100644 integration-tests/plugins/__tests__/cart/store/ff-medusa-v2.ts create mode 100644 packages/core-flows/src/handlers/cart/attach-cart-to-sales-channel.ts create mode 100644 packages/core-flows/src/handlers/cart/detach-cart-from-sales-channel.ts create mode 100644 packages/link-modules/src/definitions/cart-sales-channel.ts create mode 100644 packages/medusa/src/migrations/1698160215000-cart-sales-channels-link.ts create mode 100644 packages/medusa/src/models/cart-sales-channel.ts diff --git a/.changeset/two-chefs-complain.md b/.changeset/two-chefs-complain.md new file mode 100644 index 0000000000000..ce74e0e21364d --- /dev/null +++ b/.changeset/two-chefs-complain.md @@ -0,0 +1,9 @@ +--- +"@medusajs/orchestration": patch +"@medusajs/link-modules": patch +"@medusajs/core-flows": patch +"@medusajs/medusa": patch +"@medusajs/utils": patch +--- + +feat: SalesChannel <> Cart joiner config diff --git a/integration-tests/factories/simple-cart-factory.ts b/integration-tests/factories/simple-cart-factory.ts index d734b50de2881..679a34abfe418 100644 --- a/integration-tests/factories/simple-cart-factory.ts +++ b/integration-tests/factories/simple-cart-factory.ts @@ -19,6 +19,7 @@ import { ShippingMethodFactoryData, simpleShippingMethodFactory, } from "./simple-shipping-method-factory" +import { generateEntityId } from "@medusajs/utils" export type CartFactoryData = { id?: string @@ -32,6 +33,8 @@ export type CartFactoryData = { sales_channel_id?: string } +const isMedusaV2Enabled = process.env.MEDUSA_FF_MEDUSA_V2 == "true" + export const simpleCartFactory = async ( dataSource: DataSource, data: CartFactoryData = {}, @@ -77,7 +80,7 @@ export const simpleCartFactory = async ( } const id = data.id || `simple-cart-${Math.random() * 1000}` - const toSave = manager.create(Cart, { + let toSave = { id, email: typeof data.email !== "undefined" ? data.email : faker.internet.email(), @@ -85,7 +88,18 @@ export const simpleCartFactory = async ( customer_id: customerId, shipping_address_id: address.id, sales_channel_id: sales_channel?.id ?? data.sales_channel_id ?? null, - }) + } + + if (isMedusaV2Enabled) { + await manager.query( + `INSERT INTO "cart_sales_channel" (id, cart_id, sales_channel_id) + VALUES ('${generateEntityId(undefined, "cartsc")}', '${toSave.id}', '${ + sales_channel?.id ?? data.sales_channel_id + }');` + ) + } + + toSave = manager.create(Cart, toSave) const cart = await manager.save(toSave) diff --git a/integration-tests/plugins/__tests__/cart/store/ff-medusa-v2.ts b/integration-tests/plugins/__tests__/cart/store/ff-medusa-v2.ts new file mode 100644 index 0000000000000..bb309a962291c --- /dev/null +++ b/integration-tests/plugins/__tests__/cart/store/ff-medusa-v2.ts @@ -0,0 +1,96 @@ +import { Region } from "@medusajs/medusa" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../../environment-helpers/use-api" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import { + simpleProductFactory, + simpleSalesChannelFactory, +} from "../../../../factories" + +jest.setTimeout(30000) + +const env = { + MEDUSA_FF_MEDUSA_V2: true, +} + +describe("/store/carts", () => { + let dbConnection + let shutdownServer + + const doAfterEach = async () => { + const db = useDb() + return await db.teardown() + } + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + describe("POST /store/carts", () => { + let prod1 + let prodSale + + beforeEach(async () => { + const manager = dbConnection.manager + await manager.insert(Region, { + id: "region", + name: "Test Region", + currency_code: "usd", + tax_rate: 0, + }) + + await manager.query( + `UPDATE "country" + SET region_id='region' + WHERE iso_2 = 'us'` + ) + + prod1 = await simpleProductFactory(dbConnection, { + id: "test-product", + variants: [{ id: "test-variant_1" }], + }) + + prodSale = await simpleProductFactory(dbConnection, { + id: "test-product-sale", + variants: [ + { + id: "test-variant-sale", + prices: [{ amount: 1000, currency: "usd" }], + }, + ], + }) + + await simpleSalesChannelFactory(dbConnection, { + id: "amazon-sc", + name: "Amazon store", + }) + }) + + afterEach(async () => { + await doAfterEach() + }) + + it("should create a cart in a sales channel", async () => { + const api = useApi() + + const response = await api.post("/store/carts", { + sales_channel_id: "amazon-sc", + }) + + expect(response.status).toEqual(200) + + const getRes = await api.get(`/store/carts/${response.data.cart.id}`) + expect(getRes.status).toEqual(200) + expect(getRes.data.cart.sales_channel.id).toEqual("amazon-sc") + }) + }) +}) diff --git a/integration-tests/plugins/__tests__/cart/store/index.ts b/integration-tests/plugins/__tests__/cart/store/index.ts index 59f1b6a7ee056..0c892a959d304 100644 --- a/integration-tests/plugins/__tests__/cart/store/index.ts +++ b/integration-tests/plugins/__tests__/cart/store/index.ts @@ -23,7 +23,7 @@ describe("/store/carts", () => { beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) - dbConnection = await initDb({ cwd }) + dbConnection = await initDb({ cwd } as any) shutdownServer = await startBootstrapApp({ cwd }) }) diff --git a/integration-tests/plugins/__tests__/inventory/order/order.js b/integration-tests/plugins/__tests__/inventory/order/order.js index fab2ed92052fc..d4c8f07ee50b1 100644 --- a/integration-tests/plugins/__tests__/inventory/order/order.js +++ b/integration-tests/plugins/__tests__/inventory/order/order.js @@ -4,11 +4,7 @@ const { startBootstrapApp, } = require("../../../../environment-helpers/bootstrap-app") const { initDb, useDb } = require("../../../../environment-helpers/use-db") -const { - setPort, - useApi, - useExpressServer, -} = require("../../../../environment-helpers/use-api") +const { useApi } = require("../../../../environment-helpers/use-api") const adminSeeder = require("../../../../helpers/admin-seeder") const { diff --git a/packages/core-flows/src/definition/cart/create-cart.ts b/packages/core-flows/src/definition/cart/create-cart.ts index e93819273b196..40b5b8b1e12f0 100644 --- a/packages/core-flows/src/definition/cart/create-cart.ts +++ b/packages/core-flows/src/definition/cart/create-cart.ts @@ -18,6 +18,7 @@ import { exportWorkflow, pipe } from "@medusajs/workflows-sdk" enum CreateCartActions { setContext = "setContext", attachLineItems = "attachLineItems", + attachToSalesChannel = "attachToSalesChannel", findRegion = "findRegion", findSalesChannel = "findSalesChannel", createCart = "createCart", @@ -58,10 +59,13 @@ const workflowSteps: TransactionStepsDefinition = { noCompensation: true, next: { action: CreateCartActions.createCart, - next: { - action: CreateCartActions.attachLineItems, - noCompensation: true, - }, + next: [ + { + action: CreateCartActions.attachLineItems, + noCompensation: true, + }, + { action: CreateCartActions.attachToSalesChannel }, + ], }, }, }, @@ -134,6 +138,10 @@ const handlers = new Map([ invoke: pipe( { invoke: [ + { + from: CreateCartActions.findSalesChannel, + alias: CartHandlers.createCart.aliases.SalesChannel, + }, { from: CreateCartActions.findRegion, alias: CartHandlers.createCart.aliases.Region, @@ -186,6 +194,38 @@ const handlers = new Map([ ), }, ], + [ + CreateCartActions.attachToSalesChannel, + { + invoke: pipe( + { + invoke: [ + { + from: CreateCartActions.createCart, + alias: CartHandlers.attachCartToSalesChannel.aliases.Cart, + }, + { + from: CreateCartActions.findSalesChannel, + alias: CartHandlers.attachCartToSalesChannel.aliases.SalesChannel, + }, + ], + }, + CartHandlers.attachCartToSalesChannel + ), + compensate: pipe( + { + invoke: [ + { + from: CreateCartActions.findSalesChannel, + alias: + CartHandlers.detachCartFromSalesChannel.aliases.SalesChannel, + }, + ], + }, + CartHandlers.detachCartFromSalesChannel + ), + }, + ], ]) WorkflowManager.register(Workflows.CreateCart, workflowSteps, handlers) diff --git a/packages/core-flows/src/handlers/cart/attach-cart-to-sales-channel.ts b/packages/core-flows/src/handlers/cart/attach-cart-to-sales-channel.ts new file mode 100644 index 0000000000000..b3d0346d18d44 --- /dev/null +++ b/packages/core-flows/src/handlers/cart/attach-cart-to-sales-channel.ts @@ -0,0 +1,42 @@ +import { MedusaV2Flag } from "@medusajs/utils" +import { WorkflowArguments } from "@medusajs/workflows-sdk" + +type HandlerInputData = { + cart: { + id: string + } + sales_channel: { + sales_channel_id: string + } +} + +enum Aliases { + Cart = "cart", + SalesChannel = "sales_channel", +} + +export async function attachCartToSalesChannel({ + container, + data, +}: WorkflowArguments): Promise { + const featureFlagRouter = container.resolve("featureFlagRouter") + const remoteLink = container.resolve("remoteLink") + + if (!featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + return + } + + const cart = data[Aliases.Cart] + const salesChannel = data[Aliases.SalesChannel] + + await remoteLink.create({ + cartService: { + cart_id: cart.id, + }, + salesChannelService: { + sales_channel_id: salesChannel.sales_channel_id, + }, + }) +} + +attachCartToSalesChannel.aliases = Aliases diff --git a/packages/core-flows/src/handlers/cart/create-cart.ts b/packages/core-flows/src/handlers/cart/create-cart.ts index 3325d90bb0d05..c409db4c928fc 100644 --- a/packages/core-flows/src/handlers/cart/create-cart.ts +++ b/packages/core-flows/src/handlers/cart/create-cart.ts @@ -47,15 +47,13 @@ export async function createCart({ const cartService = container.resolve("cartService") const cartServiceTx = cartService.withTransaction(manager) - const cart = await cartServiceTx.create({ + return await cartServiceTx.create({ ...data[Aliases.SalesChannel], ...data[Aliases.Addresses], ...data[Aliases.Customer], ...data[Aliases.Region], ...data[Aliases.Context], }) - - return cart } createCart.aliases = Aliases diff --git a/packages/core-flows/src/handlers/cart/detach-cart-from-sales-channel.ts b/packages/core-flows/src/handlers/cart/detach-cart-from-sales-channel.ts new file mode 100644 index 0000000000000..24f47642be9e4 --- /dev/null +++ b/packages/core-flows/src/handlers/cart/detach-cart-from-sales-channel.ts @@ -0,0 +1,42 @@ +import { MedusaV2Flag } from "@medusajs/utils" +import { WorkflowArguments } from "@medusajs/workflows-sdk" + +type HandlerInputData = { + cart: { + id: string + } + sales_channel: { + sales_channel_id: string + } +} + +enum Aliases { + Cart = "cart", + SalesChannel = "sales_channel", +} + +export async function detachCartFromSalesChannel({ + container, + data, +}: WorkflowArguments): Promise { + const featureFlagRouter = container.resolve("featureFlagRouter") + const remoteLink = container.resolve("remoteLink") + + if (!featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + return + } + + const cart = data[Aliases.Cart] + const salesChannel = data[Aliases.SalesChannel] + + await remoteLink.dismiss({ + cartService: { + cart_id: cart.id, + }, + salesChannelService: { + sales_channel_id: salesChannel.sales_channel_id, + }, + }) +} + +detachCartFromSalesChannel.aliases = Aliases diff --git a/packages/core-flows/src/handlers/cart/index.ts b/packages/core-flows/src/handlers/cart/index.ts index 5c2f81eaa2105..a65006df5734d 100644 --- a/packages/core-flows/src/handlers/cart/index.ts +++ b/packages/core-flows/src/handlers/cart/index.ts @@ -2,3 +2,5 @@ export * from "./attach-line-items-to-cart" export * from "./create-cart" export * from "./remove-cart" export * from "./retrieve-cart" +export * from "./attach-cart-to-sales-channel" +export * from "./detach-cart-from-sales-channel" diff --git a/packages/link-modules/src/definitions/cart-sales-channel.ts b/packages/link-modules/src/definitions/cart-sales-channel.ts new file mode 100644 index 0000000000000..86b06ecd1f0e8 --- /dev/null +++ b/packages/link-modules/src/definitions/cart-sales-channel.ts @@ -0,0 +1,65 @@ +import { ModuleJoinerConfig } from "@medusajs/types" +import { LINKS } from "../links" + +export const CartSalesChannel: ModuleJoinerConfig = { + serviceName: LINKS.CartSalesChannel, + isLink: true, + databaseConfig: { + tableName: "cart_sales_channel", + idPrefix: "cartsc", + }, + alias: [ + { + name: "cart_sales_channel", + }, + { + name: "cart_sales_channels", + }, + ], + primaryKeys: ["id", "cart_id", "sales_channel_id"], + relationships: [ + { + serviceName: "cartService", + isInternalService: true, + primaryKey: "id", + foreignKey: "cart_id", + alias: "cart", + }, + { + serviceName: "salesChannelService", + isInternalService: true, + primaryKey: "id", + foreignKey: "sales_channel_id", + alias: "sales_channel", + }, + ], + extends: [ + { + serviceName: "cartService", + fieldAlias: { + sales_channel: "sales_channel_link.sales_channel", + }, + relationship: { + serviceName: LINKS.CartSalesChannel, + isInternalService: true, + primaryKey: "cart_id", + foreignKey: "id", + alias: "sales_channel_link", + }, + }, + { + serviceName: "salesChannelService", + fieldAlias: { + carts: "cart_link.cart", + }, + relationship: { + serviceName: LINKS.CartSalesChannel, + isInternalService: true, + primaryKey: "sales_channel_id", + foreignKey: "id", + alias: "cart_link", + isList: true, + }, + }, + ], +} diff --git a/packages/link-modules/src/definitions/index.ts b/packages/link-modules/src/definitions/index.ts index 571e83bcedde3..3d18bac1ba50c 100644 --- a/packages/link-modules/src/definitions/index.ts +++ b/packages/link-modules/src/definitions/index.ts @@ -3,3 +3,4 @@ export * from "./product-variant-inventory-item" export * from "./product-variant-price-set" export * from "./product-shipping-profile" export * from "./product-sales-channel" +export * from "./cart-sales-channel" diff --git a/packages/link-modules/src/links.ts b/packages/link-modules/src/links.ts index bda65e6f2ccba..117777eae4f45 100644 --- a/packages/link-modules/src/links.ts +++ b/packages/link-modules/src/links.ts @@ -28,4 +28,10 @@ export const LINKS = { "salesChannelService", "sales_channel_id" ), + CartSalesChannel: composeLinkName( + "cartService", + "cart_id", + "salesChannelService", + "sales_channel_id" + ), } diff --git a/packages/medusa/src/api/routes/store/carts/index.ts b/packages/medusa/src/api/routes/store/carts/index.ts index e4fa98227bf78..deb98e2cec6fc 100644 --- a/packages/medusa/src/api/routes/store/carts/index.ts +++ b/packages/medusa/src/api/routes/store/carts/index.ts @@ -16,6 +16,7 @@ import { StorePostCartsCartShippingMethodReq } from "./add-shipping-method" import { StorePostCartsCartPaymentSessionReq } from "./set-payment-session" import { StorePostCartsCartLineItemsItemReq } from "./update-line-item" import { StorePostCartsCartPaymentSessionUpdateReq } from "./update-payment-session" +import { MedusaV2Flag } from "@medusajs/utils" const route = Router() @@ -26,7 +27,11 @@ export default (app, container) => { app.use("/carts", route) if (featureFlagRouter.isFeatureEnabled(SalesChannelFeatureFlag.key)) { - defaultStoreCartRelations.push("sales_channel") + if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) { + defaultStoreCartRelations.push("sales_channels") + } else { + defaultStoreCartRelations.push("sales_channel") + } } // Inject plugin routes diff --git a/packages/medusa/src/joiner-configs/cart-service.ts b/packages/medusa/src/joiner-configs/cart-service.ts index 6fe73ab8eaaf5..f236208ebfe6e 100644 --- a/packages/medusa/src/joiner-configs/cart-service.ts +++ b/packages/medusa/src/joiner-configs/cart-service.ts @@ -1,15 +1,16 @@ import { Modules } from "@medusajs/modules-sdk" import { ModuleJoinerConfig } from "@medusajs/types" +import { Cart } from "../models" + export default { serviceName: "cartService", primaryKeys: ["id"], linkableKeys: { cart_id: "Cart" }, - alias: [ - { - name: "cart", - }, - ], + alias: { + name: ["cart", "carts"], + args: { entity: Cart.name }, + }, relationships: [ { serviceName: Modules.PRODUCT, diff --git a/packages/medusa/src/migrations/1680857773273-add-table-product-shipping-profile.ts b/packages/medusa/src/migrations/1680857773273-add-table-product-shipping-profile.ts index ea57470a578a9..334000fca0a62 100644 --- a/packages/medusa/src/migrations/1680857773273-add-table-product-shipping-profile.ts +++ b/packages/medusa/src/migrations/1680857773273-add-table-product-shipping-profile.ts @@ -32,7 +32,7 @@ export class addTableProductShippingProfile1680857773273 DROP INDEX IF EXISTS "idx_product_shipping_profile_product_id"; DROP INDEX IF EXISTS "idx_product_shipping_profile_profile_id"; - ALTER TABLE "product" ADD COLUMN IF NOT EXISTS "profile_id"; + ALTER TABLE "product" ADD COLUMN IF NOT EXISTS "profile_id" CHARACTER VARYING; UPDATE "product" SET "profile_id" = "product_shipping_profile"."profile_id" FROM "product_shipping_profile" diff --git a/packages/medusa/src/migrations/1698160215000-cart-sales-channels-link.ts b/packages/medusa/src/migrations/1698160215000-cart-sales-channels-link.ts new file mode 100644 index 0000000000000..3d017f5965cee --- /dev/null +++ b/packages/medusa/src/migrations/1698160215000-cart-sales-channels-link.ts @@ -0,0 +1,47 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import { MedusaV2Flag } from "@medusajs/utils" + +import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" + +export const featureFlag = [SalesChannelFeatureFlag.key, MedusaV2Flag.key] + +export class CartSalesChannelsLink1698160215000 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS "cart_sales_channel" + ( + "id" character varying NOT NULL, + "cart_id" character varying NOT NULL, + "sales_channel_id" character varying NOT NULL, + "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + "deleted_at" TIMESTAMP WITH TIME ZONE, + CONSTRAINT "cart_sales_channel_pk" PRIMARY KEY ("cart_id", "sales_channel_id"), + CONSTRAINT "cart_sales_channel_cart_id_unique" UNIQUE ("cart_id") + ); + + CREATE INDEX IF NOT EXISTS "IDX_id_cart_sales_channel" ON "cart_sales_channel" ("id"); + + insert into "cart_sales_channel" (id, cart_id, sales_channel_id) + (select 'cartsc_' || substr(md5(random()::text), 0, 27), id, sales_channel_id from "cart" WHERE sales_channel_id IS NOT NULL); + + ALTER TABLE IF EXISTS "cart" DROP CONSTRAINT IF EXISTS "FK_a2bd3c26f42e754b9249ba78fd6"; + + ALTER TABLE IF EXISTS "store" DROP CONSTRAINT IF EXISTS "FK_61b0f48cccbb5f41c750bac7286"; + `) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + UPDATE "cart" SET "sales_channel_id" = "cart_sales_channel"."sales_channel_id" + FROM "cart_sales_channel" + WHERE "cart"."id" = "cart_sales_channel"."cart_id"; + + DROP TABLE IF EXISTS "cart_sales_channel"; + + ALTER TABLE IF EXISTS "cart" ADD CONSTRAINT "FK_a2bd3c26f42e754b9249ba78fd6" FOREIGN KEY ("sales_channel_id") REFERENCES "sales_channel"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + + ALTER TABLE IF EXISTS "store" ADD CONSTRAINT "FK_61b0f48cccbb5f41c750bac7286" FOREIGN KEY ("default_sales_channel_id") REFERENCES "sales_channel"("id") ON DELETE NO ACTION ON UPDATE NO ACTION; + `) + } +} diff --git a/packages/medusa/src/models/cart-sales-channel.ts b/packages/medusa/src/models/cart-sales-channel.ts new file mode 100644 index 0000000000000..30b6d261a2f2a --- /dev/null +++ b/packages/medusa/src/models/cart-sales-channel.ts @@ -0,0 +1,29 @@ +import { BeforeInsert, Column, Index, PrimaryColumn } from "typeorm" +import { MedusaV2Flag, SalesChannelFeatureFlag } from "@medusajs/utils" + +import { generateEntityId } from "../utils" +import { SoftDeletableEntity } from "../interfaces" +import { FeatureFlagEntity } from "../utils/feature-flag-decorators" + +@FeatureFlagEntity([MedusaV2Flag.key, SalesChannelFeatureFlag.key]) +export class CartSalesChannel extends SoftDeletableEntity { + @Column() + id: string + + @Index("cart_sales_channel_cart_id_unique", { + unique: true, + }) + @PrimaryColumn() + cart_id: string + + @PrimaryColumn() + sales_channel_id: string + + /** + * @apiIgnore + */ + @BeforeInsert() + private beforeInsert(): void { + this.id = generateEntityId(this.id, "cartsc") + } +} diff --git a/packages/medusa/src/models/cart.ts b/packages/medusa/src/models/cart.ts index cc42b8aadb80b..a213b237d7a86 100644 --- a/packages/medusa/src/models/cart.ts +++ b/packages/medusa/src/models/cart.ts @@ -233,6 +233,7 @@ import { AfterLoad, BeforeInsert, + BeforeUpdate, Column, Entity, Index, @@ -241,13 +242,10 @@ import { ManyToMany, ManyToOne, OneToMany, - OneToOne + OneToOne, } from "typeorm" +import { MedusaV2Flag, SalesChannelFeatureFlag } from "@medusajs/utils" import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column" -import { - FeatureFlagColumn, - FeatureFlagDecorators, -} from "../utils/feature-flag-decorators" import { SoftDeletableEntity } from "../interfaces/models/soft-deletable-entity" import { generateEntityId } from "../utils/generate-entity-id" @@ -261,6 +259,10 @@ import { PaymentSession } from "./payment-session" import { Region } from "./region" import { SalesChannel } from "./sales-channel" import { ShippingMethod } from "./shipping-method" +import { + FeatureFlagColumn, + FeatureFlagDecorators, +} from "../utils/feature-flag-decorators" export enum CartType { DEFAULT = "default", @@ -387,15 +389,37 @@ export class Cart extends SoftDeletableEntity { @DbAwareColumn({ type: "jsonb", nullable: true }) metadata: Record - @FeatureFlagColumn("sales_channels", { type: "varchar", nullable: true }) + @FeatureFlagColumn(SalesChannelFeatureFlag.key, { + type: "varchar", + nullable: true, + }) sales_channel_id: string | null - @FeatureFlagDecorators("sales_channels", [ + @FeatureFlagDecorators(SalesChannelFeatureFlag.key, [ ManyToOne(() => SalesChannel), JoinColumn({ name: "sales_channel_id" }), ]) sales_channel: SalesChannel + @FeatureFlagDecorators( + [MedusaV2Flag.key, SalesChannelFeatureFlag.key], + [ + ManyToMany(() => SalesChannel, { cascade: ["remove", "soft-remove"] }), + JoinTable({ + name: "cart_sales_channel", + joinColumn: { + name: "cart_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "sales_channel_id", + referencedColumnName: "id", + }, + }), + ] + ) + sales_channels?: SalesChannel[] + shipping_total?: number discount_total?: number raw_discount_total?: number @@ -412,18 +436,41 @@ export class Cart extends SoftDeletableEntity { /** * @apiIgnore */ - @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") + + if (this.sales_channel_id || this.sales_channel) { + this.sales_channels = [ + { id: this.sales_channel_id || this.sales_channel?.id }, + ] as SalesChannel[] } } /** * @apiIgnore */ - @BeforeInsert() - private beforeInsert(): void { - this.id = generateEntityId(this.id, "cart") + @BeforeUpdate() + private beforeUpdate(): void { + if (this.sales_channel_id || this.sales_channel) { + this.sales_channels = [ + { id: this.sales_channel_id || this.sales_channel?.id }, + ] as SalesChannel[] + } + } + + /** + * @apiIgnore + */ + @AfterLoad() + private afterLoad(): void { + if (this.payment_sessions) { + this.payment_session = this.payment_sessions.find((p) => p.is_selected)! + } + if (this.sales_channels) { + this.sales_channel = this.sales_channels?.[0] + this.sales_channel_id = this.sales_channel?.id + delete this.sales_channels + } } } diff --git a/packages/medusa/src/models/sales-channel.ts b/packages/medusa/src/models/sales-channel.ts index a25bc2e7f8f05..b7fcea59b7713 100644 --- a/packages/medusa/src/models/sales-channel.ts +++ b/packages/medusa/src/models/sales-channel.ts @@ -1,10 +1,15 @@ import { BeforeInsert, Column, JoinTable, ManyToMany, OneToMany } from "typeorm" -import { FeatureFlagEntity } from "../utils/feature-flag-decorators" +import { MedusaV2Flag } from "@medusajs/utils" +import { + FeatureFlagDecorators, + FeatureFlagEntity, +} from "../utils/feature-flag-decorators" import { SoftDeletableEntity } from "../interfaces" import { DbAwareColumn, generateEntityId } from "../utils" import { SalesChannelLocation } from "./sales-channel-location" import { Product } from "./product" +import { Cart } from "./cart" @FeatureFlagEntity("sales_channels") export class SalesChannel extends SoftDeletableEntity { @@ -34,6 +39,22 @@ export class SalesChannel extends SoftDeletableEntity { }) products: Product[] + @FeatureFlagDecorators(MedusaV2Flag.key, [ + ManyToMany(() => Cart), + JoinTable({ + name: "cart_sales_channel", + joinColumn: { + name: "sales_channel_id", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "cart_id", + referencedColumnName: "id", + }, + }), + ]) + carts: Cart[] + @OneToMany( () => SalesChannelLocation, (scLocation) => scLocation.sales_channel, diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js index 4d9b6011c3f19..e0438788a682e 100644 --- a/packages/medusa/src/services/__tests__/cart.js +++ b/packages/medusa/src/services/__tests__/cart.js @@ -2668,6 +2668,7 @@ describe("CartService", () => { .register("newTotalsService", asClass(NewTotalsService)) .register("cartService", asClass(CartService)) .register("remoteQuery", asValue(null)) + .register("remoteLink", asValue(null)) .register("pricingModuleService", asValue(undefined)) .register("pricingService", asClass(PricingService)) diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index 36afc3834f42a..7fd82f14453e9 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -68,6 +68,8 @@ import { PaymentSessionRepository } from "../repositories/payment-session" import { ShippingMethodRepository } from "../repositories/shipping-method" import { PaymentSessionInput } from "../types/payment" import { validateEmail } from "../utils/is-email" +import { RemoteQueryFunction } from "@medusajs/types" +import { RemoteLink } from "@medusajs/modules-sdk" type InjectedDependencies = { manager: EntityManager @@ -98,6 +100,8 @@ type InjectedDependencies = { priceSelectionStrategy: IPriceSelectionStrategy productVariantInventoryService: ProductVariantInventoryService pricingService: PricingService + remoteQuery: RemoteQueryFunction + remoteLink: RemoteLink } type TotalsConfig = { @@ -139,6 +143,8 @@ class CartService extends TransactionBaseService { protected readonly priceSelectionStrategy_: IPriceSelectionStrategy protected readonly lineItemAdjustmentService_: LineItemAdjustmentService protected readonly featureFlagRouter_: FlagRouter + protected remoteQuery_: RemoteQueryFunction + protected remoteLink_: RemoteLink // eslint-disable-next-line max-len protected readonly productVariantInventoryService_: ProductVariantInventoryService protected readonly pricingService_: PricingService @@ -169,6 +175,8 @@ class CartService extends TransactionBaseService { salesChannelService, featureFlagRouter, storeService, + remoteQuery, + remoteLink, productVariantInventoryService, pricingService, }: InjectedDependencies) { @@ -202,6 +210,8 @@ class CartService extends TransactionBaseService { this.storeService_ = storeService this.productVariantInventoryService_ = productVariantInventoryService this.pricingService_ = pricingService + this.remoteQuery_ = remoteQuery + this.remoteLink_ = remoteLink } /** @@ -216,6 +226,7 @@ class CartService extends TransactionBaseService { const cartRepo = this.activeManager_.withRepository(this.cartRepository_) const query = buildQuery(selector, config) + return await cartRepo.find(query) } @@ -357,7 +368,10 @@ class CartService extends TransactionBaseService { } if ( - this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key) + this.featureFlagRouter_.isFeatureEnabled( + SalesChannelFeatureFlag.key + ) && + !this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key) ) { rawCart.sales_channel_id = ( await this.getValidatedSalesChannel(data.sales_channel_id) @@ -365,8 +379,7 @@ class CartService extends TransactionBaseService { } if (data.customer_id || data.customer) { - const customer = - (data.customer ?? + const customer = (data.customer ?? (data.customer_id && (await this.customerService_ .withTransaction(transactionManager) @@ -476,6 +489,27 @@ class CartService extends TransactionBaseService { const createdCart = cartRepo.create(rawCart) const cart = await cartRepo.save(createdCart) + + if ( + this.featureFlagRouter_.isFeatureEnabled([ + SalesChannelFeatureFlag.key, + MedusaV2Flag.key, + ]) + ) { + const salesChannel = await this.getValidatedSalesChannel( + data.sales_channel_id + ) + + await this.remoteLink_.create({ + cartService: { + cart_id: cart.id, + }, + salesChannelService: { + sales_channel_id: salesChannel.id, + }, + }) + } + await this.eventBus_ .withTransaction(transactionManager) .emit(CartService.Events.CREATED, { @@ -491,9 +525,20 @@ class CartService extends TransactionBaseService { ): Promise { let salesChannel: SalesChannel if (isDefined(salesChannelId)) { - salesChannel = await this.salesChannelService_ - .withTransaction(this.activeManager_) - .retrieve(salesChannelId) + if (this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key)) { + const query = { + sales_channel: { + __args: { + id: salesChannelId, + }, + }, + } + ;[salesChannel] = await this.remoteQuery_(query) + } else { + salesChannel = await this.salesChannelService_ + .withTransaction(this.activeManager_) + .retrieve(salesChannelId) + } } else { salesChannel = ( await this.storeService_.withTransaction(this.activeManager_).retrieve({ @@ -584,7 +629,7 @@ class CartService extends TransactionBaseService { * Returns true if all products in the cart can be fulfilled with the current * shipping methods. * @param shippingMethods - the set of shipping methods to check from - * @param lineItem - the line item + * @param lineItemShippingProfiledId - the line item * @return boolean representing whether shipping method is validated */ protected validateLineItemShipping_( @@ -655,17 +700,22 @@ class CartService extends TransactionBaseService { lineItem: LineItem, config = { validateSalesChannels: true } ): Promise { - const select: (keyof Cart)[] = ["id"] + const fields: (keyof Cart)[] = ["id"] + const relations: (keyof Cart)[] = ["shipping_methods"] if (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) { - select.push("sales_channel_id") + if (this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key)) { + relations.push("sales_channels") + } else { + fields.push("sales_channel_id") + } } return await this.atomicPhase_( async (transactionManager: EntityManager) => { let cart = await this.retrieve(cartId, { - select, - relations: ["shipping_methods"], + select: fields, + relations, }) if (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) { @@ -797,17 +847,22 @@ class CartService extends TransactionBaseService { ): Promise { const items: LineItem[] = Array.isArray(lineItems) ? lineItems : [lineItems] - const select: (keyof Cart)[] = ["id", "customer_id", "region_id"] + const fields: (keyof Cart)[] = ["id", "customer_id", "region_id"] + const relations: (keyof Cart)[] = ["shipping_methods"] if (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) { - select.push("sales_channel_id") + if (this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key)) { + relations.push("sales_channels") + } else { + fields.push("sales_channel_id") + } } return await this.atomicPhase_( async (transactionManager: EntityManager) => { let cart = await this.retrieve(cartId, { - select, - relations: ["shipping_methods"], + select: fields, + relations, }) if (this.featureFlagRouter_.isFeatureEnabled("sales_channels")) { @@ -994,7 +1049,7 @@ class CartService extends TransactionBaseService { * Updates a cart's existing line item. * @param cartId - the id of the cart to update * @param lineItemId - the id of the line item to update. - * @param lineItemUpdate - the line item to update. Must include an id field. + * @param update - the line item to update. Must include an id field. * @return the result of the update operation */ async updateLineItem( @@ -1223,8 +1278,39 @@ class CartService extends TransactionBaseService { isDefined(data.sales_channel_id) && data.sales_channel_id != cart.sales_channel_id ) { + const salesChannel = await this.getValidatedSalesChannel( + data.sales_channel_id + ) + await this.onSalesChannelChange(cart, data.sales_channel_id) - cart.sales_channel_id = data.sales_channel_id + + /** + * TODO: remove this once update cart workflow is build + * since this will be handled in a handler by the workflow + */ + if (this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key)) { + if (cart.sales_channel_id) { + await this.remoteLink_.dismiss({ + cartService: { + cart_id: cart.id, + }, + salesChannelService: { + sales_channel_id: cart.sales_channel_id, + }, + }) + } + + await this.remoteLink_.create({ + cartService: { + cart_id: cart.id, + }, + salesChannelService: { + sales_channel_id: salesChannel.id, + }, + }) + } else { + cart.sales_channel_id = salesChannel.id + } } if (isDefined(data.discounts) && data.discounts.length) { @@ -2247,15 +2333,15 @@ class CartService extends TransactionBaseService { const lineItemServiceTx = this.lineItemService_.withTransaction(transactionManager) - let productShippinProfileMap = new Map() + let productShippingProfileMap = new Map() if (this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key)) { - productShippinProfileMap = + productShippingProfileMap = await this.shippingProfileService_.getMapProfileIdsByProductIds( cart.items.map((item) => item.variant.product_id) ) } else { - productShippinProfileMap = new Map( + productShippingProfileMap = new Map( cart.items.map((item) => [ item.variant?.product?.id, item.variant?.product?.profile_id, @@ -2268,7 +2354,7 @@ class CartService extends TransactionBaseService { return lineItemServiceTx.update(item.id, { has_shipping: this.validateLineItemShipping_( methods, - productShippinProfileMap.get(item.variant?.product_id)! + productShippingProfileMap.get(item.variant?.product_id)! ), }) }) diff --git a/packages/medusa/src/services/sales-channel-location.ts b/packages/medusa/src/services/sales-channel-location.ts index c15147dffd60c..2c1d7e7b43391 100644 --- a/packages/medusa/src/services/sales-channel-location.ts +++ b/packages/medusa/src/services/sales-channel-location.ts @@ -76,7 +76,7 @@ class SalesChannelLocationService extends TransactionBaseService { .retrieve(salesChannelId) if (this.stockLocationService_) { - // trhows error if not found + // throws error if not found await this.stockLocationService_.retrieve(locationId, undefined, { transactionManager: this.activeManager_, }) @@ -124,7 +124,7 @@ class SalesChannelLocationService extends TransactionBaseService { /** * Lists the sales channels associated with a stock location. - * @param {string} salesChannelId - The ID of the stock location. + * @param {string} locationId - The ID of the stock location. * @returns {Promise} A promise that resolves with an array of sales channel IDs. */ async listSalesChannelIds(locationId: string): Promise { diff --git a/packages/medusa/src/utils/feature-flag-decorators.ts b/packages/medusa/src/utils/feature-flag-decorators.ts index ddfb45a6337eb..5bf28caaa4821 100644 --- a/packages/medusa/src/utils/feature-flag-decorators.ts +++ b/packages/medusa/src/utils/feature-flag-decorators.ts @@ -34,7 +34,7 @@ export function FeatureFlagColumn( } export function FeatureFlagDecorators( - featureFlag: string, + featureFlag: string | string[], decorators: PropertyDecorator[] ): PropertyDecorator { return function (target, propertyName) { @@ -68,7 +68,7 @@ export function FeatureFlagClassDecorators( } export function FeatureFlagEntity( - featureFlag: string, + featureFlag: string | string[], name?: string, options?: EntityOptions ): ClassDecorator { diff --git a/packages/medusa/src/utils/remote-query-fetch-data.ts b/packages/medusa/src/utils/remote-query-fetch-data.ts index 166738f82d649..5ef138f7db03d 100644 --- a/packages/medusa/src/utils/remote-query-fetch-data.ts +++ b/packages/medusa/src/utils/remote-query-fetch-data.ts @@ -17,6 +17,18 @@ export function remoteQueryFetchData(container: MedusaContainer) { ...RemoteQuery.getAllFieldsAndRelations(expand), } + const expandRelations = Object.keys(expand.expands ?? {}) + + // filter out links from relations because TypeORM will throw if the relation doesn't exist + + options.relations = options.relations.filter( + (relation) => !expandRelations.some((ex) => relation.startsWith(ex)) + ) + + options.select = options.relations.filter( + (field) => !expandRelations.some((ex) => field.startsWith(ex)) + ) + if (ids) { filters[keyField] = ids } diff --git a/packages/orchestration/src/joiner/remote-joiner.ts b/packages/orchestration/src/joiner/remote-joiner.ts index a84aada83da3d..bac1558fdcdec 100644 --- a/packages/orchestration/src/joiner/remote-joiner.ts +++ b/packages/orchestration/src/joiner/remote-joiner.ts @@ -23,16 +23,16 @@ export type RemoteFetchDataCallback = ( path?: string }> +type InternalImplodeMapping = { + location: string[] + property: string + path: string[] + isList?: boolean +} + export class RemoteJoiner { private serviceConfigCache: Map = new Map() - private implodeMapping: { - location: string[] - property: string - path: string[] - isList?: boolean - }[] = [] - private static filterFields( data: any, fields: string[], @@ -355,7 +355,8 @@ export class RemoteJoiner { private handleFieldAliases( items: any[], - parsedExpands: Map + parsedExpands: Map, + implodeMapping: InternalImplodeMapping[] ) { const getChildren = (item: any, prop: string) => { if (Array.isArray(item)) { @@ -373,7 +374,7 @@ export class RemoteJoiner { } const cleanup: [any, string][] = [] - for (const alias of this.implodeMapping) { + for (const alias of implodeMapping) { const propPath = alias.path let itemsLocation = items @@ -432,7 +433,8 @@ export class RemoteJoiner { private async handleExpands( items: any[], - parsedExpands: Map + parsedExpands: Map, + implodeMapping: InternalImplodeMapping[] = [] ): Promise { if (!parsedExpands) { return @@ -458,7 +460,7 @@ export class RemoteJoiner { } } - this.handleFieldAliases(items, parsedExpands) + this.handleFieldAliases(items, parsedExpands, implodeMapping) } private async expandProperty( @@ -567,13 +569,15 @@ export class RemoteJoiner { initialService: RemoteExpandProperty, query: RemoteJoinerQuery, serviceConfig: JoinerServiceConfig, - expands: RemoteJoinerQuery["expands"] + expands: RemoteJoinerQuery["expands"], + implodeMapping: InternalImplodeMapping[] ): Map { const parsedExpands = this.parseProperties( initialService, query, serviceConfig, - expands + expands, + implodeMapping ) const groupedExpands = this.groupExpands(parsedExpands) @@ -585,7 +589,8 @@ export class RemoteJoiner { initialService: RemoteExpandProperty, query: RemoteJoinerQuery, serviceConfig: JoinerServiceConfig, - expands: RemoteJoinerQuery["expands"] + expands: RemoteJoinerQuery["expands"], + implodeMapping: InternalImplodeMapping[] ): Map { const parsedExpands = new Map() parsedExpands.set(BASE_PATH, initialService) @@ -612,7 +617,7 @@ export class RemoteJoiner { ) ) - this.implodeMapping.push({ + implodeMapping.push({ location: currentPath, property: prop, path: fullPath, @@ -799,6 +804,7 @@ export class RemoteJoiner { (arg) => !serviceConfig.primaryKeys.includes(arg.name) ) + const implodeMapping: InternalImplodeMapping[] = [] const parsedExpands = this.parseExpands( { property: "", @@ -809,7 +815,8 @@ export class RemoteJoiner { }, queryObj, serviceConfig, - queryObj.expands! + queryObj.expands!, + implodeMapping ) const root = parsedExpands.get(BASE_PATH)! @@ -823,7 +830,11 @@ export class RemoteJoiner { const data = response.path ? response.data[response.path!] : response.data - await this.handleExpands(Array.isArray(data) ? data : [data], parsedExpands) + await this.handleExpands( + Array.isArray(data) ? data : [data], + parsedExpands, + implodeMapping + ) return response.data } diff --git a/packages/utils/src/common/is-object.ts b/packages/utils/src/common/is-object.ts index 778a601412fed..e577671adcb55 100644 --- a/packages/utils/src/common/is-object.ts +++ b/packages/utils/src/common/is-object.ts @@ -1,3 +1,3 @@ -export function isObject(obj: unknown): obj is object { - return typeof obj === "object" && !!obj +export function isObject(obj: any): obj is object { + return obj != null && obj?.constructor?.name === "Object" } diff --git a/packages/utils/src/feature-flags/utils/flag-router.ts b/packages/utils/src/feature-flags/utils/flag-router.ts index 734c38e1827e0..5879b3aac368f 100644 --- a/packages/utils/src/feature-flags/utils/flag-router.ts +++ b/packages/utils/src/feature-flags/utils/flag-router.ts @@ -19,22 +19,24 @@ export class FlagRouter implements FeatureFlagTypes.IFlagRouter { * @param flag - The flag to check * @return {boolean} - Whether the flag is enabled or not */ - public isFeatureEnabled(flag: string | Record): boolean { - if (isString(flag)) { - return !!this.flags[flag] - } - + public isFeatureEnabled( + flag: string | string[] | Record + ): boolean { if (isObject(flag)) { const [nestedFlag, value] = Object.entries(flag)[0] - if (typeof this.flags[nestedFlag] === "boolean") { return this.flags[nestedFlag] as boolean } - return !!this.flags[nestedFlag]?.[value] } - throw Error("Flag must be a string or an object") + const flags = (Array.isArray(flag) ? flag : [flag]) as string[] + return flags.every((flag_) => { + if (!isString(flag_)) { + throw Error("Flag must be a string an array of string or an object") + } + return !!this.flags[flag_] + }) } /**