Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BaseModel.toObject() fails with null nested BelongsTo relationship #1008

Open
theRealPadster opened this issue Feb 28, 2024 · 4 comments
Open

Comments

@theRealPadster
Copy link

theRealPadster commented Feb 28, 2024

Package version

18.4.2

Describe the bug

I have a model that has a nested nullable BelongsTo relationship, and when I try to use .toObject() on it, it gives me a TypeError: Cannot read properties of null (reading 'toObject')

The model is like this:
Part > Item > PartCategory
PartCategory has a parent_id: number | null column field, and a parent relationship like so:

@column()
public parent_id: number | null;

@belongsTo(() => PartCategory, {
  foreignKey: 'parent_id',
})
public parent: BelongsTo<typeof PartCategory>;

When I query my Part model, it works if the category has a parent, but fails when the category has no parent.

const partAd = await Part.query()
  .where('slug', params.slug)
  .andWhere('status', 'ACTIVE')
  .preload('item',
    (q) => q.preload('category',
      (q2) => q2.preload('parent')
    )
  )
.firstOrFail();

const partAdObj = partAd.toObject();

I was able to fix this by overriding BaseModel.toObject() in my PartCategory model, to check for falsey values before trying to call .toObject() on them.

  /**
   * Convert model to a plain Javascript object
   */
  public toObject() {
    const Model = this.constructor;
    const computed = {};
    /**
     * Relationships toObject
     */
    const preloaded = Object.keys(this.$preloaded).reduce((result, key) => {
      const value = this.$preloaded[key];
      if (!value) {
        return result;
      }
      result[key] = Array.isArray(value) ? value.map((one) => one.toObject()) : value.toObject();
      return result;
    }, {});
    /**
     * Update computed object with computed definitions
     */
    // @ts-ignore
    Model.$computedDefinitions.forEach((_, key) => {
      const computedValue = this[key];
      if (computedValue !== undefined) {
        computed[key] = computedValue;
      }
    });
    return {
      ...this.$attributes,
      ...preloaded,
      ...computed,
      $extras: this.$extras,
    };
  }

Is what I'm doing correct? Is this a bug, or am I doing something wrong? I can submit a PR with this fix if I'm correct in my diagnosis.

Full stack trace:

TypeError: Cannot read properties of null (reading 'toObject')
    at /app/node_modules/@adonisjs/lucid/build/src/Orm/BaseModel/index.js:1507:93
    at Array.reduce (<anonymous>)
    at Proxy.toObject (/app/node_modules/@adonisjs/lucid/build/src/Orm/BaseModel/index.js:1505:56)
    at /app/node_modules/@adonisjs/lucid/build/src/Orm/BaseModel/index.js:1507:93
    at Array.reduce (<anonymous>)
    at Proxy.toObject (/app/node_modules/@adonisjs/lucid/build/src/Orm/BaseModel/index.js:1505:56)
    at /app/node_modules/@adonisjs/lucid/build/src/Orm/BaseModel/index.js:1507:93
    at Array.reduce (<anonymous>)
    at Proxy.toObject (/app/node_modules/@adonisjs/lucid/build/src/Orm/BaseModel/index.js:1505:56)
    at new AdObject (/app/app/Libraries/Algolia/classes/AdObject.ts:16:25)
SCR-20240228-nnon

Reproduction repo

No response

@thetutlage
Copy link
Member

Before you create a PR, can you please create a failing test in this repo? Lucid has many moving parts, so first we should be able to reproduce the issue in a test before we can accept any PRs for the code change

@theRealPadster
Copy link
Author

Sure, I can try. Do you mean submit a PR with the test? I'll admit I've not done any tests with Adonis/Japa before, so might need to figure some stuff out first.
Would this be the correct place to add it? And since my project is still on Adonis v5, it's using Lucid 18. Is there a branch or something I should use as a base?

@thetutlage
Copy link
Member

Yes, it should be based on https://github.com/adonisjs/lucid/tree/v18 branch.

  • I think, you just have to clone the repo.
  • Install dependencies
  • Run tests using npm run test:sqlite. This way you will not need any full blown SQL servers running on your computer.
  • To run your specific tests, you can pin it using the .pin method. Learn more in Japa docs.

Feel free to ask for help if you feel stuck

@theRealPadster
Copy link
Author

theRealPadster commented Feb 29, 2024

Okay, I've got this so far:

// TODO: I'm not really checking the author, so can probably just remove...
  test('add preloaded belongsTo relationship to toObject result', async ({ assert }) => {
    class Category extends BaseModel {
      @column()
      public name: string

      @column()
      public parentId: number

      @belongsTo(() => Category)
      public parent: BelongsTo<typeof Category>
    }

    class Post extends BaseModel {
      @column()
      public title: string

      @column()
      public userId: number

      @belongsTo(() => User)
      public author: BelongsTo<typeof User>

      @hasOne(() => Category)
      public category: HasOne<typeof Category>
    }

    class User extends BaseModel {
      @column({ isPrimary: true })
      public id: number

      @column()
      public username: string

      @hasMany(() => Post)
      public posts: HasMany<typeof Post>
    }

    const user = new User()
    user.username = 'virk'

    const category = new Category()
    category.name = 'Tutorials'

    const subCategory = new Category()
    subCategory.name = 'Lucid'

    const post = new Post()
    post.title = 'Adonis 101'

    // user.$setRelated('posts', [post])
    post.$setRelated('author', user)
    subCategory.$setRelated('parent', category)
    post.$setRelated('category', subCategory)

    // passes
    assert.deepEqual(subCategory.toObject(), {
      name: 'Lucid',
      parent: {
        name: 'Tutorials',
        $extras: {},
      },
      $extras: {},
    })

    // fails
    assert.deepEqual(category.toObject(), {
      name: 'Tutorials',
      parent: null,
      $extras: {},
    })
  }).pin()

I don't think I actually need the Post/Author stuff, so should I remove that? I'm also not sure about the parent being null, since we're not actually setting the parent_id in this code, just using $setRelated, which I haven't used before.

I can just make a draft PR with what I have if that's better than here :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants