immerable-record

1.0.14 • Public • Published

Welcome, to Immerable Record!

"NodeJS Version" "npm Version" "pnpm Version" "Immer Version"
"Test Code Coverage" "Test Status" "Build Status"

Immerable Record is an immutable record data structure API, which uses pure, vanilla JavaScript objects for deep nesting.

It is similar, in contract, to the ImmutableJS Record, but uses Immer, for immutability, so you are always working with vanilla JavaScript objects, in your immutable data, no matter how deeply nested your data structure.

Contents

About
Quickstart

Methods
History API
Advanced Topics

Author And License Info
Support This Project

About

All instances returned by Immerable Record CRUD methods, (and wrapper methods defined on the extending class or object), are immutable Immer drafts (as they are returned from the Immer .produce method).

All properties of a draft returned by an Immerable Record can be accessed using vanilla JavaScript dot or square bracket notation. As with an ImmutableJS Record, any CRUD actions preformed must be done via built-in methods, which are mostly on parity, with their ImmutableJS counterparts, such as .getIn and .setIn (Immerable Record has a few additionals, such as .getInArrIdx and .setInArrIdx, for drilling into arrays).

Immerable Record also provides a history API, which enables you to easily traverse, examine, and use previous versions of your object. Records, from history, are also immutable Immer drafts.

Quickstart

Requirements

The only requirement for Immerable Record is a dependency of: "immer": "^9.0.3".
Immerable Record uses Immer as a peer dependency, so you will need to install and / or add both immer and immerable-record to your project's package.json dependencies:

"dependencies": {

  "immer": "^9.0.3",
  "immerable-record": "^1.0.14",

}

Or:

$ npm install --save immer@9.0.3 immerable-record@1.0.14

Class Extension

To begin, we'll create a simple class, which extends the ImmerableRecord class. While Immerable Record is not limited to use with Redux, this sample implementation is ideal, for creating domain objects, with which to divide your Redux state into manageable units of responsibility. This is a working example, from a released project:

import ImmerableRecord from 'immerable-record';

import RequestStatus from 'constant/RequestStatus';

// CLASS EXTENDS ImmerableRecord...
class HealthCheckStore extends ImmerableRecord {
  constructor() {
    super({
      healthCheckData: {},
      healthCheckDataRequestStatus: RequestStatus.UNINITIATED,
      healthCheckDataRequestStatusReason: {
          [RequestStatus.UNINITIATED]: RequestStatus.UNINITIATED
        },
      healthCheckDataUpdateTimestamp: 0

      // CONFIG OBJECT WITH historyLimit ENABLES HISTORY API...
    }, { historyLimit: 5 });

    // THE USUAL CLASS METHOD BINDERS...
    this.getHealthCheckData = this.getHealthCheckData.bind(this);
    this.setHealthCheckData = this.setHealthCheckData.bind(this);

    this.getHealthCheckDataRequestStatus = this.getHealthCheckDataRequestStatus.bind(this);
    this.setHealthCheckDataRequestStatus = this.setHealthCheckDataRequestStatus.bind(this);

    // PREVENT REASSIGNMENT OR EXTENSION, OF THE INITIAL INSTANCE
    // (or, use Object.seal, depending on your use case)
    //
    // SUBSEQUENT INSTANCES / DRAFTS RETURNED ARE FROZEN,
    // WITH OR WITHOUT THIS...
    Object.freeze(this);
  }

  // EXAMPLE WRAPPER METHODS (CLASS METHODS)...

  getHealthCheckData() {
    // EXAMPLE IMMERABLE RECORD .getIn METHOD USAGE...
    return this.getIn(
        [ 'healthCheckData' ]
      );
  };

  setHealthCheckData(data) {
    // EXAMPLE IMMERABLE RECORD .setIn METHOD USAGE...
    return this.setIn([ 'healthCheckData' ], data);
  };

  getHealthCheckDataRequestStatus() {
    return this.getIn(
        [ 'healthCheckDataRequestStatus' ]
      );
  };

  setHealthCheckDataRequestStatus(status) {
    return this.setIn([ 'healthCheckDataRequestStatus' ], status);
  };

  // EXAMPLE WRAPPER METHODS (ARROW FUNCTIONS)...

  getHealthCheckDataRequestStatusReason = () => {
    return this.getIn(
        [ 'healthCheckDataRequestStatusReason' ]
      );
  };

  setHealthCheckDataRequestStatusReason = (status, reason) => {
    return this.setIn(
        [ 'healthCheckDataRequestStatusReason' ],
        { [status]: reason }
      );
  };

  getHealthCheckDataUpdateTimestamp = () => {
    return this.getIn(
        [ 'healthCheckDataUpdateTimestamp' ]
      );
  }

  setHealthCheckDataUpdateTimestamp = () => {
    return this.setIn(
        [ 'healthCheckDataUpdateTimestamp' ],
        Date.now()
      );
  }

  resetHealthCheckDataUpdateTimestamp = () => {
    return this.setIn(
        [ 'healthCheckDataUpdateTimestamp' ],
        0
      );
  };
}

Object Extension

The following object usage is the object equivalent, to the above class example:

import { ImmerableRecord } from 'immerable-record';

import RequestStatus from 'constant/RequestStatus';

let healthCheckStore = new ImmerableRecord({
    healthCheckData: {},
    healthCheckDataRequestStatus: RequestStatus.UNINITIATED,
    healthCheckDataRequestStatusReason: {
        [RequestStatus.UNINITIATED]: RequestStatus.UNINITIATED
      },
    healthCheckDataUpdateTimestamp: 0

     // CONFIG OBJECT WITH historyLimit ENABLES HISTORY API...
  }, { historyLimit: 5 });

// EXAMPLE WRAPPER METHODS (STANDARD FUNCTIONS)...

healthCheckStore.getHealthCheckData = function() {
    // EXAMPLE IMMERABLE RECORD .getIn METHOD USAGE...
    return this.getIn(
        [ 'healthCheckData' ]
      );
  };

healthCheckStore.setHealthCheckData = function(data) {
    // EXAMPLE IMMERABLE RECORD .setIn METHOD USAGE...
    return this.setIn([ 'healthCheckData' ], data);
  };

healthCheckStore.getHealthCheckDataRequestStatus = function() {
    return this.getIn(
        [ 'healthCheckDataRequestStatus' ]
      );
  };

healthCheckStore.setHealthCheckDataRequestStatus = function(status) {
    return this.setIn([ 'healthCheckDataRequestStatus' ], status);
  };

// EXAMPLE GETTER WRAPPER METHODS (ARROW FUNCTION)...

healthCheckStore.getHealthCheckDataRequestStatusReason = () => {
    return healthCheckStore.getIn(
        [ 'healthCheckDataRequestStatusReason' ]
      );
  };

healthCheckStore.setHealthCheckDataRequestStatusReason = (status, reason) => {
    return healthCheckStore.setIn(
        [ 'healthCheckDataRequestStatusReason' ],
        { [status]: reason }
      );
  };

healthCheckStore.getHealthCheckDataUpdateTimestamp = function() {
    return healthCheckStore.getIn(
        [ 'healthCheckDataUpdateTimestamp' ],
        Date.now()
      );
  };

healthCheckStore.setHealthCheckDataUpdateTimestamp = function() {
    return healthCheckStore.setIn(
        [ 'healthCheckDataUpdateTimestamp' ],
        Date.now()
      );
  };

healthCheckStore.resetHealthCheckDataUpdateTimestamp = function() {
    return healthCheckStore.setIn(
        [ 'healthCheckDataUpdateTimestamp' ],
        0
      );
  };

// PREVENT REASSIGNMENT OR EXTENSION, OF THE INITIAL INSTANCE
// (or, use Object.seal, depending on your use case)
//
// SUBSEQUENT INSTANCES / DRAFTS RETURNED ARE FROZEN,
// WITH OR WITHOUT THIS...
Object.freeze(healthCheckStore);

Accessing Properties

Accessing properties of your Immerable Record instance can be done using vanilla JavaScript dot or square bracket notation.

Example (given the following data structure):

{
  parentKey: {
    childKey: {
      someKey: 'I am a nested value!',
      anotherKey: 'I am also a nested value!'
    }
  }
}

The following variable value will be: 'I am a nested value!'.

var someValue = parentKey.childKey.someKey;

The following variable value will be: 'I am also a nested value!'.

var anotherValue = parentKey.childKey['anotherKey'];

Alternatively, you can also use method: .getIn (alternatively, .getInArrIdx, to access from an array index).
The following variable value will be: 'I am a nested value!'.

var someValue = obj.getIn([
    'parentKey',
    'childKey',
    'someKey'
  ]);

Methods

Note: all available methods may be wrapped by extending classes or objects, as in the examples provided in the Quickstart section.)

Methods which return drafts (CRUD) can be chained.

.getIn(Array<String>[ keys ])

Returns a value from the field at key location, nested according to the provided keys arg. Each key traverses a level deeper, into the Record data structure.

Args (1):

  • keys (Array<String>, required): An array of keys, each of which traverses a level deeper, into the Record data structure.

Returns:
The value associated to the last key in the keys arg, after traversing into the Record data structure, according to the previous keys.

Example (given the following data structure):

{
  parentKey: {
    childKey: {
      nextKey: 'I am a nested value!'
    }
  }
}

.getIn([ 'parentKey', 'childKey', 'nextKey' ]) would return the value 'I am a nested value!'

.setIn(Array<String>[ keys ], ? value)

Sets the specified value on the field at key location. nested according to the provided keys arg. Each key traverses a level deeper, into the Record data structure.

Args (2):

  • keys (Array<String>, required): An array of keys, each of which traverses a level deeper, into the Record data structure.
  • value (?): the value to be assigned to the field associated to the last key in the keys arg.

Returns:
A new, immutable copy of the Record, with the updated field value.

Example (given the following data structure):

{
  parentKey: {
    childKey: {
      nextKey:
    }
  }
}

.setIn([ 'parentKey', 'childKey', 'nextKey' ], 2000) would set parentKey.childKey.nextKey = 2000, then return a new, immutable copy of the Record.

.deleteIn(Array<String>[ keys ])

Deletes the field at key location, nested according to the provided keys arg. Each key traverses a level deeper, into the Record data structure.

Args (1):

  • keys (Array<String>, required): An array of keys, each of which traverses a level deeper, into the Record data structure.

Returns:
A new, immutable copy of the Record, with the field at the keys location removed.

Example (given the following data structure):

{
  parentKey: {
    childKey: {
      nextKey: 'I am a nested value!'
    }
  }
}

.deleteIn([ 'parentKey', 'childKey', 'nextKey' ]) would remove field nextKey from the structure, then return a new copy of the Record, with data structure: parentKey.childKey

.getInArrIdx(Array<String>[ keys ], Number idx)

Returns the value at the array index, from the array field at key location, nested according to the provided keys arg. Each key traverses a level deeper, into the Record data structure.

Args (2):

  • keys (Array<String>, required): An array of keys, each of which traverses a level deeper, into the Record data structure.
  • idx (Number, required, returns undefined, if absent or out of bounds): The array index at which the returned value is located, in the array field associated to the last key in the keys arg.

Returns:
The value located at the index of the array field associated to the last key in the keys arg, after traversing into the Record data structure, according to the previous keys.

Example (given the following data structure):

{
  parentKey: {
    childKey: {
      nextKey: [ 1, 2, 'some value', 'foobar' ]
    }
  }
}

.getInArrIdx([ 'parentKey', 'childKey', 'nextKey' ], 2) would return the value 'some value'

.setInArrIdx(Array<String>[ keys ], Number idx, ? value)

Sets the specified value, at the array index, in the array field at key location. nested according to the provided keys array. Each key traverses a level deeper, into the Record data structure.

Args (3):

  • keys (Array<String>, required): An array of keys, each of which traverses a level deeper, into the Record data structure.
  • idx (Number, required, returns without error if absent or less than 0, adds to end (last) of array, if greater than array length): The array index at which the value is to be set, in the array field associated to the last key in the keys arg.
  • value (?): the value to be assigned at the array index of the array field associated to the last key in the keys arg.

Returns:
A new, immutable copy of the Record, with the updated array field, with the new value added at the specified index.

Example (given the following data structure):

{
  parentKey: {
    childKey: {
      nextKey: [ 1, 2, 'old value', 'foobar' ]
    }
  }
}

.setInArrIdx([ 'parentKey', 'childKey', 'nextKey' ], 2, 'a new value') would set parentKey.childKey.nextKey[2] = 'new value' (previously, 'old value'), then return a new, immutable copy of the Record.

.deleteInArrIdx(Array<String>[ keys ], Number idx)

Removes the value, at the array index field at key location, nested according to the provided keys array. Each key traverses a level deeper, into the Record data structure.

Args (2):

  • keys (Array<String>, required): An array of keys, each of which traverses a level deeper, into the Record data structure.
  • idx (Number, required, returns without error, if absent or out of bounds): The array index from which the value is to be removed, from the array field associated to the last key in the keys arg.

Returns:
A new, immutable copy of the Record, with the updated array field, with the value at the specified index removed.

Example (given the following data structure):

{ parentKey: {
    childKey: {
      nextKey:  [ 1, 2, 'old value', 'foobar' ]
    }
  }
}

.deleteInArrIdx([ 'parentKey', 'childKey', 'nextKey' ], 2) would remove old value from the structure, leaving parentKey.childKey.nextKey = [ 1, 2, 'foobar' ], then return a new copy of the Record.

.pushInArr(Array<String>[ keys ], ? value)

Adds the specified value to the end (last) of the array - the array field at key location. nested according to the provided keys array. Each key traverses a level deeper, into the Record data structure.

Args (2):

  • keys (Array<String>, required): An array of keys, each of which traverses a level deeper, into the Record data structure.
  • value (?): the value to be added top the end (last) of the array - the array field associated to the last key in the keys arg.

Returns:
A new, immutable copy of the Record, with the updated array field, with the new value added to the end (last).

Example (given the following data structure):

{
  parentKey: {
    childKey: {
      nextKey: [ 1, 2, 'some value', 'another value' ]
    }
  }
}

.pushInArr([ 'parentKey', 'childKey', 'nextKey' ], 'a new value') would add 'a new value', resulting in parentKey.childKey.nextKey = [ 1, 2, 'some value', 'another value', 'a new value' ] , then return a new, immutable copy of the Record.

.popInArr(Array<String>[ keys ])

Removes the specified value from the end (last) of the array - the array field at key location. nested according to the provided keys array. Each key traverses a level deeper, into the Record data structure.

Args (1):

  • keys (Array<String>, required): An array of keys, each of which traverses a level deeper, into the Record data structure.

Returns:
A new, immutable copy of the Record, with the updated array field, with the end (last) value removed.

Example (given the following data structure):

{
  parentKey: {
    childKey: {
      nextKey: [ 1, 2, 'some value', 'another value' ]
    }
  }
}

.popInArr([ 'parentKey', 'childKey', 'nextKey' ]) would remove 'another value', resulting in parentKey.childKey.nextKey = [ 1, 2, 'some value' ] , then return a new, immutable copy of the Record.

.unshiftInArr(Array<String>[ keys ], ? value)

Adds the specified value to the beginning (first) of the array - the array field at key location. nested according to the provided keys array. Each key traverses a level deeper, into the Record data structure.

Args (2):

  • keys (Array<String>, required): An array of keys, each of which traverses a level deeper, into the Record data structure.
  • value (?): the value to be added top the beginning (first) of the array - the array field associated to the last key in the keys arg.

Returns:
A new, immutable copy of the Record, with the updated array field, with the new value added to the beginning (first).

Example (given the following data structure):

{
  parentKey: {
    childKey: {
      nextKey: [ 1, 2, 'some value', 'another value' ]
    }
  }
}

.unshiftInArr([ 'parentKey', 'childKey', 'nextKey' ], 'a new value') would add 'a new value', resulting in parentKey.childKey.nextKey = [ 'a new value', 1, 2, 'some value', 'another value' ] , then return a new, immutable copy of the Record.

.shiftInArr(Array<String>[ keys ])

Removes the specified value from the beginning (first) of the array - the array field at key location. nested according to the provided keys array. Each key traverses a level deeper, into the Record data structure.

Args (1):

  • keys (Array<String>, required): An array of keys, each of which traverses a level deeper, into the Record data structure.

Returns:
A new, immutable copy of the Record, with the updated array field, with the beginning (first) value removed.

Example (given the following data structure):

{
  parentKey: {
    childKey: {
      nextKey: [ 1, 2, 'some value', 'another value' ]
    }
  }
}

.shiftInArr([ 'parentKey', 'childKey', 'nextKey' ]) would remove 1, resulting in parentKey.childKey.nextKey = [ 2, 'some value', 'another value' ] , then return a new, immutable copy of the Record.

History API

Every update made to an Immerable Record instance returns a new, immutable draft.
With the history API enabled, each draft is saved to history, and that history can be accessed from any draft revision.
All that is required, to enable history, is to provide a configuration object, with a historyLimit property greater than zero (0). When the number of drafts in history reaches the set limit, history will be cleared, before the next draft is added, so you will always have the most recent, up to your set limit.

With the following configuration, the 5 most recent drafts will be stored in history:

{ historyLimit: 5 }

History can then be accessed as a simple property, of any draft revision.
The following variable value will be an object containing the history drafts, along with timestamps, each indicating the timestamp of the creation of its respective draft.

var irHistory = obj.immerableRecordHistory;

The above variable, irHistory, with a hsitoryLimit of 5, will look like the following:

{
  0: {
      draft: Object { ... },
      timestamp: 1624445438549
    },
  1: {
      draft: Object { ... },
      timestamp: 1624445569463
    },
  2: {
      draft: Object { ... },
      timestamp: 1624445597180
    },
  3: {
      draft: Object { ... },
      timestamp: 1624445615310
    },
  4: {
      draft: Object { ... },
      timestamp: 1624445638760
    },
  5: {
      draft: Object { ... },
      timestamp: 1624445655949
    }
}

Advanced Topics

Building From Source

To begin, clone the github repository:

$ git clone https://github.com/antonio-malcolm/immerable-record.git

As of the latest update to this README, this project uses the following versions, of NodeJS, npm, and pnpm:

NodeJS: v14.17.1
npm: v6.14.13
pnpm: v6.9.1

To ensure you are using the current, correct versions, refer to the engines block, in package.json The author of this project uses nvm, to install and switch between NodeJS and npm versions, and doing so is highly recommended.

This project uses pnpm, to manage dependencies and workspaces, and it is also used to execute tasks, within the task management modules.
To install pnpm (current project version):

$ npm install -g pnpm@6.9.1

After installing pnpm, install the project dependencies:

$ pnpm install

After successful installation of the project dependencies, you may build any one or all of the development, production, or release builds. Each build is isolated, under directory: workspaces/immerable/build/dist/[dev, prod, release].

dev build:

$ pnpm run build:immerable:dev

prod build:

$ pnpm run build:immerable:prod

release build:

$ pnpm run build:immerable:release

Running Tests

To run all tests (as of the latest update to this README, all tests are unit tests):

$ pnpm run test:immerable

Aside from test console output, test reports are generated, for both results and coverage, which are accessible from your web browser.
After tests have been run, start the projects built-in server, and navigate to the following local URLs, in your web browser:

$ pnpm run start

http://localhost:3001/test/report/immerable/mochawesome/Test_Report_immerable-record.html
http://localhost:3001/test/report/immerable/nyc/index.html

Smoke Testing

This project comes with React components, Redux state, and REST utilities which are used to test Immerable Record in a real-world scenario.
In this scenario, Immerable Record is utilized, as the domain class, for a Redux store.

Smoke testing is performed with a live, "over-the-wire" data GET request. During the request cycle, several state changes are made, both to actively track the request status, and to store the response data.

To begin the test, build the release package (used as a smoke test dependency), and start the project's built-in server.

$ pnpm run build:immerable:release
$ pnpm run start

After successful build and server start, navigate to the following local URL, using your web browser:
http://localhost:3001/mmry/index.html

If the build and server start were successful, you should see the following request and response metrics, status, and data: "Smoke Test Status Data"

Cleanup

There are several options, for performing project cleanup.

To clean all workspace dependency, build, and test output directories:

$ pnpm run clean

To clean the immerable dependency, build, and test output directories:

$ pnpm run clean:immerable

To clean the immerable dependency directory:

$ pnpm run clean:immerable:dependency

To clean the immerable build directory:

$ pnpm run clean:immerable:build

To clean the test output directory:

$ pnpm run clean:test

Project Structure

Below is a general overview of the project structure, i.e., "where to find the relevant things".

Immerable Record source code:
workspaces/immerable/src/immerable/record/

Immerable Record build output (after build, you will find child directories dist/[dev, prod, release]):
workspaces/immerable/build/

Immerable Record test modules: test/spec/immerable/record/

Test output (generated after running tests) test/output/

Author And License Info

Immerable Record is authored by, and copyright 2021 to present, Antonio Malcolm.
All rights reserved.

Immerable Record (A.K.A., "ImmerableRecord", "immerableRecord", "Immerable Record", or "immerable record") is licensed under the BSD 3-Clause license, and is subject to the terms of the BSD 3-Clause license, found in the LICENSE file, in the root directory of this project. If a copy of the BSD 3-Clause license cannot be found, as part of this project, you can obtain one, at: https://opensource.org/licenses/BSD-3-Clause

Support This Project

This software is built with the greatest care and attention to detail, and thoroughly tested.
Any support is greatly appreciated!

"Donate: Buy Me A Coffee" "Donate: LiberaPay" "Donate: PayPal"

Package Sidebar

Install

npm i immerable-record

Weekly Downloads

0

Version

1.0.14

License

BSD-3-Clause

Unpacked Size

486 kB

Total Files

18

Last publish

Collaborators

  • antonio-malcolm