Handle embedded pg schema name inside the table name when generating indexes (#7774)
This commit is contained in:
1 parent
0b623fa27a
commit
fd87858bd9
4 files changed
+660
-66
No files matched your search
+38
@@ -89,4 +89,42 @@ describe("createPsqlIndexStatementHelper", function () {
|
|||||||
}" (${options.columns.join(", ")}) WHERE ${options.where}`
|
}" (${options.columns.join(", ")}) WHERE ${options.where}`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should generate index on an explicit pg schema", function () {
|
||||||
|
const options = {
|
||||||
|
name: "index_name",
|
||||||
|
tableName: "public.table_name",
|
||||||
|
columns: "column_name",
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexStatement = createPsqlIndexStatementHelper(options)
|
||||||
|
expect(indexStatement + "").toEqual(
|
||||||
|
`CREATE INDEX IF NOT EXISTS "${options.name}" ON "public"."table_name" (${options.columns})`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("generate index name from table name when using explicit pg schema", function () {
|
||||||
|
const options = {
|
||||||
|
tableName: "public.table_name",
|
||||||
|
columns: "column_name",
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexStatement = createPsqlIndexStatementHelper(options)
|
||||||
|
expect(indexStatement + "").toEqual(
|
||||||
|
`CREATE INDEX IF NOT EXISTS "IDX_table_name_column_name" ON "public"."table_name" (${options.columns})`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should generate index when table name was previously formatted to allow pg schema name", function () {
|
||||||
|
const options = {
|
||||||
|
name: "index_name",
|
||||||
|
tableName: 'public"."table_name',
|
||||||
|
columns: "column_name",
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexStatement = createPsqlIndexStatementHelper(options)
|
||||||
|
expect(indexStatement + "").toEqual(
|
||||||
|
`CREATE INDEX IF NOT EXISTS "${options.name}" ON "public"."table_name" (${options.columns})`
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -31,7 +31,7 @@ import { Index } from "@mikro-orm/core"
|
|||||||
*/
|
*/
|
||||||
export function createPsqlIndexStatementHelper({
|
export function createPsqlIndexStatementHelper({
|
||||||
name,
|
name,
|
||||||
tableName,
|
tableName: qualifiedName,
|
||||||
columns,
|
columns,
|
||||||
type,
|
type,
|
||||||
where,
|
where,
|
||||||
@@ -45,6 +45,20 @@ export function createPsqlIndexStatementHelper({
|
|||||||
unique?: boolean
|
unique?: boolean
|
||||||
}) {
|
}) {
|
||||||
const columnsName = Array.isArray(columns) ? columns.join("_") : columns
|
const columnsName = Array.isArray(columns) ? columns.join("_") : columns
|
||||||
|
const tokens = qualifiedName.replace(/"/g, "").split(".")
|
||||||
|
|
||||||
|
let pgSchemaName: string | undefined
|
||||||
|
let tableName: string
|
||||||
|
let tableReference: string
|
||||||
|
|
||||||
|
if (tokens.length > 1) {
|
||||||
|
pgSchemaName = tokens.shift()
|
||||||
|
tableName = tokens.join(".")
|
||||||
|
tableReference = `"${pgSchemaName}"."${tableName}"`
|
||||||
|
} else {
|
||||||
|
tableName = qualifiedName
|
||||||
|
tableReference = `"${tableName}"`
|
||||||
|
}
|
||||||
|
|
||||||
columns = Array.isArray(columns) ? columns.join(", ") : columns
|
columns = Array.isArray(columns) ? columns.join(", ") : columns
|
||||||
name = name || `IDX_${tableName}_${columnsName}${unique ? "_unique" : ""}`
|
name = name || `IDX_${tableName}_${columnsName}${unique ? "_unique" : ""}`
|
||||||
@@ -53,7 +67,7 @@ export function createPsqlIndexStatementHelper({
|
|||||||
const optionsStr = where ? ` WHERE ${where}` : ""
|
const optionsStr = where ? ` WHERE ${where}` : ""
|
||||||
const uniqueStr = unique ? "UNIQUE " : ""
|
const uniqueStr = unique ? "UNIQUE " : ""
|
||||||
|
|
||||||
const expression = `CREATE ${uniqueStr}INDEX IF NOT EXISTS "${name}" ON "${tableName}"${typeStr} (${columns})${optionsStr}`
|
const expression = `CREATE ${uniqueStr}INDEX IF NOT EXISTS "${name}" ON ${tableReference}${typeStr} (${columns})${optionsStr}`
|
||||||
return {
|
return {
|
||||||
toString: () => {
|
toString: () => {
|
||||||
return expression
|
return expression
|
||||||
|
|||||||
@@ -669,6 +669,7 @@ describe("Entity builder", () => {
|
|||||||
const metaData = MetadataStorage.getMetadataFromDecorator(User)
|
const metaData = MetadataStorage.getMetadataFromDecorator(User)
|
||||||
expect(metaData.className).toEqual("User")
|
expect(metaData.className).toEqual("User")
|
||||||
expect(metaData.path).toEqual("User")
|
expect(metaData.path).toEqual("User")
|
||||||
|
expect(metaData.tableName).toEqual("public.user")
|
||||||
|
|
||||||
expect(metaData.filters).toEqual({
|
expect(metaData.filters).toEqual({
|
||||||
softDeletable: {
|
softDeletable: {
|
||||||
@@ -772,9 +773,9 @@ describe("Entity builder", () => {
|
|||||||
'CREATE INDEX IF NOT EXISTS "IDX_user_id" ON "user" (id) WHERE deleted_at IS NULL',
|
'CREATE INDEX IF NOT EXISTS "IDX_user_id" ON "user" (id) WHERE deleted_at IS NULL',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IDX_user_email",
|
name: "IDX_user_email_unique",
|
||||||
expression:
|
expression:
|
||||||
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_email" ON "user" (email) WHERE deleted_at IS NULL',
|
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_email_unique" ON "user" (email) WHERE deleted_at IS NULL',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -870,6 +871,7 @@ describe("Entity builder", () => {
|
|||||||
const metaData = MetadataStorage.getMetadataFromDecorator(User)
|
const metaData = MetadataStorage.getMetadataFromDecorator(User)
|
||||||
expect(metaData.className).toEqual("User")
|
expect(metaData.className).toEqual("User")
|
||||||
expect(metaData.path).toEqual("User")
|
expect(metaData.path).toEqual("User")
|
||||||
|
expect(metaData.tableName).toEqual("platform.user")
|
||||||
|
|
||||||
expect(metaData.indexes).toEqual([
|
expect(metaData.indexes).toEqual([
|
||||||
{
|
{
|
||||||
@@ -878,9 +880,9 @@ describe("Entity builder", () => {
|
|||||||
'CREATE INDEX IF NOT EXISTS "IDX_user_id" ON "platform"."user" (id) WHERE deleted_at IS NULL',
|
'CREATE INDEX IF NOT EXISTS "IDX_user_id" ON "platform"."user" (id) WHERE deleted_at IS NULL',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IDX_user_email",
|
name: "IDX_user_email_unique",
|
||||||
expression:
|
expression:
|
||||||
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_email" ON "platform"."user" (email) WHERE deleted_at IS NULL',
|
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_user_email_unique" ON "platform"."user" (email) WHERE deleted_at IS NULL',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -2694,7 +2696,7 @@ describe("Entity builder", () => {
|
|||||||
|
|
||||||
const entityBuilder = createMikrORMEntity()
|
const entityBuilder = createMikrORMEntity()
|
||||||
expect(() => entityBuilder(email)).toThrow(
|
expect(() => entityBuilder(email)).toThrow(
|
||||||
'Missing property "email" on "user" entity. Make sure to define it as a relationship'
|
'Missing property "email" on "User" entity. Make sure to define it as a relationship'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2715,7 +2717,7 @@ describe("Entity builder", () => {
|
|||||||
|
|
||||||
const entityBuilder = createMikrORMEntity()
|
const entityBuilder = createMikrORMEntity()
|
||||||
expect(() => entityBuilder(email)).toThrow(
|
expect(() => entityBuilder(email)).toThrow(
|
||||||
'Invalid relationship reference for "email" on "user" entity. Make sure to define a hasOne or hasMany relationship'
|
'Invalid relationship reference for "email" on "User" entity. Make sure to define a hasOne or hasMany relationship'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2743,6 +2745,368 @@ describe("Entity builder", () => {
|
|||||||
'Cannot cascade delete "user" relationship(s) from "email" entity. Child to parent cascades are not allowed'
|
'Cannot cascade delete "user" relationship(s) from "email" entity. Child to parent cascades are not allowed'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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(),
|
||||||
|
user: model.belongsTo(() => user),
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = model.define("platform.user", {
|
||||||
|
id: model.number(),
|
||||||
|
username: model.text(),
|
||||||
|
email: model.hasOne(() => email),
|
||||||
|
})
|
||||||
|
|
||||||
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const User = entityBuilder(user)
|
||||||
|
const Email = entityBuilder(email)
|
||||||
|
|
||||||
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
deleted_at: Date | null
|
||||||
|
email: EntityConstructor<{
|
||||||
|
email: string
|
||||||
|
isVerified: boolean
|
||||||
|
deleted_at: Date | null
|
||||||
|
user: EntityConstructor<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
deleted_at: Date | null
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
expectTypeOf(new Email()).toMatchTypeOf<{
|
||||||
|
email: string
|
||||||
|
isVerified: boolean
|
||||||
|
deleted_at: Date | null
|
||||||
|
user: EntityConstructor<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
deleted_at: Date | null
|
||||||
|
email: EntityConstructor<{
|
||||||
|
email: string
|
||||||
|
isVerified: boolean
|
||||||
|
deleted_at: Date | null
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const metaData = MetadataStorage.getMetadataFromDecorator(User)
|
||||||
|
expect(metaData.className).toEqual("User")
|
||||||
|
expect(metaData.path).toEqual("User")
|
||||||
|
expect(metaData.tableName).toEqual("platform.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: "1:1",
|
||||||
|
name: "email",
|
||||||
|
entity: "Email",
|
||||||
|
nullable: false,
|
||||||
|
mappedBy: "user",
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "created_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "updated_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
onUpdate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
deleted_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "deleted_at",
|
||||||
|
nullable: true,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emailMetaData = MetadataStorage.getMetadataFromDecorator(Email)
|
||||||
|
expect(emailMetaData.className).toEqual("Email")
|
||||||
|
expect(emailMetaData.path).toEqual("Email")
|
||||||
|
expect(emailMetaData.tableName).toEqual("platform.email")
|
||||||
|
expect(emailMetaData.properties).toEqual({
|
||||||
|
email: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "string",
|
||||||
|
columnType: "text",
|
||||||
|
name: "email",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
isVerified: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "boolean",
|
||||||
|
columnType: "boolean",
|
||||||
|
name: "isVerified",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
reference: "1:1",
|
||||||
|
name: "user",
|
||||||
|
entity: "User",
|
||||||
|
nullable: false,
|
||||||
|
owner: true,
|
||||||
|
mappedBy: "email",
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "created_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "updated_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
onUpdate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
deleted_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "deleted_at",
|
||||||
|
nullable: true,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("define relationships between cross pg schemas entities", () => {
|
||||||
|
const model = new EntityBuilder()
|
||||||
|
|
||||||
|
const email = model.define("platform.email", {
|
||||||
|
email: model.text(),
|
||||||
|
isVerified: model.boolean(),
|
||||||
|
user: model.belongsTo(() => user),
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = model.define("public.user", {
|
||||||
|
id: model.number(),
|
||||||
|
username: model.text(),
|
||||||
|
email: model.hasOne(() => email),
|
||||||
|
})
|
||||||
|
|
||||||
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const User = entityBuilder(user)
|
||||||
|
const Email = entityBuilder(email)
|
||||||
|
|
||||||
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
deleted_at: Date | null
|
||||||
|
email: EntityConstructor<{
|
||||||
|
email: string
|
||||||
|
isVerified: boolean
|
||||||
|
deleted_at: Date | null
|
||||||
|
user: EntityConstructor<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
deleted_at: Date | null
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
expectTypeOf(new Email()).toMatchTypeOf<{
|
||||||
|
email: string
|
||||||
|
isVerified: boolean
|
||||||
|
deleted_at: Date | null
|
||||||
|
user: EntityConstructor<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
deleted_at: Date | null
|
||||||
|
email: EntityConstructor<{
|
||||||
|
email: string
|
||||||
|
isVerified: boolean
|
||||||
|
deleted_at: Date | null
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const metaData = MetadataStorage.getMetadataFromDecorator(User)
|
||||||
|
expect(metaData.className).toEqual("User")
|
||||||
|
expect(metaData.path).toEqual("User")
|
||||||
|
expect(metaData.tableName).toEqual("public.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: "1:1",
|
||||||
|
name: "email",
|
||||||
|
entity: "Email",
|
||||||
|
nullable: false,
|
||||||
|
mappedBy: "user",
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "created_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "updated_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
onUpdate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
deleted_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "deleted_at",
|
||||||
|
nullable: true,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emailMetaData = MetadataStorage.getMetadataFromDecorator(Email)
|
||||||
|
expect(emailMetaData.className).toEqual("Email")
|
||||||
|
expect(emailMetaData.path).toEqual("Email")
|
||||||
|
expect(emailMetaData.tableName).toEqual("platform.email")
|
||||||
|
expect(emailMetaData.properties).toEqual({
|
||||||
|
email: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "string",
|
||||||
|
columnType: "text",
|
||||||
|
name: "email",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
isVerified: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "boolean",
|
||||||
|
columnType: "boolean",
|
||||||
|
name: "isVerified",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
reference: "1:1",
|
||||||
|
name: "user",
|
||||||
|
entity: "User",
|
||||||
|
nullable: false,
|
||||||
|
owner: true,
|
||||||
|
mappedBy: "email",
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "created_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "updated_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
onUpdate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
deleted_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "deleted_at",
|
||||||
|
nullable: true,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Entity builder | manyToMany", () => {
|
describe("Entity builder | manyToMany", () => {
|
||||||
@@ -3100,7 +3464,7 @@ describe("Entity builder", () => {
|
|||||||
|
|
||||||
const entityBuilder = createMikrORMEntity()
|
const entityBuilder = createMikrORMEntity()
|
||||||
expect(() => entityBuilder(user)).toThrow(
|
expect(() => entityBuilder(user)).toThrow(
|
||||||
'Missing property "users" on "team" entity. Make sure to define it as a relationship'
|
'Missing property "users" on "Team" entity. Make sure to define it as a relationship'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -3120,7 +3484,7 @@ describe("Entity builder", () => {
|
|||||||
|
|
||||||
const entityBuilder = createMikrORMEntity()
|
const entityBuilder = createMikrORMEntity()
|
||||||
expect(() => entityBuilder(user)).toThrow(
|
expect(() => entityBuilder(user)).toThrow(
|
||||||
'Invalid relationship reference for "users" on "team" entity. Make sure to define a manyToMany relationship'
|
'Invalid relationship reference for "users" on "Team" entity. Make sure to define a manyToMany relationship'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -3680,5 +4044,176 @@ 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(),
|
||||||
|
users: model.manyToMany(() => user),
|
||||||
|
})
|
||||||
|
|
||||||
|
const user = model.define("platform.user", {
|
||||||
|
id: model.number(),
|
||||||
|
username: model.text(),
|
||||||
|
teams: model.manyToMany(() => team),
|
||||||
|
})
|
||||||
|
|
||||||
|
const entityBuilder = createMikrORMEntity()
|
||||||
|
const User = entityBuilder(user)
|
||||||
|
const Team = entityBuilder(team)
|
||||||
|
|
||||||
|
expectTypeOf(new User()).toMatchTypeOf<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
teams: EntityConstructor<{
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
users: EntityConstructor<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
expectTypeOf(new Team()).toMatchTypeOf<{
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
users: EntityConstructor<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
teams: EntityConstructor<{
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}>
|
||||||
|
}>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const metaData = MetadataStorage.getMetadataFromDecorator(User)
|
||||||
|
expect(metaData.className).toEqual("User")
|
||||||
|
expect(metaData.path).toEqual("User")
|
||||||
|
expect(metaData.tableName).toEqual("platform.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,
|
||||||
|
},
|
||||||
|
teams: {
|
||||||
|
reference: "m:n",
|
||||||
|
name: "teams",
|
||||||
|
entity: "Team",
|
||||||
|
pivotTable: "platform.team_users",
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "created_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "updated_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
onUpdate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
deleted_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "deleted_at",
|
||||||
|
nullable: true,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const teamMetaData = MetadataStorage.getMetadataFromDecorator(Team)
|
||||||
|
expect(teamMetaData.className).toEqual("Team")
|
||||||
|
expect(teamMetaData.path).toEqual("Team")
|
||||||
|
expect(teamMetaData.tableName).toEqual("platform.team")
|
||||||
|
expect(teamMetaData.properties).toEqual({
|
||||||
|
id: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "number",
|
||||||
|
columnType: "integer",
|
||||||
|
name: "id",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "string",
|
||||||
|
columnType: "text",
|
||||||
|
name: "name",
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
reference: "m:n",
|
||||||
|
name: "users",
|
||||||
|
entity: "User",
|
||||||
|
pivotTable: "platform.team_users",
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "created_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "updated_at",
|
||||||
|
defaultRaw: "now()",
|
||||||
|
onCreate: expect.any(Function),
|
||||||
|
onUpdate: expect.any(Function),
|
||||||
|
nullable: false,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
deleted_at: {
|
||||||
|
reference: "scalar",
|
||||||
|
type: "date",
|
||||||
|
columnType: "timestamptz",
|
||||||
|
name: "deleted_at",
|
||||||
|
nullable: true,
|
||||||
|
getter: false,
|
||||||
|
setter: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -104,6 +104,33 @@ const SPECIAL_PROPERTIES: {
|
|||||||
* to Mikro ORM entities.
|
* to Mikro ORM entities.
|
||||||
*/
|
*/
|
||||||
export function createMikrORMEntity() {
|
export function createMikrORMEntity() {
|
||||||
|
/**
|
||||||
|
* Parses entity name and returns model and table name from
|
||||||
|
* it
|
||||||
|
*/
|
||||||
|
function parseEntityName(entityName: string) {
|
||||||
|
/**
|
||||||
|
* Table name is going to be the snake case version of the entity name.
|
||||||
|
* Here we should preserve PG schema (if defined).
|
||||||
|
*
|
||||||
|
* For example: "platform.user" should stay as "platform.user"
|
||||||
|
*/
|
||||||
|
const tableName = camelToSnakeCase(entityName)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entity name is going to be the camelCase version of the
|
||||||
|
* name defined by the user
|
||||||
|
*/
|
||||||
|
const [pgSchema, ...rest] = tableName.split(".")
|
||||||
|
return {
|
||||||
|
tableName,
|
||||||
|
modelName: upperCaseFirst(
|
||||||
|
toCamelCase(rest.length ? rest.join("_") : pgSchema)
|
||||||
|
),
|
||||||
|
pgSchema: rest.length ? pgSchema : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The following property is used to track many to many relationship
|
* The following property is used to track many to many relationship
|
||||||
* between two entities. It is needed because we have to mark one
|
* between two entities. It is needed because we have to mark one
|
||||||
@@ -162,19 +189,12 @@ export function createMikrORMEntity() {
|
|||||||
*/
|
*/
|
||||||
function applyIndexes(
|
function applyIndexes(
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
{
|
tableName: string,
|
||||||
tableName,
|
|
||||||
pgSchema,
|
|
||||||
}: { tableName: string; pgSchema: undefined | string },
|
|
||||||
field: PropertyMetadata
|
field: PropertyMetadata
|
||||||
) {
|
) {
|
||||||
field.indexes.forEach((index) => {
|
field.indexes.forEach((index) => {
|
||||||
const name =
|
|
||||||
index.name || `IDX_${tableName}_${camelToSnakeCase(field.fieldName)}`
|
|
||||||
|
|
||||||
const providerEntityIdIndexStatement = createPsqlIndexStatementHelper({
|
const providerEntityIdIndexStatement = createPsqlIndexStatementHelper({
|
||||||
name,
|
tableName,
|
||||||
tableName: pgSchema ? `${pgSchema}"."${tableName}` : tableName,
|
|
||||||
columns: [field.fieldName],
|
columns: [field.fieldName],
|
||||||
unique: index.type === "unique",
|
unique: index.type === "unique",
|
||||||
where: "deleted_at IS NULL",
|
where: "deleted_at IS NULL",
|
||||||
@@ -190,12 +210,9 @@ export function createMikrORMEntity() {
|
|||||||
function defineHasOneRelationship(
|
function defineHasOneRelationship(
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
relationship: RelationshipMetadata,
|
relationship: RelationshipMetadata,
|
||||||
relatedEntity: DmlEntity<
|
{ relatedModelName }: { relatedModelName: string },
|
||||||
Record<string, PropertyType<any> | RelationshipType<any>>
|
|
||||||
>,
|
|
||||||
cascades: EntityCascades<string[]>
|
cascades: EntityCascades<string[]>
|
||||||
) {
|
) {
|
||||||
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
|
||||||
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
|
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
|
||||||
|
|
||||||
OneToOne({
|
OneToOne({
|
||||||
@@ -214,12 +231,9 @@ export function createMikrORMEntity() {
|
|||||||
function defineHasManyRelationship(
|
function defineHasManyRelationship(
|
||||||
MikroORMEntity: EntityConstructor<any>,
|
MikroORMEntity: EntityConstructor<any>,
|
||||||
relationship: RelationshipMetadata,
|
relationship: RelationshipMetadata,
|
||||||
relatedEntity: DmlEntity<
|
{ relatedModelName }: { relatedModelName: string },
|
||||||
Record<string, PropertyType<any> | RelationshipType<any>>
|
|
||||||
>,
|
|
||||||
cascades: EntityCascades<string[]>
|
cascades: EntityCascades<string[]>
|
||||||
) {
|
) {
|
||||||
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
|
||||||
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
|
const shouldRemoveRelated = !!cascades.delete?.includes(relationship.name)
|
||||||
|
|
||||||
OneToMany({
|
OneToMany({
|
||||||
@@ -246,7 +260,8 @@ export function createMikrORMEntity() {
|
|||||||
relationship: RelationshipMetadata,
|
relationship: RelationshipMetadata,
|
||||||
relatedEntity: DmlEntity<
|
relatedEntity: DmlEntity<
|
||||||
Record<string, PropertyType<any> | RelationshipType<any>>
|
Record<string, PropertyType<any> | RelationshipType<any>>
|
||||||
>
|
>,
|
||||||
|
{ relatedModelName }: { relatedModelName: string }
|
||||||
) {
|
) {
|
||||||
const mappedBy =
|
const mappedBy =
|
||||||
relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name)
|
relationship.mappedBy || camelToSnakeCase(MikroORMEntity.name)
|
||||||
@@ -254,7 +269,6 @@ export function createMikrORMEntity() {
|
|||||||
relatedEntity.parse()
|
relatedEntity.parse()
|
||||||
|
|
||||||
const otherSideRelation = relationSchema[mappedBy]
|
const otherSideRelation = relationSchema[mappedBy]
|
||||||
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In DML the relationships are cascaded from parent to child. A belongsTo
|
* In DML the relationships are cascaded from parent to child. A belongsTo
|
||||||
@@ -269,7 +283,7 @@ export function createMikrORMEntity() {
|
|||||||
*/
|
*/
|
||||||
if (!otherSideRelation) {
|
if (!otherSideRelation) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Missing property "${mappedBy}" on "${relatedEntity.name}" entity. Make sure to define it as a relationship`
|
`Missing property "${mappedBy}" on "${relatedModelName}" entity. Make sure to define it as a relationship`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,7 +325,7 @@ export function createMikrORMEntity() {
|
|||||||
* Other side is some unsupported data-type
|
* Other side is some unsupported data-type
|
||||||
*/
|
*/
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid relationship reference for "${mappedBy}" on "${relatedEntity.name}" entity. Make sure to define a hasOne or hasMany relationship`
|
`Invalid relationship reference for "${mappedBy}" on "${relatedModelName}" entity. Make sure to define a hasOne or hasMany relationship`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,9 +338,11 @@ export function createMikrORMEntity() {
|
|||||||
relatedEntity: DmlEntity<
|
relatedEntity: DmlEntity<
|
||||||
Record<string, PropertyType<any> | RelationshipType<any>>
|
Record<string, PropertyType<any> | RelationshipType<any>>
|
||||||
>,
|
>,
|
||||||
cascades: EntityCascades<string[]>
|
{
|
||||||
|
relatedModelName,
|
||||||
|
pgSchema,
|
||||||
|
}: { relatedModelName: string; pgSchema: string | undefined }
|
||||||
) {
|
) {
|
||||||
const relatedModelName = upperCaseFirst(relatedEntity.name)
|
|
||||||
let mappedBy = relationship.mappedBy
|
let mappedBy = relationship.mappedBy
|
||||||
let inversedBy: undefined | string
|
let inversedBy: undefined | string
|
||||||
|
|
||||||
@@ -340,7 +356,7 @@ export function createMikrORMEntity() {
|
|||||||
*/
|
*/
|
||||||
const pivotTableName = [
|
const pivotTableName = [
|
||||||
MikroORMEntity.name.toLowerCase(),
|
MikroORMEntity.name.toLowerCase(),
|
||||||
relatedEntity.name.toLowerCase(),
|
relatedModelName.toLowerCase(),
|
||||||
]
|
]
|
||||||
.sort()
|
.sort()
|
||||||
.map((token, index) => {
|
.map((token, index) => {
|
||||||
@@ -355,13 +371,13 @@ export function createMikrORMEntity() {
|
|||||||
const otherSideRelation = relatedEntity.parse().schema[mappedBy]
|
const otherSideRelation = relatedEntity.parse().schema[mappedBy]
|
||||||
if (!otherSideRelation) {
|
if (!otherSideRelation) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Missing property "${mappedBy}" on "${relatedEntity.name}" entity. Make sure to define it as a relationship`
|
`Missing property "${mappedBy}" on "${relatedModelName}" entity. Make sure to define it as a relationship`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (otherSideRelation instanceof DmlManyToMany === false) {
|
if (otherSideRelation instanceof DmlManyToMany === false) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid relationship reference for "${mappedBy}" on "${relatedEntity.name}" entity. Make sure to define a manyToMany relationship`
|
`Invalid relationship reference for "${mappedBy}" on "${relatedModelName}" entity. Make sure to define a manyToMany relationship`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +403,7 @@ export function createMikrORMEntity() {
|
|||||||
|
|
||||||
ManyToMany({
|
ManyToMany({
|
||||||
entity: relatedModelName,
|
entity: relatedModelName,
|
||||||
pivotTable: pivotTableName,
|
pivotTable: pgSchema ? `${pgSchema}.${pivotTableName}` : pivotTableName,
|
||||||
...(mappedBy ? { mappedBy: mappedBy as any } : {}),
|
...(mappedBy ? { mappedBy: mappedBy as any } : {}),
|
||||||
...(inversedBy ? { inversedBy: inversedBy as any } : {}),
|
...(inversedBy ? { inversedBy: inversedBy as any } : {}),
|
||||||
})(MikroORMEntity.prototype, relationship.name)
|
})(MikroORMEntity.prototype, relationship.name)
|
||||||
@@ -429,6 +445,15 @@ export function createMikrORMEntity() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { modelName, tableName, pgSchema } = parseEntityName(
|
||||||
|
relatedEntity.parse().name
|
||||||
|
)
|
||||||
|
const relatedEntityInfo = {
|
||||||
|
relatedModelName: modelName,
|
||||||
|
relatedTableName: tableName,
|
||||||
|
pgSchema,
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defining relationships
|
* Defining relationships
|
||||||
*/
|
*/
|
||||||
@@ -437,7 +462,7 @@ export function createMikrORMEntity() {
|
|||||||
defineHasOneRelationship(
|
defineHasOneRelationship(
|
||||||
MikroORMEntity,
|
MikroORMEntity,
|
||||||
relationship,
|
relationship,
|
||||||
relatedEntity,
|
relatedEntityInfo,
|
||||||
cascades
|
cascades
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
@@ -445,19 +470,24 @@ export function createMikrORMEntity() {
|
|||||||
defineHasManyRelationship(
|
defineHasManyRelationship(
|
||||||
MikroORMEntity,
|
MikroORMEntity,
|
||||||
relationship,
|
relationship,
|
||||||
relatedEntity,
|
relatedEntityInfo,
|
||||||
cascades
|
cascades
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case "belongsTo":
|
case "belongsTo":
|
||||||
defineBelongsToRelationship(MikroORMEntity, relationship, relatedEntity)
|
defineBelongsToRelationship(
|
||||||
|
MikroORMEntity,
|
||||||
|
relationship,
|
||||||
|
relatedEntity,
|
||||||
|
relatedEntityInfo
|
||||||
|
)
|
||||||
break
|
break
|
||||||
case "manyToMany":
|
case "manyToMany":
|
||||||
defineManyToManyRelationship(
|
defineManyToManyRelationship(
|
||||||
MikroORMEntity,
|
MikroORMEntity,
|
||||||
relationship,
|
relationship,
|
||||||
relatedEntity,
|
relatedEntity,
|
||||||
cascades
|
relatedEntityInfo
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -470,26 +500,7 @@ export function createMikrORMEntity() {
|
|||||||
return function createEntity<T extends DmlEntity<any>>(entity: T): Infer<T> {
|
return function createEntity<T extends DmlEntity<any>>(entity: T): Infer<T> {
|
||||||
class MikroORMEntity {}
|
class MikroORMEntity {}
|
||||||
const { name, schema, cascades } = entity.parse()
|
const { name, schema, cascades } = entity.parse()
|
||||||
const [pgSchema, ...rest] = name.split(".")
|
const { modelName, tableName } = parseEntityName(name)
|
||||||
|
|
||||||
/**
|
|
||||||
* Entity name is computed by splitting the pgSchema
|
|
||||||
* from the original name and converting everything
|
|
||||||
* to camelCase
|
|
||||||
*/
|
|
||||||
const entityName = rest.length
|
|
||||||
? toCamelCase(rest.join("_"))
|
|
||||||
: toCamelCase(pgSchema)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table name is the snake case version of entityName
|
|
||||||
*/
|
|
||||||
const tableName = camelToSnakeCase(entityName)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Table name is the Pascal case version of entityName
|
|
||||||
*/
|
|
||||||
const modelName = upperCaseFirst(entityName)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assigning name to the class constructor, so that it matches
|
* Assigning name to the class constructor, so that it matches
|
||||||
@@ -508,11 +519,7 @@ export function createMikrORMEntity() {
|
|||||||
const field = property.parse(name)
|
const field = property.parse(name)
|
||||||
if ("fieldName" in field) {
|
if ("fieldName" in field) {
|
||||||
defineProperty(MikroORMEntity, field)
|
defineProperty(MikroORMEntity, field)
|
||||||
applyIndexes(
|
applyIndexes(MikroORMEntity, tableName, field)
|
||||||
MikroORMEntity,
|
|
||||||
{ tableName, pgSchema: rest.length ? pgSchema : undefined },
|
|
||||||
field
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
defineRelationship(MikroORMEntity, field, cascades)
|
defineRelationship(MikroORMEntity, field, cascades)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user