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
class User extends Record {}
const user = new User({name: 'foo'})
user.surname = 'bar'
user.save()
.then(() => User.where({name: 'foo'}))
.then(([user]) => console.log(user))
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} = require('agregate')
class Entry extends Record {
static indexes = new Set('foo', 'bar')
static connection = new Connection('http://neo4j:password@localhost:7474');
}
Entry.register()
Wait, but I need relations
OK, let's add relations.
const {Connection, Record, Relation} = require('agregate')
class RecordObject extends Record {
subjects = new Relation(this, 'relation');
}
class RecordSubject extends Record {
subjects = new Relation(this, 'relation', {target: Object, direction: -1});
}
RecordObject.register()
RecordSubject.register()
async function main() {
const object = await new RecordObject({foo: 'bar'}).save()
const subject = await new RecordSubject().save()
await object.subjects.add(subject)
console.log(await subject.objects.size())
const objects = await subject.objects.entries()
console.log(objects[0].foo) =>
}
Deep relations are extremely simple:
import Role from './role'
import Permission from './permission'
export default class User extends ConnectedRecord {
roles = new Relation(this, 'has_role', {target: Role});
permissions = new Relation(this.roles, 'has_permission', {target: Permission});
hasPermission = ::this.permissions.has
}
Relation instances have bunch of pretty methods to use:
class Relation {
async only(): Record
async only(null | Record): void
async where(params?: WhereParams, opts?: WhereOpts): Array<Record>
async intersect(records: Array<Record>): Array<Record>
async add(records: Array<Record>): void
async clear(): void
async delete(records: Array<Record>): void
async entries(): Array<Record>
async has(records: Array<Record>): bool
async size(): 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:
Entry.where({foo: 1000}, {limit: 10, offset: 10, order: 'createdAt'})
Entry.where({updatedAt: {$gte: Date.now() - 1000}}, {order: ['createdAt DESC']})
Entry.where({foo: {$exists: true, $startsWith: ['b', 'ba', 'baz'], $endsWith: 'bar', $contains: 'z'}})
Entry.where({foo: {$has: [1,2,3]}, bar: {$in: [1,2,3]}})
Entry.where({foo: {$in: [[0], [1,2,3,4,5]]}}})
Hooks?
beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDestroy, afterDestroy are available hooks
class Entry extends Record {
async beforeCreate() {
const test = await Test.where({id: this.id}, this.connection)
this.testId = test.testId
}
}
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:
class Record {
@acceptsTransaction
static async firstOrInitialize(params) {
const tx = this.connection.transaction()
let [result] = await this.where(params, {limit: 1}, tx.transaction())
if (!result)
result = await new this().save(params, tx.transaction())
await tx.commit()
return result
}
}
Roadmap