Overall revamp of relationships (#7690)

This commit is contained in:
Harminder Virk authored and GitHub committed 2024-06-13 11:19:53 +05:30
1 parent 129fb447d0
commit fbd8eef18b
25 files changed
+1388 -625

No files matched your search

@@ -1,23 +1,29 @@
import { expectTypeOf } from "expect-type" import { expectTypeOf } from "expect-type"
import { HasOne } from "../relations/has_one"
import { TextSchema } from "../schema/text" import { TextSchema } from "../schema/text"
import { BaseRelationship } from "../relations/base"
describe("Base relationship", () => {
test("define a custom relationship", () => {
class HasOne<T> extends BaseRelationship<T> {
protected relationshipType: "hasOne" | "hasMany" | "manyToMany" = "hasOne"
}
describe("HasOne relationship", () => {
test("define hasOne relationship", () => {
const user = { const user = {
username: new TextSchema(), username: new TextSchema(),
} }
const entityRef = () => user const entityRef = () => user
const relationship = new HasOne(entityRef, {}) const relationship = new HasOne(entityRef, {
mappedBy: "user_id",
})
expectTypeOf(relationship["$dataType"]).toEqualTypeOf<() => typeof user>() expectTypeOf(relationship["$dataType"]).toEqualTypeOf<() => typeof user>()
expect(relationship.parse("user")).toEqual({ expect(relationship.parse("user")).toEqual({
name: "user", name: "user",
type: "hasOne", type: "hasOne",
nullable: false, nullable: false,
mappedBy: "user_id",
entity: entityRef, entity: entityRef,
options: {},
}) })
}) })
}) })
@@ -1,5 +1,5 @@
import { expectTypeOf } from "expect-type" import { expectTypeOf } from "expect-type"
import { DateTimeSchema } from "../schema/date_time" import { DateTimeSchema } from "../schema/date-time"
describe("DateTime schema", () => { describe("DateTime schema", () => {
test("create datetime schema type", () => { test("create datetime schema type", () => {
File diff suppressed because it is too large. Load diff
@@ -1,509 +0,0 @@
import { expectTypeOf } from "expect-type"
import { MetadataStorage } from "@mikro-orm/core"
import { EntityBuilder } from "../entity_builder"
import { createMikrORMEntity } from "../helpers/create_mikro_orm_entity"
import { EntityConstructor } from "../types"
describe("Entity builder", () => {
beforeEach(() => {
MetadataStorage.clear()
})
test("define an entity", () => {
const model = new EntityBuilder()
const user = model.define("user", {
id: model.number(),
username: model.text(),
email: model.text(),
})
const User = createMikrORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
email: 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",
nullable: false,
getter: false,
setter: false,
},
username: {
reference: "scalar",
type: "string",
columnType: "text",
name: "username",
nullable: false,
getter: false,
setter: false,
},
email: {
reference: "scalar",
type: "string",
columnType: "text",
name: "email",
nullable: false,
getter: false,
setter: false,
},
})
})
test("define an entity with enum property", () => {
const model = new EntityBuilder()
const user = model.define("user", {
id: model.number(),
username: model.text(),
email: model.text(),
role: model.enum(["moderator", "admin", "guest"]),
})
const User = createMikrORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
email: string
role: "moderator" | "admin" | "guest"
}>()
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",
nullable: false,
getter: false,
setter: false,
},
username: {
reference: "scalar",
type: "string",
columnType: "text",
name: "username",
nullable: false,
getter: false,
setter: false,
},
email: {
reference: "scalar",
type: "string",
columnType: "text",
name: "email",
nullable: false,
getter: false,
setter: false,
},
role: {
reference: "scalar",
enum: true,
items: expect.any(Function),
nullable: false,
name: "role",
},
})
expect(metaData.properties["role"].items()).toEqual([
"moderator",
"admin",
"guest",
])
})
test("define an entity with default value", () => {
const model = new EntityBuilder()
const user = model.define("user", {
id: model.number(),
username: model.text().default("foo"),
email: model.text(),
})
const User = createMikrORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
email: 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",
nullable: false,
getter: false,
setter: false,
},
username: {
reference: "scalar",
type: "string",
default: "foo",
columnType: "text",
name: "username",
nullable: false,
getter: false,
setter: false,
},
email: {
reference: "scalar",
type: "string",
columnType: "text",
name: "email",
nullable: false,
getter: false,
setter: false,
},
})
})
test("define hasMany relationship", () => {
const model = new EntityBuilder()
const email = model.define("email", {
email: model.text(),
isVerified: model.boolean(),
})
const user = model.define("user", {
id: model.number(),
username: model.text(),
emails: model.hasMany(() => email),
})
const User = createMikrORMEntity(user)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
emails: EntityConstructor<{ email: string; isVerified: boolean }>
}>()
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",
nullable: false,
getter: false,
setter: false,
},
username: {
reference: "scalar",
type: "string",
columnType: "text",
name: "username",
nullable: false,
getter: false,
setter: false,
},
emails: {
reference: "1:m",
name: "emails",
entity: "Email",
cascade: ["persist"],
mappedBy: expect.any(Function),
orphanRemoval: true,
},
})
})
test("define hasMany and hasOneThroughMany relationships", () => {
const model = new EntityBuilder()
const order = model.define("order", {
amount: model.number(),
user: model.hasOneThroughMany(() => user),
})
const user = model.define("user", {
id: model.number(),
username: model.text(),
orders: model.hasMany(() => order),
})
const User = createMikrORMEntity(user)
const Order = createMikrORMEntity(order)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
username: string
orders: EntityConstructor<{
amount: number
user: EntityConstructor<{
id: number
username: 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",
nullable: false,
getter: false,
setter: false,
},
username: {
reference: "scalar",
type: "string",
columnType: "text",
name: "username",
nullable: false,
getter: false,
setter: false,
},
orders: {
reference: "1:m",
name: "orders",
cascade: ["persist"],
mappedBy: expect.any(Function),
orphanRemoval: true,
entity: "Order",
},
})
const orderMetaDdata = MetadataStorage.getMetadataFromDecorator(Order)
expect(orderMetaDdata.className).toEqual("Order")
expect(orderMetaDdata.path).toEqual("Order")
expect(orderMetaDdata.properties).toEqual({
amount: {
reference: "scalar",
type: "number",
columnType: "integer",
name: "amount",
nullable: false,
getter: false,
setter: false,
},
user: {
reference: "m:1",
name: "user",
entity: "User",
persist: false,
},
user_id: {
columnType: "text",
entity: "User",
fieldName: "user_id",
mapToPk: true,
name: "user_id",
nullable: false,
onDelete: "cascade",
reference: "m:1",
},
})
})
test("define hasOne relationship on both sides", () => {
const model = new EntityBuilder()
const user = model.define("user", {
id: model.number(),
email: model.text(),
profile: model.hasOne(() => profile),
})
const profile = model.define("profile", {
id: model.number(),
user_id: model.number(),
user: model.hasOne(() => user),
})
const User = createMikrORMEntity(user)
const Profile = createMikrORMEntity(profile)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
email: string
profile: EntityConstructor<{
id: number
user_id: number
user: EntityConstructor<{
id: number
email: 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",
nullable: false,
getter: false,
setter: false,
},
email: {
reference: "scalar",
type: "string",
columnType: "text",
name: "email",
nullable: false,
getter: false,
setter: false,
},
profile: {
name: "profile",
cascade: ["persist"],
entity: "Profile",
nullable: false,
onDelete: "cascade",
owner: true,
reference: "1:1",
},
})
const profileMetadata = MetadataStorage.getMetadataFromDecorator(Profile)
expect(profileMetadata.className).toEqual("Profile")
expect(profileMetadata.path).toEqual("Profile")
expect(profileMetadata.properties).toEqual({
id: {
reference: "scalar",
type: "number",
columnType: "integer",
name: "id",
nullable: false,
getter: false,
setter: false,
},
user: {
cascade: ["persist"],
entity: "User",
name: "user",
nullable: false,
onDelete: "cascade",
owner: true,
reference: "1:1",
},
user_id: {
columnType: "integer",
getter: false,
name: "user_id",
nullable: false,
reference: "scalar",
setter: false,
type: "number",
},
})
})
test("define manyToMany relationships", () => {
const model = new EntityBuilder()
const user = model.define("user", {
id: model.number(),
email: model.text(),
books: model.manyToMany(() => book),
})
const book = model.define("book", {
id: model.number(),
title: model.text(),
authors: model.manyToMany(() => user),
})
const User = createMikrORMEntity(user)
const Book = createMikrORMEntity(book)
expectTypeOf(new User()).toMatchTypeOf<{
id: number
email: string
books: EntityConstructor<{
id: number
title: string
authors: EntityConstructor<{
id: number
email: 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",
nullable: false,
getter: false,
setter: false,
},
email: {
reference: "scalar",
type: "string",
columnType: "text",
name: "email",
nullable: false,
getter: false,
setter: false,
},
books: {
name: "books",
entity: "Book",
mappedBy: expect.any(Function),
reference: "m:n",
},
})
const bookMetaData = MetadataStorage.getMetadataFromDecorator(Book)
expect(bookMetaData.className).toEqual("Book")
expect(bookMetaData.path).toEqual("Book")
expect(bookMetaData.properties).toEqual({
id: {
reference: "scalar",
type: "number",
columnType: "integer",
name: "id",
nullable: false,
getter: false,
setter: false,
},
authors: {
entity: "User",
name: "authors",
reference: "m:n",
mappedBy: expect.any(Function),
},
title: {
columnType: "text",
getter: false,
name: "title",
nullable: false,
reference: "scalar",
setter: false,
type: "string",
},
})
})
})
@@ -1,6 +1,6 @@
import { expectTypeOf } from "expect-type" import { expectTypeOf } from "expect-type"
import { HasMany } from "../relations/has_many"
import { TextSchema } from "../schema/text" import { TextSchema } from "../schema/text"
import { HasMany } from "../relations/has-many"
describe("HasMany relationship", () => { describe("HasMany relationship", () => {
test("define hasMany relationship", () => { test("define hasMany relationship", () => {
@@ -17,7 +17,6 @@ describe("HasMany relationship", () => {
type: "hasMany", type: "hasMany",
nullable: false, nullable: false,
entity: entityRef, entity: entityRef,
options: {},
}) })
}) })
}) })
@@ -1,21 +1,15 @@
import { expectTypeOf } from "expect-type" import { expectTypeOf } from "expect-type"
import { TextSchema } from "../schema/text" import { TextSchema } from "../schema/text"
import { BaseRelationship } from "../relations/base" import { HasOne } from "../relations/has-one"
describe("Base relationship", () => {
test("define a custom relationship", () => {
class HasOne<T> extends BaseRelationship<T> {
protected relationshipType: "hasOne" | "hasMany" | "manyToMany" = "hasOne"
}
describe("HasOne relationship", () => {
test("define hasOne relationship", () => {
const user = { const user = {
username: new TextSchema(), username: new TextSchema(),
} }
const entityRef = () => user const entityRef = () => user
const relationship = new HasOne(entityRef, { const relationship = new HasOne(entityRef, {})
foreignKey: "user_id",
})
expectTypeOf(relationship["$dataType"]).toEqualTypeOf<() => typeof user>() expectTypeOf(relationship["$dataType"]).toEqualTypeOf<() => typeof user>()
expect(relationship.parse("user")).toEqual({ expect(relationship.parse("user")).toEqual({
@@ -23,25 +17,16 @@ describe("Base relationship", () => {
type: "hasOne", type: "hasOne",
nullable: false, nullable: false,
entity: entityRef, entity: entityRef,
options: {
foreignKey: "user_id",
},
}) })
}) })
test("mark relationship as nullable", () => { test("mark relationship as nullable", () => {
class HasOne<T> extends BaseRelationship<T> {
protected relationshipType: "hasOne" | "hasMany" | "manyToMany" = "hasOne"
}
const user = { const user = {
username: new TextSchema(), username: new TextSchema(),
} }
const entityRef = () => user const entityRef = () => user
const relationship = new HasOne(entityRef, { const relationship = new HasOne(entityRef, {}).nullable()
foreignKey: "user_id",
}).nullable()
expectTypeOf(relationship["$dataType"]).toEqualTypeOf< expectTypeOf(relationship["$dataType"]).toEqualTypeOf<
(() => typeof user) | null (() => typeof user) | null
@@ -51,9 +36,6 @@ describe("Base relationship", () => {
type: "hasOne", type: "hasOne",
nullable: true, nullable: true,
entity: entityRef, entity: entityRef,
options: {
foreignKey: "user_id",
},
}) })
}) })
}) })
@@ -1,6 +1,6 @@
import { expectTypeOf } from "expect-type" import { expectTypeOf } from "expect-type"
import { TextSchema } from "../schema/text" import { TextSchema } from "../schema/text"
import { ManyToMany } from "../relations/many_to_many" import { ManyToMany } from "../relations/many-to-many"
describe("ManyToMany relationship", () => { describe("ManyToMany relationship", () => {
test("define manyToMany relationship", () => { test("define manyToMany relationship", () => {
@@ -17,7 +17,6 @@ describe("ManyToMany relationship", () => {
type: "manyToMany", type: "manyToMany",
nullable: false, nullable: false,
entity: entityRef, entity: entityRef,
options: {},
}) })
}) })
}) })
@@ -2,14 +2,14 @@ import { DmlEntity } from "./entity"
import { TextSchema } from "./schema/text" import { TextSchema } from "./schema/text"
import { EnumSchema } from "./schema/enum" import { EnumSchema } from "./schema/enum"
import { JSONSchema } from "./schema/json" import { JSONSchema } from "./schema/json"
import { HasOne } from "./relations/has_one" import { HasOne } from "./relations/has-one"
import { HasMany } from "./relations/has_many" import { HasMany } from "./relations/has-many"
import { NumberSchema } from "./schema/number" import { NumberSchema } from "./schema/number"
import { BooleanSchema } from "./schema/boolean" import { BooleanSchema } from "./schema/boolean"
import { DateTimeSchema } from "./schema/date_time" import { BelongsTo } from "./relations/belongs-to"
import { ManyToMany } from "./relations/many_to_many" import { DateTimeSchema } from "./schema/date-time"
import { RelationshipType, SchemaType } from "./types" import { ManyToMany } from "./relations/many-to-many"
import { HasOneThroughMany } from "./relations/has_one_through_many" import type { RelationshipOptions, RelationshipType, SchemaType } from "./types"
/** /**
* Entity builder exposes the API to create an entity and define its * Entity builder exposes the API to create an entity and define its
@@ -80,10 +80,17 @@ export class EntityBuilder {
* You may use the "belongsTo" relationship to define the inverse * You may use the "belongsTo" relationship to define the inverse
* of the "hasOne" relationship * of the "hasOne" relationship
*/ */
hasOne<T>(entityBuilder: T, options?: Record<string, any>) { hasOne<T>(entityBuilder: T, options?: RelationshipOptions) {
return new HasOne<T>(entityBuilder, options || {}) return new HasOne<T>(entityBuilder, options || {})
} }
/**
* Define inverse of "hasOne" and "hasMany" relationship.
*/
belongsTo<T>(entityBuilder: T, options?: RelationshipOptions) {
return new BelongsTo<T>(entityBuilder, options || {})
}
/** /**
* Has many relationship defines a relationship between two entities * Has many relationship defines a relationship between two entities
* where the owner of the relationship has many instance of the * where the owner of the relationship has many instance of the
@@ -94,18 +101,10 @@ export class EntityBuilder {
* - A user "hasMany" books * - A user "hasMany" books
* - A user "hasMany" addresses * - A user "hasMany" addresses
*/ */
hasMany<T>(entityBuilder: T, options?: Record<string, any>) { hasMany<T>(entityBuilder: T, options?: RelationshipOptions) {
return new HasMany<T>(entityBuilder, options || {}) return new HasMany<T>(entityBuilder, options || {})
} }
/**
* Define a hasOneThroughMany relationship between two entities.
* @todo Remove in favor of "belongTo"
*/
hasOneThroughMany<T>(entityBuilder: T, options?: Record<string, any>) {
return new HasOneThroughMany<T>(entityBuilder, options || {})
}
/** /**
* ManyToMany relationship defines a relationship between two entities * ManyToMany relationship defines a relationship between two entities
* where the owner of the relationship has many instance of the * where the owner of the relationship has many instance of the
@@ -117,7 +116,7 @@ export class EntityBuilder {
* relationship requires a pivot table to establish a many to many * relationship requires a pivot table to establish a many to many
* relationship between two entities * relationship between two entities
*/ */
manyToMany<T>(entityBuilder: T, options?: Record<string, any>) { manyToMany<T>(entityBuilder: T, options?: RelationshipOptions) {
return new ManyToMany<T>(entityBuilder, options || {}) return new ManyToMany<T>(entityBuilder, options || {})
} }
} }
@@ -1,11 +1,10 @@
import { import {
Enum,
Entity, Entity,
OneToMany, OneToMany,
Property, Property,
OneToOne, OneToOne,
ManyToMany, ManyToMany,
Enum,
Cascade,
ManyToOne, ManyToOne,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { DmlEntity } from "../entity" import { DmlEntity } from "../entity"
@@ -15,11 +14,13 @@ import type {
Infer, Infer,
SchemaType, SchemaType,
KnownDataTypes, KnownDataTypes,
RelationshipType,
SchemaMetadata, SchemaMetadata,
RelationshipType,
EntityConstructor, EntityConstructor,
RelationshipMetadata, RelationshipMetadata,
} from "../types" } from "../types"
import { HasOne } from "../relations/has-one"
import { HasMany } from "../relations/has-many"
/** /**
* DML entity data types to PostgreSQL data types via * DML entity data types to PostgreSQL data types via
@@ -88,6 +89,130 @@ function defineProperty(
})(MikroORMEntity.prototype, field.fieldName) })(MikroORMEntity.prototype, field.fieldName)
} }
/**
* Defines has one relationship on the Mikro ORM entity.
*/
function defineHasOneRelationship(
MikroORMEntity: EntityConstructor<any>,
relationship: RelationshipMetadata,
relatedEntity: DmlEntity<
Record<string, SchemaType<any> | RelationshipType<any>>
>
) {
const relatedModelName = upperCaseFirst(relatedEntity.name)
OneToOne({
entity: relatedModelName,
nullable: relationship.nullable,
mappedBy: relationship.mappedBy as any,
})(MikroORMEntity.prototype, relationship.name)
}
/**
* Defines has many relationship on the Mikro ORM entity
*/
function defineHasManyRelationship(
MikroORMEntity: EntityConstructor<any>,
relationship: RelationshipMetadata,
relatedEntity: DmlEntity<
Record<string, SchemaType<any> | RelationshipType<any>>
>
) {
const relatedModelName = upperCaseFirst(relatedEntity.name)
OneToMany({
entity: relatedModelName,
orphanRemoval: true,
mappedBy: relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name),
})(MikroORMEntity.prototype, relationship.name)
}
/**
* Defines belongs to relationship on the Mikro ORM entity. The belongsTo
* relationship inspects the related entity for the other side of
* the relationship and then uses one of the following Mikro ORM
* relationship.
*
* - OneToOne: When the other side uses "hasOne" with "owner: true"
* - ManyToOne: When the other side uses "hasMany"
*/
function defineBelongsToRelationship(
MikroORMEntity: EntityConstructor<any>,
relationship: RelationshipMetadata,
relatedEntity: DmlEntity<
Record<string, SchemaType<any> | RelationshipType<any>>
>
) {
const mappedBy =
relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name)
const otherSideRelation = relatedEntity.schema[mappedBy]
const relatedModelName = upperCaseFirst(relatedEntity.name)
/**
* Ensure the mapped by is defined as relationship on the other side
*/
if (!otherSideRelation) {
throw new Error(
`Missing property "${mappedBy}" on "${relatedEntity.name}" entity. Make sure to define it as a relationship`
)
}
/**
* Otherside is a has many. Hence we should defined a ManyToOne
*/
if (otherSideRelation instanceof HasMany) {
ManyToOne({
entity: relatedModelName,
columnType: "text",
mapToPk: true,
fieldName: camelToSnakeCase(`${relationship.name}Id`),
nullable: relationship.nullable,
})(MikroORMEntity.prototype, camelToSnakeCase(`${relationship.name}Id`))
ManyToOne({
entity: relatedModelName,
persist: false,
})(MikroORMEntity.prototype, relationship.name)
return
}
/**
* Otherside is a has one. Hence we should defined a OneToOne
*/
if (otherSideRelation instanceof HasOne) {
const relatedModelName = upperCaseFirst(relatedEntity.name)
OneToOne({
entity: relatedModelName,
nullable: relationship.nullable,
mappedBy: mappedBy,
owner: true,
})(MikroORMEntity.prototype, relationship.name)
return
}
/**
* Other side is some unsupported data-type
*/
throw new Error(
`Invalid relationship reference for "${mappedBy}" on "${relatedEntity.name}" entity. Make sure to define a hasOne or hasMany relationship`
)
}
/**
* Defines a many to many relationship on the Mikro ORM entity
*/
function defineManyToManyRelationship(
MikroORMEntity: EntityConstructor<any>,
relationship: RelationshipMetadata,
relatedEntity: DmlEntity<
Record<string, SchemaType<any> | RelationshipType<any>>
>
) {
const relatedModelName = upperCaseFirst(relatedEntity.name)
ManyToMany({
entity: relatedModelName,
mappedBy: relationship.mappedBy as any,
})(MikroORMEntity.prototype, relationship.name)
}
/** /**
* Defines a DML entity schema field as a Mikro ORM relationship * Defines a DML entity schema field as a Mikro ORM relationship
*/ */
@@ -134,44 +259,16 @@ function defineRelationship(
*/ */
switch (relationship.type) { switch (relationship.type) {
case "hasOne": case "hasOne":
OneToOne({ defineHasOneRelationship(MikroORMEntity, relationship, relatedEntity)
entity: relatedModelName,
cascade: [Cascade.PERSIST], // accept via options
owner: true, // accept via options
onDelete: "cascade", // accept via options
nullable: false, // accept via options
})(MikroORMEntity.prototype, relationship.name)
break break
case "hasMany": case "hasMany":
OneToMany({ defineHasManyRelationship(MikroORMEntity, relationship, relatedEntity)
entity: relatedModelName,
cascade: [Cascade.PERSIST], // accept via options
orphanRemoval: true,
mappedBy: (related: any) =>
related[camelToSnakeCase(MikroORMEntity.name)], // optionally via options,
})(MikroORMEntity.prototype, relationship.name)
break break
case "hasOneThroughMany": case "belongsTo":
ManyToOne({ defineBelongsToRelationship(MikroORMEntity, relationship, relatedEntity)
entity: relatedModelName,
columnType: "text", // infer from primary key data-type
mapToPk: true,
nullable: false, // accept via options
fieldName: camelToSnakeCase(`${relationship.name}Id`),
onDelete: "cascade", // accept via options
})(MikroORMEntity.prototype, camelToSnakeCase(`${relationship.name}Id`))
ManyToOne({
entity: relatedModelName,
persist: false,
})(MikroORMEntity.prototype, relationship.name)
break break
case "manyToMany": case "manyToMany":
ManyToMany({ defineManyToManyRelationship(MikroORMEntity, relationship, relatedEntity)
entity: relatedModelName,
mappedBy: (related: any) =>
related[camelToSnakeCase(MikroORMEntity.name)], // optionally via options,
})(MikroORMEntity.prototype, relationship.name)
break break
} }
} }
@@ -1,9 +1,12 @@
import { MaybeFieldMetadata } from "../types" import { RelationshipType, SchemaType } from "../types"
/** /**
* Nullable modifier marks a schema node as nullable * Nullable modifier marks a schema node as nullable
*/ */
export class NullableModifier<T> { export class NullableModifier<
T,
Schema extends SchemaType<T> | RelationshipType<T>
> {
/** /**
* A type-only property to infer the JavScript data-type * A type-only property to infer the JavScript data-type
* of the schema property * of the schema property
@@ -14,20 +17,18 @@ export class NullableModifier<T> {
* The parent schema on which the nullable modifier is * The parent schema on which the nullable modifier is
* applied * applied
*/ */
#schema: { #schema: Schema
parse(fieldName: string): MaybeFieldMetadata
}
constructor(schema: { parse(fieldName: string): MaybeFieldMetadata }) { constructor(schema: Schema) {
this.#schema = schema this.#schema = schema
} }
/** /**
* Returns the serialized metadata * Returns the serialized metadata
*/ */
parse(fieldName: string) { parse(fieldName: string): ReturnType<Schema["parse"]> {
const schema = this.#schema.parse(fieldName) const schema = this.#schema.parse(fieldName)
schema.nullable = true schema.nullable = true
return schema return schema as ReturnType<Schema["parse"]>
} }
} }
+10 -13
View File
@@ -1,5 +1,8 @@
import { NullableModifier } from "../modifiers/nullable" import {
import { RelationshipMetadata, RelationshipType } from "../types" RelationshipMetadata,
RelationshipOptions,
RelationshipType,
} from "../types"
/** /**
* The BaseRelationship encapsulates the repetitive parts of defining * The BaseRelationship encapsulates the repetitive parts of defining
@@ -7,7 +10,8 @@ import { RelationshipMetadata, RelationshipType } from "../types"
*/ */
export abstract class BaseRelationship<T> implements RelationshipType<T> { export abstract class BaseRelationship<T> implements RelationshipType<T> {
#referencedEntity: T #referencedEntity: T
#options: Record<string, any>
protected options: RelationshipOptions
/** /**
* The relationship type. * The relationship type.
@@ -20,16 +24,9 @@ export abstract class BaseRelationship<T> implements RelationshipType<T> {
*/ */
declare $dataType: T declare $dataType: T
constructor(referencedEntity: T, options: Record<string, any>) { constructor(referencedEntity: T, options: RelationshipOptions) {
this.#referencedEntity = referencedEntity this.#referencedEntity = referencedEntity
this.#options = options this.options = options
}
/**
* Apply nullable modifier on the schema
*/
nullable() {
return new NullableModifier<T>(this)
} }
/** /**
@@ -39,8 +36,8 @@ export abstract class BaseRelationship<T> implements RelationshipType<T> {
return { return {
name: relationshipName, name: relationshipName,
nullable: false, nullable: false,
mappedBy: this.options.mappedBy,
entity: this.#referencedEntity, entity: this.#referencedEntity,
options: this.#options,
type: this.relationshipType, type: this.relationshipType,
} }
} }
@@ -0,0 +1,14 @@
import { BaseRelationship } from "./base"
import { RelationshipMetadata } from "../types"
import { NullableModifier } from "../modifiers/nullable"
export class BelongsTo<T> extends BaseRelationship<T> {
protected relationshipType: RelationshipMetadata["type"] = "belongsTo"
/**
* Apply nullable modifier on the schema
*/
nullable() {
return new NullableModifier<T, BelongsTo<T>>(this)
}
}
@@ -1,4 +1,5 @@
import { BaseRelationship } from "./base" import { BaseRelationship } from "./base"
import { NullableModifier } from "../modifiers/nullable"
import { RelationshipMetadata } from "../types" import { RelationshipMetadata } from "../types"
/** /**
@@ -13,4 +14,11 @@ import { RelationshipMetadata } from "../types"
*/ */
export class HasOne<T> extends BaseRelationship<T> { export class HasOne<T> extends BaseRelationship<T> {
protected relationshipType: RelationshipMetadata["type"] = "hasOne" protected relationshipType: RelationshipMetadata["type"] = "hasOne"
/**
* Apply nullable modifier on the schema
*/
nullable() {
return new NullableModifier<T, HasOne<T>>(this)
}
} }
@@ -1,6 +0,0 @@
import { BaseRelationship } from "./base"
import { RelationshipMetadata } from "../types"
export class HasOneThroughMany<T> extends BaseRelationship<T> {
protected relationshipType: RelationshipMetadata["type"] = "hasOneThroughMany"
}
+1 -1
View File
@@ -26,7 +26,7 @@ export abstract class BaseSchema<T> implements SchemaType<T> {
* Apply nullable modifier on the schema * Apply nullable modifier on the schema
*/ */
nullable() { nullable() {
return new NullableModifier<T>(this) return new NullableModifier<T, this>(this)
} }
/** /**
+21 -2
View File
@@ -11,6 +11,16 @@ export type KnownDataTypes =
| "dateTime" | "dateTime"
| "json" | "json"
/**
* The available on Delete actions
*/
export type OnDeleteActions =
| "cascade"
| "no action"
| "set null"
| "set default"
| (string & {})
/** /**
* Any field that contains "nullable" and "optional" properties * Any field that contains "nullable" and "optional" properties
* in their metadata are qualified as maybe fields. * in their metadata are qualified as maybe fields.
@@ -49,15 +59,22 @@ export type SchemaType<T> = {
parse(fieldName: string): SchemaMetadata parse(fieldName: string): SchemaMetadata
} }
/**
* Options accepted by all the relationships
*/
export type RelationshipOptions = {
mappedBy?: string
}
/** /**
* The meta-data returned by the relationship parse * The meta-data returned by the relationship parse
* method * method
*/ */
export type RelationshipMetadata = MaybeFieldMetadata & { export type RelationshipMetadata = MaybeFieldMetadata & {
name: string name: string
type: "hasOne" | "hasMany" | "hasOneThroughMany" | "manyToMany" type: "hasOne" | "hasMany" | "belongsTo" | "manyToMany"
entity: unknown entity: unknown
options: Record<string, any> mappedBy?: string
} }
/** /**
@@ -86,6 +103,8 @@ export type Infer<T> = T extends DmlEntity<infer Schema>
? EntityConstructor<{ ? EntityConstructor<{
[K in keyof Schema]: Schema[K]["$dataType"] extends () => infer R [K in keyof Schema]: Schema[K]["$dataType"] extends () => infer R
? Infer<R> ? Infer<R>
: Schema[K]["$dataType"] extends (() => infer R) | null
? Infer<R> | null
: Schema[K]["$dataType"] : Schema[K]["$dataType"]
}> }>
: never : never