fix: DML relation management for many to one relation ship foreign keys (#7790)

FIXES CORE-2369

cc @thetutlage 

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2024-06-24 20:54:54 +02:00
committed by GitHub
parent 25210369d9
commit 34c44078e7
9 changed files with 626 additions and 235 deletions

View File

@@ -97,17 +97,36 @@ export interface EntityConstructor<Props> extends Function {
new (): Props
}
/**
* From a IDmlEntity, infer the foreign keys name and type for belongsTo relation meaning hasOne and ManyToOne
*/
export type InferForeignKeys<T> = T extends IDmlEntity<infer Schema>
? {
[K in keyof Schema as Schema[K] extends RelationshipType<any>
? Schema[K]["type"] extends "belongsTo"
? `${K & string}_id`
: K
: K]: Schema[K] extends RelationshipType<infer R>
? Schema[K]["type"] extends "belongsTo"
? string
: Schema[K]
: Schema[K]
}
: never
/**
* Helper to infer the schema type of a DmlEntity
*/
export type Infer<T> = T extends IDmlEntity<infer Schema>
? EntityConstructor<{
[K in keyof Schema]: Schema[K]["$dataType"] extends () => infer R
? Infer<R>
: Schema[K]["$dataType"] extends (() => infer R) | null
? Infer<R> | null
: Schema[K]["$dataType"]
}>
? EntityConstructor<
{
[K in keyof Schema]: Schema[K]["$dataType"] extends () => infer R
? Infer<R>
: Schema[K]["$dataType"] extends (() => infer R) | null
? Infer<R> | null
: Schema[K]["$dataType"]
} & InferForeignKeys<T>
>
: never
/**
@@ -133,7 +152,7 @@ export type EntityCascades<Relationships> = {
}
/**
* Helper to infer the instance type of a DmlEntity once converted as an Entity
* Helper to infer the instance type of a IDmlEntity once converted as an Entity
*/
export type InferTypeOf<T extends IDmlEntity<any>> = InstanceType<Infer<T>>

View File

@@ -1,12 +1,12 @@
import { dirname, join } from "path"
import {
promises,
constants,
type Dirent,
type MakeDirectoryOptions,
promises,
type RmOptions,
type StatOptions,
type WriteFileOptions,
type MakeDirectoryOptions,
} from "fs"
const { rm, stat, mkdir, access, readdir, readFile, writeFile } = promises
@@ -31,7 +31,7 @@ export class FileSystem {
* Cleanup directory
*/
async cleanup(options?: RmOptions) {
return rm(this.basePath, {
return await rm(this.basePath, {
recursive: true,
maxRetries: 10,
force: true,
@@ -61,7 +61,7 @@ export class FileSystem {
* Remove a file
*/
async remove(filePath: string, options?: RmOptions) {
return rm(this.makePath(filePath), {
return await rm(this.makePath(filePath), {
recursive: true,
force: true,
maxRetries: 2,
@@ -103,7 +103,7 @@ export class FileSystem {
* Returns file contents
*/
async contents(filePath: string) {
return readFile(this.makePath(filePath), "utf-8")
return await readFile(this.makePath(filePath), "utf-8")
}
/**
@@ -139,14 +139,14 @@ export class FileSystem {
async createJson(filePath: string, contents: any, options?: JSONFileOptions) {
if (options && typeof options === "object") {
const { replacer, spaces, ...rest } = options
return this.create(
return await this.create(
filePath,
JSON.stringify(contents, replacer, spaces),
rest
)
}
return this.create(filePath, JSON.stringify(contents), options)
return await this.create(filePath, JSON.stringify(contents), options)
}
/**

View File

@@ -1,12 +1,13 @@
import { EntityConstructor } from "@medusajs/types"
import { MetadataStorage } from "@mikro-orm/core"
import { expectTypeOf } from "expect-type"
import { DmlEntity } from "../entity"
import { EntityBuilder } from "../entity-builder"
import { model } from "../entity-builder"
import {
createMikrORMEntity,
toMikroOrmEntities,
toMikroORMEntity,
} from "../helpers/create-mikro-orm-entity"
import { EntityConstructor } from "../types"
describe("Entity builder", () => {
beforeEach(() => {
@@ -15,7 +16,6 @@ describe("Entity builder", () => {
describe("Entity builder | properties", () => {
test("should identify a DML entity correctly", () => {
const model = new EntityBuilder()
const user = model.define("user", {
id: model.number(),
username: model.text(),
@@ -30,7 +30,6 @@ describe("Entity builder", () => {
})
test("define an entity", () => {
const model = new EntityBuilder()
const user = model.define("user", {
id: model.number(),
username: model.text(),
@@ -38,8 +37,7 @@ describe("Entity builder", () => {
spend_limit: model.bigNumber(),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
@@ -148,7 +146,6 @@ describe("Entity builder", () => {
})
test("define a property with default value", () => {
const model = new EntityBuilder()
const user = model.define("user", {
id: model.number(),
username: model.text().default("foo"),
@@ -156,8 +153,7 @@ describe("Entity builder", () => {
spend_limit: model.bigNumber().default(500.4),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
@@ -263,7 +259,6 @@ describe("Entity builder", () => {
})
test("mark property nullable", () => {
const model = new EntityBuilder()
const user = model.define("user", {
id: model.number(),
username: model.text().nullable(),
@@ -398,7 +393,6 @@ describe("Entity builder", () => {
})
test("define an entity with enum property", () => {
const model = new EntityBuilder()
const user = model.define("user", {
id: model.number(),
username: model.text(),
@@ -406,8 +400,7 @@ describe("Entity builder", () => {
role: model.enum(["moderator", "admin", "guest"]),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
@@ -507,7 +500,6 @@ describe("Entity builder", () => {
})
test("define enum property with default value", () => {
const model = new EntityBuilder()
const user = model.define("user", {
id: model.number(),
username: model.text(),
@@ -515,8 +507,7 @@ describe("Entity builder", () => {
role: model.enum(["moderator", "admin", "guest"]).default("guest"),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
@@ -616,7 +607,6 @@ describe("Entity builder", () => {
})
test("mark enum property nullable", () => {
const model = new EntityBuilder()
const user = model.define("user", {
id: model.number(),
username: model.text(),
@@ -624,8 +614,7 @@ describe("Entity builder", () => {
role: model.enum(["moderator", "admin", "guest"]).nullable(),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
@@ -730,7 +719,6 @@ describe("Entity builder", () => {
})
test("disallow defining created_at and updated_at timestamps", () => {
const model = new EntityBuilder()
expect(() =>
model.define("user", {
id: model.number(),
@@ -745,7 +733,6 @@ describe("Entity builder", () => {
})
test("disallow defining deleted_at timestamp", () => {
const model = new EntityBuilder()
expect(() =>
model.define("user", {
id: model.number(),
@@ -759,15 +746,13 @@ describe("Entity builder", () => {
})
test("define pg schema name in the entity name", () => {
const model = new EntityBuilder()
const user = model.define("public.user", {
id: model.number(),
username: model.text(),
email: model.text(),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
@@ -857,15 +842,13 @@ describe("Entity builder", () => {
describe("Entity builder | id", () => {
test("define an entity with id property", () => {
const model = new EntityBuilder()
const user = model.define("user", {
id: model.id(),
username: model.text(),
email: model.text(),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: string
username: string
@@ -956,15 +939,13 @@ describe("Entity builder", () => {
})
test("mark id as non-primary", () => {
const model = new EntityBuilder()
const user = model.define("user", {
id: model.id({ primaryKey: false }),
username: model.text(),
email: model.text(),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: string
username: string
@@ -1061,15 +1042,13 @@ describe("Entity builder", () => {
})
test("define prefix for the id", () => {
const model = new EntityBuilder()
const user = model.define("user", {
id: model.id({ primaryKey: false, prefix: "us" }),
username: model.text(),
email: model.text(),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: string
username: string
@@ -1168,7 +1147,6 @@ describe("Entity builder", () => {
describe("Entity builder | primaryKey", () => {
test("should create both id fields and primaryKey fields", () => {
const model = new EntityBuilder()
const user = model.define("user", {
id: model.id({ primaryKey: false }),
email: model.text().primaryKey(),
@@ -1271,15 +1249,13 @@ describe("Entity builder", () => {
describe("Entity builder | indexes", () => {
test("define index on a field", () => {
const model = new EntityBuilder()
const user = model.define("user", {
id: model.number().index(),
username: model.text(),
email: model.text().unique(),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
@@ -1377,15 +1353,13 @@ describe("Entity builder", () => {
})
test("define index when entity is using an explicit pg Schema", () => {
const model = new EntityBuilder()
const user = model.define("platform.user", {
id: model.number().index(),
username: model.text(),
email: model.text().unique(),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
@@ -1486,7 +1460,6 @@ describe("Entity builder", () => {
describe("Entity builder | hasOne", () => {
test("define hasOne relationship", () => {
const model = new EntityBuilder()
const email = model.define("email", {
email: model.text(),
isVerified: model.boolean(),
@@ -1498,8 +1471,7 @@ describe("Entity builder", () => {
email: model.hasOne(() => email),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
@@ -1576,7 +1548,6 @@ describe("Entity builder", () => {
})
test("mark hasOne relationship as nullable", () => {
const model = new EntityBuilder()
const email = model.define("email", {
email: model.text(),
isVerified: model.boolean(),
@@ -1588,8 +1559,7 @@ describe("Entity builder", () => {
emails: model.hasOne(() => email).nullable(),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
@@ -1667,7 +1637,6 @@ describe("Entity builder", () => {
})
test("define custom mappedBy key for relationship", () => {
const model = new EntityBuilder()
const email = model.define("email", {
email: model.text(),
isVerified: model.boolean(),
@@ -1679,8 +1648,7 @@ describe("Entity builder", () => {
email: model.hasOne(() => email, { mappedBy: "owner" }),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
@@ -1752,7 +1720,6 @@ describe("Entity builder", () => {
})
test("define delete cascades for the entity", () => {
const model = new EntityBuilder()
const email = model.define("email", {
email: model.text(),
isVerified: model.boolean(),
@@ -1768,8 +1735,7 @@ describe("Entity builder", () => {
delete: ["email"],
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
@@ -1840,7 +1806,7 @@ describe("Entity builder", () => {
},
})
const Email = entityBuilder(email)
const Email = toMikroORMEntity(email)
const emailMetaData = MetadataStorage.getMetadataFromDecorator(Email)
expect(emailMetaData.className).toEqual("Email")
expect(emailMetaData.path).toEqual("Email")
@@ -1899,7 +1865,6 @@ describe("Entity builder", () => {
})
test("define delete cascades with belongsTo on the other end", () => {
const model = new EntityBuilder()
const email = model.define("email", {
email: model.text(),
isVerified: model.boolean(),
@@ -1916,8 +1881,7 @@ describe("Entity builder", () => {
delete: ["email"],
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
@@ -1995,7 +1959,7 @@ describe("Entity builder", () => {
},
})
const Email = entityBuilder(email)
const Email = toMikroORMEntity(email)
const emailMetaData = MetadataStorage.getMetadataFromDecorator(Email)
expect(emailMetaData.className).toEqual("Email")
expect(emailMetaData.path).toEqual("Email")
@@ -2027,6 +1991,15 @@ describe("Entity builder", () => {
owner: true,
reference: "1:1",
},
user_id: {
columnType: "text",
getter: false,
name: "user_id",
nullable: false,
reference: "scalar",
setter: false,
type: "string",
},
created_at: {
reference: "scalar",
type: "date",
@@ -2065,7 +2038,6 @@ describe("Entity builder", () => {
describe("Entity builder | hasMany", () => {
test("define hasMany relationship", () => {
const model = new EntityBuilder()
const email = model.define("email", {
email: model.text(),
isVerified: model.boolean(),
@@ -2077,8 +2049,7 @@ describe("Entity builder", () => {
emails: model.hasMany(() => email),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
@@ -2150,7 +2121,6 @@ describe("Entity builder", () => {
})
test("define custom mappedBy property name for hasMany relationship", () => {
const model = new EntityBuilder()
const email = model.define("email", {
email: model.text(),
isVerified: model.boolean(),
@@ -2164,8 +2134,7 @@ describe("Entity builder", () => {
}),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
@@ -2238,7 +2207,6 @@ describe("Entity builder", () => {
})
test("define delete cascades for the entity", () => {
const model = new EntityBuilder()
const email = model.define("email", {
email: model.text(),
isVerified: model.boolean(),
@@ -2254,8 +2222,7 @@ describe("Entity builder", () => {
delete: ["emails"],
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const User = toMikroORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
@@ -2328,7 +2295,6 @@ describe("Entity builder", () => {
})
test("define delete cascades with belongsTo on the other end", () => {
const model = new EntityBuilder()
const email = model.define("email", {
email: model.text(),
isVerified: model.boolean(),
@@ -2345,9 +2311,8 @@ describe("Entity builder", () => {
delete: ["emails"],
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const Email = entityBuilder(email)
const User = toMikroORMEntity(user)
const Email = toMikroORMEntity(email)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
@@ -2441,10 +2406,11 @@ describe("Entity builder", () => {
setter: false,
},
user: {
reference: "m:1",
name: "user",
entity: "User",
name: "user",
nullable: false,
persist: false,
reference: "m:1",
},
user_id: {
columnType: "text",
@@ -2453,8 +2419,8 @@ describe("Entity builder", () => {
mapToPk: true,
name: "user_id",
nullable: false,
reference: "m:1",
onDelete: "cascade",
reference: "m:1",
},
created_at: {
reference: "scalar",
@@ -2494,8 +2460,6 @@ describe("Entity builder", () => {
describe("Entity builder | belongsTo", () => {
test("define belongsTo relationship with hasOne", () => {
const model = new EntityBuilder()
const email = model.define("email", {
email: model.text(),
isVerified: model.boolean(),
@@ -2508,9 +2472,7 @@ describe("Entity builder", () => {
email: model.hasOne(() => email),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const Email = entityBuilder(email)
const [User, Email] = [user, email].map(toMikroORMEntity)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
@@ -2630,12 +2592,21 @@ describe("Entity builder", () => {
setter: false,
},
user: {
reference: "1:1",
name: "user",
reference: "1:1",
entity: "User",
nullable: false,
owner: true,
mappedBy: "email",
owner: true,
},
user_id: {
reference: "scalar",
type: "string",
columnType: "text",
nullable: false,
name: "user_id",
getter: false,
setter: false,
},
created_at: {
reference: "scalar",
@@ -2673,8 +2644,6 @@ describe("Entity builder", () => {
})
test("mark belongsTo with hasOne as nullable", () => {
const model = new EntityBuilder()
const email = model.define("email", {
email: model.text(),
isVerified: model.boolean(),
@@ -2687,9 +2656,8 @@ describe("Entity builder", () => {
email: model.hasOne(() => email),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const Email = entityBuilder(email)
const User = toMikroORMEntity(user)
const Email = toMikroORMEntity(email)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
@@ -2803,12 +2771,21 @@ describe("Entity builder", () => {
setter: false,
},
user: {
reference: "1:1",
name: "user",
reference: "1:1",
entity: "User",
nullable: true,
owner: true,
mappedBy: "email",
owner: true,
},
user_id: {
reference: "scalar",
type: "string",
columnType: "text",
nullable: true,
name: "user_id",
getter: false,
setter: false,
},
created_at: {
reference: "scalar",
@@ -2846,8 +2823,6 @@ describe("Entity builder", () => {
})
test("define belongsTo relationship with hasMany", () => {
const model = new EntityBuilder()
const email = model.define("email", {
email: model.text(),
isVerified: model.boolean(),
@@ -2860,9 +2835,8 @@ describe("Entity builder", () => {
emails: model.hasMany(() => email),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const Email = entityBuilder(email)
const User = toMikroORMEntity(user)
const Email = toMikroORMEntity(email)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
@@ -2976,19 +2950,20 @@ describe("Entity builder", () => {
setter: false,
},
user: {
reference: "m:1",
name: "user",
reference: "m:1",
entity: "User",
persist: false,
nullable: false,
},
user_id: {
columnType: "text",
entity: "User",
fieldName: "user_id",
mapToPk: true,
name: "user_id",
nullable: false,
reference: "m:1",
entity: "User",
columnType: "text",
mapToPk: true,
fieldName: "user_id",
nullable: false,
},
created_at: {
reference: "scalar",
@@ -3026,8 +3001,6 @@ describe("Entity builder", () => {
})
test("define belongsTo with hasMany as nullable", () => {
const model = new EntityBuilder()
const email = model.define("email", {
email: model.text(),
isVerified: model.boolean(),
@@ -3040,9 +3013,8 @@ describe("Entity builder", () => {
emails: model.hasMany(() => email),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const Email = entityBuilder(email)
const User = toMikroORMEntity(user)
const Email = toMikroORMEntity(email)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
@@ -3156,19 +3128,20 @@ describe("Entity builder", () => {
setter: false,
},
user: {
reference: "m:1",
name: "user",
reference: "m:1",
entity: "User",
persist: false,
nullable: true,
},
user_id: {
columnType: "text",
entity: "User",
fieldName: "user_id",
mapToPk: true,
name: "user_id",
nullable: true,
reference: "m:1",
entity: "User",
columnType: "text",
mapToPk: true,
fieldName: "user_id",
nullable: true,
},
created_at: {
reference: "scalar",
@@ -3206,8 +3179,6 @@ describe("Entity builder", () => {
})
test("throw error when other side relationship is missing", () => {
const model = new EntityBuilder()
const email = model.define("email", {
email: model.text(),
isVerified: model.boolean(),
@@ -3219,15 +3190,12 @@ describe("Entity builder", () => {
username: model.text(),
})
const entityBuilder = createMikrORMEntity()
expect(() => entityBuilder(email)).toThrow(
expect(() => toMikroORMEntity(email)).toThrow(
'Missing property "email" on "User" entity. Make sure to define it as a relationship'
)
})
test("throw error when other side relationship is invalid", () => {
const model = new EntityBuilder()
const email = model.define("email", {
email: model.text(),
isVerified: model.boolean(),
@@ -3240,15 +3208,12 @@ describe("Entity builder", () => {
email: model.belongsTo(() => email),
})
const entityBuilder = createMikrORMEntity()
expect(() => entityBuilder(email)).toThrow(
expect(() => toMikroORMEntity(email)).toThrow(
'Invalid relationship reference for "email" on "User" entity. Make sure to define a hasOne or hasMany relationship'
)
})
test("throw error when cascading a parent from a child", () => {
const model = new EntityBuilder()
const user = model.define("user", {
id: model.number(),
username: model.text(),
@@ -3272,8 +3237,6 @@ describe("Entity builder", () => {
})
test("define relationships when entity names has pg schema name", () => {
const model = new EntityBuilder()
const email = model.define("platform.email", {
email: model.text(),
isVerified: model.boolean(),
@@ -3286,9 +3249,8 @@ describe("Entity builder", () => {
email: model.hasOne(() => email),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const Email = entityBuilder(email)
const User = toMikroORMEntity(user)
const Email = toMikroORMEntity(email)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
@@ -3410,12 +3372,21 @@ describe("Entity builder", () => {
setter: false,
},
user: {
reference: "1:1",
name: "user",
reference: "1:1",
entity: "User",
nullable: false,
owner: true,
mappedBy: "email",
owner: true,
},
user_id: {
reference: "scalar",
type: "string",
columnType: "text",
nullable: false,
name: "user_id",
getter: false,
setter: false,
},
created_at: {
reference: "scalar",
@@ -3453,8 +3424,6 @@ describe("Entity builder", () => {
})
test("define relationships between cross pg schemas entities", () => {
const model = new EntityBuilder()
const email = model.define("platform.email", {
email: model.text(),
isVerified: model.boolean(),
@@ -3467,9 +3436,8 @@ describe("Entity builder", () => {
email: model.hasOne(() => email),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const Email = entityBuilder(email)
const User = toMikroORMEntity(user)
const Email = toMikroORMEntity(email)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
@@ -3591,12 +3559,21 @@ describe("Entity builder", () => {
setter: false,
},
user: {
reference: "1:1",
name: "user",
reference: "1:1",
entity: "User",
nullable: false,
owner: true,
mappedBy: "email",
owner: true,
},
user_id: {
reference: "scalar",
type: "string",
columnType: "text",
nullable: false,
name: "user_id",
getter: false,
setter: false,
},
created_at: {
reference: "scalar",
@@ -3636,7 +3613,6 @@ describe("Entity builder", () => {
describe("Entity builder | manyToMany", () => {
test("define manyToMany relationship", () => {
const model = new EntityBuilder()
const team = model.define("team", {
id: model.number(),
name: model.text(),
@@ -3649,9 +3625,8 @@ describe("Entity builder", () => {
teams: model.manyToMany(() => team),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const Team = entityBuilder(team)
const User = toMikroORMEntity(user)
const Team = toMikroORMEntity(team)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
@@ -3805,7 +3780,6 @@ describe("Entity builder", () => {
})
test("define mappedBy on one side", () => {
const model = new EntityBuilder()
const team = model.define("team", {
id: model.number(),
name: model.text(),
@@ -3818,9 +3792,8 @@ describe("Entity builder", () => {
teams: model.manyToMany(() => team, { mappedBy: "users" }),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const Team = entityBuilder(team)
const User = toMikroORMEntity(user)
const Team = toMikroORMEntity(team)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
@@ -3975,7 +3948,6 @@ describe("Entity builder", () => {
})
test("throw error when unable to locate relationship via mappedBy", () => {
const model = new EntityBuilder()
const team = model.define("team", {
id: model.number(),
name: model.text(),
@@ -3987,14 +3959,12 @@ describe("Entity builder", () => {
teams: model.manyToMany(() => team, { mappedBy: "users" }),
})
const entityBuilder = createMikrORMEntity()
expect(() => entityBuilder(user)).toThrow(
expect(() => toMikroORMEntity(user)).toThrow(
'Missing property "users" on "Team" entity. Make sure to define it as a relationship'
)
})
test("throw error when mappedBy relationship is not a manyToMany", () => {
const model = new EntityBuilder()
const team = model.define("team", {
id: model.number(),
name: model.text(),
@@ -4007,14 +3977,12 @@ describe("Entity builder", () => {
teams: model.manyToMany(() => team, { mappedBy: "users" }),
})
const entityBuilder = createMikrORMEntity()
expect(() => entityBuilder(user)).toThrow(
expect(() => toMikroORMEntity(user)).toThrow(
'Invalid relationship reference for "users" on "Team" entity. Make sure to define a manyToMany relationship'
)
})
test("define mappedBy on both sides", () => {
const model = new EntityBuilder()
const team = model.define("team", {
id: model.number(),
name: model.text(),
@@ -4027,9 +3995,7 @@ describe("Entity builder", () => {
teams: model.manyToMany(() => team, { mappedBy: "users" }),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const Team = entityBuilder(team)
const [User, Team] = toMikroOrmEntities([user, team, {}])
expectTypeOf(new User()).toMatchTypeOf<{
id: number
@@ -4189,7 +4155,6 @@ describe("Entity builder", () => {
})
test("define mappedBy on both sides and reverse order of registering entities", () => {
const model = new EntityBuilder()
const team = model.define("team", {
id: model.number(),
name: model.text(),
@@ -4364,7 +4329,6 @@ describe("Entity builder", () => {
})
test("define multiple many to many relationships to the same entity", () => {
const model = new EntityBuilder()
const team = model.define("team", {
id: model.number(),
name: model.text(),
@@ -4571,7 +4535,6 @@ describe("Entity builder", () => {
})
test("define manyToMany relationship when entity names has pg schema name", () => {
const model = new EntityBuilder()
const team = model.define("platform.team", {
id: model.number(),
name: model.text(),
@@ -4584,9 +4547,8 @@ describe("Entity builder", () => {
teams: model.manyToMany(() => team),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const Team = entityBuilder(team)
const User = toMikroORMEntity(user)
const Team = toMikroORMEntity(team)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
@@ -4742,7 +4704,6 @@ describe("Entity builder", () => {
})
test("define custom pivot table name", () => {
const model = new EntityBuilder()
const team = model.define("team", {
id: model.number(),
name: model.text(),
@@ -4757,9 +4718,8 @@ describe("Entity builder", () => {
teams: model.manyToMany(() => team, { pivotTable: "users_teams" }),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const Team = entityBuilder(team)
const User = toMikroORMEntity(user)
const Team = toMikroORMEntity(team)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
@@ -4913,7 +4873,6 @@ describe("Entity builder", () => {
})
test("define custom pivot entity", () => {
const model = new EntityBuilder()
const team = model.define("team", {
id: model.number(),
name: model.text(),
@@ -4934,10 +4893,7 @@ describe("Entity builder", () => {
teams: model.manyToMany(() => team, { pivotEntity: () => squad }),
})
const entityBuilder = createMikrORMEntity()
const User = entityBuilder(user)
const Team = entityBuilder(team)
const Squad = entityBuilder(squad)
const [User, Team, Squad] = toMikroOrmEntities([user, team, squad])
expectTypeOf(new User()).toMatchTypeOf<{
id: number
@@ -4973,74 +4929,78 @@ describe("Entity builder", () => {
expect(squadMetaData.properties).toEqual({
id: {
reference: "scalar",
type: "number",
columnType: "integer",
name: "id",
type: "number",
nullable: false,
name: "id",
getter: false,
setter: false,
},
team: {
entity: "Team",
name: "team",
persist: false,
user_id: {
name: "user_id",
reference: "m:1",
},
team_id: {
entity: "User",
columnType: "text",
entity: "Team",
fieldName: "team_id",
mapToPk: true,
name: "team_id",
fieldName: "user_id",
nullable: false,
onDelete: undefined,
reference: "m:1",
},
user: {
entity: "User",
name: "user",
reference: "scalar",
type: "User",
persist: false,
reference: "m:1",
},
user_id: {
columnType: "text",
entity: "User",
fieldName: "user_id",
mapToPk: true,
name: "user_id",
nullable: false,
onDelete: undefined,
name: "user",
getter: false,
setter: false,
},
team_id: {
name: "team_id",
reference: "m:1",
entity: "Team",
columnType: "text",
mapToPk: true,
fieldName: "team_id",
nullable: false,
},
team: {
reference: "scalar",
type: "Team",
persist: false,
nullable: false,
name: "team",
getter: false,
setter: false,
},
created_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "created_at",
defaultRaw: "now()",
onCreate: expect.any(Function),
type: "date",
nullable: false,
onCreate: expect.any(Function),
defaultRaw: "now()",
name: "created_at",
getter: false,
setter: false,
},
updated_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "updated_at",
defaultRaw: "now()",
type: "date",
nullable: false,
onCreate: expect.any(Function),
onUpdate: expect.any(Function),
nullable: false,
defaultRaw: "now()",
name: "updated_at",
getter: false,
setter: false,
},
deleted_at: {
reference: "scalar",
type: "date",
columnType: "timestamptz",
name: "deleted_at",
type: "date",
nullable: true,
name: "deleted_at",
getter: false,
setter: false,
},

View File

@@ -154,6 +154,7 @@ 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_REALTIONS: Record<string, boolean> = {}
/**
@@ -390,6 +391,23 @@ export function createMikrORMEntity() {
)
}
function applyForeignKeyAssignationHooks(foreignKeyName: string) {
const hookName = `assignRelationFromForeignKeyValue${foreignKeyName}`
/**
* Hook to handle foreign key assignation
*/
MikroORMEntity.prototype[hookName] = function () {
this[relationship.name] ??= this[foreignKeyName]
this[foreignKeyName] ??= this[relationship.name]?.id
}
/**
* Execute hook via lifecycle decorators
*/
BeforeCreate()(MikroORMEntity.prototype, hookName)
OnInit()(MikroORMEntity.prototype, hookName)
}
/**
* Otherside is a has many. Hence we should defined a ManyToOne
*/
@@ -397,6 +415,8 @@ export function createMikrORMEntity() {
otherSideRelation instanceof HasMany ||
otherSideRelation instanceof DmlManyToMany
) {
const foreignKeyName = camelToSnakeCase(`${relationship.name}Id`)
ManyToOne({
entity: relatedModelName,
columnType: "text",
@@ -406,10 +426,22 @@ export function createMikrORMEntity() {
onDelete: shouldCascade ? "cascade" : undefined,
})(MikroORMEntity.prototype, camelToSnakeCase(`${relationship.name}Id`))
ManyToOne({
entity: relatedModelName,
persist: false,
})(MikroORMEntity.prototype, relationship.name)
if (otherSideRelation instanceof DmlManyToMany) {
Property({
type: relatedModelName,
persist: false,
nullable: relationship.nullable,
})(MikroORMEntity.prototype, relationship.name)
} else {
// HasMany case
ManyToOne({
entity: relatedModelName,
persist: false,
nullable: relationship.nullable,
})(MikroORMEntity.prototype, relationship.name)
}
applyForeignKeyAssignationHooks(foreignKeyName)
return
}
@@ -417,6 +449,8 @@ export function createMikrORMEntity() {
* Otherside is a has one. Hence we should defined a OneToOne
*/
if (otherSideRelation instanceof HasOne) {
const foreignKeyName = camelToSnakeCase(`${relationship.name}Id`)
OneToOne({
entity: relatedModelName,
nullable: relationship.nullable,
@@ -424,6 +458,23 @@ export function createMikrORMEntity() {
owner: true,
onDelete: shouldCascade ? "cascade" : undefined,
})(MikroORMEntity.prototype, relationship.name)
if (relationship.nullable) {
Object.defineProperty(MikroORMEntity.prototype, foreignKeyName, {
value: null,
configurable: true,
enumerable: true,
writable: true,
})
}
Property({
type: "string",
columnType: "text",
nullable: relationship.nullable,
})(MikroORMEntity.prototype, foreignKeyName)
applyForeignKeyAssignationHooks(foreignKeyName)
return
}
@@ -681,12 +732,18 @@ export function createMikrORMEntity() {
* return the input idempotently
* @param entity
*/
export const toMikroORMEntity = (entity: any) => {
export const toMikroORMEntity = <T>(
entity: T
): T extends DmlEntity<infer Schema> ? EntityConstructor<Schema> : T => {
let mikroOrmEntity: T | EntityConstructor<any> = entity
if (DmlEntity.isDmlEntity(entity)) {
return createMikrORMEntity()(entity)
mikroOrmEntity = createMikrORMEntity()(entity)
}
return entity
return mikroOrmEntity as T extends DmlEntity<infer Schema>
? EntityConstructor<Schema>
: T
}
/**
@@ -694,6 +751,16 @@ export const toMikroORMEntity = (entity: any) => {
* This action is idempotent if non of the entities are DmlEntity
* @param entities
*/
export const toMikroOrmEntities = function (entities: any[]) {
return entities.map(toMikroORMEntity)
export const toMikroOrmEntities = function <T extends any[]>(entities: T) {
const entityBuilder = createMikrORMEntity()
return entities.map((entity) => {
if (DmlEntity.isDmlEntity(entity)) {
return entityBuilder(entity)
}
return entity
}) as {
[K in keyof T]: T[K] extends DmlEntity<any> ? EntityConstructor<T[K]> : T[K]
}
}

View File

@@ -0,0 +1,193 @@
import { MetadataStorage, MikroORM } from "@mikro-orm/core"
import { model } from "../entity-builder"
import { toMikroOrmEntities } from "../helpers/create-mikro-orm-entity"
import { createDatabase, dropDatabase } from "pg-god"
import { mikroOrmSerializer, TSMigrationGenerator } from "../../dal"
import { FileSystem } from "../../common"
import { join } from "path"
import { EntityConstructor } from "@medusajs/types"
const DB_HOST = process.env.DB_HOST
const DB_USERNAME = process.env.DB_USERNAME
const DB_PASSWORD = process.env.DB_PASSWORD
const pgGodCredentials = {
user: DB_USERNAME,
password: DB_PASSWORD,
host: DB_HOST,
}
const fileSystem = new FileSystem(join(__dirname, "../../migrations"))
describe("manyToMany - manyToMany", () => {
const dbName = "EntityBuilder-ManyToMany"
let orm!: MikroORM
let Team: EntityConstructor<any>,
User: EntityConstructor<any>,
Squad: EntityConstructor<any>
afterAll(() => {
fileSystem.cleanup()
})
beforeEach(async () => {
MetadataStorage.clear()
const team = model.define("team", {
id: model.id(),
name: model.text(),
users: model.manyToMany(() => user, {
pivotEntity: () => squad,
mappedBy: "squads",
}),
})
const squad = model.define("teamUsers", {
id: model.id(),
user: model.belongsTo(() => user, { mappedBy: "squads" }),
squad: model.belongsTo(() => team, { mappedBy: "users" }),
})
const user = model.define("user", {
id: model.id(),
username: model.text(),
squads: model.manyToMany(() => team, {
pivotEntity: () => squad,
mappedBy: "users",
}),
})
;[User, Squad, Team] = toMikroOrmEntities([user, squad, team])
await createDatabase({ databaseName: dbName }, pgGodCredentials)
orm = await MikroORM.init({
entities: [Team, User, Squad],
tsNode: true,
dbName,
debug: true,
type: "postgresql",
migrations: {
generator: TSMigrationGenerator,
},
})
const migrator = orm.getMigrator()
await migrator.createMigration()
await migrator.up()
})
afterEach(async () => {
await orm.close()
await dropDatabase(
{ databaseName: dbName, errorIfNonExist: false },
pgGodCredentials
)
})
it(`should handle the relation properly`, async () => {
let manager = orm.em.fork()
const user1 = manager.create(User, {
username: "User 1",
})
const user2 = manager.create(User, {
username: "User 2",
})
await manager.persistAndFlush([user1, user2])
manager = orm.em.fork()
const team1 = manager.create(Team, {
name: "Team 1",
})
const team2 = manager.create(Team, {
name: "Team 2",
})
await manager.persistAndFlush([team1, team2])
manager = orm.em.fork()
const squad1 = manager.create(Squad, {
user_id: user1.id,
squad_id: team1.id,
})
const squad2 = manager.create(Squad, {
user_id: user2.id,
squad_id: team1.id,
})
await manager.persistAndFlush([squad1, squad2])
manager = orm.em.fork()
const team = await manager.findOne(
Team,
{
id: team1.id,
},
{
populate: ["users"],
}
)
const serializedSquad = mikroOrmSerializer<InstanceType<typeof Team>>(team)
expect(serializedSquad.users).toHaveLength(2)
expect(serializedSquad).toEqual({
id: team1.id,
name: "Team 1",
created_at: expect.any(Date),
updated_at: expect.any(Date),
deleted_at: null,
users: expect.arrayContaining([
{
id: user1.id,
username: "User 1",
created_at: expect.any(Date),
updated_at: expect.any(Date),
deleted_at: null,
},
{
id: user2.id,
username: "User 2",
created_at: expect.any(Date),
updated_at: expect.any(Date),
deleted_at: null,
},
]),
})
const user = await manager.findOne(
User,
{
id: user1.id,
},
{
populate: ["squads"],
}
)
const serializedUser = mikroOrmSerializer<InstanceType<typeof User>>(user)
expect(serializedUser.squads).toHaveLength(1)
expect(serializedUser).toEqual({
id: user1.id,
username: "User 1",
created_at: expect.any(Date),
updated_at: expect.any(Date),
deleted_at: null,
squads: expect.arrayContaining([
{
id: team1.id,
name: "Team 1",
created_at: expect.any(Date),
updated_at: expect.any(Date),
deleted_at: null,
},
]),
})
})
})

View File

@@ -0,0 +1,152 @@
import { MetadataStorage, MikroORM } from "@mikro-orm/core"
import { model } from "../entity-builder"
import { toMikroOrmEntities } from "../helpers/create-mikro-orm-entity"
import { createDatabase, dropDatabase } from "pg-god"
import { mikroOrmSerializer } from "../../dal"
import { FileSystem } from "../../common"
import { join } from "path"
import { EntityConstructor } from "@medusajs/types"
const DB_HOST = process.env.DB_HOST
const DB_USERNAME = process.env.DB_USERNAME
const DB_PASSWORD = process.env.DB_PASSWORD
const pgGodCredentials = {
user: DB_USERNAME,
password: DB_PASSWORD,
host: DB_HOST,
}
const fileSystem = new FileSystem(join(__dirname, "../../migrations"))
describe("manyToOne - belongTo", () => {
const dbName = "EntityBuilder-ManyToOne"
let orm!: MikroORM
let Team: EntityConstructor<any>, User: EntityConstructor<any>
afterAll(() => {
fileSystem.cleanup()
})
beforeEach(async () => {
MetadataStorage.clear()
const team = model.define("team", {
id: model.id(),
name: model.text(),
user: model.belongsTo(() => user, { mappedBy: "teams" }),
})
const user = model.define("user", {
id: model.id(),
username: model.text(),
teams: model.hasMany(() => team, { mappedBy: "user" }),
})
;[User, Team] = toMikroOrmEntities([user, team])
await createDatabase({ databaseName: dbName }, pgGodCredentials)
orm = await MikroORM.init({
entities: [Team, User],
tsNode: true,
dbName,
debug: true,
type: "postgresql",
})
const migrator = orm.getMigrator()
await migrator.createMigration()
await migrator.up()
})
afterEach(async () => {
await orm.close()
await dropDatabase(
{ databaseName: dbName, errorIfNonExist: false },
pgGodCredentials
)
})
it(`should handle the relation properly`, async () => {
let manager = orm.em.fork()
const user1 = manager.create(User, {
username: "User 1",
})
const user2 = manager.create(User, {
username: "User 2",
})
await manager.persistAndFlush([user1, user2])
manager = orm.em.fork()
const team1 = manager.create(Team, {
name: "Team 1",
user_id: user1.id,
})
const team2 = manager.create(Team, {
name: "Team 2",
user_id: user2.id,
})
await manager.persistAndFlush([team1, team2])
manager = orm.em.fork()
const team = await manager.findOne(
Team,
{
id: team1.id,
},
{
populate: ["user"],
}
)
expect(mikroOrmSerializer<InstanceType<typeof Team>>(team)).toEqual({
id: team1.id,
name: "Team 1",
created_at: expect.any(Date),
updated_at: expect.any(Date),
deleted_at: null,
user_id: user1.id,
user: {
id: user1.id,
username: "User 1",
created_at: expect.any(Date),
updated_at: expect.any(Date),
deleted_at: null,
},
})
const user = await manager.findOne(
User,
{
id: user1.id,
},
{
populate: ["teams"],
}
)
expect(mikroOrmSerializer<InstanceType<typeof User>>(user)).toEqual({
id: user1.id,
username: "User 1",
created_at: expect.any(Date),
updated_at: expect.any(Date),
deleted_at: null,
teams: [
{
id: team1.id,
name: "Team 1",
created_at: expect.any(Date),
updated_at: expect.any(Date),
deleted_at: null,
user_id: user1.id,
},
],
})
})
})

View File

@@ -13,3 +13,4 @@ export * from "./medusa-internal-service"
export * from "./medusa-service"
export * from "./migration-scripts"
export * from "./mikro-orm-cli-config-builder"

View File

@@ -1,8 +1,12 @@
import { MikroORMOptions } from "@mikro-orm/core/utils/Configuration"
import { DmlEntity, toMikroORMEntity } from "../dml"
import { TSMigrationGenerator } from "../dal"
import { AnyEntity, EntityClassGroup, EntitySchema } from "@mikro-orm/core"
import { EntityClass } from "@mikro-orm/core/typings"
import type {
AnyEntity,
EntityClass,
EntityClassGroup,
} from "@mikro-orm/core/typings"
import type { EntitySchema } from "@mikro-orm/core/metadata/EntitySchema"
type Options = Partial<MikroORMOptions> & {
entities: (

View File

@@ -1,12 +1,7 @@
import * as entities from "./src/models"
import { TSMigrationGenerator } from "@medusajs/utils"
import { defineMikroOrmCliConfig } from "@medusajs/utils"
module.exports = {
module.exports = defineMikroOrmCliConfig({
entities: Object.values(entities),
schema: "public",
clientUrl: "postgres://postgres@localhost/medusa-fulfillment",
type: "postgresql",
migrations: {
generator: TSMigrationGenerator,
},
}
databaseName: "medusa-fulfillment",
})