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

Add beforeDelete transform #70

Open
wants to merge 6 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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ To use this library, you describe the special behavior (if any) that resources o

- <a name="before-save"></a>`beforeSave` (optional): a function called on each resource provided by the client (i.e. in a `POST` or `PATCH` request) before it's sent to the adapter for saving. You can transform the data here as necessary or pre-emptively reject the request. [Usage details](https://github.com/ethanresnick/json-api-example/blob/master/src/resource-descriptions/people.js#L25).

- <a name="before-delete"></a>`beforeDelete` (optional): a function called on each resource to be deleted, allowing you to throw an error if the action is not allowed. See [Handling Errors](#handling-errors) for more information.

- <a name="labels"></a>`labelMappers` (optional): this lets you create urls (or, in REST terminology, resources) that map to different database items over time. For example, you could have an `/events/upcoming` resource or a `/users/me` resource. In those examples, "upcoming" and "me" are called the labels and, in labelMappers, you provide a function that maps each label to the proper database id(s) at any given time. The function can return a Promise if needed.

- <a name="parentType"></a>`parentType` (optional): this allows you to designate one resource type being a sub-type of another (its `parentType`). This is often used when you have two resource types that live in the same database table/collection, and their type is determined with a discriminator key. See the [`schools` type](https://github.com/ethanresnick/json-api-example/blob/master/src/resource-descriptions/schools.js#L2) in the example repository.
Expand Down Expand Up @@ -107,5 +109,19 @@ This library gives you a Front controller (shown in the example) that can handle

In the example above, routing is handled with Express's built-in `app[VERB]` methods, and the three parameters are set properly using express's built-in `:param` syntax. If you're looking for something more robust, you might be interested in [Express Simple Router](https://github.com/ethanresnick/express-simple-router). For authentication, check out [Express Simple Firewall](https://github.com/ethanresnick/express-simple-firewall).

## Handling Errors

This library provides an error contructor and a handler that you can use to return JSON API-compliant errors to the user. For an example, please see the [example repo](https://github.com/ethanresnick/json-api-example/blob/master/src/index.js#L64).

You can also throw an APIError inside `beforeSave`, `beforeRender`, and `beforeDelete` transforms to cancel the request.

```javascript
beforeDelete: function(resource, req, res, superFn) {
if (req.user.role !== "admin") {
throw APIError(403, undefined, "You are not allowed to delete this resource.");
Copy link
Owner

Choose a reason for hiding this comment

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

👍

}
}
```

## Database Adapters
An adapter handles all the interaction with the database. It is responsible for turning requests into standard [`Resource`](https://github.com/ethanresnick/json-api/blob/master/src/types/Resource.js) or [`Collection`](https://github.com/ethanresnick/json-api/blob/master/src/types/Collection.js) objects that the rest of the library will use. See the built-in [MongooseAdapter](https://github.com/ethanresnick/json-api/blob/master/src/db-adapters/Mongoose/MongooseAdapter.js) for an example.
2 changes: 1 addition & 1 deletion build/src/ResourceTypeRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ var _lodashObjectMerge2 = _interopRequireDefault(_lodashObjectMerge);
* following same format. Those getters/setters will take the resource type
* whose property is being retrieved/set, and the value to set it to, if any.
*/
var autoGetterSetterProps = ["dbAdapter", "beforeSave", "beforeRender", "labelMappers", "defaultIncludes", "info", "parentType"];
var autoGetterSetterProps = ["dbAdapter", "beforeSave", "beforeRender", "beforeDelete", "labelMappers", "defaultIncludes", "info", "parentType"];

/**
* Global defaults for resource descriptions, to be merged into defaults
Expand Down
83 changes: 53 additions & 30 deletions build/src/controllers/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ var _typesDocument = require("../types/Document");

var _typesDocument2 = _interopRequireDefault(_typesDocument);

var _typesResource = require("../types/Resource");

var _typesResource2 = _interopRequireDefault(_typesResource);

var _typesCollection = require("../types/Collection");

var _typesCollection2 = _interopRequireDefault(_typesCollection);
Expand Down Expand Up @@ -118,7 +122,7 @@ var APIController = (function () {

// Kick off the chain for generating the response.
return (0, _co2["default"])(_regeneratorRuntime.mark(function callee$2$0() {
var parsedPrimary, mappedLabel, mappedIsEmptyArray, errorsArr, apiErrors;
var parsedPrimary, mappedLabel, mappedIsEmptyArray, toTransform, errorsArr, apiErrors;
return _regeneratorRuntime.wrap(function callee$2$0$(context$3$0) {
while (1) switch (context$3$0.prev = context$3$0.next) {
case 0:
Expand Down Expand Up @@ -210,46 +214,65 @@ var APIController = (function () {
}

case 32:
if (!(request.method === "delete")) {
context$3$0.next = 37;
break;
}

toTransform = undefined;

if (Array.isArray(request.idOrIds)) {
toTransform = new _typesCollection2["default"](request.idOrIds.map(function (id) {
return new _typesResource2["default"](request.type, id);
}));
} else if (typeof request.idOrIds === "string") {
toTransform = new _typesResource2["default"](request.type, request.idOrIds);
}

context$3$0.next = 37;
return (0, _stepsApplyTransform2["default"])(toTransform, "beforeDelete", registry, frameworkReq, frameworkRes);

case 37:
if (!(typeof response.primary === "undefined")) {
context$3$0.next = 47;
context$3$0.next = 52;
break;
}

context$3$0.t0 = request.method;
context$3$0.next = context$3$0.t0 === "get" ? 36 : context$3$0.t0 === "post" ? 39 : context$3$0.t0 === "patch" ? 42 : context$3$0.t0 === "delete" ? 45 : 47;
context$3$0.next = context$3$0.t0 === "get" ? 41 : context$3$0.t0 === "post" ? 44 : context$3$0.t0 === "patch" ? 47 : context$3$0.t0 === "delete" ? 50 : 52;
break;

case 36:
context$3$0.next = 38;
case 41:
context$3$0.next = 43;
return (0, _stepsDoQueryDoGet2["default"])(request, response, registry);

case 38:
return context$3$0.abrupt("break", 47);
case 43:
return context$3$0.abrupt("break", 52);

case 39:
context$3$0.next = 41;
case 44:
context$3$0.next = 46;
return (0, _stepsDoQueryDoPost2["default"])(request, response, registry);

case 41:
return context$3$0.abrupt("break", 47);
case 46:
return context$3$0.abrupt("break", 52);

case 42:
context$3$0.next = 44;
case 47:
context$3$0.next = 49;
return (0, _stepsDoQueryDoPatch2["default"])(request, response, registry);

case 44:
return context$3$0.abrupt("break", 47);
case 49:
return context$3$0.abrupt("break", 52);

case 45:
context$3$0.next = 47;
case 50:
context$3$0.next = 52;
return (0, _stepsDoQueryDoDelete2["default"])(request, response, registry);

case 47:
context$3$0.next = 55;
case 52:
context$3$0.next = 60;
break;

case 49:
context$3$0.prev = 49;
case 54:
context$3$0.prev = 54;
context$3$0.t1 = context$3$0["catch"](0);
errorsArr = Array.isArray(context$3$0.t1) ? context$3$0.t1 : [context$3$0.t1];
apiErrors = errorsArr.map(_typesAPIError2["default"].fromError);
Expand All @@ -265,9 +288,9 @@ var APIController = (function () {
response.errors = response.errors.concat(apiErrors);
//console.log("API CONTROLLER ERRORS", errorsArr[0], errorsArr[0].stack);

case 55:
case 60:
if (!response.errors.length) {
context$3$0.next = 59;
context$3$0.next = 64;
break;
}

Expand All @@ -277,16 +300,16 @@ var APIController = (function () {
response.body = new _typesDocument2["default"](response.errors).get(true);
return context$3$0.abrupt("return", response);

case 59:
context$3$0.next = 61;
case 64:
context$3$0.next = 66;
return (0, _stepsApplyTransform2["default"])(response.primary, "beforeRender", registry, frameworkReq, frameworkRes);

case 61:
case 66:
response.primary = context$3$0.sent;
context$3$0.next = 64;
context$3$0.next = 69;
return (0, _stepsApplyTransform2["default"])(response.included, "beforeRender", registry, frameworkReq, frameworkRes);

case 64:
case 69:
response.included = context$3$0.sent;

if (response.status !== 204) {
Expand All @@ -295,11 +318,11 @@ var APIController = (function () {

return context$3$0.abrupt("return", response);

case 67:
case 72:
case "end":
return context$3$0.stop();
}
}, callee$2$0, this, [[0, 49]]);
}, callee$2$0, this, [[0, 54]]);
}));
}

Expand Down
37 changes: 20 additions & 17 deletions build/test/integration/delete-resource/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,31 @@ var _appAgent = require("../../app/agent");

var _appAgent2 = _interopRequireDefault(_appAgent);

var _fixturesCreation = require("../fixtures/creation");
describe("Delete Resource", function () {
var Agent = undefined;

describe("Deleting a resource", function () {

var Agent = undefined,
id = undefined;
before(function (done) {
_appAgent2["default"].then(function (A) {
Agent = A;
return Agent.request("POST", "/schools").type("application/vnd.api+json").send({ "data": _fixturesCreation.VALID_SCHOOL_RESOURCE_NO_ID }).promise().then(function (response) {
id = response.body.data.id;
return Agent.request("DEL", "/schools/" + id).type("application/vnd.api+json").send().promise();
}, done).then(function () {
return done();
}, done);
}, done)["catch"](done);
});

it("should delete a resource by id", function (done) {
Agent.request("GET", "/schools/" + id).accept("application/vnd.api+json").promise().then(done, function (err) {
(0, _chai.expect)(err.response.statusCode).to.equal(404);
done();
})["catch"](done);
});

describe("Valid deletion", function () {
it("should return 204", function (done) {
Agent.request("DEL", "/organizations/54419d550a5069a2129ef255").promise().then(function (res) {
(0, _chai.expect)(res.status).to.equal(204);
done();
})["catch"](done);
});
});

describe("Invalid deletion", function () {
it("should return 403", function (done) {
Agent.request("DEL", "/schools/53f54dd98d1e62ff12539db4").promise().then(done, function (err) {
(0, _chai.expect)(err.status).to.equal(403);
done();
})["catch"](done);
});
});
});
2 changes: 1 addition & 1 deletion src/ResourceTypeRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import merge from "lodash/object/merge";
* whose property is being retrieved/set, and the value to set it to, if any.
*/
const autoGetterSetterProps = ["dbAdapter", "beforeSave", "beforeRender",
"labelMappers", "defaultIncludes", "info", "parentType"];
"beforeDelete", "labelMappers", "defaultIncludes", "info", "parentType"];

/**
* Global defaults for resource descriptions, to be merged into defaults
Expand Down
19 changes: 19 additions & 0 deletions src/controllers/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import co from "co";

import Response from "../types/HTTP/Response";
import Document from "../types/Document";
import Resource from "../types/Resource";
import Collection from "../types/Collection";
import APIError from "../types/APIError";

Expand Down Expand Up @@ -114,6 +115,24 @@ class APIController {
}
}

if (request.method === "delete") {
let toTransform;

if (Array.isArray(request.idOrIds)) {
toTransform = new Collection(
request.idOrIds.map((id) => new Resource(request.type, id))
);
}

else if (typeof request.idOrIds === "string") {
toTransform = new Resource(request.type, request.idOrIds);
}

yield applyTransform(
toTransform, "beforeDelete", registry, frameworkReq, frameworkRes
);
}

// Actually fulfill the request!
// If we've already populated the primary resources, which is possible
// because the label may have mapped to no id(s), we don't need to query.
Expand Down
7 changes: 5 additions & 2 deletions test/app/database/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import OrganizationModelSchema from "./models/organization";
/*eslint-disable new-cap */
const ObjectId = mongoose.Types.ObjectId;
const govtId = ObjectId("54419d550a5069a2129ef254");
const otherGovtId = ObjectId("54419d550a5069a2129ef255");
const smithId = ObjectId("53f54dd98d1e62ff12539db2");
const doeId = ObjectId("53f54dd98d1e62ff12539db3");
const stateCollegeId = ObjectId("53f54dd98d1e62ff12539db4");
/*eslint-enable new-cap */

const OrganizationModel = OrganizationModelSchema.model;
Expand All @@ -29,11 +31,12 @@ fixtures.save("all", {
{ name: "Doug Wilson", gender: "male" }
],
Organization: [
{name: "State Government", description: "Representing the good people.", liaisons: [doeId, smithId], _id: govtId}
{name: "State Government", description: "Representing the good people.", liaisons: [doeId, smithId], _id: govtId},
{name: "Other State Government", description: "Representing the other good people.", _id: otherGovtId}
],
School: [
{name: "City College", description: "Just your average local college.", liaisons: [smithId]},
{name: "State College", description: "Just your average state college."}
{name: "State College", description: "Just your average state college.", _id: stateCollegeId}
]
});

Expand Down
8 changes: 7 additions & 1 deletion test/app/src/resource-descriptions/schools.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Promise } from "q";
import API from "../../../../../index";
let APIError = API.types.Error;

module.exports = {
parentType: "organizations",
Expand All @@ -23,10 +25,14 @@ module.exports = {
}
},

beforeSave: function(resource) {
beforeSave(resource) {
return new Promise((resolve, reject) => {
resource.attrs.description = "Modified in a Promise";
resolve(resource);
});
},

beforeDelete(resource) {
throw new APIError(403, undefined, "You are not allowed to delete schools.");
}
};
Loading