multicolour-hapi-jsonapi
Advanced tools
Comparing version 0.1.3 to 0.2.1
109
index.js
@@ -6,2 +6,3 @@ "use strict" | ||
const waterline_joi = require("waterline-joi") | ||
const handlers = require("multicolour/lib/handlers") | ||
@@ -67,2 +68,97 @@ /** | ||
generate_related_resource_routes(server, multicolour) { | ||
// Get the collections. | ||
const collections = multicolour.get("database").get("models") | ||
// Get the models that have associations. | ||
const models = Object.keys(collections) | ||
.filter(model_name => !collections[model_name].meta.junctionTable) | ||
.map(model_name => collections[model_name]) | ||
// Get the headers. | ||
const headers = joi.object(multicolour.request("header_validator").get()) | ||
.options({ allowUnknown: true }) | ||
models.forEach(model => { | ||
// Clone the attributes to prevent | ||
// any accidental overriding/side affects. | ||
const attributes = clone_attributes(model._attributes) | ||
// Save typing later. | ||
const name = model.adapter.identity | ||
// Get any relationships this model has. | ||
const model_relationships = Object.keys(attributes) | ||
.filter(attribute_name => model._attributes[attribute_name].model || model._attributes[attribute_name].collection) | ||
// Maps the relationship name back to the relevant | ||
// model in the collections array. | ||
const relationship_to = {} | ||
model_relationships.forEach(relationship_name => { | ||
const related_model = collections[model._attributes[relationship_name].model || model._attributes[relationship_name].collection] | ||
relationship_to[relationship_name] = related_model | ||
}) | ||
// Route those relationships. | ||
server.route( | ||
model_relationships.map(relationship_name => { | ||
let query_key = model._attributes[relationship_name].model ? "id" : name | ||
return { | ||
method: "GET", | ||
path: `/${name}/{${query_key}}/relationships/${relationship_name}`, | ||
config: { | ||
// auth: this.get_auth_config(), | ||
handler: (request, reply) => { | ||
// Merge the params into the query string params. | ||
request.url.query = require("util")._extend(request.url.query, request.params) | ||
// Call the handler. | ||
if (query_key === "id") { | ||
return handlers.GET.call(model, request, (err, models) => { | ||
if (err) { | ||
/* istanbul ignore next */ | ||
reply[this.get("decorator_name")](err, model) | ||
} | ||
else { | ||
// Get the ids of the related models. | ||
const ids = models.map(model => model[relationship_name] && model[relationship_name].id) | ||
// Get them. | ||
relationship_to[relationship_name] | ||
.find({ id: ids }) | ||
.populateAll() | ||
.exec((err, models) => | ||
reply[this.get("decorator_name")](models.map(model => model.toJSON()), relationship_to[relationship_name])) | ||
} | ||
}) | ||
} | ||
else { | ||
return handlers.GET.call(relationship_to[relationship_name], request, (err, models) => | ||
reply[this.get("decorator_name")](err || models, relationship_to[relationship_name]) | ||
) | ||
} | ||
}, | ||
description: `Get ${relationship_name} related to ${name}.`, | ||
notes: `Get ${relationship_name} related to ${name}.`, | ||
tags: ["api", "relationships"], | ||
validate: { | ||
headers, | ||
params: joi.object({ | ||
[query_key]: joi.string().required() | ||
}) | ||
}, | ||
response: { | ||
schema: this.get_response_schema(relationship_to[relationship_name]).meta({ | ||
className: `related_${relationship_name}` | ||
}) | ||
} | ||
} | ||
} | ||
}) | ||
) | ||
}) | ||
} | ||
/** | ||
@@ -176,3 +272,6 @@ * Get the read only schema for a collection. | ||
const name = this.get("decorator_name") | ||
const multicolour = generator.request("host") | ||
handlers.set_host(multicolour) | ||
// Register with the server some properties it requires. | ||
@@ -200,2 +299,12 @@ Multicolour_Server_Hapi | ||
// Register related resource endpoints | ||
// once the database has been started | ||
// and before the http server is started. | ||
multicolour.on("server_starting", () => | ||
this.generate_related_resource_routes( | ||
server, | ||
multicolour | ||
) | ||
) | ||
// Listen for replies so we can transform any boom | ||
@@ -202,0 +311,0 @@ // responses to be JSON API compliant. |
{ | ||
"name": "multicolour-hapi-jsonapi", | ||
"version": "0.1.3", | ||
"version": "0.2.1", | ||
"description": "JSON API reply decorator built on Waterline and waterline-jsonapi", | ||
@@ -33,9 +33,10 @@ "main": "index.js", | ||
"eslint": "^1.5.1", | ||
"istanbul": "^0.3.22", | ||
"istanbul": "^0.4.2", | ||
"joi": "^7.0.0", | ||
"multicolour": "^0.2.9", | ||
"multicolour-seed": "0.0.4", | ||
"multicolour-server-hapi": "^1.0.12", | ||
"sails-memory": "^0.10.5", | ||
"tap-spec": "^4.1.0", | ||
"tape": "^4.2.0", | ||
"multicolour": "^0.1.8" | ||
"tape": "^4.2.0" | ||
}, | ||
@@ -42,0 +43,0 @@ "dependencies": { |
# multicolour-hapi-jsonapi | ||
JSON API reply decorator built on Waterline and waterline-jsonapi | ||
[![Join the chat at https://gitter.im/newworldcode/multicolour](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/newworldcode/multicolour?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) | ||
[![Coverage Status](https://coveralls.io/repos/github/Multicolour/multicolour-hapi-jsonapi/badge.svg?branch=master)](https://coveralls.io/github/Multicolour/multicolour-hapi-jsonapi?branch=master) | ||
[![Build Status](https://travis-ci.org/Multicolour/multicolour-hapi-jsonapi.svg?branch=master)](https://travis-ci.org/Multicolour/multicolour-hapi-jsonapi) | ||
[![Code Climate](https://codeclimate.com/github/Multicolour/multicolour-hapi-jsonapi/badges/gpa.svg)](https://codeclimate.com/github/Multicolour/multicolour-hapi-jsonapi) | ||
`npm i --save multicolour-hapi-jsonapi` | ||
JSON API reply decorator built on Waterline and waterline-jsonapi to make all responses JSONAPI compliant. | ||
MIT license | ||
[Contributing](./CONTRIBUTING.md) |
@@ -9,2 +9,8 @@ "use strict" | ||
const relations = { | ||
pet: [ "owners" ], | ||
collar: [ "pet" ], | ||
person: [ "user" ] | ||
} | ||
// Set up a dummy service. | ||
@@ -42,3 +48,13 @@ const service = new Multicolour({ | ||
service.get("server").generate_routes() | ||
service.trigger("server_starting") | ||
// Run the test with a helpful name. | ||
tape(`Error "generate_payload" functional tests.`, test => { | ||
const entity = service.get("server").get("validator") | ||
test.throws(entity.generate_payload, TypeError, "Throws when incorrectly called without data or collection.") | ||
test.throws(() => entity.generate_payload({ isBoom: true }, ontology.user), TypeError, "Throws when incorrectly called without data or collection.") | ||
test.throws(() => entity.generate_payload({ isBoom: false, is_error: false }), TypeError, "Throws when incorrectly called without error payload or collection.") | ||
test.end() | ||
}) | ||
// Loop over the payloads. | ||
@@ -53,4 +69,3 @@ Object.keys(reply_payloads).forEach(test_name => { | ||
// Run the test with a helpful name. | ||
tape(`GET /${test_name} test collection.`, test => { | ||
// Create a request to the server without starting it. | ||
tape(`GET /${test_name}.`, test => { | ||
hapi.inject(options, response => { | ||
@@ -63,4 +78,19 @@ test.equal(response.statusCode, 200, "Response code should be 200") | ||
if (relations[test_name].length > 0) { | ||
tape(`GET /${test_name}/1/relationships tests`, test => { | ||
test.plan(relations[test_name].length) | ||
relations[test_name].forEach(relation => { | ||
hapi.inject({ | ||
url: `/${test_name}/1/relationships/${relation}`, | ||
method: "GET", | ||
headers | ||
}, response => { | ||
test.equal(response.statusCode, 200, `GET /${test_name}/1/relationships/${relation}: Response code should be 200`) | ||
// test.equal(joi.validate(JSON.parse(response.payload), reply_payloads[test_name]).error, null, "Payload validation should have no errors.") | ||
}) | ||
}) | ||
}) | ||
} | ||
tape("Test error response", test => { | ||
// Create a request to the server without starting it. | ||
hapi.inject(`/${test_name}`, response => { | ||
@@ -67,0 +97,0 @@ test.equal(response.statusCode, 400, "Response code should be 400") |
@@ -8,13 +8,6 @@ "use strict" | ||
name: "string", | ||
// Add a reference to User. | ||
owner: { | ||
collection: "user" | ||
}, | ||
// Add a reference to Collar. | ||
collar: { | ||
model: "collar" | ||
owners: { | ||
collection: "person" | ||
} | ||
} | ||
} |
@@ -10,18 +10,41 @@ "use strict" | ||
.then(() => { | ||
ontology.collections.pet.create([ | ||
ontology.collections.person.create([ | ||
{ | ||
breed: "beagle", | ||
type: "dog", | ||
name: "Astro", | ||
owner: 1 | ||
name: "Nikola Tesla", | ||
age: 27, | ||
user: 1 | ||
}, | ||
{ | ||
breed: "beagle", | ||
type: "dog", | ||
name: "Cosmo", | ||
owner: 1 | ||
name: "Marconi", | ||
age: 27, | ||
user: 1 | ||
} | ||
]) | ||
.then(callback) | ||
.then(() => { | ||
ontology.collections.pet.create([ | ||
{ | ||
breed: "beagle", | ||
type: "dog", | ||
name: "Astro" | ||
}, | ||
{ | ||
breed: "beagle", | ||
type: "dog", | ||
name: "Cosmo" | ||
} | ||
]) | ||
.then(() => { | ||
ontology.collections.pet | ||
.find({}) | ||
.exec((err, pets) => { | ||
pets.forEach(pet => { | ||
pet.owners.add(1) | ||
pet.owners.add(2) | ||
pet.save(() => {}) | ||
}) | ||
}) | ||
}) | ||
.then(callback) | ||
}) | ||
}) | ||
} |
@@ -13,2 +13,3 @@ "use strict" | ||
// USER | ||
const user_schema = joi.object({ | ||
@@ -22,2 +23,3 @@ id: joi.string().required(), | ||
requires_email: joi.boolean(), | ||
role: joi.string(), | ||
createdAt: joi.date(), | ||
@@ -28,9 +30,10 @@ updatedAt: joi.date() | ||
const pet_schema = joi.object({ | ||
// PERSON | ||
const person_schema = joi.object({ | ||
id: joi.string().required(), | ||
type: joi.string().required(), | ||
attributes: joi.object({ | ||
breed: joi.string().required(), | ||
type: joi.string().required(), | ||
name: joi.string().required(), | ||
age: joi.number().required(), | ||
user: joi.number(), | ||
createdAt: joi.date(), | ||
@@ -40,3 +43,3 @@ updatedAt: joi.date() | ||
relationships: joi.object({ | ||
owner: joi.object({ | ||
user: joi.object({ | ||
data: joi.object({ | ||
@@ -50,15 +53,42 @@ type: joi.string().required(), | ||
// Export schemas. | ||
// PET | ||
const pet_schema = joi.object({ | ||
id: joi.string().required(), | ||
type: joi.string().required(), | ||
attributes: joi.object({ | ||
breed: joi.string().required(), | ||
type: joi.string().required(), | ||
name: joi.string().required(), | ||
createdAt: joi.date(), | ||
updatedAt: joi.date() | ||
}), | ||
relationships: joi.object({ | ||
owners: joi.object({ | ||
data: joi.alternatives().try( | ||
joi.object({ | ||
type: joi.string().required(), | ||
id: joi.string().required() | ||
}), | ||
joi.array().items(joi.object({ | ||
type: joi.string().required(), | ||
id: joi.string().required() | ||
})) | ||
) | ||
}) | ||
}) | ||
}) | ||
// EXPORT SCHEMAS. | ||
module.exports = { | ||
pet: joi.alternatives().try( | ||
joi.object({ | ||
data: joi.array().items(pet_schema), | ||
included: joi.array().items(user_schema) | ||
data: joi.alternatives().try(pet_schema, joi.array().items(pet_schema)), | ||
included: joi.array().items(person_schema) | ||
}), | ||
errors | ||
), | ||
user: joi.alternatives().try( | ||
person: joi.alternatives().try( | ||
joi.object({ | ||
data: user_schema, | ||
included: joi.array() | ||
data: joi.alternatives().try(person_schema, joi.array().items(person_schema)), | ||
included: joi.array().items(user_schema) | ||
}), | ||
@@ -65,0 +95,0 @@ errors |
28379
12
541
14
9