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