import { BaseEntity, Collection, type Dictionary, type EntityMetadata, MetadataStorage, MikroORM, type Opt, types, Utils, } from '@mikro-orm/core'; import { Entity, ManyToOne, OneToMany, PrimaryKey, Property, ReflectMetadataProvider, } from '@mikro-orm/decorators/legacy'; import { NodeSqliteDialect, SqliteDriver } from '@mikro-orm/sql'; import { type Constructor, mixin } from '@wener/utils'; import { test } from 'vitest'; // Custom getMetadataFromDecorator to support mixin pattern // The issue: when using mixins, multiple mixin classes can have the same name (e.g., HasStatusMixinEntity) // MikroORM uses class name + path as metadata key, causing conflicts // This override assigns unique names to mixin entities (ending with 'MixinEntity') let _GlobalEntityId = 0; const _originalGetMetadataFromDecorator = MetadataStorage.getMetadataFromDecorator.bind(MetadataStorage); const _PATH_SYMBOL = (MetadataStorage as unknown as { PATH_SYMBOL: symbol }).PATH_SYMBOL; const _mixinIdMap = new WeakMap(); MetadataStorage.getMetadataFromDecorator = (target: T & Dictionary): EntityMetadata => { const originalName = target.name as string; // Only rename mixin entities (abstract classes used in mixins) if (!originalName.endsWith('__') && originalName.endsWith('MixinEntity')) { let id = _mixinIdMap.get(target); if (id === undefined) { id = _GlobalEntityId++; _mixinIdMap.set(target, id); } const newName = `${originalName}__${id}__`; Object.defineProperty(target, 'name', { value: newName, writable: true, configurable: true }); } // Use original implementation for proper metadata handling // This ensures PATH_SYMBOL is set correctly and metadata is stored properly if (!Object.hasOwn(target, _PATH_SYMBOL)) { Object.defineProperty(target, _PATH_SYMBOL, { value: Utils.lookupPathFromDecorator(target.name), writable: true, }); } return MetadataStorage.getMetadata(target.name, (target as Record)[_PATH_SYMBOL]); }; test('mixins', async () => { const orm = await getOrm(); const em = orm.em.fork(); // wrong console.log(Object.keys(em.getMetadata(AccountEntity).properties)); console.log(Object.keys(Utils.getGlobalStorage('metadata'))); console.log(UserEntity.__path); console.log(AccountEntity.__path); MetadataStorage.getMetadata(); }); @Entity({ abstract: true }) abstract class MinimalBaseEntity extends BaseEntity { @PrimaryKey({ type: types.string, defaultRaw: 'public.gen_ulid()', nullable: false }) id!: string & Opt; } async function getOrm() { const orm = await MikroORM.init({ driver: SqliteDriver, entities: [UserEntity, AccountEntity], dbName: ':memory:', driverOptions: new NodeSqliteDialect(':memory:'), metadataProvider: ReflectMetadataProvider, debug: true, }); let em = orm.em.fork(); for (const schema of [ ` create table users ( id char(32) primary key default (lower(hex(randomblob(16)))), uid char(36) not null default ( lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' || substr(lower(hex(randomblob(2))), 2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(lower(hex(randomblob(2))), 2) || '-' || lower(hex(randomblob(6)))), eid text, created_at timestamp not null default current_timestamp, updated_at timestamp not null default current_timestamp, deleted_at timestamp, state text not null default 'Active', status text not null default 'Active', attributes json not null default '{}', properties json not null default '{}', extensions json not null default '{}' ); `, ` create table account ( id char(32) primary key default (lower(hex(randomblob(16)))), uid char(36) not null default ( lower(hex(randomblob(4))) || '-' || lower(hex(randomblob(2))) || '-4' || substr(lower(hex(randomblob(2))), 2) || '-' || substr('89ab', abs(random()) % 4 + 1, 1) || substr(lower(hex(randomblob(2))), 2) || '-' || lower(hex(randomblob(6)))), eid text, created_at timestamp not null default current_timestamp, updated_at timestamp not null default current_timestamp, deleted_at timestamp, state text not null default 'Active', status text not null default 'Active', attributes json not null default '{}', properties json not null default '{}', extensions json not null default '{}', user_id text ); `, ]) { await em.execute(schema); } return { orm, em }; } @Entity({ tableName: 'users' }) class UserEntity extends mixin(MinimalBaseEntity, createStateStatusEntity({ state: 'Active', status: 'Active' })) { @OneToMany( () => AccountEntity, (e) => e.user, ) accounts = new Collection(this); } @Entity({ tableName: 'account' }) class AccountEntity extends mixin( MinimalBaseEntity, withVendorRefEntity, createStateStatusEntity({ state: 'Active', status: 'Active' }), ) { @ManyToOne(() => UserEntity) user?: UserEntity; } function withVendorRefEntity(Base: TBase) { @Entity({ abstract: true }) class HasVendorRefMixinEntity extends Base { // vendor @Property({ type: types.string, nullable: true }) cid?: string; // vendor external id @Property({ type: types.string, nullable: true }) rid?: string; } console.log(`===`, HasVendorRefMixinEntity.name); // Object.defineProperty(HasVendorRefMixinEntity, 'name', { // value: `${HasVendorRefMixinEntity.name}_${ClassID++}`, // }); return HasVendorRefMixinEntity; } function createStateStatusEntity({ status, state }: { status: string; state: string }) { return function withStatusEntity(Base: TBase) { @Entity({ abstract: true }) class HasStatusMixinEntity extends Base { @Property({ type: types.string, nullable: false, default: state }) state!: string & Opt; @Property({ type: types.string, nullable: false, default: status }) status!: string & Opt; } // Object.defineProperty(HasStatusEntity, 'name', { // value: `${HasStatusEntity.name}_${_GlobalEntityId++}`, // }); // console.warn(`===`, HasStatusEntity.name); return HasStatusMixinEntity; }; }