chore: Abstract module service (#6188)
**What**
- Remove services that do not have any custom business and replace them with a simple interfaces
- Abstract module service provide the following base implementation
- retrieve
- list
- listAndCount
- delete
- softDelete
- restore
The above methods are created for the main model and also for each other models for which a config is provided
all method such as list, listAndCount, delete, softDelete and restore are pluralized with the model it refers to
**Migration**
- [x] product
- [x] pricing
- [x] promotion
- [x] cart
- [x] auth
- [x] customer
- [x] payment
- [x] Sales channel
- [x] Workflow-*
**Usage**
**Module**
The module service can now extend the ` ModulesSdkUtils.abstractModuleServiceFactory` which returns a class with the default implementation for each method and each model following the standard naming convention mentioned above.
This factory have 3 template arguments being the container, the main model DTO and an object representing the other model with a config object that contains at list the DTO and optionally a singular and plural property in case it needs to be set manually. It looks like the following:
```ts
export default class PricingModuleService</* ... */>
extends ModulesSdkUtils.abstractModuleServiceFactory<
InjectedDependencies,
PricingTypes.PriceSetDTO,
{
Currency: { dto: PricingTypes.CurrencyDTO }
MoneyAmount: { dto: PricingTypes.MoneyAmountDTO }
PriceSetMoneyAmount: { dto: PricingTypes.PriceSetMoneyAmountDTO }
PriceSetMoneyAmountRules: {
dto: PricingTypes.PriceSetMoneyAmountRulesDTO
}
PriceRule: { dto: PricingTypes.PriceRuleDTO }
RuleType: { dto: PricingTypes.RuleTypeDTO }
PriceList: { dto: PricingTypes.PriceListDTO }
PriceListRule: { dto: PricingTypes.PriceListRuleDTO }
}
>(PriceSet, generateMethodForModels, entityNameToLinkableKeysMap)
implements PricingTypes.IPricingModuleService
{
// ...
}
```
In the above, the singular and plural can be inferred as there is no tricky naming. Also, the default implementation does not remove the fact that you need to provides all the overloads etc in your module service interface. The above will provide a default implementation following the interface `AbstractModuleService` which is also auto generated, hence you will have the following methods available:
**for the main model**
- list
- retrieve
- listAndCount
- delete
- softDelete
- restore
**for the other models**
- list**MyModels**
- retrieve**MyModel**
- listAndCount**MyModels**
- delete**MyModels**
- softDelete**MyModels**
- restore**MyModels**
**Internal module service**
The internal module service can now extend `ModulesSdkUtils.internalModuleServiceFactory` which takes only one template argument which is the container type.
All internal services provides a default implementation for all retrieve, list, listAndCount, create, update, delete, softDelete, restore methods which follow the following interface `ModulesSdkTypes.InternalModuleService`:
```ts
export interface InternalModuleService<
TEntity extends {},
TContainer extends object = object
> {
get __container__(): TContainer
retrieve(
idOrObject: string,
config?: FindConfig<any>,
sharedContext?: Context
): Promise<TEntity>
retrieve(
idOrObject: object,
config?: FindConfig<any>,
sharedContext?: Context
): Promise<TEntity>
list(
filters?: FilterQuery<any> | BaseFilterable<FilterQuery<any>>,
config?: FindConfig<any>,
sharedContext?: Context
): Promise<TEntity[]>
listAndCount(
filters?: FilterQuery<any> | BaseFilterable<FilterQuery<any>>,
config?: FindConfig<any>,
sharedContext?: Context
): Promise<[TEntity[], number]>
create(data: any[], sharedContext?: Context): Promise<TEntity[]>
create(data: any, sharedContext?: Context): Promise<TEntity>
update(data: any[], sharedContext?: Context): Promise<TEntity[]>
update(data: any, sharedContext?: Context): Promise<TEntity>
update(
selectorAndData: {
selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
data: any
},
sharedContext?: Context
): Promise<TEntity[]>
update(
selectorAndData: {
selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
data: any
}[],
sharedContext?: Context
): Promise<TEntity[]>
delete(idOrSelector: string, sharedContext?: Context): Promise<void>
delete(idOrSelector: string[], sharedContext?: Context): Promise<void>
delete(idOrSelector: object, sharedContext?: Context): Promise<void>
delete(idOrSelector: object[], sharedContext?: Context): Promise<void>
delete(
idOrSelector: {
selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
},
sharedContext?: Context
): Promise<void>
softDelete(
idsOrFilter: string[] | InternalFilterQuery,
sharedContext?: Context
): Promise<[TEntity[], Record<string, unknown[]>]>
restore(
idsOrFilter: string[] | InternalFilterQuery,
sharedContext?: Context
): Promise<[TEntity[], Record<string, unknown[]>]>
upsert(data: any[], sharedContext?: Context): Promise<TEntity[]>
upsert(data: any, sharedContext?: Context): Promise<TEntity>
}
```
When a service is auto generated you can use that interface to type your class property representing the expected internal service.
**Repositories**
The repositories can now extend `DALUtils.mikroOrmBaseRepositoryFactory` which takes one template argument being the entity or the template entity and provides all the default implementation. If the repository is auto generated you can type it using the `RepositoryService` interface. Here is the new interface typings.
```ts
export interface RepositoryService<T = any> extends BaseRepositoryService<T> {
find(options?: FindOptions<T>, context?: Context): Promise<T[]>
findAndCount(
options?: FindOptions<T>,
context?: Context
): Promise<[T[], number]>
create(data: any[], context?: Context): Promise<T[]>
// Becareful here, if you have a custom internal service, the update data should never be the entity otherwise
// both entity and update will point to the same ref and create issues with mikro orm
update(data: { entity; update }[], context?: Context): Promise<T[]>
delete(
idsOrPKs: FilterQuery<T> & BaseFilterable<FilterQuery<T>>,
context?: Context
): Promise<void>
/**
* Soft delete entities and cascade to related entities if configured.
*
* @param idsOrFilter
* @param context
*
* @returns [T[], Record<string, string[]>] the second value being the map of the entity names and ids that were soft deleted
*/
softDelete(
idsOrFilter: string[] | InternalFilterQuery,
context?: Context
): Promise<[T[], Record<string, unknown[]>]>
restore(
idsOrFilter: string[] | InternalFilterQuery,
context?: Context
): Promise<[T[], Record<string, unknown[]>]>
upsert(data: any[], context?: Context): Promise<T[]>
}
```
This commit is contained in:
committed by
GitHub
parent
abc30517cb
commit
a7be5d7b6d
@@ -0,0 +1,201 @@
|
||||
import { abstractModuleServiceFactory } from "../abstract-module-service-factory"
|
||||
|
||||
const baseRepoMock = {
|
||||
serialize: jest.fn().mockImplementation((item) => item),
|
||||
transaction: (task) => task("transactionManager"),
|
||||
getFreshManager: jest.fn().mockReturnThis(),
|
||||
}
|
||||
|
||||
const defaultContext = { __type: "MedusaContext", manager: baseRepoMock }
|
||||
const defaultTransactionContext = {
|
||||
__type: "MedusaContext",
|
||||
transactionManager: "transactionManager",
|
||||
}
|
||||
|
||||
describe("Abstract Module Service Factory", () => {
|
||||
const containerMock = {
|
||||
baseRepository: baseRepoMock,
|
||||
mainModelMockRepository: baseRepoMock,
|
||||
otherModelMock1Repository: baseRepoMock,
|
||||
otherModelMock2Repository: baseRepoMock,
|
||||
mainModelMockService: {
|
||||
retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }),
|
||||
list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]),
|
||||
delete: jest.fn().mockResolvedValue(undefined),
|
||||
softDelete: jest.fn().mockResolvedValue([[], {}]),
|
||||
restore: jest.fn().mockResolvedValue([[], {}]),
|
||||
},
|
||||
otherModelMock1Service: {
|
||||
retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }),
|
||||
list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]),
|
||||
delete: jest.fn().mockResolvedValue(undefined),
|
||||
softDelete: jest.fn().mockResolvedValue([[], {}]),
|
||||
restore: jest.fn().mockResolvedValue([[], {}]),
|
||||
},
|
||||
otherModelMock2Service: {
|
||||
retrieve: jest.fn().mockResolvedValue({ id: "1", name: "Item" }),
|
||||
list: jest.fn().mockResolvedValue([{ id: "1", name: "Item" }]),
|
||||
delete: jest.fn().mockResolvedValue(undefined),
|
||||
softDelete: jest.fn().mockResolvedValue([[], {}]),
|
||||
restore: jest.fn().mockResolvedValue([[], {}]),
|
||||
},
|
||||
}
|
||||
|
||||
const mainModelMock = class MainModelMock {}
|
||||
const otherModelMock1 = class OtherModelMock1 {}
|
||||
const otherModelMock2 = class OtherModelMock2 {}
|
||||
|
||||
const abstractModuleService = abstractModuleServiceFactory<
|
||||
any,
|
||||
any,
|
||||
{
|
||||
OtherModelMock1: {
|
||||
dto: any
|
||||
singular: "OtherModelMock1"
|
||||
plural: "OtherModelMock1s"
|
||||
}
|
||||
OtherModelMock2: {
|
||||
dto: any
|
||||
singular: "OtherModelMock2"
|
||||
plural: "OtherModelMock2s"
|
||||
}
|
||||
}
|
||||
>(
|
||||
mainModelMock,
|
||||
[
|
||||
{
|
||||
model: otherModelMock1,
|
||||
plural: "otherModelMock1s",
|
||||
singular: "otherModelMock1",
|
||||
},
|
||||
{
|
||||
model: otherModelMock2,
|
||||
plural: "otherModelMock2s",
|
||||
singular: "otherModelMock2",
|
||||
},
|
||||
]
|
||||
// Add more parameters as needed
|
||||
)
|
||||
|
||||
describe("Main Model Methods", () => {
|
||||
let instance
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
instance = new abstractModuleService(containerMock)
|
||||
})
|
||||
|
||||
test("should have retrieve method", async () => {
|
||||
const result = await instance.retrieve("1")
|
||||
expect(result).toEqual({ id: "1", name: "Item" })
|
||||
expect(containerMock.mainModelMockService.retrieve).toHaveBeenCalledWith(
|
||||
"1",
|
||||
undefined,
|
||||
defaultContext
|
||||
)
|
||||
})
|
||||
|
||||
test("should have list method", async () => {
|
||||
const result = await instance.list()
|
||||
expect(result).toEqual([{ id: "1", name: "Item" }])
|
||||
expect(containerMock.mainModelMockService.list).toHaveBeenCalledWith(
|
||||
{},
|
||||
{},
|
||||
defaultContext
|
||||
)
|
||||
})
|
||||
|
||||
test("should have delete method", async () => {
|
||||
await instance.delete("1")
|
||||
expect(containerMock.mainModelMockService.delete).toHaveBeenCalledWith(
|
||||
["1"],
|
||||
defaultTransactionContext
|
||||
)
|
||||
})
|
||||
|
||||
test("should have softDelete method", async () => {
|
||||
const result = await instance.softDelete("1")
|
||||
expect(result).toEqual(undefined)
|
||||
expect(
|
||||
containerMock.mainModelMockService.softDelete
|
||||
).toHaveBeenCalledWith(["1"], defaultTransactionContext)
|
||||
})
|
||||
|
||||
test("should have restore method", async () => {
|
||||
const result = await instance.restore("1")
|
||||
expect(result).toEqual(undefined)
|
||||
expect(containerMock.mainModelMockService.restore).toHaveBeenCalledWith(
|
||||
["1"],
|
||||
defaultTransactionContext
|
||||
)
|
||||
})
|
||||
|
||||
test("should have delete method with selector", async () => {
|
||||
await instance.delete({ selector: { id: "1" } })
|
||||
expect(containerMock.mainModelMockService.delete).toHaveBeenCalledWith(
|
||||
[{ selector: { id: "1" } }],
|
||||
defaultTransactionContext
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Other Models Methods", () => {
|
||||
let instance
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
instance = new abstractModuleService(containerMock)
|
||||
})
|
||||
|
||||
test("should have retrieve method for other models", async () => {
|
||||
const result = await instance.retrieveOtherModelMock1("1")
|
||||
expect(result).toEqual({ id: "1", name: "Item" })
|
||||
expect(
|
||||
containerMock.otherModelMock1Service.retrieve
|
||||
).toHaveBeenCalledWith("1", undefined, defaultContext)
|
||||
})
|
||||
|
||||
test("should have list method for other models", async () => {
|
||||
const result = await instance.listOtherModelMock1s()
|
||||
expect(result).toEqual([{ id: "1", name: "Item" }])
|
||||
expect(containerMock.otherModelMock1Service.list).toHaveBeenCalledWith(
|
||||
{},
|
||||
{},
|
||||
defaultContext
|
||||
)
|
||||
})
|
||||
|
||||
test("should have delete method for other models", async () => {
|
||||
await instance.deleteOtherModelMock1s("1")
|
||||
expect(containerMock.otherModelMock1Service.delete).toHaveBeenCalledWith(
|
||||
["1"],
|
||||
defaultTransactionContext
|
||||
)
|
||||
})
|
||||
|
||||
test("should have softDelete method for other models", async () => {
|
||||
const result = await instance.softDeleteOtherModelMock1s("1")
|
||||
expect(result).toEqual(undefined)
|
||||
expect(
|
||||
containerMock.otherModelMock1Service.softDelete
|
||||
).toHaveBeenCalledWith(["1"], defaultTransactionContext)
|
||||
})
|
||||
|
||||
test("should have restore method for other models", async () => {
|
||||
const result = await instance.restoreOtherModelMock1s("1")
|
||||
expect(result).toEqual(undefined)
|
||||
expect(containerMock.otherModelMock1Service.restore).toHaveBeenCalledWith(
|
||||
["1"],
|
||||
defaultTransactionContext
|
||||
)
|
||||
})
|
||||
|
||||
test("should have delete method for other models with selector", async () => {
|
||||
await instance.deleteOtherModelMock1s({ selector: { id: "1" } })
|
||||
expect(containerMock.otherModelMock1Service.delete).toHaveBeenCalledWith(
|
||||
[{ selector: { id: "1" } }],
|
||||
defaultTransactionContext
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,240 @@
|
||||
import { internalModuleServiceFactory } from "../internal-module-service-factory"
|
||||
import { lowerCaseFirst } from "../../common"
|
||||
|
||||
const defaultContext = { __type: "MedusaContext" }
|
||||
|
||||
class Model {}
|
||||
describe("Internal Module Service Factory", () => {
|
||||
const modelRepositoryName = `${lowerCaseFirst(Model.name)}Repository`
|
||||
|
||||
const containerMock = {
|
||||
[modelRepositoryName]: {
|
||||
transaction: (task) => task(),
|
||||
getFreshManager: jest.fn().mockReturnThis(),
|
||||
find: jest.fn(),
|
||||
findAndCount: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
restore: jest.fn(),
|
||||
upsert: jest.fn(),
|
||||
},
|
||||
[`composite${Model.name}Repository`]: {
|
||||
transaction: (task) => task(),
|
||||
getFreshManager: jest.fn().mockReturnThis(),
|
||||
find: jest.fn(),
|
||||
findAndCount: jest.fn(),
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
restore: jest.fn(),
|
||||
upsert: jest.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
const internalModuleService = internalModuleServiceFactory<any>(Model)
|
||||
|
||||
describe("Internal Module Service Methods", () => {
|
||||
let instance
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
instance = new internalModuleService(containerMock)
|
||||
})
|
||||
|
||||
test("should throw model id undefined error on retrieve if id is not defined", async () => {
|
||||
const err = await instance.retrieve().catch((e) => e)
|
||||
expect(err.message).toBe("model - id must be defined")
|
||||
})
|
||||
|
||||
test("should throw an error on retrieve if composite key values are not defined", async () => {
|
||||
class CompositeModel {
|
||||
id: string
|
||||
name: string
|
||||
|
||||
static meta = { primaryKeys: ["id", "name"] }
|
||||
}
|
||||
|
||||
const compositeInternalModuleService =
|
||||
internalModuleServiceFactory<any>(CompositeModel)
|
||||
|
||||
const instance = new compositeInternalModuleService(containerMock)
|
||||
|
||||
const err = await instance.retrieve().catch((e) => e)
|
||||
expect(err.message).toBe("compositeModel - id, name must be defined")
|
||||
})
|
||||
|
||||
test("should throw NOT_FOUND error on retrieve if entity not found", async () => {
|
||||
containerMock[modelRepositoryName].find.mockResolvedValueOnce([])
|
||||
|
||||
const err = await instance.retrieve("1").catch((e) => e)
|
||||
expect(err.message).toBe("Model with id: 1 was not found")
|
||||
})
|
||||
|
||||
test("should retrieve entity successfully", async () => {
|
||||
const entity = { id: "1", name: "Item" }
|
||||
containerMock[modelRepositoryName].find.mockResolvedValueOnce([entity])
|
||||
|
||||
const result = await instance.retrieve("1")
|
||||
expect(result).toEqual(entity)
|
||||
})
|
||||
|
||||
test("should retrieve entity successfully with composite key", async () => {
|
||||
class CompositeModel {
|
||||
id: string
|
||||
name: string
|
||||
|
||||
static meta = { primaryKeys: ["id", "name"] }
|
||||
}
|
||||
|
||||
const compositeInternalModuleService =
|
||||
internalModuleServiceFactory<any>(CompositeModel)
|
||||
|
||||
const instance = new compositeInternalModuleService(containerMock)
|
||||
|
||||
const entity = { id: "1", name: "Item" }
|
||||
containerMock[
|
||||
`${lowerCaseFirst(CompositeModel.name)}Repository`
|
||||
].find.mockResolvedValueOnce([entity])
|
||||
|
||||
const result = await instance.retrieve({ id: "1", name: "Item" })
|
||||
expect(result).toEqual(entity)
|
||||
})
|
||||
|
||||
test("should list entities successfully", async () => {
|
||||
const entities = [
|
||||
{ id: "1", name: "Item" },
|
||||
{ id: "2", name: "Item2" },
|
||||
]
|
||||
containerMock[modelRepositoryName].find.mockResolvedValueOnce(entities)
|
||||
|
||||
const result = await instance.list()
|
||||
expect(result).toEqual(entities)
|
||||
})
|
||||
|
||||
test("should list and count entities successfully", async () => {
|
||||
const entities = [
|
||||
{ id: "1", name: "Item" },
|
||||
{ id: "2", name: "Item2" },
|
||||
]
|
||||
const count = entities.length
|
||||
containerMock[modelRepositoryName].findAndCount.mockResolvedValueOnce([
|
||||
entities,
|
||||
count,
|
||||
])
|
||||
|
||||
const result = await instance.listAndCount()
|
||||
expect(result).toEqual([entities, count])
|
||||
})
|
||||
|
||||
test("should create entity successfully", async () => {
|
||||
const entity = { id: "1", name: "Item" }
|
||||
|
||||
containerMock[modelRepositoryName].find.mockReturnValue([entity])
|
||||
|
||||
containerMock[modelRepositoryName].create.mockImplementation(
|
||||
async (entity) => entity
|
||||
)
|
||||
|
||||
const result = await instance.create(entity)
|
||||
expect(result).toEqual(entity)
|
||||
})
|
||||
|
||||
test("should create entities successfully", async () => {
|
||||
const entities = [
|
||||
{ id: "1", name: "Item" },
|
||||
{ id: "2", name: "Item2" },
|
||||
]
|
||||
|
||||
containerMock[modelRepositoryName].find.mockResolvedValueOnce([entities])
|
||||
|
||||
containerMock[modelRepositoryName].create.mockResolvedValueOnce(entities)
|
||||
|
||||
const result = await instance.create(entities)
|
||||
expect(result).toEqual(entities)
|
||||
})
|
||||
|
||||
test("should update entity successfully", async () => {
|
||||
const updateData = { id: "1", name: "UpdatedItem" }
|
||||
|
||||
containerMock[modelRepositoryName].find.mockResolvedValueOnce([
|
||||
updateData,
|
||||
])
|
||||
|
||||
containerMock[modelRepositoryName].update.mockResolvedValueOnce([
|
||||
updateData,
|
||||
])
|
||||
|
||||
const result = await instance.update(updateData)
|
||||
expect(result).toEqual([updateData])
|
||||
})
|
||||
|
||||
test("should update entities successfully", async () => {
|
||||
const updateData = { id: "1", name: "UpdatedItem" }
|
||||
const entitiesToUpdate = [{ id: "1", name: "Item" }]
|
||||
|
||||
containerMock[modelRepositoryName].find.mockResolvedValueOnce(
|
||||
entitiesToUpdate
|
||||
)
|
||||
|
||||
containerMock[modelRepositoryName].update.mockResolvedValueOnce([
|
||||
{ entity: entitiesToUpdate[0], update: updateData },
|
||||
])
|
||||
|
||||
const result = await instance.update({ selector: {}, data: updateData })
|
||||
expect(result).toEqual([
|
||||
{ entity: entitiesToUpdate[0], update: updateData },
|
||||
])
|
||||
})
|
||||
|
||||
test("should delete entity successfully", async () => {
|
||||
await instance.delete("1")
|
||||
expect(containerMock[modelRepositoryName].delete).toHaveBeenCalledWith(
|
||||
{
|
||||
$or: [
|
||||
{
|
||||
id: "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultContext
|
||||
)
|
||||
})
|
||||
|
||||
test("should delete entities successfully", async () => {
|
||||
const entitiesToDelete = [{ id: "1", name: "Item" }]
|
||||
containerMock[modelRepositoryName].find.mockResolvedValueOnce(
|
||||
entitiesToDelete
|
||||
)
|
||||
|
||||
await instance.delete({ selector: {} })
|
||||
expect(containerMock[modelRepositoryName].delete).toHaveBeenCalledWith(
|
||||
{
|
||||
$or: [
|
||||
{
|
||||
id: "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
defaultContext
|
||||
)
|
||||
})
|
||||
|
||||
test("should soft delete entity successfully", async () => {
|
||||
await instance.softDelete("1")
|
||||
expect(
|
||||
containerMock[modelRepositoryName].softDelete
|
||||
).toHaveBeenCalledWith("1", defaultContext)
|
||||
})
|
||||
|
||||
test("should restore entity successfully", async () => {
|
||||
await instance.restore("1")
|
||||
expect(containerMock[modelRepositoryName].restore).toHaveBeenCalledWith(
|
||||
"1",
|
||||
defaultContext
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,527 @@
|
||||
/**
|
||||
* Utility factory and interfaces for module service public facing API
|
||||
*/
|
||||
import {
|
||||
Constructor,
|
||||
Context,
|
||||
FindConfig,
|
||||
IEventBusModuleService,
|
||||
Pluralize,
|
||||
RepositoryService,
|
||||
RestoreReturn,
|
||||
SoftDeleteReturn,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
isString,
|
||||
kebabCase,
|
||||
lowerCaseFirst,
|
||||
mapObjectTo,
|
||||
MapToConfig,
|
||||
pluralize,
|
||||
upperCaseFirst,
|
||||
} from "../common"
|
||||
import {
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
} from "./decorators"
|
||||
|
||||
type BaseMethods =
|
||||
| "retrieve"
|
||||
| "list"
|
||||
| "listAndCount"
|
||||
| "delete"
|
||||
| "softDelete"
|
||||
| "restore"
|
||||
|
||||
const readMethods = ["retrieve", "list", "listAndCount"] as BaseMethods[]
|
||||
const writeMethods = ["delete", "softDelete", "restore"] as BaseMethods[]
|
||||
|
||||
const methods: BaseMethods[] = [...readMethods, ...writeMethods]
|
||||
|
||||
type ModelsConfigTemplate = {
|
||||
[ModelName: string]: { singular?: string; plural?: string; dto: object }
|
||||
}
|
||||
|
||||
type ExtractSingularName<
|
||||
T extends Record<any, any>,
|
||||
K = keyof T
|
||||
> = T[K] extends { singular?: string } ? T[K]["singular"] : K
|
||||
|
||||
type ExtractPluralName<T extends Record<any, any>, K = keyof T> = T[K] extends {
|
||||
plural?: string
|
||||
}
|
||||
? T[K]["plural"]
|
||||
: Pluralize<K & string>
|
||||
|
||||
type ModelConfiguration =
|
||||
| Constructor<any>
|
||||
| { singular?: string; plural?: string; model: Constructor<any> }
|
||||
|
||||
export interface AbstractModuleServiceBase<TContainer, TMainModelDTO> {
|
||||
get __container__(): TContainer
|
||||
|
||||
retrieve(
|
||||
id: string,
|
||||
config?: FindConfig<any>,
|
||||
sharedContext?: Context
|
||||
): Promise<TMainModelDTO>
|
||||
|
||||
list(
|
||||
filters?: any,
|
||||
config?: FindConfig<any>,
|
||||
sharedContext?: Context
|
||||
): Promise<TMainModelDTO[]>
|
||||
|
||||
listAndCount(
|
||||
filters?: any,
|
||||
config?: FindConfig<any>,
|
||||
sharedContext?: Context
|
||||
): Promise<[TMainModelDTO[], number]>
|
||||
|
||||
delete(
|
||||
primaryKeyValues: string | object | string[] | object[],
|
||||
sharedContext?: Context
|
||||
): Promise<void>
|
||||
|
||||
softDelete<TReturnableLinkableKeys extends string>(
|
||||
primaryKeyValues: string | object | string[] | object[],
|
||||
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
|
||||
restore<TReturnableLinkableKeys extends string>(
|
||||
primaryKeyValues: string | object | string[] | object[],
|
||||
config?: RestoreReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiple issues on typescript around mapped types function are open, so
|
||||
* when overriding a method from the base class that is mapped dynamically from the
|
||||
* other models, we will have to ignore the error (2425)
|
||||
*
|
||||
* see: https://github.com/microsoft/TypeScript/issues/48125
|
||||
*/
|
||||
export type AbstractModuleService<
|
||||
TContainer,
|
||||
TMainModelDTO,
|
||||
TOtherModelNamesAndAssociatedDTO extends ModelsConfigTemplate
|
||||
> = AbstractModuleServiceBase<TContainer, TMainModelDTO> & {
|
||||
[K in keyof TOtherModelNamesAndAssociatedDTO as `retrieve${ExtractSingularName<
|
||||
TOtherModelNamesAndAssociatedDTO,
|
||||
K
|
||||
> &
|
||||
string}`]: (
|
||||
id: string,
|
||||
config?: FindConfig<any>,
|
||||
sharedContext?: Context
|
||||
) => Promise<TOtherModelNamesAndAssociatedDTO[K & string]["dto"]>
|
||||
} & {
|
||||
[K in keyof TOtherModelNamesAndAssociatedDTO as `list${ExtractPluralName<
|
||||
TOtherModelNamesAndAssociatedDTO,
|
||||
K
|
||||
> &
|
||||
string}`]: (
|
||||
filters?: any,
|
||||
config?: FindConfig<any>,
|
||||
sharedContext?: Context
|
||||
) => Promise<TOtherModelNamesAndAssociatedDTO[K & string]["dto"][]>
|
||||
} & {
|
||||
[K in keyof TOtherModelNamesAndAssociatedDTO as `listAndCount${ExtractPluralName<
|
||||
TOtherModelNamesAndAssociatedDTO,
|
||||
K
|
||||
> &
|
||||
string}`]: {
|
||||
(filters?: any, config?: FindConfig<any>, sharedContext?: Context): Promise<
|
||||
[TOtherModelNamesAndAssociatedDTO[K & string]["dto"][], number]
|
||||
>
|
||||
}
|
||||
} & {
|
||||
[K in keyof TOtherModelNamesAndAssociatedDTO as `delete${ExtractPluralName<
|
||||
TOtherModelNamesAndAssociatedDTO,
|
||||
K
|
||||
> &
|
||||
string}`]: {
|
||||
(
|
||||
primaryKeyValues: string | object | string[] | object[],
|
||||
sharedContext?: Context
|
||||
): Promise<void>
|
||||
}
|
||||
} & {
|
||||
[K in keyof TOtherModelNamesAndAssociatedDTO as `softDelete${ExtractPluralName<
|
||||
TOtherModelNamesAndAssociatedDTO,
|
||||
K
|
||||
> &
|
||||
string}`]: {
|
||||
<TReturnableLinkableKeys extends string>(
|
||||
primaryKeyValues: string | object | string[] | object[],
|
||||
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
}
|
||||
} & {
|
||||
[K in keyof TOtherModelNamesAndAssociatedDTO as `restore${ExtractPluralName<
|
||||
TOtherModelNamesAndAssociatedDTO,
|
||||
K
|
||||
> &
|
||||
string}`]: {
|
||||
<TReturnableLinkableKeys extends string>(
|
||||
primaryKeyValues: string | object | string[] | object[],
|
||||
config?: RestoreReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function for creating an abstract module service
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* const otherModels = new Set([
|
||||
* Currency,
|
||||
* MoneyAmount,
|
||||
* PriceList,
|
||||
* PriceListRule,
|
||||
* PriceListRuleValue,
|
||||
* PriceRule,
|
||||
* PriceSetMoneyAmount,
|
||||
* PriceSetMoneyAmountRules,
|
||||
* PriceSetRuleType,
|
||||
* RuleType,
|
||||
* ])
|
||||
*
|
||||
* const AbstractModuleService = ModulesSdkUtils.abstractModuleServiceFactory<
|
||||
* InjectedDependencies,
|
||||
* PricingTypes.PriceSetDTO,
|
||||
* // The configuration of each entity also accept singular/plural properties, if not provided then it is using english pluralization
|
||||
* {
|
||||
* Currency: { dto: PricingTypes.CurrencyDTO }
|
||||
* MoneyAmount: { dto: PricingTypes.MoneyAmountDTO }
|
||||
* PriceSetMoneyAmount: { dto: PricingTypes.PriceSetMoneyAmountDTO }
|
||||
* PriceSetMoneyAmountRules: {
|
||||
* dto: PricingTypes.PriceSetMoneyAmountRulesDTO
|
||||
* }
|
||||
* PriceRule: { dto: PricingTypes.PriceRuleDTO }
|
||||
* RuleType: { dto: PricingTypes.RuleTypeDTO }
|
||||
* PriceList: { dto: PricingTypes.PriceListDTO }
|
||||
* PriceListRule: { dto: PricingTypes.PriceListRuleDTO }
|
||||
* }
|
||||
* >(PriceSet, [...otherModels], entityNameToLinkableKeysMap)
|
||||
*
|
||||
* @param mainModel
|
||||
* @param otherModels
|
||||
* @param entityNameToLinkableKeysMap
|
||||
*/
|
||||
export function abstractModuleServiceFactory<
|
||||
TContainer,
|
||||
TMainModelDTO,
|
||||
TOtherModelNamesAndAssociatedDTO extends ModelsConfigTemplate
|
||||
>(
|
||||
mainModel: Constructor<any>,
|
||||
otherModels: ModelConfiguration[],
|
||||
entityNameToLinkableKeysMap: MapToConfig = {}
|
||||
): {
|
||||
new (container: TContainer): AbstractModuleService<
|
||||
TContainer,
|
||||
TMainModelDTO,
|
||||
TOtherModelNamesAndAssociatedDTO
|
||||
>
|
||||
} {
|
||||
const buildMethodNamesFromModel = (
|
||||
model: ModelConfiguration,
|
||||
suffixed: boolean = true
|
||||
): Record<string, string> => {
|
||||
return methods.reduce((acc, method) => {
|
||||
let modelName: string = ""
|
||||
|
||||
if (method === "retrieve") {
|
||||
modelName =
|
||||
"singular" in model && model.singular
|
||||
? model.singular
|
||||
: (model as Constructor<any>).name
|
||||
} else {
|
||||
modelName =
|
||||
"plural" in model && model.plural
|
||||
? model.plural
|
||||
: pluralize((model as Constructor<any>).name)
|
||||
}
|
||||
|
||||
const methodName = suffixed
|
||||
? `${method}${upperCaseFirst(modelName)}`
|
||||
: method
|
||||
|
||||
return { ...acc, [method]: methodName }
|
||||
}, {})
|
||||
}
|
||||
|
||||
const buildAndAssignMethodImpl = function (
|
||||
klassPrototype: any,
|
||||
method: string,
|
||||
methodName: string,
|
||||
model: Constructor<any>
|
||||
): void {
|
||||
const serviceRegistrationName = `${lowerCaseFirst(model.name)}Service`
|
||||
|
||||
const applyMethod = function (impl: Function, contextIndex) {
|
||||
klassPrototype[methodName] = impl
|
||||
|
||||
const descriptorMockRef = {
|
||||
value: klassPrototype[methodName],
|
||||
}
|
||||
|
||||
MedusaContext()(klassPrototype, methodName, contextIndex)
|
||||
|
||||
const ManagerDecorator = readMethods.includes(method as BaseMethods)
|
||||
? InjectManager
|
||||
: InjectTransactionManager
|
||||
|
||||
ManagerDecorator("baseRepository_")(
|
||||
klassPrototype,
|
||||
methodName,
|
||||
descriptorMockRef
|
||||
)
|
||||
|
||||
klassPrototype[methodName] = descriptorMockRef.value
|
||||
}
|
||||
|
||||
let methodImplementation: any = function () {
|
||||
void 0
|
||||
}
|
||||
|
||||
switch (method) {
|
||||
case "retrieve":
|
||||
methodImplementation = async function <T extends object>(
|
||||
this: AbstractModuleService_,
|
||||
id: string,
|
||||
config?: FindConfig<any>,
|
||||
sharedContext: Context = {}
|
||||
): Promise<T> {
|
||||
const entities = await this.__container__[
|
||||
serviceRegistrationName
|
||||
].retrieve(id, config, sharedContext)
|
||||
|
||||
return await this.baseRepository_.serialize<T>(entities, {
|
||||
populate: true,
|
||||
})
|
||||
}
|
||||
|
||||
applyMethod(methodImplementation, 2)
|
||||
|
||||
break
|
||||
case "list":
|
||||
methodImplementation = async function <T extends object>(
|
||||
this: AbstractModuleService_,
|
||||
filters = {},
|
||||
config: FindConfig<any> = {},
|
||||
sharedContext: Context = {}
|
||||
): Promise<T[]> {
|
||||
const entities = await this.__container__[
|
||||
serviceRegistrationName
|
||||
].list(filters, config, sharedContext)
|
||||
|
||||
return await this.baseRepository_.serialize<T[]>(entities, {
|
||||
populate: true,
|
||||
})
|
||||
}
|
||||
|
||||
applyMethod(methodImplementation, 2)
|
||||
|
||||
break
|
||||
case "listAndCount":
|
||||
methodImplementation = async function <T extends object>(
|
||||
this: AbstractModuleService_,
|
||||
filters = {},
|
||||
config: FindConfig<any> = {},
|
||||
sharedContext: Context = {}
|
||||
): Promise<T[]> {
|
||||
const [entities, count] = await this.__container__[
|
||||
serviceRegistrationName
|
||||
].listAndCount(filters, config, sharedContext)
|
||||
|
||||
return [
|
||||
await this.baseRepository_.serialize<T[]>(entities, {
|
||||
populate: true,
|
||||
}),
|
||||
count,
|
||||
]
|
||||
}
|
||||
|
||||
applyMethod(methodImplementation, 2)
|
||||
|
||||
break
|
||||
case "delete":
|
||||
methodImplementation = async function (
|
||||
this: AbstractModuleService_,
|
||||
primaryKeyValues: string | object | string[] | object[],
|
||||
sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
const primaryKeyValues_ = Array.isArray(primaryKeyValues)
|
||||
? primaryKeyValues
|
||||
: [primaryKeyValues]
|
||||
await this.__container__[serviceRegistrationName].delete(
|
||||
primaryKeyValues_,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
await this.eventBusModuleService_?.emit(
|
||||
primaryKeyValues_.map((primaryKeyValue) => ({
|
||||
eventName: `${kebabCase(model.name)}.deleted`,
|
||||
data: isString(primaryKeyValue)
|
||||
? { id: primaryKeyValue }
|
||||
: primaryKeyValue,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
applyMethod(methodImplementation, 1)
|
||||
|
||||
break
|
||||
case "softDelete":
|
||||
methodImplementation = async function <T extends { id: string }>(
|
||||
this: AbstractModuleService_,
|
||||
primaryKeyValues: string | object | string[] | object[],
|
||||
config: SoftDeleteReturn<string> = {},
|
||||
sharedContext: Context = {}
|
||||
): Promise<Record<string, string[]> | void> {
|
||||
const primaryKeyValues_ = Array.isArray(primaryKeyValues)
|
||||
? primaryKeyValues
|
||||
: [primaryKeyValues]
|
||||
|
||||
const [entities, cascadedEntitiesMap] = await this.__container__[
|
||||
serviceRegistrationName
|
||||
].softDelete(primaryKeyValues_, sharedContext)
|
||||
|
||||
const softDeletedEntities = await this.baseRepository_.serialize<T[]>(
|
||||
entities,
|
||||
{
|
||||
populate: true,
|
||||
}
|
||||
)
|
||||
|
||||
await this.eventBusModuleService_?.emit(
|
||||
softDeletedEntities.map(({ id }) => ({
|
||||
eventName: `${kebabCase(model.name)}.deleted`,
|
||||
data: { id },
|
||||
}))
|
||||
)
|
||||
|
||||
let mappedCascadedEntitiesMap
|
||||
if (config.returnLinkableKeys) {
|
||||
// Map internal table/column names to their respective external linkable keys
|
||||
// eg: product.id = product_id, variant.id = variant_id
|
||||
mappedCascadedEntitiesMap = mapObjectTo(
|
||||
cascadedEntitiesMap,
|
||||
entityNameToLinkableKeysMap,
|
||||
{
|
||||
pick: config.returnLinkableKeys,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
|
||||
}
|
||||
|
||||
applyMethod(methodImplementation, 2)
|
||||
|
||||
break
|
||||
case "restore":
|
||||
methodImplementation = async function <T extends object>(
|
||||
this: AbstractModuleService_,
|
||||
primaryKeyValues: string | object | string[] | object[],
|
||||
config: RestoreReturn<string> = {},
|
||||
sharedContext: Context = {}
|
||||
): Promise<Record<string, string[]> | void> {
|
||||
const primaryKeyValues_ = Array.isArray(primaryKeyValues)
|
||||
? primaryKeyValues
|
||||
: [primaryKeyValues]
|
||||
|
||||
const [_, cascadedEntitiesMap] = await this.__container__[
|
||||
serviceRegistrationName
|
||||
].restore(primaryKeyValues_, sharedContext)
|
||||
|
||||
let mappedCascadedEntitiesMap
|
||||
if (config.returnLinkableKeys) {
|
||||
// Map internal table/column names to their respective external linkable keys
|
||||
// eg: product.id = product_id, variant.id = variant_id
|
||||
mappedCascadedEntitiesMap = mapObjectTo(
|
||||
cascadedEntitiesMap,
|
||||
entityNameToLinkableKeysMap,
|
||||
{
|
||||
pick: config.returnLinkableKeys,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
|
||||
}
|
||||
|
||||
applyMethod(methodImplementation, 2)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
class AbstractModuleService_ {
|
||||
readonly __container__: Record<string, any>
|
||||
readonly baseRepository_: RepositoryService
|
||||
readonly eventBusModuleService_: IEventBusModuleService;
|
||||
|
||||
[key: string]: any
|
||||
|
||||
constructor(container: Record<string, any>) {
|
||||
this.__container__ = container
|
||||
this.baseRepository_ = container.baseRepository
|
||||
|
||||
try {
|
||||
this.eventBusModuleService_ = container.eventBusModuleService
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mainModelMethods = buildMethodNamesFromModel(mainModel, false)
|
||||
|
||||
/**
|
||||
* Build the main retrieve/list/listAndCount/delete/softDelete/restore methods for the main model
|
||||
*/
|
||||
|
||||
for (let [method, methodName] of Object.entries(mainModelMethods)) {
|
||||
buildAndAssignMethodImpl(
|
||||
AbstractModuleService_.prototype,
|
||||
method,
|
||||
methodName,
|
||||
mainModel
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the retrieve/list/listAndCount/delete/softDelete/restore methods for all the other models
|
||||
*/
|
||||
|
||||
const otherModelsMethods: [ModelConfiguration, Record<string, string>][] =
|
||||
otherModels.map((model) => [model, buildMethodNamesFromModel(model)])
|
||||
|
||||
for (let [model, modelsMethods] of otherModelsMethods) {
|
||||
Object.entries(modelsMethods).forEach(([method, methodName]) => {
|
||||
model = "model" in model ? model.model : model
|
||||
buildAndAssignMethodImpl(
|
||||
AbstractModuleService_.prototype,
|
||||
method,
|
||||
methodName,
|
||||
model
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return AbstractModuleService_ as unknown as new (
|
||||
container: TContainer
|
||||
) => AbstractModuleService<
|
||||
TContainer,
|
||||
TMainModelDTO,
|
||||
TOtherModelNamesAndAssociatedDTO
|
||||
>
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
import {
|
||||
Context,
|
||||
FindConfig,
|
||||
FilterQuery as InternalFilterQuery,
|
||||
} from "@medusajs/types"
|
||||
import { EntitySchema } from "@mikro-orm/core"
|
||||
import { EntityClass } from "@mikro-orm/core/typings"
|
||||
import {
|
||||
MedusaError,
|
||||
doNotForceTransaction,
|
||||
isDefined,
|
||||
isString,
|
||||
lowerCaseFirst,
|
||||
shouldForceTransaction,
|
||||
upperCaseFirst,
|
||||
} from "../common"
|
||||
import { MedusaContext } from "../modules-sdk"
|
||||
import { buildQuery } from "./build-query"
|
||||
import { InjectManager, InjectTransactionManager } from "./decorators"
|
||||
|
||||
/**
|
||||
* Utility factory and interfaces for internal module services
|
||||
*/
|
||||
|
||||
type FilterableMethods = "list" | "listAndCount"
|
||||
type Methods = "create" | "update"
|
||||
|
||||
export interface AbstractService<
|
||||
TEntity extends {},
|
||||
TContainer extends object = object,
|
||||
TDTOs extends { [K in Methods]?: any } = { [K in Methods]?: any },
|
||||
TFilters extends { [K in FilterableMethods]?: any } = {
|
||||
[K in FilterableMethods]?: any
|
||||
}
|
||||
> {
|
||||
get __container__(): TContainer
|
||||
|
||||
retrieve<TEntityMethod = TEntity>(
|
||||
id: string,
|
||||
config?: FindConfig<TEntityMethod>,
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity>
|
||||
list<TEntityMethod = TEntity>(
|
||||
filters?: TFilters["list"],
|
||||
config?: FindConfig<TEntityMethod>,
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity[]>
|
||||
listAndCount<TEntityMethod = TEntity>(
|
||||
filters?: TFilters["listAndCount"],
|
||||
config?: FindConfig<TEntityMethod>,
|
||||
sharedContext?: Context
|
||||
): Promise<[TEntity[], number]>
|
||||
create(data: TDTOs["create"][], sharedContext?: Context): Promise<TEntity[]>
|
||||
update(data: TDTOs["update"][], sharedContext?: Context): Promise<TEntity[]>
|
||||
delete(
|
||||
primaryKeyValues: string[] | object[],
|
||||
sharedContext?: Context
|
||||
): Promise<void>
|
||||
softDelete(
|
||||
idsOrFilter: string[] | InternalFilterQuery,
|
||||
sharedContext?: Context
|
||||
): Promise<[TEntity[], Record<string, unknown[]>]>
|
||||
restore(
|
||||
idsOrFilter: string[] | InternalFilterQuery,
|
||||
sharedContext?: Context
|
||||
): Promise<[TEntity[], Record<string, unknown[]>]>
|
||||
upsert(
|
||||
data: (TDTOs["create"] | TDTOs["update"])[],
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity[]>
|
||||
}
|
||||
|
||||
export function abstractServiceFactory<
|
||||
TContainer extends object = object,
|
||||
TDTOs extends { [K in Methods]?: any } = { [K in Methods]?: any },
|
||||
TFilters extends { [K in FilterableMethods]?: any } = {
|
||||
[K in FilterableMethods]?: any
|
||||
}
|
||||
>(
|
||||
model: new (...args: any[]) => any
|
||||
): {
|
||||
new <TEntity extends object = any>(container: TContainer): AbstractService<
|
||||
TEntity,
|
||||
TContainer,
|
||||
TDTOs,
|
||||
TFilters
|
||||
>
|
||||
} {
|
||||
const injectedRepositoryName = `${lowerCaseFirst(model.name)}Repository`
|
||||
const propertyRepositoryName = `__${injectedRepositoryName}__`
|
||||
|
||||
class AbstractService_<TEntity extends {}>
|
||||
implements AbstractService<TEntity, TContainer, TDTOs, TFilters>
|
||||
{
|
||||
readonly __container__: TContainer;
|
||||
[key: string]: any
|
||||
|
||||
constructor(container: TContainer) {
|
||||
this.__container__ = container
|
||||
this[propertyRepositoryName] = container[injectedRepositoryName]
|
||||
}
|
||||
|
||||
static retrievePrimaryKeys(entity: EntityClass<any> | EntitySchema<any>) {
|
||||
return (
|
||||
(entity as EntitySchema<any>).meta?.primaryKeys ??
|
||||
(entity as EntityClass<any>).prototype.__meta?.primaryKeys ?? ["id"]
|
||||
)
|
||||
}
|
||||
|
||||
@InjectManager(propertyRepositoryName)
|
||||
async retrieve<TEntityMethod = TEntity>(
|
||||
primaryKeyValues: string | string[] | object[],
|
||||
config: FindConfig<TEntityMethod> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity> {
|
||||
const primaryKeys = AbstractService_.retrievePrimaryKeys(model)
|
||||
|
||||
if (!isDefined(primaryKeyValues)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`${
|
||||
primaryKeys.length === 1
|
||||
? `"${
|
||||
lowerCaseFirst(model.name) + upperCaseFirst(primaryKeys[0])
|
||||
}"`
|
||||
: `${lowerCaseFirst(model.name)} ${primaryKeys.join(", ")}`
|
||||
} must be defined`
|
||||
)
|
||||
}
|
||||
|
||||
let primaryKeysCriteria = {}
|
||||
if (primaryKeys.length === 1) {
|
||||
primaryKeysCriteria[primaryKeys[0]] = primaryKeyValues
|
||||
} else {
|
||||
primaryKeysCriteria = (primaryKeyValues as string[] | object[]).map(
|
||||
(primaryKeyValue) => ({
|
||||
$and: primaryKeys.map((key) => ({ [key]: primaryKeyValue[key] })),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const queryOptions = buildQuery<TEntity>(primaryKeysCriteria, config)
|
||||
|
||||
const entities = await this[propertyRepositoryName].find(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
if (!entities?.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`${model.name} with ${primaryKeys.join(", ")}: ${
|
||||
Array.isArray(primaryKeyValues)
|
||||
? primaryKeyValues.map((v) =>
|
||||
[isString(v) ? v : Object.values(v)].join(", ")
|
||||
)
|
||||
: primaryKeyValues
|
||||
} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return entities[0]
|
||||
}
|
||||
|
||||
@InjectManager(propertyRepositoryName)
|
||||
async list<TEntityMethod = TEntity>(
|
||||
filters: TFilters["list"] = {},
|
||||
config: FindConfig<TEntityMethod> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
const queryOptions = buildQuery<TEntity>(filters, config)
|
||||
|
||||
return (await this[propertyRepositoryName].find(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
@InjectManager(propertyRepositoryName)
|
||||
async listAndCount<TEntityMethod = TEntity>(
|
||||
filters: TFilters["listAndCount"] = {},
|
||||
config: FindConfig<TEntityMethod> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[TEntity[], number]> {
|
||||
const queryOptions = buildQuery<TEntity>(filters, config)
|
||||
|
||||
return (await this[propertyRepositoryName].findAndCount(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)) as [TEntity[], number]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, propertyRepositoryName)
|
||||
async create(
|
||||
data: TDTOs["create"][],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return (await this[propertyRepositoryName].create(
|
||||
data,
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, propertyRepositoryName)
|
||||
async update(
|
||||
data: TDTOs["update"][],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return (await this[propertyRepositoryName].update(
|
||||
data,
|
||||
sharedContext
|
||||
)) as TEntity[]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, propertyRepositoryName)
|
||||
async delete(
|
||||
primaryKeyValues: string[] | object[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
await this[propertyRepositoryName].delete(primaryKeyValues, sharedContext)
|
||||
}
|
||||
|
||||
@InjectTransactionManager(propertyRepositoryName)
|
||||
async softDelete(
|
||||
idsOrFilter: string[] | InternalFilterQuery,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[TEntity[], Record<string, unknown[]>]> {
|
||||
return await this[propertyRepositoryName].softDelete(
|
||||
idsOrFilter,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager(propertyRepositoryName)
|
||||
async restore(
|
||||
idsOrFilter: string[] | InternalFilterQuery,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[TEntity[], Record<string, unknown[]>]> {
|
||||
return await this[propertyRepositoryName].restore(
|
||||
idsOrFilter,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager(propertyRepositoryName)
|
||||
async upsert(
|
||||
data: (TDTOs["create"] | TDTOs["update"])[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return await this[propertyRepositoryName].upsert(data, sharedContext)
|
||||
}
|
||||
}
|
||||
|
||||
return AbstractService_ as unknown as new <TEntity extends {}>(
|
||||
container: TContainer
|
||||
) => AbstractService<TEntity, TContainer, TDTOs, TFilters>
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Context } from "@medusajs/types"
|
||||
import { MedusaContextType } from "./context-parameter"
|
||||
|
||||
export function InjectManager(managerProperty?: string): MethodDecorator {
|
||||
return function (
|
||||
@@ -37,8 +38,14 @@ export function InjectManager(managerProperty?: string): MethodDecorator {
|
||||
? this
|
||||
: this[managerProperty]
|
||||
|
||||
copiedContext.manager ??= resourceWithManager.getFreshManager()
|
||||
copiedContext.transactionManager ??= originalContext?.transactionManager
|
||||
copiedContext.manager =
|
||||
originalContext.manager ?? resourceWithManager.getFreshManager()
|
||||
|
||||
if (originalContext?.transactionManager) {
|
||||
copiedContext.transactionManager = originalContext?.transactionManager
|
||||
}
|
||||
|
||||
copiedContext.__type = MedusaContextType
|
||||
|
||||
args[argIndex] = copiedContext
|
||||
|
||||
|
||||
@@ -60,9 +60,13 @@ export function InjectTransactionManager(
|
||||
})
|
||||
}
|
||||
|
||||
copiedContext.transactionManager ??= transactionManager
|
||||
copiedContext.manager ??= originalContext?.manager
|
||||
copiedContext.__type ??= MedusaContextType
|
||||
copiedContext.transactionManager = transactionManager
|
||||
|
||||
if (originalContext?.manager) {
|
||||
copiedContext.manager = originalContext?.manager
|
||||
}
|
||||
|
||||
copiedContext.__type = MedusaContextType
|
||||
|
||||
args[argIndex] = copiedContext
|
||||
|
||||
|
||||
@@ -5,4 +5,5 @@ export * from "./loaders/mikro-orm-connection-loader"
|
||||
export * from "./loaders/container-loader-factory"
|
||||
export * from "./create-pg-connection"
|
||||
export * from "./migration-scripts"
|
||||
export * from "./abstract-service-factory"
|
||||
export * from "./internal-module-service-factory"
|
||||
export * from "./abstract-module-service-factory"
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
import {
|
||||
BaseFilterable,
|
||||
Context,
|
||||
FilterQuery,
|
||||
FilterQuery as InternalFilterQuery,
|
||||
FindConfig,
|
||||
ModulesSdkTypes,
|
||||
} from "@medusajs/types"
|
||||
import { EntitySchema } from "@mikro-orm/core"
|
||||
import { EntityClass } from "@mikro-orm/core/typings"
|
||||
import {
|
||||
doNotForceTransaction,
|
||||
isDefined,
|
||||
isObject,
|
||||
isString,
|
||||
lowerCaseFirst,
|
||||
MedusaError,
|
||||
shouldForceTransaction,
|
||||
} from "../common"
|
||||
import { buildQuery } from "./build-query"
|
||||
import {
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
} from "./decorators"
|
||||
|
||||
type SelectorAndData = {
|
||||
selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
|
||||
data: any
|
||||
}
|
||||
|
||||
export function internalModuleServiceFactory<
|
||||
TContainer extends object = object
|
||||
>(
|
||||
model: any
|
||||
): {
|
||||
new <TEntity extends object = any>(
|
||||
container: TContainer
|
||||
): ModulesSdkTypes.InternalModuleService<TEntity, TContainer>
|
||||
} {
|
||||
const injectedRepositoryName = `${lowerCaseFirst(model.name)}Repository`
|
||||
const propertyRepositoryName = `__${injectedRepositoryName}__`
|
||||
|
||||
class AbstractService_<TEntity extends object>
|
||||
implements ModulesSdkTypes.InternalModuleService<TEntity, TContainer>
|
||||
{
|
||||
readonly __container__: TContainer;
|
||||
[key: string]: any
|
||||
|
||||
constructor(container: TContainer) {
|
||||
this.__container__ = container
|
||||
this[propertyRepositoryName] = container[injectedRepositoryName]
|
||||
}
|
||||
|
||||
static retrievePrimaryKeys(entity: EntityClass<any> | EntitySchema<any>) {
|
||||
return (
|
||||
(entity as EntitySchema<any>).meta?.primaryKeys ??
|
||||
(entity as EntityClass<any>).prototype.__meta?.primaryKeys ?? ["id"]
|
||||
)
|
||||
}
|
||||
|
||||
static buildUniqueCompositeKeyValue(keys: string[], data: object) {
|
||||
return keys.map((k) => data[k]).join("_")
|
||||
}
|
||||
|
||||
@InjectManager(propertyRepositoryName)
|
||||
async retrieve(
|
||||
idOrObject: string | object,
|
||||
config: FindConfig<TEntity> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity> {
|
||||
const primaryKeys = AbstractService_.retrievePrimaryKeys(model)
|
||||
|
||||
if (
|
||||
!isDefined(idOrObject) ||
|
||||
(isString(idOrObject) && primaryKeys.length > 1) ||
|
||||
((!isString(idOrObject) ||
|
||||
(isObject(idOrObject) && !idOrObject[primaryKeys[0]])) &&
|
||||
primaryKeys.length === 1)
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`${
|
||||
primaryKeys.length === 1
|
||||
? `${lowerCaseFirst(model.name) + " - " + primaryKeys[0]}`
|
||||
: `${lowerCaseFirst(model.name)} - ${primaryKeys.join(", ")}`
|
||||
} must be defined`
|
||||
)
|
||||
}
|
||||
|
||||
let primaryKeysCriteria = {}
|
||||
if (primaryKeys.length === 1) {
|
||||
primaryKeysCriteria[primaryKeys[0]] = idOrObject
|
||||
} else {
|
||||
const idOrObject_ = Array.isArray(idOrObject)
|
||||
? idOrObject
|
||||
: [idOrObject]
|
||||
primaryKeysCriteria = idOrObject_.map((primaryKeyValue) => ({
|
||||
$and: primaryKeys.map((key) => ({ [key]: primaryKeyValue[key] })),
|
||||
}))
|
||||
}
|
||||
|
||||
const queryOptions = buildQuery(primaryKeysCriteria, config)
|
||||
|
||||
const entities = await this[propertyRepositoryName].find(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
if (!entities?.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`${model.name} with ${primaryKeys.join(", ")}: ${
|
||||
Array.isArray(idOrObject)
|
||||
? idOrObject.map((v) =>
|
||||
[isString(v) ? v : Object.values(v)].join(", ")
|
||||
)
|
||||
: idOrObject
|
||||
} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return entities[0]
|
||||
}
|
||||
|
||||
@InjectManager(propertyRepositoryName)
|
||||
async list(
|
||||
filters: FilterQuery<any> | BaseFilterable<FilterQuery<any>> = {},
|
||||
config: FindConfig<any> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
const queryOptions = buildQuery(filters, config)
|
||||
|
||||
return await this[propertyRepositoryName].find(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
@InjectManager(propertyRepositoryName)
|
||||
async listAndCount(
|
||||
filters: FilterQuery<any> | BaseFilterable<FilterQuery<any>> = {},
|
||||
config: FindConfig<any> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[TEntity[], number]> {
|
||||
const queryOptions = buildQuery(filters, config)
|
||||
|
||||
return await this[propertyRepositoryName].findAndCount(
|
||||
queryOptions,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
create(data: any, sharedContext?: Context): Promise<TEntity>
|
||||
create(data: any[], sharedContext?: Context): Promise<TEntity[]>
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, propertyRepositoryName)
|
||||
async create(
|
||||
data: any | any[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity | TEntity[]> {
|
||||
if (!isDefined(data) || (Array.isArray(data) && data.length === 0)) {
|
||||
return (Array.isArray(data) ? [] : void 0) as TEntity | TEntity[]
|
||||
}
|
||||
|
||||
const data_ = Array.isArray(data) ? data : [data]
|
||||
const entities = await this[propertyRepositoryName].create(
|
||||
data_,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return Array.isArray(data) ? entities : entities[0]
|
||||
}
|
||||
|
||||
update(data: any[], sharedContext?: Context): Promise<TEntity[]>
|
||||
update(data: any, sharedContext?: Context): Promise<TEntity>
|
||||
update(
|
||||
selectorAndData: SelectorAndData,
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity[]>
|
||||
update(
|
||||
selectorAndData: SelectorAndData[],
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity[]>
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, propertyRepositoryName)
|
||||
async update(
|
||||
input: any | any[] | SelectorAndData | SelectorAndData[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity | TEntity[]> {
|
||||
if (!isDefined(input) || (Array.isArray(input) && input.length === 0)) {
|
||||
return (Array.isArray(input) ? [] : void 0) as TEntity | TEntity[]
|
||||
}
|
||||
|
||||
const primaryKeys = AbstractService_.retrievePrimaryKeys(model)
|
||||
const inputArray = Array.isArray(input) ? input : [input]
|
||||
|
||||
const toUpdateData: { entity; update }[] = []
|
||||
|
||||
// Only used when we receive data and no selector
|
||||
const keySelectorForDataOnly: any = {
|
||||
$or: [],
|
||||
}
|
||||
const keySelectorDataMap = new Map<string, any>()
|
||||
|
||||
for (const input_ of inputArray) {
|
||||
if (input_.selector) {
|
||||
const entitiesToUpdate = await this.list(
|
||||
input_.selector,
|
||||
{},
|
||||
sharedContext
|
||||
)
|
||||
// Create a pair of entity and data to update
|
||||
entitiesToUpdate.forEach((entity) => {
|
||||
toUpdateData.push({
|
||||
entity,
|
||||
update: input_.data,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// in case we are manipulating the data, then extract the primary keys as a selector and the rest as the data to update
|
||||
const selector = {}
|
||||
|
||||
primaryKeys.forEach((key) => {
|
||||
selector[key] = input_[key]
|
||||
})
|
||||
|
||||
const uniqueCompositeKey =
|
||||
AbstractService_.buildUniqueCompositeKeyValue(primaryKeys, input_)
|
||||
keySelectorDataMap.set(uniqueCompositeKey, input_)
|
||||
|
||||
keySelectorForDataOnly.$or.push(selector)
|
||||
}
|
||||
}
|
||||
|
||||
if (keySelectorForDataOnly.$or.length) {
|
||||
const entitiesToUpdate = await this.list(
|
||||
keySelectorForDataOnly,
|
||||
{},
|
||||
sharedContext
|
||||
)
|
||||
|
||||
// Create a pair of entity and data to update
|
||||
entitiesToUpdate.forEach((entity) => {
|
||||
const uniqueCompositeKey =
|
||||
AbstractService_.buildUniqueCompositeKeyValue(primaryKeys, entity)
|
||||
toUpdateData.push({
|
||||
entity,
|
||||
update: keySelectorDataMap.get(uniqueCompositeKey)!,
|
||||
})
|
||||
})
|
||||
|
||||
// Only throw for missing entities when we dont have selectors involved as selector by design can return 0 entities
|
||||
if (entitiesToUpdate.length !== keySelectorDataMap.size) {
|
||||
const entityName = (model as EntityClass<TEntity>).name ?? model
|
||||
|
||||
const compositeKeysValuesForFoundEntities = new Set(
|
||||
entitiesToUpdate.map((entity) => {
|
||||
return AbstractService_.buildUniqueCompositeKeyValue(
|
||||
primaryKeys,
|
||||
entity
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
const missingEntityValues: any[] = []
|
||||
|
||||
;[...keySelectorDataMap.keys()].filter((key) => {
|
||||
if (!compositeKeysValuesForFoundEntities.has(key)) {
|
||||
const value = key.replace(/_/gi, " - ")
|
||||
missingEntityValues.push(value)
|
||||
}
|
||||
})
|
||||
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`${entityName} with ${primaryKeys.join(
|
||||
", "
|
||||
)} "${missingEntityValues.join(", ")}" not found`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return await this[propertyRepositoryName].update(
|
||||
toUpdateData,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
delete(idOrSelector: string, sharedContext?: Context): Promise<void>
|
||||
delete(idOrSelector: string[], sharedContext?: Context): Promise<void>
|
||||
delete(idOrSelector: object, sharedContext?: Context): Promise<void>
|
||||
delete(idOrSelector: object[], sharedContext?: Context): Promise<void>
|
||||
delete(
|
||||
idOrSelector: {
|
||||
selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
|
||||
},
|
||||
sharedContext?: Context
|
||||
): Promise<void>
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, propertyRepositoryName)
|
||||
async delete(
|
||||
idOrSelector:
|
||||
| string
|
||||
| string[]
|
||||
| object
|
||||
| object[]
|
||||
| {
|
||||
selector: FilterQuery<any> | BaseFilterable<FilterQuery<any>>
|
||||
},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
if (
|
||||
!isDefined(idOrSelector) ||
|
||||
(Array.isArray(idOrSelector) && idOrSelector.length === 0)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const primaryKeys = AbstractService_.retrievePrimaryKeys(model)
|
||||
|
||||
if (
|
||||
(Array.isArray(idOrSelector) && idOrSelector.length === 0) ||
|
||||
((isString(idOrSelector) ||
|
||||
(Array.isArray(idOrSelector) && isString(idOrSelector[0]))) &&
|
||||
primaryKeys.length > 1)
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`${
|
||||
primaryKeys.length === 1
|
||||
? `"${lowerCaseFirst(model.name) + " - " + primaryKeys[0]}"`
|
||||
: `${lowerCaseFirst(model.name)} - ${primaryKeys.join(", ")}`
|
||||
} must be defined`
|
||||
)
|
||||
}
|
||||
|
||||
const deleteCriteria: any = {
|
||||
$or: [],
|
||||
}
|
||||
|
||||
if (isObject(idOrSelector) && "selector" in idOrSelector) {
|
||||
const entitiesToDelete = await this.list(
|
||||
idOrSelector.selector as FilterQuery<any>,
|
||||
{
|
||||
select: primaryKeys,
|
||||
},
|
||||
sharedContext
|
||||
)
|
||||
|
||||
for (const entity of entitiesToDelete) {
|
||||
const criteria = {}
|
||||
primaryKeys.forEach((key) => {
|
||||
criteria[key] = entity[key]
|
||||
})
|
||||
deleteCriteria.$or.push(criteria)
|
||||
}
|
||||
} else {
|
||||
const primaryKeysValues = Array.isArray(idOrSelector)
|
||||
? idOrSelector
|
||||
: [idOrSelector]
|
||||
|
||||
deleteCriteria.$or = primaryKeysValues.map((primaryKeyValue) => {
|
||||
const criteria = {}
|
||||
|
||||
if (isObject(primaryKeyValue)) {
|
||||
Object.entries(primaryKeyValue).forEach(([key, value]) => {
|
||||
criteria[key] = value
|
||||
})
|
||||
} else {
|
||||
criteria[primaryKeys[0]] = primaryKeyValue
|
||||
}
|
||||
|
||||
// TODO: Revisit
|
||||
/*primaryKeys.forEach((key) => {
|
||||
/!*if (
|
||||
isObject(primaryKeyValue) &&
|
||||
!isDefined(primaryKeyValue[key]) &&
|
||||
// primaryKeys.length > 1
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Composite key must contain all primary key fields: ${primaryKeys.join(
|
||||
", "
|
||||
)}. Found: ${Object.keys(primaryKeyValue)}`
|
||||
)
|
||||
}*!/
|
||||
|
||||
criteria[key] = isObject(primaryKeyValue)
|
||||
? primaryKeyValue[key]
|
||||
: primaryKeyValue
|
||||
})*/
|
||||
return criteria
|
||||
})
|
||||
}
|
||||
|
||||
await this[propertyRepositoryName].delete(deleteCriteria, sharedContext)
|
||||
}
|
||||
|
||||
@InjectTransactionManager(propertyRepositoryName)
|
||||
async softDelete(
|
||||
idsOrFilter: string[] | InternalFilterQuery,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[TEntity[], Record<string, unknown[]>]> {
|
||||
return await this[propertyRepositoryName].softDelete(
|
||||
idsOrFilter,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
@InjectTransactionManager(propertyRepositoryName)
|
||||
async restore(
|
||||
idsOrFilter: string[] | InternalFilterQuery,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[TEntity[], Record<string, unknown[]>]> {
|
||||
return await this[propertyRepositoryName].restore(
|
||||
idsOrFilter,
|
||||
sharedContext
|
||||
)
|
||||
}
|
||||
|
||||
upsert(data: any[], sharedContext?: Context): Promise<TEntity[]>
|
||||
upsert(data: any, sharedContext?: Context): Promise<TEntity>
|
||||
|
||||
@InjectTransactionManager(propertyRepositoryName)
|
||||
async upsert(
|
||||
data: any | any[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity | TEntity[]> {
|
||||
const data_ = Array.isArray(data) ? data : [data]
|
||||
const entities = await this[propertyRepositoryName].upsert(
|
||||
data_,
|
||||
sharedContext
|
||||
)
|
||||
return Array.isArray(data) ? entities : entities[0]
|
||||
}
|
||||
}
|
||||
|
||||
return AbstractService_ as unknown as new <TEntity extends {}>(
|
||||
container: TContainer
|
||||
) => ModulesSdkTypes.InternalModuleService<TEntity, TContainer>
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@medusajs/types"
|
||||
import { lowerCaseFirst } from "../../common"
|
||||
import { asClass } from "awilix"
|
||||
import { abstractServiceFactory } from "../abstract-service-factory"
|
||||
import { internalModuleServiceFactory } from "../internal-module-service-factory"
|
||||
import { mikroOrmBaseRepositoryFactory } from "../../dal"
|
||||
|
||||
type RepositoryLoaderOptions = {
|
||||
@@ -96,7 +96,7 @@ export function loadModuleServices({
|
||||
const finalService = moduleServicesMap.get(mappedServiceName)
|
||||
|
||||
if (!finalService) {
|
||||
moduleServicesMap.set(mappedServiceName, abstractServiceFactory(Model))
|
||||
moduleServicesMap.set(mappedServiceName, internalModuleServiceFactory(Model))
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user