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(), - }) - }) -}