Initial implementation of DML to Mikro ORM entity generation (#7667)
* feat: initial implementation of DML to Mikro ORM entity generation CORE-2279 * test: fix breaking tests * refactor: cleanup code for defining properties and relationships * feat: finish initial implementation of relationships
This commit is contained in:
@@ -1,8 +1,14 @@
|
|||||||
import { expectTypeOf } from "expect-type"
|
import { expectTypeOf } from "expect-type"
|
||||||
import { Infer, MikroORMEntity } from "../types"
|
import { MetadataStorage } from "@mikro-orm/core"
|
||||||
import { EntityBuilder } from "../entity_builder"
|
import { EntityBuilder } from "../entity_builder"
|
||||||
|
import { createMikrORMEntity } from "../helpers/create_mikro_orm_entity"
|
||||||
|
import { EntityConstructor } from "../types"
|
||||||
|
|
||||||
describe("Entity builder", () => {
|
describe("Entity builder", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
MetadataStorage.clear()
|
||||||
|
})
|
||||||
|
|
||||||
test("define an entity", () => {
|
test("define an entity", () => {
|
||||||
const model = new EntityBuilder()
|
const model = new EntityBuilder()
|
||||||
const user = model.define("user", {
|
const user = model.define("user", {
|
||||||
@@ -11,14 +17,112 @@ describe("Entity builder", () => {
|
|||||||
email: model.text(),
|
email: model.text(),
|
||||||
})
|
})
|
||||||
|
|
||||||
expectTypeOf<InstanceType<Infer<typeof user>>>().toMatchTypeOf<{
|
const User = createMikrORMEntity(user)
|
||||||
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
email: 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 relationships", () => {
|
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 hasMany relationship", () => {
|
||||||
const model = new EntityBuilder()
|
const model = new EntityBuilder()
|
||||||
const email = model.define("email", {
|
const email = model.define("email", {
|
||||||
email: model.text(),
|
email: model.text(),
|
||||||
@@ -31,18 +135,51 @@ describe("Entity builder", () => {
|
|||||||
emails: model.hasMany(() => email),
|
emails: model.hasMany(() => email),
|
||||||
})
|
})
|
||||||
|
|
||||||
expectTypeOf<InstanceType<Infer<typeof user>>>().toEqualTypeOf<{
|
const User = createMikrORMEntity(user)
|
||||||
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
emails: MikroORMEntity<{ email: string; isVerified: boolean }>
|
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 an entity with recursive relationships", () => {
|
test("define hasMany and hasOneThroughMany relationships", () => {
|
||||||
const model = new EntityBuilder()
|
const model = new EntityBuilder()
|
||||||
const order = model.define("order", {
|
const order = model.define("order", {
|
||||||
amount: model.number(),
|
amount: model.number(),
|
||||||
user: model.hasOne(() => user),
|
user: model.hasOneThroughMany(() => user),
|
||||||
})
|
})
|
||||||
|
|
||||||
const user = model.define("user", {
|
const user = model.define("user", {
|
||||||
@@ -51,16 +188,272 @@ describe("Entity builder", () => {
|
|||||||
orders: model.hasMany(() => order),
|
orders: model.hasMany(() => order),
|
||||||
})
|
})
|
||||||
|
|
||||||
expectTypeOf<InstanceType<Infer<typeof user>>>().toMatchTypeOf<{
|
const User = createMikrORMEntity(user)
|
||||||
|
const Order = createMikrORMEntity(order)
|
||||||
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
orders: MikroORMEntity<{
|
orders: EntityConstructor<{
|
||||||
amount: number
|
amount: number
|
||||||
user: MikroORMEntity<{
|
user: EntityConstructor<{
|
||||||
id: number
|
id: number
|
||||||
username: string
|
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",
|
||||||
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,11 +3,5 @@ import { RelationshipType, SchemaType } from "./types"
|
|||||||
export class DmlEntity<
|
export class DmlEntity<
|
||||||
Schema extends Record<string, SchemaType<any> | RelationshipType<any>>
|
Schema extends Record<string, SchemaType<any> | RelationshipType<any>>
|
||||||
> {
|
> {
|
||||||
#name: string
|
constructor(public name: string, public schema: Schema) {}
|
||||||
#schema: Schema
|
|
||||||
|
|
||||||
constructor(name: string, schema: Schema) {
|
|
||||||
this.#name = name
|
|
||||||
this.#schema = schema
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { BooleanSchema } from "./schema/boolean"
|
|||||||
import { DateTimeSchema } from "./schema/date_time"
|
import { DateTimeSchema } from "./schema/date_time"
|
||||||
import { ManyToMany } from "./relations/many_to_many"
|
import { ManyToMany } from "./relations/many_to_many"
|
||||||
import { RelationshipType, SchemaType } from "./types"
|
import { RelationshipType, SchemaType } from "./types"
|
||||||
|
import { EnumSchema } from "./schema/enum"
|
||||||
|
import { HasOneThroughMany } from "./relations/has_one_through_many"
|
||||||
|
|
||||||
export class EntityBuilder {
|
export class EntityBuilder {
|
||||||
define<
|
define<
|
||||||
@@ -36,6 +38,10 @@ export class EntityBuilder {
|
|||||||
return new JSONSchema()
|
return new JSONSchema()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum<const Values extends unknown>(values: Values[]) {
|
||||||
|
return new EnumSchema<Values>(values)
|
||||||
|
}
|
||||||
|
|
||||||
hasOne<T>(entityBuilder: T, options?: Record<string, any>) {
|
hasOne<T>(entityBuilder: T, options?: Record<string, any>) {
|
||||||
return new HasOne<T>(entityBuilder, options || {})
|
return new HasOne<T>(entityBuilder, options || {})
|
||||||
}
|
}
|
||||||
@@ -44,6 +50,10 @@ export class EntityBuilder {
|
|||||||
return new HasMany<T>(entityBuilder, options || {})
|
return new HasMany<T>(entityBuilder, options || {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasOneThroughMany<T>(entityBuilder: T, options?: Record<string, any>) {
|
||||||
|
return new HasOneThroughMany<T>(entityBuilder, options || {})
|
||||||
|
}
|
||||||
|
|
||||||
manyToMany<T>(entityBuilder: T, options?: Record<string, any>) {
|
manyToMany<T>(entityBuilder: T, options?: Record<string, any>) {
|
||||||
return new ManyToMany<T>(entityBuilder, options || {})
|
return new ManyToMany<T>(entityBuilder, options || {})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
OneToMany,
|
||||||
|
Property,
|
||||||
|
OneToOne,
|
||||||
|
ManyToMany,
|
||||||
|
Enum,
|
||||||
|
Cascade,
|
||||||
|
ManyToOne,
|
||||||
|
} from "@mikro-orm/core"
|
||||||
|
import { DmlEntity } from "../entity"
|
||||||
|
import { camelToSnakeCase, pluralize } from "../../common"
|
||||||
|
import { upperCaseFirst } from "../../common/upper-case-first"
|
||||||
|
import type {
|
||||||
|
Infer,
|
||||||
|
SchemaType,
|
||||||
|
KnownDataTypes,
|
||||||
|
RelationshipType,
|
||||||
|
SchemaMetadata,
|
||||||
|
EntityConstructor,
|
||||||
|
RelationshipMetadata,
|
||||||
|
} from "../types"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DML entity data types to PostgreSQL data types via
|
||||||
|
* Mikro ORM
|
||||||
|
*/
|
||||||
|
const COLUMN_TYPES: {
|
||||||
|
[K in KnownDataTypes]: string
|
||||||
|
} = {
|
||||||
|
boolean: "boolean",
|
||||||
|
dateTime: "timestamptz",
|
||||||
|
number: "integer",
|
||||||
|
string: "text",
|
||||||
|
json: "jsonb",
|
||||||
|
enum: "enum", // ignore for now
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DML entity data types to Mikro ORM property
|
||||||
|
* types
|
||||||
|
*/
|
||||||
|
const PROPERTY_TYPES: {
|
||||||
|
[K in KnownDataTypes]: string
|
||||||
|
} = {
|
||||||
|
boolean: "boolean",
|
||||||
|
dateTime: "date",
|
||||||
|
number: "number",
|
||||||
|
string: "string",
|
||||||
|
json: "any",
|
||||||
|
enum: "enum", // ignore for now
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a DML entity schema field as a Mikro ORM property
|
||||||
|
*/
|
||||||
|
function defineProperty(
|
||||||
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
|
field: SchemaMetadata
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Defining an enum property
|
||||||
|
*/
|
||||||
|
if (field.dataType.name === "enum") {
|
||||||
|
Enum({
|
||||||
|
items: () => field.dataType.options!.choices,
|
||||||
|
nullable: field.nullable,
|
||||||
|
})(MikroORMEntity.prototype, field.fieldName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnType = COLUMN_TYPES[field.dataType.name]
|
||||||
|
const propertyType = PROPERTY_TYPES[field.dataType.name]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @todo: Add support for default value
|
||||||
|
*/
|
||||||
|
Property({ columnType, type: propertyType, nullable: field.nullable })(
|
||||||
|
MikroORMEntity.prototype,
|
||||||
|
field.fieldName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a DML entity schema field as a Mikro ORM relationship
|
||||||
|
*/
|
||||||
|
function defineRelationship(
|
||||||
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
|
relationship: RelationshipMetadata
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Defining relationships
|
||||||
|
*/
|
||||||
|
const relatedEntity =
|
||||||
|
typeof relationship.entity === "function"
|
||||||
|
? (relationship.entity() as unknown)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since we don't type-check relationships, we should validate
|
||||||
|
* them at runtime
|
||||||
|
*/
|
||||||
|
if (!relatedEntity) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". Make sure to define the relationship using a factory function`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the return value is a DML entity instance
|
||||||
|
*/
|
||||||
|
if (!(relatedEntity instanceof DmlEntity)) {
|
||||||
|
throw new Error(
|
||||||
|
`Invalid relationship reference for "${MikroORMEntity.name}.${relationship.name}". Make sure to return a DML entity from the relationship callback`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defining relationships
|
||||||
|
*/
|
||||||
|
switch (relationship.type) {
|
||||||
|
case "hasOne":
|
||||||
|
OneToOne({
|
||||||
|
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
|
||||||
|
case "hasMany":
|
||||||
|
OneToMany({
|
||||||
|
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
|
||||||
|
case "hasOneThroughMany":
|
||||||
|
ManyToOne({
|
||||||
|
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
|
||||||
|
case "manyToMany":
|
||||||
|
ManyToMany({
|
||||||
|
entity: relatedModelName,
|
||||||
|
mappedBy: (related: any) =>
|
||||||
|
related[camelToSnakeCase(MikroORMEntity.name)], // optionally via options,
|
||||||
|
})(MikroORMEntity.prototype, relationship.name)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper function to define a Mikro ORM entity from a
|
||||||
|
* DML entity.
|
||||||
|
* @todo: Handle soft deleted indexes and filters
|
||||||
|
* @todo: Finalize if custom pivot entities are needed
|
||||||
|
*/
|
||||||
|
export function createMikrORMEntity<
|
||||||
|
T extends DmlEntity<Record<string, SchemaType<any> | RelationshipType<any>>>
|
||||||
|
>(entity: T): Infer<T> {
|
||||||
|
class MikroORMEntity {}
|
||||||
|
|
||||||
|
const className = upperCaseFirst(entity.name)
|
||||||
|
const tableName = pluralize(camelToSnakeCase(className))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigning name to the class constructor, so that it matches
|
||||||
|
* the DML entity name
|
||||||
|
*/
|
||||||
|
Object.defineProperty(MikroORMEntity, "name", {
|
||||||
|
get: function () {
|
||||||
|
return className
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processing schema fields
|
||||||
|
*/
|
||||||
|
Object.keys(entity.schema).forEach((property) => {
|
||||||
|
const field = entity.schema[property].parse(property)
|
||||||
|
if ("fieldName" in field) {
|
||||||
|
defineProperty(MikroORMEntity, field)
|
||||||
|
} else {
|
||||||
|
defineRelationship(MikroORMEntity, field)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converting class to a MikroORM entity
|
||||||
|
*/
|
||||||
|
return Entity({ tableName })(MikroORMEntity) as Infer<T>
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { BaseRelationship } from "./base"
|
||||||
|
import { RelationshipMetadata } from "../types"
|
||||||
|
|
||||||
|
export class HasOneThroughMany<T> extends BaseRelationship<T> {
|
||||||
|
protected relationshipType: RelationshipMetadata["type"] = "hasOneThroughMany"
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ export type KnownDataTypes =
|
|||||||
| "number"
|
| "number"
|
||||||
| "dateTime"
|
| "dateTime"
|
||||||
| "json"
|
| "json"
|
||||||
| "any"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The meta-data returned by the relationship parse
|
* The meta-data returned by the relationship parse
|
||||||
@@ -18,7 +17,7 @@ export type KnownDataTypes =
|
|||||||
*/
|
*/
|
||||||
export type RelationshipMetadata = {
|
export type RelationshipMetadata = {
|
||||||
name: string
|
name: string
|
||||||
type: "hasOne" | "hasMany" | "manyToMany"
|
type: "hasOne" | "hasMany" | "hasOneThroughMany" | "manyToMany"
|
||||||
entity: unknown
|
entity: unknown
|
||||||
options: Record<string, any>
|
options: Record<string, any>
|
||||||
}
|
}
|
||||||
@@ -66,7 +65,7 @@ export type RelationshipType<T> = {
|
|||||||
* entities on the fly, we need a way to represent a type-safe
|
* entities on the fly, we need a way to represent a type-safe
|
||||||
* constructor and its instance properties.
|
* constructor and its instance properties.
|
||||||
*/
|
*/
|
||||||
export interface MikroORMEntity<Props> extends Function {
|
export interface EntityConstructor<Props> extends Function {
|
||||||
new (): Props
|
new (): Props
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +73,7 @@ export interface MikroORMEntity<Props> extends Function {
|
|||||||
* Helper to infer the schema type of a DmlEntity
|
* Helper to infer the schema type of a DmlEntity
|
||||||
*/
|
*/
|
||||||
export type Infer<T> = T extends DmlEntity<infer Schema>
|
export type Infer<T> = T extends DmlEntity<infer Schema>
|
||||||
? MikroORMEntity<{
|
? 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"]
|
: Schema[K]["$dataType"]
|
||||||
|
|||||||
Reference in New Issue
Block a user