0.1.8 • Published 6 years ago

restful-model v0.1.8

Weekly downloads
29
License
MIT
Repository
github
Last release
6 years ago

Restful Model

Build Status Coverage Status Greenkeeper badge

A module that abstracts the process of consuming a REST endpoint from both client and server side.

Installing

npm i restful-model;

Usage

const RestService = require('restful-model');

Create a new service endpoint

const userService = new RestService('http://example.com/api/v1');
const userModel = userService.registerModel('User', '/users');

// get all users
const users = await userModel.query({view: 'thin'}); // HTTP GET http://example.com/api/v1/users?view=thin

// get user by ID
const user = await userModel.get({id: 1, view: 'full'}); // HTTP GET http://example.com/api/v1/users/1?view=full

// create user
const user = await userModel.create({full_name: "John Doe"}); // HTTP POST http://example.com/api/v1/users

// update user
const user = await userModel.update({id: 1}, {full_name: "John Doe"}); // HTTP PUT http://example.com/api/v1/users/1

// delete user
const result = await userModel.delete({id: 1}); // HTTP DELETE http://example.com/api/v1/users/1

Note: keys supplied in the first arguments for the above methods are used as params in path and query string. To use a param in the path give it the same name as the path placeholder. All other params would be used in query string.

Custom endpoints

Use RestService model config to configure models

const userService = new RestService('http://example.com/api/v1');
const modelConfig = RestService.modelConfig().customActions({
                            getFiends: {
                              method: 'get',
                              path: '/:id/friends',
                            }
                          });
const userModel = userService.registerModel('User', '/users', modelConfig);
const friends = await userModel.getFiends({id: 1, view: 'thin'});
// HTTP GET http://example.com/api/v1/users/1/friends?view=thin

Configure a model settings

Models can be configured using the RestService.modelConfig() method. This method return an instance of ModelConfig class.

Model Config - setIdField()

Model id field can be configured using the setIdField method the the ModelConfig class.

const modelConfig = RestService.modelConfig().setIdField('userId'); // configure the name of the ID field
const userService = new RestService('http://example.com/api/v1');
const userModel = userService.registerModel('Users','/users')

Model Config - relationships

NOTE: You have much better and more sophisticated options like GraphQL for querying related models. However, not all services provide the option to consume their data via GraphQL. This means that you might have to write your own resolvers and schemas to consume a service that do not support GraphQL out of the box. In cases where using tools like GraphQL do not meet your requirements, you can use relationship features in this package.

Relationship can be configured using the hasMany and hasOne methods of the ModelConfig class. These 2 methods of the ModelConfig class define model relationship and have the same signature.

ParameterDescriptionTypeDefault Value
name(required)The name of the modelstring
fieldNameThe name of the field when joining models. Also used as a relation keystring
foreignField(optional) or config When this argument is a string, it is used as the foreign key of the referenced model. When this argument is an object the next argument is skipped and all setting can be defined in it. Options: - using (string) define the method to call on the referenced model - localField(string) - foreignField(string) - fetchMode(string) (combined|exclusive) default('combined') when combined one request is sent for all entries when exclusive a request is made per entry - params (object) used to define path and query params. Giving a param the same name as a placeholder in the path would inject it in the path. All other params would be used in query string. To use a param from an entry set it value to the name of the target field and prefix it by an @ signstring or object'id'
localField(optional)The local field in the relation. Ignored when foreignField is an objectstring'id'

Model relationships: Configuration Example

// define services
const articleService = new RestService('http://articles.example.com/api/v1');
const authorService = new RestService('http://authors.example.com/api/v1');
const commentService = new RestService('http:/comments.example.com/api/v1');

// define models
const modelConfig = RestService.modelConfig()
                         .hasMany('Comments', 'comments', 'articleId')
                         .hasOne('Author', 'author', 'id', 'authorId');
const articleModel = articleService.registerModel('Article', '/articles', modelConfig);
const authorModel = authorService.registerModel('Author', '/authors');
const commentModel = commentService.registerModel('Comment', '/comments');

Model relationship: Example use cases with combined fetchMode

Get a single item (article) and fetch related entities (author, comment)
// Would get an article model with 2 extra fields author (the fetched author) and comments (array of fetched comments)
const article = await articleModel.get({id: i}, ['author','comments']);
// HTTP GET http://example.com/api/v1/articles/1
// HTTP GET http://example.com/api/v1/authors?id[]=<article.authorId>
// HTTP GET http://example.com/api/v1/comments?articleId[]=<user.id>
Get a multiple items (article) and fetch related entities (author, comment)
// Would get articles. Each item would have model with 2 extra fields author (the fetched author) and comments (array of fetched comments)
const article = await articleModel.query({}, ['author','comments']);
// HTTP GET http://example.com/api/v1/articles
// HTTP GET http://example.com/api/v1/authors?id[]=<article1.authorId>&id[]=<article2.authorId>
// HTTP GET http://example.com/api/v1/comments?articleId[]=<article1.id>&articleId[]=<article2.id>
Limitations

The examples above use a fetching of related entities with fetchMode of type combined (see). This means that a single request is made to access the related entities of retrieved items. In the above example, a single request is made to retrieve all authors related to all collected articles. This single request is made with all author IDs in a query string. However, this could cause lengthy Request URLs. And since URL have limited size (depends on the server and browser), request will large data set would fail. It is advised to use it only on small data set.

Model relationships: Example use cases with exclusive fetchMode

// set up
const articleConfig = RestService.modelConfig()
      .hasMany('Media', 'images', {
      fetchMode: 'exclusive', // exclusive | combined , exclusive = 1 request per entry, combined 1 request for all entry with query string filter
      params: {content_type: 'articles', media_type: 'images', content_id: '@id', size: 'thumb'}
    });
const articleModel = articleService.registerModel('Article', '/articles', articleConfig);
const authorModel = authorService.registerModel('Author', '/authors', RestService.modelConfig().hasMany('Article', 'post').hasOne('Media', 'photo', {
  fetchMode: 'exclusive',
  using: 'get',
  params: {content_type: 'authors', media_type: 'images', content_id: '@id', id: '@profilePhotoId', size: 'medium'}
}));
const media = mediaService.registerModel('Media', '/:content_type/:content_id/media/:media_type');
Get a single item with related entity (hasOne)
const author = await authorModel.get({id: 107}, ['photo']);
// HTTP GET http://authors.example.com/api/v1/authors/107
// HTTP GET http://media.example.com/api/v1/authors/107/media/images/<author.profilePhotoId>
Get a single item with related entities (hasMany)
const article = await articleModel.get({id: 1}, ['images']);
// HTTP GET http://authors.example.com/api/v1/authors/107
// HTTP GET http://media.example.com/api/v1/articles/1/media/images
Get multiple items with related entities (hasOne)
const authors = await authorModel.query({}, ['photo']);
// One request for all authors
// HTTP GET http://authors.example.com/api/v1/authors/107
// One request for image per author
// HTTP GET http://media.example.com/api/v1/authors/<author1.id>/media/images/<author1.profilePhotoId>
// HTTP GET http://media.example.com/api/v1/authors/<author2.id>/media/images/<author2.profilePhotoId>
// ....
Get multiple items with related entities (hasMany)
const article = await articleModel.query({}, ['images']);
// One request for all articles
// HTTP GET http://articles.example.com/api/v1/articles
// One request for images per articles
// HTTP GET http://media.example.com/api/v1/articles/<article1.id>/media/images
// HTTP GET http://media.example.com/api/v1/articles/<article2.id>/media/images
// ....
Limitations

A request is made for each item. Consider using caching where relevant.

Middlewares

Middlewares are used to process server requests. When defining middlewares the order of middlewares is important:

  • The first middleware receive the initial input. The initial input is the request options object.
  • The last middleware must return the final data of the response.

Defaults middlewares

There are two default middlewares, a Request middleware and a Response middleware.When using the default middlewares, processing a request has the following steps

  • The first middleware receives the request options makes a request then passes the response to the next middleware
  • The Response middleware receives the response as input and return the response data.

Writing middleware

Middleware function arguments

ParameterTypeDescription
inputmixedThe input value would depend on the preceding middleware. It can be a request options a response or anything else. The first middleware always receives the request options.
nextfunctionThe callback used to deliver the middleware result to the next middleware.
resolvefunctionThe resolver callback. This callback can be used to skip the rest of the middlewares and resolve the request with response data.
contextobjectContext object for sharing data with middlewares.

Aside the arguments listed above, other arguments can be passed to the following middleware by calling next() with additional arguments. The additional arguments would be appended the argument list of the next middleware function.

function(input, next, resolve, context, ...extraArgs) {
  const result = process(input); //.... do something with input
  if(/*some condition*/){
     next(result, ...extraArgs)// pass result to next middleware
  } else {
    resolve(result);// exit middleware chain with result
  }
}

Example: Adding Basic Authentication middleware

const RestService = require('restful-model');
const {fetchRequest, fetchResponse} = RestService.defaultMiddlewares;

const userService = new RestService('http://example.com/api/v1');

async function addBasicAuthHeader (httpRequestOptions, next) {
  const {username, password} = await getAuthData(/*...*/);// imaginary function
  const hash = base64Encode(`${username}:${password}`); // imaginary function
  // add authorisation header
  httpRequestOptions.headers['Authorization'] = `Basic ${hash}`;
  // pass request options to the next middleware (fetchRequest)
  next(httpRequestOptions);
}

userService.useMiddlewares([addBasicAuthHeader, fetchRequest, fetchResponse]);

Example: Passing context to middlewares for oauth

Setup

const RestService = require('restful-model');
const {OAuth} = require('oauth');

const {fetchRequest, fetchResponse} = RestService.defaultMiddlewares;
const userService = new RestService('http://example.com/api/v1');

const oauthClient = new OAuth(
  'http://some.3rd.party.api/api/v1',
  'http://example.com/api/access_token',
  clientID,
  clientSecret,
  '1.0',
  callbackURL,
  'HMAC-SHA1'
);

async function addOAuthHeader (httpRequestOptions, next, resolve, {request: requestContext}) {
  const {accessToken, accessTokenSecret} =  requestContext;
  const authHeader = oauthClient.authHeader(input.url, accessToken, accessTokenSecret, input.method)
  // add authorisation header
  httpRequestOptions.headers['Authorization'] = authHeader;
  // pass request options to the next middleware (fetchRequest)
  next(httpRequestOptions);
}

userService.useMiddlewares([addBasicAuthHeader, fetchRequest, fetchResponse]);

Now make request and pass request context

const {accessToken, accessTokenSecret} = await getUserToken(/*...*/)// imaginary function

// In the last argument we pass access token data to middlewares
let users = await userModel.query({view: 'thin'}, [], {accessToken, accessTokenSecret});

Example: Adding Cache middleware

const userService = new RestService('http://example.com/api/v1');
const userModel = userService.registerModel('User', '/users');

let cache = {};// very minimal cache. Ideally a more sophistictated cache should be used.

const requestHandler = function requestHandler(input, next, resolve) {
  if (cache[input.url]) { // use caches
    resolve(cache[input.url]);
  } else { // continure with the request
    const response = await makeRequest(input);// imaginary function
    next(response, input);// passes the original input. the next middleware would receive it as extra argument
  }
};

const responseHandler = function responseHandler(input, next, resolve, originalInput) {
  if(originalIpunt.method === 'GET') {
     cache[originalInput.url] = input;// save only get request
  } else if(originalIpunt.method !== 'GET' && cache[originalInput.url]) { // PUT, DELETE, POST
    delete cache[originalInput.url]; delete GET path if model was updated
  }
  next(input);
};

userService.useMiddlewares([requestHandler, responseHandler]);

// get all users - this call goes to the server
let users = await userModel.query({view: 'thin'}); // HTTP GET http://example.com/api/v1/users?view=thin

// get all users - the next call does not. Data is fetched from cache
users = await userModel.query({view: 'thin'}); // HTTP GET http://example.com/api/v1/users?view=thin

// get all users - this call goes to the server since a different param was passed
users = await userModel.query({view: 'full'}); // HTTP GET http://example.com/api/v1/users?view=thin
0.1.8

6 years ago

0.1.6

6 years ago

0.1.5

6 years ago

0.1.4

6 years ago

0.1.3

6 years ago

0.1.2

6 years ago

0.1.1

6 years ago

0.0.18

6 years ago

0.0.17

6 years ago

0.0.16

6 years ago

0.0.15

6 years ago

0.0.14

6 years ago

0.0.13

6 years ago

0.0.12

6 years ago

0.0.11

6 years ago

0.0.10

6 years ago

0.0.9

6 years ago

0.0.8

6 years ago

0.0.7

6 years ago

0.0.6

6 years ago

0.0.5

6 years ago

0.0.4

6 years ago

0.0.3

6 years ago

0.0.2

6 years ago

0.0.1

6 years ago