5. Customize

5.1 Adding a custom mapper

By default every JSON which is received by the one, findOne, page and list is mapped by this function:

function defaultMapper<T>(json: JSON, Class: { new (): T }): T {
  return makeInstance(Class, json);
}

It simply makes an instance of the class by calling the makeInstance util function with the retrieved JSON.

Sometimes you want to override the default mapper, in the example below we create a Pokeball which contains a pokemon. What we want is to make the pokemon an actual instance of Pokemon. We also want to store the time of retrieval from the back-end.

class Pokeball extends makeResource<Pokeball>({
  baseUrl: '/api/pokeball',
  mapper: pokeballMapper,
}) {
  public id?: number;

  /*
    In the actual JSON response pokemon is simply an object.
    But our custom mapper makes sure it will also get mapped.
  */
  public pokemon!: Pokemon;

  /* 
    Does not really exist on the back-end but is filled by the
    custom mapper.
  */
  public retrievedAt!: Date;
}

function pokeballMapper(json: any, Class: { new (): Pokeball }): Pokeball {
  const pokeball = makeInstance(Class, json);
  /* Add a completely new field */
  pokeball.retrievedAt = new Date();

  /* Make the fetched pokemon an actual instance of Pokemon */
  pokeball.pokemon = makeInstance(Pokemon, pokeball.pokemon);

  return pokeball;
}

A custom mapper is useful for when the mapping for one, findOne, page and list is exactly the same. If they differ you should instead create custom methods instead, as explained below.

5.2 Adding custom methods on Pokemon

For most situations the default Resource will work just fine, but sometimes you do want to extend and/or customize the available methods from makeResource.

The trick here is that this library exposes the same building blocks that makeResource uses under the hood. You can use these building blocks to easily create your own custom methods.

See the Utils section for the helper functions. It is recommended that you use these functions to help you customize your Resource.

5.2.1 adding instance methods

Say you want to add method which retrieves all the evolutions of a Pokémon, this is how you do it:

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

const baseUrl = '/api/pokemon';

class Pokemon extends makeResource<Pokemon>(baseUrl) {
  /* shortend the definition of the pokemon class. */

  async evolutions(): Promise<Pokemon[]> {
    if (this.id) {
      /* `get` does a GET request  */
      const list = await get(`${baseUrl}/${this.id}/evolutions`);
      return list.map((properties: JSON) => {
        /* Convert to Pokemon instances */
        return makeInstance(Pokemon, properties);
      });
    }

    return Promise.resolve([]);
  }
}

Now you can use it in the following way:

const bulbasaur = await pokemon.one(1);
const evolutions = await pokemon.evolutions();

5.2.2 adding static methods

You could also solve this problem with a static method:

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

const baseUrl = '/api/pokemon';

class Pokemon extends makeResource<Pokemon>(baseUrl) {
  /* shortend the definition of the pokemon class. */

  static async evolutions(id: number): Promise<Pokemon[]> {
    /* `get` does a GET request */
    const list = await get(`${baseUrl}/${id}/evolutions`);
    return list.map((properties: JSON) => {
      /* Convert to Pokemon instances */
      return makeInstance(Pokemon, properties);
    });
  }
}

Which you could use like this:

const evolutions = await Pokemon.evolutions(1);

5.3 Overriding methods on Pokemon

Sometimes you will find that the default implementations does not match your domain. For example there might be a difference between an Entity in a List / Page or when it is retrieved alone.

5.3.1 Overriding instance methods

You can override save and remove by simply defining them.

This example defines its own custom save implementation:

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

const baseUrl = '/api/pokemon';

export default class Pokemon extends makeResource<Pokemon>(baseUrl) {
  id!: number;
  trainer!: number;
  name!: string;
  types!: string[];
  weakness!: string[];

  /*
    Here we provide a custom implementation, which always creates
    a new pokemon, and never updates one.
  */
  save(): Promise<Pokemon> {
    return post(baseUrl, this).then((json: any) => {
      return merge(this, json);
    });
  }
}

5.3.2 Overriding static methods

You can override one, findOne, list and page by simply defining them.

This example defines its own custom page implementation:

import { makeResource, get, makeInstance, Page, QueryParams } from '@42.nl/spring-connect';

/* When a pokemon is retrieved in a page it has less info. */
export type PagePokemon = {
  id: number;
  name: string;
}

const baseUrl = '/api/pokemon';

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

  /*
    Here we provide a custom implementation, which returns a PagePokemon
    instead of a Pokemon.
  */
  static page<PagePokemon>(queryParams?: QueryParams): Promise<Page<PagePokemon>> {
    return get(baseUrl, queryParams);
  }
}

5.4 Changing the type of type id field.

You can change the type of the id field by supplying a second generic parameter to makeResource:


const baseUrl = '/api/pokemon';

class Pokemon extends makeResource<Pokemon, string>(baseUrl) {
  public id?: string;
  public name!: string;
  public types!: string[];
}

const pokemon = new Pokemon();

// This should now work because the type of ID is now a string.
pokemon.id = 'a-unique-uu-id-for-example';
pokemon.name = 'bulbasaur';
pokemon.types = ['poison', 'grass'];

await pokemon.save();