Skip to content

Custom type not working inside array of embeddables #6676

@tobiaswiesner

Description

@tobiaswiesner

Describe the bug

I try to save a runtime variable of type string | number | Date | undefined as single entity property by using the following custom type to transform it to JSON { value: 'example123', type: 'string'} and back:

export class ValueType extends Type<string | number | Date | undefined, string> {
  convertToDatabaseValue(value: string | number | Date | undefined): string {
    if (value === undefined || value === null) {
      return JSON.stringify({ value: null, type: 'null' });
    }
    if (typeof value === 'string') {
      return JSON.stringify({ value, type: 'string' });
    }
    if (typeof value === 'number') {
      return JSON.stringify({ value, type: 'number' });
    }
    if (value instanceof Date) {
      return JSON.stringify({ value: value.toISOString(), type: 'date' });
    }
    throw ValidationError.invalidType(ValueType, value, 'JS');
  }

  convertToJSValue(
    value: string | { value: any; type: string } | null | undefined,
    plattform: Platform,
  ): string | number | Date | undefined {
    if (value === undefined || value === null) {
      return undefined;
    }
    if (typeof value === 'object' && 'type' in value && 'value' in value) {
      return this._fromJson(value);
    }
    if (typeof value === 'string') {
      try {
        const json = JSON.parse(value);
        return this._fromJson(json);
      } catch (e) {
        return value;
      }
    }
    throw ValidationError.invalidType(ValueType, value, 'database');
  }

  private _fromJson(json: { value: any; type: string }): string | number | Date | undefined {
    if (json.type === 'string') return json.value;
    if (json.type === 'number') return json.value;
    if (json.type === 'date') return json.value ? new Date(json.value) : undefined;
    if (json.type === 'null') return undefined;
    throw ValidationError.invalidType(ValueType, json, 'database');
  }

  getColumnType(prop: EntityProperty, platform: Platform) {
    return platform.getJsonDeclarationSQL?.() || 'jsonb';
  }
}

Motivation behind is to have a single searchable property/column instead of one column per data type or string column + helper column for type information (even if the later would be acceptable if the parsing could be done automatically but currently it seems to be not possible?).

Issues:

  • It works well when the custom type output (jsonb) is saved as dedicated column (entity property or embeddable with option { object: false }
  • It works partially when the custom type is part of an object-based embeddable (see reproduction example OutputRuleCalculation). The custom type json gets saved as part of the embeddable json and gets also parsed back via custom type (I guess thanks to Custom type not working inside Embeddable #1191). But querying by this field does not show any results (feat(core): refactor merging to allow querying by custom type #800 covered only column-based embeddable fields?).
  • It works not at all when the embeddable is embedded as array ({ array: true }, see reproduction example OutputRuleCondition). In this case the provided runtime value will get saved as is without any custom type magic parsing it. In addition, querying by this field doesn't work either.

Reproduction

  1. Use my provided custom type from above
  2. Use my 2 embeddables and entity:
@Embeddable()export class OutputRuleCondition {
  @Property({ type: 'text', name: 'operator' })
  public operator!: string;

  @Property({
    type: ValueType,
    nullable: true,
  })
  public comparisonValue?: string | number | Date;
}


@Embeddable()
export class OutputRuleCalculation {
  @Property({ type: 'text' })
  public calculationType!: string;

  @Property({ type: ValueType, nullable: true })
  public constant?: string | number | Date;
}


@Entity({ tableName: 'example' })
export class OutputRule {
  @PrimaryKey({ type: 'uuid' })
  public id!: string;

  @Property({ type: 'boolean' })
  public isDefault!: boolean;

  @Embedded(() => OutputRuleCalculation, { object: true })
  public calculation!: OutputRuleCalculation;

  @Embedded(() => OutputRuleCondition, { array: true })
  public conditions: OutputRuleCondition[] = [];
}
  1. Save some values in DB:
const outputRule = new OutputRule();
outputRule.id = 'b2e1e6b2-1234-4c1a-9c2a-abcdefabcdef';
outputRule.isDefault = true;
outputRule.calculation = new OutputRuleCalculation();
outputRule.calculation.calculationType = 'constant';
outputRule.calculation.constant = 42; // could also be a string, Date or undefined
outputRule.conditions = [
  Object.assign(new OutputRuleCondition(), {
    operator: 'eq',
    comparisonValue: 'foo',
  }),
  Object.assign(new OutputRuleCondition(), {
    operator: 'gt',
    comparisonValue: new Date('2024-01-01T00:00:00Z'),
  }),
];
await em.persistAndFlush(outputRule);
  1. Observe DB result:
    {"constant": "{\"value\":\42,\"type\":\"number\"}", "calculation_type": "constant"}
    [{"operator": "eq", "comparison_value": "foo"},{"operator": "gt", "comparison_value": "2024-01-01T00:00:00Z'"}] -> comparisonValue should be wrapped in custom type json instead plain
  2. Get it from DB without query filter: constant gets parsed properly, conditions.comparisonValue not (as it is already saved wrong)
  3. Get it from DB with query filter:
    {"$and":[{"calculation":{"constant":{"$eq":"42"}}}]} -> Should match but it doesn't

What driver are you using?

@mikro-orm/postgresql

MikroORM version

6.4.13

Node.js version

24.0.1

Operating system

Debian

Validations

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions