diff --git a/.changeset/spotty-rules-hide.md b/.changeset/spotty-rules-hide.md new file mode 100644 index 0000000000..c0b9732de9 --- /dev/null +++ b/.changeset/spotty-rules-hide.md @@ -0,0 +1,9 @@ +--- +"@medusajs/stock-location": minor +"@medusajs/inventory": minor +"medusa-core-utils": minor +"@medusajs/modules-sdk": minor +"@medusajs/medusa": minor +--- + +Inventory and Stock location modules supporting isolated connection diff --git a/integration-tests/helpers/setup-server.js b/integration-tests/helpers/setup-server.js index 94bebd5323..621064f1b4 100644 --- a/integration-tests/helpers/setup-server.js +++ b/integration-tests/helpers/setup-server.js @@ -30,7 +30,13 @@ module.exports = ({ cwd, redisUrl, uploadDir, verbose, env }) => { : ["ignore", "ignore", "ignore", "ipc"], }) + medusaProcess.on("error", (err) => { + console.log(err) + process.exit() + }) + medusaProcess.on("uncaughtException", (err) => { + console.log(err) medusaProcess.kill() }) diff --git a/integration-tests/plugins/__tests__/inventory/inventory-items/index.js b/integration-tests/plugins/__tests__/inventory/inventory-items/index.js index e0baad38f1..e28721e31d 100644 --- a/integration-tests/plugins/__tests__/inventory/inventory-items/index.js +++ b/integration-tests/plugins/__tests__/inventory/inventory-items/index.js @@ -278,7 +278,7 @@ describe("Inventory Items endpoints", () => { }) }) - it.only("Creates an inventory item using the api", async () => { + it.skip("Creates an inventory item using the api", async () => { const product = await simpleProductFactory(dbConnection, {}) const api = useApi() diff --git a/packages/cache-inmemory/src/index.ts b/packages/cache-inmemory/src/index.ts index 5821a885e8..90f7328a70 100644 --- a/packages/cache-inmemory/src/index.ts +++ b/packages/cache-inmemory/src/index.ts @@ -2,12 +2,10 @@ import { ModuleExports } from "@medusajs/modules-sdk" import InMemoryCacheService from "./services/inmemory-cache" -const loaders = [] const service = InMemoryCacheService const moduleDefinition: ModuleExports = { service, - loaders, } export default moduleDefinition diff --git a/packages/cache-inmemory/src/services/inmemory-cache.ts b/packages/cache-inmemory/src/services/inmemory-cache.ts index 28ee9803b6..9a55ecf39f 100644 --- a/packages/cache-inmemory/src/services/inmemory-cache.ts +++ b/packages/cache-inmemory/src/services/inmemory-cache.ts @@ -59,6 +59,8 @@ class InMemoryCacheService implements ICacheService { this.invalidate(key) }, ttl * 1000) + ref.unref() + this.timoutRefs.set(key, ref) this.store.set(key, record) } diff --git a/packages/cache-redis/src/index.ts b/packages/cache-redis/src/index.ts index c9d72fb693..1c5dbca28d 100644 --- a/packages/cache-redis/src/index.ts +++ b/packages/cache-redis/src/index.ts @@ -1,7 +1,7 @@ import { ModuleExports } from "@medusajs/modules-sdk" -import { RedisCacheService } from "./services" import Loader from "./loaders" +import { RedisCacheService } from "./services" const service = RedisCacheService const loaders = [Loader] diff --git a/packages/inventory/package.json b/packages/inventory/package.json index 3089c07ca9..72cdb68e14 100644 --- a/packages/inventory/package.json +++ b/packages/inventory/package.json @@ -21,11 +21,12 @@ "cross-env": "^5.2.1", "jest": "^25.5.4", "ts-jest": "^25.5.1", - "typeorm": "^0.3.11", "typescript": "^4.4.4" }, "dependencies": { - "@medusajs/modules-sdk": "*" + "@medusajs/modules-sdk": "*", + "awilix": "^8.0.0", + "typeorm": "^0.3.11" }, "scripts": { "watch": "tsc --build --watch", @@ -35,7 +36,6 @@ "test:unit": "jest --passWithNoTests" }, "peerDependencies": { - "@medusajs/medusa": "1.7.13", - "typeorm": "^0.3.11" + "@medusajs/medusa": "1.7.13" } } diff --git a/packages/inventory/src/config.ts b/packages/inventory/src/config.ts deleted file mode 100644 index 0ff17abbc7..0000000000 --- a/packages/inventory/src/config.ts +++ /dev/null @@ -1 +0,0 @@ -export const CONNECTION_NAME = "inventory_connection" diff --git a/packages/inventory/src/index.ts b/packages/inventory/src/index.ts index 3e84682758..084596647c 100644 --- a/packages/inventory/src/index.ts +++ b/packages/inventory/src/index.ts @@ -1,12 +1,14 @@ -import ConnectionLoader from "./loaders/connection" -import InventoryService from "./services/inventory" +import loadConnection from "./loaders/connection" +import loadContainer from "./loaders/container" + +import migrations from "./migrations" import * as InventoryModels from "./models" -import * as SchemaMigration from "./migrations/schema-migrations/1665748086258-inventory_setup" +import InventoryService from "./services/inventory" + import { ModuleExports } from "@medusajs/modules-sdk" const service = InventoryService -const migrations = [SchemaMigration] -const loaders = [ConnectionLoader] +const loaders = [loadContainer, loadConnection] const models = Object.values(InventoryModels) const moduleDefinition: ModuleExports = { diff --git a/packages/inventory/src/loaders/connection.ts b/packages/inventory/src/loaders/connection.ts index 09d777bae8..096239d8ca 100644 --- a/packages/inventory/src/loaders/connection.ts +++ b/packages/inventory/src/loaders/connection.ts @@ -1,9 +1,49 @@ import { - ConfigurableModuleDeclaration, + InternalModuleDeclaration, LoaderOptions, + MODULE_RESOURCE_TYPE, + MODULE_SCOPE, } from "@medusajs/modules-sdk" +import { DataSource, DataSourceOptions } from "typeorm" + +import * as InventoryModels from "../models" +import { MedusaError } from "medusa-core-utils" +import { asValue } from "awilix" export default async ( - { configModule }: LoaderOptions, - moduleDeclaration?: ConfigurableModuleDeclaration -): Promise => {} + { options, container }: LoaderOptions, + moduleDeclaration?: InternalModuleDeclaration +): Promise => { + if ( + moduleDeclaration?.scope === MODULE_SCOPE.INTERNAL && + moduleDeclaration.resources === MODULE_RESOURCE_TYPE.SHARED + ) { + return + } + + const dbData = options?.database as Record + + if (!dbData) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + `Database config is not present at module config "options.database"` + ) + } + + const entities = Object.values(InventoryModels) + const dataSource = new DataSource({ + type: dbData.database_type, + url: dbData.database_url, + database: dbData.database_database, + extra: dbData.database_extra || {}, + schema: dbData.database_schema, + entities, + logging: dbData.database_logging, + } as DataSourceOptions) + + await dataSource.initialize() + + container.register({ + manager: asValue(dataSource.manager), + }) +} diff --git a/packages/inventory/src/loaders/container.ts b/packages/inventory/src/loaders/container.ts new file mode 100644 index 0000000000..4b4bef1cb3 --- /dev/null +++ b/packages/inventory/src/loaders/container.ts @@ -0,0 +1,20 @@ +import { InternalModuleDeclaration, LoaderOptions } from "@medusajs/modules-sdk" + +import { + InventoryItemService, + InventoryLevelService, + ReservationItemService, +} from "../services" + +import { asClass } from "awilix" + +export default async ( + { container }: LoaderOptions, + moduleDeclaration?: InternalModuleDeclaration +): Promise => { + container.register({ + inventoryItemService: asClass(InventoryItemService).singleton(), + inventoryLevelService: asClass(InventoryLevelService).singleton(), + reservationItemService: asClass(ReservationItemService).singleton(), + }) +} diff --git a/packages/inventory/src/loaders/index.ts b/packages/inventory/src/loaders/index.ts new file mode 100644 index 0000000000..3614963d8c --- /dev/null +++ b/packages/inventory/src/loaders/index.ts @@ -0,0 +1,2 @@ +export * from "./connection" +export * from "./container" diff --git a/packages/inventory/src/migrations/index.ts b/packages/inventory/src/migrations/index.ts new file mode 100644 index 0000000000..3a7a2e2c16 --- /dev/null +++ b/packages/inventory/src/migrations/index.ts @@ -0,0 +1,3 @@ +import * as setup from "./schema-migrations/1665748086258-inventory_setup" + +export default [setup] diff --git a/packages/inventory/src/migrations/schema-migrations/1665748086258-inventory_setup.ts b/packages/inventory/src/migrations/schema-migrations/1665748086258-inventory_setup.ts index e5ac73ec03..7928929de3 100644 --- a/packages/inventory/src/migrations/schema-migrations/1665748086258-inventory_setup.ts +++ b/packages/inventory/src/migrations/schema-migrations/1665748086258-inventory_setup.ts @@ -1,46 +1,4 @@ -import { ConfigModule } from "@medusajs/medusa" -import { - createConnection, - ConnectionOptions, - MigrationInterface, - QueryRunner, -} from "typeorm" - -import { CONNECTION_NAME } from "../../config" - -export const up = async ({ configModule }: { configModule: ConfigModule }) => { - const connection = await createConnection({ - name: CONNECTION_NAME, - type: configModule.projectConfig.database_type, - url: configModule.projectConfig.database_url, - extra: configModule.projectConfig.database_extra || {}, - schema: configModule.projectConfig.database_schema, - migrations: [inventorySetup1665748086258], - logging: true, - } as ConnectionOptions) - - await connection.runMigrations() - await connection.close() -} - -export const down = async ({ - configModule, -}: { - configModule: ConfigModule -}) => { - const connection = await createConnection({ - name: CONNECTION_NAME, - type: configModule.projectConfig.database_type, - url: configModule.projectConfig.database_url, - extra: configModule.projectConfig.database_extra || {}, - schema: configModule.projectConfig.database_schema, - migrations: [inventorySetup1665748086258], - logging: true, - } as ConnectionOptions) - - await connection.undoLastMigration({ transaction: "all" }) - await connection.close() -} +import { MigrationInterface, QueryRunner } from "typeorm" export class inventorySetup1665748086258 implements MigrationInterface { name = "inventorySetup1665748086258" diff --git a/packages/inventory/src/services/inventory.ts b/packages/inventory/src/services/inventory.ts index 1b865b7bfe..8bfa855ad0 100644 --- a/packages/inventory/src/services/inventory.ts +++ b/packages/inventory/src/services/inventory.ts @@ -1,7 +1,4 @@ -import { - ConfigurableModuleDeclaration, - MODULE_RESOURCE_TYPE, -} from "@medusajs/modules-sdk" +import { InternalModuleDeclaration } from "@medusajs/modules-sdk" import { CreateInventoryItemInput, @@ -22,18 +19,20 @@ import { } from "@medusajs/medusa" import { MedusaError } from "medusa-core-utils" -import { EntityManager } from "typeorm" import { InventoryItemService, InventoryLevelService, ReservationItemService, } from "./" +import { EntityManager } from "typeorm" type InjectedDependencies = { manager: EntityManager eventBusService: IEventBusService + inventoryItemService: InventoryItemService + inventoryLevelService: InventoryLevelService + reservationItemService: ReservationItemService } - export default class InventoryService extends TransactionBaseService implements IInventoryService @@ -44,34 +43,23 @@ export default class InventoryService protected readonly inventoryLevelService_: InventoryLevelService constructor( - { eventBusService, manager }: InjectedDependencies, + { + eventBusService, + manager, + inventoryItemService, + inventoryLevelService, + reservationItemService, + }: InjectedDependencies, options?: unknown, - moduleDeclaration?: ConfigurableModuleDeclaration + moduleDeclaration?: InternalModuleDeclaration ) { // @ts-ignore super(...arguments) - if (moduleDeclaration?.resources !== MODULE_RESOURCE_TYPE.SHARED) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "At the moment this module can only be used with shared resources" - ) - } - this.eventBusService_ = eventBusService - this.inventoryItemService_ = new InventoryItemService({ - eventBusService, - manager, - }) - this.inventoryLevelService_ = new InventoryLevelService({ - eventBusService, - manager, - }) - this.reservationItemService_ = new ReservationItemService({ - eventBusService, - manager, - inventoryLevelService: this.inventoryLevelService_, - }) + this.inventoryItemService_ = inventoryItemService + this.inventoryLevelService_ = inventoryLevelService + this.reservationItemService_ = reservationItemService } /** diff --git a/packages/inventory/tsconfig.json b/packages/inventory/tsconfig.json index 0fc6130e78..c92a3d6ed2 100644 --- a/packages/inventory/tsconfig.json +++ b/packages/inventory/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { - "lib": [ - "es5", - "es6", - "es2019" - ], - "target": "es5", + "lib": ["es2020"], + "target": "ES2020", "outDir": "./dist", "esModuleInterop": true, "declaration": true, @@ -19,14 +15,13 @@ "strictFunctionTypes": true, "noImplicitThis": true, "allowJs": true, - "skipLibCheck": true, - "downlevelIteration": true // to use ES5 specific tooling + "skipLibCheck": true }, - "include": ["./src/**/*", "index.d.ts"], + "include": ["src"], "exclude": [ - "./dist/**/*", + "dist", "./src/**/__tests__", "./src/**/__mocks__", "node_modules" ] -} \ No newline at end of file +} diff --git a/packages/medusa-core-utils/package.json b/packages/medusa-core-utils/package.json index 27f441da83..dc10c81e0a 100644 --- a/packages/medusa-core-utils/package.json +++ b/packages/medusa-core-utils/package.json @@ -20,16 +20,11 @@ "author": "Sebastian Rindom", "license": "MIT", "devDependencies": { - "class-transformer": "^0.5.1", - "class-validator": "^0.13.2", "cross-env": "^5.2.1", "jest": "^25.5.4", "ts-jest": "^25.5.1", "typescript": "^4.4.4" }, - "dependencies": { - "joi": "^17.3.0", - "joi-objectid": "^3.0.1" - }, + "dependencies": {}, "gitHead": "a69b1e85be1da3b1b5bc4c5446471252623c8808" } diff --git a/packages/medusa-core-utils/src/get-config-file.ts b/packages/medusa-core-utils/src/get-config-file.ts index 3b0aa198a1..ec0a11ad78 100644 --- a/packages/medusa-core-utils/src/get-config-file.ts +++ b/packages/medusa-core-utils/src/get-config-file.ts @@ -9,7 +9,7 @@ import { join } from "path" function getConfigFile( rootDir: string, configName: string -): { configModule: TConfig; configFilePath: string, error?: any } { +): { configModule: TConfig; configFilePath: string; error?: any } { const configPath = join(rootDir, configName) let configFilePath = `` let configModule @@ -22,6 +22,10 @@ function getConfigFile( err = e } + if (configModule && typeof configModule.default === "object") { + configModule = configModule.default + } + return { configModule, configFilePath, error: err } } diff --git a/packages/medusa-core-utils/src/index.ts b/packages/medusa-core-utils/src/index.ts index d67e05ebd7..e2858fbdc0 100644 --- a/packages/medusa-core-utils/src/index.ts +++ b/packages/medusa-core-utils/src/index.ts @@ -8,7 +8,7 @@ export * from "./graceful-shutdown-server" export { default as humanizeAmount } from "./humanize-amount" export { indexTypes } from "./index-types" export * from "./is-defined" +export * from "./medusa-container" export { parseCorsOrigins } from "./parse-cors-origins" export { transformIdableFields } from "./transform-idable-fields" -export { default as Validator } from "./validator" export { default as zeroDecimalCurrencies } from "./zero-decimal-currencies" diff --git a/packages/medusa-core-utils/src/medusa-container.ts b/packages/medusa-core-utils/src/medusa-container.ts new file mode 100644 index 0000000000..4948add53a --- /dev/null +++ b/packages/medusa-core-utils/src/medusa-container.ts @@ -0,0 +1,61 @@ +import { + asFunction, + asValue, + AwilixContainer, + ClassOrFunctionReturning, + createContainer, + Resolver, +} from "awilix" + +export type MedusaContainer = AwilixContainer & { + registerAdd: (name: string, registration: T) => MedusaContainer + createScope: () => MedusaContainer +} + +function asArray( + resolvers: (ClassOrFunctionReturning | Resolver)[] +): { resolve: (container: AwilixContainer) => unknown[] } { + return { + resolve: (container: AwilixContainer) => + resolvers.map((resolver) => container.build(resolver)), + } +} + +function registerAdd( + this: MedusaContainer, + name: string, + registration: typeof asFunction | typeof asValue +) { + const storeKey = name + "_STORE" + + if (this.registrations[storeKey] === undefined) { + this.register(storeKey, asValue([] as Resolver[])) + } + const store = this.resolve(storeKey) as ( + | ClassOrFunctionReturning + | Resolver + )[] + + if (this.registrations[name] === undefined) { + this.register(name, asArray(store)) + } + store.unshift(registration) + + return this +} + +export function createMedusaContainer(...args): MedusaContainer { + const container = createContainer.apply(null, args) as MedusaContainer + + container.registerAdd = registerAdd.bind(container) + + const originalScope = container.createScope + container.createScope = () => { + const scoped = originalScope() as MedusaContainer + scoped.registerAdd = registerAdd.bind(scoped) + + return scoped + } + + return container +} diff --git a/packages/medusa-core-utils/src/validator.ts b/packages/medusa-core-utils/src/validator.ts deleted file mode 100644 index 0839fe1513..0000000000 --- a/packages/medusa-core-utils/src/validator.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { default as Joi } from "joi" - -const dateFilter = () => { - return Joi.object({ - lt: Joi.alternatives(Joi.date().timestamp("unix"), Joi.date()), - gt: Joi.alternatives(Joi.date().timestamp("unix"), Joi.date()), - gte: Joi.alternatives(Joi.date().timestamp("unix"), Joi.date()), - lte: Joi.alternatives(Joi.date().timestamp("unix"), Joi.date()), - }) -} - -Object.assign(Joi, { - objectId: require("joi-objectid")(Joi), - address: () => { - return Joi.alternatives().try( - Joi.string(), - Joi.object().keys({ - first_name: Joi.string().required(), - last_name: Joi.string().required(), - company: Joi.string().optional(), - address_1: Joi.string().required(), - address_2: Joi.string().allow(null, "").optional(), - city: Joi.string().required(), - country_code: Joi.string().required(), - province: Joi.string().allow(null, "").optional(), - postal_code: Joi.string().required(), - phone: Joi.string().optional(), - metadata: Joi.object().allow(null, {}).optional(), - }) - ) - }, - dateFilter, - orderFilter: () => { - return Joi.object().keys({ - id: Joi.string(), - q: Joi.string(), - status: Joi.array() - .items( - Joi.string().valid( - "pending", - "completed", - "archived", - "canceled", - "requires_action" - ) - ) - .single(), - fulfillment_status: Joi.array() - .items( - Joi.string().valid( - "not_fulfilled", - "fulfilled", - "partially_fulfilled", - "shipped", - "partially_shipped", - "canceled", - "returned", - "partially_returned", - "requires_action" - ) - ) - .single(), - payment_status: Joi.array() - .items( - Joi.string().valid( - "captured", - "awaiting", - "not_paid", - "refunded", - "partially_refunded", - "canceled", - "requires_action" - ) - ) - .single(), - display_id: Joi.string(), - cart_id: Joi.string(), - offset: Joi.string(), - limit: Joi.string(), - expand: Joi.string(), - fields: Joi.string(), - customer_id: Joi.string(), - email: Joi.string(), - region_id: Joi.string(), - currency_code: Joi.string(), - tax_rate: Joi.string(), - canceled_at: dateFilter(), - created_at: dateFilter(), - updated_at: dateFilter(), - }) - }, - productFilter: () => { - return Joi.object().keys({ - id: Joi.string(), - q: Joi.string().allow(null, ""), - status: Joi.array() - .items(Joi.string().valid("proposed", "draft", "published", "rejected")) - .single(), - collection_id: Joi.array().items(Joi.string()).single(), - tags: Joi.array().items(Joi.string()).single(), - title: Joi.string(), - description: Joi.string(), - handle: Joi.string(), - is_giftcard: Joi.string(), - type: Joi.string(), - offset: Joi.string(), - limit: Joi.string(), - expand: Joi.string(), - fields: Joi.string(), - order: Joi.string().optional(), - created_at: dateFilter(), - updated_at: dateFilter(), - deleted_at: dateFilter(), - }) - }, -}) - -declare module "joi" { - interface Root { - objectId: Joi.StringSchema - address: () => Joi.AlternativesSchema - dateFilter: () => Joi.ObjectSchema - orderFilter: () => Joi.ObjectSchema - productFilter: () => Joi.ObjectSchema - } -} - -export default Joi diff --git a/packages/medusa/src/api/routes/admin/stock-locations/delete-stock-location.ts b/packages/medusa/src/api/routes/admin/stock-locations/delete-stock-location.ts index b523066089..c389e57727 100644 --- a/packages/medusa/src/api/routes/admin/stock-locations/delete-stock-location.ts +++ b/packages/medusa/src/api/routes/admin/stock-locations/delete-stock-location.ts @@ -75,7 +75,7 @@ export default async (req, res) => { .withTransaction(transactionManager) .removeLocation(id) - await stockLocationService.withTransaction(transactionManager).delete(id) + await stockLocationService.delete(id) if (inventoryService) { await Promise.all([ diff --git a/packages/medusa/src/commands/utils/get-migrations.js b/packages/medusa/src/commands/utils/get-migrations.js index 3baf1641d6..f635e634f1 100644 --- a/packages/medusa/src/commands/utils/get-migrations.js +++ b/packages/medusa/src/commands/utils/get-migrations.js @@ -1,11 +1,15 @@ -import glob from "glob" -import path from "path" -import fs from "fs" -import { isString } from "lodash" -import { sync as existsSync } from "fs-exists-cached" -import { getConfigFile, createRequireFromPath } from "medusa-core-utils" -import { handleConfigError } from "../../loaders/config" import { registerModules } from "@medusajs/modules-sdk" +import fs from "fs" +import { sync as existsSync } from "fs-exists-cached" +import glob from "glob" +import { isString } from "lodash" +import { + createRequireFromPath, + getConfigFile, + isDefined, +} from "medusa-core-utils" +import path from "path" +import { handleConfigError } from "../../loaders/config" function createFileContentHash(path, files) { return path + files @@ -104,7 +108,7 @@ export function getInternalModules(configModule) { let loadedModule = null try { - loadedModule = require(moduleResolution.moduleDeclaration.resolve).default + loadedModule = require(moduleResolution.resolutionPath).default } catch (error) { console.log("Error loading Module", error) continue @@ -179,15 +183,12 @@ export const getEnabledMigrations = (migrationDirs, isFlagEnabled) => { return allMigrations .map((file) => { const loaded = require(file) - if ( - typeof loaded.featureFlag === "undefined" || - isFlagEnabled(loaded.featureFlag) - ) { - return file + if (!isDefined(loaded.featureFlag) || isFlagEnabled(loaded.featureFlag)) { + delete loaded.featureFlag + return Object.values(loaded) } - - return false }) + .flat() .filter(Boolean) } @@ -201,22 +202,17 @@ export const getModuleMigrations = (configModule, isFlagEnabled) => { const isolatedMigrations = {} const moduleMigrations = (mod.migrations ?? []) - .map((migrations) => { - const all = [] - for (const migration of Object.values(migrations)) { - // TODO: revisit how Modules export their migration entrypoints up/down - if (["up", "down"].includes(migration.name)) { - isolatedMigrations[migration.name] = migration - } else if ( - typeof migration.featureFlag === "undefined" || - isFlagEnabled(migration.featureFlag) - ) { - all.push(migration) - } + .map((migration) => { + if ( + !isDefined(migration.featureFlag) || + isFlagEnabled(migration.featureFlag) + ) { + delete migration.featureFlag + return Object.values(migration) } - return all }) .flat() + .filter(Boolean) allModules.push({ moduleDeclaration: loadedModule.moduleDeclaration, diff --git a/packages/medusa/src/helpers/test-request.js b/packages/medusa/src/helpers/test-request.js index 0923744925..3437cccc34 100644 --- a/packages/medusa/src/helpers/test-request.js +++ b/packages/medusa/src/helpers/test-request.js @@ -36,7 +36,6 @@ const config = { admin_cors: "", store_cors: "", }, - moduleResolutions, } const testApp = express() @@ -70,7 +69,7 @@ featureFlagLoader(config) servicesLoader({ container, configModule: config }) strategiesLoader({ container, configModule: config }) passportLoader({ app: testApp, container, configModule: config }) -moduleLoader({ container, configModule: config }) +moduleLoader({ container, moduleResolutions }) testApp.use((req, res, next) => { req.scope = container.createScope() diff --git a/packages/medusa/src/interfaces/services/stock-location.ts b/packages/medusa/src/interfaces/services/stock-location.ts index 3826337743..44d60620ab 100644 --- a/packages/medusa/src/interfaces/services/stock-location.ts +++ b/packages/medusa/src/interfaces/services/stock-location.ts @@ -1,15 +1,12 @@ -import { EntityManager } from "typeorm" import { FindConfig } from "../../types/common" - import { - StockLocationDTO, - FilterableStockLocationProps, CreateStockLocationInput, + FilterableStockLocationProps, + StockLocationDTO, UpdateStockLocationInput, } from "../../types/stock-location" export interface IStockLocationService { - withTransaction(transactionManager?: EntityManager): this list( selector: FilterableStockLocationProps, config?: FindConfig diff --git a/packages/medusa/src/loaders/__tests__/plugins.spec.ts b/packages/medusa/src/loaders/__tests__/plugins.spec.ts index 1faf3d4a2b..06bc62d48a 100644 --- a/packages/medusa/src/loaders/__tests__/plugins.spec.ts +++ b/packages/medusa/src/loaders/__tests__/plugins.spec.ts @@ -1,17 +1,15 @@ import { - asFunction, asValue, AwilixContainer, ClassOrFunctionReturning, - createContainer, Resolver, } from "awilix" import { mkdirSync, rmSync, writeFileSync } from "fs" import { resolve } from "path" import Logger from "../logger" import { registerServices, registerStrategies } from "../plugins" -import { MedusaContainer } from "../../types/global" -import { Connection, EntityManager } from "typeorm"; +import { DataSource, EntityManager } from "typeorm" +import { createMedusaContainer } from "medusa-core-utils" // ***** TEMPLATES ***** const buildServiceTemplate = (name: string): string => { @@ -37,10 +35,10 @@ const buildBatchJobStrategyTemplate = (name: string, type: string): string => { class ${name}BatchStrategy extends AbstractBatchJobStrategy{ static identifier = '${name}-identifier'; static batchType = '${type}'; - + manager_ transactionManager_ - + validateContext(context) { throw new Error("Method not implemented.") } @@ -57,7 +55,7 @@ const buildBatchJobStrategyTemplate = (name: string, type: string): string => { throw new Error("Method not implemented.") } } - + export default ${name}BatchStrategy ` } @@ -65,7 +63,7 @@ const buildBatchJobStrategyTemplate = (name: string, type: string): string => { const buildPriceSelectionStrategyTemplate = (name: string): string => { return ` import { AbstractPriceSelectionStrategy } from "../../../../interfaces/price-selection-strategy" - + class ${name}PriceSelectionStrategy extends AbstractPriceSelectionStrategy { withTransaction() { throw new Error("Method not implemented."); @@ -74,7 +72,7 @@ const buildPriceSelectionStrategyTemplate = (name: string): string => { throw new Error("Method not implemented."); } } - + export default ${name}PriceSelectionStrategy ` } @@ -111,32 +109,10 @@ function asArray( // ***** TESTS ***** describe("plugins loader", () => { - const container = createContainer() as MedusaContainer - container.registerAdd = function ( - this: MedusaContainer, - name: string, - registration: typeof asFunction | typeof asValue - ): MedusaContainer { - const storeKey = name + "_STORE" - - if (this.registrations[storeKey] === undefined) { - this.register(storeKey, asValue([] as Resolver[])) - } - const store = this.resolve(storeKey) as ( - | ClassOrFunctionReturning - | Resolver - )[] - - if (this.registrations[name] === undefined) { - this.register(name, asArray(store)) - } - store.unshift(registration) - - return this - }.bind(container) + const container = createMedusaContainer() container.register("logger", asValue(Logger)) - container.register("manager", asValue(new EntityManager({} as Connection))) + container.register("manager", asValue(new EntityManager({} as DataSource))) const pluginsDetails = { resolve: resolve(__dirname, "__pluginsLoaderTest__"), @@ -207,8 +183,9 @@ describe("plugins loader", () => { }) it("registers price selection strategy", () => { - const priceSelectionStrategy = - container.resolve("priceSelectionStrategy") as (...args: unknown[]) => any + const priceSelectionStrategy = container.resolve( + "priceSelectionStrategy" + ) as (...args: unknown[]) => any expect(priceSelectionStrategy).toBeTruthy() expect(priceSelectionStrategy.constructor.name).toBe( @@ -217,8 +194,9 @@ describe("plugins loader", () => { }) it("registers tax calculation strategy", () => { - const taxCalculationStrategy = - container.resolve("taxCalculationStrategy") as (...args: unknown[]) => any + const taxCalculationStrategy = container.resolve( + "taxCalculationStrategy" + ) as (...args: unknown[]) => any expect(taxCalculationStrategy).toBeTruthy() expect(taxCalculationStrategy.constructor.name).toBe( @@ -227,8 +205,9 @@ describe("plugins loader", () => { }) it("registers batch job strategies as single array", () => { - const batchJobStrategies = - container.resolve("batchJobStrategies") as (...args: unknown[]) => any + const batchJobStrategies = container.resolve("batchJobStrategies") as ( + ...args: unknown[] + ) => any expect(batchJobStrategies).toBeTruthy() expect(Array.isArray(batchJobStrategies)).toBeTruthy() @@ -236,8 +215,9 @@ describe("plugins loader", () => { }) it("registers batch job strategies by type and only keep the last", () => { - const batchJobStrategy = - container.resolve("batchType_type-1") as (...args: unknown[]) => any + const batchJobStrategy = container.resolve("batchType_type-1") as ( + ...args: unknown[] + ) => any expect(batchJobStrategy).toBeTruthy() expect(batchJobStrategy.constructor.name).toBe("testBatch2BatchStrategy") diff --git a/packages/medusa/src/loaders/config.ts b/packages/medusa/src/loaders/config.ts index a6b82d7a82..0c4748b44d 100644 --- a/packages/medusa/src/loaders/config.ts +++ b/packages/medusa/src/loaders/config.ts @@ -1,7 +1,6 @@ import { getConfigFile } from "medusa-core-utils" import { ConfigModule } from "../types/global" import logger from "./logger" -import { registerModules } from "@medusajs/modules-sdk" const isProduction = ["production", "prod"].includes(process.env.NODE_ENV || "") @@ -65,8 +64,6 @@ export default (rootDirectory: string): ConfigModule => { ) } - const moduleResolutions = registerModules(configModule) - return { projectConfig: { jwt_secret: jwt_secret ?? "supersecret", @@ -74,7 +71,6 @@ export default (rootDirectory: string): ConfigModule => { ...configModule?.projectConfig, }, modules: configModule.modules ?? {}, - moduleResolutions, featureFlags: configModule?.featureFlags ?? {}, plugins: configModule?.plugins ?? [], } diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index 216b47aa54..8bb96ca9e8 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -1,11 +1,4 @@ -import { - asFunction, - asValue, - AwilixContainer, - createContainer, - Resolver, -} from "awilix" -import { ClassOrFunctionReturning } from "awilix/lib/container" +import { asValue } from "awilix" import { Express, NextFunction, Request, Response } from "express" import { track } from "medusa-telemetry" import { EOL } from "os" @@ -30,7 +23,8 @@ import servicesLoader from "./services" import strategiesLoader from "./strategies" import subscribersLoader from "./subscribers" -import { moduleLoader } from "@medusajs/modules-sdk" +import { moduleLoader, registerModules } from "@medusajs/modules-sdk" +import { createMedusaContainer } from "medusa-core-utils" type Options = { directory: string @@ -49,32 +43,9 @@ export default async ({ }> => { const configModule = loadConfig(rootDirectory) - const container = createContainer() as MedusaContainer + const container = createMedusaContainer() container.register("configModule", asValue(configModule)) - container.registerAdd = function ( - this: MedusaContainer, - name: string, - registration: typeof asFunction | typeof asValue - ) { - const storeKey = name + "_STORE" - - if (this.registrations[storeKey] === undefined) { - this.register(storeKey, asValue([] as Resolver[])) - } - const store = this.resolve(storeKey) as ( - | ClassOrFunctionReturning - | Resolver - )[] - - if (this.registrations[name] === undefined) { - this.register(name, asArray(store)) - } - store.unshift(registration) - - return this - }.bind(container) - // Add additional information to context of request expressApp.use((req: Request, res: Response, next: NextFunction) => { const ipAddress = requestIp.getClientIp(req) as string @@ -118,7 +89,11 @@ export default async ({ const modulesActivity = Logger.activity(`Initializing modules${EOL}`) track("MODULES_INIT_STARTED") - await moduleLoader({ container, configModule, logger: Logger }) + await moduleLoader({ + container, + moduleResolutions: registerModules(configModule), + logger: Logger, + }) const modAct = Logger.success(modulesActivity, "Modules initialized") || {} track("MODULES_INIT_COMPLETED", { duration: modAct.duration }) @@ -200,12 +175,3 @@ export default async ({ return { container, dbConnection, app: expressApp } } - -function asArray( - resolvers: (ClassOrFunctionReturning | Resolver)[] -): { resolve: (container: AwilixContainer) => unknown[] } { - return { - resolve: (container: AwilixContainer) => - resolvers.map((resolver) => container.build(resolver)), - } -} diff --git a/packages/medusa/src/types/global.ts b/packages/medusa/src/types/global.ts index 67c1332832..f6f4bf55d6 100644 --- a/packages/medusa/src/types/global.ts +++ b/packages/medusa/src/types/global.ts @@ -1,8 +1,5 @@ -import { - ConfigurableModuleDeclaration, - ModuleResolution, -} from "@medusajs/modules-sdk" -import { AwilixContainer } from "awilix" +import { InternalModuleDeclaration } from "@medusajs/modules-sdk" +import { MedusaContainer as coreMedusaContainer } from "medusa-core-utils" import { Request } from "express" import { LoggerOptions } from "typeorm" import { Logger as _Logger } from "winston" @@ -34,9 +31,7 @@ export type ClassConstructor = { new (...args: unknown[]): T } -export type MedusaContainer = AwilixContainer & { - registerAdd: (name: string, registration: T) => MedusaContainer -} +export type MedusaContainer = coreMedusaContainer export type Logger = _Logger & { progress: (activityId: string, msg: string) => void @@ -96,11 +91,7 @@ export type ConfigModule = { admin_cors?: string } featureFlags: Record - modules?: Record< - string, - false | string | Partial - > - moduleResolutions?: Record + modules?: Record> plugins: ( | { resolve: string diff --git a/packages/medusa/src/types/store.ts b/packages/medusa/src/types/store.ts index 20a9580121..9390a9a49a 100644 --- a/packages/medusa/src/types/store.ts +++ b/packages/medusa/src/types/store.ts @@ -1,6 +1,6 @@ import { Store, PaymentProvider, FulfillmentProvider } from "../models" import { FeatureFlagsResponse } from "./feature-flags" -import { ModulesResponse } from "@medusajs/modules-sdk" +import { ModulesResponse as sdkModulesResponse } from "@medusajs/modules-sdk" export type UpdateStoreInput = { name?: string @@ -29,6 +29,7 @@ export type UpdateStoreInput = { * description: The resolution path of the module or false if module is not installed. * type: string */ +export type ModulesResponse = sdkModulesResponse /** * @schema ExtendedStoreDTO diff --git a/packages/modules-sdk/package.json b/packages/modules-sdk/package.json index 3fa52def4d..8a3d6e27cd 100644 --- a/packages/modules-sdk/package.json +++ b/packages/modules-sdk/package.json @@ -24,6 +24,8 @@ }, "dependencies": { "awilix": "^8.0.0", + "glob": "7.1.6", + "medusa-core-utils": "^1.1.39", "medusa-telemetry": "^0.0.16", "resolve-cwd": "^3.0.0" }, diff --git a/packages/modules-sdk/src/definitions.ts b/packages/modules-sdk/src/definitions.ts index b583e44657..0a89d65107 100644 --- a/packages/modules-sdk/src/definitions.ts +++ b/packages/modules-sdk/src/definitions.ts @@ -8,6 +8,7 @@ export const MODULE_DEFINITIONS: ModuleDefinition[] = [ label: "StockLocationService", isRequired: false, canOverride: true, + dependencies: ["eventBusService"], defaultModuleDeclaration: { scope: MODULE_SCOPE.INTERNAL, resources: MODULE_RESOURCE_TYPE.SHARED, @@ -20,6 +21,7 @@ export const MODULE_DEFINITIONS: ModuleDefinition[] = [ label: "InventoryService", isRequired: false, canOverride: true, + dependencies: ["eventBusService"], defaultModuleDeclaration: { scope: MODULE_SCOPE.INTERNAL, resources: MODULE_RESOURCE_TYPE.SHARED, diff --git a/packages/modules-sdk/src/index.ts b/packages/modules-sdk/src/index.ts index 0f105816bb..6fc5746d50 100644 --- a/packages/modules-sdk/src/index.ts +++ b/packages/modules-sdk/src/index.ts @@ -1,6 +1,4 @@ -export * from "./types" -export * from "./loaders" - -export * from "./module-helper" - export * from "./definitions" +export * from "./loaders" +export * from "./module-helper" +export * from "./types" diff --git a/packages/modules-sdk/src/loaders/__tests__/module-loader.ts b/packages/modules-sdk/src/loaders/__tests__/module-loader.ts index 7833308b25..ece2e92f8d 100644 --- a/packages/modules-sdk/src/loaders/__tests__/module-loader.ts +++ b/packages/modules-sdk/src/loaders/__tests__/module-loader.ts @@ -1,14 +1,7 @@ +import { EOL } from "os" +import { AwilixContainer, ClassOrFunctionReturning, Resolver } from "awilix" +import { createMedusaContainer } from "medusa-core-utils" import { - asFunction, - asValue, - AwilixContainer, - ClassOrFunctionReturning, - createContainer, - Resolver, -} from "awilix" -import { - ConfigModule, - MedusaContainer, ModuleResolution, MODULE_RESOURCE_TYPE, MODULE_SCOPE, @@ -28,46 +21,9 @@ function asArray( const logger = { warn: jest.fn(), + error: jest.fn(), } as any -const buildConfigModule = ( - configParts: Partial -): ConfigModule => { - return { - modules: {}, - moduleResolutions: {}, - ...configParts, - } -} - -const buildContainer = () => { - const container = createContainer() as MedusaContainer - - container.registerAdd = function ( - this: MedusaContainer, - name: string, - registration: typeof asFunction | typeof asValue - ): MedusaContainer { - const storeKey = name + "_STORE" - - if (this.registrations[storeKey] === undefined) { - this.register(storeKey, asValue([] as Resolver[])) - } - const store = this.resolve(storeKey) as ( - | ClassOrFunctionReturning - | Resolver - )[] - - if (this.registrations[name] === undefined) { - this.register(name, asArray(store)) - } - store.unshift(registration) - - return this - }.bind(container) - - return container -} describe("modules loader", () => { let container @@ -76,7 +32,7 @@ describe("modules loader", () => { }) beforeEach(() => { - container = buildContainer() + container = createMedusaContainer() }) it("registers service as undefined in container when no resolution path is given", async () => { @@ -100,10 +56,7 @@ describe("modules loader", () => { }, } - const configModule = buildConfigModule({ - moduleResolutions, - }) - await moduleLoader({ container, configModule, logger }) + await moduleLoader({ container, moduleResolutions, logger }) const testService = container.resolve( moduleResolutions.testService.definition.key @@ -132,11 +85,7 @@ describe("modules loader", () => { }, } - const configModule = buildConfigModule({ - moduleResolutions, - }) - - await moduleLoader({ container, configModule, logger }) + await moduleLoader({ container, moduleResolutions, logger }) const testService = container.resolve( moduleResolutions.testService.definition.key, @@ -175,14 +124,10 @@ describe("modules loader", () => { }, } - const configModule = buildConfigModule({ - moduleResolutions, - }) - - await moduleLoader({ container, configModule, logger }) + await moduleLoader({ container, moduleResolutions, logger }) expect(logger.warn).toHaveBeenCalledWith( - "Could not resolve module: TestService. Error: Loaders for module TestService failed: loader" + `Could not resolve module: TestService. Error: Loaders for module TestService failed: loader${EOL}` ) }) @@ -207,14 +152,10 @@ describe("modules loader", () => { }, } - const configModule = buildConfigModule({ - moduleResolutions, - }) - - await moduleLoader({ container, configModule, logger }) + await moduleLoader({ container, moduleResolutions, logger }) expect(logger.warn).toHaveBeenCalledWith( - "Could not resolve module: TestService. Error: No service found in module. Make sure that your module exports a service." + `Could not resolve module: TestService. Error: No service found in module. Make sure your module exports at least one service.${EOL}` ) }) @@ -241,14 +182,11 @@ describe("modules loader", () => { }, } - const configModule = buildConfigModule({ - moduleResolutions, - }) try { - await moduleLoader({ container, configModule, logger }) + await moduleLoader({ container, moduleResolutions, logger }) } catch (err) { expect(err.message).toEqual( - "No service found in module. Make sure that your module exports a service." + "No service found in module. Make sure your module exports at least one service." ) } }) @@ -276,11 +214,8 @@ describe("modules loader", () => { }, } - const configModule = buildConfigModule({ - moduleResolutions, - }) try { - await moduleLoader({ container, configModule, logger }) + await moduleLoader({ container, moduleResolutions, logger }) } catch (err) { expect(err.message).toEqual( "The module TestService has to define its scope (internal | external)" @@ -308,14 +243,11 @@ describe("modules loader", () => { moduleDeclaration: { scope: MODULE_SCOPE.INTERNAL, }, - }, + } as any, } - const configModule = buildConfigModule({ - moduleResolutions, - }) try { - await moduleLoader({ container, configModule, logger }) + await moduleLoader({ container, moduleResolutions, logger }) } catch (err) { expect(err.message).toEqual( "The module TestService is missing its resources config" diff --git a/packages/modules-sdk/src/loaders/__tests__/module-definitions.ts b/packages/modules-sdk/src/loaders/__tests__/register-modules.ts similarity index 63% rename from packages/modules-sdk/src/loaders/__tests__/module-definitions.ts rename to packages/modules-sdk/src/loaders/__tests__/register-modules.ts index 7b5ec35383..c7577f4c93 100644 --- a/packages/modules-sdk/src/loaders/__tests__/module-definitions.ts +++ b/packages/modules-sdk/src/loaders/__tests__/register-modules.ts @@ -1,10 +1,10 @@ import { - ConfigModule, + InternalModuleDeclaration, ModuleDefinition, MODULE_RESOURCE_TYPE, MODULE_SCOPE, } from "../../types" -import { registerModules } from "../module-definition" +import { registerModules } from "../register-modules" import MODULE_DEFINITIONS from "../../definitions" const RESOLVED_PACKAGE = "@medusajs/test-service-resolved" @@ -35,17 +35,19 @@ describe("module definitions loader", () => { it("Resolves module with default definition given empty config", () => { MODULE_DEFINITIONS.push({ ...defaultDefinition }) - const res = registerModules({ modules: {} } as ConfigModule) + const res = registerModules({ modules: {} }) - expect(res[defaultDefinition.key]).toEqual({ - resolutionPath: defaultDefinition.defaultPackage, - definition: defaultDefinition, - options: {}, - moduleDeclaration: { - scope: "internal", - resources: "shared", - }, - }) + expect(res[defaultDefinition.key]).toEqual( + expect.objectContaining({ + resolutionPath: defaultDefinition.defaultPackage, + definition: defaultDefinition, + options: {}, + moduleDeclaration: { + scope: "internal", + resources: "shared", + }, + }) + ) }) describe("boolean config", () => { @@ -54,13 +56,15 @@ describe("module definitions loader", () => { const res = registerModules({ modules: { [defaultDefinition.key]: false }, - } as ConfigModule) - - expect(res[defaultDefinition.key]).toEqual({ - resolutionPath: false, - definition: defaultDefinition, - options: {}, }) + + expect(res[defaultDefinition.key]).toEqual( + expect.objectContaining({ + resolutionPath: false, + definition: defaultDefinition, + options: {}, + }) + ) }) it("Fails to resolve module with no resolution path when given false for a required module", () => { @@ -70,7 +74,7 @@ describe("module definitions loader", () => { try { registerModules({ modules: { [defaultDefinition.key]: false }, - } as ConfigModule) + }) } catch (err) { expect(err.message).toEqual( `Module: ${defaultDefinition.label} is required` @@ -88,17 +92,19 @@ describe("module definitions loader", () => { const res = registerModules({ modules: {}, - } as ConfigModule) - - expect(res[defaultDefinition.key]).toEqual({ - resolutionPath: false, - definition: definition, - options: {}, - moduleDeclaration: { - scope: "internal", - resources: "shared", - }, }) + + expect(res[defaultDefinition.key]).toEqual( + expect.objectContaining({ + resolutionPath: false, + definition: definition, + options: {}, + moduleDeclaration: { + scope: "internal", + resources: "shared", + }, + }) + ) }) }) @@ -110,17 +116,19 @@ describe("module definitions loader", () => { modules: { [defaultDefinition.key]: defaultDefinition.defaultPackage, }, - } as ConfigModule) - - expect(res[defaultDefinition.key]).toEqual({ - resolutionPath: RESOLVED_PACKAGE, - definition: defaultDefinition, - options: {}, - moduleDeclaration: { - scope: "internal", - resources: "shared", - }, }) + + expect(res[defaultDefinition.key]).toEqual( + expect.objectContaining({ + resolutionPath: RESOLVED_PACKAGE, + definition: defaultDefinition, + options: {}, + moduleDeclaration: { + scope: "internal", + resources: "shared", + }, + }) + ) }) }) @@ -131,22 +139,25 @@ describe("module definitions loader", () => { const res = registerModules({ modules: { [defaultDefinition.key]: { + scope: MODULE_SCOPE.INTERNAL, resolve: defaultDefinition.defaultPackage, resources: MODULE_RESOURCE_TYPE.ISOLATED, - }, - }, - } as ConfigModule) - - expect(res[defaultDefinition.key]).toEqual({ - resolutionPath: RESOLVED_PACKAGE, - definition: defaultDefinition, - options: {}, - moduleDeclaration: { - scope: "internal", - resources: "isolated", - resolve: defaultDefinition.defaultPackage, + } as InternalModuleDeclaration, }, }) + + expect(res[defaultDefinition.key]).toEqual( + expect.objectContaining({ + resolutionPath: RESOLVED_PACKAGE, + definition: defaultDefinition, + options: {}, + moduleDeclaration: { + scope: "internal", + resources: "isolated", + resolve: defaultDefinition.defaultPackage, + }, + }) + ) }) it("Resolves default resolution path and provides options when only options are provided", () => { @@ -158,18 +169,20 @@ describe("module definitions loader", () => { options: { test: 123 }, }, }, - } as unknown as ConfigModule) + } as any) - expect(res[defaultDefinition.key]).toEqual({ - resolutionPath: defaultDefinition.defaultPackage, - definition: defaultDefinition, - options: { test: 123 }, - moduleDeclaration: { - scope: "internal", - resources: "shared", + expect(res[defaultDefinition.key]).toEqual( + expect.objectContaining({ + resolutionPath: defaultDefinition.defaultPackage, + definition: defaultDefinition, options: { test: 123 }, - }, - }) + moduleDeclaration: { + scope: "internal", + resources: "shared", + options: { test: 123 }, + }, + }) + ) }) it("Resolves resolution path and provides options when only options are provided", () => { @@ -184,19 +197,21 @@ describe("module definitions loader", () => { resources: "isolated", }, }, - } as unknown as ConfigModule) + } as any) - expect(res[defaultDefinition.key]).toEqual({ - resolutionPath: RESOLVED_PACKAGE, - definition: defaultDefinition, - options: { test: 123 }, - moduleDeclaration: { - scope: "internal", - resources: "isolated", - resolve: defaultDefinition.defaultPackage, + expect(res[defaultDefinition.key]).toEqual( + expect.objectContaining({ + resolutionPath: RESOLVED_PACKAGE, + definition: defaultDefinition, options: { test: 123 }, - }, - }) + moduleDeclaration: { + scope: "internal", + resources: "isolated", + resolve: defaultDefinition.defaultPackage, + options: { test: 123 }, + }, + }) + ) }) }) }) diff --git a/packages/modules-sdk/src/loaders/index.ts b/packages/modules-sdk/src/loaders/index.ts index bc82711793..0ff93bba81 100644 --- a/packages/modules-sdk/src/loaders/index.ts +++ b/packages/modules-sdk/src/loaders/index.ts @@ -1,3 +1,2 @@ export * from "./module-loader" - -export * from "./module-definition" +export * from "./register-modules" diff --git a/packages/modules-sdk/src/loaders/module-definition.ts b/packages/modules-sdk/src/loaders/module-definition.ts deleted file mode 100644 index be3f1957aa..0000000000 --- a/packages/modules-sdk/src/loaders/module-definition.ts +++ /dev/null @@ -1,61 +0,0 @@ -import resolveCwd from "resolve-cwd" - -import { ConfigModule, ModuleResolution } from "../types" -import MODULE_DEFINITIONS from "../definitions" - -export const registerModules = ({ modules }: ConfigModule) => { - const moduleResolutions = {} as Record - const projectModules = modules ?? {} - - for (const definition of MODULE_DEFINITIONS) { - let resolutionPath = definition.defaultPackage - - const moduleConfiguration = projectModules[definition.key] - - if (typeof moduleConfiguration === "boolean") { - if (!moduleConfiguration && definition.isRequired) { - throw new Error(`Module: ${definition.label} is required`) - } - if (!moduleConfiguration) { - moduleResolutions[definition.key] = { - resolutionPath: false, - definition, - options: {}, - } - continue - } - } - - // If user added a module and it's overridable, we resolve that instead - if ( - definition.canOverride && - (typeof moduleConfiguration === "string" || - (typeof moduleConfiguration === "object" && - moduleConfiguration.resolve)) - ) { - resolutionPath = resolveCwd( - typeof moduleConfiguration === "string" - ? moduleConfiguration - : (moduleConfiguration.resolve as string) - ) - } - - const moduleDeclaration = - typeof moduleConfiguration === "object" ? moduleConfiguration : {} - - moduleResolutions[definition.key] = { - resolutionPath, - definition, - moduleDeclaration: { - ...definition.defaultModuleDeclaration, - ...moduleDeclaration, - }, - options: - typeof moduleConfiguration === "object" - ? moduleConfiguration.options ?? {} - : {}, - } - } - - return moduleResolutions -} diff --git a/packages/modules-sdk/src/loaders/module-loader.ts b/packages/modules-sdk/src/loaders/module-loader.ts index fb8ebef322..9ff18b9d50 100644 --- a/packages/modules-sdk/src/loaders/module-loader.ts +++ b/packages/modules-sdk/src/loaders/module-loader.ts @@ -1,38 +1,41 @@ -import { asFunction, asValue } from "awilix" -import { trackInstallation } from "medusa-telemetry" +import { asValue } from "awilix" +import { EOL } from "os" +import { loadInternalModule } from "./utils" + import { - ClassConstructor, - ConfigModule, - LoaderOptions, Logger, MedusaContainer, - ModuleExports, ModuleResolution, - MODULE_RESOURCE_TYPE, MODULE_SCOPE, -} from "../types/module" +} from "../types" import { ModulesHelper } from "../module-helper" export const moduleHelper = new ModulesHelper() -const registerModule = async ( +async function loadModule( container: MedusaContainer, resolution: ModuleResolution, - configModule: ConfigModule, logger: Logger -): Promise<{ error?: Error } | void> => { - const constainerName = resolution.definition.registrationName +): Promise<{ error?: Error } | void> { + const registrationName = resolution.definition.registrationName + + const { scope, resources } = resolution.moduleDeclaration ?? ({} as any) + + if (scope === MODULE_SCOPE.EXTERNAL) { + // TODO: implement external Resolvers + // return loadExternalModule(...) + throw new Error("External Modules are not supported yet.") + } - const { scope, resources } = resolution.moduleDeclaration ?? {} if (!scope || (scope === MODULE_SCOPE.INTERNAL && !resources)) { let message = `The module ${resolution.definition.label} has to define its scope (internal | external)` - if (scope && !resources) { + if (scope === MODULE_SCOPE.INTERNAL && !resources) { message = `The module ${resolution.definition.label} is missing its resources config` } container.register({ - [constainerName]: asValue(undefined), + [registrationName]: asValue(undefined), }) return { @@ -42,107 +45,38 @@ const registerModule = async ( if (!resolution.resolutionPath) { container.register({ - [constainerName]: asValue(undefined), + [registrationName]: asValue(undefined), }) return } - let loadedModule: ModuleExports - try { - loadedModule = (await import(resolution.resolutionPath!)).default - } catch (error) { - return { error } - } - - const moduleService = loadedModule?.service || null - - if (!moduleService) { - return { - error: new Error( - "No service found in module. Make sure that your module exports a service." - ), - } - } - - if ( - scope === MODULE_SCOPE.INTERNAL && - resources === MODULE_RESOURCE_TYPE.SHARED - ) { - const moduleModels = loadedModule?.models || null - if (moduleModels) { - moduleModels.map((val: ClassConstructor) => { - container.registerAdd("db_entities", asValue(val)) - }) - } - } - - // TODO: "cradle" should only contain dependent Modules and the EntityManager if module scope is shared - container.register({ - [constainerName]: asFunction((cradle) => { - return new moduleService( - cradle, - resolution.options, - resolution.moduleDeclaration - ) - }).singleton(), - }) - - const moduleLoaders = loadedModule?.loaders || [] - try { - for (const loader of moduleLoaders) { - await loader( - { - container, - configModule, - logger, - options: resolution.options, - }, - resolution.moduleDeclaration - ) - } - } catch (err) { - return { - error: new Error( - `Loaders for module ${resolution.definition.label} failed: ${err.message}` - ), - } - } - - trackInstallation( - { - module: resolution.definition.key, - resolution: resolution.resolutionPath, - }, - "module" - ) + return await loadInternalModule(container, resolution, logger) } export const moduleLoader = async ({ container, - configModule, + moduleResolutions, logger, -}: LoaderOptions): Promise => { - const moduleResolutions = configModule?.moduleResolutions ?? {} +}: { + container: MedusaContainer + moduleResolutions: Record + logger: Logger +}): Promise => { + for (const resolution of Object.values(moduleResolutions ?? {})) { + const registrationResult = await loadModule(container, resolution, logger!) - for (const resolution of Object.values(moduleResolutions)) { - const registrationResult = await registerModule( - container, - resolution, - configModule, - logger! - ) if (registrationResult?.error) { const { error } = registrationResult if (resolution.definition.isRequired) { - logger?.warn( - `Could not resolve required module: ${resolution.definition.label}. Error: ${error.message}` + logger?.error( + `Could not resolve required module: ${resolution.definition.label}. Error: ${error.message}${EOL}` ) throw error } logger?.warn( - `Could not resolve module: ${resolution.definition.label}. Error: ${error.message}` + `Could not resolve module: ${resolution.definition.label}. Error: ${error.message}${EOL}` ) } } diff --git a/packages/modules-sdk/src/loaders/register-modules.ts b/packages/modules-sdk/src/loaders/register-modules.ts new file mode 100644 index 0000000000..64b05cef06 --- /dev/null +++ b/packages/modules-sdk/src/loaders/register-modules.ts @@ -0,0 +1,85 @@ +import resolveCwd from "resolve-cwd" + +import { + MedusaModuleConfig, + InternalModuleDeclaration, + ModuleDefinition, + ModuleResolution, + MODULE_SCOPE, +} from "../types" +import MODULE_DEFINITIONS from "../definitions" + +export const registerModules = ({ + modules, +}: MedusaModuleConfig): Record => { + const moduleResolutions = {} as Record + const projectModules = modules ?? {} + + for (const definition of MODULE_DEFINITIONS) { + const customConfig = projectModules[definition.key] + const isObj = typeof customConfig === "object" + + if (isObj && customConfig.scope === MODULE_SCOPE.EXTERNAL) { + // TODO: getExternalModuleResolution(...) + throw new Error("External Modules are not supported yet.") + } + + moduleResolutions[definition.key] = getInternalModuleResolution( + definition, + projectModules[definition.key] as + | InternalModuleDeclaration + | false + | string + ) + } + + return moduleResolutions +} + +function getInternalModuleResolution( + definition: ModuleDefinition, + moduleConfig: InternalModuleDeclaration | false | string +): ModuleResolution { + if (typeof moduleConfig === "boolean") { + if (!moduleConfig && definition.isRequired) { + throw new Error(`Module: ${definition.label} is required`) + } + if (!moduleConfig) { + return { + resolutionPath: false, + definition, + dependencies: [], + options: {}, + } + } + } + + const isObj = typeof moduleConfig === "object" + let resolutionPath = definition.defaultPackage + + // If user added a module and it's overridable, we resolve that instead + const isString = typeof moduleConfig === "string" + if (definition.canOverride && (isString || (isObj && moduleConfig.resolve))) { + resolutionPath = resolveCwd( + isString ? moduleConfig : (moduleConfig.resolve as string) + ) + } + + const moduleDeclaration = isObj ? moduleConfig : {} + const additionalDependencies = isObj ? moduleConfig.dependencies || [] : [] + + return { + resolutionPath, + definition, + dependencies: [ + ...new Set( + (definition.dependencies || []).concat(additionalDependencies) + ), + ], + moduleDeclaration: { + ...definition.defaultModuleDeclaration, + ...moduleDeclaration, + }, + options: isObj ? moduleConfig.options ?? {} : {}, + } +} diff --git a/packages/modules-sdk/src/loaders/utils/index.ts b/packages/modules-sdk/src/loaders/utils/index.ts new file mode 100644 index 0000000000..247a39f48f --- /dev/null +++ b/packages/modules-sdk/src/loaders/utils/index.ts @@ -0,0 +1 @@ +export * from "./load-internal" diff --git a/packages/modules-sdk/src/loaders/utils/load-internal.ts b/packages/modules-sdk/src/loaders/utils/load-internal.ts new file mode 100644 index 0000000000..1142c2e9d9 --- /dev/null +++ b/packages/modules-sdk/src/loaders/utils/load-internal.ts @@ -0,0 +1,114 @@ +import { asFunction, asValue } from "awilix" +import { createMedusaContainer } from "medusa-core-utils" +import { trackInstallation } from "medusa-telemetry" +import { + Constructor, + InternalModuleDeclaration, + Logger, + MedusaContainer, + ModuleExports, + ModuleResolution, + MODULE_RESOURCE_TYPE, + MODULE_SCOPE, +} from "../../types" + +export async function loadInternalModule( + container: MedusaContainer, + resolution: ModuleResolution, + logger: Logger +): Promise<{ error?: Error } | void> { + const registrationName = resolution.definition.registrationName + + const { scope, resources } = + resolution.moduleDeclaration as InternalModuleDeclaration + + let loadedModule: ModuleExports + try { + loadedModule = (await import(resolution.resolutionPath as string)).default + } catch (error) { + return { error } + } + + if (!loadedModule?.service) { + container.register({ + [registrationName]: asValue(undefined), + }) + + return { + error: new Error( + "No service found in module. Make sure your module exports at least one service." + ), + } + } + + if ( + scope === MODULE_SCOPE.INTERNAL && + resources === MODULE_RESOURCE_TYPE.SHARED + ) { + const moduleModels = loadedModule?.models || null + if (moduleModels) { + moduleModels.map((val: Constructor) => { + container.registerAdd("db_entities", asValue(val)) + }) + } + } + + const localContainer = + resources === MODULE_RESOURCE_TYPE.ISOLATED + ? createMedusaContainer() + : (container.createScope() as MedusaContainer) + + if (resources === MODULE_RESOURCE_TYPE.ISOLATED) { + const moduleDependencies = resolution?.dependencies ?? [] + + for (const dependency of moduleDependencies) { + localContainer.register( + dependency, + asFunction(() => container.resolve(dependency)) + ) + } + } + + const moduleLoaders = loadedModule?.loaders ?? [] + try { + for (const loader of moduleLoaders) { + await loader( + { + container: localContainer, + logger, + options: resolution.options, + }, + resolution.moduleDeclaration as InternalModuleDeclaration + ) + } + } catch (err) { + container.register({ + [registrationName]: asValue(undefined), + }) + + return { + error: new Error( + `Loaders for module ${resolution.definition.label} failed: ${err.message}` + ), + } + } + + const moduleService = loadedModule.service + container.register({ + [registrationName]: asFunction((cradle) => { + return new moduleService( + localContainer.cradle, + resolution.options, + resolution.moduleDeclaration + ) + }).singleton(), + }) + + trackInstallation( + { + module: resolution.definition.key, + resolution: resolution.resolutionPath, + }, + "module" + ) +} diff --git a/packages/modules-sdk/src/module-helper.ts b/packages/modules-sdk/src/module-helper.ts index e76c1e0e84..60f0fa423d 100644 --- a/packages/modules-sdk/src/module-helper.ts +++ b/packages/modules-sdk/src/module-helper.ts @@ -1,4 +1,4 @@ -import { ModuleResolution, ModulesResponse } from "./types/module" +import { ModuleResolution, ModulesResponse } from "./types" export class ModulesHelper { private modules_: Record = {} diff --git a/packages/modules-sdk/src/types/index.ts b/packages/modules-sdk/src/types/index.ts index a7f4654100..bbef887c38 100644 --- a/packages/modules-sdk/src/types/index.ts +++ b/packages/modules-sdk/src/types/index.ts @@ -1 +1,101 @@ -export * from "./module" +import { MedusaContainer as coreMedusaContainer } from "medusa-core-utils" +import { Logger as _Logger } from "winston" + +export type MedusaContainer = coreMedusaContainer +export type Constructor = new (...args: any[]) => T + +export type LogLevel = + | "query" + | "schema" + | "error" + | "warn" + | "info" + | "log" + | "migration" +export type LoggerOptions = boolean | "all" | LogLevel[] + +export type Logger = _Logger & { + progress: (activityId: string, msg: string) => void + info: (msg: string) => void + warn: (msg: string) => void +} + +export enum MODULE_SCOPE { + INTERNAL = "internal", + EXTERNAL = "external", +} + +export enum MODULE_RESOURCE_TYPE { + SHARED = "shared", + ISOLATED = "isolated", +} + +export type InternalModuleDeclaration = { + scope: MODULE_SCOPE.INTERNAL + resources: MODULE_RESOURCE_TYPE + dependencies?: string[] + resolve?: string + options?: Record +} + +export type ExternalModuleDeclaration = { + scope: MODULE_SCOPE.EXTERNAL + server: { + type: "http" + url: string + keepAlive: boolean + } +} + +export type ModuleResolution = { + resolutionPath: string | false + definition: ModuleDefinition + options?: Record + dependencies?: string[] + moduleDeclaration?: InternalModuleDeclaration | ExternalModuleDeclaration +} + +export type ModuleDefinition = { + key: string + registrationName: string + defaultPackage: string | false + label: string + canOverride?: boolean + isRequired?: boolean + dependencies?: string[] + defaultModuleDeclaration: + | InternalModuleDeclaration + | ExternalModuleDeclaration +} + +export type LoaderOptions = { + container: MedusaContainer + options?: Record + logger?: Logger +} + +export type ModuleLoaderFunction = ( + options: LoaderOptions, + moduleDeclaration?: InternalModuleDeclaration +) => Promise + +export type ModulesResponse = { + module: string + resolution: string | false +}[] + +export type ModuleExports = { + service: Constructor + loaders?: ModuleLoaderFunction[] + migrations?: any[] + models?: Constructor[] +} + +export type MedusaModuleConfig = { + modules?: Record< + string, + | false + | string + | Partial + > +} diff --git a/packages/modules-sdk/src/types/module.ts b/packages/modules-sdk/src/types/module.ts deleted file mode 100644 index b29868d93a..0000000000 --- a/packages/modules-sdk/src/types/module.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { AwilixContainer } from "awilix" -import { Logger as _Logger } from "winston" - -export type LogLevel = - | "query" - | "schema" - | "error" - | "warn" - | "info" - | "log" - | "migration" -export type LoggerOptions = boolean | "all" | LogLevel[] - -export type ClassConstructor = { - new (...args: unknown[]): T -} - -export type MedusaContainer = AwilixContainer & { - registerAdd: (name: string, registration: T) => MedusaContainer -} - -export type Logger = _Logger & { - progress: (activityId: string, msg: string) => void - info: (msg: string) => void - warn: (msg: string) => void -} - -export enum MODULE_SCOPE { - INTERNAL = "internal", - EXTERNAL = "external", -} - -export enum MODULE_RESOURCE_TYPE { - SHARED = "shared", - ISOLATED = "isolated", -} - -export type ConfigurableModuleDeclaration = { - scope: MODULE_SCOPE.INTERNAL - resources: MODULE_RESOURCE_TYPE - resolve?: string - options?: Record -} -/* -| { - scope: MODULE_SCOPE.external - server: { - type: "built-in" | "rest" | "tsrpc" | "grpc" | "gql" - url: string - options?: Record - } - } -*/ - -export type ModuleResolution = { - resolutionPath: string | false - definition: ModuleDefinition - options?: Record - moduleDeclaration?: ConfigurableModuleDeclaration -} - -export type ModuleDefinition = { - key: string - registrationName: string - defaultPackage: string | false - label: string - canOverride?: boolean - isRequired?: boolean - defaultModuleDeclaration: ConfigurableModuleDeclaration -} - -export type LoaderOptions = { - container: MedusaContainer - configModule: ConfigModule - options?: Record - logger?: Logger -} - -export type Constructor = new (...args: any[]) => T - -export type ModuleExports = { - loaders: (( - options: LoaderOptions, - moduleDeclaration?: ConfigurableModuleDeclaration - ) => Promise)[] - service: Constructor - migrations?: any[] - models?: Constructor[] -} - -export type ConfigModule = { - options?: Record - modules?: Record< - string, - false | string | Partial - > - moduleResolutions?: Record -} - -export type ModulesResponse = { - module: string - resolution: string | false -}[] diff --git a/packages/modules-sdk/tsconfig.json b/packages/modules-sdk/tsconfig.json index 0fc6130e78..2c99c7503d 100644 --- a/packages/modules-sdk/tsconfig.json +++ b/packages/modules-sdk/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { - "lib": [ - "es5", - "es6", - "es2019" - ], - "target": "es5", + "lib": ["es2020"], + "target": "es2020", "outDir": "./dist", "esModuleInterop": true, "declaration": true, @@ -19,14 +15,13 @@ "strictFunctionTypes": true, "noImplicitThis": true, "allowJs": true, - "skipLibCheck": true, - "downlevelIteration": true // to use ES5 specific tooling + "skipLibCheck": true }, - "include": ["./src/**/*", "index.d.ts"], + "include": ["src"], "exclude": [ - "./dist/**/*", + "dist", "./src/**/__tests__", "./src/**/__mocks__", "node_modules" ] -} \ No newline at end of file +} diff --git a/packages/stock-location/package.json b/packages/stock-location/package.json index 499dca62d2..06cc4917fa 100644 --- a/packages/stock-location/package.json +++ b/packages/stock-location/package.json @@ -18,14 +18,17 @@ "license": "MIT", "devDependencies": { "@medusajs/medusa": "^1.7.7", + "@medusajs/types": "*", "cross-env": "^5.2.1", "jest": "^25.5.4", "ts-jest": "^25.5.1", - "typeorm": "^0.3.11", "typescript": "^4.4.4" }, "dependencies": { - "@medusajs/modules-sdk": "*" + "@medusajs/modules-sdk": "*", + "@medusajs/utils": "^0.0.1", + "awilix": "^8.0.0", + "typeorm": "^0.3.11" }, "scripts": { "watch": "tsc --build --watch", @@ -36,6 +39,6 @@ }, "peerDependencies": { "@medusajs/medusa": "^1.7.7", - "typeorm": "^0.3.11" + "@medusajs/types": "^0.0.1" } } diff --git a/packages/stock-location/src/config.ts b/packages/stock-location/src/config.ts deleted file mode 100644 index a404b0fff6..0000000000 --- a/packages/stock-location/src/config.ts +++ /dev/null @@ -1 +0,0 @@ -export const CONNECTION_NAME = "stock_location_connection" diff --git a/packages/stock-location/src/index.ts b/packages/stock-location/src/index.ts index f804b8af91..42740ff607 100644 --- a/packages/stock-location/src/index.ts +++ b/packages/stock-location/src/index.ts @@ -1,13 +1,13 @@ -import ConnectionLoader from "./loaders/connection" -import StockLocationService from "./services/stock-location" -import * as SchemaMigration from "./migrations/schema-migrations/1665749860179-setup" +import loadConnection from "./loaders/connection" + +import migrations from "./migrations" import * as StockLocationModels from "./models" +import StockLocationService from "./services/stock-location" + import { ModuleExports } from "@medusajs/modules-sdk" const service = StockLocationService -const migrations = [SchemaMigration] -const loaders = [ConnectionLoader] - +const loaders = [loadConnection] const models = Object.values(StockLocationModels) const moduleDefinition: ModuleExports = { diff --git a/packages/stock-location/src/loaders/connection.ts b/packages/stock-location/src/loaders/connection.ts index 09d777bae8..1c248b5178 100644 --- a/packages/stock-location/src/loaders/connection.ts +++ b/packages/stock-location/src/loaders/connection.ts @@ -1,9 +1,49 @@ import { - ConfigurableModuleDeclaration, + InternalModuleDeclaration, LoaderOptions, + MODULE_RESOURCE_TYPE, + MODULE_SCOPE, } from "@medusajs/modules-sdk" +import { DataSource, DataSourceOptions } from "typeorm" + +import * as StockLocationModels from "../models" +import { MedusaError } from "medusa-core-utils" +import { asValue } from "awilix" export default async ( - { configModule }: LoaderOptions, - moduleDeclaration?: ConfigurableModuleDeclaration -): Promise => {} + { options, container }: LoaderOptions, + moduleDeclaration?: InternalModuleDeclaration +): Promise => { + if ( + moduleDeclaration?.scope === MODULE_SCOPE.INTERNAL && + moduleDeclaration.resources === MODULE_RESOURCE_TYPE.SHARED + ) { + return + } + + const dbData = options?.database as Record + + if (!dbData) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + `Database config is not present at module config "options.database"` + ) + } + + const entities = Object.values(StockLocationModels) + const dataSource = new DataSource({ + type: dbData.database_type, + url: dbData.database_url, + database: dbData.database_database, + extra: dbData.database_extra || {}, + schema: dbData.database_schema, + entities, + logging: dbData.database_logging, + } as DataSourceOptions) + + await dataSource.initialize() + + container.register({ + manager: asValue(dataSource.manager), + }) +} diff --git a/packages/stock-location/src/migrations/index.ts b/packages/stock-location/src/migrations/index.ts new file mode 100644 index 0000000000..1bf18cac9a --- /dev/null +++ b/packages/stock-location/src/migrations/index.ts @@ -0,0 +1,3 @@ +import * as setup from "./schema-migrations/1665749860179-setup" + +export default [setup] diff --git a/packages/stock-location/src/migrations/schema-migrations/1665749860179-setup.ts b/packages/stock-location/src/migrations/schema-migrations/1665749860179-setup.ts index 2712bae839..47f422e407 100644 --- a/packages/stock-location/src/migrations/schema-migrations/1665749860179-setup.ts +++ b/packages/stock-location/src/migrations/schema-migrations/1665749860179-setup.ts @@ -1,46 +1,4 @@ -import { ConfigModule } from "@medusajs/medusa" -import { - createConnection, - ConnectionOptions, - MigrationInterface, - QueryRunner, -} from "typeorm" - -import { CONNECTION_NAME } from "../../config" - -export const up = async ({ configModule }: { configModule: ConfigModule }) => { - const connection = await createConnection({ - name: CONNECTION_NAME, - type: configModule.projectConfig.database_type, - url: configModule.projectConfig.database_url, - database: configModule.projectConfig.database_database, - schema: configModule.projectConfig.database_schema, - extra: configModule.projectConfig.database_extra || {}, - migrations: [setup1665749860179], - logging: true, - } as ConnectionOptions) - - await connection.runMigrations() -} - -export const down = async ({ - configModule, -}: { - configModule: ConfigModule -}) => { - const connection = await createConnection({ - name: CONNECTION_NAME, - type: configModule.projectConfig.database_type, - url: configModule.projectConfig.database_url, - database: configModule.projectConfig.database_database, - schema: configModule.projectConfig.database_schema, - extra: configModule.projectConfig.database_extra || {}, - migrations: [setup1665749860179], - logging: true, - } as ConnectionOptions) - - await connection.undoLastMigration({ transaction: "all" }) -} +import { MigrationInterface, QueryRunner } from "typeorm" export class setup1665749860179 implements MigrationInterface { name = "setup1665749860179" diff --git a/packages/stock-location/src/services/stock-location.ts b/packages/stock-location/src/services/stock-location.ts index 6ba222836e..7b208ecc31 100644 --- a/packages/stock-location/src/services/stock-location.ts +++ b/packages/stock-location/src/services/stock-location.ts @@ -6,18 +6,13 @@ import { IEventBusService, setMetadata, StockLocationAddressInput, - TransactionBaseService, UpdateStockLocationInput, } from "@medusajs/medusa" - -import { - ConfigurableModuleDeclaration, - MODULE_RESOURCE_TYPE, -} from "@medusajs/modules-sdk" - +import { InternalModuleDeclaration } from "@medusajs/modules-sdk" +import { SharedContext } from "@medusajs/types" +import { InjectEntityManager, MedusaContext } from "@medusajs/utils" import { isDefined, MedusaError } from "medusa-core-utils" import { EntityManager } from "typeorm" - import { StockLocation, StockLocationAddress } from "../models" type InjectedDependencies = { @@ -29,30 +24,22 @@ type InjectedDependencies = { * Service for managing stock locations. */ -export default class StockLocationService extends TransactionBaseService { +export default class StockLocationService { static Events = { CREATED: "stock-location.created", UPDATED: "stock-location.updated", DELETED: "stock-location.deleted", } + protected readonly manager_: EntityManager protected readonly eventBusService_: IEventBusService constructor( - { eventBusService }: InjectedDependencies, + { eventBusService, manager }: InjectedDependencies, options?: unknown, - moduleDeclaration?: ConfigurableModuleDeclaration + moduleDeclaration?: InternalModuleDeclaration ) { - // @ts-ignore - super(...arguments) - - if (moduleDeclaration?.resources !== MODULE_RESOURCE_TYPE.SHARED) { - throw new MedusaError( - MedusaError.Types.INVALID_ARGUMENT, - "At the moment this module can only be used with shared resources" - ) - } - + this.manager_ = manager this.eventBusService_ = eventBusService } @@ -62,11 +49,13 @@ export default class StockLocationService extends TransactionBaseService { * @param config - Additional configuration for the query. * @return A list of stock locations. */ + @InjectEntityManager() async list( selector: FilterableStockLocationProps = {}, - config: FindConfig = { relations: [], skip: 0, take: 10 } + config: FindConfig = { relations: [], skip: 0, take: 10 }, + @MedusaContext() context: SharedContext = {} ): Promise { - const manager = this.activeManager_ + const manager = context.transactionManager! const locationRepo = manager.getRepository(StockLocation) const query = buildQuery(selector, config) @@ -79,11 +68,13 @@ export default class StockLocationService extends TransactionBaseService { * @param config - Additional configuration for the query. * @return A list of stock locations and the count of matching stock locations. */ + @InjectEntityManager() async listAndCount( selector: FilterableStockLocationProps = {}, - config: FindConfig = { relations: [], skip: 0, take: 10 } + config: FindConfig = { relations: [], skip: 0, take: 10 }, + @MedusaContext() context: SharedContext = {} ): Promise<[StockLocation[], number]> { - const manager = this.activeManager_ + const manager = context.transactionManager! const locationRepo = manager.getRepository(StockLocation) const query = buildQuery(selector, config) @@ -97,9 +88,11 @@ export default class StockLocationService extends TransactionBaseService { * @return The stock location. * @throws If the stock location ID is not definedor the stock location with the given ID was not found. */ + @InjectEntityManager() async retrieve( stockLocationId: string, - config: FindConfig = {} + config: FindConfig = {}, + @MedusaContext() context: SharedContext = {} ): Promise { if (!isDefined(stockLocationId)) { throw new MedusaError( @@ -108,7 +101,7 @@ export default class StockLocationService extends TransactionBaseService { ) } - const manager = this.activeManager_ + const manager = context.transactionManager! const locationRepo = manager.getRepository(StockLocation) const query = buildQuery({ id: stockLocationId }, config) @@ -129,41 +122,44 @@ export default class StockLocationService extends TransactionBaseService { * @param data - The input data for creating a Stock Location. * @returns The created stock location. */ - async create(data: CreateStockLocationInput): Promise { - return await this.atomicPhase_(async (manager) => { - const locationRepo = manager.getRepository(StockLocation) + @InjectEntityManager() + async create( + data: CreateStockLocationInput, + @MedusaContext() context: SharedContext = {} + ): Promise { + const manager = context.transactionManager! - const loc = locationRepo.create({ - name: data.name, + const locationRepo = manager.getRepository(StockLocation) + + const loc = locationRepo.create({ + name: data.name, + }) + + if (isDefined(data.address) || isDefined(data.address_id)) { + if (typeof data.address === "string" || data.address_id) { + const addrId = (data.address ?? data.address_id) as string + loc.address_id = addrId + } else { + const locAddressRepo = manager.getRepository(StockLocationAddress) + const locAddress = locAddressRepo.create(data.address!) + const addressResult = await locAddressRepo.save(locAddress) + loc.address_id = addressResult.id + } + } + + const { metadata } = data + if (metadata) { + loc.metadata = setMetadata(loc, metadata) + } + const result = await locationRepo.save(loc) + + await this.eventBusService_ + .withTransaction(manager) + .emit(StockLocationService.Events.CREATED, { + id: result.id, }) - if (isDefined(data.address) || isDefined(data.address_id)) { - if (typeof data.address === "string" || data.address_id) { - const addrId = (data.address ?? data.address_id) as string - loc.address_id = addrId - } else { - const locAddressRepo = manager.getRepository(StockLocationAddress) - const locAddress = locAddressRepo.create(data.address!) - const addressResult = await locAddressRepo.save(locAddress) - loc.address_id = addressResult.id - } - } - - const { metadata } = data - if (metadata) { - loc.metadata = setMetadata(loc, metadata) - } - - const result = await locationRepo.save(loc) - - await this.eventBusService_ - .withTransaction(manager) - .emit(StockLocationService.Events.CREATED, { - id: result.id, - }) - - return result - }) + return result } /** @@ -172,46 +168,46 @@ export default class StockLocationService extends TransactionBaseService { * @param updateData - The update data for the stock location. * @returns The updated stock location. */ - + @InjectEntityManager() async update( stockLocationId: string, - updateData: UpdateStockLocationInput + updateData: UpdateStockLocationInput, + @MedusaContext() context: SharedContext = {} ): Promise { - return await this.atomicPhase_(async (manager) => { - const locationRepo = manager.getRepository(StockLocation) + const manager = context.transactionManager! + const locationRepo = manager.getRepository(StockLocation) - const item = await this.retrieve(stockLocationId) + const item = await this.retrieve(stockLocationId, undefined, context) - const { address, ...data } = updateData + const { address, ...data } = updateData - if (address) { - if (item.address_id) { - await this.updateAddress(item.address_id, address) - } else { - const locAddressRepo = manager.getRepository(StockLocationAddress) - const locAddress = locAddressRepo.create(address) - const addressResult = await locAddressRepo.save(locAddress) - data.address_id = addressResult.id - } + if (address) { + if (item.address_id) { + await this.updateAddress(item.address_id, address, context) + } else { + const locAddressRepo = manager.getRepository(StockLocationAddress) + const locAddress = locAddressRepo.create(address) + const addressResult = await locAddressRepo.save(locAddress) + data.address_id = addressResult.id } + } - const { metadata, ...fields } = data + const { metadata, ...fields } = data - const toSave = locationRepo.merge(item, fields) - if (metadata) { - toSave.metadata = setMetadata(toSave, metadata) - } + const toSave = locationRepo.merge(item, fields) + if (metadata) { + toSave.metadata = setMetadata(toSave, metadata) + } - await locationRepo.save(toSave) + await locationRepo.save(toSave) - await this.eventBusService_ - .withTransaction(manager) - .emit(StockLocationService.Events.UPDATED, { - id: stockLocationId, - }) + await this.eventBusService_ + .withTransaction(manager) + .emit(StockLocationService.Events.UPDATED, { + id: stockLocationId, + }) - return item - }) + return item } /** @@ -220,10 +216,11 @@ export default class StockLocationService extends TransactionBaseService { * @param address - The update data for the address. * @returns The updated stock location address. */ - + @InjectEntityManager() protected async updateAddress( addressId: string, - address: StockLocationAddressInput + address: StockLocationAddressInput, + @MedusaContext() context: SharedContext = {} ): Promise { if (!isDefined(addressId)) { throw new MedusaError( @@ -232,28 +229,27 @@ export default class StockLocationService extends TransactionBaseService { ) } - return await this.atomicPhase_(async (manager) => { - const locationAddressRepo = manager.getRepository(StockLocationAddress) + const manager = context.transactionManager! + const locationAddressRepo = manager.getRepository(StockLocationAddress) - const existingAddress = await locationAddressRepo.findOne({ - where: { id: addressId }, - }) - if (!existingAddress) { - throw new MedusaError( - MedusaError.Types.NOT_FOUND, - `StockLocation address with id ${addressId} was not found` - ) - } - - const { metadata, ...fields } = address - - const toSave = locationAddressRepo.merge(existingAddress, fields) - if (metadata) { - toSave.metadata = setMetadata(toSave, metadata) - } - - return await locationAddressRepo.save(toSave) + const existingAddress = await locationAddressRepo.findOne({ + where: { id: addressId }, }) + if (!existingAddress) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `StockLocation address with id ${addressId} was not found` + ) + } + + const { metadata, ...fields } = address + + const toSave = locationAddressRepo.merge(existingAddress, fields) + if (metadata) { + toSave.metadata = setMetadata(toSave, metadata) + } + + return await locationAddressRepo.save(toSave) } /** @@ -261,17 +257,20 @@ export default class StockLocationService extends TransactionBaseService { * @param id - The ID of the stock location to delete. * @returns An empty promise. */ - async delete(id: string): Promise { - return await this.atomicPhase_(async (manager) => { - const locationRepo = manager.getRepository(StockLocation) + @InjectEntityManager() + async delete( + id: string, + @MedusaContext() context: SharedContext = {} + ): Promise { + const manager = context.transactionManager! + const locationRepo = manager.getRepository(StockLocation) - await locationRepo.softRemove({ id }) + await locationRepo.softRemove({ id }) - await this.eventBusService_ - .withTransaction(manager) - .emit(StockLocationService.Events.DELETED, { - id, - }) - }) + await this.eventBusService_ + .withTransaction(manager) + .emit(StockLocationService.Events.DELETED, { + id, + }) } } diff --git a/packages/stock-location/tsconfig.json b/packages/stock-location/tsconfig.json index 0fc6130e78..2c99c7503d 100644 --- a/packages/stock-location/tsconfig.json +++ b/packages/stock-location/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { - "lib": [ - "es5", - "es6", - "es2019" - ], - "target": "es5", + "lib": ["es2020"], + "target": "es2020", "outDir": "./dist", "esModuleInterop": true, "declaration": true, @@ -19,14 +15,13 @@ "strictFunctionTypes": true, "noImplicitThis": true, "allowJs": true, - "skipLibCheck": true, - "downlevelIteration": true // to use ES5 specific tooling + "skipLibCheck": true }, - "include": ["./src/**/*", "index.d.ts"], + "include": ["src"], "exclude": [ - "./dist/**/*", + "dist", "./src/**/__tests__", "./src/**/__mocks__", "node_modules" ] -} \ No newline at end of file +} diff --git a/packages/types/package.json b/packages/types/package.json new file mode 100644 index 0000000000..14e48670c3 --- /dev/null +++ b/packages/types/package.json @@ -0,0 +1,29 @@ +{ + "name": "@medusajs/types", + "version": "0.0.1", + "description": "Medusa Types definition", + "main": "dist/index.js", + "repository": { + "type": "git", + "url": "https://github.com/medusajs/medusa", + "directory": "packages/types" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "author": "Medusa", + "license": "MIT", + "devDependencies": { + "cross-env": "^5.2.1", + "typeorm": "^0.3.11", + "typescript": "^4.4.4" + }, + "scripts": { + "prepare": "cross-env NODE_ENV=production yarn run build", + "build": "tsc --build", + "test": "exit 0" + } +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts new file mode 100644 index 0000000000..c901199825 --- /dev/null +++ b/packages/types/src/index.ts @@ -0,0 +1 @@ +export * from "./shared-context" diff --git a/packages/types/src/shared-context.ts b/packages/types/src/shared-context.ts new file mode 100644 index 0000000000..64d8806983 --- /dev/null +++ b/packages/types/src/shared-context.ts @@ -0,0 +1,5 @@ +import { EntityManager } from "typeorm" + +export type SharedContext = { + transactionManager?: EntityManager +} diff --git a/packages/types/tsconfig.json b/packages/types/tsconfig.json new file mode 100644 index 0000000000..2c99c7503d --- /dev/null +++ b/packages/types/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "lib": ["es2020"], + "target": "es2020", + "outDir": "./dist", + "esModuleInterop": true, + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true, + "noImplicitReturns": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "allowJs": true, + "skipLibCheck": true + }, + "include": ["src"], + "exclude": [ + "dist", + "./src/**/__tests__", + "./src/**/__mocks__", + "node_modules" + ] +} diff --git a/packages/utils/jest.config.js b/packages/utils/jest.config.js new file mode 100644 index 0000000000..7de5bf104a --- /dev/null +++ b/packages/utils/jest.config.js @@ -0,0 +1,13 @@ +module.exports = { + globals: { + "ts-jest": { + tsConfig: "tsconfig.json", + isolatedModules: false, + }, + }, + transform: { + "^.+\\.[jt]s?$": "ts-jest", + }, + testEnvironment: `node`, + moduleFileExtensions: [`js`, `ts`], +} diff --git a/packages/utils/package.json b/packages/utils/package.json new file mode 100644 index 0000000000..2542314b5d --- /dev/null +++ b/packages/utils/package.json @@ -0,0 +1,35 @@ +{ + "name": "@medusajs/utils", + "version": "0.0.1", + "description": "Medusa utilities functions shared by Medusa core and Modules", + "main": "dist/index.js", + "repository": { + "type": "git", + "url": "https://github.com/medusajs/medusa", + "directory": "packages/utils" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "author": "Medusa", + "license": "MIT", + "devDependencies": { + "@medusajs/types": "*", + "cross-env": "^5.2.1", + "jest": "^25.5.4", + "ts-jest": "^25.5.1", + "typeorm": "^0.3.11", + "typescript": "^4.4.4" + }, + "peerDependencies": { + "@medusajs/types": "^0.0.1" + }, + "scripts": { + "prepare": "cross-env NODE_ENV=production yarn run build", + "build": "tsc --build", + "test": "jest --passWithNoTests src" + } +} diff --git a/packages/utils/src/decorators/context-parameter.ts b/packages/utils/src/decorators/context-parameter.ts new file mode 100644 index 0000000000..c959ede0e5 --- /dev/null +++ b/packages/utils/src/decorators/context-parameter.ts @@ -0,0 +1,19 @@ +export function MedusaContext() { + return function ( + target: any, + propertyKey: string | symbol, + parameterIndex: number + ) { + if (!target.MedusaContext_) { + target.MedusaContext_ = {} + } + + if (propertyKey in target.MedusaContext_) { + throw new Error( + `Only one MedusaContext is allowed on method "${String(propertyKey)}".` + ) + } + + target.MedusaContext_[propertyKey] = parameterIndex + } +} diff --git a/packages/utils/src/decorators/index.ts b/packages/utils/src/decorators/index.ts new file mode 100644 index 0000000000..4ffe523014 --- /dev/null +++ b/packages/utils/src/decorators/index.ts @@ -0,0 +1,2 @@ +export * from "./context-parameter" +export * from "./inject-entity-manager" diff --git a/packages/utils/src/decorators/inject-entity-manager.ts b/packages/utils/src/decorators/inject-entity-manager.ts new file mode 100644 index 0000000000..30b5c2752f --- /dev/null +++ b/packages/utils/src/decorators/inject-entity-manager.ts @@ -0,0 +1,37 @@ +import { SharedContext } from "@medusajs/types" + +export function InjectEntityManager( + managerProperty = "manager_" +): MethodDecorator { + return function ( + target: any, + propertyKey: string | symbol, + descriptor: any + ): void { + if (!target.MedusaContext_) { + throw new Error( + `To apply @InjectEntityManager you have to flag a parameter using @MedusaContext` + ) + } + + const originalMethod = descriptor.value + + const argIndex = target.MedusaContext_[propertyKey] + descriptor.value = async function (...args: any[]) { + const context: SharedContext = args[argIndex] ?? {} + + if (context?.transactionManager) { + return await originalMethod.apply(this, args) + } + + return await this[managerProperty].transaction( + async (transactionManager) => { + args[argIndex] = args[argIndex] ?? {} + args[argIndex].transactionManager = transactionManager + + return await originalMethod.apply(this, args) + } + ) + } + } +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts new file mode 100644 index 0000000000..e117d66ea0 --- /dev/null +++ b/packages/utils/src/index.ts @@ -0,0 +1 @@ +export * from "./decorators" diff --git a/packages/utils/tsconfig.json b/packages/utils/tsconfig.json new file mode 100644 index 0000000000..2c99c7503d --- /dev/null +++ b/packages/utils/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "lib": ["es2020"], + "target": "es2020", + "outDir": "./dist", + "esModuleInterop": true, + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true, + "noImplicitReturns": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "allowJs": true, + "skipLibCheck": true + }, + "include": ["src"], + "exclude": [ + "dist", + "./src/**/__tests__", + "./src/**/__mocks__", + "node_modules" + ] +} diff --git a/yarn.lock b/yarn.lock index 94e83a6c5b..cf607318eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5742,6 +5742,7 @@ __metadata: dependencies: "@medusajs/medusa": "*" "@medusajs/modules-sdk": "*" + awilix: ^8.0.0 cross-env: ^5.2.1 jest: ^25.5.4 ts-jest: ^25.5.1 @@ -5749,7 +5750,6 @@ __metadata: typescript: ^4.4.4 peerDependencies: "@medusajs/medusa": 1.7.13 - typeorm: ^0.3.11 languageName: unknown linkType: soft @@ -5919,7 +5919,9 @@ __metadata: dependencies: awilix: ^8.0.0 cross-env: ^5.2.1 + glob: 7.1.6 jest: ^25.5.4 + medusa-core-utils: ^1.1.39 medusa-telemetry: ^0.0.16 resolve-cwd: ^3.0.0 ts-jest: ^25.5.1 @@ -5982,6 +5984,9 @@ __metadata: dependencies: "@medusajs/medusa": ^1.7.7 "@medusajs/modules-sdk": "*" + "@medusajs/types": "*" + "@medusajs/utils": ^0.0.1 + awilix: ^8.0.0 cross-env: ^5.2.1 jest: ^25.5.4 ts-jest: ^25.5.1 @@ -5989,7 +5994,32 @@ __metadata: typescript: ^4.4.4 peerDependencies: "@medusajs/medusa": ^1.7.7 + "@medusajs/types": ^0.0.1 + languageName: unknown + linkType: soft + +"@medusajs/types@*, @medusajs/types@workspace:packages/types": + version: 0.0.0-use.local + resolution: "@medusajs/types@workspace:packages/types" + dependencies: + cross-env: ^5.2.1 typeorm: ^0.3.11 + typescript: ^4.4.4 + languageName: unknown + linkType: soft + +"@medusajs/utils@^0.0.1, @medusajs/utils@workspace:packages/utils": + version: 0.0.0-use.local + resolution: "@medusajs/utils@workspace:packages/utils" + dependencies: + "@medusajs/types": "*" + cross-env: ^5.2.1 + jest: ^25.5.4 + ts-jest: ^25.5.1 + typeorm: ^0.3.11 + typescript: ^4.4.4 + peerDependencies: + "@medusajs/types": ^0.0.1 languageName: unknown linkType: soft @@ -26283,14 +26313,7 @@ __metadata: languageName: node linkType: hard -"joi-objectid@npm:^3.0.1": - version: 3.0.1 - resolution: "joi-objectid@npm:3.0.1" - checksum: 4820534ddff8779a7128661b8ed21bc7d552d53a8def60095db6e38f410c5893da21d7b91211d3af5dd5f6e4eda90a2f11b24a2e69efcef74c1a8249240ad83d - languageName: node - linkType: hard - -"joi@npm:^17.3.0, joi@npm:^17.4.2": +"joi@npm:^17.4.2": version: 17.6.0 resolution: "joi@npm:17.6.0" dependencies: @@ -28185,12 +28208,8 @@ __metadata: version: 0.0.0-use.local resolution: "medusa-core-utils@workspace:packages/medusa-core-utils" dependencies: - class-transformer: ^0.5.1 - class-validator: ^0.13.2 cross-env: ^5.2.1 jest: ^25.5.4 - joi: ^17.3.0 - joi-objectid: ^3.0.1 ts-jest: ^25.5.1 typescript: ^4.4.4 languageName: unknown