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

Feature/glblagctwo 1147 #1

Merged
merged 42 commits into from
Oct 26, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
30ea05f
Project and initial mocha test setup
mavdi Oct 13, 2015
f00cf3b
plugin up and our first test is running
mavdi Oct 14, 2015
ecab7c0
got inject-then working with mocha tests
mavdi Oct 14, 2015
d764122
added basic REST verb functions to route
mavdi Oct 14, 2015
b74d8d1
OPTIONS returning the right REST opts available
mavdi Oct 15, 2015
140790a
creating options endpoint for all resources
mavdi Oct 15, 2015
7a58f3d
only allowing application/json, tests for req, res allowed headers
mavdi Oct 15, 2015
cd7ca27
checking adapter validity with Hoek
mavdi Oct 16, 2015
8db5220
converted all tabs to spaces...
mavdi Oct 16, 2015
3483b59
more tabs to spaces
mavdi Oct 16, 2015
c097b7d
added helper function to get adapter
mavdi Oct 19, 2015
130497f
fixed adapter tests
mavdi Oct 19, 2015
a3d839d
fixed adapter tests
mavdi Oct 19, 2015
2357310
now calling into adapter functions
mavdi Oct 19, 2015
de24103
post operation and test done
mavdi Oct 19, 2015
e224e06
db removal and get all done
mavdi Oct 19, 2015
b1127e7
PUT done
mavdi Oct 19, 2015
0ecdd36
added patch method
mavdi Oct 20, 2015
5acf00e
mongoose error/reconnect working
mavdi Oct 20, 2015
514a794
refactored all utils to use the same style of modules
mavdi Oct 20, 2015
42630af
POST validation along with the tests
mavdi Oct 20, 2015
6438384
inclusion validation is done
mavdi Oct 20, 2015
cad0349
sparse field validations
mavdi Oct 20, 2015
38832ed
paging and sorting validations added
mavdi Oct 20, 2015
dce701c
implemented patch and delete as specification
mavdi Oct 22, 2015
d2c0a24
removed PUT as specification doesn't allow it
mavdi Oct 22, 2015
b502c4a
using const instead of let where possible
mavdi Oct 22, 2015
aa09deb
added filter validation
mavdi Oct 22, 2015
a364e6b
added route utils
mavdi Oct 22, 2015
2383fad
parsing the comparators
mavdi Oct 22, 2015
6c1a2d3
equal and gt filters implemented
mavdi Oct 22, 2015
2e0c271
error handling for when the resouce is not found
mavdi Oct 22, 2015
9276d90
covering all filtering params with tests
mavdi Oct 22, 2015
a98d717
Implemented includes
mavdi Oct 22, 2015
848ed57
sort implemented
mavdi Oct 22, 2015
e938f9b
paging done
mavdi Oct 22, 2015
27da43f
bit of code clean up
mavdi Oct 22, 2015
b8dc6c2
Implemented descending sort
mavdi Oct 25, 2015
b0c352f
skipping inclusions, implemented parse fields properly
mavdi Oct 25, 2015
380c470
added type, id and other validation
mavdi Oct 25, 2015
d83ec35
now allowing application/vnd.api+json
mavdi Oct 25, 2015
359edac
no need to badImplementation handling as Hapi already does it
mavdi Oct 25, 2015
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.iml
.idea
node_modules
41 changes: 41 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"version": "0.1.0",
// List of configurations. Add new configurations or edit existing ones.
"configurations": [
{
// Name of configuration; appears in the launch configuration drop down menu.
"name": "Launch Mocha",
// Type of configuration.
"type": "node",
// Workspace relative or absolute path to the program.
"program": "node_modules/mocha/bin/_mocha",
// Automatically stop program after launch.
"stopOnEntry": false,
// Command line arguments passed to the program.
"args": ["--debug-brk", "--timeout", "10000"],
// Workspace relative or absolute path to the working directory of the program being debugged. Default is the current workspace.
"cwd": ".",
// Workspace relative or absolute path to the runtime executable to be used. Default is the runtime executable on the PATH.
"runtimeExecutable": null,
// Optional arguments passed to the runtime executable.
"runtimeArgs": ["--nolazy"],
// Environment variables passed to the program.
"env": {
"NODE_ENV": "development"
},
// Use JavaScript source maps (if they exist).
"sourceMaps": false,
// If JavaScript source maps are enabled, the generated code is expected in this directory.
"outDir": null
},
{
"name": "Attach",
"type": "node",
// TCP/IP address. Default is "localhost".
"address": "localhost",
// Port to attach to.
"port": 5858,
"sourceMaps": false
}
]
}
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require ('./lib/plugin')
6 changes: 6 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs"
}
}
94 changes: 94 additions & 0 deletions lib/adapters/mongodb/converters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use strict'

const Hapi = require('hapi')
const _ = require('lodash')
const Hoek = require('hoek')
const mongoose = require('mongoose')
const uuid = require('node-uuid')

module.exports = function() {
const toJsonApi = function (resources) {
if (_.isArray(resources)) {
return _.map(resources, (resource) => {
return toJsonApiSingle(resource);
})
} else {
return toJsonApiSingle(resources);
}

function toJsonApiSingle(resource) {
var mapped = _.mapKeys(resource, function (val, key) {
if (key === '_id') return 'id'
else return key
});
return _.omit(mapped, '__v')
}
}

const toMongooseModel = function (hhSchema) {

const mongooseSchema = {}
mongooseSchema._id = {
type: String,
default: () => {
return uuid.v4()
}
}

var schemaMap = {
'string': String,
'number': Number,
'date': Date,
'buffer': Buffer,
'boolean': Boolean,
'array': Array,
'any': Object
}

mongooseSchema.type = 'string'
mongooseSchema.attributes =
_.mapValues(hhSchema.attributes, function (val) {
Hoek.assert(val.isJoi, 'attribute values in the hh schema should be defined with Joi')
return schemaMap[val._type]
})

const schema = mongoose.Schema(mongooseSchema)
return mongoose.model(hhSchema.type, schema)
}

const toMongoosePredicate = function(query) {
const mappedToModel = _.mapKeys(query.filter, function (val, key) {
if (key === 'id') return '_id'
else return `attributes.${key}`
})

return _.mapValues(mappedToModel, function (val, key) {
const supportedComparators = ['lt', 'lte', 'gt', 'gte']

//if it's a normal value strig, do a $in query
if (_.isString(val) && val.indexOf(',') !== -1) {
return {$in: val.split(',')}
}

//if it's a comparator, translate to $gt, $lt etc
const valueKey = _.keys(val)[0]
if (_.contains(supportedComparators, valueKey)) {
return {[`$${valueKey}`] : val[valueKey]}
}

else return val
})
}

const toMongooseSort = function(sort) {
if (!sort) return {'_id' : -1}
if(sort.indexOf('-') === 0) {
return {[`attributes.${sort.substr(1)}`] : -1}
}

return {[`attributes.${sort}`] : 1}
}

return { toJsonApi, toMongooseModel, toMongoosePredicate, toMongooseSort }
}

118 changes: 118 additions & 0 deletions lib/adapters/mongodb/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
'use strict'

const Hapi = require('hapi')
const Boom = require('boom')
const _ = require('lodash')
const mongoose = require('mongoose')
const converters = require('./converters')()
const utils = require('./utils')()

mongoose.Promise = require('bluebird')

module.exports = function (options) {

const models = {}

const connect = function(cb) {
mongoose.connect(options.mongodbUrl, cb)
}

const disconnect = function(cb) {
//clear out events
mongoose.connection._events = {}
mongoose.disconnect(cb)
}

mongoose.connection.on('error', connect)

const find = function (type, req) {
const model = models[type]
const query = req.query
const limit = (query.page && query.page.limit) || 1000
const skip = (query.page && query.page.offset) || 0
const sort = converters.toMongooseSort(query.sort)
const sparse = query.fields && query.fields[type].split(',')
var predicate = converters.toMongoosePredicate(query)
return model.find(predicate).skip(skip).sort(sort).limit(limit).lean().exec()
.then((resources)=> {
let data = converters.toJsonApi(resources);
if (sparse) {
data = _.map(data, (datum) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment below about mixup of jsonapi vocabulary further down below. This is fields behaviour, not includes

datum.attributes = _.pick(datum.attributes, sparse)
return datum
})
}

return {data}
})
}

const findById = function(type, req) {

const model = models[type]
return model.findById(req.params.id).lean().exec()
.then((resources) => {
if (!resources) {
return Boom.notFound()
}
return {data: converters.toJsonApi(resources)}
})
}

const create = function(type, req) {
const model = models[type]
var data = utils.getPayload(req)
return model.create(data)
.then((created) => {
return {data: converters.toJsonApi(created.toObject())}
})
}

const update = function(type, req) {

const model = models[type]
var data = utils.getPayload(req)
return model.findByIdAndUpdate(req.params.id, data)
.then((resource) => {
if (!resource) {
return Boom.notFound()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The adapter now has knowledge about Boom, which makes it a kind of leaky abstraction and would require re-implementation of these errors across all future adapters

Perhaps we can have the adapter just return the Promise, and remove the catch. The plugin.js code would then be responsible to either interpret the empty result or the error when the Promise is rejected

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'v removed all Boom.badImplementation() as Hapi already takes care of this.

Not really sure about Boom.notFound() and what to replace it with. Boom is very crisp and supports many other error types that me might need to dance around: https://github.com/hapijs/boom

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit conflicted about moving the interpretation of the empty result to plugin.js, however sustaining that pattern might lead to inventing our own error model for adapter, which is not something I want to get into.

Let's keep it as is for now, we can talk about this a bit later.

}
return findById(type, req)
})
}

const del = function(type, req) {
const model = models[type]
var predicate = converters.toMongoosePredicate({id: req.params.id})
return model.remove(predicate)
.then(() => {
return {}
})
}

const processSchema = function(hhSchema) {

if (!models[hhSchema.type]) {

// clean up existing models and schemas
delete mongoose.models[hhSchema.type]
delete mongoose.modelSchemas[hhSchema.type]

models[hhSchema.type] = converters.toMongooseModel(hhSchema)
}
return models[hhSchema.type]
}

return {
connect,
disconnect,
find,
findById,
create,
update,
delete: del,
models,
processSchema
}

}
15 changes: 15 additions & 0 deletions lib/adapters/mongodb/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use strict'

const Hapi = require('hapi')
const _ = require('lodash')
const Hoek = require('hoek')
const mongoose = require('mongoose')
const uuid = require('node-uuid')

module.exports = function() {
const getPayload = function (req) {
return (req.payload) ? req.payload.data : {}
}

return { getPayload }
}
88 changes: 88 additions & 0 deletions lib/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
'use strict'

const _ = require('lodash')
const routes = require('./routes')()
const adapterUtils = require('./utils/adapter')()
const routeUtils = require('./utils/route')()

exports.register = function (server, opts, next) {
server.expose('version', require('../package.json').version);

const adapter = opts.adapter;

adapterUtils.checkValidAdapter(adapter);

adapter.connect(() => {
server.expose('adapter', adapter);
next()
});

const get = function (schema) {
routeUtils.createOptionsRoute(server, schema)
adapter.processSchema(schema)
return _.merge(routes.get(schema), {
handler: (req, reply) => {
routeUtils.parseComparators(req)
reply(adapter.find(schema.type, req))
}
})
}

const getById = function (schema) {
routeUtils.createOptionsRoute(server, schema)
adapter.processSchema(schema)
return _.merge(routes.getById(schema), {
handler: (req, reply) => {
reply(adapter.findById(schema.type, req))
}
})
}

const post = function (schema) {
routeUtils.createOptionsRoute(server, schema)
adapter.processSchema(schema)
return _.merge(routes.post(schema), {
handler: (req, reply) => {
reply(adapter.create(schema.type, req)).code(201)
}
})
}

const patch = function (schema) {
routeUtils.createOptionsRoute(server, schema)
adapter.processSchema(schema)
return _.merge(routes.patch(schema), {
handler: (req, reply) => {
reply(adapter.update(schema.type, req))
}
})
}

const del = function (schema) {
routeUtils.createOptionsRoute(server, schema)
adapter.processSchema(schema)
return _.merge(routes.delete(schema), {
handler: (req, reply) => {
reply(adapter.delete(schema.type, req)).code(204)
}
})
}

server.expose('routes', {
get: get,
getById: getById,
post: post,
patch: patch,
delete: del
})

server.ext('onPostStop', (server, next) => {
adapter.disconnect(next)
})
}

exports.register.attributes = {
pkg: require('../package.json')
}

exports.getAdapter = adapterUtils.getStandardAdapter;
Loading