chore: Hide repository creation if they are not custom + add upsert support by default (#6127)

This commit is contained in:
Adrien de Peretti
2024-01-19 15:09:38 +01:00
committed by GitHub
parent 8a8a7183b8
commit 5e655dd59b
124 changed files with 1516 additions and 2559 deletions

View File

@@ -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_

View File

@@ -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

View File

@@ -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 {}>(

View File

@@ -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"

View File

@@ -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(),
})
})
}