Skip to content

Commit

Permalink
Implement (C)RUD for locations
Browse files Browse the repository at this point in the history
The same validator is used for creating and updating a location. It has
been modified to support a partial update.

Additional improvements:

* 2 tests have been added to ensure that the correct errors are returned
  by the API when trying to create a location with no attributes (i.e.
  it should indicate which are the required fields).

* A small error has been fixed in the invalid properties creation test.
  The test attempted to produce two different errors on the `street`
  property (blank & required), when it should have been the `street` and
  the `city` properties.

* RAML traits in `server/api/index.raml` have been split into multiple
  files in the `server/api/raml/traits` directory. A similar structure
  has been put in place to define RAML types.

* Test fixtures in `server/spec/fixtures` have been documented.

* A bug has been fixed in `server/utils/bookshelf-returning.js`. It was
  attempting to to call Knex's `returning` method with the wrong
  arguments, using an array's items as the parameters rather than the
  array itself as a single parameter.

Story: TG-43
  • Loading branch information
AlphaHydrae committed Dec 12, 2017
1 parent 9a2e019 commit df1832a
Show file tree
Hide file tree
Showing 26 changed files with 1,575 additions and 189 deletions.
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* validate accept & content type
* add jshint
* export knex clean database
* document expectations

## Development guide

Expand Down
16 changes: 15 additions & 1 deletion server/api/index.raml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,27 @@ baseUri: https://biopocket.ch/api
version: 1
mediaType: application/json

traits: !include traits.raml
traits:
authenticatedResource: !include raml/traits/authenticated-resource.raml
identifiableResource: !include raml/traits/identifiable-resource.raml
jsonConsumer: !include raml/traits/json-consumer.raml
protectedResource: !include raml/traits/protected-resource.raml
serializableResource: !include raml/traits/serializable-resource.raml

types:

# Generic types
geoJsonPoint: !include raml/types/geojson-point.raml
url: !include raml/types/url.raml

# API resources
Auth: !include auth/auth.model.raml
AuthPost: !include auth/auth.model.post.raml
Location: !include locations/locations.model.raml
LocationWrite: !include locations/locations.model.write.raml
User: !include users/users.model.raml

# Routes
/auth: !include auth/auth.raml
/locations: !include locations/locations.raml
/me: !include users/users.me.raml
Expand Down
106 changes: 102 additions & 4 deletions server/api/locations/locations.api.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
/**
* Locations management API.
*
* @module server/api/locations
*/
const serialize = require('express-serializer');

const { route } = require('../utils/api');
const Location = require('../../models/location');
const { fetcher, route } = require('../utils/api');
const { validateRequestBody } = require('../utils/validation');
const { point: validateGeoJsonPoint } = require('../validators/geojson');
const policy = require('./locations.policy');

// API resource name (used in some API errors)
exports.resourceName = 'location';

/**
* Creates a new location.
*
* @function
*/
exports.create = route(async (req, res) => {

await validateLocation(req);
Expand All @@ -15,54 +29,129 @@ exports.create = route(async (req, res) => {
res.status(201).send(await serialize(req, location, policy));
});

/**
* Lists locations ordered by name.
*
* @function
*/
exports.list = route(async (req, res) => {

const query = new Location();
const locations = await query.orderBy('name').orderBy('created_at').fetchAll();

res.send(await serialize(req, locations.models, policy));
});

/**
* Retrieves a single location.
*
* @function
*/
exports.retrieve = route(async (req, res) => {
res.send(await serialize(req, req.location, policy));
});

/**
* Updates a location.
*
* @function
*/
exports.update = route(async (req, res) => {

await validateLocation(req, true);

const location = req.location;
policy.parse(req.body, location);

function validateLocation(req) {
if (location.hasChanged()) {
await location.save();
}

res.send(await serialize(req, location, policy));
});

/**
* Deletes a location.
*
* @function
*/
exports.destroy = route(async (req, res) => {
await req.location.destroy();
res.sendStatus(204);
});

/**
* Middleware that fetches the location identified by the ID in the URL.
*
* @function
*/
exports.fetchLocation = fetcher({
model: Location,
resourceName: exports.resourceName,
coerce: id => id.toLowerCase(),
validate: 'uuid'
});

/**
* Validates the location in the request body.
*
* @param {Request} req - An Express request object.
* @param {boolean} [patchMode=false] - If true, only properties that are set will be validated (i.e. a partial update with a PATCH request).
* @returns {Promise<ValidationErrorBundle>} - A promise that will be resolved if the location is valid, or rejected with a bundle of errors if it is invalid.
*/
function validateLocation(req, patchMode = false) {
return validateRequestBody(req, function() {
return this.parallel(
this.validate(
this.json('/name'),
this.if(patchMode, this.while(this.isSet())),
this.required(),
this.type('string'),
this.notBlank(),
this.string(1, 150)
),
this.validate(
this.json('/shortName'),
this.while(this.isSet()),
this.while(this.isSetAndNotNull()),
this.type('string'),
this.notBlank(),
this.string(1, 30)
),
this.validate(
this.json('/description'),
this.if(patchMode, this.while(this.isSet())),
this.required(),
this.type('string'),
this.notBlank(),
this.string(1, 2000)
),
this.validate(
this.json('/phone'),
this.if(patchMode, this.while(this.isSet())),
this.required(),
this.type('string'),
this.notBlank(),
this.string(1, 20)
),
this.validate(
this.json('/photoUrl'),
this.if(patchMode, this.while(this.isSet())),
this.required(),
this.type('string'),
this.notBlank(),
this.string(1, 500)
),
this.validate(
this.json('/siteUrl'),
this.if(patchMode, this.while(this.isSet())),
this.required(),
this.type('string'),
this.notBlank(),
this.string(1, 500)
),
this.validate(
this.json('/geometry'),
this.if(patchMode, this.while(this.isSet())),
this.required(),
this.geoJsonPoint()
),
Expand All @@ -73,41 +162,50 @@ function validateLocation(req) {
),
this.validate(
this.json('/address'),
this.if(patchMode, this.while(this.isSet())),
this.required(),
this.type('object'),
this.parallel(
this.validate(
this.json('/street'),
this.if(patchMode, this.while(this.isSet())),
this.required(),
this.type('string'),
this.notBlank(),
this.string(1, 150),
this.notBlank()
),
this.validate(
this.json('/number'),
this.while(this.isSet()),
this.while(this.isSetAndNotNull()),
this.type('string'),
this.string(1, 10),
this.notBlank()
),
this.validate(
this.json('/zipCode'),
this.if(patchMode, this.while(this.isSet())),
this.required(),
this.type('string'),
this.notBlank(),
this.string(1, 15),
this.notBlank()
),
this.validate(
this.json('/city'),
this.if(patchMode, this.while(this.isSet())),
this.required(),
this.type('string'),
this.notBlank(),
this.string(1, 100),
this.notBlank()
),
this.validate(
this.json('/state'),
this.if(patchMode, this.while(this.isSet())),
this.required(),
this.type('string'),
this.notBlank(),
this.string(1, 30),
this.notBlank()
)
Expand Down

0 comments on commit df1832a

Please sign in to comment.