4. Mutation

4.1 A CRUD scenario

Once you have a Pokemon instance, either retrieved via one, list or page, or simply instantiated. You can save that Pokemon by calling save.

It will then either creates a new Pokemon by performing a POST when the id is empty, or updates an existing resource via a PUT request when the id exists.

/* 
  First create a pokemon by creating a new instance. 
  Or alternatively fetch the pokemon using `one`.
*/
const pokemon = new Pokemon();
pokemon.name = 'bulbasaur';
pokemon.type = ['grass', 'poison'];

/* This POST to api/pokemon, with all the properties of Pokemon as the body. */
pokemon.save().then(() => {
  /* 
    The pokemon instance will now have an ID.
    Because every property from the back-end response
    is merged into the pokemon instance automatically.
    This MUTATES the pokemon instance!
  */

  console.log(pokemon.id); // Prints "1";

  /* This PUT to api/pokemon/1, with all the properties of Pokemon as the body. */
  pokemon.save();

  /* This will DELETE to api/pokemon/1. */
  pokemon.remove().then(() => {
    /* 
      The pokemon instance will no longer have an id because
      `remove` will delete the `id` MUTATING the pokemon.
    */

    console.log(pokemon.id); /* Prints "undefined"; */
  });
});

4.2 Uploading

When uploading files it is important to know that post, put and patch support FormData as the payload argument.

Often it is easiest when uploading to simply create a static method called save which can take a form to upload.

Here is an example of a form, it contains sprites which are either Files or urls as a string:

export type PokemonFormData = {
  id?: number;
  name: string;

  spriteFront: string | File;
  spriteBack: string | File;
}

Heres a static save method on the Pokemon resource that creates a multipart/form-data; request.

import { makeResource, post, put } from '@42.nl/spring-connect';

const baseUrl = '/api/pokemon';

export default class Pokemon extends makeResource<Pokemon>(baseUrl) {
  id!: number;
  name!: string;

  spriteFront!: string;
  spriteBack!: string;

  /* This save either POST's a new pokemon, or PUTS to an existing one. */
  static save(pokemonForm: PokemonFormData): Promise<Pokemon> {
    /* The formData will contain the pokemon and optionally the two sprites. */
    const formData = new FormData();

    /* If there is a front sprite File add it to the formData. */
    if (pokemonForm.spriteFront instanceof File) {
      formData.append('front', new Blob([pokemonForm.spriteFront]));
    }

    /* If there is a back sprite File add it to the formData. */
    if (pokemonForm.spriteBack instanceof File) {
      formData.append('back', new Blob([pokemonForm.spriteBack]));
    }

    /* Now remove the sprites */
    delete pokemonForm.spriteFront;
    delete pokemonForm.spriteBack;

    /* Append the pokemon blob. */
    const pokemon = new Blob([JSON.stringify(pokemonForm)], {
      type: 'application/json',
    });
    formData.append('pokemon', pokemon);

    /* POST on create, PUT on edit. */
    const method = pokemonForm.id ? put : post;

    const url = pokemonForm.id ? `${baseUrl}/${pokemonForm.id}` : baseUrl;

    /* 
      Finally send the `multipart/form-data;` which has three entries:
      the front sprite, back sprite, and the pokemon data.
    */
    return method(url, formData);
  }
}

Now that we know how to mutate our data lets see how we can add custom methods to our Pokemon.