Comparing version 0.17.0 to 0.17.2
@@ -16,4 +16,4 @@ 'use strict'; | ||
var _templateObject = _taggedTemplateLiteral(['MATCH ', '\n SET entry += ', ', entry.updated_at = timestamp()'], ['MATCH ', '\n SET entry += ', ', entry.updated_at = timestamp()']), | ||
_templateObject2 = _taggedTemplateLiteral(['CREATE (entry:', ')\n SET entry += ', ',\n entry.created_at = timestamp(),\n entry.updated_at = timestamp(),\n entry.uuid = ', ''], ['CREATE (entry:', ')\n SET entry += ', ',\n entry.created_at = timestamp(),\n entry.updated_at = timestamp(),\n entry.uuid = ', '']), | ||
var _templateObject = _taggedTemplateLiteral(['MATCH ', '\n SET entry += ', ', entry.updatedAt = timestamp()'], ['MATCH ', '\n SET entry += ', ', entry.updatedAt = timestamp()']), | ||
_templateObject2 = _taggedTemplateLiteral(['CREATE (entry:', ')\n SET entry += ', ',\n entry.createdAt = timestamp(),\n entry.updatedAt = timestamp(),\n entry.uuid = ', ''], ['CREATE (entry:', ')\n SET entry += ', ',\n entry.createdAt = timestamp(),\n entry.updatedAt = timestamp(),\n entry.uuid = ', '']), | ||
_templateObject3 = _taggedTemplateLiteral(['', ' RETURN entry'], ['', ' RETURN entry']), | ||
@@ -52,2 +52,4 @@ _templateObject4 = _taggedTemplateLiteral(['\n MATCH ', '\n DELETE entry'], ['\n MATCH ', '\n DELETE entry']), | ||
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
@@ -99,3 +101,3 @@ | ||
function BaseRecord() { | ||
var opts = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; | ||
var props = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; | ||
var metadata = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; | ||
@@ -107,3 +109,9 @@ | ||
Object.assign(_this, opts); | ||
var uuid = props.uuid; | ||
var createdAt = props.createdAt; | ||
var updatedAt = props.updatedAt; | ||
var reflectableProps = _objectWithoutProperties(props, ['uuid', 'createdAt', 'updatedAt']); | ||
Object.assign(_this, reflectableProps); | ||
return _this; | ||
@@ -281,2 +289,12 @@ } | ||
}, { | ||
key: 'createdAt', | ||
get: function get() { | ||
return this.metadata.node ? this.metadata.node.properties.createdAt : undefined; | ||
} | ||
}, { | ||
key: 'updatedAt', | ||
get: function get() { | ||
return this.metadata.node ? this.metadata.node.properties.updatedAt : undefined; | ||
} | ||
}, { | ||
key: 'label', | ||
@@ -283,0 +301,0 @@ get: function get() { |
{ | ||
"name": "agregate", | ||
"version": "0.17.0", | ||
"description": "Schema-less JS-first ActiveRecord over neo4j", | ||
"version": "0.17.2", | ||
"description": "A missing piece of simple API for DB", | ||
"main": "build/index.js", | ||
"repository": "jabher/agregate", | ||
"scripts": { | ||
"test": "mocha --compilers js:babel-register --require ./test/globals/lib", | ||
"test-against-build": "mocha --bail --reporter nyan --require ./test/globals/build", | ||
"test": "mocha --compilers js:babel-register", | ||
"test-against-build": "AGREGATE_DIR=build mocha --bail --reporter nyan", | ||
"build": "babel lib --out-dir build", | ||
"preversion": "git add lib/* test/* && npm run build && npm run test-against-build", | ||
"postversion": "jscs --fix lib && git commit --amend --no-edit && git push -u origin master && npm publish && rm -rf ./build" | ||
"commit": "git fetch && git pull && jscs --fix lib && git commit --amend --no-edit && git push -u origin master", | ||
"preversion": "npm run build && npm run test-against-build && npm run commit", | ||
"postversion": "npm publish && rm -rf ./build" | ||
}, | ||
@@ -14,0 +15,0 @@ "keywords": [ |
172
README.md
@@ -1,64 +0,66 @@ | ||
# ActiveRecord implementation for ES2015 with neo4j as back-end | ||
_this software is beta stage and is not intended to be used in serous production projects_ | ||
_developers of this software are not responsible for data loss and corruption, lunar eclipses and dead kittens_ | ||
# Agregate | ||
##### A "missing piece" of DB clients for Node.JS, accenting on familiar JS experience, developer's freedom and simplicity of usage | ||
## What is it? | ||
> 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** | ||
ActiveRecord 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. | ||
_**disclaimer 1**: this software is beta 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._ | ||
Neo4j is Graph Database, it's schema-less and ACID-compliant. | ||
#### Enviroment and preparations | ||
Agregate's only back-end is [neo4j](http://neo4j.com) (v2 and v3 beta) for now. You can [install](http://neo4j.com/docs/stable/server-installation.html) it or [request a SaaS sandbox](http://neo4j.com/sandbox/). | ||
## How to use it? | ||
[This](https://github.com/Jabher/agregate/blob/master/.babelrc) (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. | ||
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') | ||
#### Familiar JS experience | ||
So, you need User class reflecting DB "table". You simply write | ||
``` | ||
class User extends Record {} | ||
class Entry extends Record {} | ||
Entry.connection = new Connection('http://neo4j:password@localhost:7474'); | ||
const user = new User({name: 'foo'}) | ||
user.surname = 'bar' | ||
user.save() | ||
.then(() => User.where({name: 'foo'})) | ||
.then(([user]) => console.log(user)) /*=> User { | ||
name: 'foo', | ||
surname: 'bar', | ||
createdAt: 1456560261097, | ||
updatedAt: 1456560261097 | ||
}*/ | ||
``` | ||
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. | ||
// 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 | ||
## 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](http://2014.jsconf.eu/speakers/sebastian-markbage-minimal-api-surface-area-learning-patterns-instead-of-frameworks.html). 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: | ||
```javascript | ||
const {Connection, Record} = require('agregate') | ||
//class name will be used as "table name". You can overload it with static "label" property | ||
class Entry extends Record { | ||
//indexes are optional static properties which are used only for making DB query 'CREATE INDEX' during register() call. | ||
static indexes = new Set('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 = 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' | ||
} | ||
//As we cannot have a callbacks on class constructor (at least without crazy hacks) explicit .register() call is required for any concrete class | ||
Entry.register() | ||
``` | ||
## Wait, but I need relations | ||
no problems. It's dead simple too: | ||
OK, let's add relations. | ||
```javascript | ||
const {Connection, Record, Relation} = require('active-graph-record') | ||
class ConnectedRecord extends Record { | ||
static connection = new Connection('http://neo4j:password@localhost:7474'); | ||
} | ||
const {Connection, Record, Relation} = require('agregate') | ||
//we assume that we already made something like Record.connection = ... | ||
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 RecordObject extends Record { | ||
//signature of constructor is | ||
//(source: Record|Relation, label: string[, {target?: RecordClass, direction?: number = 1}]) | ||
subjects = new Relation(this, 'relation'); | ||
} | ||
class RecordSubject extends ConnectedRecord { | ||
//target is optional! and direction is optional too, it should be -1 for reverse relations. | ||
class RecordSubject extends Record { | ||
//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 = new Relation(this, 'relation', {target: Object, direction: -1}); | ||
@@ -71,3 +73,3 @@ } | ||
async function main() { | ||
const object = await new RecordObject({baz: true}).save() | ||
const object = await new RecordObject({foo: 'bar'}).save() | ||
const subject = await new RecordSubject().save() | ||
@@ -78,62 +80,54 @@ await object.subjects.add(subject) | ||
const objects = await subject.objects.entries() | ||
console.log(objects[0].baz) => //true | ||
console.log(objects[0].foo) => //bar | ||
} | ||
``` | ||
even for deep relations: | ||
Deep relations are simple as hell: | ||
```javascript | ||
class User extends ConnectedRecord { | ||
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}); | ||
async hasPermission(permission) { | ||
return await this.permissions.has(permission) | ||
} | ||
hasPermission = ::this.permissions.has | ||
} | ||
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): | ||
Relation instances have bunch of pretty methods to use: | ||
```javascript | ||
class Record { | ||
class Relation { | ||
//overloaded method to implement one-to-one relation | ||
async only(): Record | ||
async only(null | Record): void | ||
async has(records: Array<Record>): bool | ||
//just for you to know: signature of Record.where and Relation#where are 100% same | ||
async where(params?: WhereParams, opts?: WhereOpts): Array<Record> | ||
//only non-familiar method. Returs intersection of relation and passed set | ||
async intersect(records: Array<Record>): Array<Record> | ||
//this part mimics es6 Set class | ||
async add(records: Array<Record>): void | ||
async clear(): void | ||
async delete(records: Array<Record>): void | ||
async clear(): void | ||
async entries(): Array<Record> | ||
async has(records: Array<Record>): bool | ||
//note: size is not property, but async method | ||
async size(): number | ||
async entries(): Array<Record> | ||
async where(params?: WhereParams, opts?: WhereOpts): Array<Record> | ||
} | ||
``` | ||
## AGR-backed fields | ||
## Auto-generated properties | ||
- **uuid** - non-enumerable, non-configurable, automatically generated on creation | ||
- **createdAt** - non-enumerable, non-configurable, automatically generated on creation | ||
- **updatedAt** - non-enumerable, non-configurable, automatically generated on creation and updation | ||
AGR automatically brings **uuid** key (cannot be re-defined), **created_at** and **updated_at** (in milliseconds) fields when record is reflected. | ||
## OK, but how can I make complex queries? | ||
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 | ||
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: | ||
```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: 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'}}) | ||
@@ -151,3 +145,3 @@ | ||
Sure. beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDestroy, afterDestroy are available hooks | ||
**beforeCreate**, **afterCreate**, **beforeUpdate**, **afterUpdate**, **beforeDestroy**, **afterDestroy** are available hooks | ||
@@ -157,3 +151,3 @@ ```javascript | ||
async beforeCreate() { | ||
//this.connection points here to transaction, so you have to pass it if calling other classes | ||
//this.connection points to transaction during the transaction, so you have to pass it if calling other classes | ||
const test = await Test.where({id: this.id}, this.connection) | ||
@@ -167,8 +161,12 @@ 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. | ||
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 Record#firstOrCreate sugar-ish method: | ||
@@ -175,0 +173,0 @@ |
Sorry, the diff of this file is not supported yet
128645
42
2041
197