next-model
v0.4.1
Published
Rails like models using ES6.
Downloads
15
Readme
NextModel
NextModel gives you the ability to:
- Represent models and their data.
- Represent associations between these models.
- Represent inheritance hierarchies through related models.
- Perform database operations in an object-oriented fashion.
- Uses Promises for database queries.
Roadmap / Where can i contribute
See GitHub project for current progress/tasks
Fix typos
Improve documentation
createdAt
andupdatedAt
timestampsPredefined and custom validations
Improve schema with eg. default values, limits
Improve associations eg. cascading deletions
Add more packages for eg. versioning and soft deleting
There are already some tests, but not every test case is covered.
Add more connectors for eg. graphQL and dynamoDB
includes
prefetches relations with two db queries (fetch records => pluck ids => fetch related records by ids) instead of one query per related model.User.includes({address: {}})
,Profile.includes({user: {address: {}}})
Add a solution to create Migrations
TOC
- Naming Conventions
- Example
- Model Instances
- Relations
- Searching
- Fetching
- Class Properties
- Instance Attributes
- Instance Callbacks
- Instance Actions
- Changelog
Naming Conventions
To keep the configuration as short as possible, its recommended to use the following conventions:
- Everything is in camel case.
createdAt
someOtherValue
- Classes should be singular start with an capital letter.
Address
Chat
ChatMessage
- Foreign keys are the class names starting with an lower case character and end with Id.
User
-userId
Chat
-chatId
ChatMessage
-chatMessageId
Example
// import
const NextModel = require('next-model');
// model definitions
User = class User extends NextModel {
static get modelName() {
return 'User';
}
static get schema() {
return {
id: { type: 'integer' },
firstName: { type: 'string' },
lastName: { type: 'string' },
gender: { type: 'string' },
};
}
static get hasMany() {
return {
addresses: { model: Address },
};
}
static get males() {
return this.scope({ where: { gender: 'male' }});
}
static get females() {
return this.scope({ where: { gender: 'female' }});
}
static get withFirstName(firstName) {
return this.scope({ where: { firstName }});
}
get name() {
return `${this.firstName} ${this.lastName}`;
}
};
Address = class Address extends NextModel {
static get modelName() {
return 'Address';
}
static get schema() {
return {
id: { type: 'integer' },
street: { type: 'string' },
};
}
static get belongsTo() {
return {
user: { model: User },
};
}
};
// Creating
user = User.build({ firstName: 'John', lastName: 'Doe' });
user.gender = 'male';
user.name === 'John Doe';
user.save().then(user => ... );
User.create({
firstName: 'John',
lastName: 'Doe',
gender: 'male',
}).then(user => ... );
user.addresses.create({
street: 'Bakerstr.'
}).then(address => ...);
// Searching
User.males.all.then(users => ... );
User.withFirstName('John').first(user => ... );
User.addresses.all.then(addresses => ... );
User.where({ lastName: 'Doe' }).all.then(users => ... );
User.males.order({ lastName: 'asc' }).all.then(users => ... );
Model Instances
Build
Initializes new record without saving it to the database.
address = Address.build({ street: '1st street' });
address.isNew === true;
Create
Returns a Promise
which returns the created record on success or the initialized if sth. goes wrong.
Address.create({
street: '1st street'
}).then(address => {
address.isNew === false;
}).catch(address => {
address.isNew === true;
});
From Scopes and queries
An record can be build
or create
d from scopes. These records are created with scope values as default.
address = user.addresses.build();
address.userId === user.id;
user = User.males.build();
user.gender === 'male';
user = User.withName('John').build();
user.name === 'John';
user = User.where({ gender: 'male'}).build();
user.gender === 'male';
Relations
Define the Model associations. Describe the relation between models to get predefined scopes and constructors.
Belongs To
Address = class Address extends NextModel {
static get belongsTo() {
return {
user: { model: User },
}
}
};
Address.create({
userId: id
}).then(address => {
return address.user;
}).then(user => {
user.id === id;
});
address = Address.build();
address.user = user;
address.userId === user.id;
Has Many
User = class User extends NextModel {
static get hasMany() {
return {
addresses: { model: Address },
}
}
};
user.addresses.all.then(addresses => ... );
user.addresses.create({ ... }).then(address => ... );
Has One
User = class User extends NextModel {
static get hasOne() {
return {
address: { model: Address },
}
}
};
user.address.then(address => ... );
Searching
Where
Special query syntax is dependent on used connector. But all connectors and the cache supports basic attribute filtering.
User.where({ gender: 'male' });
User.where({ age: 21 });
User.where({ name: 'John', gender: 'male' });
User.males.where({ name: 'John' });
Order
The fetched data can be sorted before fetching then. The order
function takes an object with property names as keys and the sort direction as value. Valid values are asc
and desc
.
User.order({ name: 'asc' });
User.order({ name: 'desc' });
User.order({ name: 'asc', age: 'desc' });
User.males.order({ name: 'asc' });
Scopes
Scopes are predefined search queries on a Model.
User = class User extends NextModel {
static get males() {
return this.scope({ where: { gender: 'male' }});
}
static get females() {
return this.scope({ where: { gender: 'female' }});
}
static get withName(name) {
return this.scope({ where: { name }});
}
};
Now you can use these scopes to search/filter records.
User.males;
User.withName('John');
Scopes can be chained with other scopes or search queries.
User.males.withName('John');
User.withName('John').where({ gender: 'transgender' });
Build from scope
profile = User.males.build();
profile.gender === 'male';
Scope chaining
User.males.young;
User.males.young.where({});
Fetching
If you want to read the data of the samples of the previous section you can fetch if with the following functions. Each fetching function will return a Promise
to read the data.
All
Returns all data of the query. Results can be limited by skip and limit.
User.all.then(users => ...);
User.males.all.then(users => ...);
User.where({ name: 'John' }).all.then(users => ...);
First
Returns the first record which matches the query. Use order to sort matching records before fetching the first one.
User.first.then(user => ...);
User.males.first.then(user => ...);
User.where({ name: 'John' }).first.then(user => ...);
User.order({ name: 'asc' }).first.then(user => ...);
Last
Returns last matching record. Needs to have an order set to work properly.
User.last.then(user => ...);
User.males.last.then(user => ...);
User.where({ name: 'John' }).last.then(user => ...);
User.order({ name: 'asc' }).last.then(user => ...);
Count
Returns the count of the matching records. Ignores order, skip and limit and always returns complete count of matching records.
User.count.then(count => ...);
User.males.count.then(count => ...);
User.where({ name: 'John' }).count.then(count => ...);
User.count === User.limit(5).count
Class Properties
Model Name
The model name needs to be defined for every model. The name should be singular camelcase, starting with an uppercase char.
class User extends NextModel {
static get modelName() {
return 'User';
}
}
class UserAddress extends NextModel {
static get modelName() {
return 'UserAddress';
}
}
Schema
A schema describes all (database stored) properties. Foreign keys from relations like belongsTo are automatically added to the schema. The existing types and their names are depending on the used Database connector.
class User extends NextModel {
static get schema() {
return {
id: { type: 'integer' },
name: { type: 'string' },
};
}
}
Connector
A connector is the bridge between models and the database.
Available connectors:
- knex (mySQL, postgres, sqlite3, ...)
- local-storage (Client side for Browser usage)
The connector needs to be returned as static getter.
Connector = require('next-model-knex-connector');
connector = new Connector(options);
class User extends NextModel {
static get connector() {
return connector;
}
}
Define an base model with connector to prevent adding connector to all Models.
class BaseModel extends NextModel {
static get connector() {
return connector;
}
}
class User extends BaseModel {
...
}
class Address extends BaseModel {
...
}
Identifier
Defines the name of the primary key. Default values is id
.
class User extends BaseModel {
static get identifier() {
return 'key';
}
}
You also need to define your identifier on the schema.
class User extends BaseModel {
static get identifier() {
return 'uid';
}
static get schema() {
return {
id: { type: 'uuid' },
...
};
}
}
Table Name
The table name is usually generated from modelName. Default without connector is camelcase plural, starting with lowercase char.
class User extends NextModel {
static get modelName() {
return 'User';
}
}
User.tableName === 'users';
class UserAddress extends NextModel {
static get modelName() {
return 'UserAddress';
}
}
UserAddress.tableName === 'userAddresses';
Table name generation is depending on the used connector. The following examples are for knex connector.
class User extends NextModel {
static get modelName() {
return 'User';
}
}
User.tableName === 'users';
class UserAddress extends NextModel {
static get modelName() {
return 'UserAddress';
}
}
UserAddress.tableName === 'user_addresses';
You can also manually define an custom table name.
class User extends NextModel {
static get modelName() {
return 'app_users';
}
}
The table name could also be a function.
class BaseModel extends NextModel {
static get modelName() {
return 'app_' + underscore(this.modelName);
}
}
class User extends BaseModel { ... }
User.tableName === 'app_users';
class UserAddress extends BaseModel { ... }
UserAddress.tableName === 'app_user_addresses';
Accessors
Accessors define properties which can be passed to build, create functions or assignments, but are not passed to the database. Use them to store temporary data like passing values to model but not to database layer.
class User extends NextModel {
static get accessors {
return [
'checkForConflicts',
];
}
}
user = User.build({ checkForConflicts: true });
user.checkForConflicts === true;
user = User.build({ foo: 'bar' });
user.foo === undefined;
Cache
Caching is currently only working when the cache is manually filled. All searching and fetching are supported.
User.cache = [
{ gender: 'male', name: 'John' },
{ gender: 'male', name: 'Doe' },
];
User.first.then(user => user.name === 'John');
User.order({ name: 'asc' }).first.then(user => user.name === 'Doe');
User.where( ... );
Cache is cleared when you fetch an base model out of an class, or when you unscope or reload
the data. But cache is only cleared in this scope, the model itself is not modified.
User.cache = [ ... ];
User.model.cache === undefined;
User.reload.cache === undefined;
User.cache === [ ... ];
Default Scope
Adds an default scope for all queries made on the model. You need to unscope the property to search without the default scope.
class User extends NextModel {
static get defaultScope {
return {
deletedAt: null,
};
}
}
User.first.then(user => user.deletedAt === null);
User.unscope('deletedAt').where( ... );
Default Order
Adds an default Order to all queries unless its overwritten.
class User extends NextModel {
static get defaultOrder {
return {
name: 'asc',
};
}
}
Instance Attributes
isNew
An record is new unless the record is saved to the database. NextModel checks if the identifier property is set for this attribute.
address = Address.build();
address.isNew === true;
address.save().then(address => {
address.isNew === false;
});
isPersistent
The opposite of isNew. Returns false unless the record is not saved to the database.
address = Address.build();
address.isPersistent === false;
address.save().then(address => {
address.isPersistent === true;
});
attributes
Returns an object which contains all properties defined by schema and attrAccessors.
class Address extends NextModel {
static get schema() {
return {
street: { type: 'string' },
city: { type: 'string' },
};
}
static get attrAccessors() {
return ['fetchGeoCoord'];
}
}
address = Address.build({
street: '1st street',
city: 'New York',
fetchGeoCoord: false,
});
address.foo = 'bar';
address.attributes() === {
street: '1st street',
city: 'New York',
fetchGeoCoord: false
};
databaseAttributes
Returns an object which contains all properties defined only by schema.
class Address extends NextModel {
static get schema() {
return {
street: { type: 'string' },
city: { type: 'string' },
};
}
static get attrAccessors() {
return ['fetchGeoCoord'];
}
}
address = Address.build({
street: '1st street',
city: 'New York',
fetchGeoCoord: false,
});
address.foo = 'bar';
address.databaseAttributes() === {
street: '1st street',
city: 'New York',
};
isChanged
When you change a fresh build or created Class instance this property changes to true.
address = Address.build({
street: '1st street',
city: 'New York',
});
address.isChanged === false;
address.street = '2nd street';
address.isChanged === true;
This property does not change when the value is same after assignment.
address = Address.build({
street: '1st street',
city: 'New York',
});
address.isChanged === false;
address.street = '1st street';
address.isChanged === false;
changes
The changes
property contains an Array
of changes per property which has changed. Each entry contains an from
and to
property.
address = Address.build({
street: '1st street',
city: 'New York',
});
address.changes === {};
address.street = '2nd street';
address.changes === {
street: [
{ from: '1st street', to: '2nd street' },
],
};
address.street = '3rd street';
address.changes === {
street: [
{ from: '1st street', to: '2nd street' },
{ from: '2nd street', to: '3rd street' },
],
};
This property does not change when the value is same after assignment.
address = Address.build({
street: '1st street',
city: 'New York',
});
address.changes === {};
address.street = '1st street';
address.changes === {};
Custom Attributes
class User extends NextModel {
static get schema() {
return {
firstname: {type: String},
lastname: {type: String}
}
}
get name() {
return `${this.firstName} ${this.lastName}`;
}
}
user = User.build({
firstname: 'Foo',
lastname: 'Bar'
});
user.name === 'Foo Bar';
Instance Callbacks
With callbacks you can run code before or after an action. Actions which currently supports callbacks are save
. Callbacks are always named before{Action}
and after{Action}
. Callbacks can be defined in different ways. Callbacks can be functions, redirects or arrays.
Note: Actions which are Promises also support Promises as callbacks.
Callback can be a function:
class User extends NextModel {
beforeSave() { ... }
}
Callback can return a function:
class User extends NextModel {
get beforeSave() {
return function() { ... }
}
}
Callback can redirect to a function with a string:
class User extends NextModel {
get beforeSave() {
return 'prefillDefaults';
}
prefillDefaults() { ... }
}
Callback be an array of Strings:
class User extends NextModel {
get beforeSave() {
return ['prefillDefaults', 'setTimestamps'];
}
prefillDefaults() { ... }
setTimestamps() { ... }
}
Callback be an array of mix between functions and redirects:
class User extends NextModel {
get beforeSave() {
return ['prefillDefaults', function() { ... }];
}
prefillDefaults() { ... }
}
Callback follow also multiple redirects and mixed arrays:
class User extends NextModel {
get beforeSave() {
return ['prefillActions', function() { ... }];
}
get prefillActions() {
return ['prefillDefaults', 'setTimestamps']
}
prefillDefaults() { ... }
setTimestamps() { ... }
}
Before Actions are always all executed. If any callback before the action runs on an Error the Action will not be executed. If the Action runs on an Error the after callbacks will not be executed.
Note: The execution order of callbacks can not be guaranteed, they run in parallel if possible.
Platform Specific Callbacks
NextModel can be used with Browser and Node.js. When this package is used on Server and Client side it might be useful to have different callbacks on Server and Client. Each callback can be postfixed with Client
or Server
to use this callback just on Server or Client.
Use generic callback to run it on both Platforms.
class User extends NextModel {
get beforeSave() {
...
}
}
Postfix callback with Server
to just use this callback when running with Node.js.
class User extends NextModel {
get beforeSaveServer() {
...
}
}
Postfix callback with Client
to just use this callback when running within Browser.
class User extends NextModel {
get beforeSaveClient() {
...
}
}
Change Callbacks
There is an global afterChange
callback and one additional per property named after${propertyName}Change
.
Even on multiple assignments with one call, afterChange
is called for every single property changed.
class User extends NextModel {
static afterChange() {
...
}
static afterNameChange() {
...
}
}
Build Callbacks
When promiseBuild is triggered the callback order is:
beforeBuild
-> promiseBuild
-> afterBuild
class User extends NextModel {
static beforeBuild() {
...
}
static afterBuild() {
...
}
}
Create Callbacks
When create is triggered the callback order is:
beforeCreate
-> beforeBuild
-> promiseBuild
-> afterBuild
-> beforeSave
-> save
-> afterSave
-> afterCreate
class User extends NextModel {
static beforeCreate() {
...
}
static afterCreate() {
...
}
}
Save Callbacks
When save is triggered the callback order is:
beforeSave
-> save
-> afterSave
class User extends NextModel {
beforeSave() {
...
}
afterSave() {
...
}
}
Delete Callbacks
When delete is triggered the callback order is:
beforeDelete
-> delete
-> afterDelete
class User extends NextModel {
beforeDelete() {
...
}
afterDelete() {
...
}
}
Instance Actions
assign
You can assign a new value to an schema or accessor defined property. This does not automatically save the data to the database.
address.assignAttribute('street', '1st Street');
address.assign({
street: '1st Street',
city: 'New York',
});
save
Saves the record to database. Returns a Promise
with the created record including its newly created id. An already existing record gets updated.
address = Address.build({street: '1st street'});
address.save().then(
(address) => address.isNew === false;
).catch(
(address) => address.isNew === true;
);
address.street = 'changed';
address.save().then( ... );
delete
Removes the record from database. Returns a Promise
with the deleted record.
address.isNew === false;
address.delete().then(
(address) => address.isNew === true;
).catch(
(address) => address.isNew === false;
);
reload
Fetches all schema properties new from database. All other values stay untouched. Returns a Promise
with the reloaded record.
address.isNew === false;
address.street = 'changed';
address.notAnDatabaseColumn = 'foo';
address.reload().then((address) => {
address.name === '1st Street';
address.notAnDatabaseColumn === 'foo';
});
Changelog
See history for more details.
0.0.1
2017-01-23 Initial commit with query and scoping functions0.0.2
2017-02-05 Published knex connector0.0.3
2017-02-12 Added CI0.0.4
2017-02-16 Added callbacks forbuild
,create
,save
anddelete
0.1.0
2017-02-23 Added Browser compatibility0.2.0
2017-02-25 Improved browser compatibility0.3.0
2017-02-27 Tracked property changes0.4.0
2017-02-28 Added platform specific callbacks0.4.1
2017-04-05 Bugfix: before and after callback