feat(medusa): Modules initializer (#3352)

This commit is contained in:
Carlos R. L. Rodrigues
2023-03-17 12:18:52 -03:00
committed by GitHub
parent 8a7421db5b
commit aa690beed7
51 changed files with 1290 additions and 715 deletions
+27
View File
@@ -0,0 +1,27 @@
---
"@medusajs/inventory": patch
"medusa-core-utils": patch
"@medusajs/medusa": patch
---
feat(medusa): Modules initializer
### Loading modules in a project
Example
``` typescript
import { InventoryServiceInitializeOptions, initialize } from "@medusajs/inventory"
const options: InventoryServiceInitializeOptions = {
database: {
type: "postgres",
url: DB_URL,
},
}
const inventoryService = await initialize(options)
const newInventoryItem = await inventoryService.createInventoryItem({
sku: "sku_123",
})
```
@@ -1,6 +1,6 @@
const path = require("path") const path = require("path")
const { createConnection } = require("typeorm") const { DataSource } = require("typeorm")
const { getConfigFile } = require("medusa-core-utils") const { getConfigFile } = require("medusa-core-utils")
const DB_HOST = process.env.DB_HOST const DB_HOST = process.env.DB_HOST
@@ -14,14 +14,16 @@ process.env.NODE_ENV = "development"
require("./dev-require") require("./dev-require")
async function createDB() { async function createDB() {
const connection = await createConnection({ const connection = new DataSource({
type: "postgres", type: "postgres",
url: `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}`, url: `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}`,
}) })
await connection.initialize()
await connection.query(`DROP DATABASE IF EXISTS "${DB_NAME}";`) await connection.query(`DROP DATABASE IF EXISTS "${DB_NAME}";`)
await connection.query(`CREATE DATABASE "${DB_NAME}";`) await connection.query(`CREATE DATABASE "${DB_NAME}";`)
await connection.close() await connection.destroy()
} }
module.exports = { module.exports = {
@@ -47,6 +49,7 @@ module.exports = {
const { const {
getEnabledMigrations, getEnabledMigrations,
getModuleSharedResources, getModuleSharedResources,
runIsolatedModulesMigration,
} = require("@medusajs/medusa/dist/commands/utils/get-migrations") } = require("@medusajs/medusa/dist/commands/utils/get-migrations")
const entities = modelsLoader({}, { register: false }) const entities = modelsLoader({}, { register: false })
@@ -72,7 +75,7 @@ module.exports = {
await createDB() await createDB()
const dbConnection = await createConnection({ const dbConnection = new DataSource({
type: "postgres", type: "postgres",
url: DB_URL, url: DB_URL,
entities: enabledEntities.concat(moduleModels), entities: enabledEntities.concat(moduleModels),
@@ -80,8 +83,12 @@ module.exports = {
// logging: true, // logging: true,
}) })
await dbConnection.initialize()
await dbConnection.runMigrations() await dbConnection.runMigrations()
await runIsolatedModulesMigration(configModule)
return dbConnection return dbConnection
}, },
} }
+4 -2
View File
@@ -17,14 +17,16 @@
"author": "Medusa", "author": "Medusa",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@medusajs/medusa": "*", "@medusajs/types": "*",
"cross-env": "^5.2.1", "cross-env": "^5.2.1",
"jest": "^25.5.4", "jest": "^25.5.4",
"ts-jest": "^25.5.1", "ts-jest": "^25.5.1",
"typescript": "^4.4.4" "typescript": "^4.4.4"
}, },
"dependencies": { "dependencies": {
"@medusajs/medusa": "^1.7.7",
"@medusajs/modules-sdk": "*", "@medusajs/modules-sdk": "*",
"@medusajs/utils": "^0.0.1",
"awilix": "^8.0.0", "awilix": "^8.0.0",
"typeorm": "^0.3.11" "typeorm": "^0.3.11"
}, },
@@ -36,6 +38,6 @@
"test:unit": "jest --passWithNoTests" "test:unit": "jest --passWithNoTests"
}, },
"peerDependencies": { "peerDependencies": {
"@medusajs/medusa": "1.7.13" "@medusajs/types": "^0.0.1"
} }
} }
+7
View File
@@ -2,6 +2,7 @@ import loadConnection from "./loaders/connection"
import loadContainer from "./loaders/container" import loadContainer from "./loaders/container"
import migrations from "./migrations" import migrations from "./migrations"
import { revertMigration, runMigrations } from "./migrations/run-migration"
import * as InventoryModels from "./models" import * as InventoryModels from "./models"
import InventoryService from "./services/inventory" import InventoryService from "./services/inventory"
@@ -16,6 +17,12 @@ const moduleDefinition: ModuleExports = {
migrations, migrations,
loaders, loaders,
models, models,
runMigrations,
revertMigration,
} }
export default moduleDefinition export default moduleDefinition
export * from "./initialize"
export * from "./types"
@@ -0,0 +1,24 @@
import { IEventBusService, IInventoryService } from "@medusajs/medusa"
import {
ExternalModuleDeclaration,
InternalModuleDeclaration,
MedusaModule,
} from "@medusajs/modules-sdk"
import { InventoryServiceInitializeOptions } from "../types"
export const initialize = async (
options?: InventoryServiceInitializeOptions | ExternalModuleDeclaration,
injectedDependencies?: {
eventBusService: IEventBusService
}
): Promise<IInventoryService> => {
const serviceKey = "inventoryService"
const loaded = await MedusaModule.bootstrap(
serviceKey,
"@medusajs/inventory",
options as InternalModuleDeclaration | ExternalModuleDeclaration,
injectedDependencies
)
return loaded[serviceKey] as IInventoryService
}
+11 -9
View File
@@ -6,9 +6,10 @@ import {
} from "@medusajs/modules-sdk" } from "@medusajs/modules-sdk"
import { DataSource, DataSourceOptions } from "typeorm" import { DataSource, DataSourceOptions } from "typeorm"
import * as InventoryModels from "../models"
import { MedusaError } from "medusa-core-utils"
import { asValue } from "awilix" import { asValue } from "awilix"
import { MedusaError } from "medusa-core-utils"
import * as InventoryModels from "../models"
import { InventoryServiceInitializeOptions } from "../types"
export default async ( export default async (
{ options, container }: LoaderOptions, { options, container }: LoaderOptions,
@@ -21,7 +22,8 @@ export default async (
return return
} }
const dbData = options?.database as Record<string, string> const dbData =
options?.database as InventoryServiceInitializeOptions["database"]
if (!dbData) { if (!dbData) {
throw new MedusaError( throw new MedusaError(
@@ -32,13 +34,13 @@ export default async (
const entities = Object.values(InventoryModels) const entities = Object.values(InventoryModels)
const dataSource = new DataSource({ const dataSource = new DataSource({
type: dbData.database_type, type: dbData.type,
url: dbData.database_url, url: dbData.url,
database: dbData.database_database, database: dbData.database,
extra: dbData.database_extra || {}, extra: dbData.extra || {},
schema: dbData.database_schema, schema: dbData.schema,
entities, entities,
logging: dbData.database_logging, logging: dbData.logging,
} as DataSourceOptions) } as DataSourceOptions)
await dataSource.initialize() await dataSource.initialize()
+1 -1
View File
@@ -3,7 +3,7 @@ import { InternalModuleDeclaration, LoaderOptions } from "@medusajs/modules-sdk"
import { import {
InventoryItemService, InventoryItemService,
InventoryLevelService, InventoryLevelService,
ReservationItemService, ReservationItemService
} from "../services" } from "../services"
import { asClass } from "awilix" import { asClass } from "awilix"
@@ -0,0 +1,63 @@
import { InternalModuleDeclaration, LoaderOptions } from "@medusajs/modules-sdk"
import { DataSource, DataSourceOptions } from "typeorm"
import { InventoryServiceInitializeOptions } from "../types"
import migrations from "./index"
function getDataSource(
dbData: InventoryServiceInitializeOptions["database"]
): DataSource {
return new DataSource({
type: dbData!.type,
url: dbData!.url,
database: dbData!.database,
extra: dbData!.extra || {},
migrations: migrations
.map((migration: any): Function[] => {
return Object.values(migration).filter(
(fn) => typeof fn === "function"
) as Function[]
})
.flat(),
schema: dbData!.schema,
logging: dbData!.logging,
} as DataSourceOptions)
}
export async function runMigrations(
{ options, logger }: Omit<LoaderOptions, "container">,
moduleDeclaration?: InternalModuleDeclaration
) {
const dbData =
options?.database as InventoryServiceInitializeOptions["database"]
try {
const dataSource = getDataSource(dbData)
await dataSource.initialize()
await dataSource.runMigrations()
logger?.info("Inventory module migration executed")
} catch (error) {
logger?.error(`Inventory module migration failed to run - Error: ${error}`)
}
}
export async function revertMigration(
{ options, logger }: Omit<LoaderOptions, "container">,
moduleDeclaration?: InternalModuleDeclaration
) {
const dbData =
options?.database as InventoryServiceInitializeOptions["database"]
try {
const dataSource = getDataSource(dbData)
await dataSource.initialize()
await dataSource.undoLastMigration()
logger?.info("Inventory module migration reverted")
} catch (error) {
logger?.error(
`Inventory module migration failed to revert - Error: ${error}`
)
}
}
@@ -1,4 +1,4 @@
import { Index, Unique, BeforeInsert, Column, Entity } from "typeorm" import { Index, BeforeInsert, Column, Entity } from "typeorm"
import { SoftDeletableEntity, generateEntityId } from "@medusajs/medusa" import { SoftDeletableEntity, generateEntityId } from "@medusajs/medusa"
@Entity() @Entity()
@@ -1,5 +1,3 @@
import { DeepPartial, EntityManager, FindManyOptions } from "typeorm"
import { isDefined, MedusaError } from "medusa-core-utils"
import { import {
buildQuery, buildQuery,
CreateInventoryItemInput, CreateInventoryItemInput,
@@ -7,9 +5,11 @@ import {
FindConfig, FindConfig,
IEventBusService, IEventBusService,
InventoryItemDTO, InventoryItemDTO,
TransactionBaseService,
} from "@medusajs/medusa" } from "@medusajs/medusa"
import { SharedContext } from "@medusajs/types"
import { InjectEntityManager, MedusaContext } from "@medusajs/utils"
import { isDefined, MedusaError } from "medusa-core-utils"
import { DeepPartial, EntityManager, FindManyOptions } from "typeorm"
import { InventoryItem } from "../models" import { InventoryItem } from "../models"
import { getListQuery } from "../utils/query" import { getListQuery } from "../utils/query"
@@ -18,18 +18,18 @@ type InjectedDependencies = {
manager: EntityManager manager: EntityManager
} }
export default class InventoryItemService extends TransactionBaseService { export default class InventoryItemService {
static Events = { static Events = {
CREATED: "inventory-item.created", CREATED: "inventory-item.created",
UPDATED: "inventory-item.updated", UPDATED: "inventory-item.updated",
DELETED: "inventory-item.deleted", DELETED: "inventory-item.deleted",
} }
protected readonly eventBusService_: IEventBusService protected readonly manager_: EntityManager
protected readonly eventBusService_: IEventBusService | undefined
constructor({ eventBusService }: InjectedDependencies) {
super(arguments[0])
constructor({ eventBusService, manager }: InjectedDependencies) {
this.manager_ = manager
this.eventBusService_ = eventBusService this.eventBusService_ = eventBusService
} }
@@ -38,11 +38,17 @@ export default class InventoryItemService extends TransactionBaseService {
* @param config - Configuration for query. * @param config - Configuration for query.
* @return Resolves to the list of inventory items that match the filter. * @return Resolves to the list of inventory items that match the filter.
*/ */
@InjectEntityManager()
async list( async list(
selector: FilterableInventoryItemProps = {}, selector: FilterableInventoryItemProps = {},
config: FindConfig<InventoryItem> = { relations: [], skip: 0, take: 10 } config: FindConfig<InventoryItem> = { relations: [], skip: 0, take: 10 },
@MedusaContext() context: SharedContext = {}
): Promise<InventoryItemDTO[]> { ): Promise<InventoryItemDTO[]> {
const queryBuilder = getListQuery(this.activeManager_, selector, config) const queryBuilder = getListQuery(
context.transactionManager!,
selector,
config
)
return await queryBuilder.getMany() return await queryBuilder.getMany()
} }
@@ -51,11 +57,17 @@ export default class InventoryItemService extends TransactionBaseService {
* @param config - Configuration for query. * @param config - Configuration for query.
* @return - Resolves to the list of inventory items that match the filter and the count of all matching items. * @return - Resolves to the list of inventory items that match the filter and the count of all matching items.
*/ */
@InjectEntityManager()
async listAndCount( async listAndCount(
selector: FilterableInventoryItemProps = {}, selector: FilterableInventoryItemProps = {},
config: FindConfig<InventoryItem> = { relations: [], skip: 0, take: 10 } config: FindConfig<InventoryItem> = { relations: [], skip: 0, take: 10 },
@MedusaContext() context: SharedContext = {}
): Promise<[InventoryItemDTO[], number]> { ): Promise<[InventoryItemDTO[], number]> {
const queryBuilder = getListQuery(this.activeManager_, selector, config) const queryBuilder = getListQuery(
context.transactionManager!,
selector,
config
)
return await queryBuilder.getManyAndCount() return await queryBuilder.getManyAndCount()
} }
@@ -66,9 +78,11 @@ export default class InventoryItemService extends TransactionBaseService {
* @return The retrieved inventory item. * @return The retrieved inventory item.
* @throws If the inventory item id is not defined or if the inventory item is not found. * @throws If the inventory item id is not defined or if the inventory item is not found.
*/ */
@InjectEntityManager()
async retrieve( async retrieve(
inventoryItemId: string, inventoryItemId: string,
config: FindConfig<InventoryItem> = {} config: FindConfig<InventoryItem> = {},
@MedusaContext() context: SharedContext = {}
): Promise<InventoryItem> { ): Promise<InventoryItem> {
if (!isDefined(inventoryItemId)) { if (!isDefined(inventoryItemId)) {
throw new MedusaError( throw new MedusaError(
@@ -77,7 +91,7 @@ export default class InventoryItemService extends TransactionBaseService {
) )
} }
const manager = this.activeManager_ const manager = context.transactionManager!
const itemRepository = manager.getRepository(InventoryItem) const itemRepository = manager.getRepository(InventoryItem)
const query = buildQuery({ id: inventoryItemId }, config) as FindManyOptions const query = buildQuery({ id: inventoryItemId }, config) as FindManyOptions
@@ -97,34 +111,35 @@ export default class InventoryItemService extends TransactionBaseService {
* @param input - Input for creating a new inventory item. * @param input - Input for creating a new inventory item.
* @return The newly created inventory item. * @return The newly created inventory item.
*/ */
async create(data: CreateInventoryItemInput): Promise<InventoryItem> { @InjectEntityManager()
return await this.atomicPhase_(async (manager) => { async create(
const itemRepository = manager.getRepository(InventoryItem) data: CreateInventoryItemInput,
@MedusaContext() context: SharedContext = {}
): Promise<InventoryItem> {
const manager = context.transactionManager!
const itemRepository = manager.getRepository(InventoryItem)
const inventoryItem = itemRepository.create({ const inventoryItem = itemRepository.create({
sku: data.sku, sku: data.sku,
origin_country: data.origin_country, origin_country: data.origin_country,
metadata: data.metadata, metadata: data.metadata,
hs_code: data.hs_code, hs_code: data.hs_code,
mid_code: data.mid_code, mid_code: data.mid_code,
material: data.material, material: data.material,
weight: data.weight, weight: data.weight,
length: data.length, length: data.length,
height: data.height, height: data.height,
width: data.width, width: data.width,
requires_shipping: data.requires_shipping, requires_shipping: data.requires_shipping,
})
const result = await itemRepository.save(inventoryItem)
await this.eventBusService_
.withTransaction(manager)
.emit(InventoryItemService.Events.CREATED, {
id: result.id,
})
return result
}) })
const result = await itemRepository.save(inventoryItem)
await this.eventBusService_?.emit?.(InventoryItemService.Events.CREATED, {
id: result.id,
})
return result
} }
/** /**
@@ -132,51 +147,51 @@ export default class InventoryItemService extends TransactionBaseService {
* @param update - The updates to apply to the inventory item. * @param update - The updates to apply to the inventory item.
* @return The updated inventory item. * @return The updated inventory item.
*/ */
@InjectEntityManager()
async update( async update(
inventoryItemId: string, inventoryItemId: string,
data: Omit< data: Omit<
DeepPartial<InventoryItem>, DeepPartial<InventoryItem>,
"id" | "created_at" | "metadata" | "deleted_at" "id" | "created_at" | "metadata" | "deleted_at"
> >,
@MedusaContext() context: SharedContext = {}
): Promise<InventoryItem> { ): Promise<InventoryItem> {
return await this.atomicPhase_(async (manager) => { const manager = context.transactionManager!
const itemRepository = manager.getRepository(InventoryItem) const itemRepository = manager.getRepository(InventoryItem)
const item = await this.retrieve(inventoryItemId) const item = await this.retrieve(inventoryItemId, undefined, context)
const shouldUpdate = Object.keys(data).some((key) => { const shouldUpdate = Object.keys(data).some((key) => {
return item[key] !== data[key] return item[key] !== data[key]
})
if (shouldUpdate) {
itemRepository.merge(item, data)
await itemRepository.save(item)
await this.eventBusService_
.withTransaction(manager)
.emit(InventoryItemService.Events.UPDATED, {
id: item.id,
})
}
return item
}) })
if (shouldUpdate) {
itemRepository.merge(item, data)
await itemRepository.save(item)
await this.eventBusService_?.emit?.(InventoryItemService.Events.UPDATED, {
id: item.id,
})
}
return item
} }
/** /**
* @param inventoryItemId - The id of the inventory item to delete. * @param inventoryItemId - The id of the inventory item to delete.
*/ */
async delete(inventoryItemId: string): Promise<void> { @InjectEntityManager()
await this.atomicPhase_(async (manager) => { async delete(
const itemRepository = manager.getRepository(InventoryItem) inventoryItemId: string,
@MedusaContext() context: SharedContext = {}
): Promise<void> {
const manager = context.transactionManager!
const itemRepository = manager.getRepository(InventoryItem)
await itemRepository.softRemove({ id: inventoryItemId }) await itemRepository.softRemove({ id: inventoryItemId })
await this.eventBusService_ await this.eventBusService_?.emit?.(InventoryItemService.Events.DELETED, {
.withTransaction(manager) id: inventoryItemId,
.emit(InventoryItemService.Events.DELETED, {
id: inventoryItemId,
})
}) })
} }
} }
+133 -104
View File
@@ -1,14 +1,14 @@
import { DeepPartial, EntityManager, FindManyOptions, In } from "typeorm"
import { isDefined, MedusaError } from "medusa-core-utils"
import { import {
buildQuery, buildQuery,
CreateInventoryLevelInput, CreateInventoryLevelInput,
FilterableInventoryLevelProps, FilterableInventoryLevelProps,
FindConfig, FindConfig,
IEventBusService, IEventBusService,
TransactionBaseService,
} from "@medusajs/medusa" } from "@medusajs/medusa"
import { SharedContext } from "@medusajs/types"
import { InjectEntityManager, MedusaContext } from "@medusajs/utils"
import { isDefined, MedusaError } from "medusa-core-utils"
import { DeepPartial, EntityManager, FindManyOptions, In } from "typeorm"
import { InventoryLevel } from "../models" import { InventoryLevel } from "../models"
type InjectedDependencies = { type InjectedDependencies = {
@@ -16,18 +16,18 @@ type InjectedDependencies = {
manager: EntityManager manager: EntityManager
} }
export default class InventoryLevelService extends TransactionBaseService { export default class InventoryLevelService {
static Events = { static Events = {
CREATED: "inventory-level.created", CREATED: "inventory-level.created",
UPDATED: "inventory-level.updated", UPDATED: "inventory-level.updated",
DELETED: "inventory-level.deleted", DELETED: "inventory-level.deleted",
} }
protected readonly eventBusService_: IEventBusService protected readonly manager_: EntityManager
protected readonly eventBusService_: IEventBusService | undefined
constructor({ eventBusService }: InjectedDependencies) {
super(arguments[0])
constructor({ eventBusService, manager }: InjectedDependencies) {
this.manager_ = manager
this.eventBusService_ = eventBusService this.eventBusService_ = eventBusService
} }
@@ -37,11 +37,13 @@ export default class InventoryLevelService extends TransactionBaseService {
* @param config - An object containing configuration options for the query. * @param config - An object containing configuration options for the query.
* @return Array of inventory levels. * @return Array of inventory levels.
*/ */
@InjectEntityManager()
async list( async list(
selector: FilterableInventoryLevelProps = {}, selector: FilterableInventoryLevelProps = {},
config: FindConfig<InventoryLevel> = { relations: [], skip: 0, take: 10 } config: FindConfig<InventoryLevel> = { relations: [], skip: 0, take: 10 },
@MedusaContext() context: SharedContext = {}
): Promise<InventoryLevel[]> { ): Promise<InventoryLevel[]> {
const manager = this.activeManager_ const manager = context.transactionManager!
const levelRepository = manager.getRepository(InventoryLevel) const levelRepository = manager.getRepository(InventoryLevel)
const query = buildQuery(selector, config) as FindManyOptions const query = buildQuery(selector, config) as FindManyOptions
@@ -54,11 +56,13 @@ export default class InventoryLevelService extends TransactionBaseService {
* @param config - An object containing configuration options for the query. * @param config - An object containing configuration options for the query.
* @return An array of inventory levels and a count. * @return An array of inventory levels and a count.
*/ */
@InjectEntityManager()
async listAndCount( async listAndCount(
selector: FilterableInventoryLevelProps = {}, selector: FilterableInventoryLevelProps = {},
config: FindConfig<InventoryLevel> = { relations: [], skip: 0, take: 10 } config: FindConfig<InventoryLevel> = { relations: [], skip: 0, take: 10 },
@MedusaContext() context: SharedContext = {}
): Promise<[InventoryLevel[], number]> { ): Promise<[InventoryLevel[], number]> {
const manager = this.activeManager_ const manager = context.transactionManager!
const levelRepository = manager.getRepository(InventoryLevel) const levelRepository = manager.getRepository(InventoryLevel)
const query = buildQuery(selector, config) as FindManyOptions const query = buildQuery(selector, config) as FindManyOptions
@@ -72,9 +76,11 @@ export default class InventoryLevelService extends TransactionBaseService {
* @return A inventory level. * @return A inventory level.
* @throws If the inventory level ID is not defined or the given ID was not found. * @throws If the inventory level ID is not defined or the given ID was not found.
*/ */
@InjectEntityManager()
async retrieve( async retrieve(
inventoryLevelId: string, inventoryLevelId: string,
config: FindConfig<InventoryLevel> = {} config: FindConfig<InventoryLevel> = {},
@MedusaContext() context: SharedContext = {}
): Promise<InventoryLevel> { ): Promise<InventoryLevel> {
if (!isDefined(inventoryLevelId)) { if (!isDefined(inventoryLevelId)) {
throw new MedusaError( throw new MedusaError(
@@ -83,10 +89,13 @@ export default class InventoryLevelService extends TransactionBaseService {
) )
} }
const manager = this.activeManager_ const manager = context.transactionManager!
const levelRepository = manager.getRepository(InventoryLevel) const levelRepository = manager.getRepository(InventoryLevel)
const query = buildQuery({ id: inventoryLevelId }, config) as FindManyOptions const query = buildQuery(
{ id: inventoryLevelId },
config
) as FindManyOptions
const [inventoryLevel] = await levelRepository.find(query) const [inventoryLevel] = await levelRepository.find(query)
if (!inventoryLevel) { if (!inventoryLevel) {
@@ -104,27 +113,29 @@ export default class InventoryLevelService extends TransactionBaseService {
* @param data - An object containing the properties for the new inventory level. * @param data - An object containing the properties for the new inventory level.
* @return The created inventory level. * @return The created inventory level.
*/ */
async create(data: CreateInventoryLevelInput): Promise<InventoryLevel> { @InjectEntityManager()
return await this.atomicPhase_(async (manager) => { async create(
const levelRepository = manager.getRepository(InventoryLevel) data: CreateInventoryLevelInput,
@MedusaContext() context: SharedContext = {}
): Promise<InventoryLevel> {
const manager = context.transactionManager!
const inventoryLevel = levelRepository.create({ const levelRepository = manager.getRepository(InventoryLevel)
location_id: data.location_id,
inventory_item_id: data.inventory_item_id,
stocked_quantity: data.stocked_quantity,
reserved_quantity: data.reserved_quantity,
incoming_quantity: data.incoming_quantity,
})
const saved = await levelRepository.save(inventoryLevel) const inventoryLevel = levelRepository.create({
await this.eventBusService_ location_id: data.location_id,
.withTransaction(manager) inventory_item_id: data.inventory_item_id,
.emit(InventoryLevelService.Events.CREATED, { stocked_quantity: data.stocked_quantity,
id: saved.id, reserved_quantity: data.reserved_quantity,
}) incoming_quantity: data.incoming_quantity,
return saved
}) })
const saved = await levelRepository.save(inventoryLevel)
await this.eventBusService_?.emit?.(InventoryLevelService.Events.CREATED, {
id: saved.id,
})
return saved
} }
/** /**
@@ -134,35 +145,37 @@ export default class InventoryLevelService extends TransactionBaseService {
* @return The updated inventory level. * @return The updated inventory level.
* @throws If the inventory level ID is not defined or the given ID was not found. * @throws If the inventory level ID is not defined or the given ID was not found.
*/ */
@InjectEntityManager()
async update( async update(
inventoryLevelId: string, inventoryLevelId: string,
data: Omit< data: Omit<
DeepPartial<InventoryLevel>, DeepPartial<InventoryLevel>,
"id" | "created_at" | "metadata" | "deleted_at" "id" | "created_at" | "metadata" | "deleted_at"
> >,
@MedusaContext() context: SharedContext = {}
): Promise<InventoryLevel> { ): Promise<InventoryLevel> {
return await this.atomicPhase_(async (manager) => { const manager = context.transactionManager!
const levelRepository = manager.getRepository(InventoryLevel) const levelRepository = manager.getRepository(InventoryLevel)
const item = await this.retrieve(inventoryLevelId) const item = await this.retrieve(inventoryLevelId, undefined, context)
const shouldUpdate = Object.keys(data).some((key) => { const shouldUpdate = Object.keys(data).some((key) => {
return item[key] !== data[key] return item[key] !== data[key]
})
if (shouldUpdate) {
levelRepository.merge(item, data)
await levelRepository.save(item)
await this.eventBusService_
.withTransaction(manager)
.emit(InventoryLevelService.Events.UPDATED, {
id: item.id,
})
}
return item
}) })
if (shouldUpdate) {
levelRepository.merge(item, data)
await levelRepository.save(item)
await this.eventBusService_?.emit?.(
InventoryLevelService.Events.UPDATED,
{
id: item.id,
}
)
}
return item
} }
/** /**
@@ -171,40 +184,45 @@ export default class InventoryLevelService extends TransactionBaseService {
* @param locationId - The ID of the location. * @param locationId - The ID of the location.
* @param quantity - The quantity to adjust from the reserved quantity. * @param quantity - The quantity to adjust from the reserved quantity.
*/ */
@InjectEntityManager()
async adjustReservedQuantity( async adjustReservedQuantity(
inventoryItemId: string, inventoryItemId: string,
locationId: string, locationId: string,
quantity: number quantity: number,
@MedusaContext() context: SharedContext = {}
): Promise<void> { ): Promise<void> {
await this.atomicPhase_(async (manager) => { const manager = context.transactionManager!
await manager await manager
.createQueryBuilder() .createQueryBuilder()
.update(InventoryLevel) .update(InventoryLevel)
.set({ reserved_quantity: () => `reserved_quantity + ${quantity}` }) .set({ reserved_quantity: () => `reserved_quantity + ${quantity}` })
.where( .where(
"inventory_item_id = :inventoryItemId AND location_id = :locationId", "inventory_item_id = :inventoryItemId AND location_id = :locationId",
{ inventoryItemId, locationId } { inventoryItemId, locationId }
) )
.execute() .execute()
})
} }
/** /**
* Deletes inventory levels by inventory Item ID. * Deletes inventory levels by inventory Item ID.
* @param inventoryItemId - The ID or IDs of the inventory item to delete inventory levels for. * @param inventoryItemId - The ID or IDs of the inventory item to delete inventory levels for.
*/ */
async deleteByInventoryItemId(inventoryItemId: string | string[]): Promise<void> { @InjectEntityManager()
const ids = Array.isArray(inventoryItemId) ? inventoryItemId : [inventoryItemId] async deleteByInventoryItemId(
await this.atomicPhase_(async (manager) => { inventoryItemId: string | string[],
const levelRepository = manager.getRepository(InventoryLevel) @MedusaContext() context: SharedContext = {}
): Promise<void> {
const ids = Array.isArray(inventoryItemId)
? inventoryItemId
: [inventoryItemId]
await levelRepository.delete({ inventory_item_id: In(ids) }) const manager = context.transactionManager!
const levelRepository = manager.getRepository(InventoryLevel)
await this.eventBusService_ await levelRepository.delete({ inventory_item_id: In(ids) })
.withTransaction(manager)
.emit(InventoryLevelService.Events.DELETED, { await this.eventBusService_?.emit?.(InventoryLevelService.Events.DELETED, {
inventory_item_id: inventoryItemId, inventory_item_id: inventoryItemId,
})
}) })
} }
@@ -212,18 +230,22 @@ export default class InventoryLevelService extends TransactionBaseService {
* Deletes an inventory level by ID. * Deletes an inventory level by ID.
* @param inventoryLevelId - The ID or IDs of the inventory level to delete. * @param inventoryLevelId - The ID or IDs of the inventory level to delete.
*/ */
async delete(inventoryLevelId: string | string[]): Promise<void> { @InjectEntityManager()
const ids = Array.isArray(inventoryLevelId) ? inventoryLevelId : [inventoryLevelId] async delete(
await this.atomicPhase_(async (manager) => { inventoryLevelId: string | string[],
const levelRepository = manager.getRepository(InventoryLevel) @MedusaContext() context: SharedContext = {}
): Promise<void> {
const ids = Array.isArray(inventoryLevelId)
? inventoryLevelId
: [inventoryLevelId]
await levelRepository.delete({ id: In(ids) }) const manager = context.transactionManager!
const levelRepository = manager.getRepository(InventoryLevel)
await this.eventBusService_ await levelRepository.delete({ id: In(ids) })
.withTransaction(manager)
.emit(InventoryLevelService.Events.DELETED, { await this.eventBusService_?.emit?.(InventoryLevelService.Events.DELETED, {
id: inventoryLevelId, id: inventoryLevelId,
})
}) })
} }
@@ -231,17 +253,18 @@ export default class InventoryLevelService extends TransactionBaseService {
* Deletes inventory levels by location ID. * Deletes inventory levels by location ID.
* @param locationId - The ID of the location to delete inventory levels for. * @param locationId - The ID of the location to delete inventory levels for.
*/ */
async deleteByLocationId(locationId: string): Promise<void> { @InjectEntityManager()
return await this.atomicPhase_(async (manager) => { async deleteByLocationId(
const levelRepository = manager.getRepository(InventoryLevel) locationId: string,
@MedusaContext() context: SharedContext = {}
): Promise<void> {
const manager = context.transactionManager!
const levelRepository = manager.getRepository(InventoryLevel)
await levelRepository.delete({ location_id: locationId }) await levelRepository.delete({ location_id: locationId })
await this.eventBusService_ await this.eventBusService_?.emit?.(InventoryLevelService.Events.DELETED, {
.withTransaction(manager) location_id: locationId,
.emit(InventoryLevelService.Events.DELETED, {
location_id: locationId,
})
}) })
} }
@@ -251,15 +274,17 @@ export default class InventoryLevelService extends TransactionBaseService {
* @param locationIds - The IDs of the locations. * @param locationIds - The IDs of the locations.
* @return The total stocked quantity. * @return The total stocked quantity.
*/ */
@InjectEntityManager()
async getStockedQuantity( async getStockedQuantity(
inventoryItemId: string, inventoryItemId: string,
locationIds: string[] | string locationIds: string[] | string,
@MedusaContext() context: SharedContext = {}
): Promise<number> { ): Promise<number> {
if (!Array.isArray(locationIds)) { if (!Array.isArray(locationIds)) {
locationIds = [locationIds] locationIds = [locationIds]
} }
const manager = this.activeManager_ const manager = context.transactionManager!
const levelRepository = manager.getRepository(InventoryLevel) const levelRepository = manager.getRepository(InventoryLevel)
const result = await levelRepository const result = await levelRepository
@@ -278,15 +303,17 @@ export default class InventoryLevelService extends TransactionBaseService {
* @param locationIds - The IDs of the locations. * @param locationIds - The IDs of the locations.
* @return The total available quantity. * @return The total available quantity.
*/ */
@InjectEntityManager()
async getAvailableQuantity( async getAvailableQuantity(
inventoryItemId: string, inventoryItemId: string,
locationIds: string[] | string locationIds: string[] | string,
@MedusaContext() context: SharedContext = {}
): Promise<number> { ): Promise<number> {
if (!Array.isArray(locationIds)) { if (!Array.isArray(locationIds)) {
locationIds = [locationIds] locationIds = [locationIds]
} }
const manager = this.activeManager_ const manager = context.transactionManager!
const levelRepository = manager.getRepository(InventoryLevel) const levelRepository = manager.getRepository(InventoryLevel)
const result = await levelRepository const result = await levelRepository
@@ -305,15 +332,17 @@ export default class InventoryLevelService extends TransactionBaseService {
* @param locationIds - The IDs of the locations. * @param locationIds - The IDs of the locations.
* @return The total reserved quantity. * @return The total reserved quantity.
*/ */
@InjectEntityManager()
async getReservedQuantity( async getReservedQuantity(
inventoryItemId: string, inventoryItemId: string,
locationIds: string[] | string locationIds: string[] | string,
@MedusaContext() context: SharedContext = {}
): Promise<number> { ): Promise<number> {
if (!Array.isArray(locationIds)) { if (!Array.isArray(locationIds)) {
locationIds = [locationIds] locationIds = [locationIds]
} }
const manager = this.activeManager_ const manager = context.transactionManager!
const levelRepository = manager.getRepository(InventoryLevel) const levelRepository = manager.getRepository(InventoryLevel)
const result = await levelRepository const result = await levelRepository
+233 -148
View File
@@ -13,18 +13,18 @@ import {
InventoryItemDTO, InventoryItemDTO,
InventoryLevelDTO, InventoryLevelDTO,
ReservationItemDTO, ReservationItemDTO,
TransactionBaseService,
UpdateInventoryLevelInput, UpdateInventoryLevelInput,
UpdateReservationItemInput, UpdateReservationItemInput,
} from "@medusajs/medusa" } from "@medusajs/medusa"
import { SharedContext } from "@medusajs/types"
import { InjectEntityManager, MedusaContext } from "@medusajs/utils"
import { MedusaError } from "medusa-core-utils" import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import { import {
InventoryItemService, InventoryItemService,
InventoryLevelService, InventoryLevelService,
ReservationItemService, ReservationItemService,
} from "./" } from "./"
import { EntityManager } from "typeorm"
type InjectedDependencies = { type InjectedDependencies = {
manager: EntityManager manager: EntityManager
@@ -33,11 +33,9 @@ type InjectedDependencies = {
inventoryLevelService: InventoryLevelService inventoryLevelService: InventoryLevelService
reservationItemService: ReservationItemService reservationItemService: ReservationItemService
} }
export default class InventoryService export default class InventoryService implements IInventoryService {
extends TransactionBaseService protected readonly manager_: EntityManager
implements IInventoryService protected readonly eventBusService_: IEventBusService | undefined
{
protected readonly eventBusService_: IEventBusService
protected readonly inventoryItemService_: InventoryItemService protected readonly inventoryItemService_: InventoryItemService
protected readonly reservationItemService_: ReservationItemService protected readonly reservationItemService_: ReservationItemService
protected readonly inventoryLevelService_: InventoryLevelService protected readonly inventoryLevelService_: InventoryLevelService
@@ -53,9 +51,7 @@ export default class InventoryService
options?: unknown, options?: unknown,
moduleDeclaration?: InternalModuleDeclaration moduleDeclaration?: InternalModuleDeclaration
) { ) {
// @ts-ignore this.manager_ = manager
super(...arguments)
this.eventBusService_ = eventBusService this.eventBusService_ = eventBusService
this.inventoryItemService_ = inventoryItemService this.inventoryItemService_ = inventoryItemService
this.inventoryLevelService_ = inventoryLevelService this.inventoryLevelService_ = inventoryLevelService
@@ -68,13 +64,17 @@ export default class InventoryService
* @param config - the find configuration to use * @param config - the find configuration to use
* @return A tuple of inventory items and their total count * @return A tuple of inventory items and their total count
*/ */
@InjectEntityManager()
async listInventoryItems( async listInventoryItems(
selector: FilterableInventoryItemProps, selector: FilterableInventoryItemProps,
config: FindConfig<InventoryItemDTO> = { relations: [], skip: 0, take: 10 } config: FindConfig<InventoryItemDTO> = { relations: [], skip: 0, take: 10 },
@MedusaContext() context: SharedContext = {}
): Promise<[InventoryItemDTO[], number]> { ): Promise<[InventoryItemDTO[], number]> {
return await this.inventoryItemService_ return await this.inventoryItemService_.listAndCount(
.withTransaction(this.activeManager_) selector,
.listAndCount(selector, config) config,
context
)
} }
/** /**
@@ -83,13 +83,21 @@ export default class InventoryService
* @param config - the find configuration to use * @param config - the find configuration to use
* @return A tuple of inventory levels and their total count * @return A tuple of inventory levels and their total count
*/ */
@InjectEntityManager()
async listInventoryLevels( async listInventoryLevels(
selector: FilterableInventoryLevelProps, selector: FilterableInventoryLevelProps,
config: FindConfig<InventoryLevelDTO> = { relations: [], skip: 0, take: 10 } config: FindConfig<InventoryLevelDTO> = {
relations: [],
skip: 0,
take: 10,
},
@MedusaContext() context: SharedContext = {}
): Promise<[InventoryLevelDTO[], number]> { ): Promise<[InventoryLevelDTO[], number]> {
return await this.inventoryLevelService_ return await this.inventoryLevelService_.listAndCount(
.withTransaction(this.activeManager_) selector,
.listAndCount(selector, config) config,
context
)
} }
/** /**
@@ -98,17 +106,21 @@ export default class InventoryService
* @param config - the find configuration to use * @param config - the find configuration to use
* @return A tuple of reservation items and their total count * @return A tuple of reservation items and their total count
*/ */
@InjectEntityManager()
async listReservationItems( async listReservationItems(
selector: FilterableReservationItemProps, selector: FilterableReservationItemProps,
config: FindConfig<ReservationItemDTO> = { config: FindConfig<ReservationItemDTO> = {
relations: [], relations: [],
skip: 0, skip: 0,
take: 10, take: 10,
} },
@MedusaContext() context: SharedContext = {}
): Promise<[ReservationItemDTO[], number]> { ): Promise<[ReservationItemDTO[], number]> {
return await this.reservationItemService_ return await this.reservationItemService_.listAndCount(
.withTransaction(this.activeManager_) selector,
.listAndCount(selector, config) config,
context
)
} }
/** /**
@@ -117,13 +129,17 @@ export default class InventoryService
* @param config - the find configuration to use * @param config - the find configuration to use
* @return The retrieved inventory item * @return The retrieved inventory item
*/ */
@InjectEntityManager()
async retrieveInventoryItem( async retrieveInventoryItem(
inventoryItemId: string, inventoryItemId: string,
config?: FindConfig<InventoryItemDTO> config?: FindConfig<InventoryItemDTO>,
@MedusaContext() context: SharedContext = {}
): Promise<InventoryItemDTO> { ): Promise<InventoryItemDTO> {
const inventoryItem = await this.inventoryItemService_ const inventoryItem = await this.inventoryItemService_.retrieve(
.withTransaction(this.activeManager_) inventoryItemId,
.retrieve(inventoryItemId, config) config,
context
)
return { ...inventoryItem } return { ...inventoryItem }
} }
@@ -133,16 +149,17 @@ export default class InventoryService
* @param locationId - the id of the location * @param locationId - the id of the location
* @return the retrieved inventory level * @return the retrieved inventory level
*/ */
@InjectEntityManager()
async retrieveInventoryLevel( async retrieveInventoryLevel(
inventoryItemId: string, inventoryItemId: string,
locationId: string locationId: string,
@MedusaContext() context: SharedContext = {}
): Promise<InventoryLevelDTO> { ): Promise<InventoryLevelDTO> {
const [inventoryLevel] = await this.inventoryLevelService_ const [inventoryLevel] = await this.inventoryLevelService_.list(
.withTransaction(this.activeManager_) { inventory_item_id: inventoryItemId, location_id: locationId },
.list( { take: 1 },
{ inventory_item_id: inventoryItemId, location_id: locationId }, context
{ take: 1 } )
)
if (!inventoryLevel) { if (!inventoryLevel) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.NOT_FOUND, MedusaError.Types.NOT_FOUND,
@@ -152,15 +169,21 @@ export default class InventoryService
return inventoryLevel return inventoryLevel
} }
/** /**
* Retrieves a reservation item * Retrieves a reservation item
* @param inventoryItemId - the id of the reservation item * @param inventoryItemId - the id of the reservation item
* @return the retrieved reservation level * @return the retrieved reservation level
*/ */
async retrieveReservationItem(reservationId: string): Promise<ReservationItemDTO> { @InjectEntityManager()
return await this.reservationItemService_ async retrieveReservationItem(
.withTransaction(this.activeManager_) reservationId: string,
.retrieve(reservationId) @MedusaContext() context: SharedContext = {}
): Promise<ReservationItemDTO> {
return await this.reservationItemService_.retrieve(
reservationId,
undefined,
context
)
} }
/** /**
@@ -168,19 +191,20 @@ export default class InventoryService
* @param input - the input object * @param input - the input object
* @return The created reservation item * @return The created reservation item
*/ */
@InjectEntityManager()
async createReservationItem( async createReservationItem(
input: CreateReservationItemInput input: CreateReservationItemInput,
@MedusaContext() context: SharedContext = {}
): Promise<ReservationItemDTO> { ): Promise<ReservationItemDTO> {
// Verify that the item is stocked at the location // Verify that the item is stocked at the location
const [inventoryLevel] = await this.inventoryLevelService_ const [inventoryLevel] = await this.inventoryLevelService_.list(
.withTransaction(this.activeManager_) {
.list( inventory_item_id: input.inventory_item_id,
{ location_id: input.location_id,
inventory_item_id: input.inventory_item_id, },
location_id: input.location_id, { take: 1 },
}, context
{ take: 1 } )
)
if (!inventoryLevel) { if (!inventoryLevel) {
throw new MedusaError( throw new MedusaError(
@@ -189,9 +213,10 @@ export default class InventoryService
) )
} }
const reservationItem = await this.reservationItemService_ const reservationItem = await this.reservationItemService_.create(
.withTransaction(this.activeManager_) input,
.create(input) context
)
return { ...reservationItem } return { ...reservationItem }
} }
@@ -201,12 +226,15 @@ export default class InventoryService
* @param input - the input object * @param input - the input object
* @return The created inventory item * @return The created inventory item
*/ */
@InjectEntityManager()
async createInventoryItem( async createInventoryItem(
input: CreateInventoryItemInput input: CreateInventoryItemInput,
@MedusaContext() context: SharedContext = {}
): Promise<InventoryItemDTO> { ): Promise<InventoryItemDTO> {
const inventoryItem = await this.inventoryItemService_ const inventoryItem = await this.inventoryItemService_.create(
.withTransaction(this.activeManager_) input,
.create(input) context
)
return { ...inventoryItem } return { ...inventoryItem }
} }
@@ -215,12 +243,12 @@ export default class InventoryService
* @param input - the input object * @param input - the input object
* @return The created inventory level * @return The created inventory level
*/ */
@InjectEntityManager()
async createInventoryLevel( async createInventoryLevel(
input: CreateInventoryLevelInput input: CreateInventoryLevelInput,
@MedusaContext() context: SharedContext = {}
): Promise<InventoryLevelDTO> { ): Promise<InventoryLevelDTO> {
return await this.inventoryLevelService_ return await this.inventoryLevelService_.create(input, context)
.withTransaction(this.activeManager_)
.create(input)
} }
/** /**
@@ -229,13 +257,17 @@ export default class InventoryService
* @param input - the input object * @param input - the input object
* @return The updated inventory item * @return The updated inventory item
*/ */
@InjectEntityManager()
async updateInventoryItem( async updateInventoryItem(
inventoryItemId: string, inventoryItemId: string,
input: Partial<CreateInventoryItemInput> input: Partial<CreateInventoryItemInput>,
@MedusaContext() context: SharedContext = {}
): Promise<InventoryItemDTO> { ): Promise<InventoryItemDTO> {
const inventoryItem = await this.inventoryItemService_ const inventoryItem = await this.inventoryItemService_.update(
.withTransaction(this.activeManager_) inventoryItemId,
.update(inventoryItemId, input) input,
context
)
return { ...inventoryItem } return { ...inventoryItem }
} }
@@ -243,26 +275,39 @@ export default class InventoryService
* Deletes an inventory item * Deletes an inventory item
* @param inventoryItemId - the id of the inventory item to delete * @param inventoryItemId - the id of the inventory item to delete
*/ */
async deleteInventoryItem(inventoryItemId: string): Promise<void> { @InjectEntityManager()
await this.inventoryLevelService_ async deleteInventoryItem(
.withTransaction(this.activeManager_) inventoryItemId: string,
.deleteByInventoryItemId(inventoryItemId) @MedusaContext() context: SharedContext = {}
): Promise<void> {
await this.inventoryLevelService_.deleteByInventoryItemId(
inventoryItemId,
context
)
return await this.inventoryItemService_ return await this.inventoryItemService_.delete(inventoryItemId, context)
.withTransaction(this.activeManager_)
.delete(inventoryItemId)
} }
async deleteInventoryItemLevelByLocationId(locationId: string): Promise<void> { @InjectEntityManager()
return await this.inventoryLevelService_ async deleteInventoryItemLevelByLocationId(
.withTransaction(this.activeManager_) locationId: string,
.deleteByLocationId(locationId) @MedusaContext() context: SharedContext = {}
): Promise<void> {
return await this.inventoryLevelService_.deleteByLocationId(
locationId,
context
)
} }
async deleteReservationItemByLocationId(locationId: string): Promise<void> { @InjectEntityManager()
return await this.reservationItemService_ async deleteReservationItemByLocationId(
.withTransaction(this.activeManager_) locationId: string,
.deleteByLocationId(locationId) @MedusaContext() context: SharedContext = {}
): Promise<void> {
return await this.reservationItemService_.deleteByLocationId(
locationId,
context
)
} }
/** /**
@@ -270,24 +315,23 @@ export default class InventoryService
* @param inventoryItemId - the id of the inventory item associated with the level * @param inventoryItemId - the id of the inventory item associated with the level
* @param locationId - the id of the location associated with the level * @param locationId - the id of the location associated with the level
*/ */
@InjectEntityManager()
async deleteInventoryLevel( async deleteInventoryLevel(
inventoryItemId: string, inventoryItemId: string,
locationId: string locationId: string,
@MedusaContext() context: SharedContext = {}
): Promise<void> { ): Promise<void> {
const [inventoryLevel] = await this.inventoryLevelService_ const [inventoryLevel] = await this.inventoryLevelService_.list(
.withTransaction(this.activeManager_) { inventory_item_id: inventoryItemId, location_id: locationId },
.list( { take: 1 },
{ inventory_item_id: inventoryItemId, location_id: locationId }, context
{ take: 1 } )
)
if (!inventoryLevel) { if (!inventoryLevel) {
return return
} }
return await this.inventoryLevelService_ return await this.inventoryLevelService_.delete(inventoryLevel.id, context)
.withTransaction(this.activeManager_)
.delete(inventoryLevel.id)
} }
/** /**
@@ -297,17 +341,18 @@ export default class InventoryService
* @param input - the input object * @param input - the input object
* @return The updated inventory level * @return The updated inventory level
*/ */
@InjectEntityManager()
async updateInventoryLevel( async updateInventoryLevel(
inventoryItemId: string, inventoryItemId: string,
locationId: string, locationId: string,
input: UpdateInventoryLevelInput input: UpdateInventoryLevelInput,
@MedusaContext() context: SharedContext = {}
): Promise<InventoryLevelDTO> { ): Promise<InventoryLevelDTO> {
const [inventoryLevel] = await this.inventoryLevelService_ const [inventoryLevel] = await this.inventoryLevelService_.list(
.withTransaction(this.activeManager_) { inventory_item_id: inventoryItemId, location_id: locationId },
.list( { take: 1 },
{ inventory_item_id: inventoryItemId, location_id: locationId }, context
{ take: 1 } )
)
if (!inventoryLevel) { if (!inventoryLevel) {
throw new MedusaError( throw new MedusaError(
@@ -316,9 +361,11 @@ export default class InventoryService
) )
} }
return await this.inventoryLevelService_ return await this.inventoryLevelService_.update(
.withTransaction(this.activeManager_) inventoryLevel.id,
.update(inventoryLevel.id, input) input,
context
)
} }
/** /**
@@ -327,33 +374,44 @@ export default class InventoryService
* @param input - the input object * @param input - the input object
* @return The updated inventory level * @return The updated inventory level
*/ */
@InjectEntityManager()
async updateReservationItem( async updateReservationItem(
reservationItemId: string, reservationItemId: string,
input: UpdateReservationItemInput input: UpdateReservationItemInput,
@MedusaContext() context: SharedContext = {}
): Promise<ReservationItemDTO> { ): Promise<ReservationItemDTO> {
return await this.reservationItemService_ return await this.reservationItemService_.update(
.withTransaction(this.activeManager_) reservationItemId,
.update(reservationItemId, input) input,
context
)
} }
/** /**
* Deletes reservation items by line item * Deletes reservation items by line item
* @param lineItemId - the id of the line item associated with the reservation item * @param lineItemId - the id of the line item associated with the reservation item
*/ */
async deleteReservationItemsByLineItem(lineItemId: string): Promise<void> { @InjectEntityManager()
return await this.reservationItemService_ async deleteReservationItemsByLineItem(
.withTransaction(this.activeManager_) lineItemId: string,
.deleteByLineItem(lineItemId) @MedusaContext() context: SharedContext = {}
): Promise<void> {
return await this.reservationItemService_.deleteByLineItem(
lineItemId,
context
)
} }
/** /**
* Deletes a reservation item * Deletes a reservation item
* @param reservationItemId - the id of the reservation item to delete * @param reservationItemId - the id of the reservation item to delete
*/ */
async deleteReservationItem(reservationItemId: string | string[]): Promise<void> { @InjectEntityManager()
return await this.reservationItemService_ async deleteReservationItem(
.withTransaction(this.activeManager_) reservationItemId: string | string[],
.delete(reservationItemId) @MedusaContext() context: SharedContext = {}
): Promise<void> {
return await this.reservationItemService_.delete(reservationItemId, context)
} }
/** /**
@@ -364,17 +422,18 @@ export default class InventoryService
* @return The updated inventory level * @return The updated inventory level
* @throws when the inventory level is not found * @throws when the inventory level is not found
*/ */
@InjectEntityManager()
async adjustInventory( async adjustInventory(
inventoryItemId: string, inventoryItemId: string,
locationId: string, locationId: string,
adjustment: number adjustment: number,
@MedusaContext() context: SharedContext = {}
): Promise<InventoryLevelDTO> { ): Promise<InventoryLevelDTO> {
const [inventoryLevel] = await this.inventoryLevelService_ const [inventoryLevel] = await this.inventoryLevelService_.list(
.withTransaction(this.activeManager_) { inventory_item_id: inventoryItemId, location_id: locationId },
.list( { take: 1 },
{ inventory_item_id: inventoryItemId, location_id: locationId }, context
{ take: 1 } )
)
if (!inventoryLevel) { if (!inventoryLevel) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.NOT_FOUND, MedusaError.Types.NOT_FOUND,
@@ -382,11 +441,13 @@ export default class InventoryService
) )
} }
const updatedInventoryLevel = await this.inventoryLevelService_ const updatedInventoryLevel = await this.inventoryLevelService_.update(
.withTransaction(this.activeManager_) inventoryLevel.id,
.update(inventoryLevel.id, { {
stocked_quantity: inventoryLevel.stocked_quantity + adjustment, stocked_quantity: inventoryLevel.stocked_quantity + adjustment,
}) },
context
)
return { ...updatedInventoryLevel } return { ...updatedInventoryLevel }
} }
@@ -398,20 +459,27 @@ export default class InventoryService
* @return The available quantity * @return The available quantity
* @throws when the inventory item is not found * @throws when the inventory item is not found
*/ */
@InjectEntityManager()
async retrieveAvailableQuantity( async retrieveAvailableQuantity(
inventoryItemId: string, inventoryItemId: string,
locationIds: string[] locationIds: string[],
@MedusaContext() context: SharedContext = {}
): Promise<number> { ): Promise<number> {
// Throws if item does not exist // Throws if item does not exist
await this.inventoryItemService_ await this.inventoryItemService_.retrieve(
.withTransaction(this.activeManager_) inventoryItemId,
.retrieve(inventoryItemId, { {
select: ["id"], select: ["id"],
}) },
context
)
const availableQuantity = await this.inventoryLevelService_ const availableQuantity =
.withTransaction(this.activeManager_) await this.inventoryLevelService_.getAvailableQuantity(
.getAvailableQuantity(inventoryItemId, locationIds) inventoryItemId,
locationIds,
context
)
return availableQuantity return availableQuantity
} }
@@ -423,20 +491,27 @@ export default class InventoryService
* @return The stocked quantity * @return The stocked quantity
* @throws when the inventory item is not found * @throws when the inventory item is not found
*/ */
@InjectEntityManager()
async retrieveStockedQuantity( async retrieveStockedQuantity(
inventoryItemId: string, inventoryItemId: string,
locationIds: string[] locationIds: string[],
@MedusaContext() context: SharedContext = {}
): Promise<number> { ): Promise<number> {
// Throws if item does not exist // Throws if item does not exist
await this.inventoryItemService_ await this.inventoryItemService_.retrieve(
.withTransaction(this.activeManager_) inventoryItemId,
.retrieve(inventoryItemId, { {
select: ["id"], select: ["id"],
}) },
context
)
const stockedQuantity = await this.inventoryLevelService_ const stockedQuantity =
.withTransaction(this.activeManager_) await this.inventoryLevelService_.getStockedQuantity(
.getStockedQuantity(inventoryItemId, locationIds) inventoryItemId,
locationIds,
context
)
return stockedQuantity return stockedQuantity
} }
@@ -448,20 +523,27 @@ export default class InventoryService
* @return The reserved quantity * @return The reserved quantity
* @throws when the inventory item is not found * @throws when the inventory item is not found
*/ */
@InjectEntityManager()
async retrieveReservedQuantity( async retrieveReservedQuantity(
inventoryItemId: string, inventoryItemId: string,
locationIds: string[] locationIds: string[],
@MedusaContext() context: SharedContext = {}
): Promise<number> { ): Promise<number> {
// Throws if item does not exist // Throws if item does not exist
await this.inventoryItemService_ await this.inventoryItemService_.retrieve(
.withTransaction(this.activeManager_) inventoryItemId,
.retrieve(inventoryItemId, { {
select: ["id"], select: ["id"],
}) },
context
)
const reservedQuantity = await this.inventoryLevelService_ const reservedQuantity =
.withTransaction(this.activeManager_) await this.inventoryLevelService_.getReservedQuantity(
.getReservedQuantity(inventoryItemId, locationIds) inventoryItemId,
locationIds,
context
)
return reservedQuantity return reservedQuantity
} }
@@ -473,14 +555,17 @@ export default class InventoryService
* @param quantity - the quantity to check * @param quantity - the quantity to check
* @return Whether there is sufficient inventory * @return Whether there is sufficient inventory
*/ */
@InjectEntityManager()
async confirmInventory( async confirmInventory(
inventoryItemId: string, inventoryItemId: string,
locationIds: string[], locationIds: string[],
quantity: number quantity: number,
@MedusaContext() context: SharedContext = {}
): Promise<boolean> { ): Promise<boolean> {
const availableQuantity = await this.retrieveAvailableQuantity( const availableQuantity = await this.retrieveAvailableQuantity(
inventoryItemId, inventoryItemId,
locationIds locationIds,
context
) )
return availableQuantity >= quantity return availableQuantity >= quantity
} }
@@ -1,17 +1,17 @@
import { EntityManager, FindManyOptions } from "typeorm"
import { isDefined, MedusaError } from "medusa-core-utils"
import { import {
buildQuery, buildQuery,
CreateReservationItemInput, CreateReservationItemInput,
FilterableReservationItemProps, FilterableReservationItemProps,
FindConfig, FindConfig,
IEventBusService, IEventBusService,
TransactionBaseService,
UpdateReservationItemInput, UpdateReservationItemInput,
} from "@medusajs/medusa" } from "@medusajs/medusa"
import { SharedContext } from "@medusajs/types"
import { ReservationItem } from "../models" import { InjectEntityManager, MedusaContext } from "@medusajs/utils"
import { isDefined, MedusaError } from "medusa-core-utils"
import { EntityManager, FindManyOptions } from "typeorm"
import { InventoryLevelService } from "." import { InventoryLevelService } from "."
import { ReservationItem } from "../models"
type InjectedDependencies = { type InjectedDependencies = {
eventBusService: IEventBusService eventBusService: IEventBusService
@@ -19,22 +19,23 @@ type InjectedDependencies = {
inventoryLevelService: InventoryLevelService inventoryLevelService: InventoryLevelService
} }
export default class ReservationItemService extends TransactionBaseService { export default class ReservationItemService {
static Events = { static Events = {
CREATED: "reservation-item.created", CREATED: "reservation-item.created",
UPDATED: "reservation-item.updated", UPDATED: "reservation-item.updated",
DELETED: "reservation-item.deleted", DELETED: "reservation-item.deleted",
} }
protected readonly eventBusService_: IEventBusService protected readonly manager_: EntityManager
protected readonly eventBusService_: IEventBusService | undefined
protected readonly inventoryLevelService_: InventoryLevelService protected readonly inventoryLevelService_: InventoryLevelService
constructor({ constructor({
eventBusService, eventBusService,
inventoryLevelService, inventoryLevelService,
manager,
}: InjectedDependencies) { }: InjectedDependencies) {
super(arguments[0]) this.manager_ = manager
this.eventBusService_ = eventBusService this.eventBusService_ = eventBusService
this.inventoryLevelService_ = inventoryLevelService this.inventoryLevelService_ = inventoryLevelService
} }
@@ -45,11 +46,13 @@ export default class ReservationItemService extends TransactionBaseService {
* @param config - Configuration for the query. * @param config - Configuration for the query.
* @return Array of reservation items that match the selector. * @return Array of reservation items that match the selector.
*/ */
@InjectEntityManager()
async list( async list(
selector: FilterableReservationItemProps = {}, selector: FilterableReservationItemProps = {},
config: FindConfig<ReservationItem> = { relations: [], skip: 0, take: 10 } config: FindConfig<ReservationItem> = { relations: [], skip: 0, take: 10 },
@MedusaContext() context: SharedContext = {}
): Promise<ReservationItem[]> { ): Promise<ReservationItem[]> {
const manager = this.activeManager_ const manager = context.transactionManager!
const itemRepository = manager.getRepository(ReservationItem) const itemRepository = manager.getRepository(ReservationItem)
const query = buildQuery(selector, config) as FindManyOptions const query = buildQuery(selector, config) as FindManyOptions
@@ -62,11 +65,13 @@ export default class ReservationItemService extends TransactionBaseService {
* @param config - Configuration for the query. * @param config - Configuration for the query.
* @return Array of reservation items that match the selector and the total count. * @return Array of reservation items that match the selector and the total count.
*/ */
@InjectEntityManager()
async listAndCount( async listAndCount(
selector: FilterableReservationItemProps = {}, selector: FilterableReservationItemProps = {},
config: FindConfig<ReservationItem> = { relations: [], skip: 0, take: 10 } config: FindConfig<ReservationItem> = { relations: [], skip: 0, take: 10 },
@MedusaContext() context: SharedContext = {}
): Promise<[ReservationItem[], number]> { ): Promise<[ReservationItem[], number]> {
const manager = this.activeManager_ const manager = context.transactionManager!
const itemRepository = manager.getRepository(ReservationItem) const itemRepository = manager.getRepository(ReservationItem)
const query = buildQuery(selector, config) as FindManyOptions const query = buildQuery(selector, config) as FindManyOptions
@@ -80,9 +85,11 @@ export default class ReservationItemService extends TransactionBaseService {
* @return The reservation item with the provided id. * @return The reservation item with the provided id.
* @throws If reservationItemId is not defined or if the reservation item was not found. * @throws If reservationItemId is not defined or if the reservation item was not found.
*/ */
@InjectEntityManager()
async retrieve( async retrieve(
reservationItemId: string, reservationItemId: string,
config: FindConfig<ReservationItem> = {} config: FindConfig<ReservationItem> = {},
@MedusaContext() context: SharedContext = {}
): Promise<ReservationItem> { ): Promise<ReservationItem> {
if (!isDefined(reservationItemId)) { if (!isDefined(reservationItemId)) {
throw new MedusaError( throw new MedusaError(
@@ -91,7 +98,7 @@ export default class ReservationItemService extends TransactionBaseService {
) )
} }
const manager = this.activeManager_ const manager = context.transactionManager!
const reservationItemRepository = manager.getRepository(ReservationItem) const reservationItemRepository = manager.getRepository(ReservationItem)
const query = buildQuery( const query = buildQuery(
@@ -115,37 +122,37 @@ export default class ReservationItemService extends TransactionBaseService {
* @param data - The reservation item data. * @param data - The reservation item data.
* @return The created reservation item. * @return The created reservation item.
*/ */
async create(data: CreateReservationItemInput): Promise<ReservationItem> { @InjectEntityManager()
return await this.atomicPhase_(async (manager) => { async create(
const itemRepository = manager.getRepository(ReservationItem) data: CreateReservationItemInput,
@MedusaContext() context: SharedContext = {}
): Promise<ReservationItem> {
const manager = context.transactionManager!
const itemRepository = manager.getRepository(ReservationItem)
const inventoryItem = itemRepository.create({ const inventoryItem = itemRepository.create({
inventory_item_id: data.inventory_item_id, inventory_item_id: data.inventory_item_id,
line_item_id: data.line_item_id, line_item_id: data.line_item_id,
location_id: data.location_id, location_id: data.location_id,
quantity: data.quantity, quantity: data.quantity,
metadata: data.metadata, metadata: data.metadata,
})
const [newInventoryItem] = await Promise.all([
itemRepository.save(inventoryItem),
this.inventoryLevelService_
.withTransaction(manager)
.adjustReservedQuantity(
data.inventory_item_id,
data.location_id,
data.quantity
),
])
await this.eventBusService_
.withTransaction(manager)
.emit(ReservationItemService.Events.CREATED, {
id: newInventoryItem.id,
})
return newInventoryItem
}) })
const [newInventoryItem] = await Promise.all([
itemRepository.save(inventoryItem),
this.inventoryLevelService_.adjustReservedQuantity(
data.inventory_item_id,
data.location_id,
data.quantity,
context
),
])
await this.eventBusService_?.emit?.(ReservationItemService.Events.CREATED, {
id: newInventoryItem.id,
})
return newInventoryItem
} }
/** /**
@@ -154,99 +161,99 @@ export default class ReservationItemService extends TransactionBaseService {
* @param data - The reservation item data to update. * @param data - The reservation item data to update.
* @return The updated reservation item. * @return The updated reservation item.
*/ */
@InjectEntityManager()
async update( async update(
reservationItemId: string, reservationItemId: string,
data: UpdateReservationItemInput data: UpdateReservationItemInput,
@MedusaContext() context: SharedContext = {}
): Promise<ReservationItem> { ): Promise<ReservationItem> {
return await this.atomicPhase_(async (manager) => { const manager = context.transactionManager!
const itemRepository = manager.getRepository(ReservationItem) const itemRepository = manager.getRepository(ReservationItem)
const item = await this.retrieve(reservationItemId) const item = await this.retrieve(reservationItemId)
const shouldUpdateQuantity = const shouldUpdateQuantity =
isDefined(data.quantity) && data.quantity !== item.quantity isDefined(data.quantity) && data.quantity !== item.quantity
const shouldUpdateLocation = const shouldUpdateLocation =
isDefined(data.location_id) && data.location_id !== item.location_id isDefined(data.location_id) && data.location_id !== item.location_id
const ops: Promise<unknown>[] = [] const ops: Promise<unknown>[] = []
if (shouldUpdateLocation) { if (shouldUpdateLocation) {
ops.push( ops.push(
this.inventoryLevelService_ this.inventoryLevelService_.adjustReservedQuantity(
.withTransaction(manager) item.inventory_item_id,
.adjustReservedQuantity( item.location_id,
item.inventory_item_id, item.quantity * -1,
item.location_id, context
item.quantity * -1 ),
), this.inventoryLevelService_.adjustReservedQuantity(
this.inventoryLevelService_ item.inventory_item_id,
.withTransaction(manager) data.location_id!,
.adjustReservedQuantity( data.quantity || item.quantity!,
item.inventory_item_id, context
data.location_id!,
data.quantity || item.quantity!
)
) )
} else if (shouldUpdateQuantity) { )
const quantityDiff = data.quantity! - item.quantity } else if (shouldUpdateQuantity) {
ops.push( const quantityDiff = data.quantity! - item.quantity
this.inventoryLevelService_ ops.push(
.withTransaction(manager) this.inventoryLevelService_.adjustReservedQuantity(
.adjustReservedQuantity( item.inventory_item_id,
item.inventory_item_id, item.location_id,
item.location_id, quantityDiff,
quantityDiff context
)
) )
} )
}
const mergedItem = itemRepository.merge(item, data) const mergedItem = itemRepository.merge(item, data)
ops.push(itemRepository.save(item)) ops.push(itemRepository.save(item))
await Promise.all(ops) await Promise.all(ops)
await this.eventBusService_ await this.eventBusService_?.emit?.(ReservationItemService.Events.UPDATED, {
.withTransaction(manager) id: mergedItem.id,
.emit(ReservationItemService.Events.UPDATED, {
id: mergedItem.id,
})
return mergedItem
}) })
return mergedItem
} }
/** /**
* Deletes a reservation item by line item id. * Deletes a reservation item by line item id.
* @param lineItemId - the id of the line item to delete. * @param lineItemId - the id of the line item to delete.
*/ */
async deleteByLineItem(lineItemId: string): Promise<void> { @InjectEntityManager()
await this.atomicPhase_(async (manager) => { async deleteByLineItem(
const itemRepository = manager.getRepository(ReservationItem) lineItemId: string,
@MedusaContext() context: SharedContext = {}
): Promise<void> {
const manager = context.transactionManager!
const itemRepository = manager.getRepository(ReservationItem)
const items = await this.list({ line_item_id: lineItemId }) const items = await this.list(
{ line_item_id: lineItemId },
undefined,
context
)
const ops: Promise<unknown>[] = [] const ops: Promise<unknown>[] = []
for (const item of items) { for (const item of items) {
ops.push(itemRepository.softRemove({ line_item_id: lineItemId })) ops.push(itemRepository.softRemove({ line_item_id: lineItemId }))
ops.push( ops.push(
this.inventoryLevelService_ this.inventoryLevelService_.adjustReservedQuantity(
.withTransaction(manager) item.inventory_item_id,
.adjustReservedQuantity( item.location_id,
item.inventory_item_id, item.quantity * -1,
item.location_id, context
item.quantity * -1
)
) )
} )
await Promise.all(ops) }
await Promise.all(ops)
await this.eventBusService_ await this.eventBusService_?.emit?.(ReservationItemService.Events.DELETED, {
.withTransaction(manager) line_item_id: lineItemId,
.emit(ReservationItemService.Events.DELETED, {
line_item_id: lineItemId,
})
}) })
} }
@@ -254,22 +261,23 @@ export default class ReservationItemService extends TransactionBaseService {
* Deletes reservation items by location ID. * Deletes reservation items by location ID.
* @param locationId - The ID of the location to delete reservations for. * @param locationId - The ID of the location to delete reservations for.
*/ */
async deleteByLocationId(locationId: string): Promise<void> { @InjectEntityManager()
return await this.atomicPhase_(async (manager) => { async deleteByLocationId(
const itemRepository = manager.getRepository(ReservationItem) locationId: string,
@MedusaContext() context: SharedContext = {}
): Promise<void> {
const manager = context.transactionManager!
const itemRepository = manager.getRepository(ReservationItem)
await itemRepository await itemRepository
.createQueryBuilder("reservation_item") .createQueryBuilder("reservation_item")
.softDelete() .softDelete()
.where("location_id = :locationId", { locationId }) .where("location_id = :locationId", { locationId })
.andWhere("deleted_at IS NULL") .andWhere("deleted_at IS NULL")
.execute() .execute()
await this.eventBusService_ await this.eventBusService_?.emit?.(ReservationItemService.Events.DELETED, {
.withTransaction(manager) location_id: locationId,
.emit(ReservationItemService.Events.DELETED, {
location_id: locationId,
})
}) })
} }
@@ -277,35 +285,32 @@ export default class ReservationItemService extends TransactionBaseService {
* Deletes a reservation item by id. * Deletes a reservation item by id.
* @param reservationItemId - the id of the reservation item to delete. * @param reservationItemId - the id of the reservation item to delete.
*/ */
async delete(reservationItemId: string | string[]): Promise<void> { async delete(
reservationItemId: string | string[],
@MedusaContext() context: SharedContext = {}
): Promise<void> {
const ids = Array.isArray(reservationItemId) const ids = Array.isArray(reservationItemId)
? reservationItemId ? reservationItemId
: [reservationItemId] : [reservationItemId]
return await this.atomicPhase_(async (manager) => { const manager = context.transactionManager!
const itemRepository = manager.getRepository(ReservationItem) const itemRepository = manager.getRepository(ReservationItem)
const items = await this.list({ id: ids })
const items = await this.list({ id: ids }) const promises: Promise<unknown>[] = items.map(async (item) => {
this.inventoryLevelService_.adjustReservedQuantity(
await itemRepository.softRemove(items) item.inventory_item_id,
item.location_id,
const inventoryServiceTx = item.quantity * -1,
this.inventoryLevelService_.withTransaction(manager) context
await Promise.all(
items.map(async (item) => {
return inventoryServiceTx.adjustReservedQuantity(
item.inventory_item_id,
item.location_id,
item.quantity * -1
)
})
) )
})
await this.eventBusService_ promises.push(itemRepository.softRemove(items))
.withTransaction(manager)
.emit(ReservationItemService.Events.DELETED, { await Promise.all(promises)
id: reservationItemId,
}) await this.eventBusService_?.emit?.(ReservationItemService.Events.DELETED, {
id: reservationItemId,
}) })
} }
} }
+12
View File
@@ -0,0 +1,12 @@
import { DatabaseType } from "typeorm"
export type InventoryServiceInitializeOptions = {
database?: {
type?: DatabaseType | string
url?: string
database?: string
extra?: Record<string, any>
schema?: string
logging?: boolean
}
}
@@ -85,9 +85,6 @@ export default async (req, res) => {
const orderServiceTx = orderService.withTransaction(manager) const orderServiceTx = orderService.withTransaction(manager)
const cartServiceTx = cartService.withTransaction(manager) const cartServiceTx = cartService.withTransaction(manager)
const productVariantInventoryServiceTx =
productVariantInventoryService.withTransaction(manager)
const draftOrder = await draftOrderServiceTx.retrieve(id) const draftOrder = await draftOrderServiceTx.retrieve(id)
const cart = await cartServiceTx.retrieveWithTotals(draftOrder.cart_id) const cart = await cartServiceTx.retrieveWithTotals(draftOrder.cart_id)
@@ -116,7 +113,7 @@ export default async (req, res) => {
}) })
await reserveQuantityForDraftOrder(order, { await reserveQuantityForDraftOrder(order, {
productVariantInventoryService: productVariantInventoryServiceTx, productVariantInventoryService,
}) })
return order return order
@@ -59,9 +59,7 @@ export default async (req: Request, res: Response) => {
.withTransaction(transactionManager) .withTransaction(transactionManager)
.detachInventoryItem(id) .detachInventoryItem(id)
await inventoryService await inventoryService.deleteInventoryItem(id)
.withTransaction(transactionManager)
.deleteInventoryItem(id)
}) })
res.status(200).send({ res.status(200).send({
@@ -74,11 +74,7 @@ export default async (req: Request, res: Response) => {
) )
} }
await manager.transaction(async (transactionManager) => { await inventoryService.deleteInventoryLevel(id, location_id)
await inventoryService
.withTransaction(transactionManager)
.deleteInventoryLevel(id, location_id)
})
const inventoryItem = await inventoryService.retrieveInventoryItem( const inventoryItem = await inventoryService.retrieveInventoryItem(
id, id,
@@ -77,14 +77,10 @@ export default async (req: Request, res: Response) => {
req.scope.resolve("inventoryService") req.scope.resolve("inventoryService")
const manager: EntityManager = req.scope.resolve("manager") const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (transactionManager) => { await inventoryService.updateInventoryItem(
await inventoryService id,
.withTransaction(transactionManager) req.validatedBody as AdminPostInventoryItemsInventoryItemReq
.updateInventoryItem( )
id,
req.validatedBody as AdminPostInventoryItemsInventoryItemReq
)
})
const inventoryItem = await inventoryService.retrieveInventoryItem( const inventoryItem = await inventoryService.retrieveInventoryItem(
id, id,
@@ -81,11 +81,7 @@ export default async (req: Request, res: Response) => {
const validatedBody = const validatedBody =
req.validatedBody as AdminPostInventoryItemsItemLocationLevelsLevelReq req.validatedBody as AdminPostInventoryItemsItemLocationLevelsLevelReq
await manager.transaction(async (transactionManager) => { await inventoryService.updateInventoryLevel(id, location_id, validatedBody)
await inventoryService
.withTransaction(transactionManager)
.updateInventoryLevel(id, location_id, validatedBody)
})
const inventoryItem = await inventoryService.retrieveInventoryItem( const inventoryItem = await inventoryService.retrieveInventoryItem(
id, id,
@@ -1,3 +1,14 @@
import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import { ulid } from "ulid"
import { IInventoryService } from "../../../../../interfaces"
import { ProductVariant } from "../../../../../models"
import {
ProductVariantInventoryService,
ProductVariantService,
} from "../../../../../services"
import { InventoryItemDTO } from "../../../../../types/inventory"
import { CreateProductVariantInput } from "../../../../../types/product-variant"
import { import {
DistributedTransaction, DistributedTransaction,
TransactionHandlerType, TransactionHandlerType,
@@ -6,17 +17,6 @@ import {
TransactionState, TransactionState,
TransactionStepsDefinition, TransactionStepsDefinition,
} from "../../../../../utils/transaction" } from "../../../../../utils/transaction"
import { ulid } from "ulid"
import { EntityManager } from "typeorm"
import { IInventoryService } from "../../../../../interfaces"
import {
ProductVariantInventoryService,
ProductVariantService,
} from "../../../../../services"
import { CreateProductVariantInput } from "../../../../../types/product-variant"
import { InventoryItemDTO } from "../../../../../types/inventory"
import { ProductVariant } from "../../../../../models"
import { MedusaError } from "medusa-core-utils"
enum actions { enum actions {
createVariant = "createVariant", createVariant = "createVariant",
@@ -1,6 +1,5 @@
import { IsNumber, IsObject, IsOptional, IsString } from "class-validator" import { IsNumber, IsObject, IsOptional, IsString } from "class-validator"
import { isDefined } from "medusa-core-utils" import { isDefined } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import { IInventoryService } from "../../../../interfaces" import { IInventoryService } from "../../../../interfaces"
import { validateUpdateReservationQuantity } from "./utils/validate-reservation-quantity" import { validateUpdateReservationQuantity } from "./utils/validate-reservation-quantity"
@@ -66,8 +65,6 @@ import { validateUpdateReservationQuantity } from "./utils/validate-reservation-
export default async (req, res) => { export default async (req, res) => {
const { validatedBody } = req as { validatedBody: AdminPostReservationsReq } const { validatedBody } = req as { validatedBody: AdminPostReservationsReq }
const manager: EntityManager = req.scope.resolve("manager")
const inventoryService: IInventoryService = const inventoryService: IInventoryService =
req.scope.resolve("inventoryService") req.scope.resolve("inventoryService")
@@ -82,11 +79,9 @@ export default async (req, res) => {
) )
} }
const reservation = await manager.transaction(async (manager) => { const reservation = await inventoryService.createReservationItem(
return await inventoryService validatedBody
.withTransaction(manager) )
.createReservationItem(validatedBody)
})
res.status(200).json({ reservation }) res.status(200).json({ reservation })
} }
@@ -58,9 +58,7 @@ export default async (req, res) => {
req.scope.resolve("inventoryService") req.scope.resolve("inventoryService")
const manager: EntityManager = req.scope.resolve("manager") const manager: EntityManager = req.scope.resolve("manager")
await manager.transaction(async (manager) => { await inventoryService.deleteReservationItem(id)
await inventoryService.withTransaction(manager).deleteReservationItem(id)
})
res.json({ res.json({
id, id,
@@ -91,9 +91,7 @@ export default async (req, res) => {
} }
const result = await manager.transaction(async (manager) => { const result = await manager.transaction(async (manager) => {
await inventoryService await inventoryService.updateReservationItem(id, validatedBody)
.withTransaction(manager)
.updateReservationItem(id, validatedBody)
}) })
res.status(200).json({ reservation: result }) res.status(200).json({ reservation: result })
@@ -79,12 +79,8 @@ export default async (req, res) => {
if (inventoryService) { if (inventoryService) {
await Promise.all([ await Promise.all([
inventoryService inventoryService.deleteInventoryItemLevelByLocationId(id),
.withTransaction(transactionManager) inventoryService.deleteReservationItemByLocationId(id),
.deleteInventoryItemLevelByLocationId(id),
inventoryService
.withTransaction(transactionManager)
.deleteReservationItemByLocationId(id),
]) ])
} }
}) })
+8 -1
View File
@@ -3,7 +3,11 @@ import featureFlagLoader from "../loaders/feature-flags"
import Logger from "../loaders/logger" import Logger from "../loaders/logger"
import databaseLoader from "../loaders/database" import databaseLoader from "../loaders/database"
import configModuleLoader from "../loaders/config" import configModuleLoader from "../loaders/config"
import getMigrations, { getModuleSharedResources } from "./utils/get-migrations" import getMigrations, {
getModuleSharedResources,
revertIsolatedModulesMigration,
runIsolatedModulesMigration,
} from "./utils/get-migrations"
const getDataSource = async (directory) => { const getDataSource = async (directory) => {
const configModule = configModuleLoader(directory) const configModule = configModuleLoader(directory)
@@ -34,16 +38,19 @@ const main = async function ({ directory }) {
args.shift() args.shift()
args.shift() args.shift()
const configModule = configModuleLoader(directory)
const dataSource = await getDataSource(directory) const dataSource = await getDataSource(directory)
if (args[0] === "run") { if (args[0] === "run") {
await dataSource.runMigrations() await dataSource.runMigrations()
await dataSource.destroy() await dataSource.destroy()
await runIsolatedModulesMigration(configModule)
Logger.info("Migrations completed.") Logger.info("Migrations completed.")
process.exit() process.exit()
} else if (args[0] === "revert") { } else if (args[0] === "revert") {
await dataSource.undoLastMigration({ transaction: "all" }) await dataSource.undoLastMigration({ transaction: "all" })
await dataSource.destroy() await dataSource.destroy()
await revertIsolatedModulesMigration(configModule)
Logger.info("Migrations reverted.") Logger.info("Migrations reverted.")
process.exit() process.exit()
} else if (args[0] === "show") { } else if (args[0] === "show") {
+6 -4
View File
@@ -4,7 +4,7 @@ import { sync as existsSync } from "fs-exists-cached"
import { getConfigFile } from "medusa-core-utils" import { getConfigFile } from "medusa-core-utils"
import { track } from "medusa-telemetry" import { track } from "medusa-telemetry"
import path from "path" import path from "path"
import { ConnectionOptions, createConnection } from "typeorm" import { DataSource, DataSourceOptions } from "typeorm"
import loaders from "../loaders" import loaders from "../loaders"
import { handleConfigError } from "../loaders/config" import { handleConfigError } from "../loaders/config"
@@ -73,12 +73,14 @@ const seed = async function ({ directory, migrate, seedFile }: SeedOptions) {
extra: configModule.projectConfig.database_extra || {}, extra: configModule.projectConfig.database_extra || {},
migrations: coreMigrations.concat(moduleMigrations), migrations: coreMigrations.concat(moduleMigrations),
logging: true, logging: true,
} as ConnectionOptions } as DataSourceOptions
const connection = await createConnection(connectionOptions) const connection = new DataSource(connectionOptions)
await connection.initialize()
await connection.runMigrations() await connection.runMigrations()
await connection.close() await connection.destroy()
Logger.info("Migrations completed.") Logger.info("Migrations completed.")
} }
@@ -1,4 +1,4 @@
import { registerModules } from "@medusajs/modules-sdk" import { MedusaModule, registerModules } from "@medusajs/modules-sdk"
import fs from "fs" import fs from "fs"
import { sync as existsSync } from "fs-exists-cached" import { sync as existsSync } from "fs-exists-cached"
import glob from "glob" import glob from "glob"
@@ -96,7 +96,7 @@ function resolvePlugin(pluginName) {
export function getInternalModules(configModule) { export function getInternalModules(configModule) {
const modules = [] const modules = []
const moduleResolutions = registerModules(configModule) const moduleResolutions = registerModules(configModule.modules)
for (const moduleResolution of Object.values(moduleResolutions)) { for (const moduleResolution of Object.values(moduleResolutions)) {
if ( if (
@@ -200,7 +200,6 @@ export const getModuleMigrations = (configModule, isFlagEnabled) => {
for (const loadedModule of loadedModules) { for (const loadedModule of loadedModules) {
const mod = loadedModule.loadedModule const mod = loadedModule.loadedModule
const isolatedMigrations = {}
const moduleMigrations = (mod.migrations ?? []) const moduleMigrations = (mod.migrations ?? [])
.map((migration) => { .map((migration) => {
if ( if (
@@ -218,7 +217,6 @@ export const getModuleMigrations = (configModule, isFlagEnabled) => {
moduleDeclaration: loadedModule.moduleDeclaration, moduleDeclaration: loadedModule.moduleDeclaration,
models: mod.models ?? [], models: mod.models ?? [],
migrations: moduleMigrations, migrations: moduleMigrations,
externalMigrations: isolatedMigrations,
}) })
} }
@@ -249,3 +247,43 @@ export const getModuleSharedResources = (configModule, featureFlagsRouter) => {
migrations, migrations,
} }
} }
export const runIsolatedModulesMigration = async (configModule) => {
const moduleResolutions = registerModules(configModule.modules)
for (const moduleResolution of Object.values(moduleResolutions)) {
if (
!moduleResolution.resolutionPath ||
moduleResolution.moduleDeclaration.scope !== "internal" ||
moduleResolution.moduleDeclaration.resources !== "isolated"
) {
continue
}
await MedusaModule.migrateUp(
moduleResolution.definition.key,
moduleResolution.resolutionPath,
moduleResolution.options
)
}
}
export const revertIsolatedModulesMigration = async (configModule) => {
const moduleResolutions = registerModules(configModule.modules)
for (const moduleResolution of Object.values(moduleResolutions)) {
if (
!moduleResolution.resolutionPath ||
moduleResolution.moduleDeclaration.scope !== "internal" ||
moduleResolution.moduleDeclaration.resources !== "isolated"
) {
continue
}
await MedusaModule.migrateDown(
moduleResolution.definition.key,
moduleResolution.resolutionPath,
moduleResolution.options
)
}
}
@@ -1,4 +1,3 @@
import { EntityManager } from "typeorm"
import { FindConfig } from "../../types/common" import { FindConfig } from "../../types/common"
import { import {
@@ -16,8 +15,6 @@ import {
} from "../../types/inventory" } from "../../types/inventory"
export interface IInventoryService { export interface IInventoryService {
withTransaction(transactionManager?: EntityManager): this
listInventoryItems( listInventoryItems(
selector: FilterableInventoryItemProps, selector: FilterableInventoryItemProps,
config?: FindConfig<InventoryItemDTO> config?: FindConfig<InventoryItemDTO>
+1 -1
View File
@@ -91,7 +91,7 @@ export default async ({
track("MODULES_INIT_STARTED") track("MODULES_INIT_STARTED")
await moduleLoader({ await moduleLoader({
container, container,
moduleResolutions: registerModules(configModule), moduleResolutions: registerModules(configModule?.modules),
logger: Logger, logger: Logger,
}) })
const modAct = Logger.success(modulesActivity, "Modules initialized") || {} const modAct = Logger.success(modulesActivity, "Modules initialized") || {}
@@ -24,11 +24,18 @@ describe("EventBusService", () => {
find: () => Promise.resolve([]), find: () => Promise.resolve([]),
}) })
eventBus = new EventBusService({ eventBus = new EventBusService(
manager: MockManager, {
stagedJobRepository, manager: MockManager,
logger: loggerMock, stagedJobRepository,
}) logger: loggerMock,
},
{
projectConfig: {
redis_url: "localhost",
},
}
)
}) })
afterAll(async () => { afterAll(async () => {
@@ -49,10 +56,17 @@ describe("EventBusService", () => {
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks() jest.resetAllMocks()
eventBus = new EventBusService({ eventBus = new EventBusService(
manager: MockManager, {
logger: loggerMock, manager: MockManager,
}) logger: loggerMock,
},
{
projectConfig: {
redis_url: "localhost",
},
}
)
}) })
afterAll(async () => { afterAll(async () => {
@@ -135,11 +149,18 @@ describe("EventBusService", () => {
create: (data) => data, create: (data) => data,
}) })
eventBus = new EventBusService({ eventBus = new EventBusService(
logger: loggerMock, {
manager: mockManager, logger: loggerMock,
stagedJobRepository, manager: mockManager,
}) stagedJobRepository,
},
{
projectConfig: {
redis_url: "localhost",
},
}
)
eventBus.queue_.addBulk.mockImplementationOnce(() => "hi") eventBus.queue_.addBulk.mockImplementationOnce(() => "hi")
}) })
@@ -195,11 +216,18 @@ describe("EventBusService", () => {
create: (data) => data, create: (data) => data,
}) })
eventBus = new EventBusService({ eventBus = new EventBusService(
logger: loggerMock, {
manager: mockManager, logger: loggerMock,
stagedJobRepository, manager: mockManager,
}) stagedJobRepository,
},
{
projectConfig: {
redis_url: "localhost",
},
}
)
eventBus.queue_.addBulk.mockImplementationOnce(() => "hi") eventBus.queue_.addBulk.mockImplementationOnce(() => "hi")
}) })
@@ -285,7 +313,10 @@ describe("EventBusService", () => {
stagedJobRepository, stagedJobRepository,
}, },
{ {
projectConfig: { event_options: { removeOnComplete: 10 } }, projectConfig: {
event_options: { removeOnComplete: 10 },
redis_url: "localhost",
},
} }
) )
@@ -323,11 +354,18 @@ describe("EventBusService", () => {
create: (data) => data, create: (data) => data,
}) })
eventBus = new EventBusService({ eventBus = new EventBusService(
logger: loggerMock, {
manager: mockManager, logger: loggerMock,
stagedJobRepository, manager: mockManager,
}) stagedJobRepository,
},
{
projectConfig: {
redis_url: "localhost",
},
}
)
eventBus.queue_.addBulk.mockImplementationOnce(() => "hi") eventBus.queue_.addBulk.mockImplementationOnce(() => "hi")
@@ -370,7 +408,10 @@ describe("EventBusService", () => {
stagedJobRepository, stagedJobRepository,
}, },
{ {
projectConfig: { event_options: { removeOnComplete: 10 } }, projectConfig: {
event_options: { removeOnComplete: 10 },
redis_url: "localhost",
},
} }
) )
@@ -418,14 +459,11 @@ describe("EventBusService", () => {
find: () => Promise.resolve([]), find: () => Promise.resolve([]),
}) })
eventBus = new EventBusService( eventBus = new EventBusService({
{ manager: MockManager,
manager: MockManager, stagedJobRepository,
stagedJobRepository, logger: loggerMock,
logger: loggerMock, })
},
{}
)
eventBus.subscribe("eventName", () => Promise.resolve("hi")) eventBus.subscribe("eventName", () => Promise.resolve("hi"))
result = await eventBus.worker_({ result = await eventBus.worker_({
data: { eventName: "eventName", data: {} }, data: { eventName: "eventName", data: {} },
+3 -1
View File
@@ -308,7 +308,9 @@ export default class EventBusService {
return (!isBulkEmit ? stagedJobs[0] : stagedJobs) as unknown as TResult return (!isBulkEmit ? stagedJobs[0] : stagedJobs) as unknown as TResult
} }
await this.queue_.addBulk(events) if (this.config_?.projectConfig?.redis_url) {
await this.queue_.addBulk(events)
}
} }
startEnqueuer(): void { startEnqueuer(): void {
+18 -16
View File
@@ -1,3 +1,4 @@
import { isDefined, MedusaError } from "medusa-core-utils"
import { import {
Brackets, Brackets,
EntityManager, EntityManager,
@@ -9,37 +10,36 @@ import {
IsNull, IsNull,
SelectQueryBuilder, SelectQueryBuilder,
} from "typeorm" } from "typeorm"
import {
CreateProductVariantInput,
FilterableProductVariantProps,
GetRegionPriceContext,
ProductVariantPrice,
UpdateProductVariantInput,
} from "../types/product-variant"
import {
FindWithRelationsOptions,
ProductVariantRepository,
} from "../repositories/product-variant"
import { import {
IPriceSelectionStrategy, IPriceSelectionStrategy,
PriceSelectionContext, PriceSelectionContext,
TransactionBaseService, TransactionBaseService,
} from "../interfaces" } from "../interfaces"
import { MedusaError, isDefined } from "medusa-core-utils"
import { import {
MoneyAmount, MoneyAmount,
Product, Product,
ProductOptionValue, ProductOptionValue,
ProductVariant, ProductVariant,
} from "../models" } from "../models"
import {
FindWithRelationsOptions,
ProductVariantRepository,
} from "../repositories/product-variant"
import {
CreateProductVariantInput,
FilterableProductVariantProps,
GetRegionPriceContext,
ProductVariantPrice,
UpdateProductVariantInput,
} from "../types/product-variant"
import { buildQuery, buildRelations, setMetadata } from "../utils" import { buildQuery, buildRelations, setMetadata } from "../utils"
import { CartRepository } from "../repositories/cart" import { CartRepository } from "../repositories/cart"
import EventBusService from "./event-bus"
import { FindConfig } from "../types/common"
import { MoneyAmountRepository } from "../repositories/money-amount" import { MoneyAmountRepository } from "../repositories/money-amount"
import { ProductOptionValueRepository } from "../repositories/product-option-value"
import { ProductRepository } from "../repositories/product" import { ProductRepository } from "../repositories/product"
import { ProductOptionValueRepository } from "../repositories/product-option-value"
import { FindConfig } from "../types/common"
import EventBusService from "./event-bus"
import RegionService from "./region" import RegionService from "./region"
class ProductVariantService extends TransactionBaseService { class ProductVariantService extends TransactionBaseService {
@@ -173,7 +173,9 @@ class ProductVariantService extends TransactionBaseService {
"options", "options",
]), ]),
})) as Product })) as Product
} else if (!product.id) { }
if (!product?.id) {
throw new MedusaError( throw new MedusaError(
MedusaError.Types.INVALID_DATA, MedusaError.Types.INVALID_DATA,
`Product id missing` `Product id missing`
@@ -42,9 +42,10 @@ class SalesChannelInventoryService extends TransactionBaseService {
.withTransaction(this.activeManager_) .withTransaction(this.activeManager_)
.listLocationIds(salesChannelId) .listLocationIds(salesChannelId)
return await this.inventoryService_ return await this.inventoryService_.retrieveAvailableQuantity(
.withTransaction(this.activeManager_) inventoryItemId,
.retrieveAvailableQuantity(inventoryItemId, locationIds) locationIds
)
} }
} }
@@ -18,8 +18,8 @@ type InjectedDependencies = {
class SalesChannelLocationService extends TransactionBaseService { class SalesChannelLocationService extends TransactionBaseService {
protected readonly salesChannelService_: SalesChannelService protected readonly salesChannelService_: SalesChannelService
protected readonly eventBusService: EventBusService protected readonly eventBusService_: EventBusService
protected readonly stockLocationService: IStockLocationService protected readonly stockLocationService_: IStockLocationService
constructor({ constructor({
salesChannelService, salesChannelService,
@@ -30,8 +30,8 @@ class SalesChannelLocationService extends TransactionBaseService {
super(arguments[0]) super(arguments[0])
this.salesChannelService_ = salesChannelService this.salesChannelService_ = salesChannelService
this.eventBusService = eventBusService this.eventBusService_ = eventBusService
this.stockLocationService = stockLocationService this.stockLocationService_ = stockLocationService
} }
/** /**
@@ -77,9 +77,9 @@ class SalesChannelLocationService extends TransactionBaseService {
.withTransaction(this.activeManager_) .withTransaction(this.activeManager_)
.retrieve(salesChannelId) .retrieve(salesChannelId)
if (this.stockLocationService) { if (this.stockLocationService_) {
// trhows error if not found // trhows error if not found
await this.stockLocationService.retrieve(locationId) await this.stockLocationService_.retrieve(locationId)
} }
const salesChannelLocation = this.activeManager_.create( const salesChannelLocation = this.activeManager_.create(
@@ -129,7 +129,7 @@ class SalesChannelLocationService extends TransactionBaseService {
*/ */
async listSalesChannelIds(locationId: string): Promise<string[]> { async listSalesChannelIds(locationId: string): Promise<string[]> {
const manager = this.transactionManager_ || this.manager_ const manager = this.transactionManager_ || this.manager_
const location = await this.stockLocationService.retrieve(locationId) const location = await this.stockLocationService_.retrieve(locationId)
const salesChannelLocations = await manager.find(SalesChannelLocation, { const salesChannelLocations = await manager.find(SalesChannelLocation, {
where: { location_id: location.id }, where: { location_id: location.id },
+11 -3
View File
@@ -1,6 +1,9 @@
import { InternalModuleDeclaration } from "@medusajs/modules-sdk" import {
import { MedusaContainer as coreMedusaContainer } from "medusa-core-utils" ExternalModuleDeclaration,
InternalModuleDeclaration,
} from "@medusajs/modules-sdk"
import { Request } from "express" import { Request } from "express"
import { MedusaContainer as coreMedusaContainer } from "medusa-core-utils"
import { LoggerOptions } from "typeorm" import { LoggerOptions } from "typeorm"
import { Logger as _Logger } from "winston" import { Logger as _Logger } from "winston"
import { Customer, User } from "../models" import { Customer, User } from "../models"
@@ -91,7 +94,12 @@ export type ConfigModule = {
admin_cors?: string admin_cors?: string
} }
featureFlags: Record<string, boolean | string> featureFlags: Record<string, boolean | string>
modules?: Record<string, false | string | Partial<InternalModuleDeclaration>> modules?: Record<
string,
| false
| string
| Partial<InternalModuleDeclaration | ExternalModuleDeclaration>
>
plugins: ( plugins: (
| { | {
resolve: string resolve: string
+1
View File
@@ -1,4 +1,5 @@
export * from "./definitions" export * from "./definitions"
export * from "./loaders" export * from "./loaders"
export * from "./medusa-module"
export * from "./module-helper" export * from "./module-helper"
export * from "./types" export * from "./types"
@@ -1,6 +1,6 @@
import { EOL } from "os"
import { AwilixContainer, ClassOrFunctionReturning, Resolver } from "awilix" import { AwilixContainer, ClassOrFunctionReturning, Resolver } from "awilix"
import { createMedusaContainer } from "medusa-core-utils" import { createMedusaContainer } from "medusa-core-utils"
import { EOL } from "os"
import { import {
ModuleResolution, ModuleResolution,
MODULE_RESOURCE_TYPE, MODULE_RESOURCE_TYPE,
@@ -155,7 +155,7 @@ describe("modules loader", () => {
await moduleLoader({ container, moduleResolutions, logger }) await moduleLoader({ container, moduleResolutions, logger })
expect(logger.warn).toHaveBeenCalledWith( expect(logger.warn).toHaveBeenCalledWith(
`Could not resolve module: TestService. Error: No service found in module. Make sure your module exports at least one service.${EOL}` `Could not resolve module: TestService. Error: No service found in module. Make sure your module exports a service.${EOL}`
) )
}) })
@@ -186,7 +186,7 @@ describe("modules loader", () => {
await moduleLoader({ container, moduleResolutions, logger }) await moduleLoader({ container, moduleResolutions, logger })
} catch (err) { } catch (err) {
expect(err.message).toEqual( expect(err.message).toEqual(
"No service found in module. Make sure your module exports at least one service." "No service found in module. Make sure your module exports a service."
) )
} }
}) })
@@ -1,3 +1,4 @@
import MODULE_DEFINITIONS from "../../definitions"
import { import {
InternalModuleDeclaration, InternalModuleDeclaration,
ModuleDefinition, ModuleDefinition,
@@ -5,7 +6,6 @@ import {
MODULE_SCOPE, MODULE_SCOPE,
} from "../../types" } from "../../types"
import { registerModules } from "../register-modules" import { registerModules } from "../register-modules"
import MODULE_DEFINITIONS from "../../definitions"
const RESOLVED_PACKAGE = "@medusajs/test-service-resolved" const RESOLVED_PACKAGE = "@medusajs/test-service-resolved"
jest.mock("resolve-cwd", () => jest.fn(() => RESOLVED_PACKAGE)) jest.mock("resolve-cwd", () => jest.fn(() => RESOLVED_PACKAGE))
@@ -35,7 +35,7 @@ describe("module definitions loader", () => {
it("Resolves module with default definition given empty config", () => { it("Resolves module with default definition given empty config", () => {
MODULE_DEFINITIONS.push({ ...defaultDefinition }) MODULE_DEFINITIONS.push({ ...defaultDefinition })
const res = registerModules({ modules: {} }) const res = registerModules({})
expect(res[defaultDefinition.key]).toEqual( expect(res[defaultDefinition.key]).toEqual(
expect.objectContaining({ expect.objectContaining({
@@ -54,9 +54,7 @@ describe("module definitions loader", () => {
it("Resolves module with no resolution path when given false", () => { it("Resolves module with no resolution path when given false", () => {
MODULE_DEFINITIONS.push({ ...defaultDefinition }) MODULE_DEFINITIONS.push({ ...defaultDefinition })
const res = registerModules({ const res = registerModules({ [defaultDefinition.key]: false })
modules: { [defaultDefinition.key]: false },
})
expect(res[defaultDefinition.key]).toEqual( expect(res[defaultDefinition.key]).toEqual(
expect.objectContaining({ expect.objectContaining({
@@ -72,9 +70,7 @@ describe("module definitions loader", () => {
MODULE_DEFINITIONS.push({ ...defaultDefinition, isRequired: true }) MODULE_DEFINITIONS.push({ ...defaultDefinition, isRequired: true })
try { try {
registerModules({ registerModules({ [defaultDefinition.key]: false })
modules: { [defaultDefinition.key]: false },
})
} catch (err) { } catch (err) {
expect(err.message).toEqual( expect(err.message).toEqual(
`Module: ${defaultDefinition.label} is required` `Module: ${defaultDefinition.label} is required`
@@ -90,9 +86,7 @@ describe("module definitions loader", () => {
MODULE_DEFINITIONS.push(definition) MODULE_DEFINITIONS.push(definition)
const res = registerModules({ const res = registerModules({})
modules: {},
})
expect(res[defaultDefinition.key]).toEqual( expect(res[defaultDefinition.key]).toEqual(
expect.objectContaining({ expect.objectContaining({
@@ -113,9 +107,7 @@ describe("module definitions loader", () => {
MODULE_DEFINITIONS.push({ ...defaultDefinition }) MODULE_DEFINITIONS.push({ ...defaultDefinition })
const res = registerModules({ const res = registerModules({
modules: { [defaultDefinition.key]: defaultDefinition.defaultPackage,
[defaultDefinition.key]: defaultDefinition.defaultPackage,
},
}) })
expect(res[defaultDefinition.key]).toEqual( expect(res[defaultDefinition.key]).toEqual(
@@ -137,13 +129,11 @@ describe("module definitions loader", () => {
MODULE_DEFINITIONS.push({ ...defaultDefinition }) MODULE_DEFINITIONS.push({ ...defaultDefinition })
const res = registerModules({ const res = registerModules({
modules: { [defaultDefinition.key]: {
[defaultDefinition.key]: { scope: MODULE_SCOPE.INTERNAL,
scope: MODULE_SCOPE.INTERNAL, resolve: defaultDefinition.defaultPackage,
resolve: defaultDefinition.defaultPackage, resources: MODULE_RESOURCE_TYPE.ISOLATED,
resources: MODULE_RESOURCE_TYPE.ISOLATED, } as InternalModuleDeclaration,
} as InternalModuleDeclaration,
},
}) })
expect(res[defaultDefinition.key]).toEqual( expect(res[defaultDefinition.key]).toEqual(
@@ -164,10 +154,8 @@ describe("module definitions loader", () => {
MODULE_DEFINITIONS.push({ ...defaultDefinition }) MODULE_DEFINITIONS.push({ ...defaultDefinition })
const res = registerModules({ const res = registerModules({
modules: { [defaultDefinition.key]: {
[defaultDefinition.key]: { options: { test: 123 },
options: { test: 123 },
},
}, },
} as any) } as any)
@@ -189,13 +177,11 @@ describe("module definitions loader", () => {
MODULE_DEFINITIONS.push({ ...defaultDefinition }) MODULE_DEFINITIONS.push({ ...defaultDefinition })
const res = registerModules({ const res = registerModules({
modules: { [defaultDefinition.key]: {
[defaultDefinition.key]: { resolve: defaultDefinition.defaultPackage,
resolve: defaultDefinition.defaultPackage, options: { test: 123 },
options: { test: 123 }, scope: "internal",
scope: "internal", resources: "isolated",
resources: "isolated",
},
}, },
} as any) } as any)
@@ -1,17 +1,22 @@
import resolveCwd from "resolve-cwd" import resolveCwd from "resolve-cwd"
import MODULE_DEFINITIONS from "../definitions"
import { import {
MedusaModuleConfig, ExternalModuleDeclaration,
InternalModuleDeclaration, InternalModuleDeclaration,
ModuleDefinition, ModuleDefinition,
ModuleResolution, ModuleResolution,
MODULE_SCOPE, MODULE_SCOPE,
} from "../types" } from "../types"
import MODULE_DEFINITIONS from "../definitions"
export const registerModules = ({ export const registerModules = (
modules, modules?: Record<
}: MedusaModuleConfig): Record<string, ModuleResolution> => { string,
| false
| string
| Partial<InternalModuleDeclaration | ExternalModuleDeclaration>
>
): Record<string, ModuleResolution> => {
const moduleResolutions = {} as Record<string, ModuleResolution> const moduleResolutions = {} as Record<string, ModuleResolution>
const projectModules = modules ?? {} const projectModules = modules ?? {}
@@ -26,10 +31,32 @@ export const registerModules = ({
moduleResolutions[definition.key] = getInternalModuleResolution( moduleResolutions[definition.key] = getInternalModuleResolution(
definition, definition,
projectModules[definition.key] as customConfig as InternalModuleDeclaration
| InternalModuleDeclaration )
| false }
| string
return moduleResolutions
}
export const registerMedusaModule = (
moduleKey: string,
moduleDeclaration: InternalModuleDeclaration | ExternalModuleDeclaration
): Record<string, ModuleResolution> => {
const moduleResolutions = {} as Record<string, ModuleResolution>
for (const definition of MODULE_DEFINITIONS) {
if (definition.key !== moduleKey) {
continue
}
if (moduleDeclaration.scope === MODULE_SCOPE.EXTERNAL) {
// TODO: getExternalModuleResolution(...)a
throw new Error("External Modules are not supported yet.")
}
moduleResolutions[definition.key] = getInternalModuleResolution(
definition,
moduleDeclaration as InternalModuleDeclaration
) )
} }
@@ -36,7 +36,7 @@ export async function loadInternalModule(
return { return {
error: new Error( error: new Error(
"No service found in module. Make sure your module exports at least one service." "No service found in module. Make sure your module exports a service."
), ),
} }
} }
@@ -112,3 +112,15 @@ export async function loadInternalModule(
"module" "module"
) )
} }
export async function loadModuleMigrations(
resolution: ModuleResolution
): Promise<[Function | undefined, Function | undefined]> {
let loadedModule: ModuleExports
try {
loadedModule = (await import(resolution.resolutionPath as string)).default
return [loadedModule.runMigrations, loadedModule.revertMigration]
} catch {
return [undefined, undefined]
}
}
+108
View File
@@ -0,0 +1,108 @@
import { asValue } from "awilix"
import { createMedusaContainer } from "medusa-core-utils"
import { moduleLoader, registerMedusaModule } from "./loaders"
import { loadModuleMigrations } from "./loaders/utils"
import {
ExternalModuleDeclaration,
InternalModuleDeclaration,
MODULE_RESOURCE_TYPE,
MODULE_SCOPE,
} from "./types"
const logger: any = {
log: (a) => console.log(a),
info: (a) => console.log(a),
warn: (a) => console.warn(a),
error: (a) => console.error(a),
}
export class MedusaModule {
public static async bootstrap(
moduleKey: string,
defaultPath: string,
declaration?: InternalModuleDeclaration | ExternalModuleDeclaration,
injectedDependencies?: Record<string, any>
): Promise<{
[key: string]: any
}> {
let modDeclaration = declaration
if (declaration?.scope !== MODULE_SCOPE.EXTERNAL) {
modDeclaration = {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: defaultPath,
options: declaration,
}
}
const container = createMedusaContainer()
if (injectedDependencies) {
for (const service in injectedDependencies) {
container.register(service, asValue(injectedDependencies[service]))
}
}
const moduleResolutions = registerMedusaModule(moduleKey, modDeclaration!)
await moduleLoader({ container, moduleResolutions, logger })
const services = {}
for (const resolution of Object.values(moduleResolutions)) {
const registrationName = resolution.definition.registrationName
services[registrationName] = container.resolve(registrationName)
}
return services
}
public static async migrateUp(
moduleKey: string,
modulePath: string,
options?: Record<string, any>
): Promise<void> {
const moduleResolutions = registerMedusaModule(moduleKey, {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: modulePath,
options,
})
for (const mod in moduleResolutions) {
const [migrateUp] = await loadModuleMigrations(moduleResolutions[mod])
if (typeof migrateUp === "function") {
await migrateUp({
options,
logger,
})
}
}
}
public static async migrateDown(
moduleKey: string,
modulePath: string,
options?: Record<string, any>
): Promise<void> {
const moduleResolutions = registerMedusaModule(moduleKey, {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.ISOLATED,
resolve: modulePath,
options,
})
for (const mod in moduleResolutions) {
const [, migrateDown] = await loadModuleMigrations(moduleResolutions[mod])
if (typeof migrateDown === "function") {
await migrateDown({
options,
logger,
})
}
}
}
}
+10 -11
View File
@@ -68,9 +68,9 @@ export type ModuleDefinition = {
| ExternalModuleDeclaration | ExternalModuleDeclaration
} }
export type LoaderOptions = { export type LoaderOptions<TOptions = Record<string, unknown>> = {
container: MedusaContainer container: MedusaContainer
options?: Record<string, unknown> options?: TOptions
logger?: Logger logger?: Logger
} }
@@ -89,13 +89,12 @@ export type ModuleExports = {
loaders?: ModuleLoaderFunction[] loaders?: ModuleLoaderFunction[]
migrations?: any[] migrations?: any[]
models?: Constructor<any>[] models?: Constructor<any>[]
} runMigrations?(
options: LoaderOptions,
export type MedusaModuleConfig = { moduleDeclaration: InternalModuleDeclaration
modules?: Record< ): Promise<void>
string, revertMigration?(
| false options: LoaderOptions,
| string moduleDeclaration: InternalModuleDeclaration
| Partial<InternalModuleDeclaration | ExternalModuleDeclaration> ): Promise<void>
>
} }
+4 -3
View File
@@ -1,11 +1,10 @@
import { ModuleExports } from "@medusajs/modules-sdk"
import loadConnection from "./loaders/connection" import loadConnection from "./loaders/connection"
import migrations from "./migrations" import migrations from "./migrations"
import { revertMigration, runMigrations } from "./migrations/run-migration"
import * as StockLocationModels from "./models" import * as StockLocationModels from "./models"
import StockLocationService from "./services/stock-location" import StockLocationService from "./services/stock-location"
import { ModuleExports } from "@medusajs/modules-sdk"
const service = StockLocationService const service = StockLocationService
const loaders = [loadConnection] const loaders = [loadConnection]
const models = Object.values(StockLocationModels) const models = Object.values(StockLocationModels)
@@ -15,6 +14,8 @@ const moduleDefinition: ModuleExports = {
migrations, migrations,
loaders, loaders,
models, models,
runMigrations,
revertMigration,
} }
export default moduleDefinition export default moduleDefinition
@@ -0,0 +1,24 @@
import { IEventBusService, IStockLocationService } from "@medusajs/medusa"
import {
ExternalModuleDeclaration,
InternalModuleDeclaration,
MedusaModule,
} from "@medusajs/modules-sdk"
import { StockLocationServiceInitializeOptions } from "../types"
export const initialize = async (
options?: StockLocationServiceInitializeOptions | ExternalModuleDeclaration,
injectedDependencies?: {
eventBusService: IEventBusService
}
): Promise<IStockLocationService> => {
const serviceKey = "stockLocationService"
const loaded = await MedusaModule.bootstrap(
serviceKey,
"@medusajs/stock-location",
options as InternalModuleDeclaration | ExternalModuleDeclaration,
injectedDependencies
)
return loaded[serviceKey] as IStockLocationService
}
@@ -2,13 +2,13 @@ import {
InternalModuleDeclaration, InternalModuleDeclaration,
LoaderOptions, LoaderOptions,
MODULE_RESOURCE_TYPE, MODULE_RESOURCE_TYPE,
MODULE_SCOPE, MODULE_SCOPE
} from "@medusajs/modules-sdk" } from "@medusajs/modules-sdk"
import { DataSource, DataSourceOptions } from "typeorm"
import * as StockLocationModels from "../models"
import { MedusaError } from "medusa-core-utils"
import { asValue } from "awilix" import { asValue } from "awilix"
import { MedusaError } from "medusa-core-utils"
import { DataSource, DataSourceOptions } from "typeorm"
import * as StockLocationModels from "../models"
import { StockLocationServiceInitializeOptions } from "../types"
export default async ( export default async (
{ options, container }: LoaderOptions, { options, container }: LoaderOptions,
@@ -21,7 +21,8 @@ export default async (
return return
} }
const dbData = options?.database as Record<string, string> const dbData =
options?.database as StockLocationServiceInitializeOptions["database"]
if (!dbData) { if (!dbData) {
throw new MedusaError( throw new MedusaError(
@@ -32,13 +33,13 @@ export default async (
const entities = Object.values(StockLocationModels) const entities = Object.values(StockLocationModels)
const dataSource = new DataSource({ const dataSource = new DataSource({
type: dbData.database_type, type: dbData.type,
url: dbData.database_url, url: dbData.url,
database: dbData.database_database, database: dbData.database,
extra: dbData.database_extra || {}, extra: dbData.extra || {},
schema: dbData.database_schema, schema: dbData.schema,
entities, entities,
logging: dbData.database_logging, logging: dbData.logging,
} as DataSourceOptions) } as DataSourceOptions)
await dataSource.initialize() await dataSource.initialize()
@@ -0,0 +1,65 @@
import { InternalModuleDeclaration, LoaderOptions } from "@medusajs/modules-sdk"
import { DataSource, DataSourceOptions } from "typeorm"
import { StockLocationServiceInitializeOptions } from "../types"
import migrations from "./index"
function getDataSource(
dbData: StockLocationServiceInitializeOptions["database"]
): DataSource {
return new DataSource({
type: dbData!.type,
url: dbData!.url,
database: dbData!.database,
extra: dbData!.extra || {},
migrations: migrations
.map((migration: any): Function[] => {
return Object.values(migration).filter(
(fn) => typeof fn === "function"
) as Function[]
})
.flat(),
schema: dbData!.schema,
logging: dbData!.logging,
} as DataSourceOptions)
}
export async function runMigrations(
{ options, logger }: Omit<LoaderOptions, "container">,
moduleDeclaration?: InternalModuleDeclaration
) {
const dbData =
options?.database as StockLocationServiceInitializeOptions["database"]
try {
const dataSource = getDataSource(dbData)
await dataSource.initialize()
await dataSource.runMigrations()
logger?.info("Stock Location module migration executed")
} catch (error) {
logger?.error(
`Stock Location module migration failed to run - Error: ${error}`
)
}
}
export async function revertMigration(
{ options, logger }: Omit<LoaderOptions, "container">,
moduleDeclaration?: InternalModuleDeclaration
) {
const dbData =
options?.database as StockLocationServiceInitializeOptions["database"]
try {
const dataSource = getDataSource(dbData)
await dataSource.initialize()
await dataSource.undoLastMigration()
logger?.info("Stock Location module migration reverted")
} catch (error) {
logger?.error(
`Stock Location module migration failed to revert - Error: ${error}`
)
}
}
@@ -153,11 +153,9 @@ export default class StockLocationService {
} }
const result = await locationRepo.save(loc) const result = await locationRepo.save(loc)
await this.eventBusService_ await this.eventBusService_?.emit?.(StockLocationService.Events.CREATED, {
.withTransaction(manager) id: result.id,
.emit(StockLocationService.Events.CREATED, { })
id: result.id,
})
return result return result
} }
@@ -201,11 +199,9 @@ export default class StockLocationService {
await locationRepo.save(toSave) await locationRepo.save(toSave)
await this.eventBusService_ await this.eventBusService_?.emit?.(StockLocationService.Events.UPDATED, {
.withTransaction(manager) id: stockLocationId,
.emit(StockLocationService.Events.UPDATED, { })
id: stockLocationId,
})
return item return item
} }
@@ -267,10 +263,8 @@ export default class StockLocationService {
await locationRepo.softRemove({ id }) await locationRepo.softRemove({ id })
await this.eventBusService_ await this.eventBusService_?.emit?.(StockLocationService.Events.DELETED, {
.withTransaction(manager) id,
.emit(StockLocationService.Events.DELETED, { })
id,
})
} }
} }
@@ -0,0 +1,12 @@
import { DatabaseType } from "typeorm"
export type StockLocationServiceInitializeOptions = {
database?: {
type?: DatabaseType | string
url?: string
database?: string
extra?: Record<string, any>
schema?: string
logging?: boolean
}
}
@@ -4,16 +4,16 @@ export function MedusaContext() {
propertyKey: string | symbol, propertyKey: string | symbol,
parameterIndex: number parameterIndex: number
) { ) {
if (!target.MedusaContext_) { if (!target.MedusaContextIndex_) {
target.MedusaContext_ = {} target.MedusaContextIndex_ = {}
} }
if (propertyKey in target.MedusaContext_) { if (propertyKey in target.MedusaContextIndex_) {
throw new Error( throw new Error(
`Only one MedusaContext is allowed on method "${String(propertyKey)}".` `Only one MedusaContext is allowed on method "${String(propertyKey)}".`
) )
} }
target.MedusaContext_[propertyKey] = parameterIndex target.MedusaContextIndex_[propertyKey] = parameterIndex
} }
} }
@@ -8,7 +8,7 @@ export function InjectEntityManager(
propertyKey: string | symbol, propertyKey: string | symbol,
descriptor: any descriptor: any
): void { ): void {
if (!target.MedusaContext_) { if (!target.MedusaContextIndex_) {
throw new Error( throw new Error(
`To apply @InjectEntityManager you have to flag a parameter using @MedusaContext` `To apply @InjectEntityManager you have to flag a parameter using @MedusaContext`
) )
@@ -16,7 +16,7 @@ export function InjectEntityManager(
const originalMethod = descriptor.value const originalMethod = descriptor.value
const argIndex = target.MedusaContext_[propertyKey] const argIndex = target.MedusaContextIndex_[propertyKey]
descriptor.value = async function (...args: any[]) { descriptor.value = async function (...args: any[]) {
const context: SharedContext = args[argIndex] ?? {} const context: SharedContext = args[argIndex] ?? {}
+4 -2
View File
@@ -5741,8 +5741,10 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@medusajs/inventory@workspace:packages/inventory" resolution: "@medusajs/inventory@workspace:packages/inventory"
dependencies: dependencies:
"@medusajs/medusa": "*" "@medusajs/medusa": ^1.7.7
"@medusajs/modules-sdk": "*" "@medusajs/modules-sdk": "*"
"@medusajs/types": "*"
"@medusajs/utils": ^0.0.1
awilix: ^8.0.0 awilix: ^8.0.0
cross-env: ^5.2.1 cross-env: ^5.2.1
jest: ^25.5.4 jest: ^25.5.4
@@ -5750,7 +5752,7 @@ __metadata:
typeorm: ^0.3.11 typeorm: ^0.3.11
typescript: ^4.4.4 typescript: ^4.4.4
peerDependencies: peerDependencies:
"@medusajs/medusa": 1.7.13 "@medusajs/types": ^0.0.1
languageName: unknown languageName: unknown
linkType: soft linkType: soft