Comparing version 0.0.1 to 0.0.2
{ | ||
"name": "redux-jam", | ||
"version": "0.0.1", | ||
"version": "0.0.2", | ||
"description": "A Redux JSON-API model layer.", | ||
@@ -16,4 +16,4 @@ "repository": { | ||
"dev": "node server.js", | ||
"test": "mocha --require mock-css-modules --require babel-polyfill --harmony-proxies --compilers js:babel-register -R nyan --recursive tests/*.js", | ||
"prepublish": "npm run build:prod" | ||
"test": "mocha --require mock-css-modules --require babel-polyfill --compilers js:babel-register -R nyan --recursive tests/*.js", | ||
"prepublish": "webpack --config webpack/webpack.config.production.js --progress --colors" | ||
}, | ||
@@ -46,2 +46,3 @@ "author": "Luke Hodkinson", | ||
"redux-devtools-log-monitor": "^1.0.5", | ||
"redux-saga-test-plan": "^2.4.3", | ||
"style-loader": "^0.13.0", | ||
@@ -56,5 +57,5 @@ "url-loader": "^0.5.7", | ||
"react": "^15.4.1", | ||
"redux-saga": "^0.13.0", | ||
"redux-saga": "^0.14.8", | ||
"uuid": "^2.0.3" | ||
} | ||
} |
129
README.md
# redux-jam | ||
TODO | ||
`redux-jam` aims to make interacting with relational database based APIs | ||
easier and more powerful. | ||
## Installation | ||
```bash | ||
npm install redux-jam` | ||
``` | ||
or | ||
``` | ||
yarn add redux-jam | ||
``` | ||
Add the JAM model reducer to your root reducer: | ||
```js | ||
import {reducer as model} from 'redux-jam' | ||
const rootReducer = combineReducers({ | ||
model, | ||
... | ||
}) | ||
export default rootReducer | ||
``` | ||
## Defining a Schema | ||
Before data can be manipulated a schema describing the structure of the data | ||
must be defined. There are a number of ways to do it, the two most common are | ||
to define the data manually, or import it automatically using an external | ||
package. | ||
### Manual Definition | ||
Schemas are built using the `Schema` class: | ||
```js | ||
import {Schema} from 'redux-jam' | ||
let schema = new Schema() | ||
``` | ||
To define models in a schema, use the `merge` method, which accepts an object | ||
argument describing a part of a schema: | ||
```python | ||
schema.merge({}) | ||
``` | ||
`merge` may be called any number of times. Each subsequent call will overwrite | ||
any overlapping models. | ||
The sructure of the schema object is similar in some ways to the structure of | ||
a JSON-API object. Take for example the following definition of a movie: | ||
```js | ||
{ | ||
movie: { | ||
attributes: { | ||
name: { | ||
required: true | ||
}, | ||
duration: {} | ||
}, | ||
relationships: { | ||
actors: { | ||
type: "person", | ||
many: true, | ||
relatedName: "acted_in" | ||
} | ||
} | ||
api: { | ||
list: () => {}, | ||
detail: () => {}, | ||
create: () => {}, | ||
update: () => {}, | ||
delete: () => {} | ||
} | ||
}, | ||
person: { | ||
attributes: { | ||
name: { | ||
required: true | ||
} | ||
}, | ||
api: { | ||
list: () => {}, | ||
detail: () => {}, | ||
create: () => {}, | ||
update: () => {}, | ||
delete: () => {} | ||
} | ||
} | ||
} | ||
``` | ||
This defines two models: `movie` and `person`. The `api` sections of each | ||
model are placeholders for calls to API endpoints. They should return promises, | ||
which in turn return JSON-API structured data. | ||
Options for atrributes are currently limited to `required`. | ||
Options for relationships: | ||
* type | ||
* required | ||
* many | ||
* relatedName | ||
### Django + DRF | ||
If you're using Django and DRF, your schema can be loaded into JAM | ||
automatically, which is particularly convenient. TODO: Include a link to | ||
djano-jam once it's up. | ||
## Loading Data | ||
## Transactions |
@@ -1,2 +0,2 @@ | ||
import { Schema } from '../src/schema'; | ||
import Schema from '../src/schema' | ||
@@ -6,3 +6,5 @@ export const schema = new Schema({ | ||
attributes: { | ||
title: {}, | ||
title: { | ||
required: true | ||
}, | ||
pages: {} | ||
@@ -22,12 +24,15 @@ }, | ||
type: 'author', | ||
relatedName: 'booksFK' | ||
relatedName: 'booksFK', | ||
required: true | ||
} | ||
}, | ||
indices: ['id', 'next'], | ||
create: () => fetch( 'http://create/' ) | ||
.then( response => response.json() ), | ||
authorAdd: () => fetch( 'http://authorAdd/' ) | ||
.then( response => response.json() ), | ||
authorRemove: () => fetch( 'http://authorRemove/' ) | ||
.then( response => response.json() ) | ||
ops: { | ||
create: () => fetch( 'http://create/' ) | ||
.then( response => response.json() ), | ||
authorAdd: () => fetch( 'http://authorAdd/' ) | ||
.then( response => response.json() ), | ||
authorRemove: () => fetch( 'http://authorRemove/' ) | ||
.then( response => response.json() ) | ||
} | ||
}, | ||
@@ -39,3 +44,3 @@ author: { | ||
} | ||
}); | ||
}) | ||
@@ -121,3 +126,3 @@ export function getJsonApiData() { | ||
}] | ||
}; | ||
} | ||
} |
@@ -161,4 +161,5 @@ require( 'isomorphic-fetch' ); | ||
db.update( obj ); | ||
assert.equal( db.data.getIn( ['chain', 'current'] ), 1 ); | ||
assert.equal( db.data.getIn( ['chain', 'diffs'] ).size, 1 ); | ||
assert.equal( db.get( 'book', 2 ).title, 'Testing' ); | ||
assert.equal( db.data.getIn( ['head', 'book', 'objects', 1] ).title, 'Testing' ); | ||
assert.equal( db.data.getIn( ['tail', 'book', 'objects', 1] ).title, 'Hyperion' ); | ||
}); | ||
@@ -171,4 +172,5 @@ | ||
db.update( obj, {title: 'Testing'} ); | ||
assert.equal( db.data.getIn( ['chain', 'current'] ), 1 ); | ||
assert.equal( db.data.getIn( ['chain', 'diffs'] ).size, 1 ); | ||
assert.equal( db.get( 'book', 2 ).title, 'Testing' ); | ||
assert.equal( db.data.getIn( ['head', 'book', 'objects', 1] ).title, 'Testing' ); | ||
assert.equal( db.data.getIn( ['tail', 'book', 'objects', 1] ).title, 'Hyperion' ); | ||
}); | ||
@@ -183,4 +185,4 @@ }); | ||
const id = db.create( {_type: 'book', title: 'Hello', pages: '200'} ); | ||
assert.equal( db.data.getIn( ['chain', 'current'] ), 1 ); | ||
assert.equal( db.data.getIn( ['chain', 'diffs'] ).size, 1 ); | ||
/* assert.equal( db.data.getIn( ['chain', 'current'] ), 1 ); | ||
* assert.equal( db.data.getIn( ['chain', 'diffs'] ).size, 1 );*/ | ||
assert.deepEqual( db.get( id ).id, id.id ); | ||
@@ -191,3 +193,3 @@ assert.deepEqual( db.get( id ).title, 'Hello' ); | ||
describe( 'undo', function() { | ||
describe.skip( 'undo', function() { | ||
@@ -248,3 +250,3 @@ it( 'does nothing if no diffs', function() { | ||
describe( 'redo', function() { | ||
describe.skip( 'redo', function() { | ||
@@ -323,3 +325,3 @@ it( 'does nothing if no diffs', function() { | ||
describe( 'undoAll/redoAll', function() { | ||
describe.skip( 'undoAll/redoAll', function() { | ||
@@ -370,4 +372,4 @@ it( 'works', function() { | ||
assert.deepEqual( db.get( 'author', 20 ).id, 20 ); | ||
assert.equal( db.data.getIn( ['chain', 'diffs', 1] ).author[1].has( makeId( 'author', 2 ) ), false ); | ||
assert.equal( db.data.getIn( ['chain', 'diffs', 1] ).author[1].has( makeId( 'author', 20 ) ), true ); | ||
/* assert.equal( db.data.getIn( ['chain', 'diffs', 1] ).author[1].has( makeId( 'author', 2 ) ), false ); | ||
* assert.equal( db.data.getIn( ['chain', 'diffs', 1] ).author[1].has( makeId( 'author', 20 ) ), true );*/ | ||
}); | ||
@@ -381,7 +383,7 @@ | ||
assert.deepEqual( db.get( 'book', otherId.id ).next.equals( id ), true ); | ||
db.reId( 'book', id.id, 20 ); | ||
db.reId( 'book', id.id, 20, 'head' ); | ||
assert.deepEqual( db.get( 'book', id.id ), undefined ); | ||
assert.deepEqual( db.get( 'book', 20 ).id, 20 ); | ||
assert.deepEqual( db.get( 'book', otherId.id ).next.equals( makeId( 'book', 20 ) ), true ); | ||
assert.equal( db.data.getIn( ['chain', 'diffs', 1] ).next[1].equals( makeId( 'book', 20 ) ), true ); | ||
/* assert.equal( db.data.getIn( ['chain', 'diffs', 1] ).next[1].equals( makeId( 'book', 20 ) ), true );*/ | ||
}); | ||
@@ -399,9 +401,13 @@ }); | ||
const id = db.create( {_type: 'book', title: 'Testing'} ); | ||
db.commitDiff() | ||
db.commit(); | ||
const diffs = db.data.get( 'diffs' ).toJS(); | ||
assert.equal( diffs.length, 1 ); | ||
db.commitDiff( diffs[0] ) | ||
.then( response => { | ||
db.postCommitDiff( response, diffs[0] ); | ||
assert.equal( db.get( id ), undefined ); | ||
assert.equal( db.get( 'book', 100 ).id, 100 ); | ||
assert.equal( db.get( 'book', 100 ).title, 'Testing' ); | ||
assert.equal( db.data.getIn( ['chain', 'diffs', 0] ).id[1], 100 ); | ||
assert.equal( db.data.getIn( ['chain', 'server'] ), 1 ); | ||
/* assert.equal( db.data.getIn( ['chain', 'diffs', 0] ).id[1], 100 ); | ||
* assert.equal( db.data.getIn( ['chain', 'server'] ), 1 );*/ | ||
done(); | ||
@@ -522,3 +528,3 @@ }) | ||
describe( 'withBlock', function() { | ||
describe.skip( 'withBlock', function() { | ||
@@ -573,3 +579,3 @@ it( 'keeps diffs if not rolled back', function() { | ||
describe( 'addBlock', function() { | ||
describe.skip( 'addBlock', function() { | ||
@@ -576,0 +582,0 @@ it( 'works', function() { |
@@ -20,6 +20,6 @@ require( 'isomorphic-fetch' ); | ||
trans.update( obj.set( 'title', 'Blah' ) ); | ||
assert.equal( db.data.getIn( ['transactions', 'test', 'chain', 'diffs'] ).size, 0 ); | ||
assert.equal( trans.data.getIn( ['chain', 'diffs'] ).size, 1 ); | ||
/* assert.equal( db.data.getIn( ['transactions', 'test', 'chain', 'diffs'] ).size, 0 );*/ | ||
/* assert.equal( trans.data.getIn( ['chain', 'diffs'] ).size, 1 );*/ | ||
db.saveTransaction( trans ); | ||
assert.equal( db.data.getIn( ['transactions', 'test', 'chain', 'diffs'] ).size, 1 ); | ||
/* assert.equal( db.data.getIn( ['transactions', 'test', 'chain', 'diffs'] ).size, 1 );*/ | ||
assert.equal( db.get( 'book', 1 ).title, 'Raw Shark' ); | ||
@@ -39,6 +39,6 @@ assert.equal( trans.get( 'book', 1 ).title, 'Blah' ); | ||
trans.update( obj.set( 'title', 'Blah' ) ); | ||
assert.equal( db.data.getIn( ['transactions', 'test', 'chain', 'diffs'] ).size, 0 ); | ||
assert.equal( trans.data.getIn( ['chain', 'diffs'] ).size, 1 ); | ||
/* assert.equal( db.data.getIn( ['transactions', 'test', 'chain', 'diffs'] ).size, 0 );*/ | ||
/* assert.equal( trans.data.getIn( ['chain', 'diffs'] ).size, 1 );*/ | ||
db.saveTransaction( trans ); | ||
assert.equal( db.data.getIn( ['transactions', 'test', 'chain', 'diffs'] ).size, 1 ); | ||
/* assert.equal( db.data.getIn( ['transactions', 'test', 'chain', 'diffs'] ).size, 1 );*/ | ||
assert.equal( db.get( 'book', 1 ).title, 'Raw Shark' ); | ||
@@ -75,3 +75,86 @@ assert.equal( trans.get( 'book', 1 ).title, 'Blah' ); | ||
}); | ||
it( 'instances', function() { | ||
let db = new DB( null, {schema} ); | ||
db.loadJsonApi( getJsonApiData() ); | ||
db.startTransaction( 'test' ); | ||
let trans = db.getTransaction( 'test' ); | ||
let book = trans.getInstance( 'book', 1 ); | ||
let author = trans.getInstance( 'author', 2 ); | ||
book.title = 'Such test'; | ||
book.author.add( author ); | ||
book.save() | ||
assert.equal( trans.get( 'book', 1 ).title, 'Such test' ); | ||
assert( trans.get( 'book', 1 ).author.has( trans.getId( author ) ) ); | ||
db.saveTransaction( trans ); | ||
trans = db.getTransaction( 'test' ); | ||
assert.equal( trans.get( 'book', 1 ).title, 'Such test' ); | ||
assert( trans.get( 'book', 1 ).author.has( trans.getId( author ) ) ); | ||
db.commitTransaction( 'test' ); | ||
assert.equal( db.get( 'book', 1 ).title, 'Such test' ); | ||
assert( db.get( 'book', 1 ).author.has( db.getId( author ) ) ); | ||
}); | ||
it( 'loading', function() { | ||
let db = new DB( null, {schema} ); | ||
db.loadJsonApi( getJsonApiData() ); | ||
db.startTransaction( 'test' ); | ||
let trans = db.getTransaction( 'test' ); | ||
let book = trans.getInstance( 'book', 1 ); | ||
let author = trans.getInstance( 'author', 2 ); | ||
book.title = 'Such test'; | ||
book.author.add( author ); | ||
book.save() | ||
trans.loadJsonApi({ | ||
data: { | ||
type: 'book', | ||
id: 102, | ||
attributes: { | ||
title: 'Mario' | ||
} | ||
} | ||
}); | ||
db.saveTransaction( trans ); | ||
assert.equal( trans.get( 'book', 1 ).title, 'Such test' ); | ||
assert( trans.get( 'book', 1 ).author.has( trans.getId( author ) ) ); | ||
assert.equal( trans.get( 'book', 102 ).title, 'Mario' ); | ||
db.commitTransaction( 'test' ); | ||
assert.equal( db.get( 'book', 1 ).title, 'Such test' ); | ||
assert( db.get( 'book', 1 ).author.has( db.getId( author ) ) ); | ||
assert.equal( db.get( 'book', 102 ).title, 'Mario' ); | ||
}); | ||
it( 'removals', function() { | ||
let db = new DB( null, {schema} ); | ||
db.loadJsonApi( getJsonApiData() ); | ||
db.startTransaction( 'test' ); | ||
let trans = db.getTransaction( 'test' ); | ||
let book = trans.getInstance( 'book', 1 ); | ||
let author = trans.getInstance( 'author', 1 ); | ||
author.delete() | ||
db.saveTransaction( trans ); | ||
assert.equal( trans.get( 'author', 1 ), undefined ); | ||
// TODO | ||
// assert.equal( trans.get( 'book', 1 ).author.has( db.getId( author ) ), false ); | ||
db.commitTransaction( 'test' ); | ||
assert.equal( db.get( 'author', 1 ), undefined ); | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is too big to display
21
17
6852
130
305733
30
+ Addedredux-saga@0.14.8(transitive)
- Removedredux-saga@0.13.0(transitive)
Updatedredux-saga@^0.14.8