The SQL store is a wrapper over sequelize that provides a more user friendly interface of defining entity models, automatically handling CRUD actions on an entity, with paginated find.
npm i --save thorin-store-sql@1.x
'use strict'; // app.js entry file const thorin = require('thorin'); thorin.addStore(require('thorin-store-sql')); // <- add this line thorin.run((err) => {});
# run to setup the database and module node app.js --setup=store.sql # On each database reset, use the above command
'use strict';
const storeObj = thorin.store('sql'),
Sequelize = storeObj.getSequelize(); // returns the exported object from require('sequelize');
'use strict';
const storeObj = thorin.store('sql'),
seqObj = storeObj.getInstance(); // returns the equivallent of new (require('sequelize'))()
--setup=store.sql
or --setup=all
, the SQL store will
reset the database structure, essentially performing a DROP CREATE on all your models. sync()
function to re-create tablesrun()
function of the store is called.
storeObj.addModel('/path/to/my/model.js')
the store will require() the path and load it up (see below the loading procedure)storeObj.addModel({name, code, fullPath})
use the name and code as the model's properties, and use the fullPath as the model's absolute path'use strinct';
function buildModel(modelObj, Seq) {
modelObj
.field('id', Seq.PRIMARY);
// other model settings
}
thorin.store('sql').addModel(buildModel, {
code: 'myModelCode', // the model code
name: 'my_model_code' // the table name
});
transaction
object.
'use strict';
const storeObj = thorin.store('sql');
storeObj.transaction((t) => {
const calls = [];
let accObj;
calls.push(() => {
return storeObj.model('account').find({
where: {id: 1},
transaction: t
}).then((aObj) => {accObj = aObj});
});
calls.push((stop) => {
if(!accObj) return stop(thorin.error('ACCOUNT.NOT_FOUND');
return accObj.update({is_active: true}, { transaction: t })
});
return thorin.series(calls); // returns a promise
}).then((res) => {
// transaction committed.
}).catch((err) => {
// transaction rolled back.
});
You use a store model to define the fields, indexes, static functions and json representation of your tables.
Store models are wrapper over Sequelize's models, used for auto-loading dependencies and relations. By default,
a model's toJSON will return all the fields under the dataValues
property of the Sequelize
model.In most cases, your application's models will be under the app/models folder and export a function that will be called when
by the thorin store, when loading up and defining your models.
The default conventions for when defining your models are:
created_at
that will always hold the creation date of every entry.
type
must be
a Sequelize type and the opt
can contain additional field-specific options.
Seq.PRIMARY
shorthand for Seq.INTEGER with autoIncrement, primaryKey
set to true
Seq.UUID
shorthand for Seq.STRING(50) that generates a random key, using uuid for its defaultValue
and automatically adds an index to your field.// Field calls can be chained.
modelObj
.field('id', Seq.PRIMARY)
.field('uuid', Seq.UUID)
.field('name', Seq.STRING(25), {
defaultValue: 'JohnDow'
})
.field('type', Seq.ENUM('one','two'))
.field('arrow', Seq.STRING, {
allowNull: true
});
toJSON
function that will be called when the model's toJSON function is called by a
transport layer, or performing JSON.stringify() A model might contain multiple json functions, differentiated by their name (see below)
this
context, you must not use an arrow function.
// The JSON representation when calling instanceObj.toJSON()
modelObj.json(function() {
return {
id: this.id,
name: this.name
};
});
// The JSON representation when calling instanceObj.toJSON('specific');
modelObj.json('specific', function(){
return {
id: this.id,
name: this.name,
specific: true
};
});
// Attach a static function
modelObj.static(function doStatic() {
log.info('Do static stuff');
});
// Attach a named static function
modelObj.static('doStaticTwo', () => {
log.info('Do two static stuffs');
});
// Attach an object
modelObj.static('TYPE', {
SOME_KIND_OF: 'STATIC.VALUES'
});
// Somewhere else in your application
const store = thorin.store('sql'),
myModel = store.model('myModel');
myModel.doStatic(); // call or use the static functions
myModel.TYPE; // contains the object.
static()
modelObj.error('SOME_CODE', 'Some error message', 400);
modelObj.error(thorin.error('SOME.ERROR', 'Some message', 405);
instance
of your model.// Attach the function to the instance of a model.
modelObj.method(function doSomething(){
log.info('Do something for model ' + this.id); // "this" is the model's instance scope.
});
// Somewhere in your app
const Model = thorin.store('sql').model('myModel');
let iObj = Model.build({id: 2});
iObj.doSomething(); // call the function of the model instance.
modelObj.index('my_field'); // simple index
modelObj.index(['field_one', 'field_two'], { // multiple fields in a unique index.
unique: true
});
// Check if we have a name.
// The validate function is synchronous, and errors thrown here will be returned by the transport.
modelObj.validate(function() {
if(!this.name || this.name.length < 3) throw thorin.error('INVALID_NAME', 'Please enter a valid name');
});
You can define relationships between your models by using the functions below. The store takes care of model loading, so you do not have to keep any kind of variable references. We use the model's code as the reference.
When an association is created, some default options are created:
onDelete: 'CASCADE'
- when associated entry is deleted, delete this one as well
onUpdate: 'CASCADE'
- when associated entry is updated, update the key as well.
foreignKey: 'target_model_id'
- the model's table_name with the primary key suffix, usually _id
For a more in-detail view on these associations, see docs.
A full example of a model definition file can be viewed below.
'use strict'; // File: app/models/account.js module.exports = function(modelObj, Seq) { // modelObj is already created by the store, having the code set to account and tableName to account modelObj .field('id', Seq.PRIMARY) .field('name', Seq.STRING(30)) .field('password', Seq.STRING(200)) // hashed version .field('image_url', Seq.STRING(300), { defaultValue: null // this will automatically add allowNull: true }); modelObj .method(function sayHi() { log.info(`${this.name} says hi!`); }) .json(function() { let d = { id: this.id, name: this.name, created_at: this.created_at // auto-generated field. }; if(this.image_url) d.image_url = this.image_url; return d; }) .error('NOT_FOUND', 'The account was not found', 404); modelObj .hasMany('application', { as: 'applications' }); }
'use strict'; // File: app/models/application.js module.exports = (modelObj, Seq) => { modelObj .field('id', Seq.PRIMARY) .field('name', Seq.STRING) modelObj .belongsTo('account'); };
The SQL Store offers auto-generated CRUDF functionality in the form of actions. This means that
in stead of you manually doing a findOne() or findAll() + count() on a model, you can just use
storeObj.crudify()
and an action with input validation based on your model definition
will be registered in the dispatcher.
The process is simple, you just have to call storeObj.crudify()
and it will return a generated
action that you can extend and inject code in various steps along the way. The best way to inject code before
the crudify function is to attach an action template to the generated crudify action. Note that filter
triggering is synchronous, any error thrown inside the filter callback will stop the request.
All model instances that are generated or altered by a crudify method will have a fromCrudify=true
property
attached to them.
Each crudify method will trigger specific filters
in various points in time, during the lifecycle of the request.
Using these filters enable you to alter incoming, outgoing or generated model instances.
'use strict';
const storeObj = thorin.store('sql');
storeObj
.crudify('modelName', 'read', {
namespace: 'some.namespace.to.attach',
name: 'account' // will generate an action called: some.namespace.to.attach.account.read
})
.filter('read.before', (intentObj, query) => {
// alter the read query to show only active modelName
query.where.is_active = true;
});
Handles the CREATE action of a store model. It will look for all the model fields
and require them via the input()
function of the generated action. By default,
it will also create an alias POST /{namespace}/{modelName}
.
result
and ready to finalize the action.
'use strict';
const store = thorin.store('sql');
store
.crudify('application', 'create')
.template('session.account')
.use('application.canCreate')
.use((intentObj, next) => {
if(intentObj.session.account) {
intentObj.input('account_id', intentObj.session.account);
}
log.info(`Creating a new application`);
next();
})
.filter('create.before', (intentObj, appObj) => {
// override any is_administrative fields in the form.
appObj.set('is_administrative', false);
})
.filter('create.after', (intentObj, appObj) => {
log.info(`Application ${appObj.id} created`);
});
Handles the READ action of a store model. It essentially builds an action that will perform a SELECT
or findOne()
a model based on the incoming primary key (eg, id) and limit the results to one. By default, it will also
create an alias GET /{namespace}/{modelName}/:{primaryKey}
.
result
and ready to finalize the action.
Handles the FIND action of a store model. It essentially builds an action that will perform a SELECT
or findAll()
on a model, including filtering by public fields of a model. The default functionality includes sorting, pagination and field filtering
LIMIT
statement.
When the action is generated, specific input fields will be injected by the crudification. These will allow you to perform the above mentioned actions. Any public model field will also be placed as an input and can filter the results.
ORDER BY
statement. Values are: asc, desc, default to ascLIMIT
statement, defaults to 10LIMIT
statement to skip x entities and return the next data set.The find crudified action will also generate some metadata that can be easily used for pagination. The below fields are placed under the meta field in the resulting JSON
result
and ready to finalize the action.
Handles the UPDATE action of a store model. It essentially builds an action that will perform an UPDATE
statement on a model instance.
This functionality only works for models that have exactly one primary key defined.
The action will register all the accessible fields of the model as input requirements, using their associated field type.
result
and ready to finalize the action.
Handles the DELETE action of a store model. It essentially builds an action that will perform a DELETE
statement on a model instance.
This functionality only works for models that have exactly one primary key defined.
The action will register the model's primary key name as a required input with its associated type and use it to search for the entity we want to delete.
If the entity instance has a canDelete()
method attached, we will call it before destroying the entry. If it returns a falsy value, we will not delete it.
You can always create a new issue on GitHub or contact one of the core founders by chat.