Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(falcor): book operations #50

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"opml-generator": "^1.1.1",
"rx": "^4.1.0",
"shortid": "^2.2.4",
"slug": "^0.9.1",
"uservoice-sso": "^0.1.0"
},
"devDependencies": {
Expand Down
70 changes: 70 additions & 0 deletions src/falcor/books/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Observable } from 'rx';
import { keys, $ref } from '../../utils';
import {
getBooks,
setBookProps,
getWorldByBook
} from '../transforms/books'
import slug from 'slug'


export default ( db, req, res ) => {
const {user} = req;
return [
{
route: 'booksById[{keys:ids}]["_id", "title", "slug"]',
get: ({ids})=> {
return db
::getBooks(ids,user._id)
.flatMap(book =>
[
{path: ["booksById", book._id, "_id"], value: book._id},
{path: ["booksById", book._id, "title"], value: book.title},
{path: ["booksById", book._id, "slug"], value: slug(book.title,{lower: true})},
]
)
}
},
{
route: 'booksById[{keys:ids}]["title"]',
set: pathSet => {
return db
::setBookProps( pathSet.booksById, user )
.flatMap(book => {
return [
{path: ["booksById", book._id, "title"], value: book.title},
{path: ["booksById", book._id, "slug"], value: slug(book.title,{lower: true})},
]
})
}
},
{
route: 'booksById[{keys:ids}].world',
get: pathSet => {
const {ids} = pathSet;
return db
::getBooks(ids,user._id)
.flatMap(book =>
db::getWorldByBook(book._id)
.map((worldId) =>
[
{path: ["booksById", book._id, "world",], value: $ref(['worldsById', worldId])},
]
)
)
}
},
{
route: 'booksById[{keys:ids}].status',
get: pathSet => {
const {ids} = pathSet;
ids.map(id => {
return [
{path: ["booksById", id, "status"], value: 0},
]
})
}
},

]
}
2 changes: 2 additions & 0 deletions src/falcor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import users from './users';
import characters from './characters';
import outlines from './outlines';
import elements from './elements';
import books from './books'

export default db => falcorExpress.dataSourceRoute( ( req, res ) => new Router(
[]
Expand All @@ -13,5 +14,6 @@ export default db => falcorExpress.dataSourceRoute( ( req, res ) => new Router(
.concat( characters( db, req, res ) )
.concat( outlines( db, req, res ) )
.concat( elements( db, req, res ) )
.concat( books( db, req, res ) )
));

83 changes: 83 additions & 0 deletions src/falcor/transforms/books.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {Observable} from 'rx';
import {
keysO,
} from '../../utils';

import {accessControl} from './index'


export function getBooks(ids, userId, secure) {
return this.flatMap(db =>
Observable.from(ids)
.flatMap(id =>
this::permissionBook(id, userId, secure)
.map(permission => {
if (permission === false)
throw new Error("Not authorized");
return id;
})
)
.toArray()
.flatMap(ids => {
return db.mongo.collection('books').find({_id: {$in: ids}}).toArray()
}
)
.flatMap(normalize => normalize)
)
}

export function setBookProps(propsById) {
return this.flatMap(db => {
return keysO(propsById)
.flatMap(_id => {
return db.mongo.collection('books').findOneAndUpdate({_id}, {$set: propsById[_id]}, {
returnOriginal: false,
});
})
.map(book => book.value)
;
});
}


export function getBooksLength(worldID) {
const query = `
match (b:Book)-[rel:IN]->(w:World)
WHERE rel.archived = false and w._id = {worldID}
return count(b) as count
`;
return this.flatMap(db =>db.neo.run(query, {worldID}))
.map(record =>
record.get('count').toNumber()
)
}

export function getWorldByBook(bookId) {
const query = `
match (b:Book)-[rel:IN]->(w:World)
WHERE rel.archived = false and b._id = {bookId}
return w._id as id
`;
return this.flatMap(db => db.neo.run(query,{bookId}))
.map(record =>
record.get('id')
)
}


export function permissionBook(bookId, userId, write) {

const permissions = accessControl(write);

const query = `
MATCH (b:Book)-[r:IN]->(w:World)<-[rel]-(u:User)
WHERE b._id = {bookId} AND u._id ={userId}
AND (${permissions})
return count(rel) > 0 as permission
`;

return this.flatMap(db => db.neo.run(query, {bookId, userId}))
.map(record =>
record.get('permission')
)
}
181 changes: 181 additions & 0 deletions src/falcor/transforms/books.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import test from 'tape';
import {Observable} from 'rx';
import database from '../../db';
import {
getBooks,
getBooksLength,
permissionBook
} from './books'

const dbconf = {
mongodb: {
uri: process.env.MONGO_URI || 'mongodb://localhost:27017/dev',
},

neo4j: {
uri: process.env.NEO_URI || 'bolt://localhost',
},
};

const populate = `
CREATE (reader:User { _id:"testUser1" })
CREATE (user:User { _id:"testUser" })
CREATE (world:World {_id: "testWorld"})<-[:OWNER {archived: false}]-(user)
CREATE (world)<-[:READER {archived: false}]-(reader)
CREATE (:Book {_id : "testBook"})-[:IN {archived: false}]->(world)
CREATE (:Book {_id: "testBook1" })-[:IN {archived: false}]->(world)
CREATE (:Book {_id: "testBookAlone"})
return user
`;
const remove = `
MATCH (n)
OPTIONAL MATCH (n)-[r]-()
WITH n,r LIMIT 50000
WHERE n._id =~ ".*test.*"
DELETE n,r
RETURN count(n) as deletedNodesCount
`;

const books = [
{_id: 'testBook', title: 'Book for test'},
{_id: 'testBook1', title: 'Book for test2'},
];

const bookIds = books.map(book => book._id);

test('permissionBook', t => {
const db = database(dbconf);
let neo, mongo, actual, expected;
let book = 'testBook';
let owner = 'testUser';
let reader = 'testUser1'

db
.map(db => {
neo = db.neo;
mongo = db.mongo;
return neo;
})
.flatMap(neo =>
neo.run(populate)
)
.flatMap(neo =>
db::permissionBook(book, owner)
)
.flatMap(permission => {

actual = permission;
expected = true;
t.equals(actual, expected, "should have permission to read the book");


return db::permissionBook(book, owner, true)
})
.flatMap(permission => {

actual = permission;
expected = true;
t.equals(actual, expected, "should have permission to modify the book");

return db::permissionBook(book, reader, true)
})
.flatMap(permission => {

actual = permission;
expected = false;
t.equals(actual, expected, "should not have permission to modify the book");

return neo.run(remove);
})
.subscribe(() => {
mongo.close().then(() => neo.close(() => neo.disconnect()));
t.end();
})
});

test('getBooks', t => {
const db = database(dbconf);
let neo, mongo, actual, expected;
let booksToFind = ["testBook", "testBook1"];
let userId = 'testUser';
db
.map(db => {
neo = db.neo;
mongo = db.mongo;
return neo;
})
.flatMap(neo =>
neo.run(populate)
)
.flatMap(() =>
mongo.collection('books').insertMany(books)
)
.flatMap(neo =>
db::getBooks(booksToFind, userId)
)
.flatMap((book) => {

actual = bookIds.find(id => book._id) != null;
expected = true;
t.equals(actual, expected, 'should match one of the ids that has been passed to getBooks');

return neo.run(remove)
})

.flatMap(() => {
//t.throws(() => db::getBooks(['invalidID'], userId), 'should throw an exception');
return mongo.collection('books').removeMany({_id: {$in: bookIds}})
})
.subscribe(
() => {
mongo.close().then(() => neo.close(() => neo.disconnect()));
t.end();
},
error => {
mongo.close().then(() => neo.close(() => neo.disconnect()));
}
)
});


test('getBooksLength', t => {
const db = database(dbconf);
let neo, mongo, actual, expected;

db
.map(db => {
neo = db.neo;
mongo = db.mongo;
return neo;
})
.flatMap(neo =>
neo.run(populate)
)
.flatMap(() =>
db::getBooksLength('testWorld')
)
.flatMap(length => {

actual = length;
expected = 2;
t.equals(actual, expected, "should match the number of books the world has");

return neo.run(remove);
})
.flatMap(() =>
db::getBooksLength('invalidWorld')
)
.flatMap(length => {

actual = length;
expected = 0;
t.equals(actual, expected, "should not have any book when the world is not valid");

return neo.run(remove);
})
.subscribe(() => {
mongo.close().then(() => neo.close(() => neo.disconnect()));
t.end();
})
});

26 changes: 26 additions & 0 deletions src/falcor/transforms/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,25 @@ export function remove ( collection, user, _id ) {
});
}


export function archiveDocument(collection, _id, userId) {
return this.flatMap(db => {
db = db.mongo.collection(collection);
return db.findOneAndUpdate(
{_id: _id},
{
$set: {
archived: true,
archived_at: Date.now(),
archiver: userId,
}
},
{ returnOriginal: false }
)
})
.map(r => r.value);
}

export function archiveNode(nodeLabel, nodeId, userId) {
const query = `
MATCH (node:${nodeLabel} {_id: {nodeId} })
Expand Down Expand Up @@ -223,3 +242,10 @@ export function archiveRelationship(relType, fromNodeId, toNodeId, userId) {
db.neo.run(query,{fromNodeId, toNodeId, userId, relType})
)
}


export function accessControl(write){
return write
? `TYPE(rel) = "OWNER" OR TYPE(rel) = "WRITER"`
: `TYPE(rel) = "OWNER" OR TYPE(rel) = "WRITER" OR TYPE(rel) = "READER"`
}
Loading