1.0.0-beta.6 • Published 4 years ago

@rowanmanning/data-store v1.0.0-beta.6

Weekly downloads
8
License
MIT
Repository
github
Last release
4 years ago

@rowanmanning/data-store

Represent your data with model-like objects.

:warning: This is pre-release software, use in production at your own risk

Table of Contents

Requirements

This library requires the following to run:

Usage

Install with npm:

npm install @rowanmanning/data-store

Load the library into your code with a require call:

const DataStore = require('@rowanmanning/data-store');

Creating a data store

const banana = new DataStore({
    color: 'yellow',
    shape: 'curved',
    scientificName: 'Musa'
});

Getting properties

Use the get method to get values from the store:

banana.get('color'); // 'yellow'
banana.get('shape'); // 'curved'

Property names are normalised, so the following will all return the same data:

banana.get('scientificName');  // 'Musa'
banana.get('scientific_name'); // 'Musa'
banana.get('ScientificName');  // 'Musa'
banana.get('scientific-name'); // 'Musa'

If a getter is defined on the DataStore or an extending class, it will be run during the fetching of the matching property. Getters follow the pattern get<PropertyName>:

banana.getScientificName = () => {
    return `👩‍🔬🌱 ${this.data.scientificName}`;
};

banana.get('scientificName'); // '👩‍🔬🌱 Musa'

Setting properties

Use the set method to set values in the store:

banana.set('color', 'green');
banana.set('shape', 'straight');

or

banana.set({
    color: 'green',
    shape: 'straight'
});

Property names are also normalised in setting, so the following will all set the same property (scientificName):

banana.set('scientificName', 'Musa');
banana.set('scientific_name', 'Musa');
banana.set('ScientificName', 'Musa');
banana.set('scientific-name', 'Musa');

If a setter is defined on the DataStore or an extending class, it will be run during the setting of the matching property. Setters follow the pattern set<PropertyName>:

banana.setColor = (value) => {
    return this.data.color = value.trim().toLowerCase();
};

banana.set('color', '  GREEN  ');

Serializing the data store

You can get the data store represented as a simple object with key/value pairs like this:

banana.serialize(); // { color: 'yellow', shape: 'curved', ... }

Extending a data store

Data stores are more useful when they're extended to suit the shape of your data. You can specify property getters and setters as well as customise the way property names are normalised.

class Fruit extends DataStore {

    setColor(value) {
        if (!['red', 'green', 'blue', 'yellow'].includes(color)) {
            throw new Error('Invalid color');
        }
        return this.data.color = value;
    }

}

const banana = new Fruit({
    color: 'yellow',
    shape: 'curved'
});

Getters

Getters defined on an extended DataStore must be named in the format get<PropertyName>, where the property name is camel-case.

Getters receive no arguments, and must return the requested value or throw an error. A getter can use the get method to access existing properties or the data property on the instance.

Getters can be async functions, but none of the resolution logic caters for async functions. This means that calling .get with a property that has an async getter will always return a Promise.

class Fruit extends DataStore {

    // Using the `data` property
    getColor() {
        return this.data.color;
    }

    // Using the `get` method to create a "virtual" property
    getUpperCaseColor() {
        return this.get('color').toUpperCase();
    }

    // This is BAD and will result in an infinite loop
    getShape() {
        return this.get('shape');
    }

}

When using the data property, you bypass the property normalisation process. You can ensure that you're always getting the correct property name by using the .normalizePropertyForStorage static method:

class Fruit extends DataStore {

    getScientificName() {
        const normalizedName = Fruit.normalizePropertyForStorage('scientific-name');
        return this.data[normalizedName];
    }

}

Setters

Setters defined on an extended DataStore must be named in the format set<PropertyName>, where the property name is camel-case.

Setters receive one argument: the value that the property should be set to. They must return the set value or throw an error. A setter can use the set method to access existing properties or the data property on the instance.

Setters can be async functions, but none of the resolution logic caters for async functions. This means that calling .set with a property that has an async setter will not necessarily store the data immediately. This could result in race conditions.

class Fruit extends DataStore {

    // Using the `data` property
    setColor(value) {
        return this.data.color = value;
    }

    // Using the `set` method to create a "virtual" setter
    setUpperCaseColor(value) {
        return this.set('color', value.toLowerCase());
    }

    // This is BAD and will result in an infinite loop
    setShape(value) {
        return this.set('shape', value);
    }

}

When using the data property, you bypass the property normalisation process. You can ensure that you're always setting the correct property name by using the .normalizePropertyForStorage static method:

class Fruit extends DataStore {

    setScientificName(value) {
        const normalizedName = Fruit.normalizePropertyForStorage('scientific-name');
        return this.data[normalizedName] = value;
    }

}

Validators

Validators defined on an extended DataStore must be named in the format validate<PropertyName>, where the property name is camel-case.

Validators receive one argument: the value that the property is being set to. They must either return nothing or throw an error (ideally either DataStore.ValidationError, or using the built-in invalidate method).

Validators cannot be async functions – they will not function correctly if they are defined as asynchronous.

class Fruit extends DataStore {

    validateColor(value) {
        if (!['red', 'green', 'blue', 'yellow'].includes(color)) {
            throw new DataStore.ValidationError('Invalid color');
        }
    }

}

// Throws because color is invalid
const banana = new Fruit({
    color: 'yellowy-green'
});

The built-in invalidate method is a shortcut for creating validation errors. Validation errors have an optional second argument named details, which can be used to attach extra information to an error.

class Fruit extends DataStore {

    validateColor(value) {
        const validColors = ['red', 'green', 'blue', 'yellow'];
        if (!validColors.includes(color)) {
            this.invalidate('Invalid color', {
                given: value,
                expected: validColors
            });
        }
    }

}

Property normalisation

Property names are normalised when they're set on a DataStore instance, including during construction. The default normalization is to convert property names to camel-back case. E.g. scientific_name becomes scientificName.

This can be overridden in extending classes by reimplementing the normalizePropertyForStorage static method:

class Fruit extends DataStore {

    static normalizePropertyForStorage(property) {
        return property.toUpperCase();
    }

}

const banana = new Fruit({
    color: 'yellow'
});

banana.data.COLOR === 'yellow'; // true

Serialized property normalization

Property names can also be normalised when a DataStore instance is serialized. By default they are left in the same format as they are stored under (camel-back case by default).

You may wish to override this behaviour during serialization, e.g. for complying with database field names. This can be overridden in extending classes by reimplementing the normalizePropertyForSerialization static method:

const varname = require('varname'); // Or another library that changes variable names

class Fruit extends DataStore {

    static normalizePropertyForSerialization(property) {
        return varname.underscore(property);
    }

}

const banana = new Fruit({
    scientificName: 'yellow'
});

banana.serialize(); // { scientific_name: 'yellow' }

Allowed properties

The properties that are allowed to be set on a data store can be limited by specifying static properties on an extending class. The properties are allowedProperties and disallowedProperties, and they must be set to an array.

These allow/disallow lists are checked internally by the set method, and only normalised properties should be added to the list as properties are checked post-normalisation.

class Fruit extends DataStore {}

Fruit.allowedProperties = [
    'color',
    'shape'
];

// Throws because `requiresPeeling` is not an allowed property
const banana = new Fruit({
    color: 'yellow',
    requiresPeeling: true
});
class Fruit extends DataStore {}

Fruit.disallowedProperties = [
    'shape'
];

// Throws because `shape` is a disallowed property
const banana = new Fruit({
    color: 'yellow',
    shape: 'curved'
});

Contributing

To contribute to this library, clone this repo locally and commit your code on a separate branch. Please write unit tests for your code, and run the linter before opening a pull-request:

make test    # run all tests
make verify  # run all linters

License

Licensed under the MIT license. Copyright © 2019, Rowan Manning

1.0.0-beta.6

4 years ago

1.0.0-beta.5

5 years ago

1.0.0-beta.4

5 years ago

1.0.0-beta.3

5 years ago

1.0.0-beta.2

5 years ago

1.0.0-beta.1

5 years ago

1.0.0-alpha.1

5 years ago