feat: convert MikroORM entities to DML entities (#10043)
* feat: convert MikroORM entities to DML entities * feat: wip on repository changes * continue repositories and types rework * fix order repository usage * continue to update product category repository * Add foreign key as part of the inferred DML type * ../../core/types/src/dml/index.ts * ../../core/types/src/dml/index.ts * fix: relationships mapping * handle nullable foreign keys types * handle nullable foreign keys types * handle nullable foreign keys types * continue to update product category repository * fix all product category repositories issues * fix product category service types * fix product module service types * fix product module service types * fix repository template type * refactor: use a singleton DMLToMikroORM factory instance Since the MikroORM MetadataStorage is global, we will also have to turn DML to MikroORM entities conversion use a global bucket as well * refactor: update product module to use DML in tests * wip: tests * WIP product linkable fixes * continue type fixing and start test fixing * test: fix more tests * fix repository * fix pivot table computaion + fix mikro orm repository * fix many to many management and configuration * fix many to many management and configuration * fix many to many management and configuration * update product tag relation configuration * Introduce experimental dml hooks to fix some issues with categories * more fixes * fix product tests * add missing id prefixes * fix product category handle management * test: fix more failing tests * test: make it all green * test: fix breaking tests * fix: build issues * fix: build issues * fix: more breaking tests * refactor: fix issues after merge * refactor: fix issues after merge * refactor: surpress types error * test: fix DML failing tests * improve many to many inference + tests * Wip fix columns from product entity * remove product model before create hook and manage handle validation and transformation at the service level * test: fix breaking unit tests * fix: product module service to not update handle on product update * fix define link and joiner config * test: fix joiner config test * test: fix joiner config test * fix joiner config primary keys * Fix joiner config builder * Fix joiner config builder * test: remove only modifier from test * refactor: remove hooks usage from product collection * refactor: remove hooks usage from product-option * refactor: remove hooks usage for computing category handle * refactor: remove hooks usage from productCategory model * refactor: remove hooks from DML * refactor: remove cruft * cleanup * re add foerign key indexes * chore: remove unused types * refactor: cleanup * migration and models configuration adjustments * cleanup * fix random ordering * fix * test: fix product-category tests * test: update breaking DML tests * test: array assertion to not care about ordering * fix: temporarily apply id ordering for products * fix ordering * fix ordering remove logs --------- Co-authored-by: adrien2p <adrien.deperetti@gmail.com> Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
co-authored by
adrien2p
Oli Juhl
parent
d6fa912b22
commit
9f204817b0
@@ -69,7 +69,7 @@
|
||||
"scripts": {
|
||||
"build": "rimraf dist && tsc --build",
|
||||
"watch": "tsc --build --watch",
|
||||
"test": "jest --silent=false --bail --maxWorkers=50% --forceExit --testPathIgnorePatterns='/integration-tests/' -- src/**/__tests__/**/*.ts",
|
||||
"test": "jest --silent --bail --maxWorkers=50% --forceExit --testPathIgnorePatterns='/integration-tests/' -- src/**/__tests__/**/*.ts",
|
||||
"test:integration": "jest --silent --bail --runInBand --forceExit -- src/**/integration-tests/__tests__/**/*.ts"
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -120,9 +120,9 @@ class Entity3 {
|
||||
}
|
||||
}
|
||||
|
||||
const Entity1Repository = mikroOrmBaseRepositoryFactory<Entity1>(Entity1)
|
||||
const Entity2Repository = mikroOrmBaseRepositoryFactory<Entity2>(Entity2)
|
||||
const Entity3Repository = mikroOrmBaseRepositoryFactory<Entity3>(Entity3)
|
||||
const Entity1Repository = mikroOrmBaseRepositoryFactory(Entity1)
|
||||
const Entity2Repository = mikroOrmBaseRepositoryFactory(Entity2)
|
||||
const Entity3Repository = mikroOrmBaseRepositoryFactory(Entity3)
|
||||
|
||||
describe("mikroOrmRepository", () => {
|
||||
let orm!: MikroORM
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {
|
||||
BaseFilterable,
|
||||
Context,
|
||||
DAL,
|
||||
FilterQuery,
|
||||
FindOptions,
|
||||
InferEntityType,
|
||||
InferRepositoryReturnType,
|
||||
FilterQuery as InternalFilterQuery,
|
||||
PerformedActions,
|
||||
RepositoryService,
|
||||
@@ -15,11 +17,10 @@ import {
|
||||
EntityName,
|
||||
EntityProperty,
|
||||
EntitySchema,
|
||||
LoadStrategy,
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
LoadStrategy,
|
||||
ReferenceType,
|
||||
RequiredEntityData,
|
||||
} from "@mikro-orm/core"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import {
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
MedusaError,
|
||||
promiseAll,
|
||||
} from "../../common"
|
||||
import { toMikroORMEntity } from "../../dml"
|
||||
import { buildQuery } from "../../modules-sdk/build-query"
|
||||
import {
|
||||
getSoftDeletedCascadedEntitiesIdsMappedBy,
|
||||
@@ -37,7 +39,7 @@ import { dbErrorMapper } from "./db-error-mapper"
|
||||
import { mikroOrmSerializer } from "./mikro-orm-serializer"
|
||||
import { mikroOrmUpdateDeletedAtRecursively } from "./utils"
|
||||
|
||||
export class MikroOrmBase<T = any> {
|
||||
export class MikroOrmBase {
|
||||
readonly manager_: any
|
||||
|
||||
protected constructor({ manager }) {
|
||||
@@ -90,10 +92,12 @@ export class MikroOrmBase<T = any> {
|
||||
* related ones.
|
||||
*/
|
||||
|
||||
export class MikroOrmBaseRepository<T extends object = object>
|
||||
extends MikroOrmBase<T>
|
||||
export class MikroOrmBaseRepository<const T extends object = object>
|
||||
extends MikroOrmBase
|
||||
implements RepositoryService<T>
|
||||
{
|
||||
entity: EntityClass<InferEntityType<T>>
|
||||
|
||||
constructor(...args: any[]) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
@@ -144,43 +148,55 @@ export class MikroOrmBaseRepository<T extends object = object>
|
||||
})
|
||||
}
|
||||
|
||||
create(data: unknown[], context?: Context): Promise<T[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
update(data: { entity; update }[], context?: Context): Promise<T[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
delete(
|
||||
idsOrPKs: FilterQuery<T> & BaseFilterable<FilterQuery<T>>,
|
||||
create(
|
||||
data: unknown[],
|
||||
context?: Context
|
||||
): Promise<void> {
|
||||
): Promise<InferRepositoryReturnType<T>[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
find(options?: DAL.FindOptions<T>, context?: Context): Promise<T[]> {
|
||||
update(
|
||||
data: { entity; update }[],
|
||||
context?: Context
|
||||
): Promise<InferRepositoryReturnType<T>[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
delete(idsOrPKs: FindOptions<T>["where"], context?: Context): Promise<void> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
find(
|
||||
options?: DAL.FindOptions<T>,
|
||||
context?: Context
|
||||
): Promise<InferRepositoryReturnType<T>[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
findAndCount(
|
||||
options?: DAL.FindOptions<T>,
|
||||
context?: Context
|
||||
): Promise<[T[], number]> {
|
||||
): Promise<[InferRepositoryReturnType<T>[], number]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
upsert(data: unknown[], context: Context = {}): Promise<T[]> {
|
||||
upsert(
|
||||
data: unknown[],
|
||||
context: Context = {}
|
||||
): Promise<InferRepositoryReturnType<T>[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
upsertWithReplace(
|
||||
data: unknown[],
|
||||
config: UpsertWithReplaceConfig<T> = {
|
||||
config: UpsertWithReplaceConfig<InferRepositoryReturnType<T>> = {
|
||||
relations: [],
|
||||
},
|
||||
context: Context = {}
|
||||
): Promise<{ entities: T[]; performedActions: PerformedActions }> {
|
||||
): Promise<{
|
||||
entities: InferRepositoryReturnType<T>[]
|
||||
performedActions: PerformedActions
|
||||
}> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
@@ -188,10 +204,10 @@ export class MikroOrmBaseRepository<T extends object = object>
|
||||
filters:
|
||||
| string
|
||||
| string[]
|
||||
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)
|
||||
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)[],
|
||||
| DAL.FindOptions<T>["where"]
|
||||
| DAL.FindOptions<T>["where"][],
|
||||
sharedContext: Context = {}
|
||||
): Promise<[T[], Record<string, unknown[]>]> {
|
||||
): Promise<[InferRepositoryReturnType<T>[], Record<string, unknown[]>]> {
|
||||
const entities = await this.find({ where: filters as any }, sharedContext)
|
||||
const date = new Date()
|
||||
|
||||
@@ -212,8 +228,8 @@ export class MikroOrmBaseRepository<T extends object = object>
|
||||
async restore(
|
||||
idsOrFilter: string[] | InternalFilterQuery,
|
||||
sharedContext: Context = {}
|
||||
): Promise<[T[], Record<string, unknown[]>]> {
|
||||
const query = buildQuery(idsOrFilter, {
|
||||
): Promise<[InferRepositoryReturnType<T>[], Record<string, unknown[]>]> {
|
||||
const query = buildQuery<T>(idsOrFilter, {
|
||||
withDeleted: true,
|
||||
})
|
||||
|
||||
@@ -245,13 +261,13 @@ export class MikroOrmBaseRepository<T extends object = object>
|
||||
|
||||
findOptions.where = {
|
||||
$and: [findOptions.where, { $or: retrieveConstraintsToApply(q) }],
|
||||
} as unknown as DAL.FilterQuery<T & { q?: string }>
|
||||
} as unknown as DAL.FindOptions<T & { q?: string }>["where"]
|
||||
}
|
||||
}
|
||||
|
||||
export class MikroOrmBaseTreeRepository<
|
||||
T extends object = object
|
||||
> extends MikroOrmBase<T> {
|
||||
const T extends object = object
|
||||
> extends MikroOrmBase {
|
||||
constructor() {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
@@ -261,7 +277,7 @@ export class MikroOrmBaseTreeRepository<
|
||||
options?: DAL.FindOptions,
|
||||
transformOptions?: RepositoryTransformOptions,
|
||||
context?: Context
|
||||
): Promise<T[]> {
|
||||
): Promise<InferRepositoryReturnType<T>[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
@@ -269,15 +285,21 @@ export class MikroOrmBaseTreeRepository<
|
||||
options?: DAL.FindOptions,
|
||||
transformOptions?: RepositoryTransformOptions,
|
||||
context?: Context
|
||||
): Promise<[T[], number]> {
|
||||
): Promise<[InferRepositoryReturnType<T>[], number]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
create(data: unknown[], context?: Context): Promise<T[]> {
|
||||
create(
|
||||
data: unknown[],
|
||||
context?: Context
|
||||
): Promise<InferRepositoryReturnType<T>[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
update(data: unknown[], context?: Context): Promise<T[]> {
|
||||
update(
|
||||
data: unknown[],
|
||||
context?: Context
|
||||
): Promise<InferRepositoryReturnType<T>[]> {
|
||||
throw new Error("Method not implemented.")
|
||||
}
|
||||
|
||||
@@ -286,12 +308,18 @@ export class MikroOrmBaseTreeRepository<
|
||||
}
|
||||
}
|
||||
|
||||
export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
entity: any
|
||||
export function mikroOrmBaseRepositoryFactory<const T extends object>(
|
||||
entity: T
|
||||
): {
|
||||
new ({ manager }: { manager: any }): MikroOrmBaseRepository<T>
|
||||
} {
|
||||
const mikroOrmEntity = toMikroORMEntity(entity) as EntityClass<
|
||||
InferEntityType<T>
|
||||
>
|
||||
|
||||
class MikroOrmAbstractBaseRepository_ extends MikroOrmBaseRepository<T> {
|
||||
entity = mikroOrmEntity
|
||||
|
||||
// @ts-ignore
|
||||
constructor(...args: any[]) {
|
||||
// @ts-ignore
|
||||
@@ -315,19 +343,19 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
})
|
||||
}
|
||||
|
||||
async create(data: any[], context?: Context): Promise<T[]> {
|
||||
async create(
|
||||
data: any[],
|
||||
context?: Context
|
||||
): Promise<InferRepositoryReturnType<T>[]> {
|
||||
const manager = this.getActiveManager<EntityManager>(context)
|
||||
|
||||
const entities = data.map((data_) => {
|
||||
return manager.create(
|
||||
entity as EntityName<T>,
|
||||
data_ as RequiredEntityData<T>
|
||||
)
|
||||
return manager.create(this.entity, data_)
|
||||
})
|
||||
|
||||
manager.persist(entities)
|
||||
|
||||
return entities
|
||||
return entities as InferRepositoryReturnType<T>[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,7 +378,7 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
const relations = manager
|
||||
.getDriver()
|
||||
.getMetadata()
|
||||
.get(entity.name).relations
|
||||
.get(this.entity.name).relations
|
||||
|
||||
// In case an empty array is provided for a collection relation of type m:n, this relation needs to be init in order to be
|
||||
// able to perform an application cascade action.
|
||||
@@ -397,7 +425,10 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
}
|
||||
}
|
||||
|
||||
async update(data: { entity; update }[], context?: Context): Promise<T[]> {
|
||||
async update(
|
||||
data: { entity; update }[],
|
||||
context?: Context
|
||||
): Promise<InferRepositoryReturnType<T>[]> {
|
||||
const manager = this.getActiveManager<EntityManager>(context)
|
||||
|
||||
await this.initManyToManyToDetachAllItemsIfNeeded(data, context)
|
||||
@@ -411,17 +442,17 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
}
|
||||
|
||||
async delete(
|
||||
filters: FilterQuery<T> & BaseFilterable<FilterQuery<T>>,
|
||||
filters: FindOptions<T>["where"],
|
||||
context?: Context
|
||||
): Promise<void> {
|
||||
const manager = this.getActiveManager<EntityManager>(context)
|
||||
await manager.nativeDelete<T>(entity as EntityName<T>, filters as any)
|
||||
await manager.nativeDelete<T>(this.entity, filters)
|
||||
}
|
||||
|
||||
async find(
|
||||
options: DAL.FindOptions<T> = { where: {} },
|
||||
options: DAL.FindOptions<T> = { where: {} } as DAL.FindOptions<T>,
|
||||
context?: Context
|
||||
): Promise<T[]> {
|
||||
): Promise<InferRepositoryReturnType<T>[]> {
|
||||
const manager = this.getActiveManager<EntityManager>(context)
|
||||
|
||||
const findOptions_ = { ...options }
|
||||
@@ -443,17 +474,17 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
findOptions: findOptions_,
|
||||
})
|
||||
|
||||
return await manager.find(
|
||||
entity as EntityName<T>,
|
||||
return (await manager.find(
|
||||
this.entity as EntityName<T>,
|
||||
findOptions_.where as MikroFilterQuery<T>,
|
||||
findOptions_.options as MikroOptions<T>
|
||||
)
|
||||
)) as InferRepositoryReturnType<T>[]
|
||||
}
|
||||
|
||||
async findAndCount(
|
||||
findOptions: DAL.FindOptions<T> = { where: {} },
|
||||
findOptions: DAL.FindOptions<T> = { where: {} } as DAL.FindOptions<T>,
|
||||
context: Context = {}
|
||||
): Promise<[T[], number]> {
|
||||
): Promise<[InferRepositoryReturnType<T>[], number]> {
|
||||
const manager = this.getActiveManager<EntityManager>(context)
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
@@ -467,18 +498,22 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
findOptions: findOptions_,
|
||||
})
|
||||
|
||||
return await manager.findAndCount(
|
||||
entity as EntityName<T>,
|
||||
findOptions_.where as MikroFilterQuery<T>,
|
||||
findOptions_.options as MikroOptions<T>
|
||||
)
|
||||
return (await manager.findAndCount(
|
||||
this.entity,
|
||||
findOptions_.where,
|
||||
findOptions_.options as any // MikroOptions<T>
|
||||
)) as [InferRepositoryReturnType<T>[], number]
|
||||
}
|
||||
|
||||
async upsert(data: any[], context: Context = {}): Promise<T[]> {
|
||||
async upsert(
|
||||
data: any[],
|
||||
context: Context = {}
|
||||
): Promise<InferRepositoryReturnType<T>[]> {
|
||||
const manager = this.getActiveManager<EntityManager>(context)
|
||||
|
||||
const primaryKeys =
|
||||
MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(entity)
|
||||
const primaryKeys = MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(
|
||||
this.entity
|
||||
)
|
||||
|
||||
let primaryKeysCriteria: { [key: string]: any }[] = []
|
||||
if (primaryKeys.length === 1) {
|
||||
@@ -497,7 +532,7 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
}))
|
||||
}
|
||||
|
||||
let allEntities: T[][] = []
|
||||
let allEntities: InferRepositoryReturnType<T>[][] = []
|
||||
|
||||
if (primaryKeysCriteria.length) {
|
||||
allEntities = await Promise.all(
|
||||
@@ -513,7 +548,10 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
|
||||
const existingEntities = allEntities.flat()
|
||||
|
||||
const existingEntitiesMap = new Map<string, T>()
|
||||
const existingEntitiesMap = new Map<
|
||||
string,
|
||||
InferRepositoryReturnType<T>
|
||||
>()
|
||||
existingEntities.forEach((entity) => {
|
||||
if (entity) {
|
||||
const key =
|
||||
@@ -525,9 +563,9 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
}
|
||||
})
|
||||
|
||||
const upsertedEntities: T[] = []
|
||||
const createdEntities: T[] = []
|
||||
const updatedEntities: T[] = []
|
||||
const upsertedEntities: InferRepositoryReturnType<T>[] = []
|
||||
const createdEntities: InferRepositoryReturnType<T>[] = []
|
||||
const updatedEntities: InferRepositoryReturnType<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 -
|
||||
@@ -542,8 +580,8 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
const updatedType = manager.assign(existingEntity, data_)
|
||||
updatedEntities.push(updatedType)
|
||||
} else {
|
||||
const newEntity = manager.create<T>(entity, data_)
|
||||
createdEntities.push(newEntity)
|
||||
const newEntity = manager.create(this.entity, data_)
|
||||
createdEntities.push(newEntity as InferRepositoryReturnType<T>)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -558,7 +596,7 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
}
|
||||
|
||||
// TODO return the all, created, updated entities
|
||||
return upsertedEntities
|
||||
return upsertedEntities as InferRepositoryReturnType<T>[]
|
||||
}
|
||||
|
||||
// UpsertWithReplace does several things to simplify module implementation.
|
||||
@@ -570,11 +608,14 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
// We only support 1-level depth of upserts. We don't support custom fields on the many-to-many pivot tables for now
|
||||
async upsertWithReplace(
|
||||
data: any[],
|
||||
config: UpsertWithReplaceConfig<T> = {
|
||||
config: UpsertWithReplaceConfig<InferRepositoryReturnType<T>> = {
|
||||
relations: [],
|
||||
},
|
||||
context: Context = {}
|
||||
): Promise<{ entities: T[]; performedActions: PerformedActions }> {
|
||||
): Promise<{
|
||||
entities: InferRepositoryReturnType<T>[]
|
||||
performedActions: PerformedActions
|
||||
}> {
|
||||
const performedActions: PerformedActions = {
|
||||
created: {},
|
||||
updated: {},
|
||||
@@ -593,7 +634,7 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
const allRelations = manager
|
||||
.getDriver()
|
||||
.getMetadata()
|
||||
.get(entity.name).relations
|
||||
.get(this.entity.name).relations
|
||||
|
||||
const nonexistentRelations = arrayDifference(
|
||||
(config.relations as any) ?? [],
|
||||
@@ -624,7 +665,11 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
)
|
||||
})
|
||||
|
||||
const mainEntity = this.getEntityWithId(manager, entity.name, entryCopy)
|
||||
const mainEntity = this.getEntityWithId(
|
||||
manager,
|
||||
this.entity.name,
|
||||
entryCopy
|
||||
)
|
||||
reconstructedResponse.push({ ...mainEntity, ...reconstructedEntry })
|
||||
originalDataMap.set(mainEntity.id, entry)
|
||||
|
||||
@@ -634,7 +679,7 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
let {
|
||||
orderedEntities: upsertedTopLevelEntities,
|
||||
performedActions: performedActions_,
|
||||
} = await this.upsertMany_(manager, entity.name, toUpsert)
|
||||
} = await this.upsertMany_(manager, this.entity.name, toUpsert)
|
||||
|
||||
this.mergePerformedActions(performedActions, performedActions_)
|
||||
|
||||
@@ -954,10 +999,10 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
filters:
|
||||
| string
|
||||
| string[]
|
||||
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)
|
||||
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)[],
|
||||
| DAL.FindOptions<T>["where"]
|
||||
| DAL.FindOptions<T>["where"][],
|
||||
sharedContext: Context = {}
|
||||
): Promise<[T[], Record<string, unknown[]>]> {
|
||||
): Promise<[InferRepositoryReturnType<T>[], Record<string, unknown[]>]> {
|
||||
if (Array.isArray(filters) && !filters.filter(Boolean).length) {
|
||||
return [[], {}]
|
||||
}
|
||||
@@ -975,10 +1020,10 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
filters:
|
||||
| string
|
||||
| string[]
|
||||
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)
|
||||
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)[],
|
||||
| DAL.FindOptions<T>["where"]
|
||||
| DAL.FindOptions<T>["where"][],
|
||||
sharedContext: Context = {}
|
||||
): Promise<[T[], Record<string, unknown[]>]> {
|
||||
): Promise<[InferRepositoryReturnType<T>[], Record<string, unknown[]>]> {
|
||||
if (Array.isArray(filters) && !filters.filter(Boolean).length) {
|
||||
return [[], {}]
|
||||
}
|
||||
@@ -996,11 +1041,12 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
filters:
|
||||
| string
|
||||
| string[]
|
||||
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)
|
||||
| (FilterQuery<T> & BaseFilterable<FilterQuery<T>>)[]
|
||||
) {
|
||||
const primaryKeys =
|
||||
MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(entity)
|
||||
| DAL.FindOptions<T>["where"]
|
||||
| DAL.FindOptions<T>["where"][]
|
||||
): DAL.FindOptions<T>["where"] {
|
||||
const primaryKeys = MikroOrmAbstractBaseRepository_.retrievePrimaryKeys(
|
||||
this.entity
|
||||
)
|
||||
|
||||
const filterArray = Array.isArray(filters) ? filters : [filters]
|
||||
const normalizedFilters: FilterQuery = {
|
||||
@@ -1014,7 +1060,7 @@ export function mikroOrmBaseRepositoryFactory<T extends object = object>(
|
||||
}),
|
||||
}
|
||||
|
||||
return normalizedFilters
|
||||
return normalizedFilters as DAL.FindOptions<T>["where"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,15 @@ import { DmlEntity } from "../entity"
|
||||
import { model } from "../entity-builder"
|
||||
import { DuplicateIdPropertyError } from "../errors"
|
||||
import {
|
||||
createMikrORMEntity,
|
||||
toMikroOrmEntities,
|
||||
mikroORMEntityBuilder,
|
||||
toMikroORMEntity,
|
||||
toMikroOrmEntities,
|
||||
} from "../helpers/create-mikro-orm-entity"
|
||||
|
||||
describe("Entity builder", () => {
|
||||
beforeEach(() => {
|
||||
MetadataStorage.clear()
|
||||
mikroORMEntityBuilder.clear()
|
||||
})
|
||||
|
||||
const defaultColumnMetadata = {
|
||||
@@ -992,12 +993,14 @@ describe("Entity builder", () => {
|
||||
})
|
||||
|
||||
const User = toMikroORMEntity(user)
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
expectTypeOf(new User()).toEqualTypeOf<{
|
||||
id: number
|
||||
username: string
|
||||
email: string
|
||||
role: "moderator" | "admin" | "guest"
|
||||
deleted_at: Date | null
|
||||
created_at: Date
|
||||
updated_at: Date
|
||||
}>()
|
||||
|
||||
const metaData = MetadataStorage.getMetadataFromDecorator(User)
|
||||
@@ -1939,6 +1942,11 @@ describe("Entity builder", () => {
|
||||
expression:
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_email_unique" ON "user" (email) WHERE deleted_at IS NULL',
|
||||
},
|
||||
{
|
||||
expression:
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_user_deleted_at" ON "user" (deleted_at) WHERE deleted_at IS NULL',
|
||||
name: "IDX_user_deleted_at",
|
||||
},
|
||||
])
|
||||
|
||||
expect(metaData.filters).toEqual({
|
||||
@@ -2050,6 +2058,11 @@ describe("Entity builder", () => {
|
||||
expression:
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_email_unique" ON "platform"."user" (email) WHERE deleted_at IS NULL',
|
||||
},
|
||||
{
|
||||
expression:
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_user_deleted_at" ON "platform"."user" (deleted_at) WHERE deleted_at IS NULL',
|
||||
name: "IDX_user_deleted_at",
|
||||
},
|
||||
])
|
||||
|
||||
expect(metaData.filters).toEqual({
|
||||
@@ -2160,6 +2173,11 @@ describe("Entity builder", () => {
|
||||
expression:
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_myEmail_unique" ON "user" (myEmail) WHERE deleted_at IS NULL',
|
||||
},
|
||||
{
|
||||
expression:
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_user_deleted_at" ON "user" (deleted_at) WHERE deleted_at IS NULL',
|
||||
name: "IDX_user_deleted_at",
|
||||
},
|
||||
])
|
||||
|
||||
expect(metaData.filters).toEqual({
|
||||
@@ -2819,7 +2837,6 @@ describe("Entity builder", () => {
|
||||
reference: "scalar",
|
||||
setter: false,
|
||||
type: "string",
|
||||
isForeignKey: true,
|
||||
persist: false,
|
||||
},
|
||||
created_at: {
|
||||
@@ -2943,12 +2960,21 @@ describe("Entity builder", () => {
|
||||
nullable: false,
|
||||
onDelete: undefined,
|
||||
reference: "m:1",
|
||||
isForeignKey: true,
|
||||
},
|
||||
...defaultColumnMetadata,
|
||||
})
|
||||
|
||||
expect(metaData.indexes).toEqual([
|
||||
{
|
||||
expression:
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_user_group_id" ON "user" (group_id) WHERE deleted_at IS NULL',
|
||||
name: "IDX_user_group_id",
|
||||
},
|
||||
{
|
||||
expression:
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_user_deleted_at" ON "user" (deleted_at) WHERE deleted_at IS NULL',
|
||||
name: "IDX_user_deleted_at",
|
||||
},
|
||||
{
|
||||
expression:
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_email_account_unique" ON "user" (email, account) WHERE deleted_at IS NULL',
|
||||
@@ -2969,11 +2995,6 @@ describe("Entity builder", () => {
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_unique-name" ON "user" (organization, account, group_id) WHERE deleted_at IS NULL',
|
||||
name: "IDX_unique-name",
|
||||
},
|
||||
{
|
||||
expression:
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_user_group_id" ON "user" (group_id) WHERE deleted_at IS NULL',
|
||||
name: "IDX_user_group_id",
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
@@ -3025,6 +3046,16 @@ describe("Entity builder", () => {
|
||||
)
|
||||
|
||||
expect(metaData.indexes).toEqual([
|
||||
{
|
||||
expression:
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_user_group_id" ON "user" (group_id) WHERE deleted_at IS NULL',
|
||||
name: "IDX_user_group_id",
|
||||
},
|
||||
{
|
||||
expression:
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_user_deleted_at" ON "user" (deleted_at) WHERE deleted_at IS NULL',
|
||||
name: "IDX_user_deleted_at",
|
||||
},
|
||||
{
|
||||
expression:
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_user_organization_account" ON "user" (organization, account) WHERE email IS NOT NULL AND deleted_at IS NULL',
|
||||
@@ -3050,11 +3081,6 @@ describe("Entity builder", () => {
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_user_account_group_id" ON "user" (account, group_id) WHERE is_owner IS TRUE AND deleted_at IS NULL',
|
||||
name: "IDX_user_account_group_id",
|
||||
},
|
||||
{
|
||||
expression:
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_user_group_id" ON "user" (group_id) WHERE deleted_at IS NULL',
|
||||
name: "IDX_user_group_id",
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
@@ -3121,6 +3147,11 @@ describe("Entity builder", () => {
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_user_group_id" ON "user" (group_id) WHERE deleted_at IS NULL',
|
||||
name: "IDX_user_group_id",
|
||||
},
|
||||
{
|
||||
expression:
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_user_deleted_at" ON "user" (deleted_at) WHERE deleted_at IS NULL',
|
||||
name: "IDX_user_deleted_at",
|
||||
},
|
||||
])
|
||||
|
||||
const Setting = toMikroORMEntity(setting)
|
||||
@@ -3132,6 +3163,11 @@ describe("Entity builder", () => {
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_setting_user_id" ON "setting" (user_id) WHERE deleted_at IS NULL',
|
||||
name: "IDX_setting_user_id",
|
||||
},
|
||||
{
|
||||
expression:
|
||||
'CREATE INDEX IF NOT EXISTS "IDX_setting_deleted_at" ON "setting" (deleted_at) WHERE deleted_at IS NULL',
|
||||
name: "IDX_setting_deleted_at",
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -3543,7 +3579,6 @@ describe("Entity builder", () => {
|
||||
nullable: false,
|
||||
onDelete: "cascade",
|
||||
reference: "m:1",
|
||||
isForeignKey: true,
|
||||
},
|
||||
created_at: {
|
||||
reference: "scalar",
|
||||
@@ -3616,6 +3651,11 @@ describe("Entity builder", () => {
|
||||
}
|
||||
}>()
|
||||
|
||||
const userInstance = new User()
|
||||
expectTypeOf<
|
||||
(typeof userInstance)["email"]["user_id"]
|
||||
>().toEqualTypeOf<string>()
|
||||
|
||||
expectTypeOf(new Email()).toMatchTypeOf<{
|
||||
email: string
|
||||
isVerified: boolean
|
||||
@@ -3631,6 +3671,7 @@ describe("Entity builder", () => {
|
||||
}
|
||||
}
|
||||
}>()
|
||||
expectTypeOf(new Email().user_id).toEqualTypeOf<string>()
|
||||
|
||||
const metaData = MetadataStorage.getMetadataFromDecorator(User)
|
||||
expect(metaData.className).toEqual("User")
|
||||
@@ -3740,7 +3781,6 @@ describe("Entity builder", () => {
|
||||
name: "user_id",
|
||||
getter: false,
|
||||
setter: false,
|
||||
isForeignKey: true,
|
||||
persist: false,
|
||||
},
|
||||
created_at: {
|
||||
@@ -3810,6 +3850,11 @@ describe("Entity builder", () => {
|
||||
}
|
||||
}>()
|
||||
|
||||
const userInstance = new User()
|
||||
expectTypeOf<(typeof userInstance)["email"]["user_id"]>().toEqualTypeOf<
|
||||
string | null
|
||||
>()
|
||||
|
||||
expectTypeOf(new Email()).toMatchTypeOf<{
|
||||
email: string
|
||||
isVerified: boolean
|
||||
@@ -3822,6 +3867,7 @@ describe("Entity builder", () => {
|
||||
}
|
||||
} | null
|
||||
}>()
|
||||
expectTypeOf(new Email().user_id).toEqualTypeOf<string | null>()
|
||||
|
||||
const metaData = MetadataStorage.getMetadataFromDecorator(User)
|
||||
expect(metaData.className).toEqual("User")
|
||||
@@ -3931,7 +3977,6 @@ describe("Entity builder", () => {
|
||||
name: "user_id",
|
||||
getter: false,
|
||||
setter: false,
|
||||
isForeignKey: true,
|
||||
persist: false,
|
||||
},
|
||||
created_at: {
|
||||
@@ -4121,7 +4166,6 @@ describe("Entity builder", () => {
|
||||
mapToPk: true,
|
||||
fieldName: "user_id",
|
||||
nullable: false,
|
||||
isForeignKey: true,
|
||||
},
|
||||
created_at: {
|
||||
reference: "scalar",
|
||||
@@ -4310,7 +4354,6 @@ describe("Entity builder", () => {
|
||||
mapToPk: true,
|
||||
fieldName: "user_id",
|
||||
nullable: true,
|
||||
isForeignKey: true,
|
||||
},
|
||||
created_at: {
|
||||
reference: "scalar",
|
||||
@@ -4566,7 +4609,6 @@ describe("Entity builder", () => {
|
||||
name: "user_id",
|
||||
getter: false,
|
||||
setter: false,
|
||||
isForeignKey: true,
|
||||
persist: false,
|
||||
},
|
||||
created_at: {
|
||||
@@ -4765,7 +4807,6 @@ describe("Entity builder", () => {
|
||||
name: "user_id",
|
||||
getter: false,
|
||||
setter: false,
|
||||
isForeignKey: true,
|
||||
persist: false,
|
||||
},
|
||||
created_at: {
|
||||
@@ -4872,7 +4913,6 @@ describe("Entity builder", () => {
|
||||
mapToPk: true,
|
||||
nullable: false,
|
||||
onDelete: undefined,
|
||||
isForeignKey: true,
|
||||
},
|
||||
children: {
|
||||
cascade: undefined,
|
||||
@@ -4983,7 +5023,6 @@ describe("Entity builder", () => {
|
||||
name: "parent_id",
|
||||
type: "string",
|
||||
columnType: "text",
|
||||
isForeignKey: true,
|
||||
persist: false,
|
||||
reference: "scalar",
|
||||
getter: false,
|
||||
@@ -5110,8 +5149,9 @@ describe("Entity builder", () => {
|
||||
reference: "m:n",
|
||||
name: "teams",
|
||||
entity: "Team",
|
||||
owner: true,
|
||||
pivotTable: "team_users",
|
||||
mappedBy: "users",
|
||||
inversedBy: "users",
|
||||
},
|
||||
created_at: {
|
||||
reference: "scalar",
|
||||
@@ -5177,7 +5217,9 @@ describe("Entity builder", () => {
|
||||
users: {
|
||||
reference: "m:n",
|
||||
name: "users",
|
||||
mappedBy: "teams",
|
||||
entity: "User",
|
||||
owner: false,
|
||||
pivotTable: "team_users",
|
||||
},
|
||||
created_at: {
|
||||
@@ -5288,8 +5330,9 @@ describe("Entity builder", () => {
|
||||
reference: "m:n",
|
||||
name: "teams",
|
||||
entity: "Team",
|
||||
owner: true,
|
||||
pivotTable: "team_users",
|
||||
mappedBy: "users",
|
||||
inversedBy: "users",
|
||||
},
|
||||
created_at: {
|
||||
reference: "scalar",
|
||||
@@ -5356,6 +5399,8 @@ describe("Entity builder", () => {
|
||||
reference: "m:n",
|
||||
name: "users",
|
||||
entity: "User",
|
||||
owner: false,
|
||||
mappedBy: "teams",
|
||||
pivotTable: "team_users",
|
||||
},
|
||||
created_at: {
|
||||
@@ -5500,6 +5545,188 @@ describe("Entity builder", () => {
|
||||
reference: "m:n",
|
||||
name: "teams",
|
||||
entity: "Team",
|
||||
owner: true,
|
||||
pivotTable: "team_users",
|
||||
inversedBy: "users",
|
||||
},
|
||||
created_at: {
|
||||
reference: "scalar",
|
||||
type: "date",
|
||||
columnType: "timestamptz",
|
||||
name: "created_at",
|
||||
fieldName: "created_at",
|
||||
defaultRaw: "now()",
|
||||
onCreate: expect.any(Function),
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
updated_at: {
|
||||
reference: "scalar",
|
||||
type: "date",
|
||||
columnType: "timestamptz",
|
||||
name: "updated_at",
|
||||
fieldName: "updated_at",
|
||||
defaultRaw: "now()",
|
||||
onCreate: expect.any(Function),
|
||||
onUpdate: expect.any(Function),
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
deleted_at: {
|
||||
reference: "scalar",
|
||||
type: "date",
|
||||
columnType: "timestamptz",
|
||||
name: "deleted_at",
|
||||
fieldName: "deleted_at",
|
||||
nullable: true,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
})
|
||||
|
||||
const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team)
|
||||
expect(teamMetaData.className).toEqual("Team")
|
||||
expect(teamMetaData.path).toEqual("Team")
|
||||
expect(teamMetaData.properties).toEqual({
|
||||
id: {
|
||||
reference: "scalar",
|
||||
type: "number",
|
||||
columnType: "integer",
|
||||
name: "id",
|
||||
fieldName: "id",
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
name: {
|
||||
reference: "scalar",
|
||||
type: "string",
|
||||
columnType: "text",
|
||||
name: "name",
|
||||
fieldName: "name",
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
users: {
|
||||
reference: "m:n",
|
||||
name: "users",
|
||||
entity: "User",
|
||||
owner: false,
|
||||
pivotTable: "team_users",
|
||||
mappedBy: "teams",
|
||||
},
|
||||
created_at: {
|
||||
reference: "scalar",
|
||||
type: "date",
|
||||
columnType: "timestamptz",
|
||||
name: "created_at",
|
||||
fieldName: "created_at",
|
||||
defaultRaw: "now()",
|
||||
onCreate: expect.any(Function),
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
updated_at: {
|
||||
reference: "scalar",
|
||||
type: "date",
|
||||
columnType: "timestamptz",
|
||||
name: "updated_at",
|
||||
fieldName: "updated_at",
|
||||
defaultRaw: "now()",
|
||||
onCreate: expect.any(Function),
|
||||
onUpdate: expect.any(Function),
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
deleted_at: {
|
||||
reference: "scalar",
|
||||
type: "date",
|
||||
columnType: "timestamptz",
|
||||
name: "deleted_at",
|
||||
fieldName: "deleted_at",
|
||||
nullable: true,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("define mappedBy on both sides and reverse order of registering entities", () => {
|
||||
const team = model.define("team", {
|
||||
id: model.number(),
|
||||
name: model.text(),
|
||||
users: model.manyToMany(() => user, { mappedBy: "teams" }),
|
||||
})
|
||||
|
||||
const user = model.define("user", {
|
||||
id: model.number(),
|
||||
username: model.text(),
|
||||
teams: model.manyToMany(() => team, { mappedBy: "users" }),
|
||||
})
|
||||
|
||||
const Team = toMikroORMEntity(team)
|
||||
const User = toMikroORMEntity(user)
|
||||
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
username: string
|
||||
teams: {
|
||||
id: number
|
||||
name: string
|
||||
users: {
|
||||
id: number
|
||||
username: string
|
||||
}[]
|
||||
}[]
|
||||
}>()
|
||||
|
||||
expectTypeOf(new Team()).toMatchTypeOf<{
|
||||
id: number
|
||||
name: string
|
||||
users: {
|
||||
id: number
|
||||
username: string
|
||||
teams: {
|
||||
id: number
|
||||
name: string
|
||||
}[]
|
||||
}[]
|
||||
}>()
|
||||
|
||||
const metaData = MetadataStorage.getMetadataFromDecorator(User)
|
||||
expect(metaData.className).toEqual("User")
|
||||
expect(metaData.path).toEqual("User")
|
||||
expect(metaData.properties).toEqual({
|
||||
id: {
|
||||
reference: "scalar",
|
||||
type: "number",
|
||||
columnType: "integer",
|
||||
name: "id",
|
||||
fieldName: "id",
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
username: {
|
||||
reference: "scalar",
|
||||
type: "string",
|
||||
columnType: "text",
|
||||
name: "username",
|
||||
fieldName: "username",
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
teams: {
|
||||
reference: "m:n",
|
||||
name: "teams",
|
||||
entity: "Team",
|
||||
owner: false,
|
||||
pivotTable: "team_users",
|
||||
mappedBy: "users",
|
||||
},
|
||||
@@ -5568,11 +5795,8 @@ describe("Entity builder", () => {
|
||||
reference: "m:n",
|
||||
name: "users",
|
||||
entity: "User",
|
||||
owner: true,
|
||||
pivotTable: "team_users",
|
||||
/**
|
||||
* The other side should be inversed in order for Mikro ORM
|
||||
* to work. Both sides cannot have mappedBy.
|
||||
*/
|
||||
inversedBy: "teams",
|
||||
},
|
||||
created_at: {
|
||||
@@ -5613,190 +5837,6 @@ describe("Entity builder", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("define mappedBy on both sides and reverse order of registering entities", () => {
|
||||
const team = model.define("team", {
|
||||
id: model.number(),
|
||||
name: model.text(),
|
||||
users: model.manyToMany(() => user, { mappedBy: "teams" }),
|
||||
})
|
||||
|
||||
const user = model.define("user", {
|
||||
id: model.number(),
|
||||
username: model.text(),
|
||||
teams: model.manyToMany(() => team, { mappedBy: "users" }),
|
||||
})
|
||||
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const Team = entityBuilder(team)
|
||||
const User = entityBuilder(user)
|
||||
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
username: string
|
||||
teams: {
|
||||
id: number
|
||||
name: string
|
||||
users: {
|
||||
id: number
|
||||
username: string
|
||||
}[]
|
||||
}[]
|
||||
}>()
|
||||
|
||||
expectTypeOf(new Team()).toMatchTypeOf<{
|
||||
id: number
|
||||
name: string
|
||||
users: {
|
||||
id: number
|
||||
username: string
|
||||
teams: {
|
||||
id: number
|
||||
name: string
|
||||
}[]
|
||||
}[]
|
||||
}>()
|
||||
|
||||
const metaData = MetadataStorage.getMetadataFromDecorator(User)
|
||||
expect(metaData.className).toEqual("User")
|
||||
expect(metaData.path).toEqual("User")
|
||||
expect(metaData.properties).toEqual({
|
||||
id: {
|
||||
reference: "scalar",
|
||||
type: "number",
|
||||
columnType: "integer",
|
||||
name: "id",
|
||||
fieldName: "id",
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
username: {
|
||||
reference: "scalar",
|
||||
type: "string",
|
||||
columnType: "text",
|
||||
name: "username",
|
||||
fieldName: "username",
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
teams: {
|
||||
reference: "m:n",
|
||||
name: "teams",
|
||||
entity: "Team",
|
||||
pivotTable: "team_users",
|
||||
/**
|
||||
* The other side should be inversed in order for Mikro ORM
|
||||
* to work. Both sides cannot have mappedBy.
|
||||
*/
|
||||
inversedBy: "users",
|
||||
},
|
||||
created_at: {
|
||||
reference: "scalar",
|
||||
type: "date",
|
||||
columnType: "timestamptz",
|
||||
name: "created_at",
|
||||
fieldName: "created_at",
|
||||
defaultRaw: "now()",
|
||||
onCreate: expect.any(Function),
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
updated_at: {
|
||||
reference: "scalar",
|
||||
type: "date",
|
||||
columnType: "timestamptz",
|
||||
name: "updated_at",
|
||||
fieldName: "updated_at",
|
||||
defaultRaw: "now()",
|
||||
onCreate: expect.any(Function),
|
||||
onUpdate: expect.any(Function),
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
deleted_at: {
|
||||
reference: "scalar",
|
||||
type: "date",
|
||||
columnType: "timestamptz",
|
||||
name: "deleted_at",
|
||||
fieldName: "deleted_at",
|
||||
nullable: true,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
})
|
||||
|
||||
const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team)
|
||||
expect(teamMetaData.className).toEqual("Team")
|
||||
expect(teamMetaData.path).toEqual("Team")
|
||||
expect(teamMetaData.properties).toEqual({
|
||||
id: {
|
||||
reference: "scalar",
|
||||
type: "number",
|
||||
columnType: "integer",
|
||||
name: "id",
|
||||
fieldName: "id",
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
name: {
|
||||
reference: "scalar",
|
||||
type: "string",
|
||||
columnType: "text",
|
||||
name: "name",
|
||||
fieldName: "name",
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
users: {
|
||||
reference: "m:n",
|
||||
name: "users",
|
||||
entity: "User",
|
||||
pivotTable: "team_users",
|
||||
mappedBy: "teams",
|
||||
},
|
||||
created_at: {
|
||||
reference: "scalar",
|
||||
type: "date",
|
||||
columnType: "timestamptz",
|
||||
name: "created_at",
|
||||
fieldName: "created_at",
|
||||
defaultRaw: "now()",
|
||||
onCreate: expect.any(Function),
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
updated_at: {
|
||||
reference: "scalar",
|
||||
type: "date",
|
||||
columnType: "timestamptz",
|
||||
name: "updated_at",
|
||||
fieldName: "updated_at",
|
||||
defaultRaw: "now()",
|
||||
onCreate: expect.any(Function),
|
||||
onUpdate: expect.any(Function),
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
deleted_at: {
|
||||
reference: "scalar",
|
||||
type: "date",
|
||||
columnType: "timestamptz",
|
||||
name: "deleted_at",
|
||||
fieldName: "deleted_at",
|
||||
nullable: true,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("define multiple many to many relationships to the same entity", () => {
|
||||
const team = model.define("team", {
|
||||
id: model.number(),
|
||||
@@ -5816,9 +5856,8 @@ describe("Entity builder", () => {
|
||||
teams: model.manyToMany(() => team, { mappedBy: "users" }),
|
||||
})
|
||||
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
const Team = entityBuilder(team)
|
||||
const User = entityBuilder(user)
|
||||
const Team = toMikroORMEntity(team)
|
||||
const User = toMikroORMEntity(user)
|
||||
|
||||
expectTypeOf(new User()).toMatchTypeOf<{
|
||||
id: number
|
||||
@@ -5886,19 +5925,17 @@ describe("Entity builder", () => {
|
||||
reference: "m:n",
|
||||
name: "teams",
|
||||
entity: "Team",
|
||||
owner: false,
|
||||
pivotTable: "team_users",
|
||||
/**
|
||||
* The other side should be inversed in order for Mikro ORM
|
||||
* to work. Both sides cannot have mappedBy.
|
||||
*/
|
||||
inversedBy: "users",
|
||||
mappedBy: "users",
|
||||
},
|
||||
activeTeams: {
|
||||
reference: "m:n",
|
||||
name: "activeTeams",
|
||||
entity: "Team",
|
||||
owner: false,
|
||||
pivotTable: "team_users",
|
||||
inversedBy: "activeTeamsUsers",
|
||||
mappedBy: "activeTeamsUsers",
|
||||
},
|
||||
created_at: {
|
||||
reference: "scalar",
|
||||
@@ -5965,15 +6002,17 @@ describe("Entity builder", () => {
|
||||
reference: "m:n",
|
||||
name: "users",
|
||||
entity: "User",
|
||||
owner: true,
|
||||
pivotTable: "team_users",
|
||||
mappedBy: "teams",
|
||||
inversedBy: "teams",
|
||||
},
|
||||
activeTeamsUsers: {
|
||||
reference: "m:n",
|
||||
name: "activeTeamsUsers",
|
||||
entity: "User",
|
||||
owner: true,
|
||||
pivotTable: "team_users",
|
||||
mappedBy: "activeTeams",
|
||||
inversedBy: "activeTeams",
|
||||
},
|
||||
created_at: {
|
||||
reference: "scalar",
|
||||
@@ -6086,8 +6125,9 @@ describe("Entity builder", () => {
|
||||
reference: "m:n",
|
||||
name: "teams",
|
||||
entity: "Team",
|
||||
owner: true,
|
||||
pivotTable: "platform.team_users",
|
||||
mappedBy: "users",
|
||||
inversedBy: "users",
|
||||
},
|
||||
created_at: {
|
||||
reference: "scalar",
|
||||
@@ -6155,6 +6195,8 @@ describe("Entity builder", () => {
|
||||
reference: "m:n",
|
||||
name: "users",
|
||||
entity: "User",
|
||||
owner: false,
|
||||
mappedBy: "teams",
|
||||
pivotTable: "platform.team_users",
|
||||
},
|
||||
created_at: {
|
||||
@@ -6195,13 +6237,168 @@ describe("Entity builder", () => {
|
||||
})
|
||||
})
|
||||
|
||||
test("should compute the pivot table name correctly", () => {
|
||||
const team = model.define("teamSquad", {
|
||||
id: model.number(),
|
||||
name: model.text(),
|
||||
users: model.manyToMany(() => user),
|
||||
})
|
||||
|
||||
const user = model.define("RandomUser", {
|
||||
id: model.number(),
|
||||
username: model.text(),
|
||||
teams: model.manyToMany(() => team, {
|
||||
mappedBy: "users",
|
||||
}),
|
||||
})
|
||||
|
||||
const User = toMikroORMEntity(user)
|
||||
const Team = toMikroORMEntity(team)
|
||||
|
||||
const metaData = MetadataStorage.getMetadataFromDecorator(User)
|
||||
expect(metaData.className).toEqual("RandomUser")
|
||||
expect(metaData.path).toEqual("RandomUser")
|
||||
expect(metaData.properties).toEqual({
|
||||
id: {
|
||||
reference: "scalar",
|
||||
type: "number",
|
||||
columnType: "integer",
|
||||
name: "id",
|
||||
fieldName: "id",
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
username: {
|
||||
reference: "scalar",
|
||||
type: "string",
|
||||
columnType: "text",
|
||||
name: "username",
|
||||
fieldName: "username",
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
teams: {
|
||||
reference: "m:n",
|
||||
name: "teams",
|
||||
entity: "TeamSquad",
|
||||
owner: true,
|
||||
pivotTable: "random_user_team_squads",
|
||||
inversedBy: "users",
|
||||
},
|
||||
created_at: {
|
||||
reference: "scalar",
|
||||
type: "date",
|
||||
columnType: "timestamptz",
|
||||
name: "created_at",
|
||||
fieldName: "created_at",
|
||||
defaultRaw: "now()",
|
||||
onCreate: expect.any(Function),
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
updated_at: {
|
||||
reference: "scalar",
|
||||
type: "date",
|
||||
columnType: "timestamptz",
|
||||
name: "updated_at",
|
||||
fieldName: "updated_at",
|
||||
defaultRaw: "now()",
|
||||
onCreate: expect.any(Function),
|
||||
onUpdate: expect.any(Function),
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
deleted_at: {
|
||||
reference: "scalar",
|
||||
type: "date",
|
||||
columnType: "timestamptz",
|
||||
name: "deleted_at",
|
||||
fieldName: "deleted_at",
|
||||
nullable: true,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
})
|
||||
|
||||
const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team)
|
||||
expect(teamMetaData.className).toEqual("TeamSquad")
|
||||
expect(teamMetaData.path).toEqual("TeamSquad")
|
||||
expect(teamMetaData.properties).toEqual({
|
||||
id: {
|
||||
reference: "scalar",
|
||||
type: "number",
|
||||
columnType: "integer",
|
||||
name: "id",
|
||||
fieldName: "id",
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
name: {
|
||||
reference: "scalar",
|
||||
type: "string",
|
||||
columnType: "text",
|
||||
name: "name",
|
||||
fieldName: "name",
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
users: {
|
||||
reference: "m:n",
|
||||
name: "users",
|
||||
entity: "RandomUser",
|
||||
owner: false,
|
||||
mappedBy: "teams",
|
||||
pivotTable: "random_user_team_squads",
|
||||
},
|
||||
created_at: {
|
||||
reference: "scalar",
|
||||
type: "date",
|
||||
columnType: "timestamptz",
|
||||
name: "created_at",
|
||||
fieldName: "created_at",
|
||||
defaultRaw: "now()",
|
||||
onCreate: expect.any(Function),
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
updated_at: {
|
||||
reference: "scalar",
|
||||
type: "date",
|
||||
columnType: "timestamptz",
|
||||
name: "updated_at",
|
||||
fieldName: "updated_at",
|
||||
defaultRaw: "now()",
|
||||
onCreate: expect.any(Function),
|
||||
onUpdate: expect.any(Function),
|
||||
nullable: false,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
deleted_at: {
|
||||
reference: "scalar",
|
||||
type: "date",
|
||||
columnType: "timestamptz",
|
||||
name: "deleted_at",
|
||||
fieldName: "deleted_at",
|
||||
nullable: true,
|
||||
getter: false,
|
||||
setter: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("define custom pivot table name", () => {
|
||||
const team = model.define("team", {
|
||||
id: model.number(),
|
||||
name: model.text(),
|
||||
users: model.manyToMany(() => user, {
|
||||
pivotTable: "users_teams",
|
||||
}),
|
||||
users: model.manyToMany(() => user),
|
||||
})
|
||||
|
||||
const user = model.define("user", {
|
||||
@@ -6270,8 +6467,9 @@ describe("Entity builder", () => {
|
||||
reference: "m:n",
|
||||
name: "teams",
|
||||
entity: "Team",
|
||||
owner: true,
|
||||
pivotTable: "users_teams",
|
||||
mappedBy: "users",
|
||||
inversedBy: "users",
|
||||
},
|
||||
created_at: {
|
||||
reference: "scalar",
|
||||
@@ -6337,7 +6535,9 @@ describe("Entity builder", () => {
|
||||
users: {
|
||||
reference: "m:n",
|
||||
name: "users",
|
||||
owner: false,
|
||||
entity: "User",
|
||||
mappedBy: "teams",
|
||||
pivotTable: "users_teams",
|
||||
},
|
||||
created_at: {
|
||||
@@ -6454,7 +6654,6 @@ describe("Entity builder", () => {
|
||||
mapToPk: true,
|
||||
fieldName: "user_id",
|
||||
nullable: false,
|
||||
isForeignKey: true,
|
||||
},
|
||||
user: {
|
||||
reference: "scalar",
|
||||
@@ -6473,7 +6672,6 @@ describe("Entity builder", () => {
|
||||
mapToPk: true,
|
||||
fieldName: "team_id",
|
||||
nullable: false,
|
||||
isForeignKey: true,
|
||||
},
|
||||
team: {
|
||||
reference: "scalar",
|
||||
@@ -6549,8 +6747,9 @@ describe("Entity builder", () => {
|
||||
reference: "m:n",
|
||||
name: "teams",
|
||||
entity: "Team",
|
||||
owner: true,
|
||||
pivotEntity: "TeamUsers",
|
||||
mappedBy: "users",
|
||||
inversedBy: "users",
|
||||
},
|
||||
created_at: {
|
||||
reference: "scalar",
|
||||
@@ -6617,6 +6816,8 @@ describe("Entity builder", () => {
|
||||
reference: "m:n",
|
||||
name: "users",
|
||||
entity: "User",
|
||||
owner: false,
|
||||
mappedBy: "teams",
|
||||
pivotEntity: "TeamUsers",
|
||||
},
|
||||
created_at: {
|
||||
|
||||
@@ -56,6 +56,14 @@ export type ManyToManyOptions = RelationshipOptions &
|
||||
* @ignore
|
||||
*/
|
||||
pivotEntity?: never
|
||||
/**
|
||||
* The column name in the pivot table that for the current entity
|
||||
*/
|
||||
joinColumn?: string
|
||||
/**
|
||||
* The column name in the pivot table for the opposite entity
|
||||
*/
|
||||
inverseJoinColumn?: string
|
||||
}
|
||||
| {
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { PropertyType } from "@medusajs/types"
|
||||
import { DmlEntity } from "../entity"
|
||||
import { parseEntityName } from "./entity-builder/parse-entity-name"
|
||||
import { getGraphQLAttributeFromDMLPropety } from "./graphql-builder/get-attribute"
|
||||
import { setGraphQLRelationship } from "./graphql-builder/set-relationship"
|
||||
import { getGraphQLAttributeFromDMLPropety } from "./graphql-builder/get-attribute"
|
||||
|
||||
export function generateGraphQLFromEntity<T extends DmlEntity<any, any>>(
|
||||
entity: T
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
Constructor,
|
||||
DMLSchema,
|
||||
EntityConstructor,
|
||||
IDmlEntity,
|
||||
@@ -6,10 +7,11 @@ import type {
|
||||
PropertyType,
|
||||
} from "@medusajs/types"
|
||||
import { Entity, Filter } from "@mikro-orm/core"
|
||||
import { mikroOrmSoftDeletableFilterOptions } from "../../dal"
|
||||
|
||||
import { DmlEntity } from "../entity"
|
||||
import { DuplicateIdPropertyError } from "../errors"
|
||||
import { IdProperty } from "../properties/id"
|
||||
import { DuplicateIdPropertyError } from "../errors"
|
||||
import { mikroOrmSoftDeletableFilterOptions } from "../../dal"
|
||||
import { applySearchable } from "./entity-builder/apply-searchable"
|
||||
import { defineProperty } from "./entity-builder/define-property"
|
||||
import { defineRelationship } from "./entity-builder/define-relationship"
|
||||
@@ -21,7 +23,7 @@ import { applyEntityIndexes, applyIndexes } from "./mikro-orm/apply-indexes"
|
||||
* value is a function that can be used to convert DML entities
|
||||
* to Mikro ORM entities.
|
||||
*/
|
||||
export function createMikrORMEntity() {
|
||||
function createMikrORMEntity() {
|
||||
/**
|
||||
* The following property is used to track many to many relationship
|
||||
* between two entities. It is needed because we have to mark one
|
||||
@@ -35,20 +37,21 @@ export function createMikrORMEntity() {
|
||||
* - [user.teams]: true // the teams relationship on user is an owner
|
||||
* - [team.users] // cannot be an owner
|
||||
*/
|
||||
// TODO: if we use the util toMikroOrmEntities then a new builder will be used each time, lets think about this. Currently if means that with many to many we need to use the same builder
|
||||
const MANY_TO_MANY_TRACKED_RELATIONS: Record<string, boolean> = {}
|
||||
let MANY_TO_MANY_TRACKED_RELATIONS: Record<string, boolean> = {}
|
||||
let ENTITIES: Record<string, Constructor<any>> = {}
|
||||
|
||||
/**
|
||||
* A helper function to define a Mikro ORM entity from a
|
||||
* DML entity.
|
||||
*/
|
||||
return function createEntity<T extends DmlEntity<any, any>>(
|
||||
entity: T
|
||||
): Infer<T> {
|
||||
function createEntity<T extends DmlEntity<any, any>>(entity: T): Infer<T> {
|
||||
class MikroORMEntity {}
|
||||
|
||||
const { schema, cascades, indexes: entityIndexes = [] } = entity.parse()
|
||||
const { modelName, tableName } = parseEntityName(entity)
|
||||
if (ENTITIES[modelName]) {
|
||||
return ENTITIES[modelName] as Infer<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigning name to the class constructor, so that it matches
|
||||
@@ -80,11 +83,14 @@ export function createMikrORMEntity() {
|
||||
hasIdAlreadyDefined = true
|
||||
}
|
||||
|
||||
defineProperty(MikroORMEntity, name, property as PropertyType<any>)
|
||||
defineProperty(MikroORMEntity, property as PropertyType<any>, {
|
||||
propertyName: name,
|
||||
tableName,
|
||||
})
|
||||
applyIndexes(MikroORMEntity, tableName, field)
|
||||
applySearchable(MikroORMEntity, field)
|
||||
} else {
|
||||
defineRelationship(MikroORMEntity, field, cascades, context)
|
||||
defineRelationship(MikroORMEntity, entity, field, cascades, context)
|
||||
applySearchable(MikroORMEntity, field)
|
||||
}
|
||||
})
|
||||
@@ -94,12 +100,31 @@ export function createMikrORMEntity() {
|
||||
/**
|
||||
* Converting class to a MikroORM entity
|
||||
*/
|
||||
return Entity({ tableName })(
|
||||
const RegisteredEntity = Entity({ tableName })(
|
||||
Filter(mikroOrmSoftDeletableFilterOptions)(MikroORMEntity)
|
||||
) as Infer<T>
|
||||
|
||||
ENTITIES[modelName] = RegisteredEntity
|
||||
return RegisteredEntity
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the internally tracked entities and relationships
|
||||
*/
|
||||
createEntity.clear = function () {
|
||||
MANY_TO_MANY_TRACKED_RELATIONS = {}
|
||||
ENTITIES = {}
|
||||
}
|
||||
return createEntity
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to convert DML entities to MikroORM entity. Use
|
||||
* "toMikroORMEntity" if you are ensure the input is a DML entity
|
||||
* or not.
|
||||
*/
|
||||
export const mikroORMEntityBuilder = createMikrORMEntity()
|
||||
|
||||
/**
|
||||
* Takes a DML entity and returns a Mikro ORM entity otherwise
|
||||
* return the input idempotently
|
||||
@@ -111,7 +136,7 @@ export const toMikroORMEntity = <T>(
|
||||
let mikroOrmEntity: T | EntityConstructor<any> = entity
|
||||
|
||||
if (DmlEntity.isDmlEntity(entity)) {
|
||||
mikroOrmEntity = createMikrORMEntity()(entity)
|
||||
mikroOrmEntity = mikroORMEntityBuilder(entity)
|
||||
}
|
||||
|
||||
return mikroOrmEntity as T extends IDmlEntity<any, any> ? Infer<T> : T
|
||||
@@ -123,11 +148,9 @@ export const toMikroORMEntity = <T>(
|
||||
* @param entities
|
||||
*/
|
||||
export const toMikroOrmEntities = function <T extends any[]>(entities: T) {
|
||||
const entityBuilder = createMikrORMEntity()
|
||||
|
||||
return entities.map((entity) => {
|
||||
if (DmlEntity.isDmlEntity(entity)) {
|
||||
return entityBuilder(entity)
|
||||
return mikroORMEntityBuilder(entity)
|
||||
}
|
||||
|
||||
return entity
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Utils,
|
||||
} from "@mikro-orm/core"
|
||||
import { PrimaryKeyModifier } from "../../properties/primary-key"
|
||||
import { applyEntityIndexes } from "../mikro-orm/apply-indexes"
|
||||
|
||||
/**
|
||||
* DML entity data types to PostgreSQL data types via
|
||||
@@ -64,7 +65,8 @@ const PROPERTY_TYPES: {
|
||||
const SPECIAL_PROPERTIES: {
|
||||
[propertyName: string]: (
|
||||
MikroORMEntity: EntityConstructor<any>,
|
||||
field: PropertyMetadata
|
||||
field: PropertyMetadata,
|
||||
tableName: string
|
||||
) => void
|
||||
} = {
|
||||
created_at: (MikroORMEntity, field) => {
|
||||
@@ -88,6 +90,21 @@ const SPECIAL_PROPERTIES: {
|
||||
onUpdate: () => new Date(),
|
||||
})(MikroORMEntity.prototype, field.fieldName)
|
||||
},
|
||||
deleted_at: (MikroORMEntity, field, tableName) => {
|
||||
Property({
|
||||
columnType: "timestamptz",
|
||||
type: "date",
|
||||
nullable: true,
|
||||
fieldName: field.fieldName,
|
||||
})(MikroORMEntity.prototype, field.fieldName)
|
||||
|
||||
applyEntityIndexes(MikroORMEntity, tableName, [
|
||||
{
|
||||
on: ["deleted_at"],
|
||||
where: "deleted_at IS NULL",
|
||||
},
|
||||
])
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,8 +112,8 @@ const SPECIAL_PROPERTIES: {
|
||||
*/
|
||||
export function defineProperty(
|
||||
MikroORMEntity: EntityConstructor<any>,
|
||||
propertyName: string,
|
||||
property: PropertyType<any>
|
||||
property: PropertyType<any>,
|
||||
{ tableName, propertyName }: { tableName: string; propertyName: string }
|
||||
) {
|
||||
const field = property.parse(propertyName)
|
||||
/**
|
||||
@@ -112,18 +129,18 @@ export function defineProperty(
|
||||
}
|
||||
|
||||
if (SPECIAL_PROPERTIES[field.fieldName]) {
|
||||
SPECIAL_PROPERTIES[field.fieldName](MikroORMEntity, field)
|
||||
SPECIAL_PROPERTIES[field.fieldName](MikroORMEntity, field, tableName)
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Defining an big number property
|
||||
* A big number property always comes with a raw_{{ fieldName }} column
|
||||
* where the config of the bigNumber is set.
|
||||
* The `raw_` field is generated during DML schema generation as a json
|
||||
* dataType.
|
||||
*/
|
||||
if (field.dataType.name === "bigNumber") {
|
||||
/**
|
||||
* Defining an big number property
|
||||
* A big number property always comes with a raw_{{ fieldName }} column
|
||||
* where the config of the bigNumber is set.
|
||||
* The `raw_` field is generated during DML schema generation as a json
|
||||
* dataType.
|
||||
*/
|
||||
MikroOrmBigNumberProperty({
|
||||
nullable: field.nullable,
|
||||
fieldName: field.fieldName,
|
||||
|
||||
@@ -16,17 +16,77 @@ import {
|
||||
rel,
|
||||
} from "@mikro-orm/core"
|
||||
import { camelToSnakeCase, pluralize } from "../../../common"
|
||||
import { ForeignKey } from "../../../dal/mikro-orm/decorators/foreign-key"
|
||||
import { DmlEntity } from "../../entity"
|
||||
import { HasMany } from "../../relations/has-many"
|
||||
import { HasOne } from "../../relations/has-one"
|
||||
import { ManyToMany as DmlManyToMany } from "../../relations/many-to-many"
|
||||
import { applyEntityIndexes } from "../mikro-orm/apply-indexes"
|
||||
import { parseEntityName } from "./parse-entity-name"
|
||||
|
||||
type Context = {
|
||||
MANY_TO_MANY_TRACKED_RELATIONS: Record<string, boolean>
|
||||
}
|
||||
|
||||
function retrieveOtherSideRelationshipManyToMany({
|
||||
relationship,
|
||||
relatedEntity,
|
||||
relatedModelName,
|
||||
entity,
|
||||
}: {
|
||||
relationship: RelationshipMetadata
|
||||
relatedEntity: DmlEntity<
|
||||
Record<string, PropertyType<any> | RelationshipType<any>>,
|
||||
any
|
||||
>
|
||||
relatedModelName: string
|
||||
entity: DmlEntity<any, any>
|
||||
}): [string, RelationshipType<any>] {
|
||||
if (relationship.mappedBy) {
|
||||
return [
|
||||
relationship.mappedBy,
|
||||
relatedEntity.parse().schema[relationship.mappedBy],
|
||||
] as [string, RelationshipType<any>]
|
||||
}
|
||||
|
||||
/**
|
||||
* Since we don't have the information about the other side of the
|
||||
* relationship, we will try to find all the other side many to many that refers to the current entity.
|
||||
* If there is any, we will try to find if at least one of them has a mappedBy.
|
||||
*/
|
||||
const potentialOtherSide = Object.entries(relatedEntity.schema)
|
||||
.filter(([, propConfig]) => DmlManyToMany.isManyToMany(propConfig))
|
||||
.filter(([prop, propConfig]) => {
|
||||
const parsedProp = propConfig.parse(prop) as RelationshipMetadata
|
||||
|
||||
const relatedEntity =
|
||||
typeof parsedProp.entity === "function"
|
||||
? parsedProp.entity()
|
||||
: undefined
|
||||
|
||||
if (!relatedEntity) {
|
||||
throw new Error(
|
||||
`Invalid relationship reference for "${relatedModelName}.${prop}". Make sure to define the relationship using a factory function`
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
(parsedProp.mappedBy === relationship.name &&
|
||||
parseEntityName(relatedEntity).modelName ===
|
||||
parseEntityName(entity).modelName) ||
|
||||
parseEntityName(relatedEntity).modelName ===
|
||||
parseEntityName(entity).modelName
|
||||
)
|
||||
}) as unknown as [string, RelationshipType<any>][]
|
||||
|
||||
if (potentialOtherSide.length > 1) {
|
||||
throw new Error(
|
||||
`Invalid relationship reference for "${entity.name}.${relationship.name}". Make sure to set the mappedBy property on one side or the other or both.`
|
||||
)
|
||||
}
|
||||
|
||||
return potentialOtherSide[0] ?? []
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a many to many relationship without mappedBy and checks if the other side of the relationship is defined and possesses mappedBy.
|
||||
* @param MikroORMEntity
|
||||
@@ -39,6 +99,7 @@ function validateManyToManyRelationshipWithoutMappedBy({
|
||||
relationship,
|
||||
relatedEntity,
|
||||
relatedModelName,
|
||||
entity,
|
||||
}: {
|
||||
MikroORMEntity: EntityConstructor<any>
|
||||
relationship: RelationshipMetadata
|
||||
@@ -47,42 +108,23 @@ function validateManyToManyRelationshipWithoutMappedBy({
|
||||
any
|
||||
>
|
||||
relatedModelName: string
|
||||
entity: DmlEntity<any, any>
|
||||
}) {
|
||||
/**
|
||||
* Since we don't have the information about the other side of the
|
||||
* relationship, we will try to find all the other side many to many that refers to the current entity.
|
||||
* If there is any, we will try to find if at least one of them has a mappedBy.
|
||||
*/
|
||||
const potentialOtherSides = Object.entries(relatedEntity.schema)
|
||||
.filter(([, propConfig]) => DmlManyToMany.isManyToMany(propConfig))
|
||||
.filter(([prop, propConfig]) => {
|
||||
const parsedProp = propConfig.parse(prop) as RelationshipMetadata
|
||||
const relatedEntity =
|
||||
typeof parsedProp.entity === "function"
|
||||
? parsedProp.entity()
|
||||
: undefined
|
||||
const [, potentialOtherSide] = retrieveOtherSideRelationshipManyToMany({
|
||||
relationship,
|
||||
relatedEntity,
|
||||
relatedModelName,
|
||||
entity,
|
||||
})
|
||||
|
||||
if (!relatedEntity) {
|
||||
throw new Error(
|
||||
`Invalid relationship reference for "${relatedModelName}.${prop}". Make sure to define the relationship using a factory function`
|
||||
)
|
||||
}
|
||||
|
||||
return parseEntityName(relatedEntity).modelName === MikroORMEntity.name
|
||||
}) as unknown as [string, RelationshipType<any>][]
|
||||
|
||||
if (potentialOtherSides.length) {
|
||||
const hasMappedBy = potentialOtherSides.some(
|
||||
([, propConfig]) => !!propConfig.parse("").mappedBy
|
||||
)
|
||||
if (!hasMappedBy) {
|
||||
throw new Error(
|
||||
`Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". "mappedBy" should be defined on one side or the other.`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (!potentialOtherSide) {
|
||||
throw new Error(
|
||||
`Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". The other side of the relationship is missing.`
|
||||
`Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". "mappedBy" should be defined on one side or the other.`
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -140,6 +182,7 @@ export function defineHasManyRelationship(
|
||||
*/
|
||||
export function defineBelongsToRelationship(
|
||||
MikroORMEntity: EntityConstructor<any>,
|
||||
entity: DmlEntity<any, any>,
|
||||
relationship: RelationshipMetadata,
|
||||
relatedEntity: DmlEntity<
|
||||
Record<string, PropertyType<any> | RelationshipType<any>>,
|
||||
@@ -194,8 +237,7 @@ export function defineBelongsToRelationship(
|
||||
return
|
||||
}
|
||||
|
||||
this[relationship.name] ??= this[foreignKeyName]
|
||||
this[foreignKeyName] ??= this[relationship.name]?.id
|
||||
this[foreignKeyName] ??= this[relationship.name]?.id ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -222,7 +264,6 @@ export function defineBelongsToRelationship(
|
||||
nullable: relationship.nullable,
|
||||
onDelete: shouldCascade ? "cascade" : undefined,
|
||||
})(MikroORMEntity.prototype, foreignKeyName)
|
||||
ForeignKey()(MikroORMEntity.prototype, foreignKeyName)
|
||||
|
||||
if (DmlManyToMany.isManyToMany(otherSideRelation)) {
|
||||
Property({
|
||||
@@ -239,6 +280,13 @@ export function defineBelongsToRelationship(
|
||||
})(MikroORMEntity.prototype, relationship.name)
|
||||
}
|
||||
|
||||
const { tableName } = parseEntityName(entity)
|
||||
applyEntityIndexes(MikroORMEntity, tableName, [
|
||||
{
|
||||
on: [foreignKeyName],
|
||||
where: "deleted_at IS NULL",
|
||||
},
|
||||
])
|
||||
applyForeignKeyAssignationHooks(foreignKeyName)
|
||||
return
|
||||
}
|
||||
@@ -270,7 +318,14 @@ export function defineBelongsToRelationship(
|
||||
nullable: relationship.nullable,
|
||||
persist: false,
|
||||
})(MikroORMEntity.prototype, foreignKeyName)
|
||||
ForeignKey()(MikroORMEntity.prototype, foreignKeyName)
|
||||
|
||||
const { tableName } = parseEntityName(entity)
|
||||
applyEntityIndexes(MikroORMEntity, tableName, [
|
||||
{
|
||||
on: [foreignKeyName],
|
||||
where: "deleted_at IS NULL",
|
||||
},
|
||||
])
|
||||
|
||||
applyForeignKeyAssignationHooks(foreignKeyName)
|
||||
return
|
||||
@@ -289,6 +344,7 @@ export function defineBelongsToRelationship(
|
||||
*/
|
||||
export function defineManyToManyRelationship(
|
||||
MikroORMEntity: EntityConstructor<any>,
|
||||
entity: DmlEntity<any, any>,
|
||||
relationship: RelationshipMetadata,
|
||||
relatedEntity: DmlEntity<
|
||||
Record<string, PropertyType<any> | RelationshipType<any>>,
|
||||
@@ -296,59 +352,60 @@ export function defineManyToManyRelationship(
|
||||
>,
|
||||
{
|
||||
relatedModelName,
|
||||
relatedTableName,
|
||||
pgSchema,
|
||||
}: { relatedModelName: string; pgSchema: string | undefined },
|
||||
}: {
|
||||
relatedModelName: string
|
||||
pgSchema: string | undefined
|
||||
relatedTableName: string
|
||||
},
|
||||
{ MANY_TO_MANY_TRACKED_RELATIONS }: Context
|
||||
) {
|
||||
let mappedBy = relationship.mappedBy
|
||||
let inversedBy: undefined | string
|
||||
let pivotEntityName: undefined | string
|
||||
let pivotTableName: undefined | string
|
||||
let joinColumn: undefined | string = relationship.options.joinColumn
|
||||
let inverseJoinColumn: undefined | string =
|
||||
relationship.options.inverseJoinColumn
|
||||
|
||||
const [otherSideRelationshipProperty, otherSideRelationship] =
|
||||
retrieveOtherSideRelationshipManyToMany({
|
||||
relationship,
|
||||
relatedEntity,
|
||||
relatedModelName,
|
||||
entity,
|
||||
})
|
||||
|
||||
/**
|
||||
* Validating other side of relationship when mapped by is defined
|
||||
*/
|
||||
if (mappedBy) {
|
||||
const otherSideRelation = relatedEntity.parse().schema[mappedBy]
|
||||
if (!otherSideRelation) {
|
||||
if (!otherSideRelationship) {
|
||||
throw new Error(
|
||||
`Missing property "${mappedBy}" on "${relatedModelName}" entity. Make sure to define it as a relationship`
|
||||
)
|
||||
}
|
||||
|
||||
if (!DmlManyToMany.isManyToMany(otherSideRelation)) {
|
||||
if (!DmlManyToMany.isManyToMany(otherSideRelationship)) {
|
||||
throw new Error(
|
||||
`Invalid relationship reference for "${mappedBy}" on "${relatedModelName}" entity. Make sure to define a manyToMany relationship`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the other side has defined a mapped by and if that
|
||||
* mapping is already tracked as the owner.
|
||||
*
|
||||
* - If yes, we will inverse our mapped by
|
||||
* - Otherwise, we will track ourselves as the owner.
|
||||
*/
|
||||
if (
|
||||
otherSideRelation.parse(mappedBy).mappedBy &&
|
||||
MANY_TO_MANY_TRACKED_RELATIONS[`${relatedModelName}.${mappedBy}`]
|
||||
) {
|
||||
inversedBy = mappedBy
|
||||
mappedBy = undefined
|
||||
} else {
|
||||
MANY_TO_MANY_TRACKED_RELATIONS[
|
||||
`${MikroORMEntity.name}.${relationship.name}`
|
||||
] = true
|
||||
}
|
||||
} else {
|
||||
validateManyToManyRelationshipWithoutMappedBy({
|
||||
MikroORMEntity,
|
||||
relationship,
|
||||
relatedEntity,
|
||||
relatedModelName,
|
||||
entity,
|
||||
})
|
||||
}
|
||||
|
||||
MANY_TO_MANY_TRACKED_RELATIONS[
|
||||
`${MikroORMEntity.name}.${relationship.name}`
|
||||
] = true
|
||||
|
||||
/**
|
||||
* Validating pivot entity when it is defined and computing
|
||||
* its name
|
||||
@@ -371,6 +428,10 @@ export function defineManyToManyRelationship(
|
||||
}
|
||||
|
||||
if (!pivotEntityName) {
|
||||
const { tableName } = parseEntityName(entity)
|
||||
let tableNameWithoutSchema: string
|
||||
let relatedTableNameWithoutSchema: string
|
||||
|
||||
/**
|
||||
* Pivot table name is created as follows (when not explicitly provided)
|
||||
*
|
||||
@@ -379,20 +440,58 @@ export function defineManyToManyRelationship(
|
||||
* - Converting them from camelCase to snake_case.
|
||||
* - And finally pluralizing the second entity name.
|
||||
*/
|
||||
|
||||
let [schema, ...tableTokens] = tableName.split(".")
|
||||
if (!tableTokens.length) {
|
||||
tableNameWithoutSchema = schema
|
||||
} else {
|
||||
tableNameWithoutSchema = tableTokens.join(".")
|
||||
}
|
||||
|
||||
const [relatedSchema, ...relatedTableTokens] = relatedTableName.split(".")
|
||||
if (!relatedTableTokens.length) {
|
||||
relatedTableNameWithoutSchema = relatedSchema
|
||||
} else {
|
||||
relatedTableNameWithoutSchema = relatedTableTokens.join(".")
|
||||
}
|
||||
|
||||
pivotTableName =
|
||||
relationship.options.pivotTable ??
|
||||
[MikroORMEntity.name.toLowerCase(), relatedModelName.toLowerCase()]
|
||||
otherSideRelationship.parse("").options.pivotTable ??
|
||||
[tableNameWithoutSchema, relatedTableNameWithoutSchema]
|
||||
.sort()
|
||||
.map((token, index) => {
|
||||
if (index === 1) {
|
||||
return pluralize(camelToSnakeCase(token))
|
||||
return pluralize(token)
|
||||
}
|
||||
return camelToSnakeCase(token)
|
||||
return token
|
||||
})
|
||||
.join("_")
|
||||
}
|
||||
|
||||
const otherSideRelationOptions = otherSideRelationship.parse("").options
|
||||
|
||||
const isOwner =
|
||||
!!joinColumn ||
|
||||
!!inverseJoinColumn ||
|
||||
!!relationship.options.pivotTable ||
|
||||
/**
|
||||
* We can't infer it from the current entity so lets
|
||||
* look at the otherside configuration as well to make a choice
|
||||
*/
|
||||
(!otherSideRelationOptions.pivotTable &&
|
||||
!otherSideRelationOptions.joinColumn &&
|
||||
!otherSideRelationOptions.inverseJoinColumn &&
|
||||
!MANY_TO_MANY_TRACKED_RELATIONS[
|
||||
`${relatedModelName}.${otherSideRelationshipProperty}`
|
||||
])
|
||||
|
||||
const mappedByProp = isOwner ? "inversedBy" : "mappedBy"
|
||||
const mappedByPropValue =
|
||||
mappedBy ?? inversedBy ?? otherSideRelationshipProperty
|
||||
|
||||
ManyToMany({
|
||||
owner: isOwner,
|
||||
entity: relatedModelName,
|
||||
...(pivotTableName
|
||||
? {
|
||||
@@ -402,8 +501,9 @@ export function defineManyToManyRelationship(
|
||||
}
|
||||
: {}),
|
||||
...(pivotEntityName ? { pivotEntity: pivotEntityName } : {}),
|
||||
...(mappedBy ? { mappedBy: mappedBy as any } : {}),
|
||||
...(inversedBy ? { inversedBy: inversedBy as any } : {}),
|
||||
...({ [mappedByProp]: mappedByPropValue } as any),
|
||||
...(joinColumn ? { joinColumn } : {}),
|
||||
...(inverseJoinColumn ? { inverseJoinColumn } : {}),
|
||||
})(MikroORMEntity.prototype, relationship.name)
|
||||
}
|
||||
|
||||
@@ -412,6 +512,7 @@ export function defineManyToManyRelationship(
|
||||
*/
|
||||
export function defineRelationship(
|
||||
MikroORMEntity: EntityConstructor<any>,
|
||||
entity: DmlEntity<any, any>,
|
||||
relationship: RelationshipMetadata,
|
||||
cascades: EntityCascades<string[]>,
|
||||
context: Context
|
||||
@@ -474,6 +575,7 @@ export function defineRelationship(
|
||||
case "belongsTo":
|
||||
defineBelongsToRelationship(
|
||||
MikroORMEntity,
|
||||
entity,
|
||||
relationship,
|
||||
relatedEntity,
|
||||
relatedEntityInfo
|
||||
@@ -482,6 +584,7 @@ export function defineRelationship(
|
||||
case "manyToMany":
|
||||
defineManyToManyRelationship(
|
||||
MikroORMEntity,
|
||||
entity,
|
||||
relationship,
|
||||
relatedEntity,
|
||||
relatedEntityInfo,
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
EntityIndex,
|
||||
PropertyMetadata,
|
||||
} from "@medusajs/types"
|
||||
import { MetadataStorage } from "@mikro-orm/core"
|
||||
import { createPsqlIndexStatementHelper } from "../../../common"
|
||||
import { validateIndexFields } from "../mikro-orm/build-indexes"
|
||||
|
||||
@@ -38,8 +37,7 @@ export function applyEntityIndexes(
|
||||
tableName: string,
|
||||
entityIndexes: EntityIndex[] = []
|
||||
) {
|
||||
const foreignKeyIndexes = applyForeignKeyIndexes(MikroORMEntity)
|
||||
const indexes = [...entityIndexes, ...foreignKeyIndexes]
|
||||
const indexes = [...entityIndexes]
|
||||
|
||||
indexes.forEach((index) => {
|
||||
validateIndexFields(MikroORMEntity, index)
|
||||
@@ -55,29 +53,3 @@ export function applyEntityIndexes(
|
||||
entityIndexStatement.MikroORMIndex()(MikroORMEntity)
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
When a "oneToMany" relationship is found on the MikroORM entity, we create an index by default
|
||||
on the foreign key property.
|
||||
*/
|
||||
function applyForeignKeyIndexes(MikroORMEntity: EntityConstructor<any>) {
|
||||
const foreignKeyIndexes: EntityIndex[] = []
|
||||
|
||||
for (const foreignKey of getEntityForeignKeys(MikroORMEntity)) {
|
||||
foreignKeyIndexes.push({
|
||||
on: [foreignKey],
|
||||
where: "deleted_at IS NULL",
|
||||
})
|
||||
}
|
||||
|
||||
return foreignKeyIndexes
|
||||
}
|
||||
|
||||
function getEntityForeignKeys(MikroORMEntity: EntityConstructor<any>) {
|
||||
const properties =
|
||||
MetadataStorage.getMetadataFromDecorator(MikroORMEntity).properties
|
||||
|
||||
return Object.keys(properties).filter(
|
||||
(propertyName) => properties[propertyName].isForeignKey
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import {
|
||||
MikroORM,
|
||||
} from "@mikro-orm/core"
|
||||
import { model } from "../../entity-builder"
|
||||
import { toMikroOrmEntities } from "../../helpers/create-mikro-orm-entity"
|
||||
import {
|
||||
mikroORMEntityBuilder,
|
||||
toMikroOrmEntities,
|
||||
} from "../../helpers/create-mikro-orm-entity"
|
||||
import { createDatabase, dropDatabase } from "pg-god"
|
||||
import { CustomTsMigrationGenerator, mikroOrmSerializer } from "../../../dal"
|
||||
import { EntityConstructor } from "@medusajs/types"
|
||||
@@ -28,6 +31,7 @@ describe("EntityBuilder | enum", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
MetadataStorage.clear()
|
||||
mikroORMEntityBuilder.clear()
|
||||
|
||||
const user = model.define("user", {
|
||||
id: model.id().primaryKey(),
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { MetadataStorage, MikroORM } from "@mikro-orm/core"
|
||||
import { model } from "../../entity-builder"
|
||||
import { toMikroOrmEntities } from "../../helpers/create-mikro-orm-entity"
|
||||
import {
|
||||
mikroORMEntityBuilder,
|
||||
toMikroOrmEntities,
|
||||
} from "../../helpers/create-mikro-orm-entity"
|
||||
import { createDatabase, dropDatabase } from "pg-god"
|
||||
import { CustomTsMigrationGenerator, mikroOrmSerializer } from "../../../dal"
|
||||
import { EntityConstructor } from "@medusajs/types"
|
||||
@@ -24,6 +27,7 @@ describe("hasOne - belongTo", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
MetadataStorage.clear()
|
||||
mikroORMEntityBuilder.clear()
|
||||
|
||||
const team = model.define("team", {
|
||||
id: model.id().primaryKey(),
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { join } from "path"
|
||||
import { MetadataStorage, MikroORM } from "@mikro-orm/core"
|
||||
import { model } from "../../entity-builder"
|
||||
import { toMikroOrmEntities } from "../../helpers/create-mikro-orm-entity"
|
||||
import {
|
||||
mikroORMEntityBuilder,
|
||||
toMikroOrmEntities,
|
||||
} from "../../helpers/create-mikro-orm-entity"
|
||||
import { createDatabase, dropDatabase } from "pg-god"
|
||||
import { CustomTsMigrationGenerator, mikroOrmSerializer } from "../../../dal"
|
||||
import { EntityConstructor } from "@medusajs/types"
|
||||
import { pgGodCredentials } from "../utils"
|
||||
import { FileSystem } from "../../../common"
|
||||
import { join } from "path"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
export const fileSystem = new FileSystem(
|
||||
join(__dirname, "../../integration-tests-migrations-many-to-many")
|
||||
@@ -27,6 +32,7 @@ describe("manyToMany - manyToMany", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
MetadataStorage.clear()
|
||||
mikroORMEntityBuilder.clear()
|
||||
|
||||
const team = model.define("team", {
|
||||
id: model.id().primaryKey(),
|
||||
@@ -191,43 +197,108 @@ describe("manyToMany - manyToMany", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it(`should fail to load the dml's if both side of the relation are missing the mappedBy options`, () => {
|
||||
it(`should not fail to load the dml's if both side of the relation are missing the mappedBy options`, () => {
|
||||
mikroORMEntityBuilder.clear()
|
||||
|
||||
const team = model.define("team", {
|
||||
id: model.id().primaryKey(),
|
||||
name: model.text(),
|
||||
users: model.manyToMany(() => user, {
|
||||
pivotEntity: () => squad,
|
||||
pivot_table: "team_users",
|
||||
}),
|
||||
})
|
||||
|
||||
const squad = model.define("teamUsers", {
|
||||
const user = model.define("user", {
|
||||
id: model.id().primaryKey(),
|
||||
user: model.belongsTo(() => user, { mappedBy: "squads" }),
|
||||
squad: model.belongsTo(() => team, { mappedBy: "users" }),
|
||||
username: model.text(),
|
||||
squads: model.manyToMany(() => team),
|
||||
})
|
||||
|
||||
;[User, Team] = toMikroOrmEntities([user, team])
|
||||
|
||||
const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team)
|
||||
expect((teamMetaData.properties as any).users.mappedBy).toBe("squads")
|
||||
expect((teamMetaData.properties as any).users.owner).toBe(false)
|
||||
|
||||
const userMetaData = MetadataStorage.getMetadataFromDecorator(User)
|
||||
expect((userMetaData.properties as any).squads.mappedBy).not.toBeDefined()
|
||||
expect((userMetaData.properties as any).squads.inversedBy).toBe("users")
|
||||
expect((userMetaData.properties as any).squads.owner).toBe(true)
|
||||
})
|
||||
|
||||
it(`should load the dml's correclty when both side of the relation are specifying the mappedBy options without pivot table`, () => {
|
||||
mikroORMEntityBuilder.clear()
|
||||
|
||||
const team = model.define("team", {
|
||||
id: model.id().primaryKey(),
|
||||
name: model.text(),
|
||||
users: model.manyToMany(() => user, {
|
||||
mappedBy: "squads",
|
||||
}),
|
||||
})
|
||||
|
||||
const user = model.define("user", {
|
||||
id: model.id().primaryKey(),
|
||||
username: model.text(),
|
||||
squads: model.manyToMany(() => team, {
|
||||
pivotEntity: () => squad,
|
||||
mappedBy: "users",
|
||||
}),
|
||||
})
|
||||
|
||||
let [User, Team] = toMikroOrmEntities([user, team])
|
||||
|
||||
const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team)
|
||||
expect((teamMetaData.properties as any).users.mappedBy).toBe("squads")
|
||||
expect((teamMetaData.properties as any).users.owner).toBe(false)
|
||||
expect((teamMetaData.properties as any).users.pivotTable).toBe("team_users")
|
||||
|
||||
const userMetaData = MetadataStorage.getMetadataFromDecorator(User)
|
||||
expect((userMetaData.properties as any).squads.mappedBy).not.toBeDefined()
|
||||
expect((userMetaData.properties as any).squads.inversedBy).toBe("users")
|
||||
expect((userMetaData.properties as any).squads.owner).toBe(true)
|
||||
expect((userMetaData.properties as any).squads.pivotTable).toBe(
|
||||
"team_users"
|
||||
)
|
||||
})
|
||||
|
||||
it(`should fail to load the dml's if both side of the relation are missing the mappedBy options and multiple relations points to the same entity`, () => {
|
||||
mikroORMEntityBuilder.clear()
|
||||
|
||||
const team = model.define("team", {
|
||||
id: model.id().primaryKey(),
|
||||
name: model.text(),
|
||||
users: model.manyToMany(() => user, {
|
||||
pivot_table: "team_users",
|
||||
}),
|
||||
users2: model.manyToMany(() => user, {
|
||||
pivot_table: "team_users2",
|
||||
}),
|
||||
})
|
||||
|
||||
const user = model.define("user", {
|
||||
id: model.id().primaryKey(),
|
||||
username: model.text(),
|
||||
squads: model.manyToMany(() => team),
|
||||
squads2: model.manyToMany(() => team),
|
||||
})
|
||||
|
||||
let error!: Error
|
||||
|
||||
try {
|
||||
;[User, Squad, Team] = toMikroOrmEntities([user, squad, team])
|
||||
;[User, Team] = toMikroOrmEntities([user, team])
|
||||
} catch (e) {
|
||||
error = e
|
||||
}
|
||||
|
||||
expect(error).toBeTruthy()
|
||||
expect(error.message).toEqual(
|
||||
'Invalid relationship reference for "User.squads". "mappedBy" should be defined on one side or the other.'
|
||||
expect(error).toBeDefined()
|
||||
expect(error?.message).toEqual(
|
||||
'Invalid relationship reference for "user.squads". Make sure to set the mappedBy property on one side or the other or both.'
|
||||
)
|
||||
})
|
||||
|
||||
it(`should fail to load the dml's if the relation is defined only on one side`, () => {
|
||||
mikroORMEntityBuilder.clear()
|
||||
|
||||
const team = model.define("team", {
|
||||
id: model.id().primaryKey(),
|
||||
name: model.text(),
|
||||
@@ -248,7 +319,7 @@ describe("manyToMany - manyToMany", () => {
|
||||
|
||||
expect(error).toBeTruthy()
|
||||
expect(error.message).toEqual(
|
||||
'Invalid relationship reference for "Team.users". The other side of the relationship is missing.'
|
||||
'Invalid relationship reference for "Team.users". "mappedBy" should be defined on one side or the other.'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { MetadataStorage, MikroORM } from "@mikro-orm/core"
|
||||
import { model } from "../../entity-builder"
|
||||
import { toMikroOrmEntities } from "../../helpers/create-mikro-orm-entity"
|
||||
import {
|
||||
mikroORMEntityBuilder,
|
||||
toMikroOrmEntities,
|
||||
} from "../../helpers/create-mikro-orm-entity"
|
||||
import { createDatabase, dropDatabase } from "pg-god"
|
||||
import {
|
||||
CustomTsMigrationGenerator,
|
||||
@@ -30,6 +33,7 @@ describe("manyToOne - belongTo", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
MetadataStorage.clear()
|
||||
mikroORMEntityBuilder.clear()
|
||||
|
||||
const team = model.define("team", {
|
||||
id: model.id().primaryKey(),
|
||||
|
||||
+5
-2
@@ -4,9 +4,11 @@ import { MetadataStorage } from "@mikro-orm/core"
|
||||
|
||||
import { Migrations } from "../../index"
|
||||
import { FileSystem } from "../../../common"
|
||||
import { DmlEntity, model } from "../../../dml"
|
||||
import { DmlEntity, mikroORMEntityBuilder, model } from "../../../dml"
|
||||
import { defineMikroOrmCliConfig } from "../../../modules-sdk"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
const DB_HOST = process.env.DB_HOST ?? "localhost"
|
||||
const DB_USERNAME = process.env.DB_USERNAME ?? ""
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD ?? " "
|
||||
@@ -29,7 +31,8 @@ describe("Generate migrations", () => {
|
||||
afterEach(async () => {
|
||||
await fs.cleanup()
|
||||
MetadataStorage.clear()
|
||||
}, 300 * 1000)
|
||||
mikroORMEntityBuilder.clear()
|
||||
})
|
||||
|
||||
test("generate migrations for a single entity", async () => {
|
||||
const User = model.define("User", {
|
||||
|
||||
@@ -282,7 +282,7 @@ describe("joiner-config-builder", () => {
|
||||
})
|
||||
})
|
||||
|
||||
it.only("should return a full joiner configuration with custom aliases overriding defaults", () => {
|
||||
it("should return a full joiner configuration with custom aliases overriding defaults", () => {
|
||||
const joinerConfig = defineJoinerConfig(Modules.FULFILLMENT, {
|
||||
models: [FulfillmentSet],
|
||||
alias: [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DAL, FindConfig } from "@medusajs/types"
|
||||
import { DAL, FindConfig, InferRepositoryReturnType } from "@medusajs/types"
|
||||
import { deduplicate, isObject } from "../common"
|
||||
|
||||
import { SoftDeletableFilterKey } from "../dal/mikro-orm/mikro-orm-soft-deletable-filter"
|
||||
@@ -10,17 +10,19 @@ type FilterFlags = {
|
||||
withDeleted?: boolean
|
||||
}
|
||||
|
||||
export function buildQuery<T = any, TDto = any>(
|
||||
export function buildQuery<const T = any>(
|
||||
filters: Record<string, any> = {},
|
||||
config: FindConfig<TDto> & { primaryKeyFields?: string | string[] } = {}
|
||||
config: FindConfig<InferRepositoryReturnType<T>> & {
|
||||
primaryKeyFields?: string | string[]
|
||||
} = {}
|
||||
): Required<DAL.FindOptions<T>> {
|
||||
const where: DAL.FilterQuery<T> = {}
|
||||
const where = {} as DAL.FilterQuery<T>
|
||||
const filterFlags: FilterFlags = {}
|
||||
buildWhere(filters, where, filterFlags)
|
||||
|
||||
delete config.primaryKeyFields
|
||||
|
||||
const findOptions: DAL.OptionsQuery<T, any> = {
|
||||
const findOptions: DAL.FindOptions<T>["options"] = {
|
||||
populate: deduplicate(config.relations ?? []),
|
||||
fields: config.select as string[],
|
||||
limit: (Number.isSafeInteger(config.take) && config.take) || undefined,
|
||||
@@ -28,7 +30,9 @@ export function buildQuery<T = any, TDto = any>(
|
||||
}
|
||||
|
||||
if (config.order) {
|
||||
findOptions.orderBy = config.order as DAL.OptionsQuery<T>["orderBy"]
|
||||
findOptions.orderBy = config.order as Required<
|
||||
DAL.FindOptions<T>
|
||||
>["options"]["orderBy"]
|
||||
}
|
||||
|
||||
if (config.withDeleted || filterFlags.withDeleted) {
|
||||
@@ -50,7 +54,7 @@ export function buildQuery<T = any, TDto = any>(
|
||||
Object.assign(findOptions, config.options)
|
||||
}
|
||||
|
||||
return { where, options: findOptions }
|
||||
return { where, options: findOptions } as Required<DAL.FindOptions<T>>
|
||||
}
|
||||
|
||||
function buildWhere(
|
||||
|
||||
@@ -314,6 +314,7 @@ ${serviceBObj.module}: {
|
||||
|
||||
const isModuleAPrimaryKeyValid =
|
||||
moduleAPrimaryKeys.includes(serviceAPrimaryKey)
|
||||
|
||||
if (!isModuleAPrimaryKeyValid) {
|
||||
throw new Error(
|
||||
`Primary key ${serviceAPrimaryKey} is not defined on service ${serviceAObj.module}`
|
||||
|
||||
@@ -9,7 +9,6 @@ import * as path from "path"
|
||||
import { dirname, join, normalize } from "path"
|
||||
import {
|
||||
camelToSnakeCase,
|
||||
deduplicate,
|
||||
getCallerFilePath,
|
||||
isObject,
|
||||
lowerCaseFirst,
|
||||
@@ -165,29 +164,40 @@ export function defineJoinerConfig(
|
||||
}
|
||||
linkableKeys = mergedLinkableKeys
|
||||
|
||||
if (!primaryKeys && modelDefinitions.size) {
|
||||
/**
|
||||
* Merge custom primary keys from the joiner config with the infered primary keys
|
||||
* from the models.
|
||||
*
|
||||
* TODO: Maybe worth looking into the real needs for primary keys.
|
||||
* It can happen that we could just remove that but we need to investigate (looking at the
|
||||
* lookups from the remote joiner to identify which entity a property refers to)
|
||||
*/
|
||||
primaryKeys ??= []
|
||||
const finalPrimaryKeys = new Set(primaryKeys)
|
||||
if (modelDefinitions.size) {
|
||||
const linkConfig = buildLinkConfigFromModelObjects(
|
||||
serviceName,
|
||||
Object.fromEntries(modelDefinitions)
|
||||
)
|
||||
|
||||
primaryKeys = deduplicate(
|
||||
Object.values(linkConfig).flatMap((entityLinkConfig) => {
|
||||
return (Object.values(entityLinkConfig as any) as any[])
|
||||
.filter((linkableConfig) => isObject(linkableConfig))
|
||||
.map((linkableConfig) => {
|
||||
// @ts-ignore
|
||||
return linkableConfig.primaryKey
|
||||
})
|
||||
})
|
||||
)
|
||||
Object.values(linkConfig).flatMap((entityLinkConfig) => {
|
||||
return Object.values(
|
||||
entityLinkConfig as Record<string, { primaryKey: string }>
|
||||
)
|
||||
.filter((linkableConfig) => isObject(linkableConfig))
|
||||
.forEach((linkableConfig) => {
|
||||
finalPrimaryKeys.add(linkableConfig.primaryKey)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
primaryKeys = Array.from(finalPrimaryKeys.add("id"))
|
||||
|
||||
// TODO: In the context of DML add a validation on primary keys and linkable keys if the consumer provide them manually. follow up pr
|
||||
|
||||
return {
|
||||
serviceName,
|
||||
primaryKeys: primaryKeys ?? ["id"],
|
||||
primaryKeys,
|
||||
schema,
|
||||
linkableKeys: linkableKeys,
|
||||
alias: [
|
||||
@@ -342,26 +352,44 @@ export function buildLinkableKeysFromMikroOrmObjects(
|
||||
export function buildLinkConfigFromModelObjects<
|
||||
const ServiceName extends string,
|
||||
const T extends Record<string, IDmlEntity<any, any>>
|
||||
>(serviceName: ServiceName, models: T): InfersLinksConfig<ServiceName, T> {
|
||||
>(
|
||||
serviceName: ServiceName,
|
||||
models: T,
|
||||
linkableKeys: Record<string, string> = {}
|
||||
): InfersLinksConfig<ServiceName, T> {
|
||||
// In case some models have been provided to a custom joiner config, the linkable will be limited
|
||||
// to that set of models. We dont want to expose models that should not be linkable.
|
||||
const linkableModels = Object.values(linkableKeys)
|
||||
const linkConfig = {} as InfersLinksConfig<ServiceName, T>
|
||||
|
||||
for (const model of Object.values(models) ?? []) {
|
||||
if (!DmlEntity.isDmlEntity(model)) {
|
||||
const classLikeModelName = upperCaseFirst(model.name)
|
||||
|
||||
if (
|
||||
!DmlEntity.isDmlEntity(model) ||
|
||||
(linkableModels.length && !linkableModels.includes(classLikeModelName))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
const schema = model.schema
|
||||
// @ts-ignore
|
||||
|
||||
/**
|
||||
* When using a linkable, if a specific linkable property is not specified, the toJSON
|
||||
* function will be called and return the first linkable available for this model.
|
||||
*/
|
||||
const modelLinkConfig = (linkConfig[lowerCaseFirst(model.name)] ??= {
|
||||
toJSON: function () {
|
||||
const linkables = Object.entries(this)
|
||||
.filter(([name]) => name !== "toJSON")
|
||||
.map(([, object]) => object)
|
||||
const lastIndex = linkables.length - 1
|
||||
return linkables[lastIndex]
|
||||
return linkables[0]
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Build all linkable properties for the model
|
||||
*/
|
||||
for (const [property, value] of Object.entries(schema)) {
|
||||
if (BaseRelationship.isRelationship(value)) {
|
||||
continue
|
||||
@@ -378,10 +406,46 @@ export function buildLinkConfigFromModelObjects<
|
||||
primaryKey: property,
|
||||
serviceName,
|
||||
field: lowerCaseFirst(model.name),
|
||||
entity: upperCaseFirst(model.name),
|
||||
entity: classLikeModelName,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the joiner config specify some custom linkable keys, we merge them with the
|
||||
* existing linkable keys infered from the model above.
|
||||
*/
|
||||
const linkableKeysPerModel = Object.entries(linkableKeys).reduce(
|
||||
(acc, [key, entityName]) => {
|
||||
acc[entityName] ??= []
|
||||
acc[entityName].push(key)
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
for (const linkableKey of linkableKeysPerModel[classLikeModelName] ?? []) {
|
||||
const snakeCasedModelName = camelToSnakeCase(toCamelCase(model.name))
|
||||
|
||||
// Linkable keys by default are prepared with snake cased model name _id
|
||||
// So to be able to compare only the property we have to remove the first part
|
||||
const inferredReferenceProperty = linkableKey.replace(
|
||||
`${snakeCasedModelName}_`,
|
||||
""
|
||||
)
|
||||
|
||||
if (modelLinkConfig[inferredReferenceProperty]) {
|
||||
continue
|
||||
}
|
||||
|
||||
modelLinkConfig[linkableKey] = {
|
||||
linkable: linkableKey,
|
||||
primaryKey: linkableKey,
|
||||
serviceName,
|
||||
field: lowerCaseFirst(model.name),
|
||||
entity: upperCaseFirst(model.name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return linkConfig as InfersLinksConfig<ServiceName, T>
|
||||
|
||||
@@ -2,9 +2,9 @@ import {
|
||||
BaseFilterable,
|
||||
Context,
|
||||
FilterQuery,
|
||||
FilterQuery as InternalFilterQuery,
|
||||
FindConfig,
|
||||
InferEntityType,
|
||||
FilterQuery as InternalFilterQuery,
|
||||
ModulesSdkTypes,
|
||||
PerformedActions,
|
||||
UpsertWithReplaceConfig,
|
||||
@@ -62,7 +62,7 @@ export function MedusaInternalService<
|
||||
}
|
||||
|
||||
static applyFreeTextSearchFilter(
|
||||
filters: FilterQuery,
|
||||
filters: FilterQuery & { q?: string },
|
||||
config: FindConfig<any>
|
||||
): void {
|
||||
if (isDefined(filters?.q)) {
|
||||
|
||||
@@ -49,15 +49,22 @@ export function Module<
|
||||
DmlEntity.isDmlEntity(model)
|
||||
)
|
||||
|
||||
// TODO: Custom joiner config should take precedence over the DML auto generated linkable
|
||||
// Thats in the case of manually providing models in custom joiner config.
|
||||
// TODO: Add support for non linkable modifier DML object to be skipped from the linkable generation
|
||||
|
||||
const linkableKeys = service.prototype.__joinerConfig().linkableKeys
|
||||
|
||||
if (dmlObjects.length) {
|
||||
linkable = buildLinkConfigFromModelObjects<ServiceName, ModelObjects>(
|
||||
serviceName,
|
||||
modelObjects
|
||||
modelObjects,
|
||||
linkableKeys
|
||||
) as Linkable
|
||||
} else {
|
||||
linkable = buildLinkConfigFromLinkableKeys(
|
||||
serviceName,
|
||||
service.prototype.__joinerConfig().linkableKeys
|
||||
linkableKeys
|
||||
) as Linkable
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user