From 3db2f95e65909f4fff432990b48be74509052e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Mon, 29 Jan 2024 09:47:28 +0100 Subject: [PATCH] feat: sales channel module (#5923) --- .changeset/flat-bees-laugh.md | 9 + integration-tests/plugins/medusa-config.js | 5 + .../cart/attach-cart-to-sales-channel.ts | 5 +- .../cart/detach-cart-from-sales-channel.ts | 5 +- .../attach-sales-channel-to-products.ts | 2 +- .../detach-sales-channel-from-products.ts | 2 +- .../src/definitions/cart-sales-channel.ts | 9 +- .../src/definitions/product-sales-channel.ts | 4 +- packages/link-modules/src/links.ts | 10 +- .../medusa/src/joiner-configs/cart-service.ts | 2 +- packages/medusa/src/joiner-configs/index.ts | 1 - .../joiner-configs/sales-channel-service.ts | 32 --- packages/medusa/src/services/cart.ts | 14 +- packages/modules-sdk/src/definitions.ts | 17 ++ packages/sales-channel/.gitignore | 6 + packages/sales-channel/README.md | 3 + .../integration-tests/__fixtures__/index.ts | 40 +++ .../services/sales-channel-module.spec.ts | 248 ++++++++++++++++++ .../integration-tests/setup-env.js | 6 + .../sales-channel/integration-tests/setup.js | 3 + .../integration-tests/utils/config.ts | 6 + .../integration-tests/utils/database.ts | 18 ++ .../integration-tests/utils/index.ts | 1 + packages/sales-channel/jest.config.js | 22 ++ .../sales-channel/mikro-orm.config.dev.ts | 8 + packages/sales-channel/package.json | 61 +++++ packages/sales-channel/src/index.ts | 27 ++ .../sales-channel/src/initialize/index.ts | 34 +++ packages/sales-channel/src/joiner-config.ts | 31 +++ .../sales-channel/src/loaders/connection.ts | 37 +++ .../sales-channel/src/loaders/container.ts | 10 + packages/sales-channel/src/loaders/index.ts | 2 + .../.snapshot-medusa-sales-channel-tst.json | 105 ++++++++ .../src/migrations/Migration20240115152146.ts | 12 + packages/sales-channel/src/models/index.ts | 1 + .../sales-channel/src/models/sales-channel.ts | 61 +++++ .../sales-channel/src/module-definition.ts | 13 + .../sales-channel/src/repositories/index.ts | 1 + .../sales-channel/src/scripts/bin/run-seed.ts | 31 +++ .../sales-channel/src/scripts/seed-utils.ts | 16 ++ .../services/__fixtures__/sales-channel.ts | 17 ++ .../services/__tests__/sales-channle.spec.ts | 34 +++ packages/sales-channel/src/services/index.ts | 2 + .../src/services/sales-channel-module.ts | 247 +++++++++++++++++ .../src/services/sales-channel.ts | 24 ++ packages/sales-channel/src/types/index.ts | 5 + .../sales-channel/src/types/repositories.ts | 15 ++ packages/sales-channel/tsconfig.json | 37 +++ packages/sales-channel/tsconfig.spec.json | 8 + packages/types/src/sales-channel/common.ts | 10 + packages/types/src/sales-channel/index.ts | 2 + packages/types/src/sales-channel/mutations.ts | 12 + packages/types/src/sales-channel/service.ts | 59 +++++ yarn.lock | 33 ++- 54 files changed, 1364 insertions(+), 61 deletions(-) create mode 100644 .changeset/flat-bees-laugh.md delete mode 100644 packages/medusa/src/joiner-configs/sales-channel-service.ts create mode 100644 packages/sales-channel/.gitignore create mode 100644 packages/sales-channel/README.md create mode 100644 packages/sales-channel/integration-tests/__fixtures__/index.ts create mode 100644 packages/sales-channel/integration-tests/__tests__/services/sales-channel-module.spec.ts create mode 100644 packages/sales-channel/integration-tests/setup-env.js create mode 100644 packages/sales-channel/integration-tests/setup.js create mode 100644 packages/sales-channel/integration-tests/utils/config.ts create mode 100644 packages/sales-channel/integration-tests/utils/database.ts create mode 100644 packages/sales-channel/integration-tests/utils/index.ts create mode 100644 packages/sales-channel/jest.config.js create mode 100644 packages/sales-channel/mikro-orm.config.dev.ts create mode 100644 packages/sales-channel/package.json create mode 100644 packages/sales-channel/src/index.ts create mode 100644 packages/sales-channel/src/initialize/index.ts create mode 100644 packages/sales-channel/src/joiner-config.ts create mode 100644 packages/sales-channel/src/loaders/connection.ts create mode 100644 packages/sales-channel/src/loaders/container.ts create mode 100644 packages/sales-channel/src/loaders/index.ts create mode 100644 packages/sales-channel/src/migrations/.snapshot-medusa-sales-channel-tst.json create mode 100644 packages/sales-channel/src/migrations/Migration20240115152146.ts create mode 100644 packages/sales-channel/src/models/index.ts create mode 100644 packages/sales-channel/src/models/sales-channel.ts create mode 100644 packages/sales-channel/src/module-definition.ts create mode 100644 packages/sales-channel/src/repositories/index.ts create mode 100644 packages/sales-channel/src/scripts/bin/run-seed.ts create mode 100644 packages/sales-channel/src/scripts/seed-utils.ts create mode 100644 packages/sales-channel/src/services/__fixtures__/sales-channel.ts create mode 100644 packages/sales-channel/src/services/__tests__/sales-channle.spec.ts create mode 100644 packages/sales-channel/src/services/index.ts create mode 100644 packages/sales-channel/src/services/sales-channel-module.ts create mode 100644 packages/sales-channel/src/services/sales-channel.ts create mode 100644 packages/sales-channel/src/types/index.ts create mode 100644 packages/sales-channel/src/types/repositories.ts create mode 100644 packages/sales-channel/tsconfig.json create mode 100644 packages/sales-channel/tsconfig.spec.json create mode 100644 packages/types/src/sales-channel/mutations.ts create mode 100644 packages/types/src/sales-channel/service.ts diff --git a/.changeset/flat-bees-laugh.md b/.changeset/flat-bees-laugh.md new file mode 100644 index 0000000000000..d56929a425be7 --- /dev/null +++ b/.changeset/flat-bees-laugh.md @@ -0,0 +1,9 @@ +--- +"@medusajs/core-flows": patch +"@medusajs/link-modules": patch +"@medusajs/medusa": patch +"@medusajs/modules-sdk": patch +"@medusajs/types": patch +--- + +feat: Sales Channel module diff --git a/integration-tests/plugins/medusa-config.js b/integration-tests/plugins/medusa-config.js index 6927791ed71dc..843f7264edfed 100644 --- a/integration-tests/plugins/medusa-config.js +++ b/integration-tests/plugins/medusa-config.js @@ -71,5 +71,10 @@ module.exports = { resources: "shared", resolve: "@medusajs/promotion", }, + [Modules.SALES_CHANNEL]: { + scope: "internal", + resources: "shared", + resolve: "@medusajs/sales-channel", + }, }, } 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 index b3d0346d18d44..bd85cbc1233bb 100644 --- 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 @@ -1,5 +1,6 @@ import { MedusaV2Flag } from "@medusajs/utils" import { WorkflowArguments } from "@medusajs/workflows-sdk" +import { Modules } from "@medusajs/modules-sdk" type HandlerInputData = { cart: { @@ -30,10 +31,10 @@ export async function attachCartToSalesChannel({ const salesChannel = data[Aliases.SalesChannel] await remoteLink.create({ - cartService: { + [Modules.CART]: { cart_id: cart.id, }, - salesChannelService: { + [Modules.SALES_CHANNEL]: { sales_channel_id: salesChannel.sales_channel_id, }, }) 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 index 24f47642be9e4..92fa59d9b70bb 100644 --- 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 @@ -1,5 +1,6 @@ import { MedusaV2Flag } from "@medusajs/utils" import { WorkflowArguments } from "@medusajs/workflows-sdk" +import { Modules } from "@medusajs/modules-sdk" type HandlerInputData = { cart: { @@ -30,10 +31,10 @@ export async function detachCartFromSalesChannel({ const salesChannel = data[Aliases.SalesChannel] await remoteLink.dismiss({ - cartService: { + [Modules.CART]: { cart_id: cart.id, }, - salesChannelService: { + [Modules.SALES_CHANNEL]: { sales_channel_id: salesChannel.sales_channel_id, }, }) diff --git a/packages/core-flows/src/handlers/product/attach-sales-channel-to-products.ts b/packages/core-flows/src/handlers/product/attach-sales-channel-to-products.ts index d76792be6d940..88ab7544fe3f6 100644 --- a/packages/core-flows/src/handlers/product/attach-sales-channel-to-products.ts +++ b/packages/core-flows/src/handlers/product/attach-sales-channel-to-products.ts @@ -51,7 +51,7 @@ export async function attachSalesChannelToProducts({ [Modules.PRODUCT]: { product_id: id, }, - salesChannelService: { + [Modules.SALES_CHANNEL]: { sales_channel_id: salesChannelId, }, }) diff --git a/packages/core-flows/src/handlers/product/detach-sales-channel-from-products.ts b/packages/core-flows/src/handlers/product/detach-sales-channel-from-products.ts index 31d029ab9ec1a..cc0b3d95f0f6e 100644 --- a/packages/core-flows/src/handlers/product/detach-sales-channel-from-products.ts +++ b/packages/core-flows/src/handlers/product/detach-sales-channel-from-products.ts @@ -49,7 +49,7 @@ export async function detachSalesChannelFromProducts({ [Modules.PRODUCT]: { product_id: id, }, - salesChannelService: { + [Modules.SALES_CHANNEL]: { sales_channel_id: salesChannelId, }, }) diff --git a/packages/link-modules/src/definitions/cart-sales-channel.ts b/packages/link-modules/src/definitions/cart-sales-channel.ts index 86b06ecd1f0e8..e990437ff47d7 100644 --- a/packages/link-modules/src/definitions/cart-sales-channel.ts +++ b/packages/link-modules/src/definitions/cart-sales-channel.ts @@ -1,5 +1,6 @@ import { ModuleJoinerConfig } from "@medusajs/types" import { LINKS } from "../links" +import { Modules } from "@medusajs/modules-sdk" export const CartSalesChannel: ModuleJoinerConfig = { serviceName: LINKS.CartSalesChannel, @@ -19,14 +20,14 @@ export const CartSalesChannel: ModuleJoinerConfig = { primaryKeys: ["id", "cart_id", "sales_channel_id"], relationships: [ { - serviceName: "cartService", + serviceName: Modules.CART, isInternalService: true, primaryKey: "id", foreignKey: "cart_id", alias: "cart", }, { - serviceName: "salesChannelService", + serviceName: Modules.SALES_CHANNEL, isInternalService: true, primaryKey: "id", foreignKey: "sales_channel_id", @@ -35,7 +36,7 @@ export const CartSalesChannel: ModuleJoinerConfig = { ], extends: [ { - serviceName: "cartService", + serviceName: Modules.CART, fieldAlias: { sales_channel: "sales_channel_link.sales_channel", }, @@ -48,7 +49,7 @@ export const CartSalesChannel: ModuleJoinerConfig = { }, }, { - serviceName: "salesChannelService", + serviceName: Modules.SALES_CHANNEL, fieldAlias: { carts: "cart_link.cart", }, diff --git a/packages/link-modules/src/definitions/product-sales-channel.ts b/packages/link-modules/src/definitions/product-sales-channel.ts index 8d675284e7e9c..a7cc47688c3a8 100644 --- a/packages/link-modules/src/definitions/product-sales-channel.ts +++ b/packages/link-modules/src/definitions/product-sales-channel.ts @@ -26,7 +26,7 @@ export const ProductSalesChannel: ModuleJoinerConfig = { alias: "product", }, { - serviceName: "salesChannelService", + serviceName: Modules.SALES_CHANNEL, isInternalService: true, primaryKey: "id", foreignKey: "sales_channel_id", @@ -48,7 +48,7 @@ export const ProductSalesChannel: ModuleJoinerConfig = { }, }, { - serviceName: "salesChannelService", + serviceName: Modules.SALES_CHANNEL, relationship: { serviceName: LINKS.ProductSalesChannel, isInternalService: true, diff --git a/packages/link-modules/src/links.ts b/packages/link-modules/src/links.ts index 5f6c39dfff5a0..9d2a479ef202e 100644 --- a/packages/link-modules/src/links.ts +++ b/packages/link-modules/src/links.ts @@ -25,25 +25,25 @@ export const LINKS = { ProductSalesChannel: composeLinkName( Modules.PRODUCT, "product_id", - "salesChannelService", + Modules.SALES_CHANNEL, "sales_channel_id" ), CartSalesChannel: composeLinkName( - "cartService", + Modules.CART, "cart_id", - "salesChannelService", + Modules.SALES_CHANNEL, "sales_channel_id" ), OrderSalesChannel: composeLinkName( "orderService", "order_id", - "salesChannelService", + Modules.SALES_CHANNEL, "sales_channel_id" ), PublishableApiKeySalesChannel: composeLinkName( "publishableApiKeyService", "publishable_key_id", - "salesChannelService", + Modules.SALES_CHANNEL, "sales_channel_id" ), } diff --git a/packages/medusa/src/joiner-configs/cart-service.ts b/packages/medusa/src/joiner-configs/cart-service.ts index f236208ebfe6e..878cfe2cdd3af 100644 --- a/packages/medusa/src/joiner-configs/cart-service.ts +++ b/packages/medusa/src/joiner-configs/cart-service.ts @@ -4,7 +4,7 @@ import { ModuleJoinerConfig } from "@medusajs/types" import { Cart } from "../models" export default { - serviceName: "cartService", + serviceName: Modules.CART, primaryKeys: ["id"], linkableKeys: { cart_id: "Cart" }, alias: { diff --git a/packages/medusa/src/joiner-configs/index.ts b/packages/medusa/src/joiner-configs/index.ts index 7f7849d2d7fbd..fb21643b5d92d 100644 --- a/packages/medusa/src/joiner-configs/index.ts +++ b/packages/medusa/src/joiner-configs/index.ts @@ -1,6 +1,5 @@ export * as cart from "./cart-service" export * as customer from "./customer-service" export * as region from "./region-service" -export * as salesChannel from "./sales-channel-service" export * as shippingProfile from "./shipping-profile-service" export * as publishableApiKey from "./publishable-api-key-service" diff --git a/packages/medusa/src/joiner-configs/sales-channel-service.ts b/packages/medusa/src/joiner-configs/sales-channel-service.ts deleted file mode 100644 index c67996455eb9c..0000000000000 --- a/packages/medusa/src/joiner-configs/sales-channel-service.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ModuleJoinerConfig } from "@medusajs/types" - -export default { - serviceName: "salesChannelService", - primaryKeys: ["id"], - linkableKeys: { sales_channel_id: "SalesChannel" }, - schema: ` - scalar Date - scalar JSON - - type SalesChannel { - id: ID! - name: String! - description: String! - is_disabled: Boolean - created_at: Date! - updated_at: Date! - deleted_at: Date - metadata: JSON - } - `, - alias: [ - { - name: "sales_channel", - args: { entity: "SalesChannel" }, - }, - { - name: "sales_channels", - args: { entity: "SalesChannel" }, - }, - ], -} as ModuleJoinerConfig diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index 25e7bf30a1319..eb98af721a5fa 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -69,7 +69,7 @@ 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" +import { Modules, RemoteLink } from "@medusajs/modules-sdk" type InjectedDependencies = { manager: EntityManager @@ -501,10 +501,10 @@ class CartService extends TransactionBaseService { ) await this.remoteLink_.create({ - cartService: { + [Modules.CART]: { cart_id: cart.id, }, - salesChannelService: { + [Modules.SALES_CHANNEL]: { sales_channel_id: salesChannel.id, }, }) @@ -1294,20 +1294,20 @@ class CartService extends TransactionBaseService { if (this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key)) { if (cart.sales_channel_id) { await this.remoteLink_.dismiss({ - cartService: { + [Modules.CART]: { cart_id: cart.id, }, - salesChannelService: { + [Modules.SALES_CHANNEL]: { sales_channel_id: cart.sales_channel_id, }, }) } await this.remoteLink_.create({ - cartService: { + [Modules.CART]: { cart_id: cart.id, }, - salesChannelService: { + [Modules.SALES_CHANNEL]: { sales_channel_id: salesChannel.id, }, }) diff --git a/packages/modules-sdk/src/definitions.ts b/packages/modules-sdk/src/definitions.ts index 4be77eb5e331a..3f6cdf12ae8e2 100644 --- a/packages/modules-sdk/src/definitions.ts +++ b/packages/modules-sdk/src/definitions.ts @@ -17,6 +17,7 @@ export enum Modules { PROMOTION = "promotion", AUTHENTICATION = "authentication", WORKFLOW_ENGINE = "workflows", + SALES_CHANNEL = "salesChannel", CART = "cart", CUSTOMER = "customer", PAYMENT = "payment", @@ -32,6 +33,7 @@ export enum ModuleRegistrationName { PROMOTION = "promotionModuleService", AUTHENTICATION = "authenticationModuleService", WORKFLOW_ENGINE = "workflowsModuleService", + SALES_CHANNEL = "salesChannelModuleService", CART = "cartModuleService", CUSTOMER = "customerModuleService", PAYMENT = "paymentModuleService", @@ -48,6 +50,7 @@ export const MODULE_PACKAGE_NAMES = { [Modules.PROMOTION]: "@medusajs/promotion", [Modules.AUTHENTICATION]: "@medusajs/authentication", [Modules.WORKFLOW_ENGINE]: "@medusajs/workflow-engine-inmemory", + [Modules.SALES_CHANNEL]: "@medusajs/sales-channel", [Modules.CART]: "@medusajs/cart", [Modules.CUSTOMER]: "@medusajs/customer", [Modules.PAYMENT]: "@medusajs/payment", @@ -182,6 +185,20 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = resources: MODULE_RESOURCE_TYPE.SHARED, }, }, + [Modules.SALES_CHANNEL]: { + key: Modules.SALES_CHANNEL, + registrationName: ModuleRegistrationName.SALES_CHANNEL, + defaultPackage: false, + label: upperCaseFirst(ModuleRegistrationName.SALES_CHANNEL), + isRequired: false, + canOverride: true, + isQueryable: true, + dependencies: ["logger"], + defaultModuleDeclaration: { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.SHARED, + }, + }, [Modules.CART]: { key: Modules.CART, registrationName: ModuleRegistrationName.CART, diff --git a/packages/sales-channel/.gitignore b/packages/sales-channel/.gitignore new file mode 100644 index 0000000000000..874c6c69d3341 --- /dev/null +++ b/packages/sales-channel/.gitignore @@ -0,0 +1,6 @@ +/dist +node_modules +.DS_store +.env* +.env +*.sql diff --git a/packages/sales-channel/README.md b/packages/sales-channel/README.md new file mode 100644 index 0000000000000..0886078716816 --- /dev/null +++ b/packages/sales-channel/README.md @@ -0,0 +1,3 @@ +# Sales Channel Module + +Sales Channel module enables management of sales channels that are used for grouping Products/Carts/Orders etc. diff --git a/packages/sales-channel/integration-tests/__fixtures__/index.ts b/packages/sales-channel/integration-tests/__fixtures__/index.ts new file mode 100644 index 0000000000000..919cd51e4e6e4 --- /dev/null +++ b/packages/sales-channel/integration-tests/__fixtures__/index.ts @@ -0,0 +1,40 @@ +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { SalesChannel } from "@models" + +const salesChannelData = [ + { + id: "channel-1", + name: "Channel 1", + description: "Channel description 1", + is_disabled: false, + }, + { + id: "channel-2", + name: "Channel 2", + description: "Channel description 2", + is_disabled: false, + }, + { + id: "channel-3", + name: "Channel 3", + description: "Channel description 3", + is_disabled: true, + }, +] + +export async function createSalesChannels( + manager: SqlEntityManager, + channelData: any[] = salesChannelData +): Promise { + const channels: SalesChannel[] = [] + + for (let data of channelData) { + const sc = manager.create(SalesChannel, data) + + channels.push(sc) + } + + await manager.persistAndFlush(channels) + + return channels +} diff --git a/packages/sales-channel/integration-tests/__tests__/services/sales-channel-module.spec.ts b/packages/sales-channel/integration-tests/__tests__/services/sales-channel-module.spec.ts new file mode 100644 index 0000000000000..4e2cf514193e7 --- /dev/null +++ b/packages/sales-channel/integration-tests/__tests__/services/sales-channel-module.spec.ts @@ -0,0 +1,248 @@ +import { SqlEntityManager } from "@mikro-orm/postgresql" + +import { ISalesChannelModuleService } from "@medusajs/types" + +import { initialize } from "../../../src" + +import { DB_URL, MikroOrmWrapper } from "../../utils" +import { createSalesChannels } from "../../__fixtures__" + +jest.setTimeout(30000) + +describe("Sales Channel Service", () => { + let service: ISalesChannelModuleService + let testManager: SqlEntityManager + let repositoryManager: SqlEntityManager + + beforeEach(async () => { + await MikroOrmWrapper.setupDatabase() + repositoryManager = await MikroOrmWrapper.forkManager() + + service = await initialize({ + database: { + clientUrl: DB_URL, + schema: process.env.MEDUSA_SALES_CHANNEL_DB_SCHEMA, + }, + }) + + testManager = await MikroOrmWrapper.forkManager() + + await createSalesChannels(testManager) + }) + + afterEach(async () => { + await MikroOrmWrapper.clearDatabase() + }) + + describe("create", () => { + it("should create a SalesChannel successfully", async () => { + const [created] = await service.create([ + { + name: "test", + description: "test", + }, + ]) + + const [channel] = await service.list({ + name: [created.name], + }) + + expect(channel.name).toEqual("test") + expect(channel.description).toEqual("test") + }) + }) + + describe("retrieve", () => { + const id = "channel-1" + + it("should return SalesChannel for the given id", async () => { + const result = await service.retrieve(id) + + expect(result).toEqual( + expect.objectContaining({ + id, + }) + ) + }) + + it("should throw an error when SalesChannelId with id does not exist", async () => { + let error + + try { + await service.retrieve("does-not-exist") + } catch (e) { + error = e + } + + expect(error.message).toEqual( + "SalesChannel with id: does-not-exist was not found" + ) + }) + }) + + describe("update", () => { + const id = "channel-2" + + it("should update the name of the SalesChannel successfully", async () => { + await service.update([ + { + id, + name: "Update name 2", + is_disabled: true, + }, + ]) + + const channel = await service.retrieve(id) + + expect(channel.name).toEqual("Update name 2") + expect(channel.is_disabled).toEqual(true) + }) + + it("should throw an error when a id does not exist", async () => { + let error + + try { + await service.update([ + { + id: "does-not-exist", + }, + ]) + } catch (e) { + error = e + } + + expect(error.message).toEqual( + 'SalesChannel with id "does-not-exist" not found' + ) + }) + }) + + describe("list", () => { + it("should return a list of SalesChannels", async () => { + const result = await service.list() + + expect(result).toEqual([ + expect.objectContaining({ + id: "channel-1", + }), + expect.objectContaining({ + id: "channel-2", + }), + expect.objectContaining({ + id: "channel-3", + }), + ]) + }) + + it("should list SalesChannels by name", async () => { + const result = await service.list({ + name: ["Channel 2", "Channel 3"], + }) + + expect(result).toEqual([ + expect.objectContaining({ + id: "channel-2", + }), + expect.objectContaining({ + id: "channel-3", + }), + ]) + }) + }) + + describe("listAndCount", () => { + it("should return sales channels and count", async () => { + const [result, count] = await service.listAndCount() + + expect(count).toEqual(3) + expect(result).toEqual([ + expect.objectContaining({ + id: "channel-1", + }), + expect.objectContaining({ + id: "channel-2", + }), + expect.objectContaining({ + id: "channel-3", + }), + ]) + }) + + it("should return sales channels and count when filtered", async () => { + const [result, count] = await service.listAndCount({ + id: ["channel-2"], + }) + + expect(count).toEqual(1) + expect(result).toEqual([ + expect.objectContaining({ + id: "channel-2", + }), + ]) + }) + + it("should return sales channels and count when using skip and take", async () => { + const [results, count] = await service.listAndCount( + {}, + { skip: 1, take: 1 } + ) + + expect(count).toEqual(3) + expect(results).toEqual([ + expect.objectContaining({ + id: "channel-2", + }), + ]) + }) + + it("should return requested fields", async () => { + const [result, count] = await service.listAndCount( + {}, + { + take: 1, + select: ["id", "name"], + } + ) + + const serialized = JSON.parse(JSON.stringify(result)) + + expect(count).toEqual(3) + expect(serialized).toEqual([ + { + id: "channel-1", + name: "Channel 1", + }, + ]) + }) + + it("should filter disabled channels", async () => { + const [result, count] = await service.listAndCount( + { is_disabled: true }, + { select: ["id"] } + ) + + const serialized = JSON.parse(JSON.stringify(result)) + + expect(count).toEqual(1) + expect(serialized).toEqual([ + { + id: "channel-3", + }, + ]) + }) + }) + + describe("delete", () => { + const id = "channel-2" + + it("should delete the SalesChannel given an id successfully", async () => { + await service.delete([id]) + + const result = await service.list({ + id: [id], + }) + + expect(result).toHaveLength(0) + }) + }) +}) diff --git a/packages/sales-channel/integration-tests/setup-env.js b/packages/sales-channel/integration-tests/setup-env.js new file mode 100644 index 0000000000000..5daa6d85845f9 --- /dev/null +++ b/packages/sales-channel/integration-tests/setup-env.js @@ -0,0 +1,6 @@ +if (typeof process.env.DB_TEMP_NAME === "undefined") { + const tempName = parseInt(process.env.JEST_WORKER_ID || "1") + process.env.DB_TEMP_NAME = `medusa-sales-channel-integration-${tempName}` +} + +process.env.MEDUSA_SALES_CHANNEL_DB_SCHEMA = "public" diff --git a/packages/sales-channel/integration-tests/setup.js b/packages/sales-channel/integration-tests/setup.js new file mode 100644 index 0000000000000..43f99aab4ac94 --- /dev/null +++ b/packages/sales-channel/integration-tests/setup.js @@ -0,0 +1,3 @@ +import { JestUtils } from "medusa-test-utils" + +JestUtils.afterAllHookDropDatabase() diff --git a/packages/sales-channel/integration-tests/utils/config.ts b/packages/sales-channel/integration-tests/utils/config.ts new file mode 100644 index 0000000000000..73f470e65ba0b --- /dev/null +++ b/packages/sales-channel/integration-tests/utils/config.ts @@ -0,0 +1,6 @@ +import { ModuleServiceInitializeOptions } from "@medusajs/types" + +export const databaseOptions: ModuleServiceInitializeOptions["database"] = { + schema: "public", + clientUrl: "medusa-sales-channel-test", +} diff --git a/packages/sales-channel/integration-tests/utils/database.ts b/packages/sales-channel/integration-tests/utils/database.ts new file mode 100644 index 0000000000000..40496a5b41aa5 --- /dev/null +++ b/packages/sales-channel/integration-tests/utils/database.ts @@ -0,0 +1,18 @@ +import { TestDatabaseUtils } from "medusa-test-utils" + +import * as SalesChannelModels from "@models" + +const pathToMigrations = "../../src/migrations" +const mikroOrmEntities = SalesChannelModels as unknown as any[] + +export const MikroOrmWrapper = TestDatabaseUtils.getMikroOrmWrapper( + mikroOrmEntities, + pathToMigrations +) + +export const MikroOrmConfig = TestDatabaseUtils.getMikroOrmConfig( + mikroOrmEntities, + pathToMigrations +) + +export const DB_URL = TestDatabaseUtils.getDatabaseURL() diff --git a/packages/sales-channel/integration-tests/utils/index.ts b/packages/sales-channel/integration-tests/utils/index.ts new file mode 100644 index 0000000000000..6b917ed30e5e7 --- /dev/null +++ b/packages/sales-channel/integration-tests/utils/index.ts @@ -0,0 +1 @@ +export * from "./database" diff --git a/packages/sales-channel/jest.config.js b/packages/sales-channel/jest.config.js new file mode 100644 index 0000000000000..456054fe8ae27 --- /dev/null +++ b/packages/sales-channel/jest.config.js @@ -0,0 +1,22 @@ +module.exports = { + moduleNameMapper: { + "^@models": "/src/models", + "^@services": "/src/services", + "^@repositories": "/src/repositories", + "^@types": "/src/types", + }, + transform: { + "^.+\\.[jt]s?$": [ + "ts-jest", + { + tsConfig: "tsconfig.spec.json", + isolatedModules: true, + }, + ], + }, + testEnvironment: `node`, + moduleFileExtensions: [`js`, `ts`], + modulePathIgnorePatterns: ["dist/"], + setupFiles: ["/integration-tests/setup-env.js"], + setupFilesAfterEnv: ["/integration-tests/setup.js"], +} diff --git a/packages/sales-channel/mikro-orm.config.dev.ts b/packages/sales-channel/mikro-orm.config.dev.ts new file mode 100644 index 0000000000000..48d29b9bb442c --- /dev/null +++ b/packages/sales-channel/mikro-orm.config.dev.ts @@ -0,0 +1,8 @@ +import * as entities from "./src/models" + +module.exports = { + entities: Object.values(entities), + schema: "public", + clientUrl: "postgres://postgres@localhost/medusa-sales-channel", + type: "postgresql", +} diff --git a/packages/sales-channel/package.json b/packages/sales-channel/package.json new file mode 100644 index 0000000000000..a08ad96d646cd --- /dev/null +++ b/packages/sales-channel/package.json @@ -0,0 +1,61 @@ +{ + "name": "@medusajs/sales-channel", + "version": "0.1.0", + "description": "Medusa Sales Channel module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "engines": { + "node": ">=16" + }, + "bin": { + "medusa-sales-channel-seed": "dist/scripts/bin/run-seed.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/medusajs/medusa", + "directory": "packages/sales-channel" + }, + "publishConfig": { + "access": "public" + }, + "author": "Medusa", + "license": "MIT", + "scripts": { + "watch": "tsc --build --watch", + "watch:test": "tsc --build tsconfig.spec.json --watch", + "prepublishOnly": "cross-env NODE_ENV=production tsc --build && tsc-alias -p tsconfig.json", + "build": "rimraf dist && tsc --build && tsc-alias -p tsconfig.json", + "test": "jest --runInBand --bail --forceExit -- src/**/__tests__/**/*.ts", + "test:integration": "jest --runInBand --forceExit -- integration-tests/**/__tests__/**/*.ts", + "migration:generate": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:generate", + "migration:initial": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create --initial", + "migration:create": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create", + "migration:up": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:up", + "orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear" + }, + "devDependencies": { + "@mikro-orm/cli": "5.9.7", + "cross-env": "^5.2.1", + "jest": "^29.6.3", + "medusa-test-utils": "^1.1.40", + "rimraf": "^3.0.2", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "tsc-alias": "^1.8.6", + "typescript": "^5.1.6" + }, + "dependencies": { + "@medusajs/modules-sdk": "^1.12.4", + "@medusajs/types": "^1.11.8", + "@medusajs/utils": "^1.11.1", + "@mikro-orm/core": "5.9.7", + "@mikro-orm/migrations": "5.9.7", + "@mikro-orm/postgresql": "5.9.7", + "awilix": "^8.0.0", + "dotenv": "^16.1.4", + "knex": "2.4.2" + } +} diff --git a/packages/sales-channel/src/index.ts b/packages/sales-channel/src/index.ts new file mode 100644 index 0000000000000..cde39437b10bb --- /dev/null +++ b/packages/sales-channel/src/index.ts @@ -0,0 +1,27 @@ +import { Modules } from "@medusajs/modules-sdk" +import { ModulesSdkUtils } from "@medusajs/utils" + +import * as SalesChannelModels from "@models" + +import { moduleDefinition } from "./module-definition" + +export default moduleDefinition + +const migrationScriptOptions = { + moduleName: Modules.SALES_CHANNEL, + models: SalesChannelModels, + pathToMigrations: __dirname + "/migrations", +} + +export const runMigrations = ModulesSdkUtils.buildMigrationScript( + migrationScriptOptions +) +export const revertMigration = ModulesSdkUtils.buildRevertMigrationScript( + migrationScriptOptions +) + +export * from "./initialize" +export * from "./types" +export * from "./loaders" +export * from "./models" +export * from "./services" diff --git a/packages/sales-channel/src/initialize/index.ts b/packages/sales-channel/src/initialize/index.ts new file mode 100644 index 0000000000000..74a84a8116a6f --- /dev/null +++ b/packages/sales-channel/src/initialize/index.ts @@ -0,0 +1,34 @@ +import { + ExternalModuleDeclaration, + InternalModuleDeclaration, + MedusaModule, + MODULE_PACKAGE_NAMES, + Modules, +} from "@medusajs/modules-sdk" +import { ModulesSdkTypes, ISalesChannelModuleService } from "@medusajs/types" +import { InitializeModuleInjectableDependencies } from "@types" + +import { moduleDefinition } from "../module-definition" + +export const initialize = async ( + options?: + | ModulesSdkTypes.ModuleServiceInitializeOptions + | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions + | ExternalModuleDeclaration + | InternalModuleDeclaration, + injectedDependencies?: InitializeModuleInjectableDependencies +): Promise => { + const serviceKey = Modules.SALES_CHANNEL + + const loaded = await MedusaModule.bootstrap({ + moduleKey: serviceKey, + defaultPath: MODULE_PACKAGE_NAMES[Modules.SALES_CHANNEL], + declaration: options as + | InternalModuleDeclaration + | ExternalModuleDeclaration, + injectedDependencies, + moduleExports: moduleDefinition, + }) + + return loaded[serviceKey] +} diff --git a/packages/sales-channel/src/joiner-config.ts b/packages/sales-channel/src/joiner-config.ts new file mode 100644 index 0000000000000..44673e2f697f3 --- /dev/null +++ b/packages/sales-channel/src/joiner-config.ts @@ -0,0 +1,31 @@ +import { Modules } from "@medusajs/modules-sdk" +import { ModuleJoinerConfig } from "@medusajs/types" +import { MapToConfig } from "@medusajs/utils" +import { SalesChannel } from "@models" + +export const LinkableKeys = { + sales_channel_id: SalesChannel.name, +} + +const entityLinkableKeysMap: MapToConfig = {} +Object.entries(LinkableKeys).forEach(([key, value]) => { + entityLinkableKeysMap[value] ??= [] + entityLinkableKeysMap[value].push({ + mapTo: key, + valueFrom: key.split("_").pop()!, + }) +}) + +export const entityNameToLinkableKeysMap: MapToConfig = entityLinkableKeysMap + +export const joinerConfig: ModuleJoinerConfig = { + serviceName: Modules.SALES_CHANNEL, + primaryKeys: ["id"], + linkableKeys: LinkableKeys, + alias: [ + { + name: ["sales_channel", "sales_channels"], + args: { entity: "SalesChannel" }, + }, + ], +} as ModuleJoinerConfig diff --git a/packages/sales-channel/src/loaders/connection.ts b/packages/sales-channel/src/loaders/connection.ts new file mode 100644 index 0000000000000..fc88330aa2b30 --- /dev/null +++ b/packages/sales-channel/src/loaders/connection.ts @@ -0,0 +1,37 @@ +import { + InternalModuleDeclaration, + LoaderOptions, + Modules, +} from "@medusajs/modules-sdk" +import { ModulesSdkTypes } from "@medusajs/types" +import { ModulesSdkUtils } from "@medusajs/utils" +import { EntitySchema } from "@mikro-orm/core" + +import * as SalesChannelModels from "@models" + +export default async ( + { + options, + container, + logger, + }: LoaderOptions< + | ModulesSdkTypes.ModuleServiceInitializeOptions + | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions + >, + moduleDeclaration?: InternalModuleDeclaration +): Promise => { + const entities = Object.values( + SalesChannelModels + ) as unknown as EntitySchema[] + const pathToMigrations = __dirname + "/../migrations" + + await ModulesSdkUtils.mikroOrmConnectionLoader({ + moduleName: Modules.SALES_CHANNEL, + entities, + container, + options, + moduleDeclaration, + logger, + pathToMigrations, + }) +} diff --git a/packages/sales-channel/src/loaders/container.ts b/packages/sales-channel/src/loaders/container.ts new file mode 100644 index 0000000000000..28ea110f2dbd1 --- /dev/null +++ b/packages/sales-channel/src/loaders/container.ts @@ -0,0 +1,10 @@ +import { ModulesSdkUtils } from "@medusajs/utils" +import * as ModuleModels from "@models" +import * as ModuleRepositories from "@repositories" +import * as ModuleServices from "@services" + +export default ModulesSdkUtils.moduleContainerLoaderFactory({ + moduleModels: ModuleModels, + moduleRepositories: ModuleRepositories, + moduleServices: ModuleServices, +}) diff --git a/packages/sales-channel/src/loaders/index.ts b/packages/sales-channel/src/loaders/index.ts new file mode 100644 index 0000000000000..3614963d8c21e --- /dev/null +++ b/packages/sales-channel/src/loaders/index.ts @@ -0,0 +1,2 @@ +export * from "./connection" +export * from "./container" diff --git a/packages/sales-channel/src/migrations/.snapshot-medusa-sales-channel-tst.json b/packages/sales-channel/src/migrations/.snapshot-medusa-sales-channel-tst.json new file mode 100644 index 0000000000000..da19c5d3082ec --- /dev/null +++ b/packages/sales-channel/src/migrations/.snapshot-medusa-sales-channel-tst.json @@ -0,0 +1,105 @@ +{ + "namespaces": [ + "public" + ], + "name": "public", + "tables": [ + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "name": { + "name": "name", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "description": { + "name": "description", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "is_disabled": { + "name": "is_disabled", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "false", + "mappedType": "boolean" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "default": "now()", + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "sales_channel", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "deleted_at" + ], + "composite": false, + "keyName": "IDX_sales_channel_deleted_at", + "primary": false, + "unique": false + }, + { + "keyName": "sales_channel_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {} + } + ] +} diff --git a/packages/sales-channel/src/migrations/Migration20240115152146.ts b/packages/sales-channel/src/migrations/Migration20240115152146.ts new file mode 100644 index 0000000000000..ccf9f899f193c --- /dev/null +++ b/packages/sales-channel/src/migrations/Migration20240115152146.ts @@ -0,0 +1,12 @@ +import { Migration } from "@mikro-orm/migrations" + +export class Migration20240115152146 extends Migration { + async up(): Promise { + this.addSql( + 'create table if not exists "sales_channel" ("id" text not null, "name" text not null, "description" text null, "is_disabled" boolean not null default false, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "sales_channel_pkey" primary key ("id"));' + ) + this.addSql( + 'create index "IDX_sales_channel_deleted_at" on "sales_channel" ("deleted_at");' + ) + } +} diff --git a/packages/sales-channel/src/models/index.ts b/packages/sales-channel/src/models/index.ts new file mode 100644 index 0000000000000..fd3d46086821c --- /dev/null +++ b/packages/sales-channel/src/models/index.ts @@ -0,0 +1 @@ +export { default as SalesChannel } from "./sales-channel" diff --git a/packages/sales-channel/src/models/sales-channel.ts b/packages/sales-channel/src/models/sales-channel.ts new file mode 100644 index 0000000000000..9ecbb82631b31 --- /dev/null +++ b/packages/sales-channel/src/models/sales-channel.ts @@ -0,0 +1,61 @@ +import { DALUtils, generateEntityId } from "@medusajs/utils" + +import { + BeforeCreate, + Entity, + Filter, + Index, + OptionalProps, + PrimaryKey, + Property, +} from "@mikro-orm/core" +import { DAL } from "@medusajs/types" + +type SalesChannelOptionalProps = "is_disabled" | DAL.EntityDateColumns + +@Entity() +@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) +export default class SalesChannel { + [OptionalProps]?: SalesChannelOptionalProps + + @PrimaryKey({ columnType: "text" }) + id!: string + + @Property({ columnType: "text" }) + name!: string + + @Property({ columnType: "text", nullable: true }) + description: string | null = null + + @Property({ columnType: "boolean", default: false }) + is_disabled = false + + @Property({ + onCreate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date + + @Index({ name: "IDX_sales_channel_deleted_at" }) + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date | null = null + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "sc") + } + + @BeforeCreate() + onInit() { + this.id = generateEntityId(this.id, "sc") + } +} diff --git a/packages/sales-channel/src/module-definition.ts b/packages/sales-channel/src/module-definition.ts new file mode 100644 index 0000000000000..ea646d3f28183 --- /dev/null +++ b/packages/sales-channel/src/module-definition.ts @@ -0,0 +1,13 @@ +import { ModuleExports } from "@medusajs/types" +import { SalesChannelModuleService } from "@services" + +import loadConnection from "./loaders/connection" +import loadContainer from "./loaders/container" + +const service = SalesChannelModuleService +const loaders = [loadContainer, loadConnection] as any + +export const moduleDefinition: ModuleExports = { + service, + loaders, +} diff --git a/packages/sales-channel/src/repositories/index.ts b/packages/sales-channel/src/repositories/index.ts new file mode 100644 index 0000000000000..147c9cc259fa4 --- /dev/null +++ b/packages/sales-channel/src/repositories/index.ts @@ -0,0 +1 @@ +export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils" diff --git a/packages/sales-channel/src/scripts/bin/run-seed.ts b/packages/sales-channel/src/scripts/bin/run-seed.ts new file mode 100644 index 0000000000000..750138065c43e --- /dev/null +++ b/packages/sales-channel/src/scripts/bin/run-seed.ts @@ -0,0 +1,31 @@ +#!/usr/bin/env node + +import { ModulesSdkUtils } from "@medusajs/utils" +import { Modules } from "@medusajs/modules-sdk" +import * as ProductModels from "@models" +import { createSalesChannels } from "../seed-utils" +import { EOL } from "os" + +const args = process.argv +const path = args.pop() as string + +export default (async () => { + const { config } = await import("dotenv") + config() + if (!path) { + throw new Error( + `filePath is required.${EOL}Example: medusa-product-seed ` + ) + } + + const run = ModulesSdkUtils.buildSeedScript({ + moduleName: Modules.PRODUCT, + models: ProductModels, + pathToMigrations: __dirname + "/../../migrations", + seedHandler: async ({ manager, data }) => { + const { salesChannelData } = data + await createSalesChannels(manager, salesChannelData) + }, + }) + await run({ path }) +})() diff --git a/packages/sales-channel/src/scripts/seed-utils.ts b/packages/sales-channel/src/scripts/seed-utils.ts new file mode 100644 index 0000000000000..61c3f27a95d15 --- /dev/null +++ b/packages/sales-channel/src/scripts/seed-utils.ts @@ -0,0 +1,16 @@ +import { SalesChannel } from "@models" +import { RequiredEntityData } from "@mikro-orm/core" +import { SqlEntityManager } from "@mikro-orm/postgresql" + +export async function createSalesChannels( + manager: SqlEntityManager, + data: RequiredEntityData[] +) { + const channels = data.map((channel) => { + return manager.create(SalesChannel, channel) + }) + + await manager.persistAndFlush(channels) + + return channels +} diff --git a/packages/sales-channel/src/services/__fixtures__/sales-channel.ts b/packages/sales-channel/src/services/__fixtures__/sales-channel.ts new file mode 100644 index 0000000000000..c4a716dde4ad6 --- /dev/null +++ b/packages/sales-channel/src/services/__fixtures__/sales-channel.ts @@ -0,0 +1,17 @@ +import { SalesChannelService, SalesChannelModuleService } from "@services" +import { asClass, asValue, createContainer } from "awilix" + +export const mockContainer = createContainer() + +mockContainer.register({ + transaction: asValue(async (task) => await task()), + salesChannelRepository: asValue({ + find: jest.fn().mockImplementation(async ({ where: { code } }) => { + return [{}] + }), + findAndCount: jest.fn().mockResolvedValue([[], 0]), + getFreshManager: jest.fn().mockResolvedValue({}), + }), + salesChannelService: asClass(SalesChannelService), + salesChannelModuleService: asClass(SalesChannelModuleService), +}) diff --git a/packages/sales-channel/src/services/__tests__/sales-channle.spec.ts b/packages/sales-channel/src/services/__tests__/sales-channle.spec.ts new file mode 100644 index 0000000000000..d8dab120bdf96 --- /dev/null +++ b/packages/sales-channel/src/services/__tests__/sales-channle.spec.ts @@ -0,0 +1,34 @@ +import { mockContainer } from "../__fixtures__/sales-channel" + +describe("Sales channel service", function () { + beforeEach(function () { + jest.clearAllMocks() + }) + + it("should list sales channels with filters and relations", async function () { + const salesChannelRepository = mockContainer.resolve( + "salesChannelRepository" + ) + const salesChannelService = mockContainer.resolve("salesChannelService") + + const config = { + select: ["id", "name"], + } + + await salesChannelService.list({}, config) + + expect(salesChannelRepository.find).toHaveBeenCalledWith( + { + where: {}, + options: { + fields: ["id", "name"], + limit: 15, + offset: 0, + withDeleted: undefined, + populate: [], + }, + }, + expect.any(Object) + ) + }) +}) diff --git a/packages/sales-channel/src/services/index.ts b/packages/sales-channel/src/services/index.ts new file mode 100644 index 0000000000000..117f8a36c6b86 --- /dev/null +++ b/packages/sales-channel/src/services/index.ts @@ -0,0 +1,2 @@ +export { default as SalesChannelService } from "./sales-channel" +export { default as SalesChannelModuleService } from "./sales-channel-module" diff --git a/packages/sales-channel/src/services/sales-channel-module.ts b/packages/sales-channel/src/services/sales-channel-module.ts new file mode 100644 index 0000000000000..270629f7c67a4 --- /dev/null +++ b/packages/sales-channel/src/services/sales-channel-module.ts @@ -0,0 +1,247 @@ +import { + Context, + DAL, + FilterableSalesChannelProps, + FindConfig, + InternalModuleDeclaration, + ISalesChannelModuleService, + ModuleJoinerConfig, + RestoreReturn, + SalesChannelDTO, + SoftDeleteReturn, +} from "@medusajs/types" +import { + InjectManager, + InjectTransactionManager, + mapObjectTo, + MedusaContext, +} from "@medusajs/utils" +import { CreateSalesChannelDTO, UpdateSalesChannelDTO } from "@medusajs/types" + +import { SalesChannel } from "@models" + +import SalesChannelService from "./sales-channel" +import { + joinerConfig, + entityNameToLinkableKeysMap, + LinkableKeys, +} from "../joiner-config" + +type InjectedDependencies = { + baseRepository: DAL.RepositoryService + salesChannelService: SalesChannelService +} + +export default class SalesChannelModuleService< + TEntity extends SalesChannel = SalesChannel +> implements ISalesChannelModuleService +{ + protected baseRepository_: DAL.RepositoryService + protected readonly salesChannelService_: SalesChannelService + + constructor( + { baseRepository, salesChannelService }: InjectedDependencies, + protected readonly moduleDeclaration: InternalModuleDeclaration + ) { + this.baseRepository_ = baseRepository + this.salesChannelService_ = salesChannelService + } + + __joinerConfig(): ModuleJoinerConfig { + return joinerConfig + } + + async create( + data: CreateSalesChannelDTO[], + sharedContext?: Context + ): Promise + + async create( + data: CreateSalesChannelDTO, + sharedContext?: Context + ): Promise + + @InjectTransactionManager("baseRepository_") + async create( + data: CreateSalesChannelDTO | CreateSalesChannelDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const input = Array.isArray(data) ? data : [data] + + const result = await this.salesChannelService_.create(input, sharedContext) + + return await this.baseRepository_.serialize( + Array.isArray(data) ? result : result[0], + { + populate: true, + } + ) + } + + async delete(ids: string[], sharedContext?: Context): Promise + + async delete(id: string, sharedContext?: Context): Promise + + @InjectTransactionManager("baseRepository_") + async delete( + ids: string | string[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const salesChannelIds = Array.isArray(ids) ? ids : [ids] + await this.salesChannelService_.delete(salesChannelIds, sharedContext) + } + + @InjectTransactionManager("baseRepository_") + protected async softDelete_( + salesChannelIds: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise<[TEntity[], Record]> { + return await this.salesChannelService_.softDelete( + salesChannelIds, + sharedContext + ) + } + + @InjectManager("baseRepository_") + async softDelete< + TReturnableLinkableKeys extends string = Lowercase< + keyof typeof LinkableKeys + > + >( + salesChannelIds: string[], + { returnLinkableKeys }: SoftDeleteReturn = {}, + sharedContext: Context = {} + ): Promise, string[]> | void> { + const [_, cascadedEntitiesMap] = await this.softDelete_( + salesChannelIds, + sharedContext + ) + + let mappedCascadedEntitiesMap + if (returnLinkableKeys) { + mappedCascadedEntitiesMap = mapObjectTo< + Record, string[]> + >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { + pick: returnLinkableKeys, + }) + } + + return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 + } + + @InjectTransactionManager("baseRepository_") + async restore_( + salesChannelIds: string[], + @MedusaContext() sharedContext: Context = {} + ): Promise<[TEntity[], Record]> { + return await this.salesChannelService_.restore( + salesChannelIds, + sharedContext + ) + } + + @InjectManager("baseRepository_") + async restore< + TReturnableLinkableKeys extends string = Lowercase< + keyof typeof LinkableKeys + > + >( + salesChannelIds: string[], + { returnLinkableKeys }: RestoreReturn = {}, + sharedContext: Context = {} + ): Promise, string[]> | void> { + const [_, cascadedEntitiesMap] = await this.restore_( + salesChannelIds, + sharedContext + ) + + let mappedCascadedEntitiesMap + if (returnLinkableKeys) { + mappedCascadedEntitiesMap = mapObjectTo< + Record, string[]> + >(cascadedEntitiesMap, entityNameToLinkableKeysMap, { + pick: returnLinkableKeys, + }) + } + + return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0 + } + + async update( + data: UpdateSalesChannelDTO[], + sharedContext?: Context + ): Promise + + async update( + data: UpdateSalesChannelDTO, + sharedContext?: Context + ): Promise + + @InjectTransactionManager("baseRepository_") + async update( + data: UpdateSalesChannelDTO | UpdateSalesChannelDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const input = Array.isArray(data) ? data : [data] + + const result = await this.salesChannelService_.update(input, sharedContext) + + return await this.baseRepository_.serialize( + Array.isArray(data) ? result : result[0], + { + populate: true, + } + ) + } + + @InjectManager("baseRepository_") + async retrieve( + salesChannelId: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const salesChannel = await this.salesChannelService_.retrieve( + salesChannelId, + config + ) + + return await this.baseRepository_.serialize(salesChannel, { + populate: true, + }) + } + + @InjectManager("baseRepository_") + async list( + filters: {} = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const salesChannels = await this.salesChannelService_.list(filters, config) + + return await this.baseRepository_.serialize( + salesChannels, + { + populate: true, + } + ) + } + + @InjectManager("baseRepository_") + async listAndCount( + filters: FilterableSalesChannelProps = {}, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise<[SalesChannelDTO[], number]> { + const [salesChannels, count] = await this.salesChannelService_.listAndCount( + filters, + config + ) + + return [ + await this.baseRepository_.serialize(salesChannels, { + populate: true, + }), + count, + ] + } +} diff --git a/packages/sales-channel/src/services/sales-channel.ts b/packages/sales-channel/src/services/sales-channel.ts new file mode 100644 index 0000000000000..83e6ef90b4bcd --- /dev/null +++ b/packages/sales-channel/src/services/sales-channel.ts @@ -0,0 +1,24 @@ +import { DAL } from "@medusajs/types" +import { ModulesSdkUtils } from "@medusajs/utils" +import { CreateSalesChannelDTO, UpdateSalesChannelDTO } from "@medusajs/types" + +import { SalesChannel } from "@models" + +type InjectedDependencies = { + salesChannelRepository: DAL.RepositoryService +} + +export default class SalesChannelService< + TEntity extends SalesChannel = SalesChannel +> extends ModulesSdkUtils.abstractServiceFactory< + InjectedDependencies, + { + create: CreateSalesChannelDTO + update: UpdateSalesChannelDTO + } +>(SalesChannel) { + constructor(container: InjectedDependencies) { + // @ts-ignore + super(...arguments) + } +} diff --git a/packages/sales-channel/src/types/index.ts b/packages/sales-channel/src/types/index.ts new file mode 100644 index 0000000000000..0f252977b02a2 --- /dev/null +++ b/packages/sales-channel/src/types/index.ts @@ -0,0 +1,5 @@ +import { Logger } from "@medusajs/types" + +export type InitializeModuleInjectableDependencies = { + logger?: Logger +} diff --git a/packages/sales-channel/src/types/repositories.ts b/packages/sales-channel/src/types/repositories.ts new file mode 100644 index 0000000000000..e1a90fef2194c --- /dev/null +++ b/packages/sales-channel/src/types/repositories.ts @@ -0,0 +1,15 @@ +import { DAL } from "@medusajs/types" + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +import { SalesChannel } from "@models" +import { CreateSalesChannelDTO, UpdateSalesChannelDTO } from "@medusajs/types" + +export interface ISalesChannelRepository< + TEntity extends SalesChannel = SalesChannel +> extends DAL.RepositoryService< + TEntity, + { + create: CreateSalesChannelDTO + update: UpdateSalesChannelDTO + } + > {} diff --git a/packages/sales-channel/tsconfig.json b/packages/sales-channel/tsconfig.json new file mode 100644 index 0000000000000..4b79cd603235c --- /dev/null +++ b/packages/sales-channel/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "lib": ["es2020"], + "target": "es2020", + "outDir": "./dist", + "esModuleInterop": true, + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": false, + "noImplicitReturns": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "allowJs": true, + "skipLibCheck": true, + "downlevelIteration": true, // to use ES5 specific tooling + "baseUrl": ".", + "resolveJsonModule": true, + "paths": { + "@models": ["./src/models"], + "@services": ["./src/services"], + "@repositories": ["./src/repositories"], + "@types": ["./src/types"] + } + }, + "include": ["src"], + "exclude": [ + "dist", + "./src/**/__tests__", + "./src/**/__mocks__", + "./src/**/__fixtures__", + "node_modules" + ] +} diff --git a/packages/sales-channel/tsconfig.spec.json b/packages/sales-channel/tsconfig.spec.json new file mode 100644 index 0000000000000..48e47e8cbb3be --- /dev/null +++ b/packages/sales-channel/tsconfig.spec.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "include": ["src", "integration-tests"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "sourceMap": true + } +} diff --git a/packages/types/src/sales-channel/common.ts b/packages/types/src/sales-channel/common.ts index 728640ce249f7..e3e7e4cdcc886 100644 --- a/packages/types/src/sales-channel/common.ts +++ b/packages/types/src/sales-channel/common.ts @@ -1,3 +1,5 @@ +import { BaseFilterable } from "../dal" + export interface SalesChannelLocationDTO { sales_channel_id: string location_id: string @@ -6,8 +8,16 @@ export interface SalesChannelLocationDTO { export interface SalesChannelDTO { id: string + name: string description: string | null is_disabled: boolean metadata: Record | null locations?: SalesChannelLocationDTO[] } + +export interface FilterableSalesChannelProps + extends BaseFilterable { + id?: string[] + name?: string[] + is_disabled?: boolean +} diff --git a/packages/types/src/sales-channel/index.ts b/packages/types/src/sales-channel/index.ts index 488a94fdffa50..0c73656566caa 100644 --- a/packages/types/src/sales-channel/index.ts +++ b/packages/types/src/sales-channel/index.ts @@ -1 +1,3 @@ export * from "./common" +export * from "./mutations" +export * from "./service" diff --git a/packages/types/src/sales-channel/mutations.ts b/packages/types/src/sales-channel/mutations.ts new file mode 100644 index 0000000000000..8cf15696aeeb2 --- /dev/null +++ b/packages/types/src/sales-channel/mutations.ts @@ -0,0 +1,12 @@ +export interface CreateSalesChannelDTO { + name: string + description?: string + is_disabled?: boolean +} + +export interface UpdateSalesChannelDTO { + id: string + name?: string + description?: string + is_disabled?: boolean +} diff --git a/packages/types/src/sales-channel/service.ts b/packages/types/src/sales-channel/service.ts new file mode 100644 index 0000000000000..adea3012a4608 --- /dev/null +++ b/packages/types/src/sales-channel/service.ts @@ -0,0 +1,59 @@ +import { IModuleService } from "../modules-sdk" +import { FilterableSalesChannelProps, SalesChannelDTO } from "./common" +import { FindConfig } from "../common" +import { Context } from "../shared-context" +import { RestoreReturn, SoftDeleteReturn } from "../dal" +import { CreateSalesChannelDTO, UpdateSalesChannelDTO } from "./mutations" + +export interface ISalesChannelModuleService extends IModuleService { + create( + data: CreateSalesChannelDTO[], + sharedContext?: Context + ): Promise + create( + data: CreateSalesChannelDTO, + sharedContext?: Context + ): Promise + + update( + data: UpdateSalesChannelDTO[], + sharedContext?: Context + ): Promise + update( + data: UpdateSalesChannelDTO, + sharedContext?: Context + ): Promise + + delete(ids: string[], sharedContext?: Context): Promise + delete(id: string, sharedContext?: Context): Promise + + retrieve( + id: string, + config?: FindConfig, + sharedContext?: Context + ): Promise + + list( + filters?: FilterableSalesChannelProps, + config?: FindConfig, + sharedContext?: Context + ): Promise + + listAndCount( + filters?: FilterableSalesChannelProps, + config?: FindConfig, + sharedContext?: Context + ): Promise<[SalesChannelDTO[], number]> + + softDelete( + salesChannelIds: string[], + config?: SoftDeleteReturn, + sharedContext?: Context + ): Promise | void> + + restore( + salesChannelIds: string[], + config?: RestoreReturn, + sharedContext?: Context + ): Promise | void> +} diff --git a/yarn.lock b/yarn.lock index 22a27292efadf..aa9baf3b88764 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8370,7 +8370,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/modules-sdk@^1.12.2, @medusajs/modules-sdk@^1.12.3, @medusajs/modules-sdk@^1.12.5, @medusajs/modules-sdk@^1.12.6, @medusajs/modules-sdk@^1.12.7, @medusajs/modules-sdk@^1.8.8, @medusajs/modules-sdk@workspace:^, @medusajs/modules-sdk@workspace:packages/modules-sdk": +"@medusajs/modules-sdk@^1.12.2, @medusajs/modules-sdk@^1.12.3, @medusajs/modules-sdk@^1.12.4, @medusajs/modules-sdk@^1.12.5, @medusajs/modules-sdk@^1.12.6, @medusajs/modules-sdk@^1.12.7, @medusajs/modules-sdk@^1.8.8, @medusajs/modules-sdk@workspace:^, @medusajs/modules-sdk@workspace:packages/modules-sdk": version: 0.0.0-use.local resolution: "@medusajs/modules-sdk@workspace:packages/modules-sdk" dependencies: @@ -8565,6 +8565,33 @@ __metadata: languageName: unknown linkType: soft +"@medusajs/sales-channel@workspace:packages/sales-channel": + version: 0.0.0-use.local + resolution: "@medusajs/sales-channel@workspace:packages/sales-channel" + dependencies: + "@medusajs/modules-sdk": ^1.12.4 + "@medusajs/types": ^1.11.8 + "@medusajs/utils": ^1.11.1 + "@mikro-orm/cli": 5.9.7 + "@mikro-orm/core": 5.9.7 + "@mikro-orm/migrations": 5.9.7 + "@mikro-orm/postgresql": 5.9.7 + awilix: ^8.0.0 + cross-env: ^5.2.1 + dotenv: ^16.1.4 + jest: ^29.6.3 + knex: 2.4.2 + medusa-test-utils: ^1.1.40 + rimraf: ^3.0.2 + ts-jest: ^29.1.1 + ts-node: ^10.9.1 + tsc-alias: ^1.8.6 + typescript: ^5.1.6 + bin: + medusa-sales-channel-seed: dist/scripts/bin/run-seed.js + languageName: unknown + linkType: soft + "@medusajs/stock-location@workspace:packages/stock-location": version: 0.0.0-use.local resolution: "@medusajs/stock-location@workspace:packages/stock-location" @@ -8608,7 +8635,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/types@^1.10.0, @medusajs/types@^1.11.10, @medusajs/types@^1.11.11, @medusajs/types@^1.11.5, @medusajs/types@^1.11.6, @medusajs/types@^1.11.9, @medusajs/types@^1.8.10, @medusajs/types@workspace:^, @medusajs/types@workspace:packages/types": +"@medusajs/types@^1.10.0, @medusajs/types@^1.11.10, @medusajs/types@^1.11.11, @medusajs/types@^1.11.5, @medusajs/types@^1.11.6, @medusajs/types@^1.11.8, @medusajs/types@^1.11.9, @medusajs/types@^1.8.10, @medusajs/types@workspace:^, @medusajs/types@workspace:packages/types": version: 0.0.0-use.local resolution: "@medusajs/types@workspace:packages/types" dependencies: @@ -8711,7 +8738,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/utils@^1.1.41, @medusajs/utils@^1.10.5, @medusajs/utils@^1.11.2, @medusajs/utils@^1.11.3, @medusajs/utils@^1.11.4, @medusajs/utils@^1.9.2, @medusajs/utils@^1.9.4, @medusajs/utils@workspace:^, @medusajs/utils@workspace:packages/utils": +"@medusajs/utils@^1.1.41, @medusajs/utils@^1.10.5, @medusajs/utils@^1.11.1, @medusajs/utils@^1.11.2, @medusajs/utils@^1.11.3, @medusajs/utils@^1.11.4, @medusajs/utils@^1.9.2, @medusajs/utils@^1.9.4, @medusajs/utils@workspace:^, @medusajs/utils@workspace:packages/utils": version: 0.0.0-use.local resolution: "@medusajs/utils@workspace:packages/utils" dependencies: