Agregate
A "missing piece" of DB clients for Node.JS, accenting on familiar JS experience, developer's freedom and simplicity of usage
Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. Donald Knuth
disclaimer: this software is alpha stage and is not intended to be used in heavy production projects. I am is not responsible for data loss and corruption, lunar eclipses and dead kittens.
Enviroment and preparations
Agregate's only back-end for now is neo4j (v2 and v3 beta are supported) for now. You can install it or request a free SaaS sandbox trial.
This (es6, es2015, plus decorators, static class properties and bind operator) babel preset is recommended to be used for the best experience. However, library is shipped with compiled to ES5 files by default. Note: bind instance property expressions work only in babel 6.5+ plugins, older versions will throw an error due to already removed bug in babel
Familiar JS experience
So, you need User class reflecting DB "table". You simply write
{} const user = name: 'foo'usersurname = 'bar'user //=> User {name: 'foo', surname: 'bar'}
No factories, complex configs and CLI tools. Every enumerable property (except relations, but it will be explained later) is reflectable into DB, back and forth.
Developer's freedom
Common DB lib usually requires you to keep specific file structure and/or using CLI tools and/or remember hundreds of methods, properties and signatures. Agregate was aimed to keep Minimal API Surface Area. Agregate API is fully promise-based, Relation is trying to mimic Set API, and Record instance has just 2 core methods - Record#save and Record#delete, whose API is obvious.
Simplicity of usage
The whole declaration of class would be something like:
const Connection Record = //class name will be used as "table name". You can overload it with static "label" property //indexes are optional static properties //which are used only for making DB query 'CREATE INDEX' during register() call. static indexes = 'foo' 'bar' //for now agregate is backed by npmjs.com/package/neo4j, //so Connection constructor is just proxying everything up to that package. //You can usually just use URL string syntax. //static properties are inheritable, so you only need to declare in once in parent class static connection = 'http://neo4j:password@localhost:7474';//As we cannot have a callbacks on class constructor, (at least without crazy hacks) //explicit .register() call is required for any concrete classEntry
Wait, but I need relations
OK, let's add relations.
const Connection Record Relation = //we assume that we already made something like Record.connection = ... //signature of constructor is //(source: Record|Relation, label: string[, {target?: RecordClass, direction?: number = 1}]) subjects = this 'relation'; //target is limitation of relation to one record group, and direction is, well, direction. //Direction is 1 by default, which is '->' relation. -1 relation is '<-'. //It means there can be 2 relations with same label in different directions. //0-relation is plain '-', there can be only 1 relation of this type. subjects = this 'relation' target: Object direction: -1; RecordObjectRecordSubject { const object = await foo: 'bar' const subject = await await objectsubjects console // => 1 const objects = await subjectobjects console //bar}
Deep relations are extremely simple:
roles = this 'has_role' target: Role; permissions = thisroles 'has_permission' target: Permission; hasPermission = ::thispermissionshas
Relation instances have bunch of pretty methods to use:
//overloaded method to implement one-to-one relation async : Record async : void //just for you to know: signature of Record.where and Relation#where are 100% same async : Array<Record> //only non-familiar method. Returs intersection of relation and passed set async : Array<Record> //this part mimics es6 Set class async : void async : void async deleterecords: Array<Record>: void async : Array<Record> async : bool //note: size is not property, but async method async : number
Auto-generated record properties
all of the properties are non-enumerable, non-configurable and exists only for reflected record.
- uuid - automatically generated on creation
- createdAt - automatically generated on creation
- updatedAt - automatically generated on creation and update
OK, but how can I make complex queries?
Record.where and Relation#where methods are provided for querying.
All details are provided in API page, in brief - order, limit, offset can be used for filtering. Equality, existence, numeric (greater/less), string (starts/ends with, contains), array (contains/includes) queries are provided.
Examples:
EntryEntryEntry // here e.g. {foo: [1,2,3,4,5], bar: 3} will be reflected.// $has stands for "db record has fields", $in - for "db record is in list of possible fields"Entry //$in can also work with arrayEntry
Hooks?
beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDestroy, afterDestroy are available hooks
async { //this.connection points to transaction during the transaction, so you have to pass it if calling other classes const test = await Test thistestId = testtestId }
Transactions and atomicity?
Yes, Agregate has transactions.
Hooks (see above) are always ran inside a transaction (transaction is same for pre-hook, operation itself and post-hook).
Decorator @acceptsTransaction({force: true}) can be used (with babel-plugin-transform-decorators-legacy, will be changed to new syntax when new spec will become stable), or transaction can be constructed explicitly by connection.transaction()
All transactions should be committed or rolled back. Decorator commits everything automatically on success, rolls back on error.
On SIGINT Agregate will attempt to rollback all not closed yet transactions. By default Neo4j rolls back transactions in 60 seconds after last query.
Good example of transaction usage is provided Record.firstOrCreate sugar-ish method:
@acceptsTransaction static async { const tx = thisconnection let result = await this if !result result = await await tx return result }
Roadmap
- sort
- offset and limit
- has
- deep relations
- indexes
- rich Record.where query syntax ($lt, $gt, $lte, $gte, $has, $in and so on)
- one-to-one relations
- relations validation
- total test coverage
- performance optimisations
- optimistic and pessimistic locks?
- tests for transaction usage
- tests for eventEmitter