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:
Harminder Virk
2024-11-26 20:01:02 +05:30
committed by GitHub
co-authored by adrien2p Oli Juhl
parent d6fa912b22
commit 9f204817b0
61 changed files with 2315 additions and 1939 deletions
+1 -1
View File
@@ -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"
}
}
@@ -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(),
@@ -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
}
}