Code Monkey home page Code Monkey logo

bedwetter's Introduction

bedwetter

Auto-generated, RESTful, CRUDdy route handlers

to be used with hapi 8 (and 7) and its Waterline plugin, dogwater.


What it does

Bedwetter registers route handlers based upon the method and path of your route. It turns them into RESTful API endpoints that automatically interact with the model defined using dogwater. The route handler is based on one of eight bedwetters:

  • POST is used for create, add when add is used to create a record then add it to a relation, and for update
  • PATCH is also used for update
  • PUT is used for add when it's used to simply add a record to a relation
  • GET is used for find, findOne, and populate (get related records or check an association)
  • DELETE is used for destroy and remove (remove a record from a relation)

For now, see SailsJs's documentation on Blueprints for info about parameters for the bedwetters. A portion of the code is adapted from this SailsJs hook.

Bedwetter also allows you to manage resources/records with owners. There are options to act on behalf of a user via hapi authentication. You can set owners automatically on new records, only display records when owned by the authenticated user, and make bedwetters behave like the primary record is the authenticated user.

Bedwetting Patterns

Suppose users are associated with comments via dogwater/Waterline. The user model associates comments in an attribute named comments. Here are some examples as to how the plugin will deduce which of the eight bedwetters to use, based upon route method and path definition.

  • GET /usersfind

    Returns an array of users with an HTTP 200 OK response.

  • GET /users/countfind with /count

    Returns the integer number of users matched with an HTTP 200 OK response.

  • GET /users/{id}findOne

    Returns user id with an HTTP 200 OK response. Responds with an HTTP 404 Not Found response if the user is not found.

  • GET /users/{id}/commentspopulate

    Returns an array of comments associated with user id. Returns HTTP 200 OK if that user is found. Returns an HTTP 404 Not Found response if that user is not found.

  • GET /users/{id}/comments/countpopulate with /count

    Returns the integer number of comments associated with user id. Returns HTTP 200 OK if that user is found. Returns an HTTP 404 Not Found response if that user is not found.

  • GET /users/{id}/comments/{childId}populate

    Returns HTTP 204 No Content if comment childId is associated with user id. Returns an HTTP 404 Not Found response if that user is not found or that comment is not associated with the user.

  • POST /userscreate

    Creates a new user using the request payload and returns it with an HTTP 201 Created response.

  • POST /users/{id}/commentsadd

    Creates a new comment using the request payload and associates that comment with user id. Returns that comment with an HTTP 201 Created response. If that user is not found, returns an HTTP 404 Not Found response.

  • PUT /users/{id}/comments/{childId}add

    Associates comment childId with user id. Returns an HTTP 204 No Content response on success. If the user or comment are not found, returns an HTTP 404 Not Found response.

  • DELETE /users/{id}destroy

    Destroys user id. Returns an HTTP 204 No Content response on success. If the user doesn't exist, returns an HTTP 404 Not Found response.

  • DELETE /users/{id}/comment/{childId}remove

    Removes association between user id and comment childId. Returns an HTTP 204 No Content response on success. If the user or comment doesn't exist, returns an HTTP 404 Not Found response.

  • PATCH /users/{id} or POST /user/{id}update

    Updates user id using the request payload (which will typically only contain the attributes to update) and responds with the updated user. Returns an HTTP 200 OK response on success. If the user doesn't exist, returns an HTTP 404 Not Found response.

Options

Options can be passed to the plugin when registered or defined directly on the route handler. Those defined on the route handler override those passed to the plugin on a per-route basis.

Acting as a User

These options allow you to act on behalf of the authenticated user. Typically the user info is taken directly off the credentials object without checking the Request.auth.isAuthenticated flag. This allows you to use authentication modes however you wish. For examples, for now please see tests at test/options/actAsUser.js.

  • actAsUser (boolean, defaults false). Applies to findOne, find, create, update, destroy, add, remove, and populate.

    This must be set to true for the following options in the section to take effect. The acting user is defined by hapi authentication credentials and the userIdProperty option.

  • userIdProperty (string, defaults "id"). Applies to findOne, find, create, update, destroy, add, remove, and populate.

    When actAsUser is true this option takes effect. It defines a path into Request.auth.credentials to determine the acting user's id. For example, if the credentials object equals {user: {info: {id: 17}}} then "user.info.id" would grab user id 17. See Hoek.reach, which is used to convert the string to a deep property in the hapi credentials object.

  • userUrlPrefix (string, defaults "/user"). Applies to findOne, update, destroy, add, remove, and populate.

    When actAsUser is true this option takes effect. This option works in tandem with userModel. When a route path begins with userUrlPrefix (after any other inert prefix has been stripped via the prefix option), the URL is transformed to begin /:userModel/:actingUserId before matching for a bedwetter; it essentially sets the primary record to the acting user.

  • userModel (string, defaults "users"). Applies to findOne, update, destroy, add, remove, and populate.

    When actAsUser is true this option takes effect. This option works in tandem with userUrlPrefix. When a route path begins with userUrlPrefix (after any other inert prefix has been stripped via the prefix option), the URL is transformed to begin /:userModel/:actingUserId before matching for a bedwetter; it essentially sets the primary record to the acting user. E.g., by default when actAsUser is enabled, route path PUT /user/following/10 would internally be considered as PUT /users/17/following/10, which corresponds to the add bedwetter applied to the authenticated user.

  • requireOwner (boolean, defaults false). Applies to findOne, find, create, update, destroy, add, remove, and populate.

    When actAsUser is true this option takes effect. The option forces any record to that's being viewed or modified (including associations) to be owned by the user. Ownership is determined by matching the acting user's id against the attribute of the record determined by ownerAttr or childOwnerAttr.

  • setOwner (boolean, defaults false). Applies to create, update, add.

    When actAsUser is true this option takes effect. The option forces any record to that's being created or updated (including associated records) to be owned by the acting user. The owner is set on the record's attribute determined by ownerAttr or childOwnerAttr.

  • ownerAttr (string or false, defaults "owner"). Applies to findOne, find, update, destroy, add, remove, and populate.

    When actAsUser is true this option takes effect. If false, requireOwner and setOwner are disabled on the primary record. Otherwise, requireOwner and setOwner options act using the primary record's attribute with name specified by ownerAttr.

  • childOwnerAttr (string or false, defaults "owner"). Applies to add, remove, and populate.

    When actAsUser is true this option takes effect. If false, requireOwner and setOwner are disabled on the child record. Otherwise, requireOwner and setOwner options act using the child record's attribute with name specified by childOwnerAttr.

Other Options

  • prefix (string). Applies to findOne, find, create, update, destroy, add, remove, and populate.

    Allows one to specify a prefix to the route path that will be ignored when determining which bedwetter to apply.

  • createdLocation (string). Applies to create and sometimes to add.

    When this set (typically as a route-level option rather than a plugin-level option), a Location header will be added to responses with a URL pointing to the created record. This option will act as the first argument to util.format when set, and there should be a single placeholder for the created record's id.

  • model (string). Applies to findOne, find, create, update, destroy, add, remove, and populate.

    Name of the model's Waterline identity. If not provided as an option, it is deduced from the route path.

    Ex: /user/1/files/3 has the model user.

  • associationAttr (string). Applies to add, remove, and populate

    Name of the association's Waterline attribute. If not provided as an option, it is deduced from the route path.

    Ex: /user/1/files/3 has the association attribute files (i.e., the Waterline model user has an attribute, files containing records in a one-to-many relationship).

  • criteria (object). Applies to find and populate.

    • blacklist (array)

      An array of attribute names.  The criteria blacklist disallows searching by certain attribute criteria.
      
  • where (object). Applies to find and populate. When where.id is specified, also applies to findOne, update, destroy, add, and remove.

    Typically sets default criteria for the records in a list. Keys represent are attribute names and values represent values for those attributes. This can be overridden by query parameters. When where.id is set, this is is used instead of the primary key path parameter (similarly to the id option), but does not override the id option.

  • id (string or integer). Applies to findOne, update, destroy, add, remove, and populate.

    Fixes a supplied primary key to a certain value. Typically this primary key would be pulled from the route parameter. In most cases this will cause a confusing implementation, but may be worth keeping to interact with future features.

  • limit (positive integer). Applies to find and populate.

    Set default limit of records returned in a list. If not provided, this defaults to 30.

  • maxLimit (positive integer). Applies to find and populate.

    If a user requests a certain number of records to be returned in a list (using the limit query parameter), it cannot exceed this maximum limit.

  • populate (boolean). Applies to find and findOne.

    Determines if all association attributes are by default populated (overridden by populate query parameter, which contains a comma-separated list of attributes). Defaults to false.

  • skip (positive integer). Applies to find and populate.

    Sets default number of records to skip in a list (overridden by skip query parameter). Defaults to 0.

  • sort (string). Applies to find and populate.

    Sets default sorting criteria (i.e. createdDate ASC) (overridden by sort query parameter). Defaults to no sort applied.

  • values (object). Applies to create, update, and sometimes to add. Sets default attribute values in key-value pairs for records to be created or updated. Also includes a blacklist parameter:

    • blacklist (array)

      An array of attribute names to be omitted when creating or updating a record.
      
  • deletedFlag (boolean, defaults false). Applies to destroy.

    Rather than destroying the object, this will simply set a flag on the object using the deletedAttr and deletedValue options.

  • deletedAttr (string, defaults "deleted"). Applies to destroy.

    Model attribute to be updated with the deletedValue.

  • deletedValue (string|int, defaults 1). Applies to destroy.

    Value to be updated on the model attribute specified deletedAttr when the deletedFlag option is enabled.

  • omit (string|array, defaults []). Applies to add, create, find, findOne, populate, update.

    When returning a record or array of records, the list of fields will not be included in the response per record. When populating a record association, you may use [Hoek.reach](https://github.com/hapijs/hoek#reachobj-chain-options style key identifiers to omit deep properties. If the property holds an array, deep omissions will omit the property from each record in the array.

  • pkAttr (string or false, defaults false). Applies to add, destroy, findOne, populate, remove, update.

    This overrides which attribute used for looking-up the primary/parent record. By default bedwetter uses the model's primary key. This option can be used to look-up records by a unique identifier other than the primary key.

    Ex: To look users up by their username attribute rather than their numeric primary key id, set pkAttr to "username". Then GET /users/john-doe will return the user with username "john-doe".

  • childPkAttr (string or false, defaults false). Applies to add, populate, remove.

    This overrides which attribute used for looking-up the secondary/child record. By default bedwetter uses the model's primary key. This option can be used to look-up records by a unique identifier other than the primary key.

Request State

The bedwetter request state can be accessed on Request.plugins.bedwetter. It it an object containing the following properties:

  • action (string). Indicates which one of the eight bedwetter actions was used. It is one of find, findone, update, create, destroy, populate, add, or remove.
  • options (object). These are active bedwetter options used for the request. If any hooks modified the options, that will be reflected here.
  • primaryRecord (Waterline model). This provides access to any primary record associated with this request. This will not be set if there is no primary record.
  • secondaryRecord (Waterline model). This provides access to any secondary record associated with this request. This will not be set if there is no secondary record.

Usage

Here's a sort of crappy example.

// Assume `server` is a hapi server with the bedwetter plugin registered.
// Models with identities "zoo" and "treat" exist via dogwater.
// zoos and treats are in a many-to-many correspondence with each other.
// I suggest checking out ./test

server.route([
{ // findOne
    method: 'GET',
    path: '/zoo/{id}',
    handler: {
        bedwetter: options
    }
},
{ // find
    method: 'GET',
    path: '/treat',
    handler: {
        bedwetter: options
    }
},
{ // find with prefix
    method: 'GET',
    path: '/v1/treat',
    handler: {
        bedwetter: {
            prefix: '/v1'
        }
    }
},
{ // destroy
    method: 'DELETE',
    path: '/treat/{id}',
    handler: {
        bedwetter: options
    }
},
{ // create
    method: 'POST',
    path: '/zoo',
    handler: {
        bedwetter: options
    }
},
{ // update
    method: ['PATCH', 'POST'],
    path: '/treat/{id}',
    handler: {
        bedwetter: options
    }
},
{ // remove
    method: 'DELETE',
    path: '/zoo/{id}/treats/{childId}',
    handler: {
        bedwetter: options
    }
},
{ // create then add
    method: 'POST',
    path: '/zoo/{id}/treats',
    handler: {
        bedwetter: options
    }
},
{ // add
    method: 'PUT',
    path: '/zoo/{id}/treats/{childId}',
    handler: {
        bedwetter: options
    }
},
{ // populate
    method: 'GET',
    path: '/zoo/{id}/treats/{childId?}',
    handler: {
        bedwetter: options
    }
}]);

bedwetter's People

Contributors

bryant1410 avatar devinivy avatar g-div avatar thrivingkings avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

bedwetter's Issues

Question about populate children

I have entities events and eventlinks. An event can have multiple eventlinks. And, eventlinks are tied to linktypes. populate: true is set on all GET routes.

Defined in the models as:
events.js

    eventlinks: {
      collection: 'eventlink',
      via: 'event'
    }

and
eventlinks.js

    event: {
      columnName: 'eventId',
      model : 'event'
    },
    linkType: {
      columnName: 'linkTypeId',
      model: 'linktype',
      populate: true
    },

When i call the endpoint /api/eventlinks/1, I get the details of the linktype in the response payload:

{
  "event": {
    "eventDateTime": "2016-07-09T17:21:52.000Z",
    "id": 1,
    "eventType": 1,
    "organisation": 1,
  },
  "linkType": {
    "title": "customer",
    "createdAt": null,
    "updatedAt": null,
    "id": 1,
    "brand": 1
  },
  "createdAt": "2016-07-09T17:31:25.000Z",
  "updatedAt": "2016-07-09T17:31:25.000Z",
  "id": 1,
}

But, when I call the endpoint /api/events/1, which will include all the eventlinks, it only shows the id for the linkType:

{
  "eventlinks": [
    {
      "createdAt": "2016-07-09T17:31:25.000Z",
      "updatedAt": "2016-07-09T17:31:25.000Z",
      "id": 1,
      "event": 1,
      "linkType": 1
    },
    {
      "createdAt": "2016-07-09T17:31:43.000Z",
      "updatedAt": "2016-07-09T17:31:43.000Z",
      "id": 2,
      "event": 1,
      "linkType": 2
    },
    {
      "createdAt": "2016-07-09T17:32:06.000Z",
      "updatedAt": "2016-07-09T17:32:06.000Z",
      "id": 3,
      "event": 1,
      "linkType": 5
    },
    {
      "createdAt": "2016-07-09T17:32:17.000Z",
      "updatedAt": "2016-07-09T17:32:17.000Z",
      "id": 4,
      "event": 1,
      "linkType": 6
    }
  ],
  "eventType": {
    "title": "customer call",
    "createdAt": null,
    "updatedAt": null,
    "id": 1
  },
  "createdAt": "2016-07-09T17:21:52.000Z",
  "updatedAt": "2016-07-09T17:21:52.000Z",
  "id": 1
}

Is this by design?

Add soft delete option, disallow reads/writes on soft-deleted records.

Currently soft delete options just perform an update on a deleted flag. When soft delete is being used, one should be able to block reads/writes to soft-deleted records. This would require,

  1. Not displaying soft-deleted records in find and findOne.
  2. Not updating soft-deleted records in update.
  3. Disallowing add and remove if primary record or child record are soft-deleted.
  4. Disallowing populate if primary record or child record are soft-deleted.

Hapi v17 Support

Overview

If you are not aware yet, Hapi v17 is making the transition from callbacks to async/await, as well as deprecating some other rarely used functionality. This is a breaking change that may make your plugin no longer compatible with the Hapi API.

Changelog

Draft release notes can be found here: hapijs/hapi#3658

Target Release

The target release date for v17 is the beginning of November.

Tasks

  • Reply to this to acknowledge that you are actively maintaining this module and are willing to update it
  • Update plugin to be fully async/await compatible using the v17 branch from Hapi for testing

    Possible dev flow for updating

    • Clone Hapi
    • npm link within the Hapi repo
    • npm link hapi within your plugin repo
    • Your plugin will now be using v17 of Hapi branch for tests
  • Release new major version of your plugin on npm. Please use a major version increment as this will be a breaking change and it would be terrible for it to sneak into current versions of Hapi.

Notes

  • Support is being dropped for all versions of Node <8.0.0.
  • Hapi v16 will continue to be supported for as long as there exists a Node LTS actively being supported that is not compatible with v17.
  • Targeted release date is November 1st, 2017. Please try to have your plugin updated before then.
  • This issue is being opened because your plugin is listed on the official hapi website

Validate, normalize options.

Normalize options a bit. Let options interact with each other then settle down. For example, requireOwner should perhaps be set to false if actAsUser is false. Then we wont have to always check, for example options.actAsUser && options.requireOwner. We could just check options.requireOwner.

Also, use Joi to validate the options.

Write test for v1.1.1 change

Write test for the bug this commit addresses in 1.1.0: f4cc718.
If userUrlPrefix was /user and the userModel was /users (per defaults), normalizing path would result in checking /user/users, which would fail.

Bug with add function

Bedwetter isn't reflecting the newly created association correctly.
Newly created comment from request.response.source:

{ body: 'Hey @don-moe check out this trixel!',
  owner: 1,
  createdAt: Mon Jun 01 2015 13:57:44 GMT-0400 (EDT),
  updatedAt: Mon Jun 01 2015 13:57:44 GMT-0400 (EDT),
  id: 6 }

It is missing a trixel field. Bedwetter seems to be returning a stale version for the record.

Route config issue

My app fails when I set up a route like the following:

module.exports = [
  {
    method: 'GET',
    path: `api/users/{id}`,
    config: {
      handler: {
        bedwetter: {}
      }
    }
  }
]

If I change the line bedwetter: {} with a reference to a function, it works. But, then I obviously loose the bedwetter functionality.

I suspect this might be because of the version of hapi (13.0)/dogwater (2.0) that I'm using. Or, am I missing something else?

Post Handler hook

I need to emit a socketIO event when certain bedwetter handler routes finish successfully(no Boom errors).
Bedwetter registers itself as a handler and I can't seem to find a nice way to do a post-handler action.

What would be nice is if bedwetter provides some sort of a post handler hook or alternatively allow bedwetter to be executed as one of the pre-handler functions.

For the time being I can perhaps use the 'onPostHandler' extension point and look for successful routes but that doesn't seem like the best solution. Is there a better way ?

Issue with PUT route

I have defined the following routes for an entity:

  {
    method: 'GET',
    path: `${process.env.ENDPOINT_PREFIX_API_CSQA}/brands`,
    config: {
      handler: {
        bedwetter: {
          model: 'brand',
          populate: true
        }
      },
      auth: false
    }
  },
  {
    method: 'GET',
    path: `${process.env.ENDPOINT_PREFIX_API_CSQA}/brands/{id}`,
    config: {
      handler: {
        bedwetter: {
          model: 'brand',
          populate: true
        }
      },
      auth: false
    }
  },
  {
    method: 'PUT',
    path: `${process.env.ENDPOINT_PREFIX_API_CSQA}/brands/{id}`,
    config: {
      handler: {
        bedwetter: {
          model: 'brand'
        }
      },
      auth: false
    }
  },
  {
    method: 'POST',
    path: `${process.env.ENDPOINT_PREFIX_API_CSQA}/brands`,
    config: {
      handler: {
        bedwetter: {
          model: 'brand'
        }
      },
      auth: false
    }
  },
  {
    method: 'DELETE',
    path: `${process.env.ENDPOINT_PREFIX_API_CSQA}/brands/{id}`,
    config: {
      handler: {
        bedwetter: {
          model: 'brand'
        }
      },
      auth: false
    }
  }

And, here's the model definition:

'user strict';

module.exports = {
  identity: 'brand',
  connection: 'dbCSQA',
  tableName: 'Brand',
  migrate: 'safe',
  autoPK: false,
  autoCreatedAt: 'createdAt',
  autoUpdatedAt: 'updatedAt',
  attributes: {
    id: {
      columnName: 'brandId',
      type: 'integer',
      primaryKey: true,
      autoIncrement: true,
      unique: true
    },
    brandName: {
      type : 'string',
      required : true,
      unique: true,
      maxLength: 50
    },
    brandAssetAbbr: {
      type : 'string',
      required : true,
      unique: true,
      maxLength: 5
    }
  }
}

All the routes work 100%, except when I add in the PUT route, the server does not startup.

Deal with strings as input. Fix populate "where" input.

For example, here the childPk is a string if it comes from the path parameter. The where criteria then fails! If where is provided as a non-object (JUST the string/integer), all records are returned. That's what currently happens.

var childPk = parseInt(actionUtil.parsePk(request, options, true));
    var where = childPk ? {id: childPk} : actionUtil.parseCriteria(request, options);

    Model
      .findOne(parentPk)
      .populate(relation, {
        where: where,
        skip: actionUtil.parseSkip(request, options),
        limit: actionUtil.parseLimit(request, options),
        sort: actionUtil.parseSort(request, options)
      })

Tie in with hapi credentials.

Allows a user to act on behalf of herself.
E.g., /users/10/follow/2 becomes /i/follow/2.

Also allows potential implementation of document ownership. A user can only update an item if he owns it.

Question on routes

If I have two entity models as follows:

module.exports = {
  identity: 'assessment',
  attributes: {
  id: {
      columnName: 'assessmentId',
      type: 'integer',
      primaryKey: true,
      autoIncrement: true,
      unique: true
    },
    questionnaire: {
      columnName: 'questionnaireId',
      model: 'questionnaire'
  }
}
module.exports = {
  identity: 'questionnaire',
  attributes: {
    id: {
      columnName: 'questionnaireId',
      type: 'integer',
      primaryKey: true,
      autoIncrement: true,
      unique: true
    },
    title: {
      type : 'string',
      required: true,
      maxLength: 100
    },
    assessments: {
      collection: 'assessment',
      via: 'questionnaire'
    }
  }
}

I can use the route GET /api/csqa/assessments?questionnaire=1 to get all assessments link to questionnaire 1.

Butt, how would I specify the inverse. i.e. getting all questionnaires used by assessment 1?

Allow export as hapi route prerequisite

Allow hapi route prerequisite to handle request as a bedwetter. Dynamic handler generation. Or perhaps mirror request from web server to api server via route prerequisites. This would allow nice cohesion between web/api development. Would make bedwetter reusable in non-api web apps.

Adding populate to POST and PATCH

When setting populate: true to a GET route, the response payload includes the attributes of the associated entities. This does, however, does not work for PUT or PATCH. Is there anyway to achieve this currently?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.