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 authored and GitHub committed 2024-06-27 14:45:27 +00:00
1 parent 6e65158af3
commit 91faa5ffdb
8 files changed
+133 -64

No files matched your search

+8 -6
View File
@@ -106,12 +106,12 @@ export interface EntityConstructor<Props> extends Function {
*/ */
export type InferForeignKeys<T> = T extends IDmlEntity<infer Schema> export type InferForeignKeys<T> = T extends IDmlEntity<infer Schema>
? { ? {
[K in keyof Schema as Schema[K] extends RelationshipType<any> [K in keyof Schema as Schema[K] extends { type: infer Type }
? Schema[K]["type"] extends "belongsTo" ? Type extends RelationshipTypes
? `${K & string}_id` ? `${K & string}_id`
: K : K
: K]: Schema[K] extends RelationshipType<infer R> : K]: Schema[K] extends { type: infer Type }
? Schema[K]["type"] extends "belongsTo" ? Type extends RelationshipTypes
? string ? string
: Schema[K] : Schema[K]
: Schema[K] : Schema[K]
@@ -174,8 +174,10 @@ export type InferIndexableProperties<T> = keyof (T extends IDmlEntity<
infer Schema infer Schema
> >
? { ? {
[K in keyof Schema as Schema[K] extends RelationshipType<any> [K in keyof Schema as Schema[K] extends { type: infer Type }
? never ? Type extends RelationshipTypes
? never
: K
: K]: string : K]: string
} & InferForeignKeys<T> } & InferForeignKeys<T>
: never) : never)
@@ -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
}
}
@@ -2482,6 +2482,7 @@ describe("Entity builder", () => {
reference: "scalar", reference: "scalar",
setter: false, setter: false,
type: "string", type: "string",
isForeignKey: true,
}, },
created_at: { created_at: {
reference: "scalar", reference: "scalar",
@@ -2598,6 +2599,7 @@ describe("Entity builder", () => {
nullable: false, nullable: false,
onDelete: undefined, onDelete: undefined,
reference: "m:1", reference: "m:1",
isForeignKey: true,
}, },
...defaultColumnMetadata, ...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', 'CREATE UNIQUE INDEX IF NOT EXISTS "IDX_unique-name" ON "user" (organization, account, group_id) WHERE deleted_at IS NULL',
name: "IDX_unique-name", 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', '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", 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, nullable: false,
onDelete: "cascade", onDelete: "cascade",
reference: "m:1", reference: "m:1",
isForeignKey: true,
}, },
created_at: { created_at: {
reference: "scalar", reference: "scalar",
@@ -3307,6 +3320,7 @@ describe("Entity builder", () => {
name: "user_id", name: "user_id",
getter: false, getter: false,
setter: false, setter: false,
isForeignKey: true,
}, },
created_at: { created_at: {
reference: "scalar", reference: "scalar",
@@ -3486,6 +3500,7 @@ describe("Entity builder", () => {
name: "user_id", name: "user_id",
getter: false, getter: false,
setter: false, setter: false,
isForeignKey: true,
}, },
created_at: { created_at: {
reference: "scalar", reference: "scalar",
@@ -3664,6 +3679,7 @@ describe("Entity builder", () => {
mapToPk: true, mapToPk: true,
fieldName: "user_id", fieldName: "user_id",
nullable: false, nullable: false,
isForeignKey: true,
}, },
created_at: { created_at: {
reference: "scalar", reference: "scalar",
@@ -3842,6 +3858,7 @@ describe("Entity builder", () => {
mapToPk: true, mapToPk: true,
fieldName: "user_id", fieldName: "user_id",
nullable: true, nullable: true,
isForeignKey: true,
}, },
created_at: { created_at: {
reference: "scalar", reference: "scalar",
@@ -4087,6 +4104,7 @@ describe("Entity builder", () => {
name: "user_id", name: "user_id",
getter: false, getter: false,
setter: false, setter: false,
isForeignKey: true,
}, },
created_at: { created_at: {
reference: "scalar", reference: "scalar",
@@ -4274,6 +4292,7 @@ describe("Entity builder", () => {
name: "user_id", name: "user_id",
getter: false, getter: false,
setter: false, setter: false,
isForeignKey: true,
}, },
created_at: { created_at: {
reference: "scalar", reference: "scalar",
@@ -5644,6 +5663,7 @@ describe("Entity builder", () => {
mapToPk: true, mapToPk: true,
fieldName: "user_id", fieldName: "user_id",
nullable: false, nullable: false,
isForeignKey: true,
}, },
user: { user: {
reference: "scalar", reference: "scalar",
@@ -5662,6 +5682,7 @@ describe("Entity builder", () => {
mapToPk: true, mapToPk: true,
fieldName: "team_id", fieldName: "team_id",
nullable: false, nullable: false,
isForeignKey: true,
}, },
team: { team: {
reference: "scalar", reference: "scalar",
+3
View File
@@ -122,6 +122,9 @@ export class DmlEntity<Schema extends DMLSchema> implements IDmlEntity<Schema> {
return this return this
} }
/**
* Adds indexes to be created at during model creation on the DML entity.
*/
indexes(indexes: EntityIndex<Schema, string | QueryCondition>[]) { indexes(indexes: EntityIndex<Schema, string | QueryCondition>[]) {
for (const index of indexes) { for (const index of indexes) {
index.where = transformIndexWhere(index) index.where = transformIndexWhere(index)
@@ -2,14 +2,11 @@ import type { EntityConstructor, Infer } from "@medusajs/types"
import { Entity, Filter } from "@mikro-orm/core" import { Entity, Filter } from "@mikro-orm/core"
import { mikroOrmSoftDeletableFilterOptions } from "../../dal" import { mikroOrmSoftDeletableFilterOptions } from "../../dal"
import { DmlEntity } from "../entity" import { DmlEntity } from "../entity"
import {
applyEntityIndexes,
applyIndexes,
} from "./entity-builder/apply-indexes"
import { applySearchable } from "./entity-builder/apply-searchable" import { applySearchable } from "./entity-builder/apply-searchable"
import { defineProperty } from "./entity-builder/define-property" import { defineProperty } from "./entity-builder/define-property"
import { defineRelationship } from "./entity-builder/define-relationship" import { defineRelationship } from "./entity-builder/define-relationship"
import { parseEntityName } from "./entity-builder/parse-entity-name" 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 * Factory function to create the mikro orm entity builder. The return
@@ -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)
})
}
@@ -5,8 +5,6 @@ import {
RelationshipMetadata, RelationshipMetadata,
RelationshipType, RelationshipType,
} from "@medusajs/types" } from "@medusajs/types"
import { DmlEntity } from "../../entity"
import { parseEntityName } from "./parse-entity-name"
import { import {
BeforeCreate, BeforeCreate,
ManyToMany, ManyToMany,
@@ -17,9 +15,12 @@ import {
Property, Property,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { camelToSnakeCase, pluralize } from "../../../common" 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 { HasMany } from "../../relations/has-many"
import { HasOne } from "../../relations/has-one" import { HasOne } from "../../relations/has-one"
import { ManyToMany as DmlManyToMany } from "../../relations/many-to-many" import { ManyToMany as DmlManyToMany } from "../../relations/many-to-many"
import { parseEntityName } from "./parse-entity-name"
type Context = { type Context = {
MANY_TO_MANY_TRACKED_REALTIONS: Record<string, boolean> MANY_TO_MANY_TRACKED_REALTIONS: Record<string, boolean>
@@ -138,10 +139,11 @@ export function defineBelongsToRelationship(
entity: relatedModelName, entity: relatedModelName,
columnType: "text", columnType: "text",
mapToPk: true, mapToPk: true,
fieldName: camelToSnakeCase(`${relationship.name}Id`), fieldName: foreignKeyName,
nullable: relationship.nullable, nullable: relationship.nullable,
onDelete: shouldCascade ? "cascade" : undefined, onDelete: shouldCascade ? "cascade" : undefined,
})(MikroORMEntity.prototype, camelToSnakeCase(`${relationship.name}Id`)) })(MikroORMEntity.prototype, foreignKeyName)
ForeignKey()(MikroORMEntity.prototype, foreignKeyName)
if (DmlManyToMany.isManyToMany(otherSideRelation)) { if (DmlManyToMany.isManyToMany(otherSideRelation)) {
Property({ Property({
@@ -190,6 +192,7 @@ export function defineBelongsToRelationship(
columnType: "text", columnType: "text",
nullable: relationship.nullable, nullable: relationship.nullable,
})(MikroORMEntity.prototype, foreignKeyName) })(MikroORMEntity.prototype, foreignKeyName)
ForeignKey()(MikroORMEntity.prototype, foreignKeyName)
applyForeignKeyAssignationHooks(foreignKeyName) applyForeignKeyAssignationHooks(foreignKeyName)
return return
@@ -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
)
}