# Save mixin

The save mixin allows to append CRUD (opens new window) functionality into the model.

# Model definition

The mixin provides a factory function that returns a mixin function. It is a general interface that allows to send arguments to the factory only once.

Of course, it's not necessary to define intermediate classes. Let's optimize the previous example:

# Flags

So, currently our model has a new method save() and the following readonly flags.

# Creating flag

Type: boolean. This flag indicating that the mixin is creating a record in the storage.

# Updating flag

Type: boolean. This flag indicating that the mixin is updating a record in the storage.

# Destroying flag

Type: boolean. This flag indicating that the mixin is deleting a record from the storage.

# Saving flag

Type: boolean. This flag indicating that the mixin is saving a record to the storage.

# New flag

Also the save mixin uses a flag from the base model class. This is a new flag. The flag indicating that the model data was never saved in the storage.

# Options

The mixin expects an options object with the following interface:

interface SaveOptions<T extends object> extends Options {
  /**
   * Mixin name.
   */
  mixinType: 'save';

  /**
   * A function that creates a new record in the storage.
   */
  create?: CreateFunc<T>;

  /**
   * A function that saves an existing record to the storage.
   */
  update?: UpdateFunc<T>;

  /**
   * A function that deletes an exisiting record from the storage.
   */
  destroy?: DestroyFunc<T>;
}

Type T is the model data type. That object should be send to the model constructor.

# Creating

You have to define the create property for SaveOptions if you want to create new records in your storage. The property must be a function that conforms to this interface:

type CreateFunc<T> = (data: T) => Promise<T | unknown> | T | unknown;

Type T is the model data type. The data argument must be an object, that will be send to the storage.

WARNING

If the function returns an object, that object will replace the model data property. If the function throws an error that error will be thrown outside.

For example we define a create function (we are using axios (opens new window)):

Instancing the model and try to save the new record to the storage:

const instance = new Model(undefined, { mixinType: 'save', create });

instance.data.firstName = 'John';
instance.data.lastName = 'Doe';

console.log(instance.new); // outputs: true

async function tryToSave() {
  try {
    await instance.save();
  } catch (error) {
    console.error('Could not save an instance by the reason:', error);
    console.log(instance.new); // outputs: true

    return;
  }

  console.log('An instance has been saved successfully');
  console.log(instance.new); // outputs: false
}

tryToSave(); // call the async function

The creating flag can be used to enable a loader indicator.

# Updating

Because the mixin doesn't know conditions of the model data identifier, the developer have to reset the new flag of existing records manually into the model constructor.

Set the update property for SaveOptions to enable the record update operation. The property must be a function that conforms to this interface:

type UpdateFunc<T> = (id: unknown, data: T) => Promise<T | unknown> | T | unknown;

The id argument is a value of a data field which is accessible by idKey. The data argument must be an object, that will be send to the storage.

WARNING

If the function returns an object, that object will replace the model data property. If the function throws an error that error will be thrown outside.

Let's define an update function:

and test it together with the model:

const instance = new Model({ id: 10, firstName: 'John', lastName: 'Doe' }, { mixinType: 'save', create, update });

console.log(instance.new); // outputs: false
console.log(instance.dirty); // outputs: false

instance.data.firstName = 'James';
instance.data.lastName = 'Smith';

console.log(instance.dirty); // outputs: true

async function tryToSave() {
  const isNew = instance.new;

  try {
    await instance.save();
  } catch (error) {
    const text = isNew ? 'create' : 'update';

    console.error(`Could not ${text} an instance by the reason:`, error);
    console.log(instance.dirty); // outputs: true

    return;
  }

  const text = isNew ? 'created' : 'updated';

  console.log(`An instance has been ${text} successfully`);
  console.log(instance.dirty); // outputs: false
}

tryToSave();

The updating flag can be used to enable a loader indicator.

# Destroying

Simply define and use the destroy property of SaveOptions to enable the record delete operation. The property must be a function that conforms to this interface:

type DestroyFunc<T> = (id: unknown, data: T) => Promise<void> | void;

The id argument is a value of a data field which is accessible by idKey. The data argument must be an object, that may be send to the storage.

WARNING

The record delete operation sets the destroyed flag, but doesn't destroys the model instance.

It is necessary to define a destroy function to complete the mixin's options list.

A complete example: