herbsjs / gotu

Entities - Create your Entity and enjoy an elegant validation and serialization
Other
11 stars 19 forks source link

Computed field for entity #68

Open vma-vortx opened 1 year ago

vma-vortx commented 1 year ago

Describe the solution you'd like Like in Object-oriented design, I'd like to read a field from an entity based on values of other fields. This would bring a concept of encapsulation to herbs' entities. This property, ideally, should not be able to be modified.

For example:

var person = new Person();
person.Name = "Victor;
person.Surname = "Melias";
console.log(person.Fullname); //should return "Victor Melias"

Additional context In OOD languages this can be done by setting the properties with "private set", but there's no alternative in Herbs to do this without any kind of workaround

dalssoft commented 1 year ago

Hi @VictorTBX , thanks for your suggestion.

It seems your suggestion is related to the concept of computed properties, which is a feature that is not yet available in Herbs.

Here are some benchmarks for this feature:

Django: https://docs.djangoproject.com/en/4.1/topics/db/models/#model-methods Laravel: https://laravel.com/docs/8.x/eloquent-mutators#defining-an-accessor Phoenix: https://hexdocs.pm/phoenix/ecto_models.html#computed-fields PostgreSQL: https://www.postgresql.org/docs/current/ddl-generated-columns.html SQL Server: https://learn.microsoft.com/en-us/sql/relational-databases/tables/specify-computed-columns-in-a-table?view=sql-server-ver16 and many others

On vanila JS, you can use getters and setters to achieve this:

const person = {
  name: 'Victor',
  surname: 'Melias',
  get fullname() {
    return `${this.name} ${this.surname}`
  }
}

With that, my suggestion would be a new parameter in the entity definition, something like:

const Person = entity('Person', {
  id: field(Number),
  name: field(String),
  surname: field(String),
  fullname: field(String, { value: (person) => `${person.name} ${person.surname}` })
})

A few things to consider:

Ex:

const Person = entity('Person', {
    ...
    fullname: field(String, { value: (person) => `${person.name} ${person.surname}` })
})

or

const Order = entity('Order', {
    ...
    total: field(Number, { value: (order) => order.items.reduce((total, item) => total + item.price, 0) })
})
const person = new Person({ name: 'Victor', surname: 'Melias' })
console.log(person.fullname) // should return 'Victor Melias'
person.name = 'John'
console.log(person.fullname) // should return 'John Melias'
const person = new Person({ name: 'Victor', surname: 'Melias' })
console.log(person.fullname) // should return 'Victor Melias'
person.fullname = 'John Melias' // should throw an error
const person = new Person({ name: 'Victor', surname: 'Melias' })
console.log(person.fullname) // should return 'Victor Melias'
console.log(person.toJSON()) // should return { id: 1, name: 'Victor', surname: 'Melias', fullname: 'Victor Melias' }
const Person = entity('Person', {
    ...
    fullname: field(String, { value: (person) => `${person.name} ${person.surname}` }),
    fullnameLength: field(Number, { value: (person) => person.fullname.length })
})
const Person = entity('Person', {
    ...
    fullname: field(String, { value: async (person) => {
        const { name, surname } = await fetchPerson(person.id)
        return `${name} ${surname}`
    } })
})

This is open to discussion, since this would change how to call a field (sync vs async).

const person = new Person({ name: 'Victor', surname: 'Melias' })
console.log(await person.fullname) // should return 'Victor Melias'

The problem here is that this also changes how to retrive value of a calculated field in glues, like herbs2rest and herbs2gql since they are expecting a sync value.

So having a async value could be a next step, so I think it would be better to have a sync value first.

const Person = entity('Person', {
    ...
    fullname: field(String, { value: function () {
        return `${this.name} ${this.surname}`
    } })
})

What else do you think we should consider?

Again, thanks for your suggestion. Let's discuss it and see if we can implement it. I think it would be a great feature.

vxsander commented 1 year ago
const Person = entity('Person', {
  id: field(Number),
  name: field(String),
  surname: field(String),
  fullname: field(String, { value: function () {
    if (person.name && person.surname)    
      return `${this.name} ${this.surname}`
    } })
})
const person = new Person({ surname: 'Melias' })
console.log(person.fullname) // should return undefined
person.name = 'Victor'
console.log(person.fullname) // should return 'Victor Melias'
vma-vortx commented 1 year ago

Great, thanks @dalssoft

I think those are all things to be considered and will help to develop this feature. The async I can't see an application for this, since entities should not retrieve information from external sources and all entities methods can be done as sync.

vma-vortx commented 1 year ago

I started developing this feature, but I'm stucked with two problems:

How mandatory are these two concepts?

The async is not developed as well, but I have successfully tested the other features, like toJson()

dalssoft commented 1 year ago

Great! Thanks @VictorTBX .

I recommend you to create a PR and we can discuss the issues you are facing there. I'm curios to understand why these features are a problem. I think it will be clear with code.

VictorTBX commented 1 year ago

@dalssoft

I've opened a draft https://github.com/herbsjs/gotu/pull/69