From 91faa5ffdb2c21007b9062042f8afb7ce5691012 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Thu, 27 Jun 2024 16:45:27 +0200 Subject: [PATCH] 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 --- packages/core/types/src/dml/index.ts | 14 ++-- .../dal/mikro-orm/decorators/foreign-key.ts | 10 +++ .../src/dml/__tests__/entity-builder.spec.ts | 21 +++++ packages/core/utils/src/dml/entity.ts | 3 + .../dml/helpers/create-mikro-orm-entity.ts | 5 +- .../helpers/entity-builder/apply-indexes.ts | 50 ----------- .../entity-builder/define-relationship.ts | 11 ++- .../dml/helpers/mikro-orm/apply-indexes.ts | 83 +++++++++++++++++++ 8 files changed, 133 insertions(+), 64 deletions(-) create mode 100644 packages/core/utils/src/dal/mikro-orm/decorators/foreign-key.ts delete mode 100644 packages/core/utils/src/dml/helpers/entity-builder/apply-indexes.ts create mode 100644 packages/core/utils/src/dml/helpers/mikro-orm/apply-indexes.ts diff --git a/packages/core/types/src/dml/index.ts b/packages/core/types/src/dml/index.ts index 4fd0a4e016..cbf256ab10 100644 --- a/packages/core/types/src/dml/index.ts +++ b/packages/core/types/src/dml/index.ts @@ -106,12 +106,12 @@ export interface EntityConstructor extends Function { */ export type InferForeignKeys = T extends IDmlEntity ? { - [K in keyof Schema as Schema[K] extends RelationshipType - ? 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 - ? 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 = keyof (T extends IDmlEntity< infer Schema > ? { - [K in keyof Schema as Schema[K] extends RelationshipType - ? never + [K in keyof Schema as Schema[K] extends { type: infer Type } + ? Type extends RelationshipTypes + ? never + : K : K]: string } & InferForeignKeys : never) diff --git a/packages/core/utils/src/dal/mikro-orm/decorators/foreign-key.ts b/packages/core/utils/src/dal/mikro-orm/decorators/foreign-key.ts new file mode 100644 index 0000000000..8d2cf213af --- /dev/null +++ b/packages/core/utils/src/dal/mikro-orm/decorators/foreign-key.ts @@ -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 + } +} diff --git a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts index 2a09226a27..3abff26809 100644 --- a/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts +++ b/packages/core/utils/src/dml/__tests__/entity-builder.spec.ts @@ -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", diff --git a/packages/core/utils/src/dml/entity.ts b/packages/core/utils/src/dml/entity.ts index 30ebba5441..51a80d9066 100644 --- a/packages/core/utils/src/dml/entity.ts +++ b/packages/core/utils/src/dml/entity.ts @@ -122,6 +122,9 @@ export class DmlEntity implements IDmlEntity { return this } + /** + * Adds indexes to be created at during model creation on the DML entity. + */ indexes(indexes: EntityIndex[]) { for (const index of indexes) { index.where = transformIndexWhere(index) diff --git a/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts b/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts index 006fb14d5d..5468d14793 100644 --- a/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts +++ b/packages/core/utils/src/dml/helpers/create-mikro-orm-entity.ts @@ -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 diff --git a/packages/core/utils/src/dml/helpers/entity-builder/apply-indexes.ts b/packages/core/utils/src/dml/helpers/entity-builder/apply-indexes.ts deleted file mode 100644 index 82437b501c..0000000000 --- a/packages/core/utils/src/dml/helpers/entity-builder/apply-indexes.ts +++ /dev/null @@ -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, - 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, - 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) - }) -} diff --git a/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts b/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts index 2669a730c5..d0852b5e6d 100644 --- a/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts +++ b/packages/core/utils/src/dml/helpers/entity-builder/define-relationship.ts @@ -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 @@ -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 diff --git a/packages/core/utils/src/dml/helpers/mikro-orm/apply-indexes.ts b/packages/core/utils/src/dml/helpers/mikro-orm/apply-indexes.ts new file mode 100644 index 0000000000..421d675f75 --- /dev/null +++ b/packages/core/utils/src/dml/helpers/mikro-orm/apply-indexes.ts @@ -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, + 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, + 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) { + const foreignKeyIndexes: EntityIndex[] = [] + + for (const foreignKey of getEntityForeignKeys(MikroORMEntity)) { + foreignKeyIndexes.push({ + on: [foreignKey], + where: "deleted_at IS NULL", + }) + } + + return foreignKeyIndexes +} + +function getEntityForeignKeys(MikroORMEntity: EntityConstructor) { + const properties = + MetadataStorage.getMetadataFromDecorator(MikroORMEntity).properties + + return Object.keys(properties).filter( + (propertyName) => properties[propertyName].isForeignKey + ) +}