feat(utils): DML generates foreign key indexes by default (#7855)

**what:**

- adds default indexes on foreign keys when belongs to relationship is present

RESOLVES CORE-2404
This commit is contained in:
Riqwan Thamir
2024-06-27 16:45:27 +02:00
committed by GitHub
parent 6e65158af3
commit 91faa5ffdb
8 changed files with 133 additions and 64 deletions

View File

@@ -106,12 +106,12 @@ export interface EntityConstructor<Props> extends Function {
*/
export type InferForeignKeys<T> = T extends IDmlEntity<infer Schema>
? {
[K in keyof Schema as Schema[K] extends RelationshipType<any>
? Schema[K]["type"] extends "belongsTo"
[K in keyof Schema as Schema[K] extends { type: infer Type }
? Type extends RelationshipTypes
? `${K & string}_id`
: K
: K]: Schema[K] extends RelationshipType<infer R>
? Schema[K]["type"] extends "belongsTo"
: K]: Schema[K] extends { type: infer Type }
? Type extends RelationshipTypes
? string
: Schema[K]
: Schema[K]
@@ -174,8 +174,10 @@ export type InferIndexableProperties<T> = keyof (T extends IDmlEntity<
infer Schema
>
? {
[K in keyof Schema as Schema[K] extends RelationshipType<any>
? never
[K in keyof Schema as Schema[K] extends { type: infer Type }
? Type extends RelationshipTypes
? never
: K
: K]: string
} & InferForeignKeys<T>
: never)

View File

@@ -0,0 +1,10 @@
import { MetadataStorage } from "@mikro-orm/core"
export function ForeignKey() {
return function (target, propertyName) {
const meta = MetadataStorage.getMetadataFromDecorator(target.constructor)
const prop = meta.properties[propertyName] || {}
prop["isForeignKey"] = true
meta.properties[prop.name] = prop
}
}

View File

@@ -2482,6 +2482,7 @@ describe("Entity builder", () => {
reference: "scalar",
setter: false,
type: "string",
isForeignKey: true,
},
created_at: {
reference: "scalar",
@@ -2598,6 +2599,7 @@ describe("Entity builder", () => {
nullable: false,
onDelete: undefined,
reference: "m:1",
isForeignKey: true,
},
...defaultColumnMetadata,
})
@@ -2623,6 +2625,11 @@ describe("Entity builder", () => {
'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_unique-name" ON "user" (organization, account, group_id) WHERE deleted_at IS NULL',
name: "IDX_unique-name",
},
{
expression:
'CREATE INDEX IF NOT EXISTS "IDX_user_group_id" ON "user" (group_id) WHERE deleted_at IS NULL',
name: "IDX_user_group_id",
},
])
})
@@ -2699,6 +2706,11 @@ describe("Entity builder", () => {
'CREATE INDEX IF NOT EXISTS "IDX_user_account_group_id" ON "user" (account, group_id) WHERE is_owner IS TRUE AND deleted_at IS NULL',
name: "IDX_user_account_group_id",
},
{
expression:
'CREATE INDEX IF NOT EXISTS "IDX_user_group_id" ON "user" (group_id) WHERE deleted_at IS NULL',
name: "IDX_user_group_id",
},
])
})
@@ -3121,6 +3133,7 @@ describe("Entity builder", () => {
nullable: false,
onDelete: "cascade",
reference: "m:1",
isForeignKey: true,
},
created_at: {
reference: "scalar",
@@ -3307,6 +3320,7 @@ describe("Entity builder", () => {
name: "user_id",
getter: false,
setter: false,
isForeignKey: true,
},
created_at: {
reference: "scalar",
@@ -3486,6 +3500,7 @@ describe("Entity builder", () => {
name: "user_id",
getter: false,
setter: false,
isForeignKey: true,
},
created_at: {
reference: "scalar",
@@ -3664,6 +3679,7 @@ describe("Entity builder", () => {
mapToPk: true,
fieldName: "user_id",
nullable: false,
isForeignKey: true,
},
created_at: {
reference: "scalar",
@@ -3842,6 +3858,7 @@ describe("Entity builder", () => {
mapToPk: true,
fieldName: "user_id",
nullable: true,
isForeignKey: true,
},
created_at: {
reference: "scalar",
@@ -4087,6 +4104,7 @@ describe("Entity builder", () => {
name: "user_id",
getter: false,
setter: false,
isForeignKey: true,
},
created_at: {
reference: "scalar",
@@ -4274,6 +4292,7 @@ describe("Entity builder", () => {
name: "user_id",
getter: false,
setter: false,
isForeignKey: true,
},
created_at: {
reference: "scalar",
@@ -5644,6 +5663,7 @@ describe("Entity builder", () => {
mapToPk: true,
fieldName: "user_id",
nullable: false,
isForeignKey: true,
},
user: {
reference: "scalar",
@@ -5662,6 +5682,7 @@ describe("Entity builder", () => {
mapToPk: true,
fieldName: "team_id",
nullable: false,
isForeignKey: true,
},
team: {
reference: "scalar",

View File

@@ -122,6 +122,9 @@ export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
return this
}
/**
* Adds indexes to be created at during model creation on the DML entity.
*/
indexes(indexes: EntityIndex<Schema, string | QueryCondition>[]) {
for (const index of indexes) {
index.where = transformIndexWhere(index)

View File

@@ -2,14 +2,11 @@ import type { EntityConstructor, Infer } from "@medusajs/types"
import { Entity, Filter } from "@mikro-orm/core"
import { mikroOrmSoftDeletableFilterOptions } from "../../dal"
import { DmlEntity } from "../entity"
import {
applyEntityIndexes,
applyIndexes,
} from "./entity-builder/apply-indexes"
import { applySearchable } from "./entity-builder/apply-searchable"
import { defineProperty } from "./entity-builder/define-property"
import { defineRelationship } from "./entity-builder/define-relationship"
import { parseEntityName } from "./entity-builder/parse-entity-name"
import { applyEntityIndexes, applyIndexes } from "./mikro-orm/apply-indexes"
/**
* Factory function to create the mikro orm entity builder. The return

View File

@@ -1,50 +0,0 @@
import {
EntityConstructor,
EntityIndex,
PropertyMetadata,
} from "@medusajs/types"
import { createPsqlIndexStatementHelper } from "../../../common"
import { validateIndexFields } from "../mikro-orm/build-indexes"
/**
* Prepares indexes for a given field
*/
export function applyIndexes(
MikroORMEntity: EntityConstructor<any>,
tableName: string,
field: PropertyMetadata
) {
field.indexes.forEach((index) => {
const providerEntityIdIndexStatement = createPsqlIndexStatementHelper({
tableName,
columns: [field.fieldName],
unique: index.type === "unique",
where: "deleted_at IS NULL",
})
providerEntityIdIndexStatement.MikroORMIndex()(MikroORMEntity)
})
}
/**
* Prepares indexes for a given field
*/
export function applyEntityIndexes(
MikroORMEntity: EntityConstructor<any>,
tableName: string,
indexes: EntityIndex[]
) {
indexes.forEach((index) => {
validateIndexFields(MikroORMEntity, index)
const providerEntityIdIndexStatement = createPsqlIndexStatementHelper({
tableName,
name: index.name,
columns: index.on as string[],
unique: index.unique,
where: index.where,
})
providerEntityIdIndexStatement.MikroORMIndex()(MikroORMEntity)
})
}

View File

@@ -5,8 +5,6 @@ import {
RelationshipMetadata,
RelationshipType,
} from "@medusajs/types"
import { DmlEntity } from "../../entity"
import { parseEntityName } from "./parse-entity-name"
import {
BeforeCreate,
ManyToMany,
@@ -17,9 +15,12 @@ import {
Property,
} from "@mikro-orm/core"
import { camelToSnakeCase, pluralize } from "../../../common"
import { ForeignKey } from "../../../dal/mikro-orm/decorators/foreign-key"
import { DmlEntity } from "../../entity"
import { HasMany } from "../../relations/has-many"
import { HasOne } from "../../relations/has-one"
import { ManyToMany as DmlManyToMany } from "../../relations/many-to-many"
import { parseEntityName } from "./parse-entity-name"
type Context = {
MANY_TO_MANY_TRACKED_REALTIONS: Record<string, boolean>
@@ -138,10 +139,11 @@ export function defineBelongsToRelationship(
entity: relatedModelName,
columnType: "text",
mapToPk: true,
fieldName: camelToSnakeCase(`${relationship.name}Id`),
fieldName: foreignKeyName,
nullable: relationship.nullable,
onDelete: shouldCascade ? "cascade" : undefined,
})(MikroORMEntity.prototype, camelToSnakeCase(`${relationship.name}Id`))
})(MikroORMEntity.prototype, foreignKeyName)
ForeignKey()(MikroORMEntity.prototype, foreignKeyName)
if (DmlManyToMany.isManyToMany(otherSideRelation)) {
Property({
@@ -190,6 +192,7 @@ export function defineBelongsToRelationship(
columnType: "text",
nullable: relationship.nullable,
})(MikroORMEntity.prototype, foreignKeyName)
ForeignKey()(MikroORMEntity.prototype, foreignKeyName)
applyForeignKeyAssignationHooks(foreignKeyName)
return

View File

@@ -0,0 +1,83 @@
import {
EntityConstructor,
EntityIndex,
PropertyMetadata,
} from "@medusajs/types"
import { MetadataStorage } from "@mikro-orm/core"
import { createPsqlIndexStatementHelper } from "../../../common"
import { validateIndexFields } from "../mikro-orm/build-indexes"
/**
* Creates indexes for a given field
*/
export function applyIndexes(
MikroORMEntity: EntityConstructor<any>,
tableName: string,
field: PropertyMetadata
) {
field.indexes.forEach((index) => {
const providerEntityIdIndexStatement = createPsqlIndexStatementHelper({
tableName,
columns: [field.fieldName],
unique: index.type === "unique",
where: "deleted_at IS NULL",
})
providerEntityIdIndexStatement.MikroORMIndex()(MikroORMEntity)
})
}
/**
* Creates indexes for a MikroORM entity
*
* Default Indexes:
* - Foreign key indexes will be applied to all manyToOne relationships.
*/
export function applyEntityIndexes(
MikroORMEntity: EntityConstructor<any>,
tableName: string,
entityIndexes: EntityIndex[]
) {
const foreignKeyIndexes = applyForeignKeyIndexes(MikroORMEntity)
const indexes = [...entityIndexes, ...foreignKeyIndexes]
indexes.forEach((index) => {
validateIndexFields(MikroORMEntity, index)
const entityIndexStatement = createPsqlIndexStatementHelper({
tableName,
name: index.name,
columns: index.on as string[],
unique: index.unique,
where: index.where,
})
entityIndexStatement.MikroORMIndex()(MikroORMEntity)
})
}
/*
When a "oneToMany" relationship is found on the MikroORM entity, we create an index by default
on the foreign key property.
*/
function applyForeignKeyIndexes(MikroORMEntity: EntityConstructor<any>) {
const foreignKeyIndexes: EntityIndex[] = []
for (const foreignKey of getEntityForeignKeys(MikroORMEntity)) {
foreignKeyIndexes.push({
on: [foreignKey],
where: "deleted_at IS NULL",
})
}
return foreignKeyIndexes
}
function getEntityForeignKeys(MikroORMEntity: EntityConstructor<any>) {
const properties =
MetadataStorage.getMetadataFromDecorator(MikroORMEntity).properties
return Object.keys(properties).filter(
(propertyName) => properties[propertyName].isForeignKey
)
}