Computed properties through related models

#1

I have the following problem to solve:

  • I need to calculate two properties for a list through their related items.

  • The database should not be changed, that is, the fields must be calculated only at the time of the query.

First try:

I tried to get this calculated properties through computed fields, but it doesnt worked:

static get computed () {
        return ['computed_values']
}

getComputedValues ( obj ) {
        return obj.getRelated('items')
}

My page shows:

TypeError:
obj.getRelated is not a function

And when I change the function to return all items:

getComputedValues ( obj ) {
        return obj.items
}

It shows the list.computed_value field as undefined. :frowning:

Second Try:

At the moment, I can add the fields by calling the following hook after the fetch and find methods:

'use strict'

const ListHook = exports = module.exports = {}

ListHook.evaluateQuantities = async (modelInstances, PaginationMetadata) => {
    if (Array.isArray(modelInstances)) {
        modelInstances.map(evaluateQuantities)
    }else{
        modelInstances = evaluateQuantities(modelInstances)
    }
}

function evaluateQuantities(List) {
    let items = List.getRelated('items')

    if (items != undefined && items.rows.length > 0) {
        List.desired_quantity = items.rows.length
        List.actual_quantity = items.rows.reduce(
            ( total, item ) => { return total + (item.actual_quantity >= item.desired_quantity) },
            0
        )
    } else {
        List.desired_quantity = 0
        List.actual_quantity = 0
    }

    return List
}

When I call the list.desired_quantity or list.actual_quantity attribute on the view, I can see the values that I wanted.

Now comes the problem: When I try to update the List model through the bellow code…

async update ({ params, request, response }) {
        const list = await List.find(params.id)

        const data = request.only(['name', 'description'])

        list.merge(data)
        list.save()

        return response.redirect('back')
 }

This error message is show:

knex:query update `lists` set `name` = ?, `updated_at` = ?, `desired_quantity` = ?, `actual_quantity` = ? where `id` = ? undefined +5ms
  knex:bindings [ 'Edited List', '2019-05-04 16:39:58', 0, 0, 8 ] undefined +5ms
  knex:client releasing connection to pool: __knexUid1 +4ms
warning: 
  WARNING: Adonis has detected an unhandled promise rejection, which may
  cause undesired behavior in production.
  To stop this warning, use catch() on promises or wrap await
  calls inside try/catch.

{ Error: SQLITE_ERROR: no such column: desired_quantity
    at Client_SQLite3._query (/.../node_modules/knex/lib/dialects/sqlite3/index.js:117:12)
    at Client_SQLite3.query (/.../node_modules/knex/lib/client.js:202:17)
    at Runner.<anonymous> (/.../node_modules/knex/lib/runner.js:146:36)
From previous event:
    at /.../node_modules/knex/lib/runner.js:48:21
    at runCallback (timers.js:705:18)
    at tryOnImmediate (timers.js:676:5)
From previous event:
    at Runner.run (/.../node_modules/knex/lib/runner.js:34:31)
    at Builder.Target.then (/.../node_modules/knex/lib/interface.js:20:43)
    at process._tickCallback (internal/process/next_tick.js:68:7) errno: 1, code: 'SQLITE_ERROR' }

I tried to solve the problem by adding the following hook to remove the calculated fields before performing the update:

ListHook.removeComputedProperties = async (modelInstances) => {
    if (Array.isArray(modelInstances)) {
        modelInstances.map(removeComputedProperties)
    }else{
        modelInstances = removeComputedProperties(modelInstances)
    }
}

function removeComputedProperties(List) {
    delete List.desired_quantity
    delete List.actual_quantity

    return List
}

In the end, the model looked like this:

'use strict'

const Model = use('Model')

class List extends Model {
    
    static boot() {
		super.boot()		
		
		this.addHook('afterFind', 'ListHook.evaluateQuantities')
		this.addHook('afterFetch', 'ListHook.evaluateQuantities')
        this.addHook('afterPaginate', 'ListHook.evaluateQuantities')
        
        this.addHook('beforeUpdate', 'ListHook.removeComputedProperties')
    }

    items() {
        return this
            .hasMany('App/Models/Item')
            .where('deleted_at', null)
    }
}

module.exports = List

But unfortunately the console keeps showing the same error :frowning:²

I Finally found the solution!

I had to change the removeComputedProperties hook to:

function removeComputedProperties(List) {
    delete List.$attributes.desired_quantity
    delete List.$attributes.actual_quantity

    return List
}

Now my question is: is this the best way to use computed properties based on related models? :thinking:

1 Like