From 45e2228a07fef92c68e0ac3a209554f7e02b5773 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Fri, 10 May 2024 11:27:33 +0200 Subject: [PATCH] chore(utils): Provide a mikro orm base entity (#7286) **What** Provide a new mikro orm base entity in order to abstract away the on init and on create which include the id auto generation if not provided. The main goal is to reduce complexity in entity definition --- .changeset/old-candles-cough.md | 5 + packages/core/utils/src/dal/index.ts | 2 +- .../mikro-orm/__tests__/base-entity.spec.ts | 130 ++++++++++++++++++ .../utils/src/dal/mikro-orm/base-entity.ts | 68 +++++++++ .../core/utils/src/dal/repositories/index.ts | 1 - .../repositories/load-custom-repositories.ts | 35 ----- 6 files changed, 204 insertions(+), 37 deletions(-) create mode 100644 .changeset/old-candles-cough.md create mode 100644 packages/core/utils/src/dal/mikro-orm/__tests__/base-entity.spec.ts create mode 100644 packages/core/utils/src/dal/mikro-orm/base-entity.ts delete mode 100644 packages/core/utils/src/dal/repositories/index.ts delete mode 100644 packages/core/utils/src/dal/repositories/load-custom-repositories.ts diff --git a/.changeset/old-candles-cough.md b/.changeset/old-candles-cough.md new file mode 100644 index 0000000000000..c09b0772b45ed --- /dev/null +++ b/.changeset/old-candles-cough.md @@ -0,0 +1,5 @@ +--- +"@medusajs/utils": patch +--- + +chore(utils): Provide a mikro orm base entity diff --git a/packages/core/utils/src/dal/index.ts b/packages/core/utils/src/dal/index.ts index 30f303d1ab7b2..43f10afd5c017 100644 --- a/packages/core/utils/src/dal/index.ts +++ b/packages/core/utils/src/dal/index.ts @@ -4,7 +4,7 @@ export * from "./mikro-orm/mikro-orm-free-text-search-filter" export * from "./mikro-orm/mikro-orm-repository" export * from "./mikro-orm/mikro-orm-soft-deletable-filter" export * from "./mikro-orm/mikro-orm-serializer" +export * from "./mikro-orm/base-entity" export * from "./mikro-orm/utils" export * from "./mikro-orm/decorators/searchable" -export * from "./repositories" export * from "./utils" diff --git a/packages/core/utils/src/dal/mikro-orm/__tests__/base-entity.spec.ts b/packages/core/utils/src/dal/mikro-orm/__tests__/base-entity.spec.ts new file mode 100644 index 0000000000000..09afa1b181ed0 --- /dev/null +++ b/packages/core/utils/src/dal/mikro-orm/__tests__/base-entity.spec.ts @@ -0,0 +1,130 @@ +import { Entity, MikroORM, OnInit, Property } from "@mikro-orm/core" +import { BaseEntity } from "../base-entity" + +describe("BaseEntity", () => { + it("should handle the id generation using the provided prefix", async () => { + @Entity() + class Entity1 extends BaseEntity { + constructor() { + super({ prefix_id: "prod" }) + } + } + + const orm = await MikroORM.init({ + entities: [Entity1], + dbName: "test", + type: "postgresql", + }) + + const manager = orm.em.fork() + const entity1 = manager.create(Entity1, {}) + + expect(entity1.id).toMatch(/prod_[0-9]/) + + await orm.close() + }) + + it("should handle the id generation without a provided prefix using the first three letter of the entity lower cased", async () => { + @Entity() + class Entity1 extends BaseEntity {} + + const orm = await MikroORM.init({ + entities: [Entity1], + dbName: "test", + type: "postgresql", + }) + + const manager = orm.em.fork() + const entity1 = manager.create(Entity1, {}) + + expect(entity1.id).toMatch(/ent_[0-9]/) + + await orm.close() + }) + + it("should handle the id generation without a provided prefix inferring it based on the words composing the entity name excluding model and entity as part of the name", async () => { + @Entity() + class ProductModel extends BaseEntity {} + + @Entity() + class ProductCategoryEntity extends BaseEntity {} + + @Entity() + class ProductOptionValue extends BaseEntity {} + + const orm = await MikroORM.init({ + entities: [ProductModel, ProductCategoryEntity, ProductOptionValue], + dbName: "test", + type: "postgresql", + }) + + const manager = orm.em.fork() + + const product = manager.create(ProductModel, {}) + const productCategory = manager.create(ProductCategoryEntity, {}) + const productOptionValue = manager.create(ProductOptionValue, {}) + + expect(product.id).toMatch(/pro_[0-9]/) + expect(productCategory.id).toMatch(/prc_[0-9]/) + expect(productOptionValue.id).toMatch(/pov_[0-9]/) + + await orm.close() + }) + + it("should handle the id generation even with custom onInit or beforeCreate", async () => { + @Entity() + class ProductModel extends BaseEntity { + @Property() + custom_prop: string + + @OnInit() + onInit() { + this.custom_prop = "custom" + } + } + + @Entity() + class ProductCategoryEntity extends BaseEntity { + @Property() + custom_prop: string + + @OnInit() + onInit() { + this.custom_prop = "custom" + } + } + + @Entity() + class ProductOptionValue extends BaseEntity { + @Property() + custom_prop: string + + @OnInit() + onInit() { + this.custom_prop = "custom" + } + } + + const orm = await MikroORM.init({ + entities: [ProductModel, ProductCategoryEntity, ProductOptionValue], + dbName: "test", + type: "postgresql", + }) + + const manager = orm.em.fork() + + const product = manager.create(ProductModel, {}) + const productCategory = manager.create(ProductCategoryEntity, {}) + const productOptionValue = manager.create(ProductOptionValue, {}) + + expect(product.id).toMatch(/pro_[0-9]/) + expect(productCategory.id).toMatch(/prc_[0-9]/) + expect(productOptionValue.id).toMatch(/pov_[0-9]/) + + expect(product.custom_prop).toBe("custom") + expect(productCategory.custom_prop).toBe("custom") + expect(productOptionValue.custom_prop).toBe("custom") + + await orm.close() + }) +}) diff --git a/packages/core/utils/src/dal/mikro-orm/base-entity.ts b/packages/core/utils/src/dal/mikro-orm/base-entity.ts new file mode 100644 index 0000000000000..89ecdfe442db9 --- /dev/null +++ b/packages/core/utils/src/dal/mikro-orm/base-entity.ts @@ -0,0 +1,68 @@ +import { + BeforeCreate, + Entity, + OnInit, + OptionalProps, + PrimaryKey, +} from "@mikro-orm/core" +import { generateEntityId } from "../../common" + +@Entity({ abstract: true }) +export class BaseEntity { + [OptionalProps]?: BaseEntity["id"] | BaseEntity["__prefix_id__"] + + private __prefix_id__?: string + + constructor({ prefix_id }: { prefix_id?: string } = {}) { + this.__prefix_id__ = prefix_id + } + + @PrimaryKey({ columnType: "text" }) + id!: string + + @OnInit() + @BeforeCreate() + onInitOrBeforeCreate_() { + this.id ??= this.generateEntityId(this.__prefix_id__) + } + + private generateEntityId(prefixId?: string): string { + if (prefixId) { + return generateEntityId(undefined, prefixId) + } + + let ensuredPrefixId = Object.getPrototypeOf(this).constructor.name as string + + /* + * Split the class name (camel case) into words and exclude model and entity from the words + */ + const words = ensuredPrefixId + .split(/(?=[A-Z])/) + .filter((word) => !["entity", "model"].includes(word.toLowerCase())) + const wordsLength = words.length + + /* + * if the class name (camel case) contains one word, the prefix id is the first three letters of the word + * if the class name (camel case) contains two words, the prefix id is the first two letters of the first word plus the first letter of the second one + * if the class name (camel case) contains more than two words, the prefix id is the first letter of each word + */ + if (wordsLength === 1) { + ensuredPrefixId = words[0].substring(0, 3) + } else if (wordsLength === 2) { + ensuredPrefixId = words + .map((word, index) => { + return word.substring(0, 2 - index) + }) + .join("") + } else { + ensuredPrefixId = words + .map((word) => { + return word[0] + }) + .join("") + } + + this.__prefix_id__ = ensuredPrefixId.toLowerCase() + return generateEntityId(undefined, this.__prefix_id__) + } +} diff --git a/packages/core/utils/src/dal/repositories/index.ts b/packages/core/utils/src/dal/repositories/index.ts deleted file mode 100644 index 50f5b9b41de68..0000000000000 --- a/packages/core/utils/src/dal/repositories/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./load-custom-repositories" diff --git a/packages/core/utils/src/dal/repositories/load-custom-repositories.ts b/packages/core/utils/src/dal/repositories/load-custom-repositories.ts deleted file mode 100644 index 7a14507c6fc78..0000000000000 --- a/packages/core/utils/src/dal/repositories/load-custom-repositories.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Constructor, DAL } from "@medusajs/types" -import { asClass } from "awilix" -import { lowerCaseFirst } from "../../common" - -/** - * Load the repositories from the custom repositories object. If a repository is not - * present in the custom repositories object, the default repository will be used. - * - * @param customRepositories - * @param container - */ -export function loadCustomRepositories({ - defaultRepositories, - customRepositories, - container, -}) { - const customRepositoriesMap = new Map(Object.entries(customRepositories)) - - Object.entries(defaultRepositories).forEach(([key, defaultRepository]) => { - let finalRepository = customRepositoriesMap.get(key) - - if ( - !finalRepository || - !(finalRepository as Constructor).prototype.find - ) { - finalRepository = defaultRepository - } - - container.register({ - [lowerCaseFirst(key)]: asClass( - finalRepository as Constructor - ).singleton(), - }) - }) -}