# Models

Models in Nodewood are not the ORM models you may expect. They don't even have to represent a database entity at all - they represent a business logic entity, and that can come from a database, a third-party API, temporal data, whatever your business logic requires.

# Creating a new Model

From the root of your project, run nodewood add model FEATURE NAME. This will create a new Model in app/features/FEATURE/lib/models/NAMEModel.js. Typically, you will pair Models with Validators, so if you have a PostModel, you'll likely want to pair it with a PostValidator.

The simplest model looks something like:

const { FieldNumber, FieldString } = require('#lib/Fields');

const FIELDS = {
  id: new FieldNumber({ label: 'ID' }),
  firstName: new FieldString({ label: 'First Name' }),
  lastName: new FieldString({ label: 'Last Name' }),
};

class SampleModel {
  constructor({ id, first_name, last_name } = {}) {
    super(FIELDS);

    this.id = id;
    this.firstName = first_name;
    this.lastName = last_name;
  }

  toJSON() {
    return {
      id: this.id,
      first_name: this.firstName,
      last_name: this.lastName,
    };
  }
}

module.exports = {
  SampleModel,
  FIELDS,
};

# Constructor

The model's constructor accepts a snake-case-keyed object and assigns those values to camel-cased local properties. It also passes the fields for this model to the base Model class to make it easier to display this model's fields.

constructor({ id, first_name, last_name } = {}) {
  super(FIELDS);

  this.id = id;
  this.firstName = first_name;
  this.lastName = last_name;
}

Most databases and JSON formats use snake-case, and most JavaScript code is camel-cased, so this helps translate between persistence/transmission formats and your code.

Note that only business logic-related values are saved to the model. The database table your model is based on may have fields like created_at and updated_at, but if those fields aren't important to the logic of your application (i.e. only used in sorting when loading from the database), you don't need to save them as model fields. Further, secure fields (like passwords) often shouldn't be stored in models either. They should be accessed securely from within services.

# toJSON()

This function is essentially the opposite of the constructor, taking the camel-cased local properties of the model and turning them back into a snake-case-keyed object for use when saving in a database or sending back from an API.

toJSON() {
  return {
    id: this.id,
    first_name: this.firstName,
    last_name: this.lastName,
  };
}

# Fields

Models have a FIELDS variable that describe the fields of that model for display purposes:

const FIELDS = {
  id: new FieldNumber({ label: 'ID' }),
  firstName: new FieldString({ label: 'First Name' }),
  lastName: new FieldString({ label: 'Last Name' }),
};

It is important to note that Fields don't contain the value of the field, they describe it for display. Values for fields are stored in the Model themselves.

Each field is defined with a class from wood/lib/src/Fields.js that has different display options available to set in its constructor, and a value() function you can call with the value of the field to display it appropriately. This makes it easy to loop over a collection of models and just call the value() function do format it as expected, without needing to store that information in multiple other places in your application.

The base Model class actually has copies of these functions to make it easier to display the model's fields, properly formatted by its fields, as we'll explore in the next section.

# Using fields

To iterate over a list of fields in a model and display the values in a user-friendly way:

const { ExampleModel, FIELDS } = require('#features/example/lib/models/ExampleModel');

const example = new ExampleModel(someValues);

for (const key of Object.keys(FIELDS)) {
  console.log(`${example.label(key)} ${example.value(key)}`);
}

You can also add classes to the label or data when defining the fields, and you can choose to apply these classes to the container or inner text. This is useful for certain fields that you want to always be displayed centered in their elements:

const FIELDS = {
  approved: new FieldBoolean({
    label: 'Approved',
    valueClasses: ['text-center'],
    valueInnerClasses: ['underline'],
  }),
};
<tr v-for="key in Object.keys(FIELDS)">
  <td :class="example.valueClass(key)">
    <span :class="example.valueInnerClass(key)">
      {{ example.value(key) }}
    </span>
  </td>
</tr>

This will show the "approved" field centered in its container, with the inner text underlined.

# Custom value class functions

Sometimes you will want to add a class to a field based on that field's value (positive/negative money value, true/false, etc). In these cases, you can define valueClassFn or valueInnerClassFn on the field:

const FIELDS = {
  balance: new FieldNumber({
    label: 'Balance',
    valueInnerClassFn: (value) => value >= 0 ? 'text-green-500' : 'text-red-600',
  }),
};

# List of Field types

The following fields come with Nodewood, but you can create your own by extending { FieldDisplay } in #lib/Fields.

# FieldString

The simplest of fields, FieldString displays the field without any modification or formatting.

Code:

const address = new FieldString({ label: 'Address' });
console.log(`${address.label()}: ${address.value('55 West St')}`);

Output:

Address: 55 West St

# FieldNumber

The Number field uses the Numbro library to format the provided value as a number.

Code:

const price = new FieldNumber({
  label: 'Price',
  numberFormat: {
    thousandSeparated: true,
    mantissa: 2,
  }
});
const day = new FieldNumber({
  label: 'Day',
  numberFormat: {
    output: "ordinal",
  },
})

console.log(`${price.label()}: $${price.value(5555.5)}`);
console.log(`${day.label()}: ${day.value(2)}`);

Output:

Price: $5,555.50
Day: 2nd

# FieldBoolean

Use a Boolean field when you have a value that is either true or false, exclusively. The default text for the value is 'Yes' and 'No', but that can be customized.

Code:

const enabled = new FieldBoolean({
  label: 'Enabled',
  trueValue: 'Indeed',
  falseValue: 'Negatory',
});

console.log(`Is ${enabled.label()}: ${enabled.value(true)}`);
console.log(`Isn't ${enabled.label()}: ${enabled.value(false)}`);

Output:

Is Enabled: Indeed
Isn't Enabled: Negatory

# FieldDate

The Date field uses the Moment library to format the provided value as a date. You can also specify the value you wish to display if a date is optional and no date has been specified.

Code:

const openingNight = new FieldDate({
  label: 'Opening Night',
  dateFormat: 'MMMM Do YYYY, h:mm:ss a',
  nullValue: 'Not Scheduled',
});

console.log(`Hamilton ${openingNight.label()}: ${openingNight.value('2015-08-06 20:30:00')}`);
console.log(`Cats - The Return ${openingNight.label()}: ${openingNight.value(null)}`);

Output:

Hamilton Opening Night: August 6th 2015, 8:30 pm
Cats - The Return Opening Night: Not Scheduled

# FieldEnum

Enum fields are like super-powered booleans. However, instead of two defined values, Enums can translate a list of values to a corresponding list of display values.

Code:

const role = new FieldEnum({
  label: 'Role',
  enumList: {
    'leader': 'Team Leader',
    'member': 'Team Member',
    'w+m1': 'Team Fortress 2',
  },
  dataClasses: ['text-center'],
});

console.log(`Miyamoto's ${role.label()}: ${role.value('leader')}`);
console.log(`Dave's ${role.label()}: ${role.value('member')}`);
console.log(`Pyro's ${role.label()}: ${role.value('w+m1')}`);

Output:

Miyamoto's Role: Team Leader
Dave's Role: Team Member
Pyro's Role: Team Fortress 2

# Adding a new model

To add a new model with some sample code to get you rolling, run:

nodewood add model FEATURE NAME

Replace FEATURE with the name of the feature you wish to add a model for, and NAME with the name of the model you wish to create.