Repository: mikro-orm/mikro-orm (TypeScript)

Issue:
Add support for Embedded column name
**Is your feature request related to a problem? Please describe.**
Currently it is possible to define the name of a column to be different from the name of the ts/js field
```ts
  @Property({ name: 'n' })
  name!: string;
```

It is also possible to define an embedded object
```ts
  @Embedded(() => Profile, { object: true, nullable: true })
  profile?: Profile;
```

but it doesn't seem to be possible to combine both

**Describe the solution you'd like**
Would be nice to be able to do something like
```ts
  @Embedded(() => Profile, { object: true, nullable: true, name: 'p' })
  profile?: Profile;
```

**Describe alternatives you've considered**
The only alternative is to have the attribute name match the db name.

**Additional context**
This might be more of a mongoDB use case where nested objects are fairly common and it's not unusual to shorten the db field name for storage optimization, but still want the application name to be verbose to keep the code understandable.

But Im sure it can also have some SQL use cases...

FieldName not respected when storing embeddables as objects
### Describe the bug

If we store an embeddable as an object, the corresponding `fieldName` is not respected for the name of the column holding the JSON object.

Please find a test below exemplifying the issue.

```ts
import {
    EntitySchema,
    IntegerType,
    MikroORM,
    ReferenceKind,
    StringType,
} from "@mikro-orm/core";
import { test } from "vitest";
import { PostgreSqlDriver } from "@mikro-orm/postgresql";
import { v4 as uuidv4 } from "uuid";

/**
 * Domain model.
 */
class Address {
    _street: string;
    _postalCode: string;
    _city: string;
    _country: string;

    constructor(
        street: string,
        postalCode: string,
        city: string,
        country: string,
    ) {
        this._street = street;
        this._postalCode = postalCode;
        this._city = city;
        this._country = country;
    }
}

export class User {
    _id: number;
    _fullName: string;
    _address: Address;

    constructor(fullName: string, address: Address) {
        this._fullName = fullName;
        this._address = address;
    }
}

/**
 * Mikro-orm entity schemas.
 */
const addressEntitySchema = new EntitySchema({
    class: Address,
    embeddable: true,
    properties: {
        _street: {
            fieldName: "street",
            type: new StringType(),
        },
        _postalCode: {
            fieldName: "postalCode",
            type: new StringType(),
        },
        _city: {
            fieldName: "city",
            type: new StringType(),
        },
        _country: {
            fieldName: "country",
            type: new StringType(),
        },
    },
});

export const userEntitySchema = new EntitySchema({
    class: User,
    properties: {
        _id: {
            fieldName: "id",
            primary: true,
            type: new IntegerType(),
        },
        _fullName: {
            fieldName: "fullName",
            type: new StringType(),
        },
        _address: {
            fieldName: "address",
            kind: ReferenceKind.EMBEDDED,
            object: true,
            entity: () => Address,
        },
    },
    schema: "public",
    tableName: "User",
});

const dbName: string = `db-${uuidv4()}`;
let orm: MikroORM;

test("Should create the right db schema", async () => {
    orm = await MikroORM.init({
        entities: [addressEntitySchema, userEntitySchema],
        host: process.env.POSTGRES_HOST,
        port: Number(process.env.POSTGRES_PORT),
        user: process.env.POSTGRES_USER_SUPERUSER,
        password: process.env.POSTGRES_USER_SUPERUSER_PASSWORD,
        dbName: dbName,
        debug: /^true$/i.test(process.env.DEBUG || ""),
        driver: PostgreSqlDriver,
        allowGlobalContext: true,
    });

    // Creating fixed schemas in the database
    await orm.schema.createSchema();

    /**
     * The following User table is created:
     * 
     * create table "User" (
     *   "id" serial primary key, 
     *   "fullName" varchar(255) not null, 
     *   "_address" jsonb not null
     * );
     * 
     * According to what we specified above in the entity 
     * schemas, it should be:
     * 
     * create table "User" (
     *   "id" serial primary key, 
     *   "fullName" varchar(255) not null, 
     *   "address" jsonb not null
     * );
     */

    // Cleaning up orm resources (this closes connections)
    await orm.close();

    // Dropping the database
    await orm.schema.dropDatabase();
});

```

### Reproduction

See provided reproduction above.

### What driver are you using?

@mikro-orm/postgresql

### MikroORM version

6.4.6

### Node.js version

18+

### Operating system

MacOS

### Validations

- [x] Read the [Contributing Guidelines](https://github.com/mikro-orm/mikro-orm/blob/master/CONTRIBUTING.md).
- [x] Read the [docs](https://mikro-orm.io/docs).
- [x] Check that there isn't [already an issue](https://github.com/mikro-orm/mikro-orm/issues) that reports the same bug to avoid creating a duplicate.
- [x] Check that this is a concrete bug. For Q&A open a [GitHub Discussion](https://github.com/mikro-orm/mikro-orm/discussions) or join our [Discord](https://discord.gg/w8bjxFHS7X).
- [x] The provided reproduction is a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) of the bug.


Discussion: Hmm I would say this should be already working.
Maybe I'm not looking at the right place but this is the definition for `EmbeddedOptions`:
```ts
export type EmbeddedOptions = {
    entity?: string | (() => AnyEntity | AnyEntity[]);
    type?: string;
    prefix?: string | boolean;
    nullable?: boolean;
    object?: boolean;
    array?: boolean;
    hidden?: boolean;
    serializer?: (value: any) => any;
    serializedName?: string;
};
```
in `node_modules/@mikro-orm/core/decorators/Embedded.d.ts` (I'm using `"@mikro-orm/core": "^6.1.11"`).

It doesn't seem to have a `name` property available.
Ok, but that might be just a type-level issue, have you tried to put it in (and cast the options `as any`)? 
I just gave it a try.

On insert, it skips the field, meaning the document created in mongoDB doesn't have the data, neither with the class attribute name nor the decorator config name.

On select, it throws an error `TypeError: Cannot read properties of undefined (reading 'object')` (at `node_modules/@mikro-orm/core/utils/EntityComparator.js:266:88`).
@maphe I got the same error as well but with `node_modules/@mikro-orm/core/metadata/MetadataValidator.js:17`
Been awhile but as @B4nan says if you just typecast the options to `any` it seems to work now at least in v6.4? 

I'm using Postgres and a few different variations of embeddables.
> Been awhile but as [@B4nan](https://github.com/B4nan) says if you just typecast the options to `any` it seems to work now at least in v6.4?
> 
> I'm using Postgres and a few different variations of embeddables.

Hi
I've just tested this case, unfortunately it doesn't work. When persisting an entity with a custom `fieldName` specified in the options `@Embedded()`  of embedded, the `fieldName` is not taken into account


<img width="577" alt="Image" src="https://github.com/user-attachments/assets/9090a2df-ba16-43d2-867f-9dc52bf79e1d" />


<img width="562" alt="Image" src="https://github.com/user-attachments/assets/a918ed78-c3f9-411d-9b57-e2964f6fb31e" />

I tried different combinations of `object: true/false`, `fieldName`, `name`, `serializedName`

Please provide a [complete reproduction](https://github.com/mikro-orm/reproduction), something I can run myself to see the problem. Some say it works, some say the opposite, clearly you are testing in a different way. Screenshots won't help.

> fieldName, name, serializedName

`fieldName` and `name` are aliases. `serializedName` has nothing to do with this.

Same behaviour occurs if we use an array of embeddables.
Thanks for the repro! I'll close this as a duplicate of #5361, finally a usable reproduction, so it can hopefully be resolved soon.
Had a quick look, at the problem seems to be in normalization of `prop.fieldName` to `prop.fieldNames`. If you specify the latter, it works correctly:

```ts
    _address: {
      fieldNames: ['address'],
      kind: ReferenceKind.EMBEDDED,
      object: true,
      entity: () => Address,
    },
```
@B4nan Thanks for the feedback and the suggested workaround. Just tested on my side, and it indeed works correctly using `fieldNames` instead of `fieldName`.



Fix (patch):
diff --git a/packages/core/src/decorators/Embedded.ts b/packages/core/src/decorators/Embedded.ts
index f78f7489acea..963c42ba5fa8 100644
--- a/packages/core/src/decorators/Embedded.ts
+++ b/packages/core/src/decorators/Embedded.ts
@@ -1,15 +1,16 @@
-import type { AnyEntity, EntityKey, EntityProperty } from '../typings';
+import type { AnyEntity, EntityKey, EntityName, EntityProperty } from '../typings';
 import { MetadataStorage, MetadataValidator } from '../metadata';
 import { Utils } from '../utils';
 import { ReferenceKind } from '../enums';
+import type { PropertyOptions } from './Property';
 
-export function Embedded<T extends object>(type: EmbeddedOptions | (() => AnyEntity) = {}, options: EmbeddedOptions = {}) {
+export function Embedded<Owner extends object, Target>(type: EmbeddedOptions<Owner, Target> | (() => EntityName<Target> | EntityName<Target>[]) = {}, options: EmbeddedOptions<Owner, Target> = {}) {
   return function (target: AnyEntity, propertyName: string) {
-    const meta = MetadataStorage.getMetadataFromDecorator(target.constructor as T);
+    const meta = MetadataStorage.getMetadataFromDecorator(target.constructor as Owner);
     MetadataValidator.validateSingleDecorator(meta, propertyName, ReferenceKind.EMBEDDED);
     options = type instanceof Function ? { entity: type, ...options } : { ...type, ...options };
     Utils.defaultValue(options, 'prefix', true);
-    meta.properties[propertyName as EntityKey<T>] = {
+    meta.properties[propertyName as EntityKey<Owner>] = {
       name: propertyName,
       kind: ReferenceKind.EMBEDDED,
       ...options,
@@ -22,17 +23,10 @@ export function Embedded<T extends object>(type: EmbeddedOptions | (() => AnyEnt
 /** With `absolute` the prefix is set at the root of the entity (regardless of the nesting level) */
 export type EmbeddedPrefixMode = 'absolute' | 'relative';
 
-export type EmbeddedOptions = {
-  entity?: string | (() => AnyEntity | AnyEntity[]);
-  type?: string;
+export interface EmbeddedOptions<Owner, Target> extends PropertyOptions<Owner> {
+  entity?: string | (() => EntityName<Target> | EntityName<Target>[]);
   prefix?: string | boolean;
   prefixMode?: EmbeddedPrefixMode;
-  nullable?: boolean;
   object?: boolean;
   array?: boolean;
-  hidden?: boolean;
-  serializer?: (value: any) => any;
-  serializedName?: string;
-  groups?: string[];
-  persist?: boolean;
-};
+}
diff --git a/packages/core/src/metadata/EntitySchema.ts b/packages/core/src/metadata/EntitySchema.ts
index af0902acecac..199a51a38745 100644
--- a/packages/core/src/metadata/EntitySchema.ts
+++ b/packages/core/src/metadata/EntitySchema.ts
@@ -40,7 +40,7 @@ export type EntitySchemaProperty<Target, Owner> =
   | ({ kind: ReferenceKind.ONE_TO_ONE | '1:1' } & TypeDef<Target> & OneToOneOptions<Owner, Target>)
   | ({ kind: ReferenceKind.ONE_TO_MANY | '1:m' } & TypeDef<Target> & OneToManyOptions<Owner, Target>)
   | ({ kind: ReferenceKind.MANY_TO_MANY | 'm:n' } & TypeDef<Target> & ManyToManyOptions<Owner, Target>)
-  | ({ kind: ReferenceKind.EMBEDDED | 'embedded' } & EmbeddedTypeDef<Target> & EmbeddedOptions & PropertyOptions<Owner>)
+  | ({ kind: ReferenceKind.EMBEDDED | 'embedded' } & EmbeddedTypeDef<Target> & EmbeddedOptions<Owner, Target> & PropertyOptions<Owner>)
   | ({ enum: true } & EnumOptions<Owner>)
   | (TypeDef<Target> & PropertyOptions<Owner>);
 type OmitBaseProps<Entity, Base> = IsNever<Base> extends true ? Entity : Omit<Entity, keyof Base>;
@@ -90,26 +90,7 @@ export class EntitySchema<Entity = any, Base = never> {
   }
 
   addProperty(name: EntityKey<Entity>, type?: TypeType, options: PropertyOptions<Entity> | EntityProperty<Entity> = {}): void {
-    const rename = <U> (data: U, from: string, to: string): void => {
-      if (from in options && !(to in options)) {
-        // @ts-ignore
-        options[to] = [options[from]];
-        // @ts-ignore
-        delete options[from];
-      }
-    };
-
-    if (name !== options.name) {
-      Utils.renameKey(options, 'name', 'fieldName');
-    }
-
-    rename(options, 'fieldName', 'fieldNames');
-    rename(options, 'ref', 'ref');
-    rename(options, 'joinColumn', 'joinColumns');
-    rename(options, 'inverseJoinColumn', 'inverseJoinColumns');
-    rename(options, 'referenceColumnName', 'referencedColumnNames');
-    rename(options, 'columnType', 'columnTypes');
-
+    this.renameCompositeOptions(name, options);
     const prop = { name, kind: ReferenceKind.SCALAR, ...options, type: this.normalizeType(options, type) } as EntityProperty<Entity>;
 
     if (type && Type.isMappedType((type as Constructor).prototype)) {
@@ -166,7 +147,8 @@ export class EntitySchema<Entity = any, Base = never> {
     this.addProperty(name, type, options);
   }
 
-  addEmbedded<Target = AnyEntity>(name: EntityKey<Entity>, options: EmbeddedOptions): void {
+  addEmbedded<Target = AnyEntity>(name: EntityKey<Entity>, options: EmbeddedOptions<Entity, Target>): void {
+    this.renameCompositeOptions(name, options);
     Utils.defaultValue(options, 'prefix', true);
 
     if (options.array) {
@@ -336,7 +318,7 @@ export class EntitySchema<Entity = any, Base = never> {
           this.addManyToMany<any>(name, options.type, options);
           break;
         case ReferenceKind.EMBEDDED:
-          this.addEmbedded(name, options as EmbeddedOptions);
+          this.addEmbedded(name, options as EmbeddedOptions<any, any>);
           break;
         default:
           if (options.enum) {
@@ -374,7 +356,7 @@ export class EntitySchema<Entity = any, Base = never> {
     }
   }
 
-  private normalizeType(options: PropertyOptions<Entity> | EntityProperty, type?: string | any | Constructor<Type>) {
+  private normalizeType(options: PropertyOptions<Entity> | EntityProperty | EmbeddedOptions<Entity, any>, type?: string | any | Constructor<Type>) {
     if ('entity' in options) {
       if (Utils.isString(options.entity)) {
         type = options.type = options.entity;
@@ -403,4 +385,27 @@ export class EntitySchema<Entity = any, Base = never> {
     } as EntityProperty<T>;
   }
 
+  private rename<U extends object>(data: U, from: string, to: string): void {
+    if (from in data && !(to in data)) {
+      // @ts-ignore
+      data[to] = [data[from]];
+      // @ts-ignore
+      delete data[from];
+    }
+  }
+
+  private renameCompositeOptions(name: EntityKey<Entity>, options: PropertyOptions<Entity> | EntityProperty<Entity> | EmbeddedOptions<Entity, any> = {}): void {
+    if (name !== options.name && !options.fieldNames) {
+      Utils.renameKey(options, 'name', 'fieldName');
+    } else if (options.name && (options.fieldNames?.length ?? 0) > 1) {
+      delete options.name;
+    }
+
+    this.rename(options, 'fieldName', 'fieldNames');
+    this.rename(options, 'joinColumn', 'joinColumns');
+    this.rename(options, 'inverseJoinColumn', 'inverseJoinColumns');
+    this.rename(options, 'referenceColumnName', 'referencedColumnNames');
+    this.rename(options, 'columnType', 'columnTypes');
+  }
+
 }