When trying to save a child entity, TypeORM's lazy loading feature fails to update

I've been troubleshooting an issue and trying various methods to resolve it, but I haven't had any success. Hopefully, someone here can assist me.

Essentially, I have a one-to-one relationship that needs to be lazy-loaded. The relationship tree in my project is quite extensive, requiring promises for loading.

The problem arises when I save a child entity, as the generated parent update SQL is missing the necessary update fields: UPDATE `a` SET WHERE `id` = 1

This functionality works perfectly fine without using lazy-loading (Promises).

I've set up a simple example using the code generator tool.

Entity A

@Entity()
export class A {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @OneToOne(
        (type: any) => B,
        async (o: B) => await o.a
    )
    @JoinColumn()
    public b: Promise<B>;
}

Entity B

@Entity()
export class B {

    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    name: string;

    @OneToOne(
        (type: any) => A,
        async (o: A) => await o.b)
    a: Promise<A>;

}

main.ts

createConnection().then(async connection => {

    const aRepo = getRepository(A);
    const bRepo = getRepository(B);

    console.log("Inserting a new user into the database...");
    const a = new A();
    a.name = "something";
    const aCreated = aRepo.create(a);
    await aRepo.save(aCreated);

    const as = await aRepo.find();
    console.log("Loaded A: ", as);

    const b = new B();
    b.name = "something";
    const bCreated = bRepo.create(b);
    bCreated.a =  Promise.resolve(as[0]);
    await bRepo.save(bCreated);

    const as2 = await aRepo.find();
    console.log("Loaded A: ", as2);

}).catch(error => console.log(error));

Output

Inserting a new user into the database...
query: SELECT `b`.`id` AS `b_id`, `b`.`name` AS `b_name` FROM `b` `b` INNER JOIN `a` `A` ON `A`.`bId` = `b`.`id` WHERE `A`.`id` IN (?) -- PARAMETERS: [[null]]
query: START TRANSACTION
query: INSERT INTO `a`(`id`, `name`, `bId`) VALUES (DEFAULT, ?, DEFAULT) -- PARAMETERS: ["something"]
query: UPDATE `a` SET  WHERE `id` = ? -- PARAMETERS: [1]
query failed: UPDATE `a` SET  WHERE `id` = ? -- PARAMETERS: [1]

If I remove the promises from the entities, everything works fine:

Entity A

...
    @OneToOne(
        (type: any) => B,
        (o: B) => o.a
    )
    @JoinColumn()
    public b: B;
}

Entity B

...
    @OneToOne(
        (type: any) => A,
        (o: A) => o.b)
    a: A;

}

main.ts

createConnection().then(async connection => {
...
    const bCreated = bRepo.create(b);
    bCreated.a =  as[0];
    await bRepo.save(bCreated);
...

Output

query: INSERT INTO `b`(`id`, `name`) VALUES (DEFAULT, ?) -- PARAMETERS: ["something"]
query: UPDATE `a` SET `bId` = ? WHERE `id` = ? -- PARAMETERS: [1,1]
query: COMMIT
query: SELECT `A`.`id` AS `A_id`, `A`.`name` AS `A_name`, `A`.`bId` AS `A_bId` FROM `a` `A`

I've also created a Git project demonstrating this for easy testing and understanding.

1) Using Promises (not working) https://github.com/cuzzea/bug-typeorm/tree/promise-issue

2) No Lazy Loading (working) https://github.com/cuzzea/bug-typeorm/tree/no-promise-no-issue

Answer №1

Upon investigating your promise-issue repository branch, I came across some intriguing findings:

  1. The issue with the invalid UPDATE query seems to be triggered by the initial await aRepo.save(aCreated);, rather than the insertion of B and subsequent foreign key assignment to a.b. By setting a.b = null before aRepo.create(a), you can avoid this problem.

  2. To prevent the unexpected invalid UPDATE, it is suggested to add the initialization a.b = null; before aRepo.create(a) like so:

    const a = new A();
    a.name = "something";
    a.b = null;
    const aCreated = aRepo.create(a);
    await aRepo.save(aCreated);
  3. Furthermore, it appears that using async functions for the inverseSide argument in @OneToOne() (e.g., async (o: B) => await o.a) is incorrect. The correct approach is to use (o: B) => o.a, as per the indications in the documentation and the generics on OneToOne. This is because TypeORM resolves the promise before passing the value to the function, causing issues with an async function returning another Promise instead of the property value.

  4. It has come to my attention that you are passing an instance of class A to aRepo.create(), which is unnecessary. You can directly pass your instance to aRepo.save(a), as Repository.create() simply copies the values from the provided object into a new entity instance. It also seems that .create() creates promises when they do not exist initially, potentially leading to the unresolved promise issue observed. Removing the aRepo.create(a) step and adjusting the save method to await aRepo.save(a); might help resolve this issue. There could be differences in how lazy load properties are handled when instanceof T is already defined as well, which warrants further investigation.

An attempt was made to upgrade the typeorm package to typeorm@next (version 0.3.0-alpha.12), but the issue persists.

I see that you have opened a GitHub issue related to this matter. I will work on creating a test case to demonstrate this over the next few days.

I trust this information sufficiently addresses your query!

Update

Further examination reveals that item 4) from the aforementioned list is likely the root cause of the issue at hand.

In RelationLoader.enableLazyLoad(), TypeORM overrides lazy property accessors on @Entity instances with its own getter and setter methods, such as

Object.defineProperty(A, 'b', ...)
. These overloaded accessors load and cache the associated B record, returning a Promise<B>.

When Repository.create() processes all relations for the created entity and encounters an object, it constructs a new related object from the provided values. However, this logic does not handle a Promise object properly, resulting in an attempt to construct a related entity directly from the Promise's properties.

Thus, in the scenario described earlier, aRepo.create(a) generates a new A, traverses the A relations (i.e., b), and produces an empty B based on the Promise associated with a.b. As Promise instances lack any shared properties with B, the new B remains undefined. Consequently, without specifying an id, the foreign key name and value required for aRepo.save() are missing, leading to the encountered error.

Hence, directly passing a to aRepo.save() while eliminating the aRepo.create(a) step appears to be the appropriate resolution in this scenario.

This underlying issue should be addressed; however, rectifying it may pose a challenge since enabling Repository.create() to await the Promise is currently unattainable due to the non-async nature of Repository.create().

Answer №2

Revisiting @Timshel's exceptional response (and efforts to address the underlying problem in typeorm itself).

For those stumbling upon this post seeking a workaround while waiting for https://github.com/typeorm/typeorm/pull/2902 to be merged, I believe I have found a solution (assuming you are utilizing the ActiveRecord pattern with typeorm). To recap, crucial details regarding this issue are largely absent from the documentation and must be gathered from various GitHub issues and conversations:

As highlighted here and on the related thread, employing a Promise for a lazily loaded relation field when using the create method does not function as expected, despite the function's type signature indicating otherwise (even though the documentation implies that lazy loaded fields should be wrapped in Promise.resolve for saving purposes). According to insights from @Timshel's feedback in the aforementioned pull request:

necessary TypeScript type conversions when assigning object literals to lazy-loaded properties

This means that by utilizing the create method, if you provide a plain entity object (instead of a Promise containing said object) for one of these lazily loaded fields, typeorm will accurately set this value, enabling successful saving. Furthermore, you will automatically receive a promise when accessing this field later on. The excerpt above suggests leveraging this approach by forcefully casting your entity objects as Promises before passing them into create. However, this process must be done on a case-by-case basis, and any inadvertent adherence to the type signature instead of force casting could lead to unexpected outcomes at runtime. Wouldn't it be beneficial if we could rectify this type signature to prompt the compiler to signal errors only when misusing this function? Well, we can! Here's how :).

import {
  BaseEntity,
  DeepPartial,
  ObjectType,
} from 'typeorm';

/**
 * Conditional type that takes a type and maps every property which is
 * a Promise to the unwrapped value of that Promise. Specifically to correct the type
 * of typeorm's create method. Using this otherwise would likely be incredibly unwise.
 *
 * For example this type:
 * {
 *   hey: number,
 *   thing: Promise<ThingEntity>,
 *   sup: string
 * }
 *
 * gets mapped to:
 * {
 *   hey: number,
 *   thing: ThingEntity,
 *   sup: string
 * }
 *
 */
type DePromisifyValue<T> = T extends Promise<infer U> ? U : T;
type DePromisifyObject<T> = T extends object
  ? { [K in keyof T]: DePromisifyValue<T[K]> }
  : T;

export abstract class CommonEntity extends BaseEntity {
  static create<T extends CommonEntity>(
    this: ObjectType<T>,
    entityLike?: DeepPartial<DePromisifyObject<T>>
  ): T {
    if (!entityLike) {
      return super.create<T>();
    }
    return super.create<T>(entityLike as DeepPartial<T>);
  }
}

This customized implementation of the create method defines a version that accepts the same object argument as the original method, except where any Promise fields are substituted with their unwrapped counterparts (e.g.,

myLazyLoadedUser: Promise<UserEntity>
becomes myLazyLoadedUser: UserEntity). Subsequently, this altered object is forwarded to the original create method and coerced back to the former version comprising all the Promise fields—a preference aligned with the expectations of BaseEntity (whether sincere or feigned). Eventually, some form of coercion becomes inevitable until the underlying issue within typeorm is resolved. Nevertheless, this workaround necessitates coercion at a single central juncture, ensuring correctness. By extending this CommonEntity (or an alternative moniker) instead of BaseEntity, the create method enforces the appropriate type. Thereby eliminating the need to envelop values in Promise.resolve. Moreover, the resulting entity retains its lazy loaded fields bearing their native Promise types.

Note: I have not addressed the type specification for create when an array of objects is supplied. While I personally have no use for this at present, devising a type definition for this scenario using a similar approach is undoubtedly achievable with adequate effort.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Angular Firebase Email Verification sent to an alternate email address

I am facing a challenge with my website that only accepts emails from a specific domain, such as a university domain. Users who want to sign up using Facebook or Google need to verify their university email, but I am struggling to find a way to send the ve ...

unable to see the new component in the display

Within my app component class, I am attempting to integrate a new component. I have added the selector of this new component to the main class template. `import {CountryCapitalComponent} from "./app.country"; @Component({ selector: 'app-roo ...

Schedule - the information list is not visible on the calendar

I am trying to create a timeline that loads all data and events from a datasource. I have been using a dev extreme component for this purpose, but unfortunately, the events are not displaying on the calendar. Can anyone offer any insights into what I might ...

"Exploring the differences between normalization structures and observable entities in ngrx

I'm currently grappling with the concept of "entity arrays" in my ngrx Store. Let's say I have a collection of PlanDTO retrieved from my api server. Based on the research I've done, it seems necessary to set up a kind of "table" to store th ...

Sharing references in React Native using TypeScript: The (property) React.MutableRefObject<null>.current is potentially null

I'm working on a React Native form with multiple fields, and I want the focus to move from one field to the next when the user validates the input using the virtual keyboard. This is what I have so far: <NamedTextInput name={&apo ...

What is the best way for me to examine [...more] closely?

import * as Joi from 'joi'; import 'joi-extract-type'; const schema = { aaaaaaa: Joi.number() .integer() .positive() .allow(null), bbbbbb: Joi.number() .integer() .positive() .all ...

Using an External JavaScript Library in TypeScript and Angular 4: A Comprehensive Guide

My current project involves implementing Google Login and Jquery in Typescript. I have ensured that the necessary files are included in the project: jquery.min and the import of Google using <script defer src="https://apis.google.com/js/platform.js"> ...

A guide to implementing lazy loading using BXSlider

I have implemented an image gallery using Bxslider, but I am facing an issue with the loading time as there are more than 15 images in each slide. To address this problem, I want to load images one by one when a particular slide appears. I came across an e ...

Unable to utilize object functions post-casting操作。

I've encountered an issue while trying to access some methods of my model object as it keeps returning an error stating that the function does not exist. Below is the model class I have created : class Expense { private name: string; private ti ...

Enhancing Angular 4 classes with dependency injection techniques

Currently utilizing angular 4 and angular cli for my project development. I have created some classes that serve as the base for my components! However, as the constructors of these classes grow during development, I find myself in a phase where I need to ...

Utilizing Node.js, Webpack, and TypeScript to gain access to the directory path of source files within a project, rather than the project

Just to clarify, I'm not looking for the process.cwd in this question, but I need to access the absolute path of the source project. For example: Source code: C:\Users\user1\projects\lib1\src\library.ts (which will beco ...

Trouble integrating PDF from REST API with Angular 2 application

What specific modifications are necessary in order for an Angular 2 / 4 application to successfully load a PDF file from a RESTful http call into the web browser? It's important to note that the app being referred to extends http to include a JWT in ...

TypedScript: A comprehensive guide to safely omitting deep object paths

Hi there, I have a complex question that I would like some help with. I've created a recursive Omit type in TypeScript. It takes a type T and a tuple of strings (referred to as a 'path'), then removes the last item on the path and returns t ...

Notify the user with a message that our support is limited to Chrome, Firefox, and Edge browsers when utilizing Angular

How can I display a message stating that we only support Chrome, Safari, Firefox, and Edge browsers conditionally for users accessing our site from other browsers like Opera using Angular 10? Does anyone have a code snippet to help me achieve this? I atte ...

Retrieve the value of the Type Property dynamically during execution

Looking at this type generated from a graphql schema: export type UserPageEntry = { readonly __typename?: 'UserPageEntry' } I am interested in retrieving the string representation of its only property type ('UserPageEntry') during co ...

Angular 6 - Receiving @Input causes output to multiply by 4 instead of displaying just once

In my Angular project, I have two components set up. Here is the code for both: app.component.ts: import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styl ...

Passing a class as a parameter in Typescript functions

When working with Angular 2 testing utilities, I usually follow this process: fixture = TestBed.createComponent(EditableValueComponent); The EditableValueComponent is just a standard component class that I use. I am curious about the inner workings: st ...

Increasing the token size in the Metaplex Auction House CLI for selling items

Utilizing the Metaplex Auction House CLI (ah-cli) latest version (commit 472973f2437ecd9cd0e730254ecdbd1e8fbbd953 from May 27 12:54:11 2022) has posed a limitation where it only allows the use of --token-size 1 and does not permit the creation of auction s ...

Creating an array that exclusively contains numbers using anonymous object export: A step-by-step guide

I am struggling with a record that is designed to only accept values of type number[] or numbers. This is how it is structured: type numberRecords = Record<string,number[]|number>; I ran into an error when trying this: export const myList:numberRec ...

`Getting Started with TypeScript in an ASP.Net MVC Application`

Due to certain reasons, we have decided to begin our project with TS rather than JS. We are facing issues with the variables set in the MVC Views, which are established by the Model of each View. For example, tes.cshtml: @model Testmodel <script> ...