Framework
Strongly-typed JavaScript object with support for validation and error handling.
Install / Use
/learn @rawmodel/FrameworkREADME
Rawmodel Framework
Rawmodel is a strongly-typed JavaScript object with support for validation and error handling. It's a lightweight open source framework for the server and browser (using module bundler), written with TypeScript. It's actively maintained, well tested and already used in production environments. The source code is available on GitHub where you can also find our issue tracker.
Introduction
Rawmodel provides a mechanism for creating strongly-typed data objects with built-in logic for unified data validation and error handling. It has a simple and intuitive API and tends to be a powerful, magic-free, minimalistic and unopinionated framework for writing application data layers where you have a complete control. It could be a perfect fit when writing an Express.js action, GraphQL resolver or similar and it's easily extendable.
Installation
Run the command below to install the package.
$ npm install --save @rawmodel/core
$ npm install --save @rawmodel/handlers // OPTIONAL
$ npm install --save @rawmodel/parsers // OPTIONAL
$ npm install --save @rawmodel/schema // OPTIONAL
$ npm install --save @rawmodel/validators // OPTIONAL
This package uses promises thus you need to use Promise polyfill when promises are not supported.
Example
The code below shows a basic usage example.
import { Model, prop } from '@rawmodel/core';
// defining a basic model
class User extends Model {
@prop()
public name: string;
}
// usage example
const model = new User({
'name': 'John Smith',
});
model.name; // => 'John Smith'
Usage
Below we explain some of the most important features that this framework provides. Please check the API section to see a complete list of features.
Defining Props
Model properties are defined using the prop ES6 decorator. The code below is an example of a basic model class with a name property.
import { Model, prop } from '@rawmodel/core';
class User extends Model {
@prop()
public name: string;
}
const user = new User();
user.name = 'John Smith';
user.name; // -> "John Smith"
Type Casting
Each property has a built-in system for type casting, thus we can force a value to be automatically converted to a specific type when setting a value.
import { ParserKind } from '@rawmodel/core';
import { stringParser } from '@rawmodel/parsers';
class User extends Model {
@prop({
parser: {
resolver: stringParser(),
},
})
public name: string;
}
Common types are supported by default. A Model also represents a type and you can create your own parsers when needed. Please see the API section for further details.
Nested Models
As mentioned above, a model class is already a type. This way you can create complex nested structures by nesting models as shown in the example below.
import { Model, ParserKind, prop } from '@rawmodel/core';
class Address extends Model {
@prop()
public country: string;
}
class Friend extends Model {
@prop()
public name: string;
}
class User extends Model {
@prop({
parser: {
resolver: Address,
},
})
public address: Address;
@prop({
parser: {
array: true,
resolver: Friend,
},
})
public friends: Friend[];
}
Prop Default Value
We can set a defaultValue for each property which will automatically populate a property on creation.
The defaultValue can also be a method which returns a dynamic value. This function shares the context of the associated model.
@prop({
defaultValue() { return new Date() },
})
public now: string;
Prop Fake Value
Similar to default values, we can set a fakeValue for each property, to populate a property with fake data when calling the fake() method. This is useful when writting automated tests.
The fakeValue can also be a method which returns a dynamic value. This function shares the context of the associated model.
@prop({
fakeValue() { return new Date() },
})
public today: string;
Prop Empty Value
By default, all defined properties are set to null. Similar to default and fake values we can set an emptyValue option for each property, to automatically replace null values.
The emptyValue can also be a method which returns a dynamic value. This function shares the context of the associated model.
@prop({
emptyValue() { return '' },
})
public name: string;
Prop Value Transformation
A property can have a custom getter and a custom setter. This function shares the context of the associated model.
@prop({
getter(value) { return value },
setter(value) { return value },
})
public name: string;
Value Assignments
Model's properties are like properties of a Javascript Object. We can easily assign a value to a property through its setter method (e.g. model.name = 'value';). Instead of assigning properties one by one, we can use the populate() method to assign values to multiple enumerable properties.
model.populate({
'name': 'John Smith',
'age': 35,
});
We can allow only selected properties to be populated by using population strategies (e.g. useful when populating data received from a form).
class User extends Model {
@prop({
populatable: ['internal'], // list population strategy names
})
public id: string;
@prop({
populatable: ['input', 'internal'], // list population strategy names
})
public name: string;
}
const data = {
'id': 100,
'name': 'John Smith'
};
const user = new User();
user.populate(data); // -> { "id": 100, "name": "John Smith" }
user.populate(data, 'internal'); // -> { "id": 100, "name": "John Smith" }
user.serialize(data, 'input'); // -> { id: null, "name": "John Smith" }
Model properties also support dynamic data assignments. In translation, this means that we can populate a property using a function that shares the context of the associated model and is realized on property assignment.
user.name = () => 'Join';
It's encouraged to use the populate() method for assigning values unless you know how RawModel works in-depth. Adding items to an array through the native push method, directly assigning model instances and similar data manipulation can lead to strange effects.
Serialization & Filtering
Model provides useful methods for object serialization and filtering. All enumerable properties are serializable by default and are thus included in the result object returned by the serialize() method. We can customize the output and include or exclude properties for different situations by using serialization strategies.
class User extends Model {
@prop({
serializable: ['output'], // list serialization strategy names
})
public id: string;
@prop({
serializable: ['input', 'output'], // list serialization strategy names
})
public name: string;
}
const user = new User({
'id': 100,
'name': 'John Smith',
});
user.serialize(); // -> { "id": 100, "name": "John Smith" }
user.serialize('input'); // -> { "name": "John Smith" }
user.serialize('output'); // -> { "id": 100, "name": "John Smith" }
A model can also be serialized into an array by using the flatten() method. We can thus easily scroll through all model values in a loop. The method also supports strategies thus we can customize the output and include or exclude properties for different situations.
user.flatten(); // [{ path, value, prop }, ...]
user.flatten('input');
user.flatten('output');
Commits & Rollbacks
RawModel tracks changes for all properties and provides a mechanism for committing values and rollbacks.
class User extends Model {
@prop()
public name: string;
}
const user = new User();
user.name = 'Mandy Taylor'; // changing property's value
user.isChanged(); // -> true
user.commit(); // set `initialValue` of each property to the value of `value`
user.isChanged(); // -> false
user.name = 'Tina Fey'; // changing property's value
user.rollback(); // -> reset `value` of each property to its `initialValue` (last committed value)
Note that the commit method will memorize a serialized data and the rollback method will apply it back. Assigning functions or instances to properties is discourages.
Validation
RawModel provides a simple mechanism for validating properties. All validators shares the context of the associated model.
class User extends Model {
@prop({
validators: [ // property validation setup
{ // validator recipe
resolver(v) { return !!v }, // [required] validator function
code: 422, // [optional] error code
},
],
})
public name: string;
}
const user = new User();
user.validate().catch((err) => {
user.collectErrors(); // -> [{ path: ['name'], code: 422 }]
});
Error Handling
RawModel provides a mechanism for handling property-related errors. The logic is aligned with the validation thus the validation and error handling can easily be managed in a unified way. This is great because we always deal with validation errors and can thus directly send these errors back to a user in a unified format. All handlers shares the context of the associated model.
class User extends Model {
@prop({
handlers: [ // property error handling setup
{ // handler recipe
resolver(e) { return e.message === 'foo' }, // [required] error resolve function
code: 31000, // [optional] error code
},
],
})
public n
