chore: Hide repository creation if they are not custom + add upsert support by default (#6127)
This commit is contained in:
committed by
GitHub
parent
8a8a7183b8
commit
5e655dd59b
@@ -2,6 +2,7 @@ import {
|
||||
Context,
|
||||
DAL,
|
||||
FilterQuery as InternalFilterQuery,
|
||||
RepositoryService,
|
||||
RepositoryTransformOptions,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
@@ -16,9 +17,9 @@ import {
|
||||
EntityName,
|
||||
FilterQuery as MikroFilterQuery,
|
||||
} from "@mikro-orm/core/typings"
|
||||
import { MedusaError, isString } from "../../common"
|
||||
import { isString, MedusaError } from "../../common"
|
||||
import { MedusaContext } from "../../decorators"
|
||||
import { InjectTransactionManager, buildQuery } from "../../modules-sdk"
|
||||
import { buildQuery, InjectTransactionManager } from "../../modules-sdk"
|
||||
import {
|
||||
getSoftDeletedCascadedEntitiesIdsMappedBy,
|
||||
transactionWrapper,
|
||||
@@ -104,6 +105,10 @@ export class MikroOrmBaseRepository<
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
upsert(data: unknown[], context: Context = {}): Promise<T[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async softDelete(
|
||||
idsOrFilter: string[] | InternalFilterQuery,
|
||||
@@ -228,7 +233,10 @@ export function mikroOrmBaseRepositoryFactory<
|
||||
[K in DtoBasedMutationMethods]?: any
|
||||
}
|
||||
>(entity: EntityClass<T> | EntitySchema<T>) {
|
||||
class MikroOrmAbstractBaseRepository_ extends MikroOrmBaseRepository<T> {
|
||||
class MikroOrmAbstractBaseRepository_
|
||||
extends MikroOrmBaseRepository<T>
|
||||
implements RepositoryService<T, TDTOs>
|
||||
{
|
||||
// @ts-ignore
|
||||
constructor(...args: any[]) {
|
||||
// @ts-ignore
|
||||
@@ -411,6 +419,83 @@ export function mikroOrmBaseRepositoryFactory<
|
||||
findOptions_.options as MikroOptions<T>
|
||||
)
|
||||
}
|
||||
|
||||
async upsert(
|
||||
data: (TDTOs["create"] | TDTOs["update"])[],
|
||||
context: Context = {}
|
||||
): Promise<T[]> {
|
||||
// TODO: Move this logic to the service packages/utils/src/modules-sdk/abstract-service-factory.ts
|
||||
const manager = this.getActiveManager<EntityManager>(context)
|
||||
|
||||
const primaryKeys =
|
||||
MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(entity)
|
||||
|
||||
let primaryKeysCriteria: { [key: string]: any }[] = []
|
||||
if (primaryKeys.length === 1) {
|
||||
primaryKeysCriteria.push({
|
||||
[primaryKeys[0]]: data.map((d) => d[primaryKeys[0]]),
|
||||
})
|
||||
} else {
|
||||
primaryKeysCriteria = data.map((d) => ({
|
||||
$and: primaryKeys.map((key) => ({ [key]: d[key] })),
|
||||
}))
|
||||
}
|
||||
|
||||
const allEntities = await Promise.all(
|
||||
primaryKeysCriteria.map(
|
||||
async (criteria) =>
|
||||
await this.find({ where: criteria } as DAL.FindOptions<T>, context)
|
||||
)
|
||||
)
|
||||
|
||||
const existingEntities = allEntities.flat()
|
||||
|
||||
const existingEntitiesMap = new Map<string, T>()
|
||||
existingEntities.forEach((entity) => {
|
||||
if (entity) {
|
||||
const key =
|
||||
MikroOrmAbstractBaseRepository_.buildUniqueCompositeKeyValue(
|
||||
primaryKeys,
|
||||
entity
|
||||
)
|
||||
existingEntitiesMap.set(key, entity)
|
||||
}
|
||||
})
|
||||
|
||||
const upsertedEntities: T[] = []
|
||||
const createdEntities: T[] = []
|
||||
const updatedEntities: T[] = []
|
||||
|
||||
data.forEach((data_) => {
|
||||
// In case the data provided are just strings, then we build an object with the primary key as the key and the data as the valuecd -
|
||||
const key =
|
||||
MikroOrmAbstractBaseRepository_.buildUniqueCompositeKeyValue(
|
||||
primaryKeys,
|
||||
data_
|
||||
)
|
||||
|
||||
const existingEntity = existingEntitiesMap.get(key)
|
||||
if (existingEntity) {
|
||||
const updatedType = manager.assign(existingEntity, data_)
|
||||
updatedEntities.push(updatedType)
|
||||
} else {
|
||||
const newEntity = manager.create(entity, data_)
|
||||
createdEntities.push(newEntity)
|
||||
}
|
||||
})
|
||||
|
||||
if (createdEntities.length) {
|
||||
manager.persist(createdEntities)
|
||||
upsertedEntities.push(...createdEntities)
|
||||
}
|
||||
|
||||
if (updatedEntities.length) {
|
||||
manager.persist(updatedEntities)
|
||||
upsertedEntities.push(...updatedEntities)
|
||||
}
|
||||
|
||||
return upsertedEntities
|
||||
}
|
||||
}
|
||||
|
||||
return MikroOrmAbstractBaseRepository_
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Context, DAL, RepositoryTransformOptions } from "@medusajs/types"
|
||||
import { MedusaContext } from "../decorators"
|
||||
import { transactionWrapper } from "./utils"
|
||||
|
||||
class AbstractBase<T = any> {
|
||||
protected readonly manager_: any
|
||||
@@ -49,6 +50,8 @@ export abstract class AbstractBaseRepository<T = any>
|
||||
|
||||
abstract delete(ids: string[], context?: Context): Promise<void>
|
||||
|
||||
abstract upsert(data: unknown[], context?: Context): Promise<T[]>
|
||||
|
||||
abstract softDelete(
|
||||
ids: string[],
|
||||
context?: Context
|
||||
|
||||
@@ -64,6 +64,10 @@ export interface AbstractService<
|
||||
idsOrFilter: string[] | InternalFilterQuery,
|
||||
sharedContext?: Context
|
||||
): Promise<[TEntity[], Record<string, unknown[]>]>
|
||||
upsert(
|
||||
data: (TDTOs["create"] | TDTOs["update"])[],
|
||||
sharedContext?: Context
|
||||
): Promise<TEntity[]>
|
||||
}
|
||||
|
||||
export function abstractServiceFactory<
|
||||
@@ -75,7 +79,7 @@ export function abstractServiceFactory<
|
||||
>(
|
||||
model: new (...args: any[]) => any
|
||||
): {
|
||||
new <TEntity extends {}>(container: TContainer): AbstractService<
|
||||
new <TEntity extends object = any>(container: TContainer): AbstractService<
|
||||
TEntity,
|
||||
TContainer,
|
||||
TDTOs,
|
||||
@@ -237,6 +241,14 @@ export function abstractServiceFactory<
|
||||
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 {}>(
|
||||
|
||||
@@ -2,6 +2,7 @@ export * from "./load-module-database-config"
|
||||
export * from "./decorators"
|
||||
export * from "./build-query"
|
||||
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"
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import {
|
||||
Constructor,
|
||||
LoaderOptions,
|
||||
MedusaContainer,
|
||||
ModuleServiceInitializeCustomDataLayerOptions,
|
||||
ModuleServiceInitializeOptions,
|
||||
RepositoryService,
|
||||
} from "@medusajs/types"
|
||||
import { lowerCaseFirst } from "../../common"
|
||||
import { asClass } from "awilix"
|
||||
import { abstractServiceFactory } from "../abstract-service-factory"
|
||||
import { mikroOrmBaseRepositoryFactory } from "../../dal"
|
||||
|
||||
type RepositoryLoaderOptions = {
|
||||
moduleModels: Record<string, any>
|
||||
moduleRepositories?: Record<string, any>
|
||||
customRepositories: Record<string, any>
|
||||
container: MedusaContainer
|
||||
}
|
||||
|
||||
type ServiceLoaderOptions = {
|
||||
moduleModels: Record<string, any>
|
||||
moduleServices: Record<string, any>
|
||||
container: MedusaContainer
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for creating a container loader for a module.
|
||||
*
|
||||
* @param moduleModels
|
||||
* @param moduleServices
|
||||
* @param moduleRepositories
|
||||
* @param customRepositoryLoader The default repository loader is based on mikro orm. If you want to use a custom repository loader, you can pass it here.
|
||||
*/
|
||||
export function moduleContainerLoaderFactory({
|
||||
moduleModels,
|
||||
moduleServices,
|
||||
moduleRepositories = {},
|
||||
customRepositoryLoader = loadModuleRepositories,
|
||||
}: {
|
||||
moduleModels: Record<string, any>
|
||||
moduleServices: Record<string, any>
|
||||
moduleRepositories?: Record<string, any>
|
||||
customRepositoryLoader?: (options: RepositoryLoaderOptions) => void
|
||||
}): ({ container, options }: LoaderOptions) => Promise<void> {
|
||||
return async ({
|
||||
container,
|
||||
options,
|
||||
}: LoaderOptions<
|
||||
| ModuleServiceInitializeOptions
|
||||
| ModuleServiceInitializeCustomDataLayerOptions
|
||||
>) => {
|
||||
const customRepositories = (
|
||||
options as ModuleServiceInitializeCustomDataLayerOptions
|
||||
)?.repositories
|
||||
|
||||
loadModuleServices({
|
||||
moduleModels,
|
||||
moduleServices,
|
||||
container,
|
||||
})
|
||||
|
||||
const repositoryLoader = customRepositoryLoader ?? loadModuleRepositories
|
||||
repositoryLoader({
|
||||
moduleModels,
|
||||
moduleRepositories,
|
||||
customRepositories: customRepositories ?? {},
|
||||
container,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the services from the module services object. If a service is not
|
||||
* present a default service will be created for the model.
|
||||
*
|
||||
* @param moduleModels
|
||||
* @param moduleServices
|
||||
* @param container
|
||||
*/
|
||||
export function loadModuleServices({
|
||||
moduleModels,
|
||||
moduleServices,
|
||||
container,
|
||||
}: ServiceLoaderOptions) {
|
||||
const moduleServicesMap = new Map(
|
||||
Object.entries(moduleServices).map(([key, repository]) => [
|
||||
lowerCaseFirst(key),
|
||||
repository,
|
||||
])
|
||||
)
|
||||
|
||||
// Build default services for all models that are not present in the module services
|
||||
Object.values(moduleModels).forEach((Model) => {
|
||||
const mappedServiceName = lowerCaseFirst(Model.name) + "Service"
|
||||
const finalService = moduleServicesMap.get(mappedServiceName)
|
||||
|
||||
if (!finalService) {
|
||||
moduleServicesMap.set(mappedServiceName, abstractServiceFactory(Model))
|
||||
}
|
||||
})
|
||||
|
||||
const allServices = [...moduleServicesMap]
|
||||
|
||||
allServices.forEach(([key, service]) => {
|
||||
container.register({
|
||||
[lowerCaseFirst(key)]: asClass(service as Constructor<any>).singleton(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the repositories from the custom repositories object. If a repository is not
|
||||
* present in the custom repositories object, the default repository will be used from the module repository.
|
||||
* If none are present, a default repository will be created for the model.
|
||||
*
|
||||
* @param moduleModels
|
||||
* @param moduleRepositories
|
||||
* @param customRepositories
|
||||
* @param container
|
||||
*/
|
||||
export function loadModuleRepositories({
|
||||
moduleModels,
|
||||
moduleRepositories = {},
|
||||
customRepositories,
|
||||
container,
|
||||
}: RepositoryLoaderOptions) {
|
||||
const customRepositoriesMap = new Map(
|
||||
Object.entries(customRepositories).map(([key, repository]) => [
|
||||
lowerCaseFirst(key),
|
||||
repository,
|
||||
])
|
||||
)
|
||||
const moduleRepositoriesMap = new Map(
|
||||
Object.entries(moduleRepositories).map(([key, repository]) => [
|
||||
lowerCaseFirst(key),
|
||||
repository,
|
||||
])
|
||||
)
|
||||
|
||||
// Build default repositories for all models that are not present in the custom repositories or module repositories
|
||||
Object.values(moduleModels).forEach((Model) => {
|
||||
const mappedRepositoryName = lowerCaseFirst(Model.name) + "Repository"
|
||||
let finalRepository = customRepositoriesMap.get(mappedRepositoryName)
|
||||
finalRepository ??= moduleRepositoriesMap.get(mappedRepositoryName)
|
||||
|
||||
if (!finalRepository) {
|
||||
moduleRepositoriesMap.set(
|
||||
mappedRepositoryName,
|
||||
mikroOrmBaseRepositoryFactory(Model)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const allRepositories = [...customRepositoriesMap, ...moduleRepositoriesMap]
|
||||
|
||||
allRepositories.forEach(([key, repository]) => {
|
||||
let finalRepository = customRepositoriesMap.get(key)
|
||||
|
||||
if (!finalRepository) {
|
||||
finalRepository = repository
|
||||
}
|
||||
|
||||
container.register({
|
||||
[lowerCaseFirst(key)]: asClass(
|
||||
finalRepository as Constructor<RepositoryService>
|
||||
).singleton(),
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user