From 5a8a889c6d943d7c6ccea600ffe51ac1e13c1d85 Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Wed, 19 Jul 2023 15:35:36 -0300 Subject: [PATCH] feat(modules-sdk): Remote Query (#4463) * feat: Remote Query --- .../cache-inmemory/src/initialize/index.ts | 4 +- packages/cache-inmemory/tsconfig.spec.json | 2 +- packages/cache-redis/src/initialize/index.ts | 4 +- .../event-bus-local/src/initialize/index.ts | 4 +- .../event-bus-redis/src/initialize/index.ts | 4 +- packages/inventory/src/initialize/index.ts | 4 +- packages/inventory/src/joiner-config.ts | 51 ++++ packages/inventory/src/services/inventory.ts | 13 + packages/medusa/package.json | 1 - .../src/loaders/__tests__/models.spec.ts | 6 +- packages/medusa/src/loaders/models.ts | 4 +- packages/medusa/src/loaders/plugins.ts | 37 +-- packages/modules-sdk/package.json | 3 +- packages/modules-sdk/src/definitions.ts | 7 +- packages/modules-sdk/src/index.ts | 1 + .../src/loaders/__tests__/medusa-module.ts | 216 ++++++++++++++- packages/modules-sdk/src/medusa-module.ts | 152 ++++++++++- packages/modules-sdk/src/remote-query.ts | 208 ++++++++++++++ packages/orchestration/package.json | 2 +- .../src/__mocks__/joiner/mock_data.ts | 45 +++- .../src/__tests__/joiner/graphql-ast.ts | 48 +++- .../__tests__/joiner/remote-joiner-data.ts | 11 +- .../src/__tests__/joiner/remote-joiner.ts | 54 +++- .../orchestration/src/joiner/graphql-ast.ts | 51 ++-- .../orchestration/src/joiner/remote-joiner.ts | 177 +++++++++--- .../services/product-category/index.ts | 6 +- .../services/product-collection/index.ts | 4 +- .../__tests__/services/product-tag/index.ts | 8 +- .../services/product-variant/index.ts | 19 +- .../__tests__/services/product/index.ts | 14 +- .../product/integration-tests/utils/config.ts | 6 + packages/product/package.json | 1 - packages/product/src/initialize/index.ts | 4 +- packages/product/src/joiner-config.ts | 99 +++++++ packages/product/src/loaders/connection.ts | 1 - packages/product/src/loaders/container.ts | 29 +- .../migrations/.snapshot-medusa-products.json | 254 +++++------------- .../src/models/product-option-value.ts | 5 +- packages/product/src/models/product-option.ts | 2 +- .../product/src/models/product-variant.ts | 2 +- packages/product/src/module-definition.ts | 5 +- .../product/src/scripts/migration-down.ts | 6 +- packages/product/src/services/index.ts | 6 +- .../src/services/product-collection.ts | 3 +- .../src/services/product-module-service.ts | 6 + .../stock-location/src/initialize/index.ts | 6 +- packages/stock-location/src/joiner-config.ts | 15 ++ .../src/services/stock-location.ts | 9 +- packages/types/src/inventory/index.ts | 2 +- .../inventory/{inventory.ts => service.ts} | 8 +- packages/types/src/joiner/index.ts | 14 +- packages/types/src/modules-sdk/index.ts | 5 + packages/types/src/product/index.ts | 2 +- packages/types/src/product/service.ts | 6 +- packages/types/src/stock-location/index.ts | 2 +- packages/types/src/stock-location/service.ts | 43 +++ yarn.lock | 8 +- 57 files changed, 1286 insertions(+), 423 deletions(-) create mode 100644 packages/inventory/src/joiner-config.ts create mode 100644 packages/modules-sdk/src/remote-query.ts create mode 100644 packages/product/integration-tests/utils/config.ts create mode 100644 packages/product/src/joiner-config.ts create mode 100644 packages/stock-location/src/joiner-config.ts rename packages/types/src/inventory/{inventory.ts => service.ts} (96%) create mode 100644 packages/types/src/stock-location/service.ts diff --git a/packages/cache-inmemory/src/initialize/index.ts b/packages/cache-inmemory/src/initialize/index.ts index 3e518213b1..1e0629c905 100644 --- a/packages/cache-inmemory/src/initialize/index.ts +++ b/packages/cache-inmemory/src/initialize/index.ts @@ -11,12 +11,12 @@ export const initialize = async ( options?: InMemoryCacheModuleOptions | ExternalModuleDeclaration ): Promise => { const serviceKey = Modules.CACHE - const loaded = await MedusaModule.bootstrap( + const loaded = await MedusaModule.bootstrap( serviceKey, "@medusajs/cache-inmemory", options as InternalModuleDeclaration | ExternalModuleDeclaration, undefined ) - return loaded[serviceKey] as ICacheService + return loaded[serviceKey] } diff --git a/packages/cache-inmemory/tsconfig.spec.json b/packages/cache-inmemory/tsconfig.spec.json index 9b62409191..46adcc61f4 100644 --- a/packages/cache-inmemory/tsconfig.spec.json +++ b/packages/cache-inmemory/tsconfig.spec.json @@ -1,5 +1,5 @@ { "extends": "./tsconfig.json", - "include": ["src"], + "include": ["src", "integration-tests"], "exclude": ["node_modules"] } diff --git a/packages/cache-redis/src/initialize/index.ts b/packages/cache-redis/src/initialize/index.ts index c13842cb1b..1a69dd7fe9 100644 --- a/packages/cache-redis/src/initialize/index.ts +++ b/packages/cache-redis/src/initialize/index.ts @@ -11,12 +11,12 @@ export const initialize = async ( options?: RedisCacheModuleOptions | ExternalModuleDeclaration ): Promise => { const serviceKey = Modules.CACHE - const loaded = await MedusaModule.bootstrap( + const loaded = await MedusaModule.bootstrap( serviceKey, "@medusajs/cache-redis", options as InternalModuleDeclaration | ExternalModuleDeclaration, undefined ) - return loaded[serviceKey] as ICacheService + return loaded[serviceKey] } diff --git a/packages/event-bus-local/src/initialize/index.ts b/packages/event-bus-local/src/initialize/index.ts index 660e348691..f2bec97960 100644 --- a/packages/event-bus-local/src/initialize/index.ts +++ b/packages/event-bus-local/src/initialize/index.ts @@ -3,10 +3,10 @@ import { IEventBusService } from "@medusajs/types" export const initialize = async (): Promise => { const serviceKey = Modules.EVENT_BUS - const loaded = await MedusaModule.bootstrap( + const loaded = await MedusaModule.bootstrap( serviceKey, "@medusajs/event-bus-local" ) - return loaded[serviceKey] as IEventBusService + return loaded[serviceKey] } diff --git a/packages/event-bus-redis/src/initialize/index.ts b/packages/event-bus-redis/src/initialize/index.ts index 151a688b7e..4ffddc4dc1 100644 --- a/packages/event-bus-redis/src/initialize/index.ts +++ b/packages/event-bus-redis/src/initialize/index.ts @@ -11,12 +11,12 @@ export const initialize = async ( options?: EventBusRedisModuleOptions | ExternalModuleDeclaration ): Promise => { const serviceKey = Modules.EVENT_BUS - const loaded = await MedusaModule.bootstrap( + const loaded = await MedusaModule.bootstrap( serviceKey, "@medusajs/event-bus-redis", options as InternalModuleDeclaration | ExternalModuleDeclaration, undefined ) - return loaded[serviceKey] as IEventBusService + return loaded[serviceKey] } diff --git a/packages/inventory/src/initialize/index.ts b/packages/inventory/src/initialize/index.ts index 99461e9e6f..37cb99dea5 100644 --- a/packages/inventory/src/initialize/index.ts +++ b/packages/inventory/src/initialize/index.ts @@ -15,7 +15,7 @@ export const initialize = async ( } ): Promise => { const serviceKey = Modules.INVENTORY - const loaded = await MedusaModule.bootstrap( + const loaded = await MedusaModule.bootstrap( serviceKey, "@medusajs/inventory", options as InternalModuleDeclaration | ExternalModuleDeclaration, @@ -23,5 +23,5 @@ export const initialize = async ( injectedDependencies ) - return loaded[serviceKey] as IInventoryService + return loaded[serviceKey] } diff --git a/packages/inventory/src/joiner-config.ts b/packages/inventory/src/joiner-config.ts new file mode 100644 index 0000000000..00ce5d586c --- /dev/null +++ b/packages/inventory/src/joiner-config.ts @@ -0,0 +1,51 @@ +import { Modules } from "@medusajs/modules-sdk" +import { JoinerServiceConfig } from "@medusajs/types" + +export const joinerConfig: JoinerServiceConfig = { + serviceName: Modules.INVENTORY, + primaryKeys: ["id"], + alias: [ + { + name: "inventory_items", + }, + { + name: "inventory", + }, + { + name: "inventory_level", + args: { + methodSuffix: "InventoryLevels", + }, + }, + { + name: "inventory_levels", + args: { + methodSuffix: "InventoryLevels", + }, + }, + { + name: "reservation_items", + args: { + methodSuffix: "ReservationItems", + }, + }, + { + name: "reservation_item", + args: { + methodSuffix: "ReservationItems", + }, + }, + { + name: "reservation", + args: { + methodSuffix: "ReservationItems", + }, + }, + { + name: "reservations", + args: { + methodSuffix: "ReservationItems", + }, + }, + ], +} diff --git a/packages/inventory/src/services/inventory.ts b/packages/inventory/src/services/inventory.ts index 08670f14ca..33e7d23021 100644 --- a/packages/inventory/src/services/inventory.ts +++ b/packages/inventory/src/services/inventory.ts @@ -11,6 +11,7 @@ import { IInventoryService, InventoryItemDTO, InventoryLevelDTO, + JoinerServiceConfig, MODULE_RESOURCE_TYPE, ReservationItemDTO, SharedContext, @@ -23,6 +24,7 @@ import { MedusaError, } from "@medusajs/utils" import { EntityManager } from "typeorm" +import { joinerConfig } from "../joiner-config" import InventoryItemService from "./inventory-item" import InventoryLevelService from "./inventory-level" import ReservationItemService from "./reservation-item" @@ -57,6 +59,10 @@ export default class InventoryService implements IInventoryService { this.reservationItemService_ = reservationItemService } + __joinerConfig(): JoinerServiceConfig { + return joinerConfig + } + /** * Lists inventory items that match the given selector * @param selector - the selector to filter inventory items by @@ -75,6 +81,13 @@ export default class InventoryService implements IInventoryService { context ) } + async list( + selector: FilterableInventoryItemProps, + config: FindConfig = { relations: [], skip: 0, take: 10 }, + context: SharedContext = {} + ): Promise { + return await this.inventoryItemService_.list(selector, config, context) + } /** * Lists inventory levels that match the given selector diff --git a/packages/medusa/package.json b/packages/medusa/package.json index f1d06eee01..69bd7608a4 100644 --- a/packages/medusa/package.json +++ b/packages/medusa/package.json @@ -42,7 +42,6 @@ "test": "jest" }, "peerDependencies": { - "@medusajs/types": "^1.8.7", "medusa-interfaces": "^1.3.7", "typeorm": "^0.3.16" }, diff --git a/packages/medusa/src/loaders/__tests__/models.spec.ts b/packages/medusa/src/loaders/__tests__/models.spec.ts index 7ec8e5af54..7d38ae836c 100644 --- a/packages/medusa/src/loaders/__tests__/models.spec.ts +++ b/packages/medusa/src/loaders/__tests__/models.spec.ts @@ -12,7 +12,7 @@ describe("models loader", () => { beforeAll(async () => { try { - models = modelsLoader({ + models = await modelsLoader({ container, isTest: true, coreTestPathGlob: "../models/{product,product-variant}.ts", @@ -30,9 +30,9 @@ describe("models loader", () => { }) it("ensure that the product model is an extended model", () => { - const productModel = container.resolve("productModel") + const productModel = models.find((model) => model.name === "Product") - expect(productModel.custom_attribute).toEqual("test") + expect(new productModel().custom_attribute).toEqual("test") }) it("ensure that the extended product model is registered in db_entities", () => { diff --git a/packages/medusa/src/loaders/models.ts b/packages/medusa/src/loaders/models.ts index aba6c9d22a..8d7ac29087 100644 --- a/packages/medusa/src/loaders/models.ts +++ b/packages/medusa/src/loaders/models.ts @@ -8,7 +8,6 @@ import path from "path" import { ClassConstructor, MedusaContainer } from "../types/global" import { EntitySchema } from "typeorm" import { asClass, asValue } from "awilix" -import { upperCaseFirst } from "@medusajs/utils" type ModelLoaderParams = { container: MedusaContainer @@ -63,9 +62,8 @@ export default ( // If an extension file is found, override it with that instead if (mappedExtensionModel) { const coreModel = require(modelPath) - const modelName = upperCaseFirst( + const modelName = formatRegistrationNameWithoutNamespace(modelPath) - ) coreModel[modelName] = mappedExtensionModel val = mappedExtensionModel diff --git a/packages/medusa/src/loaders/plugins.ts b/packages/medusa/src/loaders/plugins.ts index de6df5f2e3..1bfcbbfdcf 100644 --- a/packages/medusa/src/loaders/plugins.ts +++ b/packages/medusa/src/loaders/plugins.ts @@ -1,19 +1,3 @@ -import { SearchUtils, upperCaseFirst } from "@medusajs/utils" -import { aliasTo, asFunction, asValue, Lifetime } from "awilix" -import { Express } from "express" -import fs from "fs" -import { sync as existsSync } from "fs-exists-cached" -import glob from "glob" -import _ from "lodash" -import { createRequireFromPath } from "medusa-core-utils" -import { - FileService, - FulfillmentService, - OauthService, -} from "medusa-interfaces" -import { trackInstallation } from "medusa-telemetry" -import path from "path" -import { EntitySchema } from "typeorm" import { AbstractTaxService, isBatchJobStrategy, @@ -23,23 +7,40 @@ import { isPriceSelectionStrategy, isTaxCalculationStrategy, } from "../interfaces" -import { MiddlewareService } from "../services" import { ClassConstructor, ConfigModule, Logger, MedusaContainer, } from "../types/global" +import { + FileService, + FulfillmentService, + OauthService, +} from "medusa-interfaces" +import { Lifetime, aliasTo, asFunction, asValue } from "awilix" +import { SearchUtils, upperCaseFirst } from "@medusajs/utils" import { formatRegistrationName, formatRegistrationNameWithoutNamespace, } from "../utils/format-registration-name" -import { getModelExtensionsMap } from "./helpers/get-model-extension-map" import { registerPaymentProcessorFromClass, registerPaymentServiceFromClass, } from "./helpers/plugins" + +import { EntitySchema } from "typeorm" +import { Express } from "express" +import { MiddlewareService } from "../services" +import _ from "lodash" +import { createRequireFromPath } from "medusa-core-utils" +import { sync as existsSync } from "fs-exists-cached" +import fs from "fs" +import { getModelExtensionsMap } from "./helpers/get-model-extension-map" +import glob from "glob" import logger from "./logger" +import path from "path" +import { trackInstallation } from "medusa-telemetry" type Options = { rootDirectory: string diff --git a/packages/modules-sdk/package.json b/packages/modules-sdk/package.json index cb04b84cf3..7bee635851 100644 --- a/packages/modules-sdk/package.json +++ b/packages/modules-sdk/package.json @@ -23,7 +23,8 @@ "typescript": "^4.4.4" }, "dependencies": { - "@medusajs/types": "^1.8.8", + "@medusajs/orchestration": "^0.0.2", + "@medusajs/types": "^1.8.11", "@medusajs/utils": "^1.9.1", "awilix": "^8.0.0", "resolve-cwd": "^3.0.0" diff --git a/packages/modules-sdk/src/definitions.ts b/packages/modules-sdk/src/definitions.ts index 8a74888e2c..35cba19fc9 100644 --- a/packages/modules-sdk/src/definitions.ts +++ b/packages/modules-sdk/src/definitions.ts @@ -9,7 +9,7 @@ export enum Modules { STOCK_LOCATION = "stockLocationService", INVENTORY = "inventoryService", CACHE = "cacheService", - PRODUCT = "productModuleService", + PRODUCT = "productService", } export const ModulesDefinition: { [key: string]: ModuleDefinition } = { @@ -33,6 +33,7 @@ export const ModulesDefinition: { [key: string]: ModuleDefinition } = { label: "StockLocationService", isRequired: false, canOverride: true, + isQueryable: true, dependencies: ["eventBusService"], defaultModuleDeclaration: { scope: MODULE_SCOPE.INTERNAL, @@ -46,6 +47,7 @@ export const ModulesDefinition: { [key: string]: ModuleDefinition } = { label: "InventoryService", isRequired: false, canOverride: true, + isQueryable: true, dependencies: ["eventBusService"], defaultModuleDeclaration: { scope: MODULE_SCOPE.INTERNAL, @@ -66,11 +68,12 @@ export const ModulesDefinition: { [key: string]: ModuleDefinition } = { }, [Modules.PRODUCT]: { key: Modules.PRODUCT, - registrationName: Modules.PRODUCT, + registrationName: "productModuleService", defaultPackage: false, label: "ProductModuleService", isRequired: false, canOverride: true, + isQueryable: true, dependencies: [], defaultModuleDeclaration: { scope: MODULE_SCOPE.EXTERNAL, diff --git a/packages/modules-sdk/src/index.ts b/packages/modules-sdk/src/index.ts index c890af6931..b2737e11e6 100644 --- a/packages/modules-sdk/src/index.ts +++ b/packages/modules-sdk/src/index.ts @@ -3,3 +3,4 @@ export * from "./definitions" export * from "./loaders" export * from "./medusa-module" export * from "./module-helper" +export * from "./remote-query" diff --git a/packages/modules-sdk/src/loaders/__tests__/medusa-module.ts b/packages/modules-sdk/src/loaders/__tests__/medusa-module.ts index 317e048184..b00d76bda3 100644 --- a/packages/modules-sdk/src/loaders/__tests__/medusa-module.ts +++ b/packages/modules-sdk/src/loaders/__tests__/medusa-module.ts @@ -3,27 +3,49 @@ import { MODULE_RESOURCE_TYPE, MODULE_SCOPE, } from "@medusajs/types" -import { MedusaModule } from "../../medusa-module" -const mockRegisterMedusaModule = jest - .fn() - .mockImplementation(() => Promise.resolve([])) -const mockModuleLoader = jest.fn().mockImplementation(() => Promise.resolve({})) +import { MedusaModule } from "../../medusa-module" +import { asValue } from "awilix" + +const mockRegisterMedusaModule = jest.fn().mockImplementation(() => { + return { + moduleKey: { + definition: { + key: "moduleKey", + registrationName: "moduleKey", + }, + moduleDeclaration: { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.SHARED, + }, + }, + } +}) + +const mockModuleLoader = jest.fn().mockImplementation(({ container }) => { + container.register({ + moduleKey: asValue({}), + }) + return Promise.resolve({}) +}) jest.mock("./../../loaders", () => ({ registerMedusaModule: jest .fn() .mockImplementation((...args) => mockRegisterMedusaModule()), - moduleLoader: jest.fn().mockImplementation((...args) => mockModuleLoader()), + moduleLoader: jest + .fn() + .mockImplementation((...args) => mockModuleLoader.apply(this, args)), })) -describe("Medusa Module", () => { +describe("Medusa Modules", () => { beforeEach(() => { + MedusaModule.clearInstances() jest.resetModules() jest.clearAllMocks() }) - it("MedusaModule bootstrap - Singleton instances", async () => { + it("should create singleton instances", async () => { await MedusaModule.bootstrap("moduleKey", "@path", { scope: MODULE_SCOPE.INTERNAL, resources: MODULE_RESOURCE_TYPE.ISOLATED, @@ -57,4 +79,182 @@ describe("Medusa Module", () => { expect(mockRegisterMedusaModule).toBeCalledTimes(2) expect(mockModuleLoader).toBeCalledTimes(2) }) + + it("should prevent the module being loaded multiple times under concurrent requests", async () => { + const load: any = [] + + for (let i = 5; i--; ) { + load.push( + MedusaModule.bootstrap("moduleKey", "@path", { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.ISOLATED, + resolve: "@path", + options: { + abc: 123, + }, + } as InternalModuleDeclaration) + ) + } + + const intances = Promise.all(load) + + expect(mockRegisterMedusaModule).toBeCalledTimes(1) + expect(mockModuleLoader).toBeCalledTimes(1) + expect(intances[(await intances).length - 1]).toBe(intances[0]) + }) + + it("getModuleInstance should return the first instance of the module if there is none flagged as 'main'", async () => { + const moduleA = await MedusaModule.bootstrap("moduleKey", "@path", { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.ISOLATED, + resolve: "@path", + options: { + abc: 123, + }, + } as InternalModuleDeclaration) + + const moduleB = await MedusaModule.bootstrap("moduleKey", "@path", { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.ISOLATED, + resolve: "@path", + options: { + different_options: "abc", + }, + } as InternalModuleDeclaration) + + expect(MedusaModule.getModuleInstance("moduleKey")).toEqual(moduleA) + }) + + it("should return the module flagged as 'main' when multiple instances are available", async () => { + const moduleA = await MedusaModule.bootstrap("moduleKey", "@path", { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.ISOLATED, + resolve: "@path", + options: { + abc: 123, + }, + } as InternalModuleDeclaration) + + const moduleB = await MedusaModule.bootstrap("moduleKey", "@path", { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.ISOLATED, + resolve: "@path", + main: true, + options: { + different_options: "abc", + }, + } as InternalModuleDeclaration) + + expect(MedusaModule.getModuleInstance("moduleKey")).toEqual(moduleB) + }) + + it("should retrieve the module by their given alias", async () => { + const moduleA = await MedusaModule.bootstrap("moduleKey", "@path", { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.ISOLATED, + resolve: "@path", + alias: "mod_A", + options: { + abc: 123, + }, + } as InternalModuleDeclaration) + + const moduleB = await MedusaModule.bootstrap("moduleKey", "@path", { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.ISOLATED, + resolve: "@path", + main: true, + alias: "mod_B", + options: { + different_options: "abc", + }, + } as InternalModuleDeclaration) + + const moduleC = await MedusaModule.bootstrap("moduleKey", "@path", { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.ISOLATED, + resolve: "@path", + alias: "mod_C", + options: { + moduleC: true, + }, + } as InternalModuleDeclaration) + + // main + expect(MedusaModule.getModuleInstance("moduleKey")).toEqual(moduleB) + + expect(MedusaModule.getModuleInstance("moduleKey", "mod_A")).toEqual( + moduleA + ) + expect(MedusaModule.getModuleInstance("moduleKey", "mod_B")).toEqual( + moduleB + ) + expect(MedusaModule.getModuleInstance("moduleKey", "mod_C")).toEqual( + moduleC + ) + }) + + it("should prevent two main modules being set as 'main'", async () => { + await MedusaModule.bootstrap("moduleKey", "@path", { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.ISOLATED, + resolve: "@path", + alias: "mod_A", + options: { + abc: 123, + }, + } as InternalModuleDeclaration) + + await MedusaModule.bootstrap("moduleKey", "@path", { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.ISOLATED, + resolve: "@path", + main: true, + alias: "mod_B", + options: { + different_options: "abc", + }, + } as InternalModuleDeclaration) + + const moduleC = MedusaModule.bootstrap("moduleKey", "@path", { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.ISOLATED, + resolve: "@path", + main: true, + alias: "mod_C", + options: { + moduleC: true, + }, + } as InternalModuleDeclaration) + + expect(moduleC).rejects.toThrow( + "Module moduleKey already have a 'main' registered." + ) + }) + + it("should prevent the same alias be used for different instances of the same module", async () => { + await MedusaModule.bootstrap("moduleKey", "@path", { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.ISOLATED, + resolve: "@path", + alias: "module_alias", + options: { + different_options: "abc", + }, + } as InternalModuleDeclaration) + + const moduleC = MedusaModule.bootstrap("moduleKey", "@path", { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.ISOLATED, + resolve: "@path", + alias: "module_alias", + options: { + moduleC: true, + }, + } as InternalModuleDeclaration) + + expect(moduleC).rejects.toThrow( + "Module moduleKey already registed as 'module_alias'. Please choose a different alias." + ) + }) }) diff --git a/packages/modules-sdk/src/medusa-module.ts b/packages/modules-sdk/src/medusa-module.ts index 8831ae09aa..be20290011 100644 --- a/packages/modules-sdk/src/medusa-module.ts +++ b/packages/modules-sdk/src/medusa-module.ts @@ -1,17 +1,21 @@ import { ExternalModuleDeclaration, InternalModuleDeclaration, + JoinerServiceConfig, MODULE_RESOURCE_TYPE, MODULE_SCOPE, + ModuleDefinition, ModuleExports, + ModuleResolution, } from "@medusajs/types" import { createMedusaContainer, simpleHash, stringifyCircular, } from "@medusajs/utils" -import { asValue } from "awilix" import { moduleLoader, registerMedusaModule } from "./loaders" + +import { asValue } from "awilix" import { loadModuleMigrations } from "./loaders/utils" const logger: any = { @@ -21,19 +25,105 @@ const logger: any = { error: (a) => console.error(a), } +declare global { + interface MedusaModule { + getLoadedModules(): Map + } +} + +type ModuleAlias = { + key: string + hash: string + alias?: string + main?: boolean +} + export class MedusaModule { private static instances_: Map = new Map() + private static modules_: Map = new Map() + private static loading_: Map> = new Map() + + public static getLoadedModules(): Map< + string, + any & { + __joinerConfig: JoinerServiceConfig + __definition: ModuleDefinition + } + > { + return MedusaModule.instances_ + } + public static clearInstances(): void { MedusaModule.instances_.clear() + MedusaModule.modules_.clear() } - public static async bootstrap( + + public static isInstalled(moduleKey: string, alias?: string): boolean { + if (alias) { + return ( + MedusaModule.modules_.has(moduleKey) && + MedusaModule.modules_.get(moduleKey)!.some((m) => m.alias === alias) + ) + } + + return MedusaModule.modules_.has(moduleKey) + } + + public static getModuleInstance( + moduleKey: string, + alias?: string + ): any | undefined { + if (!MedusaModule.modules_.has(moduleKey)) { + return + } + + let mod + const modules = MedusaModule.modules_.get(moduleKey)! + if (alias) { + mod = modules.find((m) => m.alias === alias) + + return MedusaModule.instances_.get(mod?.hash) + } + + mod = modules.find((m) => m.main) ?? modules[0] + + return MedusaModule.instances_.get(mod?.hash) + } + + private static registerModule( + moduleKey: string, + loadedModule: ModuleAlias + ): void { + if (!MedusaModule.modules_.has(moduleKey)) { + MedusaModule.modules_.set(moduleKey, []) + } + + const modules = MedusaModule.modules_.get(moduleKey)! + + if (modules.some((m) => m.alias === loadedModule.alias)) { + throw new Error( + `Module ${moduleKey} already registed as '${loadedModule.alias}'. Please choose a different alias.` + ) + } + + if (loadedModule.main) { + if (modules.some((m) => m.main)) { + throw new Error(`Module ${moduleKey} already have a 'main' registered.`) + } + } + + modules.push(loadedModule) + MedusaModule.modules_.set(moduleKey, modules!) + } + + public static async bootstrap( moduleKey: string, defaultPath: string, declaration?: InternalModuleDeclaration | ExternalModuleDeclaration, moduleExports?: ModuleExports, injectedDependencies?: Record ): Promise<{ - [key: string]: any + [key: string]: T }> { const hashKey = simpleHash( stringifyCircular({ moduleKey, defaultPath, declaration }) @@ -43,13 +133,32 @@ export class MedusaModule { return MedusaModule.instances_.get(hashKey) } - let modDeclaration = declaration + if (MedusaModule.loading_.has(hashKey)) { + return MedusaModule.loading_.get(hashKey) + } + + let finishLoading: any + let errorLoading: any + MedusaModule.loading_.set( + hashKey, + new Promise((resolve, reject) => { + finishLoading = resolve + errorLoading = reject + }) + ) + + let modDeclaration = + declaration ?? + ({} as InternalModuleDeclaration | ExternalModuleDeclaration) + if (declaration?.scope !== MODULE_SCOPE.EXTERNAL) { modDeclaration = { scope: MODULE_SCOPE.INTERNAL, resources: MODULE_RESOURCE_TYPE.ISOLATED, resolve: defaultPath, options: declaration, + alias: declaration?.alias, + main: declaration?.main, } } @@ -67,22 +176,45 @@ export class MedusaModule { moduleExports ) - await moduleLoader({ - container, - moduleResolutions, - logger, - }) + try { + await moduleLoader({ + container, + moduleResolutions, + logger, + }) + } catch (err) { + errorLoading(err) + throw err + } const services = {} - for (const resolution of Object.values(moduleResolutions)) { + for (const resolution of Object.values( + moduleResolutions + ) as ModuleResolution[]) { const keyName = resolution.definition.key const registrationName = resolution.definition.registrationName services[keyName] = container.resolve(registrationName) + services[keyName].__definition = resolution.definition + + if (resolution.definition.isQueryable) { + services[keyName].__joinerConfig = await services[ + keyName + ].__joinerConfig() + } + + MedusaModule.registerModule(keyName, { + key: keyName, + hash: hashKey, + alias: modDeclaration.alias ?? hashKey, + main: !!modDeclaration.main, + }) } MedusaModule.instances_.set(hashKey, services) + finishLoading(services) + MedusaModule.loading_.delete(hashKey) return services } diff --git a/packages/modules-sdk/src/remote-query.ts b/packages/modules-sdk/src/remote-query.ts new file mode 100644 index 0000000000..b06415440e --- /dev/null +++ b/packages/modules-sdk/src/remote-query.ts @@ -0,0 +1,208 @@ +import { RemoteJoiner } from "@medusajs/orchestration" +import { + JoinerRelationship, + JoinerServiceConfig, + ModuleDefinition, + RemoteExpandProperty, +} from "@medusajs/types" +import { toPascalCase } from "@medusajs/utils" +import { MedusaModule } from "./medusa-module" + +export class RemoteQuery { + private remoteJoiner: RemoteJoiner + private modulesMap: Map = new Map() + + constructor( + modulesLoaded?: (any & { + __joinerConfig: JoinerServiceConfig + __definition: ModuleDefinition + })[], + remoteFetchData?: ( + expand: RemoteExpandProperty, + keyField: string, + ids?: (unknown | unknown[])[], + relationship?: JoinerRelationship + ) => Promise<{ + data: unknown[] | { [path: string]: unknown[] } + path?: string + }> + ) { + if (!modulesLoaded?.length) { + modulesLoaded = [...MedusaModule.getLoadedModules().entries()].map( + ([, mod]) => mod + ) + } + + const servicesConfig: JoinerServiceConfig[] = [] + for (const modService of modulesLoaded) { + const mod: any = Object.values(modService)[0] + + if (!mod.__definition.isQueryable) { + continue + } + + if (this.modulesMap.has(mod.__definition.key)) { + throw new Error( + `Duplicated instance of module ${mod.__definition.key} is not allowed.` + ) + } + + this.modulesMap.set(mod.__definition.key, mod) + servicesConfig.push(mod.__joinerConfig) + } + + this.remoteJoiner = new RemoteJoiner( + servicesConfig, + remoteFetchData ?? this.remoteFetchData.bind(this) + ) + } + + public setFetchDataCallback( + remoteFetchData: ( + expand: RemoteExpandProperty, + keyField: string, + ids?: (unknown | unknown[])[], + relationship?: any + ) => Promise<{ + data: unknown[] | { [path: string]: unknown[] } + path?: string + }> + ): void { + this.remoteJoiner.setFetchDataCallback(remoteFetchData) + } + + private static getAllFieldsAndRelations( + data: any, + prefix = "" + ): { select: string[]; relations: string[] } { + let fields: Set = new Set() + let relations: string[] = [] + + data.fields?.forEach((field: string) => { + fields.add(prefix ? `${prefix}.${field}` : field) + }) + + if (data.expands) { + for (const property in data.expands) { + const newPrefix = prefix ? `${prefix}.${property}` : property + + relations.push(newPrefix) + fields.delete(newPrefix) + + const result = RemoteQuery.getAllFieldsAndRelations( + data.expands[property], + newPrefix + ) + + result.select.forEach(fields.add, fields) + relations = relations.concat(result.relations) + } + } + + return { select: [...fields], relations } + } + + private hasPagination(options: { [attr: string]: unknown }): boolean { + if (!options) { + return false + } + + const attrs = ["skip", "cursor"] + return Object.keys(options).some((key) => attrs.includes(key)) + } + + private buildPagination(options, count) { + return { + skip: options.skip, + take: options.take, + cursor: options.cursor, + // TODO: next cursor + count, + } + } + + public async remoteFetchData( + expand: RemoteExpandProperty, + keyField: string, + ids?: (unknown | unknown[])[], + relationship?: JoinerRelationship + ): Promise<{ + data: unknown[] | { [path: string]: unknown } + path?: string + }> { + const serviceConfig = expand.serviceConfig + const service = this.modulesMap.get(serviceConfig.serviceName) + + let filters = {} + const options = { + ...RemoteQuery.getAllFieldsAndRelations(expand), + } + + const availableOptions = [ + "skip", + "take", + "limit", + "offset", + "cursor", + "sort", + ] + const availableOptionsAlias = new Map([ + ["limit", "take"], + ["offset", "skip"], + ]) + + for (const arg of expand.args || []) { + if (arg.name === "filters" && arg.value) { + filters = { ...arg.value } + } else if (availableOptions.includes(arg.name)) { + const argName = availableOptionsAlias.has(arg.name) + ? availableOptionsAlias.get(arg.name)! + : arg.name + options[argName] = arg.value + } + } + + if (ids) { + filters[keyField] = ids + } + + const hasPagination = this.hasPagination(options) + + let methodName = hasPagination ? "listAndCount" : "list" + + if (relationship?.args?.methodSuffix) { + methodName += toPascalCase(relationship.args.methodSuffix) + } else if (serviceConfig?.args?.methodSuffix) { + methodName += toPascalCase(serviceConfig.args.methodSuffix) + } + + if (typeof service[methodName] !== "function") { + throw new Error( + `Method "${methodName}" does not exist on "${serviceConfig.serviceName}"` + ) + } + + const result = await service[methodName](filters, options) + + if (hasPagination) { + const [data, count] = result + return { + data: { + rows: data, + metadata: this.buildPagination(options, count), + }, + path: "rows", + } + } + + return { + data: result, + } + } + + public async query(query: string, variables: any = {}): Promise { + return await this.remoteJoiner.query( + RemoteJoiner.parseQuery(query, variables) + ) + } +} diff --git a/packages/orchestration/package.json b/packages/orchestration/package.json index 6ee5681a8f..fd6ee7754c 100644 --- a/packages/orchestration/package.json +++ b/packages/orchestration/package.json @@ -17,7 +17,7 @@ "author": "Medusa", "license": "MIT", "devDependencies": { - "@medusajs/types": "^1.8.10", + "@medusajs/types": "^1.8.11", "cross-env": "^5.2.1", "jest": "^25.5.4", "ts-jest": "^25.5.1", diff --git a/packages/orchestration/src/__mocks__/joiner/mock_data.ts b/packages/orchestration/src/__mocks__/joiner/mock_data.ts index 75ccb7a11b..1da74c70d6 100644 --- a/packages/orchestration/src/__mocks__/joiner/mock_data.ts +++ b/packages/orchestration/src/__mocks__/joiner/mock_data.ts @@ -3,22 +3,36 @@ import { remoteJoinerData } from "./../../__fixtures__/joiner/data" export const serviceConfigs: JoinerServiceConfig[] = [ { - serviceName: "User", + serviceName: "user", primaryKeys: ["id"], + args: { + methodSuffix: "User", + }, + alias: [ + { + name: "me", + args: { + extraArgument: 123, + }, + }, + { + name: "customer", + }, + ], relationships: [ { foreignKey: "products.product_id", - serviceName: "Product", + serviceName: "product", primaryKey: "id", alias: "product", }, ], extends: [ { - serviceName: "Variant", - resolve: { + serviceName: "variantService", + relationship: { foreignKey: "user_id", - serviceName: "User", + serviceName: "user", primaryKey: "id", alias: "user", }, @@ -26,55 +40,58 @@ export const serviceConfigs: JoinerServiceConfig[] = [ ], }, { - serviceName: "Product", + serviceName: "product", primaryKeys: ["id", "sku"], relationships: [ { foreignKey: "user_id", - serviceName: "User", + serviceName: "user", primaryKey: "id", alias: "user", }, ], }, { - serviceName: "Variant", + serviceName: "variantService", + alias: { + name: "variant", + }, primaryKeys: ["id"], relationships: [ { foreignKey: "product_id", - serviceName: "Product", + serviceName: "product", primaryKey: "id", alias: "product", }, { foreignKey: "variant_id", primaryKey: "id", - serviceName: "Order", + serviceName: "order", alias: "orders", inverse: true, // In an inverted relationship the foreign key is on Order and the primary key is on variant }, ], }, { - serviceName: "Order", + serviceName: "order", primaryKeys: ["id"], relationships: [ { foreignKey: "product_id", - serviceName: "Product", + serviceName: "product", primaryKey: "id", alias: "product", }, { foreignKey: "products.variant_id,product_id", - serviceName: "Variant", + serviceName: "variantService", primaryKey: "id,product_id", alias: "variant", }, { foreignKey: "user_id", - serviceName: "User", + serviceName: "user", primaryKey: "id", alias: "user", }, diff --git a/packages/orchestration/src/__tests__/joiner/graphql-ast.ts b/packages/orchestration/src/__tests__/joiner/graphql-ast.ts index 8ce56afd0a..c096ee3082 100644 --- a/packages/orchestration/src/__tests__/joiner/graphql-ast.ts +++ b/packages/orchestration/src/__tests__/joiner/graphql-ast.ts @@ -19,7 +19,7 @@ describe("RemoteJoiner.parseQuery", () => { const rjQuery = parser.parseQuery() expect(rjQuery).toEqual({ - service: "order", + alias: "order", fields: ["id", "number", "date"], expands: [], }) @@ -50,7 +50,7 @@ describe("RemoteJoiner.parseQuery", () => { const rjQuery = parser.parseQuery() expect(rjQuery).toEqual({ - service: "order", + alias: "order", fields: ["id", "number", "date"], expands: [], args: [ @@ -77,6 +77,44 @@ describe("RemoteJoiner.parseQuery", () => { }) }) + it("Simple query with mapping fields to services", async () => { + const graphqlQuery = ` + query { + order { + id + number + date + products { + product_id + variant_id + order + variant { + name + sku + } + } + } + } + ` + const parser = new GraphQLParser(graphqlQuery, {}) + const rjQuery = parser.parseQuery() + + expect(rjQuery).toEqual({ + alias: "order", + fields: ["id", "number", "date", "products"], + expands: [ + { + property: "products", + fields: ["product_id", "variant_id", "order", "variant"], + }, + { + property: "products.variant", + fields: ["name", "sku"], + }, + ], + }) + }) + it("Nested query with fields", async () => { const graphqlQuery = ` query { @@ -100,7 +138,7 @@ describe("RemoteJoiner.parseQuery", () => { const rjQuery = parser.parseQuery() expect(rjQuery).toEqual({ - service: "order", + alias: "order", fields: ["id", "number", "date", "products"], expands: [ { @@ -138,7 +176,7 @@ describe("RemoteJoiner.parseQuery", () => { const rjQuery = parser.parseQuery() expect(rjQuery).toEqual({ - service: "order", + alias: "order", fields: ["id", "number", "date", "products"], expands: [ { @@ -205,7 +243,7 @@ describe("RemoteJoiner.parseQuery", () => { const rjQuery = parser.parseQuery() expect(rjQuery).toEqual({ - service: "order", + alias: "order", fields: ["id", "number", "date", "products"], expands: [ { diff --git a/packages/orchestration/src/__tests__/joiner/remote-joiner-data.ts b/packages/orchestration/src/__tests__/joiner/remote-joiner-data.ts index cd2b20ae38..aee4989580 100644 --- a/packages/orchestration/src/__tests__/joiner/remote-joiner-data.ts +++ b/packages/orchestration/src/__tests__/joiner/remote-joiner-data.ts @@ -45,8 +45,9 @@ const fetchServiceDataCallback = async ( relationship?: any ) => { const serviceConfig = expand.serviceConfig - const moduleRegistryName = - lowerCaseFirst(serviceConfig.serviceName) + "Service" + const moduleRegistryName = !serviceConfig.serviceName.endsWith("Service") + ? lowerCaseFirst(serviceConfig.serviceName) + "Service" + : serviceConfig.serviceName const service = container.resolve(moduleRegistryName) const methodName = relationship?.inverse @@ -74,7 +75,7 @@ describe("RemoteJoiner", () => { it("Simple query of a service, its id and no fields specified", async () => { const query = { - service: "User", + service: "user", args: [ { name: "id", @@ -143,7 +144,7 @@ describe("RemoteJoiner", () => { it("Query of a service, expanding a property and restricting the fields expanded", async () => { const query = { - service: "User", + service: "user", args: [ { name: "id", @@ -215,7 +216,7 @@ describe("RemoteJoiner", () => { it("Query a service expanding multiple nested properties", async () => { const query = { - service: "Order", + service: "order", fields: ["number", "date", "products"], expands: [ { diff --git a/packages/orchestration/src/__tests__/joiner/remote-joiner.ts b/packages/orchestration/src/__tests__/joiner/remote-joiner.ts index 5034c85931..798a6d4b57 100644 --- a/packages/orchestration/src/__tests__/joiner/remote-joiner.ts +++ b/packages/orchestration/src/__tests__/joiner/remote-joiner.ts @@ -49,7 +49,7 @@ describe("RemoteJoiner", () => { it("Simple query of a service, its id and no fields specified", async () => { const query = { - service: "User", + service: "user", args: [ { name: "id", @@ -69,20 +69,62 @@ describe("RemoteJoiner", () => { }) }) - it("Transforms main service name into PascalCase", async () => { + it("Simple query of a service by its alias", async () => { const query = { - service: "user", + alias: "customer", fields: ["id"], + args: [ + { + name: "id", + value: "1", + }, + ], } await joiner.query(query) expect(serviceMock.userService).toHaveBeenCalledTimes(1) + expect(serviceMock.userService).toHaveBeenCalledWith({ + args: [], + fields: ["id"], + options: { id: ["1"] }, + }) + }) + + it("Simple query of a service by its alias with extra arguments", async () => { + const query = { + alias: "me", + fields: ["id"], + args: [ + { + name: "id", + value: 1, + }, + { + name: "arg1", + value: "abc", + }, + ], + } + + await joiner.query(query) + + expect(serviceMock.userService).toHaveBeenCalledTimes(1) + expect(serviceMock.userService).toHaveBeenCalledWith({ + args: [ + { + name: "arg1", + value: "abc", + }, + ], + fields: ["id"], + options: { id: [1] }, + }) }) it("Simple query of a service, its id and a few fields specified", async () => { const query = { - service: "User", + service: "user", args: [ { name: "id", @@ -148,7 +190,7 @@ describe("RemoteJoiner", () => { it("Query a service using more than 1 argument, expanding a property with another argument", async () => { const query = { - service: "User", + service: "user", args: [ { name: "id", @@ -213,7 +255,7 @@ describe("RemoteJoiner", () => { it("Query a service expanding multiple nested properties", async () => { const query = { - service: "Order", + service: "order", fields: ["number", "date", "products"], expands: [ { diff --git a/packages/orchestration/src/joiner/graphql-ast.ts b/packages/orchestration/src/joiner/graphql-ast.ts index c33e9fdb60..aa3e1d0983 100644 --- a/packages/orchestration/src/joiner/graphql-ast.ts +++ b/packages/orchestration/src/joiner/graphql-ast.ts @@ -86,28 +86,29 @@ class GraphQLParser { if (selection.kind === "Field") { const fieldNode = selection as FieldNode - if (fieldNode.selectionSet) { - const entityName = parentName - ? `${parentName}.${fieldNode.name.value}` - : fieldNode.name.value - - const nestedEntity: Entity = { - property: entityName.replace(`${mainService}.`, ""), - fields: fieldNode.selectionSet.selections.map( - (field) => (field as FieldNode).name.value - ), - args: this.parseArguments(fieldNode.arguments!), - } - - entities.push(nestedEntity) - entities.push( - ...this.extractEntities( - fieldNode.selectionSet, - entityName, - mainService - ) - ) + if (!fieldNode.selectionSet) { + return } + + const propName = fieldNode.name.value + const entityName = parentName ? `${parentName}.${propName}` : propName + + const nestedEntity: Entity = { + property: entityName.replace(`${mainService}.`, ""), + fields: fieldNode.selectionSet.selections.map( + (field) => (field as FieldNode).name.value + ), + args: this.parseArguments(fieldNode.arguments!), + } + + entities.push(nestedEntity) + entities.push( + ...this.extractEntities( + fieldNode.selectionSet, + entityName, + mainService + ) + ) } }) @@ -126,8 +127,9 @@ class GraphQLParser { const rootFieldNode = queryDefinition.selectionSet .selections[0] as FieldNode + const propName = rootFieldNode.name.value const remoteJoinConfig: RemoteJoinerQuery = { - service: rootFieldNode.name.value, + alias: propName, fields: [], expands: [], } @@ -140,10 +142,11 @@ class GraphQLParser { remoteJoinConfig.fields = rootFieldNode.selectionSet.selections.map( (field) => (field as FieldNode).name.value ) + remoteJoinConfig.expands = this.extractEntities( rootFieldNode.selectionSet, - rootFieldNode.name.value, - rootFieldNode.name.value + propName, + propName ) } diff --git a/packages/orchestration/src/joiner/remote-joiner.ts b/packages/orchestration/src/joiner/remote-joiner.ts index 799fece99b..978908f575 100644 --- a/packages/orchestration/src/joiner/remote-joiner.ts +++ b/packages/orchestration/src/joiner/remote-joiner.ts @@ -1,16 +1,16 @@ import { JoinerRelationship, JoinerServiceConfig, + JoinerServiceConfigAlias, RemoteExpandProperty, RemoteJoinerQuery, RemoteNestedExpands, } from "@medusajs/types" -import { isDefined, toPascalCase } from "@medusajs/utils" +import { isDefined } from "@medusajs/utils" import GraphQLParser from "./graphql-ast" const BASE_PATH = "_root" export class RemoteJoiner { - private serviceConfigs: JoinerServiceConfig[] private serviceConfigCache: Map = new Map() private static filterFields( @@ -84,37 +84,77 @@ export class RemoteJoiner { } constructor( - serviceConfigs: JoinerServiceConfig[], + private serviceConfigs: JoinerServiceConfig[], private remoteFetchData: ( expand: RemoteExpandProperty, - pkField: string, + keyField: string, ids?: (unknown | unknown[])[], relationship?: any ) => Promise<{ - data: unknown[] | { [path: string]: unknown[] } + data: unknown[] | { [path: string]: unknown } path?: string }> ) { this.serviceConfigs = this.buildReferences(serviceConfigs) } + public setFetchDataCallback( + remoteFetchData: ( + expand: RemoteExpandProperty, + keyField: string, + ids?: (unknown | unknown[])[], + relationship?: any + ) => Promise<{ + data: unknown[] | { [path: string]: unknown } + path?: string + }> + ): void { + this.remoteFetchData = remoteFetchData + } + private buildReferences(serviceConfigs: JoinerServiceConfig[]) { const expandedRelationships: Map = new Map() for (const service of serviceConfigs) { - // self-reference - const propName = service.serviceName.toLowerCase() + if (this.serviceConfigCache.has(service.serviceName)) { + throw new Error(`Service "${service.serviceName}" is already defined.`) + } + if (!service.relationships) { service.relationships = [] } - service.relationships?.push({ - alias: propName, - foreignKey: propName + "_id", - primaryKey: "id", - serviceName: service.serviceName, - }) + // add aliases + if (!service.alias) { + service.alias = [{ name: service.serviceName.toLowerCase() }] + } else if (!Array.isArray(service.alias)) { + service.alias = [service.alias] + } - this.serviceConfigCache.set(service.serviceName, service) + // self-reference + for (const alias of service.alias) { + if (this.serviceConfigCache.has(`alias_${alias.name}}`)) { + const defined = this.serviceConfigCache.get(`alias_${alias.name}}`) + throw new Error( + `Cannot add alias "${alias.name}" for "${service.serviceName}". It is already defined for Service "${defined?.serviceName}".` + ) + } + + const args = + service.args || alias.args + ? { ...service.args, ...alias.args } + : undefined + + service.relationships?.push({ + alias: alias.name, + foreignKey: alias.name + "_id", + primaryKey: "id", + serviceName: service.serviceName, + args, + }) + this.cacheServiceConfig(serviceConfigs, undefined, alias.name) + } + + this.cacheServiceConfig(serviceConfigs, service.serviceName) if (!service.extends) { continue @@ -125,13 +165,13 @@ export class RemoteJoiner { expandedRelationships.set(extend.serviceName, []) } - expandedRelationships.get(extend.serviceName)!.push(extend.resolve) + expandedRelationships.get(extend.serviceName)!.push(extend.relationship) } } for (const [serviceName, relationships] of expandedRelationships) { if (!this.serviceConfigCache.has(serviceName)) { - throw new Error(`Service ${serviceName} not found`) + throw new Error(`Service "${serviceName}" was not found`) } const service = this.serviceConfigCache.get(serviceName) @@ -141,16 +181,49 @@ export class RemoteJoiner { return serviceConfigs } - private findServiceConfig( - serviceName: string + private getServiceConfig( + serviceName?: string, + serviceAlias?: string ): JoinerServiceConfig | undefined { - if (!this.serviceConfigCache.has(serviceName)) { - const config = this.serviceConfigs.find( - (config) => config.serviceName === serviceName - ) - this.serviceConfigCache.set(serviceName, config!) + if (serviceAlias) { + const name = `alias_${serviceAlias}` + return this.serviceConfigCache.get(name) } - return this.serviceConfigCache.get(serviceName) + + return this.serviceConfigCache.get(serviceName!) + } + + private cacheServiceConfig( + serviceConfigs, + serviceName?: string, + serviceAlias?: string + ): void { + if (serviceAlias) { + const name = `alias_${serviceAlias}` + if (!this.serviceConfigCache.has(name)) { + let aliasConfig: JoinerServiceConfigAlias | undefined + const config = serviceConfigs.find((conf) => { + const aliases = conf.alias as JoinerServiceConfigAlias[] + const hasArgs = aliases?.find((alias) => alias.name === serviceAlias) + aliasConfig = hasArgs + return hasArgs + }) + + if (config) { + const serviceConfig = { ...config } + if (aliasConfig) { + serviceConfig.args = { ...config?.args, ...aliasConfig?.args } + } + this.serviceConfigCache.set(name, serviceConfig) + } + } + return + } + + const config = serviceConfigs.find( + (config) => config.serviceName === serviceName + ) + this.serviceConfigCache.set(serviceName!, config) } private async fetchData( @@ -159,7 +232,7 @@ export class RemoteJoiner { ids?: (unknown | unknown[])[], relationship?: any ): Promise<{ - data: unknown[] | { [path: string]: unknown[] } + data: unknown[] | { [path: string]: unknown } path?: string }> { let uniqueIds = Array.isArray(ids) ? ids : ids ? [ids] : undefined @@ -218,7 +291,7 @@ export class RemoteJoiner { const stack: [ any[], - RemoteJoinerQuery, + Partial, Map, string, Set @@ -245,23 +318,25 @@ export class RemoteJoiner { resolvedPaths.add(expandedPath) const property = expand.property || "" - const parentServiceConfig = this.findServiceConfig(currentQuery.service) + const parentServiceConfig = this.getServiceConfig( + currentQuery.service, + currentQuery.alias + ) await this.expandProperty(currentItems, parentServiceConfig!, expand) - const relationship = parentServiceConfig?.relationships?.find( - (relation) => relation.alias === property - ) - const nestedItems = RemoteJoiner.getNestedItems(currentItems, property) if (nestedItems.length > 0) { - const nextProp = relationship - ? { - ...currentQuery, - service: relationship.serviceName, - } - : currentQuery + const relationship = expand.serviceConfig + + let nextProp = currentQuery + if (relationship) { + const relQuery = { + service: relationship.serviceName, + } + nextProp = relQuery + } stack.push([ nestedItems, @@ -356,9 +431,19 @@ export class RemoteJoiner { if (Array.isArray(item[field])) { item[relationship.alias] = item[field] - .map((id) => relatedDataMap[id]) + .map((id) => { + if (relationship.isList && !Array.isArray(relatedDataMap[id])) { + relatedDataMap[id] = [relatedDataMap[id]] + } + + return relatedDataMap[id] + }) .filter((relatedItem) => relatedItem !== undefined) } else { + if (relationship.isList && !Array.isArray(relatedDataMap[itemKey])) { + relatedDataMap[itemKey] = [relatedDataMap[itemKey]] + } + item[relationship.alias] = relatedDataMap[itemKey] } } @@ -439,9 +524,7 @@ export class RemoteJoiner { } } - currentServiceConfig = this.findServiceConfig( - relationship.serviceName - ) + currentServiceConfig = this.getServiceConfig(relationship.serviceName) if (!currentServiceConfig) { throw new Error( @@ -522,11 +605,17 @@ export class RemoteJoiner { } async query(queryObj: RemoteJoinerQuery): Promise { - queryObj.service = toPascalCase(queryObj.service) - const serviceConfig = this.findServiceConfig(queryObj.service) + const serviceConfig = this.getServiceConfig( + queryObj.service, + queryObj.alias + ) if (!serviceConfig) { - throw new Error(`Service not found: ${queryObj.service}`) + if (queryObj.alias) { + throw new Error(`Service with alias "${queryObj.alias}" was not found.`) + } + + throw new Error(`Service "${queryObj.service}" was not found.`) } let pkName = serviceConfig.primaryKeys[0] diff --git a/packages/product/integration-tests/__tests__/services/product-category/index.ts b/packages/product/integration-tests/__tests__/services/product-category/index.ts index c5878466fe..c200323ae0 100644 --- a/packages/product/integration-tests/__tests__/services/product-category/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-category/index.ts @@ -1,12 +1,12 @@ import { SqlEntityManager } from "@mikro-orm/postgresql" -import { ProductCategoryService } from "@services" -import { ProductCategoryRepository } from "@repositories" import { ProductCategory } from "@models" +import { ProductCategoryRepository } from "@repositories" +import { ProductCategoryService } from "@services" -import { TestDatabase } from "../../../utils" import { createProductCategories } from "../../../__fixtures__/product-category" import { productCategoriesData } from "../../../__fixtures__/product-category/data" +import { TestDatabase } from "../../../utils" jest.setTimeout(30000) diff --git a/packages/product/integration-tests/__tests__/services/product-collection/index.ts b/packages/product/integration-tests/__tests__/services/product-collection/index.ts index 1659599452..28b620dd56 100644 --- a/packages/product/integration-tests/__tests__/services/product-collection/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-collection/index.ts @@ -1,11 +1,11 @@ import { SqlEntityManager } from "@mikro-orm/postgresql" import { ProductCollection } from "@models" -import { ProductCollectionService } from "@services" import { ProductCollectionRepository } from "@repositories" +import { ProductCollectionService } from "@services" -import { TestDatabase } from "../../../utils" import { createCollections } from "../../../__fixtures__/product" +import { TestDatabase } from "../../../utils" jest.setTimeout(30000) diff --git a/packages/product/integration-tests/__tests__/services/product-tag/index.ts b/packages/product/integration-tests/__tests__/services/product-tag/index.ts index d402c6ecd6..57ce42f4b6 100644 --- a/packages/product/integration-tests/__tests__/services/product-tag/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-tag/index.ts @@ -1,12 +1,12 @@ import { SqlEntityManager } from "@mikro-orm/postgresql" -import { ProductTagService } from "@services" -import { ProductTagRepository } from "@repositories" import { Product } from "@models" +import { ProductTagRepository } from "@repositories" +import { ProductTagService } from "@services" -import { TestDatabase } from "../../../utils" -import { createProductAndTags } from "../../../__fixtures__/product" import { ProductTypes } from "@medusajs/types" +import { createProductAndTags } from "../../../__fixtures__/product" +import { TestDatabase } from "../../../utils" jest.setTimeout(30000) diff --git a/packages/product/integration-tests/__tests__/services/product-variant/index.ts b/packages/product/integration-tests/__tests__/services/product-variant/index.ts index af6bbbf52e..4492174bad 100644 --- a/packages/product/integration-tests/__tests__/services/product-variant/index.ts +++ b/packages/product/integration-tests/__tests__/services/product-variant/index.ts @@ -280,18 +280,15 @@ describe("ProductVariant Service", () => { expect.objectContaining({ id: productVariantTestOne, title: "variant 1", - }), + }) ) }) it("should return requested attributes when requested through config", async () => { - const result = await service.retrieve( - variantOne.id, - { - select: ["id", "title", "product.title"] as any, - relations: ["product"], - } - ) + const result = await service.retrieve(variantOne.id, { + select: ["id", "title", "product.title"] as any, + relations: ["product"], + }) expect(result).toEqual( expect.objectContaining({ @@ -302,7 +299,7 @@ describe("ProductVariant Service", () => { id: "product-1", title: "product 1", }), - }), + }) ) }) @@ -315,7 +312,9 @@ describe("ProductVariant Service", () => { error = e } - expect(error.message).toEqual("ProductVariant with id: does-not-exist was not found") + expect(error.message).toEqual( + "ProductVariant with id: does-not-exist was not found" + ) }) it("should throw an error when an id is not provided", async () => { diff --git a/packages/product/integration-tests/__tests__/services/product/index.ts b/packages/product/integration-tests/__tests__/services/product/index.ts index 5e9ea1a6af..c316ee5a80 100644 --- a/packages/product/integration-tests/__tests__/services/product/index.ts +++ b/packages/product/integration-tests/__tests__/services/product/index.ts @@ -1,11 +1,4 @@ -import { TestDatabase } from "../../../utils" -import { ProductService } from "@services" -import { ProductRepository } from "@repositories" import { Image, Product, ProductCategory, ProductVariant } from "@models" -import { SqlEntityManager } from "@mikro-orm/postgresql" -import { ProductDTO } from "@medusajs/types" - -import { createProductCategories } from "../../../__fixtures__/product-category" import { assignCategoriesToProduct, createImages, @@ -17,7 +10,14 @@ import { productsData, variantsData, } from "../../../__fixtures__/product/data" + +import { ProductDTO } from "@medusajs/types" +import { ProductRepository } from "@repositories" +import { ProductService } from "@services" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { TestDatabase } from "../../../utils" import { buildProductOnlyData } from "../../../__fixtures__/product/data/create-product" +import { createProductCategories } from "../../../__fixtures__/product-category" import { kebabCase } from "@medusajs/utils" jest.setTimeout(30000) diff --git a/packages/product/integration-tests/utils/config.ts b/packages/product/integration-tests/utils/config.ts new file mode 100644 index 0000000000..99945b885e --- /dev/null +++ b/packages/product/integration-tests/utils/config.ts @@ -0,0 +1,6 @@ +import { ProductServiceInitializeOptions } from "../../src/types" + +export const databaseOptions: ProductServiceInitializeOptions["database"] = { + schema: "public", + clientUrl: "medusa-products-test", +} diff --git a/packages/product/package.json b/packages/product/package.json index de0510e11f..37a9ea384c 100644 --- a/packages/product/package.json +++ b/packages/product/package.json @@ -37,7 +37,6 @@ }, "devDependencies": { "@mikro-orm/cli": "5.7.12", - "@mikro-orm/migrations": "5.7.12", "cross-env": "^5.2.1", "faker": "^6.6.6", "jest": "^25.5.4", diff --git a/packages/product/src/initialize/index.ts b/packages/product/src/initialize/index.ts index 628230f533..1088c56808 100644 --- a/packages/product/src/initialize/index.ts +++ b/packages/product/src/initialize/index.ts @@ -18,7 +18,7 @@ export const initialize = async ( ): Promise => { const serviceKey = Modules.PRODUCT - const loaded = await MedusaModule.bootstrap( + const loaded = await MedusaModule.bootstrap( serviceKey, MODULE_PACKAGE_NAMES[Modules.PRODUCT], options as InternalModuleDeclaration | ExternalModuleDeclaration, @@ -26,5 +26,5 @@ export const initialize = async ( injectedDependencies ) - return loaded[serviceKey] as IProductModuleService + return loaded[serviceKey] } diff --git a/packages/product/src/joiner-config.ts b/packages/product/src/joiner-config.ts new file mode 100644 index 0000000000..54e7bbd105 --- /dev/null +++ b/packages/product/src/joiner-config.ts @@ -0,0 +1,99 @@ +import { Modules } from "@medusajs/modules-sdk" +import { JoinerServiceConfig } from "@medusajs/types" + +export const joinerConfig: JoinerServiceConfig = { + serviceName: Modules.PRODUCT, + primaryKeys: ["id", "handle"], + alias: [ + { + name: "product", + }, + { + name: "products", + }, + { + name: "variant", + args: { + methodSuffix: "Variants", + }, + }, + { + name: "variants", + args: { + methodSuffix: "Variants", + }, + }, + { + name: "product_option", + args: { + methodSuffix: "Options", + }, + }, + { + name: "product_options", + args: { + methodSuffix: "Options", + }, + }, + { + name: "product_type", + args: { + methodSuffix: "Types", + }, + }, + { + name: "product_types", + args: { + methodSuffix: "Types", + }, + }, + { + name: "product_image", + args: { + methodSuffix: "Images", + }, + }, + { + name: "product_images", + args: { + methodSuffix: "Images", + }, + }, + { + name: "product_tag", + args: { + methodSuffix: "Tags", + }, + }, + { + name: "product_tags", + args: { + methodSuffix: "Tags", + }, + }, + { + name: "product_collection", + args: { + methodSuffix: "Collections", + }, + }, + { + name: "product_collections", + args: { + methodSuffix: "Collections", + }, + }, + { + name: "product_category", + args: { + methodSuffix: "Categories", + }, + }, + { + name: "product_categories", + args: { + methodSuffix: "Categories", + }, + }, + ], +} diff --git a/packages/product/src/loaders/connection.ts b/packages/product/src/loaders/connection.ts index 85863dc91e..4d60943f89 100644 --- a/packages/product/src/loaders/connection.ts +++ b/packages/product/src/loaders/connection.ts @@ -54,7 +54,6 @@ async function loadDefault({ database, container }) { } const entities = Object.values(ProductModels) as unknown as EntitySchema[] - const orm = await createConnection(database, entities) container.register({ diff --git a/packages/product/src/loaders/container.ts b/packages/product/src/loaders/container.ts index 3975dcbbb4..e3f490a991 100644 --- a/packages/product/src/loaders/container.ts +++ b/packages/product/src/loaders/container.ts @@ -1,18 +1,5 @@ -import { LoaderOptions } from "@medusajs/modules-sdk" - -import { asClass } from "awilix" -import { - ProductCategoryService, - ProductCollectionService, - ProductImageService, - ProductModuleService, - ProductOptionService, - ProductService, - ProductTagService, - ProductTypeService, - ProductVariantService, -} from "@services" import * as DefaultRepositories from "@repositories" + import { BaseRepository, ProductCategoryRepository, @@ -25,6 +12,20 @@ import { ProductVariantRepository, } from "@repositories" import { Constructor, DAL, ModulesSdkTypes } from "@medusajs/types" +import { + ProductCategoryService, + ProductCollectionService, + ProductImageService, + ProductModuleService, + ProductOptionService, + ProductService, + ProductTagService, + ProductTypeService, + ProductVariantService, +} from "@services" + +import { LoaderOptions } from "@medusajs/modules-sdk" +import { asClass } from "awilix" import { lowerCaseFirst } from "@medusajs/utils" export default async ({ diff --git a/packages/product/src/migrations/.snapshot-medusa-products.json b/packages/product/src/migrations/.snapshot-medusa-products.json index 43fb0c6d2d..cf29bd0bcf 100644 --- a/packages/product/src/migrations/.snapshot-medusa-products.json +++ b/packages/product/src/migrations/.snapshot-medusa-products.json @@ -1,7 +1,5 @@ { - "namespaces": [ - "public" - ], + "namespaces": ["public"], "name": "public", "tables": [ { @@ -117,27 +115,21 @@ "indexes": [ { "keyName": "IDX_product_category_path", - "columnNames": [ - "mpath" - ], + "columnNames": ["mpath"], "composite": false, "primary": false, "unique": false }, { "keyName": "IDX_product_category_handle", - "columnNames": [ - "handle" - ], + "columnNames": ["handle"], "composite": false, "primary": false, "unique": true }, { "keyName": "product_category_pkey", - "columnNames": [ - "id" - ], + "columnNames": ["id"], "composite": false, "primary": true, "unique": true @@ -147,13 +139,9 @@ "foreignKeys": { "product_category_parent_category_id_foreign": { "constraintName": "product_category_parent_category_id_foreign", - "columnNames": [ - "parent_category_id" - ], + "columnNames": ["parent_category_id"], "localTableName": "public.product_category", - "referencedColumnNames": [ - "id" - ], + "referencedColumnNames": ["id"], "referencedTableName": "public.product_category", "deleteRule": "set null", "updateRule": "cascade" @@ -213,9 +201,7 @@ "schema": "public", "indexes": [ { - "columnNames": [ - "deleted_at" - ], + "columnNames": ["deleted_at"], "composite": false, "keyName": "IDX_product_collection_deleted_at", "primary": false, @@ -223,18 +209,14 @@ }, { "keyName": "IDX_product_collection_handle_unique", - "columnNames": [ - "handle" - ], + "columnNames": ["handle"], "composite": false, "primary": false, "unique": true }, { "keyName": "product_collection_pkey", - "columnNames": [ - "id" - ], + "columnNames": ["id"], "composite": false, "primary": true, "unique": true @@ -287,18 +269,14 @@ "schema": "public", "indexes": [ { - "columnNames": [ - "url" - ], + "columnNames": ["url"], "composite": false, "keyName": "IDX_product_image_url", "primary": false, "unique": false }, { - "columnNames": [ - "deleted_at" - ], + "columnNames": ["deleted_at"], "composite": false, "keyName": "IDX_product_image_deleted_at", "primary": false, @@ -306,9 +284,7 @@ }, { "keyName": "image_pkey", - "columnNames": [ - "id" - ], + "columnNames": ["id"], "composite": false, "primary": true, "unique": true @@ -361,9 +337,7 @@ "schema": "public", "indexes": [ { - "columnNames": [ - "deleted_at" - ], + "columnNames": ["deleted_at"], "composite": false, "keyName": "IDX_product_tag_deleted_at", "primary": false, @@ -371,9 +345,7 @@ }, { "keyName": "product_tag_pkey", - "columnNames": [ - "id" - ], + "columnNames": ["id"], "composite": false, "primary": true, "unique": true @@ -426,9 +398,7 @@ "schema": "public", "indexes": [ { - "columnNames": [ - "deleted_at" - ], + "columnNames": ["deleted_at"], "composite": false, "keyName": "IDX_product_type_deleted_at", "primary": false, @@ -436,9 +406,7 @@ }, { "keyName": "product_type_pkey", - "columnNames": [ - "id" - ], + "columnNames": ["id"], "composite": false, "primary": true, "unique": true @@ -511,12 +479,7 @@ "autoincrement": false, "primary": false, "nullable": false, - "enumItems": [ - "draft", - "proposed", - "published", - "rejected" - ], + "enumItems": ["draft", "proposed", "published", "rejected"], "mappedType": "enum" }, "thumbnail": { @@ -681,18 +644,14 @@ "schema": "public", "indexes": [ { - "columnNames": [ - "type_id" - ], + "columnNames": ["type_id"], "composite": false, "keyName": "IDX_product_type_id", "primary": false, "unique": false }, { - "columnNames": [ - "deleted_at" - ], + "columnNames": ["deleted_at"], "composite": false, "keyName": "IDX_product_deleted_at", "primary": false, @@ -700,18 +659,14 @@ }, { "keyName": "IDX_product_handle_unique", - "columnNames": [ - "handle" - ], + "columnNames": ["handle"], "composite": false, "primary": false, "unique": true }, { "keyName": "product_pkey", - "columnNames": [ - "id" - ], + "columnNames": ["id"], "composite": false, "primary": true, "unique": true @@ -721,26 +676,18 @@ "foreignKeys": { "product_collection_id_foreign": { "constraintName": "product_collection_id_foreign", - "columnNames": [ - "collection_id" - ], + "columnNames": ["collection_id"], "localTableName": "public.product", - "referencedColumnNames": [ - "id" - ], + "referencedColumnNames": ["id"], "referencedTableName": "public.product_collection", "deleteRule": "set null", "updateRule": "cascade" }, "product_type_id_foreign": { "constraintName": "product_type_id_foreign", - "columnNames": [ - "type_id" - ], + "columnNames": ["type_id"], "localTableName": "public.product", - "referencedColumnNames": [ - "id" - ], + "referencedColumnNames": ["id"], "referencedTableName": "public.product_type", "deleteRule": "set null", "updateRule": "cascade" @@ -800,18 +747,14 @@ "schema": "public", "indexes": [ { - "columnNames": [ - "product_id" - ], + "columnNames": ["product_id"], "composite": false, "keyName": "IDX_product_option_product_id", "primary": false, "unique": false }, { - "columnNames": [ - "deleted_at" - ], + "columnNames": ["deleted_at"], "composite": false, "keyName": "IDX_product_option_deleted_at", "primary": false, @@ -819,9 +762,7 @@ }, { "keyName": "product_option_pkey", - "columnNames": [ - "id" - ], + "columnNames": ["id"], "composite": false, "primary": true, "unique": true @@ -831,13 +772,9 @@ "foreignKeys": { "product_option_product_id_foreign": { "constraintName": "product_option_product_id_foreign", - "columnNames": [ - "product_id" - ], + "columnNames": ["product_id"], "localTableName": "public.product_option", - "referencedColumnNames": [ - "id" - ], + "referencedColumnNames": ["id"], "referencedTableName": "public.product", "updateRule": "cascade" } @@ -869,10 +806,7 @@ "indexes": [ { "keyName": "product_tags_pkey", - "columnNames": [ - "product_id", - "product_tag_id" - ], + "columnNames": ["product_id", "product_tag_id"], "composite": true, "primary": true, "unique": true @@ -882,26 +816,18 @@ "foreignKeys": { "product_tags_product_id_foreign": { "constraintName": "product_tags_product_id_foreign", - "columnNames": [ - "product_id" - ], + "columnNames": ["product_id"], "localTableName": "public.product_tags", - "referencedColumnNames": [ - "id" - ], + "referencedColumnNames": ["id"], "referencedTableName": "public.product", "deleteRule": "cascade", "updateRule": "cascade" }, "product_tags_product_tag_id_foreign": { "constraintName": "product_tags_product_tag_id_foreign", - "columnNames": [ - "product_tag_id" - ], + "columnNames": ["product_tag_id"], "localTableName": "public.product_tags", - "referencedColumnNames": [ - "id" - ], + "referencedColumnNames": ["id"], "referencedTableName": "public.product_tag", "deleteRule": "cascade", "updateRule": "cascade" @@ -934,10 +860,7 @@ "indexes": [ { "keyName": "product_images_pkey", - "columnNames": [ - "product_id", - "product_image_id" - ], + "columnNames": ["product_id", "product_image_id"], "composite": true, "primary": true, "unique": true @@ -947,26 +870,18 @@ "foreignKeys": { "product_images_product_id_foreign": { "constraintName": "product_images_product_id_foreign", - "columnNames": [ - "product_id" - ], + "columnNames": ["product_id"], "localTableName": "public.product_images", - "referencedColumnNames": [ - "id" - ], + "referencedColumnNames": ["id"], "referencedTableName": "public.product", "deleteRule": "cascade", "updateRule": "cascade" }, "product_images_product_image_id_foreign": { "constraintName": "product_images_product_image_id_foreign", - "columnNames": [ - "product_image_id" - ], + "columnNames": ["product_image_id"], "localTableName": "public.product_images", - "referencedColumnNames": [ - "id" - ], + "referencedColumnNames": ["id"], "referencedTableName": "public.image", "deleteRule": "cascade", "updateRule": "cascade" @@ -999,10 +914,7 @@ "indexes": [ { "keyName": "product_category_product_pkey", - "columnNames": [ - "product_id", - "product_category_id" - ], + "columnNames": ["product_id", "product_category_id"], "composite": true, "primary": true, "unique": true @@ -1012,26 +924,18 @@ "foreignKeys": { "product_category_product_product_id_foreign": { "constraintName": "product_category_product_product_id_foreign", - "columnNames": [ - "product_id" - ], + "columnNames": ["product_id"], "localTableName": "public.product_category_product", - "referencedColumnNames": [ - "id" - ], + "referencedColumnNames": ["id"], "referencedTableName": "public.product", "deleteRule": "cascade", "updateRule": "cascade" }, "product_category_product_product_category_id_foreign": { "constraintName": "product_category_product_product_category_id_foreign", - "columnNames": [ - "product_category_id" - ], + "columnNames": ["product_category_id"], "localTableName": "public.product_category_product", - "referencedColumnNames": [ - "id" - ], + "referencedColumnNames": ["id"], "referencedTableName": "public.product_category", "deleteRule": "cascade", "updateRule": "cascade" @@ -1259,18 +1163,14 @@ "schema": "public", "indexes": [ { - "columnNames": [ - "deleted_at" - ], + "columnNames": ["deleted_at"], "composite": false, "keyName": "IDX_product_variant_deleted_at", "primary": false, "unique": false }, { - "columnNames": [ - "product_id" - ], + "columnNames": ["product_id"], "composite": false, "keyName": "IDX_product_variant_product_id", "primary": false, @@ -1278,45 +1178,35 @@ }, { "keyName": "IDX_product_variant_sku_unique", - "columnNames": [ - "sku" - ], + "columnNames": ["sku"], "composite": false, "primary": false, "unique": true }, { "keyName": "IDX_product_variant_barcode_unique", - "columnNames": [ - "barcode" - ], + "columnNames": ["barcode"], "composite": false, "primary": false, "unique": true }, { "keyName": "IDX_product_variant_ean_unique", - "columnNames": [ - "ean" - ], + "columnNames": ["ean"], "composite": false, "primary": false, "unique": true }, { "keyName": "IDX_product_variant_upc_unique", - "columnNames": [ - "upc" - ], + "columnNames": ["upc"], "composite": false, "primary": false, "unique": true }, { "keyName": "product_variant_pkey", - "columnNames": [ - "id" - ], + "columnNames": ["id"], "composite": false, "primary": true, "unique": true @@ -1326,13 +1216,9 @@ "foreignKeys": { "product_variant_product_id_foreign": { "constraintName": "product_variant_product_id_foreign", - "columnNames": [ - "product_id" - ], + "columnNames": ["product_id"], "localTableName": "public.product_variant", - "referencedColumnNames": [ - "id" - ], + "referencedColumnNames": ["id"], "referencedTableName": "public.product", "deleteRule": "cascade", "updateRule": "cascade" @@ -1401,27 +1287,21 @@ "schema": "public", "indexes": [ { - "columnNames": [ - "option_id" - ], + "columnNames": ["option_id"], "composite": false, "keyName": "IDX_product_option_value_option_id", "primary": false, "unique": false }, { - "columnNames": [ - "variant_id" - ], + "columnNames": ["variant_id"], "composite": false, "keyName": "IDX_product_option_value_variant_id", "primary": false, "unique": false }, { - "columnNames": [ - "deleted_at" - ], + "columnNames": ["deleted_at"], "composite": false, "keyName": "IDX_product_option_value_deleted_at", "primary": false, @@ -1429,9 +1309,7 @@ }, { "keyName": "product_option_value_pkey", - "columnNames": [ - "id" - ], + "columnNames": ["id"], "composite": false, "primary": true, "unique": true @@ -1441,25 +1319,17 @@ "foreignKeys": { "product_option_value_option_id_foreign": { "constraintName": "product_option_value_option_id_foreign", - "columnNames": [ - "option_id" - ], + "columnNames": ["option_id"], "localTableName": "public.product_option_value", - "referencedColumnNames": [ - "id" - ], + "referencedColumnNames": ["id"], "referencedTableName": "public.product_option", "updateRule": "cascade" }, "product_option_value_variant_id_foreign": { "constraintName": "product_option_value_variant_id_foreign", - "columnNames": [ - "variant_id" - ], + "columnNames": ["variant_id"], "localTableName": "public.product_option_value", - "referencedColumnNames": [ - "id" - ], + "referencedColumnNames": ["id"], "referencedTableName": "public.product_variant", "deleteRule": "cascade", "updateRule": "cascade" diff --git a/packages/product/src/models/product-option-value.ts b/packages/product/src/models/product-option-value.ts index 764c605549..941e40c0c6 100644 --- a/packages/product/src/models/product-option-value.ts +++ b/packages/product/src/models/product-option-value.ts @@ -7,11 +7,10 @@ import { PrimaryKey, Property, } from "@mikro-orm/core" -import { generateEntityId } from "@medusajs/utils" +import { ProductOption, ProductVariant } from "./index" -import ProductOption from "./product-option" -import { ProductVariant } from "./index" import { SoftDeletable } from "../utils" +import { generateEntityId } from "@medusajs/utils" type OptionalFields = | "created_at" diff --git a/packages/product/src/models/product-option.ts b/packages/product/src/models/product-option.ts index 3ce3246c66..a764795d31 100644 --- a/packages/product/src/models/product-option.ts +++ b/packages/product/src/models/product-option.ts @@ -1,3 +1,4 @@ +import { generateEntityId } from "@medusajs/utils" import { BeforeCreate, Cascade, @@ -10,7 +11,6 @@ import { PrimaryKey, Property, } from "@mikro-orm/core" -import { generateEntityId } from "@medusajs/utils" import { Product } from "./index" import ProductOptionValue from "./product-option-value" import { SoftDeletable } from "../utils" diff --git a/packages/product/src/models/product-variant.ts b/packages/product/src/models/product-variant.ts index b8c7ccf329..3bc78441fc 100644 --- a/packages/product/src/models/product-variant.ts +++ b/packages/product/src/models/product-variant.ts @@ -1,3 +1,4 @@ +import { generateEntityId } from "@medusajs/utils" import { BeforeCreate, Cascade, @@ -11,7 +12,6 @@ import { Property, Unique, } from "@mikro-orm/core" -import { generateEntityId } from "@medusajs/utils" import { Product } from "@models" import ProductOptionValue from "./product-option-value" import { SoftDeletable } from "../utils" diff --git a/packages/product/src/module-definition.ts b/packages/product/src/module-definition.ts index be772a047f..0485dfdbd0 100644 --- a/packages/product/src/module-definition.ts +++ b/packages/product/src/module-definition.ts @@ -1,8 +1,9 @@ +import * as ProductModels from "@models" + import { ModuleExports } from "@medusajs/types" import { ProductModuleService } from "@services" -import loadContainer from "./loaders/container" import loadConnection from "./loaders/connection" -import * as ProductModels from "@models" +import loadContainer from "./loaders/container" const service = ProductModuleService const loaders = [loadContainer, loadConnection] as any diff --git a/packages/product/src/scripts/migration-down.ts b/packages/product/src/scripts/migration-down.ts index ad0a1b7fce..a67de38c71 100644 --- a/packages/product/src/scripts/migration-down.ts +++ b/packages/product/src/scripts/migration-down.ts @@ -1,8 +1,10 @@ -import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types" -import { createConnection } from "../utils" import * as ProductModels from "@models" + +import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types" + import { EntitySchema } from "@mikro-orm/core" import { ModulesSdkUtils } from "@medusajs/utils" +import { createConnection } from "../utils" /** * This script is only valid for mikro orm managers. If a user provide a custom manager diff --git a/packages/product/src/services/index.ts b/packages/product/src/services/index.ts index 6772acd4c5..0d7ba770b1 100644 --- a/packages/product/src/services/index.ts +++ b/packages/product/src/services/index.ts @@ -1,9 +1,9 @@ -export { default as ProductModuleService } from "./product-module-service" export { default as ProductService } from "./product" +export { default as ProductCategoryService } from "./product-category" +export { default as ProductCollectionService } from "./product-collection" +export { default as ProductModuleService } from "./product-module-service" export { default as ProductTagService } from "./product-tag" export { default as ProductVariantService } from "./product-variant" -export { default as ProductCollectionService } from "./product-collection" -export { default as ProductCategoryService } from "./product-category" export { default as ProductTypeService } from "./product-type" export { default as ProductOptionService } from "./product-option" export { default as ProductImageService } from "./product-image" diff --git a/packages/product/src/services/product-collection.ts b/packages/product/src/services/product-collection.ts index 62d02a7d28..0ce5fd549d 100644 --- a/packages/product/src/services/product-collection.ts +++ b/packages/product/src/services/product-collection.ts @@ -1,7 +1,8 @@ -import { ProductCollection } from "@models" import { Context, DAL, FindConfig, ProductTypes } from "@medusajs/types" import { ModulesSdkUtils, retrieveEntity } from "@medusajs/utils" +import { ProductCollection } from "@models" + type InjectedDependencies = { productCollectionRepository: DAL.RepositoryService } diff --git a/packages/product/src/services/product-module-service.ts b/packages/product/src/services/product-module-service.ts index 8c741c9474..68c185745a 100644 --- a/packages/product/src/services/product-module-service.ts +++ b/packages/product/src/services/product-module-service.ts @@ -23,6 +23,7 @@ import { DAL, FindConfig, InternalModuleDeclaration, + JoinerServiceConfig, ProductTypes, } from "@medusajs/types" import ProductImageService from "./product-image" @@ -34,6 +35,7 @@ import { MedusaContext, } from "@medusajs/utils" import { shouldForceTransaction } from "../utils" +import { joinerConfig } from "./../joiner-config" type InjectedDependencies = { baseRepository: DAL.RepositoryService @@ -96,6 +98,10 @@ export default class ProductModuleService< this.productOptionService_ = productOptionService } + __joinerConfig(): JoinerServiceConfig { + return joinerConfig + } + async list( filters: ProductTypes.FilterableProductProps = {}, config: FindConfig = {}, diff --git a/packages/stock-location/src/initialize/index.ts b/packages/stock-location/src/initialize/index.ts index 152f6187d2..355b4a845f 100644 --- a/packages/stock-location/src/initialize/index.ts +++ b/packages/stock-location/src/initialize/index.ts @@ -5,8 +5,8 @@ import { Modules, } from "@medusajs/modules-sdk" import { IEventBusService, IStockLocationService } from "@medusajs/types" -import { StockLocationServiceInitializeOptions } from "../types" import { moduleDefinition } from "../module-definition" +import { StockLocationServiceInitializeOptions } from "../types" export const initialize = async ( options: StockLocationServiceInitializeOptions | ExternalModuleDeclaration, @@ -15,7 +15,7 @@ export const initialize = async ( } ): Promise => { const serviceKey = Modules.STOCK_LOCATION - const loaded = await MedusaModule.bootstrap( + const loaded = await MedusaModule.bootstrap( serviceKey, "@medusajs/stock-location", options as InternalModuleDeclaration | ExternalModuleDeclaration, @@ -23,5 +23,5 @@ export const initialize = async ( injectedDependencies ) - return loaded[serviceKey] as IStockLocationService + return loaded[serviceKey] } diff --git a/packages/stock-location/src/joiner-config.ts b/packages/stock-location/src/joiner-config.ts new file mode 100644 index 0000000000..b71126429f --- /dev/null +++ b/packages/stock-location/src/joiner-config.ts @@ -0,0 +1,15 @@ +import { Modules } from "@medusajs/modules-sdk" +import { JoinerServiceConfig } from "@medusajs/types" + +export const joinerConfig: JoinerServiceConfig = { + serviceName: Modules.STOCK_LOCATION, + primaryKeys: ["id"], + alias: [ + { + name: "stock_location", + }, + { + name: "stock_locations", + }, + ], +} diff --git a/packages/stock-location/src/services/stock-location.ts b/packages/stock-location/src/services/stock-location.ts index 764b0adc3e..124aaed2c6 100644 --- a/packages/stock-location/src/services/stock-location.ts +++ b/packages/stock-location/src/services/stock-location.ts @@ -4,6 +4,7 @@ import { FilterableStockLocationProps, FindConfig, IEventBusService, + JoinerServiceConfig, MODULE_RESOURCE_TYPE, SharedContext, StockLocationAddressInput, @@ -11,13 +12,13 @@ import { } from "@medusajs/types" import { InjectEntityManager, - isDefined, MedusaContext, MedusaError, + isDefined, setMetadata, } from "@medusajs/utils" import { EntityManager } from "typeorm" - +import { joinerConfig } from "../joiner-config" import { StockLocation, StockLocationAddress } from "../models" import { buildQuery } from "../utils/build-query" @@ -49,6 +50,10 @@ export default class StockLocationService { this.eventBusService_ = eventBusService } + __joinerConfig(): JoinerServiceConfig { + return joinerConfig + } + /** * Lists all stock locations that match the given selector. * @param selector - Properties to filter by. diff --git a/packages/types/src/inventory/index.ts b/packages/types/src/inventory/index.ts index 3f4dbbc489..eade309433 100644 --- a/packages/types/src/inventory/index.ts +++ b/packages/types/src/inventory/index.ts @@ -1,2 +1,2 @@ export * from "./common" -export * from "./inventory" +export * from "./service" diff --git a/packages/types/src/inventory/inventory.ts b/packages/types/src/inventory/service.ts similarity index 96% rename from packages/types/src/inventory/inventory.ts rename to packages/types/src/inventory/service.ts index 6f144ee20f..ea37bea93c 100644 --- a/packages/types/src/inventory/inventory.ts +++ b/packages/types/src/inventory/service.ts @@ -13,9 +13,11 @@ import { } from "./common" import { FindConfig } from "../common" +import { JoinerServiceConfig } from "../joiner" import { SharedContext } from ".." export interface IInventoryService { + __joinerConfig(): JoinerServiceConfig listInventoryItems( selector: FilterableInventoryItemProps, config?: FindConfig, @@ -72,12 +74,12 @@ export interface IInventoryService { ): Promise createInventoryLevel( - data: CreateInventoryLevelInput , + data: CreateInventoryLevelInput, context?: SharedContext ): Promise - + createInventoryLevels( - data: CreateInventoryLevelInput[], + data: CreateInventoryLevelInput[], context?: SharedContext ): Promise diff --git a/packages/types/src/joiner/index.ts b/packages/types/src/joiner/index.ts index 3061d3f60f..0edd29f3d7 100644 --- a/packages/types/src/joiner/index.ts +++ b/packages/types/src/joiner/index.ts @@ -4,16 +4,25 @@ export type JoinerRelationship = { primaryKey: string serviceName: string inverse?: boolean // In an inverted relationship the foreign key is on the other service and the primary key is on the current service + isList?: boolean // Force the relationship to return a list + args?: Record // Extra arguments to pass to the remoteFetchData callback +} + +export interface JoinerServiceConfigAlias { + name: string + args?: Record // Extra arguments to pass to the remoteFetchData callback } export interface JoinerServiceConfig { serviceName: string + alias?: JoinerServiceConfigAlias | JoinerServiceConfigAlias[] // Property name to use as entrypoint to the service primaryKeys: string[] relationships?: JoinerRelationship[] extends?: { serviceName: string - resolve: JoinerRelationship + relationship: JoinerRelationship }[] + args?: Record // Extra arguments to pass to the remoteFetchData callback } export interface JoinerArgument { @@ -23,7 +32,8 @@ export interface JoinerArgument { } export interface RemoteJoinerQuery { - service: string + service?: string + alias?: string expands?: Array<{ property: string fields: string[] diff --git a/packages/types/src/modules-sdk/index.ts b/packages/types/src/modules-sdk/index.ts index 2ed4ab48f9..d49ad27cca 100644 --- a/packages/types/src/modules-sdk/index.ts +++ b/packages/types/src/modules-sdk/index.ts @@ -31,6 +31,8 @@ export type InternalModuleDeclaration = { dependencies?: string[] resolve?: string options?: Record + alias?: string // If multiple modules are registered with the same key, the alias can be used to differentiate them + main?: boolean // If the module is the main module for the key when multiple ones are registered } export type ExternalModuleDeclaration = { @@ -40,6 +42,8 @@ export type ExternalModuleDeclaration = { url: string keepAlive: boolean } + alias?: string // If multiple modules are registered with the same key, the alias can be used to differentiate them + main?: boolean // If the module is the main module for the key when multiple ones are registered } export type ModuleResolution = { @@ -58,6 +62,7 @@ export type ModuleDefinition = { label: string canOverride?: boolean isRequired?: boolean + isQueryable?: boolean // If the modules should be queryable via Remote Joiner dependencies?: string[] defaultModuleDeclaration: | InternalModuleDeclaration diff --git a/packages/types/src/product/index.ts b/packages/types/src/product/index.ts index 1aa665fd54..eade309433 100644 --- a/packages/types/src/product/index.ts +++ b/packages/types/src/product/index.ts @@ -1,2 +1,2 @@ -export * from "./service" export * from "./common" +export * from "./service" diff --git a/packages/types/src/product/service.ts b/packages/types/src/product/service.ts index d2ba499628..9d04bce26e 100644 --- a/packages/types/src/product/service.ts +++ b/packages/types/src/product/service.ts @@ -11,10 +11,14 @@ import { ProductTagDTO, ProductVariantDTO, } from "./common" -import { FindConfig } from "../common" + import { Context } from "../shared-context" +import { FindConfig } from "../common" +import { JoinerServiceConfig } from "../joiner" export interface IProductModuleService { + __joinerConfig(): JoinerServiceConfig + retrieve(productId: string, sharedContext?: Context): Promise list( diff --git a/packages/types/src/stock-location/index.ts b/packages/types/src/stock-location/index.ts index a73fc94bd6..eade309433 100644 --- a/packages/types/src/stock-location/index.ts +++ b/packages/types/src/stock-location/index.ts @@ -1,2 +1,2 @@ export * from "./common" -export * from "./stock-location" +export * from "./service" diff --git a/packages/types/src/stock-location/service.ts b/packages/types/src/stock-location/service.ts new file mode 100644 index 0000000000..d327e6e0ce --- /dev/null +++ b/packages/types/src/stock-location/service.ts @@ -0,0 +1,43 @@ +import { FindConfig } from "../common/common" +import { JoinerServiceConfig } from "../joiner" +import { SharedContext } from "../shared-context" +import { + CreateStockLocationInput, + FilterableStockLocationProps, + StockLocationDTO, + UpdateStockLocationInput, +} from "./common" + +export interface IStockLocationService { + __joinerConfig(): JoinerServiceConfig + list( + selector: FilterableStockLocationProps, + config?: FindConfig, + context?: SharedContext + ): Promise + + listAndCount( + selector: FilterableStockLocationProps, + config?: FindConfig, + context?: SharedContext + ): Promise<[StockLocationDTO[], number]> + + retrieve( + id: string, + config?: FindConfig, + context?: SharedContext + ): Promise + + create( + input: CreateStockLocationInput, + context?: SharedContext + ): Promise + + update( + id: string, + input: UpdateStockLocationInput, + context?: SharedContext + ): Promise + + delete(id: string, context?: SharedContext): Promise +} diff --git a/yarn.lock b/yarn.lock index 8eba09e878..8caefb9e82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6403,7 +6403,6 @@ __metadata: uuid: ^9.0.0 winston: ^3.8.2 peerDependencies: - "@medusajs/types": ^1.8.7 medusa-interfaces: ^1.3.7 typeorm: ^0.3.16 bin: @@ -6415,7 +6414,8 @@ __metadata: version: 0.0.0-use.local resolution: "@medusajs/modules-sdk@workspace:packages/modules-sdk" dependencies: - "@medusajs/types": ^1.8.8 + "@medusajs/orchestration": ^0.0.2 + "@medusajs/types": ^1.8.11 "@medusajs/utils": ^1.9.1 awilix: ^8.0.0 cross-env: ^5.2.1 @@ -6472,11 +6472,11 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/orchestration@workspace:packages/orchestration": +"@medusajs/orchestration@^0.0.2, @medusajs/orchestration@workspace:packages/orchestration": version: 0.0.0-use.local resolution: "@medusajs/orchestration@workspace:packages/orchestration" dependencies: - "@medusajs/types": ^1.8.10 + "@medusajs/types": ^1.8.11 "@medusajs/utils": ^1.9.2 cross-env: ^5.2.1 graphql: ^16.6.0