Schema-less ActiveRecord over neo4j
npm install active-graph-recordActiveRecord is common pattern in software development which declares that there is special class or classes who are
responsible for database reflection, line-by-line or node-by-node.
Neo4j is Graph Database, it's schema-less and ACID-compliant.
Dead simple. It's mostly purposed for ES2015-featured JavaScript, so all of the examples are written using it.
``javascript
const {Connection, Record} = require('active-graph-record')
class Entry extends Record {}
Entry.connection = new Connection('http://neo4j:password@localhost:7474');
// or with babel-preset-stage-1
// here and further where static properties are used they can be replaced
// by assignment of property to class function, like shown above
class Entry extends Record {
static connection = new Connection('http://neo4j:password@localhost:7474');
}
Entry.register() //creates indexes and makes some internal magic for resolving
async function main() {
const entry = new Entry()
entry.foo = 'bar'
await entry.save()
const entries = await Entry.where({foo: 'bar'})
console.log(entries.length) // => 1
console.log(entries[0].foo) // => 'bar'
}
`
no problems. It's dead simple too:
`javascript
const {Connection, Record, Relation} = require('active-graph-record')
class ConnectedRecord extends Record {
static connection = new Connection('http://neo4j:password@localhost:7474');
}
class RecordObject extends ConnectedRecord {
subjects = new Relation(this, 'relation' /internal relation label-name/);
//note: it is NOT a static property. It can be replaced with
constructor(...args){
super(...args);
this.subjects = new Relation(this, 'relation' /internal relation label-name/);
}
}
class RecordSubject extends ConnectedRecord {
//target is optional! and direction is optional too, it should be -1 for reverse relations.
subjects = new Relation(this, 'relation', {target: Object, direction: -1});
}
RecordObject.register()
RecordSubject.register()
async function main() {
const object = await new RecordObject({baz: true}).save()
const subject = await new RecordSubject().save()
await object.subjects.add(subject)
console.log(await subject.objects.size()) // => 1
const objects = await subject.objects.entries()
console.log(objects[0].baz) => //true
}
`
even for deep relations:
`javascript
class User extends ConnectedRecord {
roles = new Relation(this, 'has_role', {target: Role});
permissions = new Relation(this.roles, 'has_permission', {target: Permission});
async hasPermission(permission) {
return await this.permissions.has(permission)
}
}
class Role extends ConnectedRecord {
users = new Relation(this, 'has_role', {target: Role, direction: -1});
permissions = new Relation(this, 'has_permission', {target: Permission});
}
class Permission extends ConnectedRecord {
roles = new Relation(this, 'has_permission', {target: Role, direction: -1});
users = new Relation(this.roles, 'has_role', {target: User, direction: -1});
}
`
Relation instances have bunch of pretty methods to use (you can always pass a transaction as last argument):
`javascript
class Record {
async only(): Record
async only(null | Record): void
async has(records: Array
async intersect(records: Array
async add(records: Array
async delete(records: Array
async clear(): void
async size(): number
async entries(): Array
async where(params?: WhereParams, opts?: WhereOpts): Array
}
`
AGR automatically brings uuid key (cannot be re-defined), created_at and updated_at (in milliseconds) fields when record is reflected.
Record and Relation have static where method to use 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 available
Examples:
`javascript
Entry.where({foo: 1000}, {limit: 10, offset: 10, order: 'created_at'})
Entry.where({updated_at: {$gte: Date.now() - 1000}}, {order: ['created_at DESC']})
Entry.where({foo: {$exists: true, $startsWith: ['b', 'ba', 'baz'], $endsWith: 'bar', $contains: 'z'}})
// 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.where({foo: {$has: [1,2,3]}, bar: {$in: [1,2,3]}})
//$in can also work with array
Entry.where({foo: {$in: [[0], [1,2,3,4,5]]}}})
`
Sure. beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDestroy, afterDestroy are available hooks
`javascript`
class Entry extends Record {
async beforeCreate() {
//this.connection points here to transaction, so you have to pass it if calling other classes
const test = await Test.where({id: this.id}, this.connection)
this.testId = test.testId
}
}
Yes, AGR has transactions.
Hooks (see above) are always inside a transaction.
They can be used by using special decorator _(with babel-plugin-transform-decorators-legacy, will be changed to new syntax when new spec will become stable)_ @acceptsTransaction({force: true}) or called explicitly by connection.transaction()
All transactions should be committed or rolled back.
On SIGINT AGR 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 Record#firstOrCreate sugar-ish method:
`javascript``
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
}
}