feat(product): Move mikro orm utils to the utils package (#4631)
Move utils to the utils package as much as possible Co-authored-by: Shahed Nasser <27354907+shahednasser@users.noreply.github.com>
This commit is contained in:
co-authored by
Shahed Nasser
parent
648eb106d6
commit
4073b73130
@@ -2,3 +2,4 @@ export * as DecoratorUtils from "./decorators"
|
||||
export * as EventBusUtils from "./event-bus"
|
||||
export * as SearchUtils from "./search"
|
||||
export * as ModulesSdkUtils from "./modules-sdk"
|
||||
export * as DALUtils from "./dal"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const SoftDeletableFilterKey = "softDeletable"
|
||||
@@ -22,4 +22,3 @@ export * from "./stringify-circular"
|
||||
export * from "./to-kebab-case"
|
||||
export * from "./to-pascal-case"
|
||||
export * from "./wrap-handler"
|
||||
export * from "./dal"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./mikro-orm/mikro-orm-repository"
|
||||
export * from "./repository"
|
||||
export * from "./utils"
|
||||
export * from "./mikro-orm/mikro-orm-create-connection"
|
||||
export * from "./mikro-orm/mikro-orm-soft-deletable-filter"
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ModuleServiceInitializeOptions } from "@medusajs/types"
|
||||
|
||||
export async function mikroOrmCreateConnection(
|
||||
database: ModuleServiceInitializeOptions["database"],
|
||||
entities: any[]
|
||||
) {
|
||||
const { MikroORM } = await import("@mikro-orm/postgresql")
|
||||
|
||||
const schema = database.schema || "public"
|
||||
const orm = await MikroORM.init({
|
||||
discovery: { disableDynamicFileAccess: true },
|
||||
entities,
|
||||
debug: database.debug ?? process.env.NODE_ENV?.startsWith("dev") ?? false,
|
||||
baseDir: process.cwd(),
|
||||
clientUrl: database.clientUrl,
|
||||
schema,
|
||||
driverOptions: database.driverOptions ?? {
|
||||
connection: { ssl: true },
|
||||
},
|
||||
tsNode: process.env.APP_ENV === "development",
|
||||
type: "postgresql",
|
||||
migrations: {
|
||||
path: __dirname + "/../migrations",
|
||||
},
|
||||
})
|
||||
|
||||
return orm
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
import { Context, DAL, RepositoryTransformOptions } from "@medusajs/types"
|
||||
import { MedusaContext } from "../../decorators"
|
||||
import { buildQuery, InjectTransactionManager } from "../../modules-sdk"
|
||||
import {
|
||||
mikroOrmSerializer,
|
||||
mikroOrmUpdateDeletedAtRecursively,
|
||||
transactionWrapper,
|
||||
} from "../utils"
|
||||
|
||||
class MikroOrmBase<T = any> {
|
||||
protected readonly manager_: any
|
||||
|
||||
protected constructor({ manager }) {
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
getFreshManager<TManager = unknown>(): TManager {
|
||||
return (this.manager_.fork
|
||||
? this.manager_.fork()
|
||||
: this.manager_) as unknown as TManager
|
||||
}
|
||||
|
||||
getActiveManager<TManager = unknown>(
|
||||
@MedusaContext()
|
||||
{ transactionManager, manager }: Context = {}
|
||||
): TManager {
|
||||
return (transactionManager ?? manager ?? this.manager_) as TManager
|
||||
}
|
||||
|
||||
async transaction<TManager = unknown>(
|
||||
task: (transactionManager: TManager) => Promise<any>,
|
||||
options: {
|
||||
isolationLevel?: string
|
||||
enableNestedTransactions?: boolean
|
||||
transaction?: TManager
|
||||
} = {}
|
||||
): Promise<any> {
|
||||
// @ts-ignore
|
||||
return await transactionWrapper.bind(this)(task, options)
|
||||
}
|
||||
|
||||
async serialize<TOutput extends object | object[]>(
|
||||
data: any,
|
||||
options?: any
|
||||
): Promise<TOutput> {
|
||||
return await mikroOrmSerializer<TOutput>(data, options)
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class MikroOrmAbstractBaseRepository<T = any>
|
||||
extends MikroOrmBase
|
||||
implements DAL.RepositoryService<T>
|
||||
{
|
||||
abstract find(options?: DAL.FindOptions<T>, context?: Context)
|
||||
|
||||
abstract findAndCount(
|
||||
options?: DAL.FindOptions<T>,
|
||||
context?: Context
|
||||
): Promise<[T[], number]>
|
||||
|
||||
abstract create(data: unknown[], context?: Context): Promise<T[]>
|
||||
|
||||
update(data: unknown[], context?: Context): Promise<T[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
abstract delete(ids: string[], context?: Context): Promise<void>
|
||||
|
||||
@InjectTransactionManager()
|
||||
async softDelete(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<T[]> {
|
||||
const entities = await this.find({ where: { id: { $in: ids } } as any })
|
||||
const date = new Date()
|
||||
|
||||
await mikroOrmUpdateDeletedAtRecursively(manager, entities, date)
|
||||
|
||||
return entities
|
||||
}
|
||||
|
||||
@InjectTransactionManager()
|
||||
async restore(
|
||||
ids: string[],
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<T[]> {
|
||||
const query = buildQuery(
|
||||
{ id: { $in: ids } },
|
||||
{
|
||||
withDeleted: true,
|
||||
}
|
||||
)
|
||||
|
||||
const entities = await this.find(query)
|
||||
|
||||
await mikroOrmUpdateDeletedAtRecursively(manager, entities, null)
|
||||
|
||||
return entities
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class MikroOrmAbstractTreeRepositoryBase<T = any>
|
||||
extends MikroOrmBase<T>
|
||||
implements DAL.TreeRepositoryService<T>
|
||||
{
|
||||
protected constructor({ manager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
}
|
||||
|
||||
abstract find(
|
||||
options?: DAL.FindOptions<T>,
|
||||
transformOptions?: RepositoryTransformOptions,
|
||||
context?: Context
|
||||
)
|
||||
|
||||
abstract findAndCount(
|
||||
options?: DAL.FindOptions<T>,
|
||||
transformOptions?: RepositoryTransformOptions,
|
||||
context?: Context
|
||||
): Promise<[T[], number]>
|
||||
|
||||
abstract create(data: unknown, context?: Context): Promise<T>
|
||||
|
||||
abstract delete(id: string, context?: Context): Promise<void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Priviliged extends of the abstract classes unless most of the methods can't be implemented
|
||||
* in your repository. This base repository is also used to provide a base repository
|
||||
* injection if needed to be able to use the common methods without being related to an entity.
|
||||
* In this case, none of the method will be implemented except the manager and transaction
|
||||
* related ones.
|
||||
*/
|
||||
|
||||
export class MikroOrmBaseRepository extends MikroOrmAbstractBaseRepository {
|
||||
constructor({ manager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
}
|
||||
|
||||
create(data: unknown[], context?: Context): Promise<any[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
update(data: unknown[], context?: Context): Promise<any[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
delete(ids: string[], context?: Context): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
find(options?: DAL.FindOptions, context?: Context): Promise<any[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
findAndCount(
|
||||
options?: DAL.FindOptions,
|
||||
context?: Context
|
||||
): Promise<[any[], number]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
|
||||
export class MikroOrmBaseTreeRepository extends MikroOrmAbstractTreeRepositoryBase {
|
||||
constructor({ manager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
}
|
||||
|
||||
find(
|
||||
options?: DAL.FindOptions,
|
||||
transformOptions?: RepositoryTransformOptions,
|
||||
context?: Context
|
||||
): Promise<any[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
findAndCount(
|
||||
options?: DAL.FindOptions,
|
||||
transformOptions?: RepositoryTransformOptions,
|
||||
context?: Context
|
||||
): Promise<[any[], number]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
create(data: unknown, context?: Context): Promise<any> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
delete(id: string, context?: Context): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export const SoftDeletableFilterKey = "softDeletable"
|
||||
|
||||
interface FilterArguments {
|
||||
withDeleted?: boolean
|
||||
}
|
||||
|
||||
export const mikroOrmSoftDeletableFilterOptions = {
|
||||
name: SoftDeletableFilterKey,
|
||||
cond: ({ withDeleted }: FilterArguments = {}) => {
|
||||
if (withDeleted) {
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
deleted_at: null,
|
||||
}
|
||||
},
|
||||
default: true,
|
||||
args: false,
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Context, DAL, RepositoryTransformOptions } from "@medusajs/types"
|
||||
import { MedusaContext } from "../decorators"
|
||||
|
||||
class AbstractBase<T = any> {
|
||||
protected readonly manager_: any
|
||||
|
||||
protected constructor({ manager }) {
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
getActiveManager<TManager = unknown>(
|
||||
@MedusaContext()
|
||||
{ transactionManager, manager }: Context = {}
|
||||
): TManager {
|
||||
return (transactionManager ?? manager ?? this.manager_) as TManager
|
||||
}
|
||||
|
||||
async transaction<TManager = unknown>(
|
||||
task: (transactionManager: TManager) => Promise<any>,
|
||||
{
|
||||
transaction,
|
||||
isolationLevel,
|
||||
enableNestedTransactions = false,
|
||||
}: {
|
||||
isolationLevel?: string
|
||||
enableNestedTransactions?: boolean
|
||||
transaction?: TManager
|
||||
} = {}
|
||||
): Promise<any> {
|
||||
// @ts-ignore
|
||||
return await transactionWrapper.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class AbstractBaseRepository<T = any>
|
||||
extends AbstractBase
|
||||
implements DAL.RepositoryService<T>
|
||||
{
|
||||
abstract find(options?: DAL.FindOptions<T>, context?: Context)
|
||||
|
||||
abstract findAndCount(
|
||||
options?: DAL.FindOptions<T>,
|
||||
context?: Context
|
||||
): Promise<[T[], number]>
|
||||
|
||||
abstract create(data: unknown[], context?: Context): Promise<T[]>
|
||||
|
||||
abstract update(data: unknown[], context?: Context): Promise<T[]>
|
||||
|
||||
abstract delete(ids: string[], context?: Context): Promise<void>
|
||||
|
||||
abstract softDelete(ids: string[], context?: Context): Promise<T[]>
|
||||
|
||||
abstract restore(ids: string[], context?: Context): Promise<T[]>
|
||||
|
||||
abstract getFreshManager<TManager = unknown>(): TManager
|
||||
|
||||
abstract serialize<TOutput extends object | object[]>(
|
||||
data: any,
|
||||
options?: any
|
||||
): Promise<TOutput>
|
||||
}
|
||||
|
||||
export abstract class AbstractTreeRepositoryBase<T = any>
|
||||
extends AbstractBase<T>
|
||||
implements DAL.TreeRepositoryService<T>
|
||||
{
|
||||
protected constructor({ manager }) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
}
|
||||
|
||||
abstract find(
|
||||
options?: DAL.FindOptions<T>,
|
||||
transformOptions?: RepositoryTransformOptions,
|
||||
context?: Context
|
||||
)
|
||||
|
||||
abstract findAndCount(
|
||||
options?: DAL.FindOptions<T>,
|
||||
transformOptions?: RepositoryTransformOptions,
|
||||
context?: Context
|
||||
): Promise<[T[], number]>
|
||||
|
||||
abstract create(data: unknown, context?: Context): Promise<T>
|
||||
|
||||
abstract delete(id: string, context?: Context): Promise<void>
|
||||
|
||||
abstract getFreshManager<TManager = unknown>(): TManager
|
||||
|
||||
abstract serialize<TOutput extends object | object[]>(
|
||||
data: any,
|
||||
options?: any
|
||||
): Promise<TOutput>
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { SoftDeletableFilterKey } from "../dal"
|
||||
|
||||
export async function transactionWrapper<TManager = unknown>(
|
||||
this: any,
|
||||
task: (transactionManager: unknown) => Promise<any>,
|
||||
{
|
||||
transaction,
|
||||
isolationLevel,
|
||||
enableNestedTransactions = false,
|
||||
}: {
|
||||
isolationLevel?: string
|
||||
transaction?: TManager
|
||||
enableNestedTransactions?: boolean
|
||||
} = {}
|
||||
): Promise<any> {
|
||||
// Reuse the same transaction if it is already provided and nested transactions are disabled
|
||||
if (!enableNestedTransactions && transaction) {
|
||||
return await task(transaction)
|
||||
}
|
||||
|
||||
const options = {}
|
||||
|
||||
if (transaction) {
|
||||
Object.assign(options, { ctx: transaction })
|
||||
}
|
||||
|
||||
if (isolationLevel) {
|
||||
Object.assign(options, { isolationLevel })
|
||||
}
|
||||
|
||||
const transactionMethod =
|
||||
this.manager_.transaction ?? this.manager_.transactional
|
||||
return await transactionMethod.bind(this.manager_)(task, options)
|
||||
}
|
||||
|
||||
export const mikroOrmUpdateDeletedAtRecursively = async <
|
||||
T extends object = any
|
||||
>(
|
||||
manager: any,
|
||||
entities: T[],
|
||||
value: Date | null
|
||||
) => {
|
||||
for (const entity of entities) {
|
||||
if (!("deleted_at" in entity)) continue
|
||||
;(entity as any).deleted_at = value
|
||||
|
||||
const relations = manager
|
||||
.getDriver()
|
||||
.getMetadata()
|
||||
.get(entity.constructor.name).relations
|
||||
|
||||
const relationsToCascade = relations.filter((relation) =>
|
||||
relation.cascade.includes("soft-remove" as any)
|
||||
)
|
||||
|
||||
for (const relation of relationsToCascade) {
|
||||
let collectionRelation = entity[relation.name]
|
||||
|
||||
if (!collectionRelation.isInitialized()) {
|
||||
await collectionRelation.init()
|
||||
}
|
||||
|
||||
const relationEntities = await collectionRelation.getItems({
|
||||
filters: {
|
||||
[SoftDeletableFilterKey]: {
|
||||
withDeleted: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await mikroOrmUpdateDeletedAtRecursively(manager, relationEntities, value)
|
||||
}
|
||||
|
||||
await manager.persist(entity)
|
||||
}
|
||||
}
|
||||
|
||||
export const mikroOrmSerializer = async <TOutput extends object>(
|
||||
data: any,
|
||||
options?: any
|
||||
): Promise<TOutput> => {
|
||||
options ??= {}
|
||||
const { serialize } = await import("@mikro-orm/core")
|
||||
const result = serialize(data, options)
|
||||
return result as unknown as Promise<TOutput>
|
||||
}
|
||||
@@ -5,3 +5,4 @@ export * from "./decorators"
|
||||
export * from "./event-bus"
|
||||
export * from "./search"
|
||||
export * from "./modules-sdk"
|
||||
export * from "./dal"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DAL, FindConfig } from "@medusajs/types"
|
||||
|
||||
import { deduplicate, isObject, SoftDeletableFilterKey } from "../common"
|
||||
import { deduplicate, isObject } from "../common"
|
||||
import { SoftDeletableFilterKey } from "../dal"
|
||||
|
||||
export function buildQuery<T = any, TDto = any>(
|
||||
filters: Record<string, any> = {},
|
||||
|
||||
Reference in New Issue
Block a user