Mikro-orm: Maximum call stack size exceeded: on EntityHelper

Created on 19 Nov 2020  ·  4Comments  ·  Source: mikro-orm/mikro-orm

Hello I have an n: n relationship between two tables:

Component:

@Entity({ tableName: 'components' })
export class ComponentInstance {
  @PrimaryKey()
  public readonly id: string;
  @Unique({ name: 'component' })
  @Property()
  public readonly serial_number: string;
  @ManyToOne(() => Product, { fieldName: 'product_id' })
  public product!: Product;
  @ManyToOne(() => ProductStocks, { fieldName: 'stock_id' })
  public stock!: ProductStocks;
  @OneToOne({
    entity: () => EquipmentInstance,
    mappedBy: 'component',
    strategy: LoadStrategy.JOINED,
  })
  public equipment: EquipmentInstance;
  @OneToMany(() => EquipmentHasComponents, comp => comp.component, {
    strategy: LoadStrategy.JOINED,
  })
  public equipments = new Collection<EquipmentHasComponents>(this);
  @Property({ persist: false })
  public departament?: Departament;
  @Property()
  public created_at = new Date();
  @Property({ onUpdate: () => new Date() })
  public updated_at = new Date();
  @Property()
  public deleted_at?: Date;

  constructor(container: instanceContainer) {
    this.id = container.id ? container.id : v4();
    this.serial_number = container.serial_number;
    this.product = container.product;
    this.stock = container.stock;
    this.departament = container.departament;
  }
}

Equipment:

@Entity({ tableName: 'equipments' })
export class EquipmentInstance {
  @PrimaryKey()
  public patrimony_code: string;
  @ManyToOne({ entity: () => Employee, fieldName: 'employee_id' })
  public employee!: Employee;
  @Unique()
  @OneToOne(() => ComponentInstance, component => component.equipment, {
    owner: true,
    orphanRemoval: true,
    cascade: [],
    fieldName: 'component_id',
  })
  public component: ComponentInstance;
  @OneToMany(() => EquipmentHasComponents, equips => equips.equipment)
  public components = new Collection<EquipmentHasComponents>(this);
  @Property()
  public createdAt = new Date();
  @Property({ onUpdate: () => new Date() })
  public updatedAt = new Date();
  @Property()
  public deletedAt?: Date;
}

Equipments_has_components:

@Entity({ tableName: 'equipment_has_components' })
export class EquipmentHasComponents {
  @ManyToOne({
    entity: () => EquipmentInstance,
    fieldName: 'equipment_id',
    primary: true,
  })
  public equipment!: EquipmentInstance;
  @ManyToOne({
    entity: () => ComponentInstance,
    fieldName: 'component_id',
    primary: true,
  })
  public component!: ComponentInstance;
  [PrimaryKeyType]: [EquipmentInstance, ComponentInstance];
  @Property()
  public createdAt = new Date();
  @Property({ onUpdate: () => new Date() })
  public updatedAt = new Date();
  @Property()
  public deletedAt?: Date;
  constructor(equipment: EquipmentInstance, component: ComponentInstance) {
    this.equipment = equipment;
    this.component = component;
  }
}

and to popularize the n: n relationship I do the following:

    const equipDomain = EquipmentInstance.build({
      component: componentOwner,
      employee,
      patrimony_code,
    });
    if (hasAllComponents && hasAllComponents.length > 0) {
      equipDomain.components.set(
        hasAllComponents.map(component => {
          return EquipmentHasComponents.build(equipDomain, component);
        }),
      );
    }

and this to persist:

  public create = async (
    instance: EquipmentInstance,
  ): Promise<EquipmentInstance> => {
    if (!(instance instanceof EquipmentInstance))
      throw new Error(`Invalid Data Type`);
    await this.em.persist(instance).flush();
    return instance;
  };

sql queries:

[query] begin
[query] insert into "equipments" ("component_id", "created_at", "employee_id", "patrimony_code", "updated_at") values ('c20570f1-d1c3-4a4a-af92-60fd2a283462', '2020-11-19T14:40:59.495Z', 'c4bb1652-f457-4ea2-b5ae-940344779277', 'xa', '2020-11-19T14:40:59.495Z') returning "patrimony_code" [took 
2 ms]
[query] insert into "equipment_has_components" ("equipment_id", "component_id", "created_at", "updated_at") values ('xa', '7137c284-e22f-4cf3-8aa7-1b1394df4f81', '2020-11-19T14:40:59.495Z', '2020-11-19T14:40:59.495Z'), ('xa', 'aa40a86e-c882-425b-b0aa-4432f32275fc', '2020-11-19T14:40:59.496Z', 
'2020-11-19T14:40:59.496Z'), ('xa', 'b31aefd1-0fbc-4ed0-a4ba-17f0ba05174e', '2020-11-19T14:40:59.496Z', '2020-11-19T14:40:59.496Z') returning "equipment_id", "component_id" [took 2 ms]
[query] commit

It works normally, persist without problems, but when returning as json, I'm getting this error in entityHelper:

RangeError: Maximum call stack size exceeded
    at Function.processProperty (C:\Users\gabrielti\Documents\Emasa-Management\node_modules\@mikro-orm\core\entity\EntityTransformer.js:155:42)
    at C:\Users\gabrielti\Documents\Emasa-Management\node_modules\@mikro-orm\core\entity\EntityTransformer.js:108:43
    at Array.map (<anonymous>)
    at Function.toObject (C:\Users\gabrielti\Documents\Emasa-Management\node_modules\@mikro-orm\core\entity\EntityTransformer.js:103:14)
    at EquipmentInstance.prototype.toJSON (C:\Users\gabrielti\Documents\Emasa-Management\node_modules\@mikro-orm\core\entity\EntityHelper.js:27:62)
    at JSON.stringify (<anonymous>)
    at stringify (C:\Users\gabrielti\Documents\Emasa-Management\node_modules\express\lib\response.js:1123:12)
    at ServerResponse.json (C:\Users\gabrielti\Documents\Emasa-Management\node_modules\express\lib\response.js:260:14)
    at EquipmentsController.assignEquipment (C:\Users\gabrielti\Documents\Emasa-Management\src\modules\equipments\ui\equipment.controller.ts:31:23)
    at processTicksAndRejections (node:internal/process/task_queues:93:5)
RangeError: Maximum call stack size exceeded
    at Function.processProperty (C:\Users\gabrielti\Documents\Emasa-Management\node_modules\@mikro-orm\core\entity\EntityTransformer.js:155:42)
    at C:\Users\gabrielti\Documents\Emasa-Management\node_modules\@mikro-orm\core\entity\EntityTransformer.js:108:43
    at Array.map (<anonymous>)
    at Function.toObject (C:\Users\gabrielti\Documents\Emasa-Management\node_modules\@mikro-orm\core\entity\EntityTransformer.js:103:14)
    at EquipmentInstance.prototype.toJSON (C:\Users\gabrielti\Documents\Emasa-Management\node_modules\@mikro-orm\core\entity\EntityHelper.js:27:62)
    at JSON.stringify (<anonymous>)
    at stringify (C:\Users\gabrielti\Documents\Emasa-Management\node_modules\express\lib\response.js:1123:12)
    at ServerResponse.json (C:\Users\gabrielti\Documents\Emasa-Management\node_modules\express\lib\response.js:260:14)
    at EquipmentsController.assignEquipment (C:\Users\gabrielti\Documents\Emasa-Management\src\modules\equipments\ui\equipment.controller.ts:31:23)
    at processTicksAndRejections (node:internal/process/task_queues:93:5)

populated entity structure:

EquipmentInstance {
  components: Collection {
    '0': EquipmentHasComponents {
      createdAt: 2020-11-19T14:42:13.817Z,
      updatedAt: 2020-11-19T14:42:13.817Z,
      equipment: [EquipmentInstance],
      component: [ComponentInstance]
    },
    '1': EquipmentHasComponents {
      createdAt: 2020-11-19T14:42:13.817Z,
      updatedAt: 2020-11-19T14:42:13.817Z,
      equipment: [EquipmentInstance],
      component: [ComponentInstance]
    },
    '2': EquipmentHasComponents {
      createdAt: 2020-11-19T14:42:13.817Z,
      updatedAt: 2020-11-19T14:42:13.817Z,
      equipment: [EquipmentInstance],
      component: [ComponentInstance]
    },
    initialized: true,
    dirty: false
  },
  createdAt: 2020-11-19T14:42:13.817Z,
  updatedAt: 2020-11-19T14:42:13.817Z,
  patrimony_code: 'xa',
  employee: Employee {
    id: 'c4bb1652-f457-4ea2-b5ae-940344779277',
    matricula: '24',
    first_name: 'a',
    last_name: 'b',
    position: 'tecnico',
    departament: Ref<Departament> { id: '43476c5c-dbcc-41e4-975b-599725c1b28e' },
    createdAt: 2020-11-19T12:13:42.452Z,
    updatedAt: 2020-11-19T12:13:42.452Z,
    deletedAt: null
  },
  component: ComponentInstance {
    id: 'c20570f1-d1c3-4a4a-af92-60fd2a283462',
    serial_number: 'x4',
    product: Ref<Product> { id: '31ec4f27-6753-42ba-a4d0-728ca0aa52f2' },
    stock: Ref<ProductStocks> { id: 'ec15e7e6-c921-4088-b055-54554ecc49c0' },
    equipment: EquipmentInstance {
      components: [Collection],
      createdAt: 2020-11-19T14:42:13.817Z,
      updatedAt: 2020-11-19T14:42:13.817Z,
      patrimony_code: 'xa',
      employee: [Employee],
      component: [ComponentInstance]
    },
    equipments: Collection { initialized: true, dirty: false },
    created_at: 2020-11-19T12:16:11.050Z,
    updated_at: 2020-11-19T12:16:11.050Z,
    deleted_at: null
  }
}

Most helpful comment

There were no changes in serialization itself, the "issue" as I already noted is simply caused by your entity graph, and the solution is to use custom toJSON or property serializer. There will never be perfect out of box serialization that will just work as _everyone_ expects, there is no AI or an umpalumpa behind the scenes :]

What we can do for sure to mitigate this is to introduce max depth limit for serialization, so at least it will be easy to see what gets locked in the cycled (we could even warn when the limit is reached). Using something like 20 could be enough for 99% usecases.

All 4 comments

You have a cycle in there, either use custom toJSON or property serializer to cut it.

https://mikro-orm.io/docs/serializing/

I wouldn't be surprised if the issue here is your weird mapping, that snippet with equipDomain.components.set() is looking suspicious. Loading of entities should be handled by entity manager and the populate parameter, that way cycles during serialization are handled automatically.

If you want me to help with this, you need to provide executable reproduction. The repro needs to be minimal, no express or other unrelated stuff should be there, just a script. Also drop everything unrelated from your entities - you keep posting the full stuff here and it makes it much harder to read. There are many unrelated properties in there.

Você tem um ciclo aí, use o toJSONserializador personalizado ou de propriedade para cortá-lo.

https://mikro-orm.io/docs/serializing/

Eu não ficaria surpreso se o problema aqui fosse o seu mapeamento estranho, aquele trecho com equipDomain.components.set()parece suspeito. O carregamento de entidades deve ser feito pelo gerenciador de entidades e pelo populateparâmetro, de forma que os ciclos durante a serialização sejam feitos automaticamente.

Se você quiser que eu ajude com isso, você precisa fornecer uma reprodução executável. A reprodução deve ser mínima, nenhum material expresso ou não relacionado deve estar lá, apenas um script. Elimine também tudo o que não estiver relacionado com suas entidades - você continua postando o material completo aqui e fica muito mais difícil de ler. Existem muitas propriedades não relacionadas lá.

I got to test if the problem was in my set and did not give an error, only when returning my json by express, he throws no error
I made a repository:
https://github.com/sptGabriel/mikroormERR/tree/main
very simple with all setup and reedme with endpoint and json data to getError
if u can tell me how i would do this
Loading of entities should be handled by entity manager and the populate parameter
I would be very grateful

I've run into similar issues with version 4.3.1 where I did not have this with version 4.2.3. I'm unsure if I can create a simple repro repo, since it happens in our app where we have quite a complicated structure of entities and relations.

There were no changes in serialization itself, the "issue" as I already noted is simply caused by your entity graph, and the solution is to use custom toJSON or property serializer. There will never be perfect out of box serialization that will just work as _everyone_ expects, there is no AI or an umpalumpa behind the scenes :]

What we can do for sure to mitigate this is to introduce max depth limit for serialization, so at least it will be easy to see what gets locked in the cycled (we could even warn when the limit is reached). Using something like 20 could be enough for 99% usecases.

Was this page helpful?
0 / 5 - 0 ratings