Giter VIP home page Giter VIP logo

mappersmith's Introduction

npm version Node.js CI Windows Tests

Mappersmith

Mappersmith is a lightweight rest client for node.js and the browser. It creates a client for your API, gathering all configurations into a single place, freeing your code from HTTP configurations.

Table of Contents

Installation

npm install mappersmith --save

or

yarn add mappersmith

Build from the source

Install the dependencies

yarn

Build

yarn build
yarn release # for minified version

Usage

To create a client for your API you will need to provide a simple manifest. If your API reside in the same domain as your app you can skip the host configuration. Each resource has a name and a list of methods with its definitions, like:

import forge, { configs } from "mappersmith"
import { Fetch } from "mappersmith/gateway/fetch"

configs.gateway = Fetch;

const github = forge({
  clientId: "github",
  host: "https://www.githubstatus.com",
  resources: {
    Status: {
      current: { path: "/api/v2/status.json" },
      summary: { path: "/api/v2/summary.json" },
      components: { path: "/api/v2/components.json" },
    },
  },
});

github.Status.current().then((response) => {
  console.log(`summary`, response.data());
});

Commonjs

If you are using commonjs, your require should look like:

const forge = require("mappersmith").default;
const { configs } = require("mappersmith");
const FetchGateway = require("mappersmith/gateway/fetch").default;

Configuring my resources

Each resource has a name and a list of methods with its definitions. A method definition can have host, path, method, headers, params, bodyAttr, headersAttr and authAttr. Example:

const client = forge({
  resources: {
    User: {
      all: { path: '/users' },

      // {id} is a dynamic segment and will be replaced by the parameter "id"
      // when called
      byId: { path: '/users/{id}' },

      // {group} is also a dynamic segment but it has default value "general"
      byGroup: { path: '/users/groups/{group}', params: { group: 'general' } },

      // {market?} is an optional dynamic segment. If called without a value
      // for the "market" parameter, {market?} will be removed from the path
      // including any prefixing "/".
      // This example: '/{market?}/users' => '/users'
      count: { path: '/{market?}/users' } }
    },
    Blog: {
      // The HTTP method can be configured through the `method` key, and a default
      // header "X-Special-Header" has been configured for this resource
      create: { method: 'post', path: '/blogs', headers: { 'X-Special-Header': 'value' } },

      // There are no restrictions for dynamic segments and HTTP methods
      addComment: { method: 'put', path: '/blogs/{id}/comment' },

      // `queryParamAlias` will map parameter names to their alias when
      // constructing the query string
      bySubject: { path: '/blogs', queryParamAlias: { subjectId: 'subject_id' } },

      // `path` is a function to map passed params to a custom path
      byDate: { path: ({date}) => `${date.getYear()}/${date.getMonth()}/${date.getDate()}` }
    }
  }
})

Parameters

If your method doesn't require any parameter, you can just call it without them:

client.User
  .all() // https://my.api.com/users
  .then((response) => console.log(response.data()))
  .catch((response) => console.error(response.data()))

Every parameter that doesn't match a pattern {parameter-name} in path will be sent as part of the query string:

client.User.all({ active: true }) // https://my.api.com/users?active=true

When a method requires a parameters and the method is called without it, Mappersmith will raise an error:

client.User.byId(/* missing id */)
// throw '[Mappersmith] required parameter missing (id), "/users/{id}" cannot be resolved'

You can optionally set parameterEncoder: yourEncodingFunction to change the default encoding function for parameters. This is useful when you are calling an endpoint which for example requires not encoded characters like : that are otherwise encoded by the default behaviour of the encodeURIComponent function (external documentation).

const client = forge({
  host: 'https://custom-host.com',
  parameterEncoder: yourEncodingFunction,
  resources: { ... }
})

Default Parameters

It is possible to configure default parameters for your resources, just use the key params in the definition. It will replace params in the URL or include query strings.

If we call client.User.byGroup without any params it will default group to "general"

client.User.byGroup() // https://my.api.com/users/groups/general

And, of course, we can override the defaults:

client.User.byGroup({ group: 'cool' }) // https://my.api.com/users/groups/cool

Renaming query parameters

Sometimes the expected format of your query parameters doesn't match that of your codebase. For example, maybe you're using camelCase in your code but the API you are calling expects snake_case. In that case, set queryParamAlias in the definition to an object that describes a mapping between your input parameter and the desired output format.

This mapping will not be applied to params in the URL.

client.Blog.all({ subjectId: 10 }) // https://my.api.com/blogs?subject_id=10

Body

To send values in the request body (usually for POST, PUT or PATCH methods) you will use the special parameter body:

client.Blog.create({
  body: {
    title: 'Title',
    tags: ['party', 'launch']
  }
})

By default, it will create a urlencoded version of the object (title=Title&tags[]=party&tags[]=launch). If the body used is not an object it will use the original value. If body is not possible as a special parameter for your API you can configure it through the param bodyAttr:

// ...
{
  create: { method: 'post', path: '/blogs', bodyAttr: 'payload' }
}
// ...

client.Blog.create({
  payload: {
    title: 'Title',
    tags: ['party', 'launch']
  }
})

NOTE: It's possible to post body as JSON, check the EncodeJsonMiddleware below for more information NOTE: The bodyAttr param can be set at manifest level.

Headers

To define headers in the method call use the parameter headers:

client.User.all({ headers: { Authorization: 'token 1d1435k' } })

If headers is not possible as a special parameter for your API you can configure it through the param headersAttr:

// ...
{
  all: { path: '/users', headersAttr: 'h' }
}
// ...

client.User.all({ h: { Authorization: 'token 1d1435k' } })

NOTE: The headersAttr param can be set at manifest level.

Basic auth

To define credentials for basic auth use the parameter auth:

client.User.all({ auth: { username: 'bob', password: 'bob' } })

The available attributes are: username and password. This will set an Authorization header. This can still be overridden by custom headers.

If auth is not possible as a special parameter for your API you can configure it through the param authAttr:

// ...
{
  all: { path: '/users', authAttr: 'secret' }
}
// ...

client.User.all({ secret: { username: 'bob', password: 'bob' } })

NOTE: A default basic auth can be configured with the use of the BasicAuthMiddleware, check the middleware section below for more information. NOTE: The authAttr param can be set at manifest level.

Timeout

To define the number of milliseconds before the request times out use the parameter timeout:

client.User.all({ timeout: 1000 })

If timeout is not possible as a special parameter for your API you can configure it through the param timeoutAttr:

// ...
{
  all: { path: '/users', timeoutAttr: 'maxWait' }
}
// ...

client.User.all({ maxWait: 500 })

NOTE: A default timeout can be configured with the use of the TimeoutMiddleware, check the middleware section below for more information. NOTE: The timeoutAttr param can be set at manifest level.

Alternative host

There are some cases where a resource method resides in another host, in those cases you can use the host key to configure a new host:

// ...
{
  all: { path: '/users', host: 'http://old-api.com' }
}
// ...

client.User.all() // http://old-api.com/users

In case you need to overwrite the host for a specific call, you can do so through the param host:

// ...
{
  all: { path: '/users', host: 'http://old-api.com' }
}
// ...

client.User.all({ host: 'http://very-old-api.com' }) // http://very-old-api.com/users

If host is not possible as a special parameter for your API, you can configure it through the param hostAttr:

// ...
{
  all: { path: '/users', hostAttr: 'baseUrl' }
}
// ...

client.User.all({ baseUrl: 'http://very-old-api.com' }) // http://very-old-api.com/users

NOTE: Since version 2.34.0 you need to also use allowResourceHostOverride: true, example:

const client = forge({
  host: 'https://new-host.com',
  allowResourceHostOverride: true,
  resources: {
    User: {
      all: { path: '/users', host: 'https://old-host.com }
    }
  }
})

Whenever using host overrides, be diligent about how you pass parameters to your resource methods. If you spread unverified attributes, you might open your server to SSR attacks.

Alternative path

In case you need to overwrite the path for a specific call, you can do so through the param path:

// ...
{
  all: { path: '/users' }
}
// ...

client.User.all({ path: '/people' })

If path is not possible as a special parameter for your API, you can configure it through the param pathAttr:

// ...
{
  all: { path: '/users', pathAttr: '__path' }
}
// ...

client.User.all({ __path: '/people' })

Binary data

If the data being fetched is in binary form, such as a PDF, you may add the binary key, and set it to true. The response data will then be a Buffer in NodeJS, and a Blob in the browser.

// ...
{
  report: { path: '/report.pdf', binary: true }
}
// ...

Promises

Mappersmith does not apply any polyfills, it depends on a native Promise implementation to be supported. If your environment doesn't support Promises, please apply the polyfill first. One option can be then/promises

In some cases it is not possible to use/assign the global Promise constant, for those cases you can define the promise implementation used by Mappersmith.

For example, using the project rsvp.js (a tiny implementation of Promises/A+):

import RSVP from 'rsvp'
import { configs } from 'mappersmith'

configs.Promise = RSVP.Promise

All Promise references in Mappersmith use configs.Promise. The default value is the global Promise.

Response object

Mappersmith will provide an instance of its own Response object to the promises. This object has the methods:

  • request() - Returns the original Request
  • status() - Returns the status number
  • success() - Returns true for status greater than 200 and lower than 400
  • headers() - Returns an object with all headers, keys in lower case
  • header(name) - Returns the value of the header
  • data() - Returns the response data, if Content-Type is application/json it parses the response and returns an object
  • error() - Returns the last error instance that caused the request to fail or null

Middleware

The behavior between your client and the API can be customized with middleware. A middleware is a function which returns an object with two methods: request and response.

Creating middleware

The prepareRequest method receives a function which returns a Promise resolving the Request. This function must return a Promise resolving the request. The method enhance can be used to generate a new request based on the previous one.

const MyMiddleware = () => ({
  prepareRequest(next) {
    return next().then(request => request.enhance({
      headers: { 'x-special-request': '->' }
    }))
  }
})

If you have multiple middleware it is possible to pass information from an earlier ran middleware to a later one via the request context:

const MyMiddlewareOne = () => ({
  async prepareRequest(next) {
    const request = await next().then(request => request.enhance({}, { message: 'hello from mw1' }))
  }
})

const MyMiddlewareTwo = () => ({
  async prepareRequest(next) {
    const request = await next()
    const { message } = request.getContext()
    // Logs: "hello from mw1"
    console.log(message)
    return request
  }
})

The above example assumes you synthesized your middleware in this order when calling forge: middleware: [MyMiddlewareOne, MyMiddlewareTwo]

The response method receives a function which returns a Promise resolving the Response. This function must return a Promise resolving the Response. The method enhance can be used to generate a new response based on the previous one.

const MyMiddleware = () => ({
  response(next) {
    return next().then((response) => response.enhance({
      headers: { 'x-special-response': '<-' }
    }))
  }
})

Context (deprecated)

โš ๏ธ setContext is not safe for concurrent use, and shouldn't be used!

Why is it not safe? Basically, the setContext function mutates a global state (see here), hence it is the last call to setContext that decides its global value. Which leads to a race condition when handling concurrent requests.

Optional arguments

It can, optionally, receive resourceName, resourceMethod, #context, clientId and mockRequest. Example:

const MyMiddleware = ({ resourceName, resourceMethod, context, clientId, mockRequest }) => ({
  /* ... */
})

client.User.all()
// resourceName: 'User'
// resourceMethod: 'all'
// clientId: 'myClient'
// context: {}
// mockRequest: false
mockRequest

Before mocked clients can assert whether or not their mock definition matches a request they have to execute their middleware on that request. This means that middleware might be executed multiple times for the same request. More specifically, the middleware will be executed once per mocked client that utilises the middleware until a mocked client with a matching definition is found. If you want to avoid middleware from being called multiple times you can use the optional "mockRequest" boolean flag. The value of this flag will be truthy whenever the middleware is being executed during the mock definition matching phase. Otherwise its value will be falsy. Example:

const MyMiddleware = ({ mockRequest }) => {
  prepareRequest(next) {
    if (mockRequest) {
      ... // executed once for each mocked client that utilises the middleware
    }
    if (!mockRequest) {
      ... // executed once for the matching mock definition
    }
    return next().then(request => request)
  }
}
Abort

The prepareRequest phase can optionally receive a function called "abort". This function can be used to abort the middleware execution early-on and throw a custom error to the user. Example:

const MyMiddleware = () => {
  prepareRequest(next, abort) {
    return next().then(request =>
      request.header('x-special')
        ? response
        : abort(new Error('"x-special" must be set!'))
    )
  }
}
Renew

The response phase can optionally receive a function called "renew". This function can be used to rerun the middleware stack. This feature is useful in some scenarios, for example, automatically refreshing an expired access token. Example:

const AccessTokenMiddleware = () => {
  // maybe this is stored elsewhere, here for simplicity
  let accessToken = null

  return () => ({
    request(request) {
      return Promise
        .resolve(accessToken)
        .then((token) => token || fetchAccessToken())
        .then((token) => {
          accessToken = token
          return request.enhance({
            headers: { 'Authorization': `Token ${token}` }
          })
        })
    },
    response(next, renew) {
      return next().catch(response => {
        if (response.status() === 401) { // token expired
          accessToken = null
          return renew()
        }

        return next()
      })
    }
  })
}

Then:

const AccessToken = AccessTokenMiddleware()
const client = forge({
  // ...
  middleware: [ AccessToken ],
  // ...
})

"renew" can only be invoked sometimes before it's considered an infinite loop, make sure your middleware can distinguish an error from a "renew". By default, mappersmith will allow 2 calls to "renew". This can be configured with configs.maxMiddlewareStackExecutionAllowed. It's advised to keep this number low. Example:

import { configs } from 'mappersmith'
configs.maxMiddlewareStackExecutionAllowed = 3

If an infinite loop is detected, mappersmith will throw an error.

request

The response phase can optionally receive an argument called "request". This argument is the final request (after the whole middleware chain has prepared and all prepareRequest been executed). This is useful in some scenarios, for example when you want to get access to the request without invoking next:

const CircuitBreakerMiddleware = () => {
  return () => ({
    response(next, renew, request) {
      // Creating the breaker required some information available only on `request`:
      const breaker = createBreaker({ ..., timeout: request.timeout })
      // Note: `next` is still wrapped:
      return breaker.invoke(createExecutor(next))
    }
  })
}

Configuring middleware

Middleware scope can be Global, Client or on Resource level. The order will be applied in this order: Resource level applies first, then Client level, and finally Global level. The subsections below describes the differences and how to use them correctly.

Resource level middleware

Resource middleware are configured using the key middleware in the resource level of manifest, example:

const client = forge({
  clientId: 'myClient',
  resources: {
    User: {
      all: {
        // only the `all` resource will include MyMiddleware:
        middleware: [ MyMiddleware ],
        path: '/users'
      }
    }
  }
})

Client level middleware

Client middleware are configured using the key middleware in the root level of manifest, example:

const client = forge({
  clientId: 'myClient',
  // all resources in this client will include MyMiddleware:
  middleware: [ MyMiddleware ],
  resources: {
    User: {
      all: { path: '/users' }
    }
  }
})

Global middleware

Global middleware are configured on a config level, and all new clients will automatically include the defined middleware, example:

import { forge, configs } from 'mappersmith'

configs.middleware = [MyMiddleware]
// all clients defined from now on will include MyMiddleware
  • Global middleware can be disabled for specific clients with the option ignoreGlobalMiddleware, e.g:
forge({
  ignoreGlobalMiddleware: true,
  // + the usual configurations
})

Built-in middleware

BasicAuth

Automatically configure your requests with basic auth

import { BasicAuthMiddleware } from 'mappersmith/middleware'
const BasicAuth = BasicAuthMiddleware({ username: 'bob', password: 'bob' })

const client = forge({
  middleware: [ BasicAuth ],
  /* ... */
})

client.User.all()
// => header: "Authorization: Basic Ym9iOmJvYg=="

** The default auth can be overridden with the explicit use of the auth parameter, example:

client.User.all({ auth: { username: 'bill', password: 'bill' } })
// auth will be { username: 'bill', password: 'bill' } instead of { username: 'bob', password: 'bob' }

CSRF

Automatically configure your requests by adding a header with the value of a cookie - If it exists. The name of the cookie (defaults to "csrfToken") and the header (defaults to "x-csrf-token") can be set as following;

import { CsrfMiddleware } from 'mappersmith/middleware'

const client = forge({
  middleware: [ CsrfMiddleware('csrfToken', 'x-csrf-token') ],
  /* ... */
})

client.User.all()

Duration

Automatically adds X-Started-At, X-Ended-At and X-Duration headers to the response.

import { DurationMiddleware } from 'mappersmith/middleware'

const client = forge({
  middleware: [ DurationMiddleware ],
  /* ... */
})

client.User.all({ body: { name: 'bob' } })
// => headers: "X-Started-At=1492529128453;X-Ended-At=1492529128473;X-Duration=20"

EncodeJson

Automatically encode your objects into JSON

import { EncodeJsonMiddleware } from 'mappersmith/middleware'

const client = forge({
  middleware: [ EncodeJsonMiddleware ],
  /* ... */
})

client.User.all({ body: { name: 'bob' } })
// => body: {"name":"bob"}
// => header: "Content-Type=application/json;charset=utf-8"

GlobalErrorHandler

Provides a catch-all function for all requests. If the catch-all function returns true it prevents the original promise to continue.

import { GlobalErrorHandlerMiddleware, setErrorHandler } from 'mappersmith/middleware'

setErrorHandler((response) => {
  console.log('global error handler')
  return response.status() === 500
})

const client = forge({
  middleware: [ GlobalErrorHandlerMiddleware ],
  /* ... */
})

client.User
  .all()
  .catch((response) => console.error('my error'))

// If status != 500
// output:
//   -> global error handler
//   -> my error

// IF status == 500
// output:
//   -> global error handler

Log

Log all requests and responses. Might be useful in development mode.

import { LogMiddleware } from 'mappersmith/middleware'

const client = forge({
  middleware: [ LogMiddleware ],
  /* ... */
})

Retry

This middleware will automatically retry GET requests up to the configured amount of retries using a randomization function that grows exponentially. The retry count and the time used will be included as a header in the response. By default on requests with response statuses >= 500 will be retried.

It's possible to configure the header names and parameters used in the calculation by providing a configuration object when creating the middleware.

If no configuration is passed when creating the middleware then the defaults will be used.

import { RetryMiddleware } from 'mappersmith/middleware'

const retryConfigs = {
  headerRetryCount: 'X-Mappersmith-Retry-Count',
  headerRetryTime: 'X-Mappersmith-Retry-Time',
  maxRetryTimeInSecs: 5,
  initialRetryTimeInSecs: 0.1,
  factor: 0.2, // randomization factor
  multiplier: 2, // exponential factor
  retries: 5, // max retries
  validateRetry: (response) => response.responseStatus >= 500 // a function that returns true if the request should be retried
}

const client = forge({
  middleware: [ Retry(retryConfigs) ],
  /* ... */
})

Timeout

Automatically configure your requests with a default timeout

import { TimeoutMiddleware } from 'mappersmith/middleware'
const Timeout = TimeoutMiddleware(500)

const client = forge({
  middleware: [ Timeout ],
  /* ... */
})

client.User.all()

** The default timeout can be overridden with the explicit use of the timeout parameter, example:

client.User.all({ timeout: 100 })
// timeout will be 100 instead of 500

Middleware legacy notes

This section is only relevant for mappersmith versions older than but not including 2.27.0, when the method prepareRequest did not exist. This section describes how to create a middleware using older versions.

Since version 2.27.0 a new method was introduced: prepareRequest. This method aims to replace the request method in future versions of mappersmith, it has a similar signature as the response method and it is always async. All previous middleware are backward compatible, the default implementation of prepareRequest will call the request method if it exists.

The request method receives an instance of the Request object and it must return a Request. The method enhance can be used to generate a new request based on the previous one.

Example:

const MyMiddleware = () => ({
  request(request) {
    return request.enhance({
      headers: { 'x-special-request': '->' }
    })
  },

  response(next) {
    return next().then((response) => response.enhance({
      headers: { 'x-special-response': '<-' }
    }))
  }
})

The request phase can be asynchronous, just return a promise resolving a request. Example:

const MyMiddleware = () => ({
  request(request) {
    return Promise.resolve(
      request.enhance({
        headers: { 'x-special-token': 'abc123' }
      })
    )
  }
})

Testing Mappersmith

Mappersmith plays nice with all test frameworks, the generated client is a plain javascript object and all the methods can be mocked without any problem. However, this experience can be greatly improved with the test library.

The test library has 4 utilities: install, uninstall, mockClient and mockRequest

install and uninstall

They are used to setup the test library, example using jasmine:

import { install, uninstall } from 'mappersmith/test'

describe('Feature', () => {
  beforeEach(() => install())
  afterEach(() => uninstall())
})

mockClient

mockClient offers a high level abstraction, it works directly on your client mocking the resources and their methods.

It accepts the methods:

  • resource(resourceName), ex: resource('Users')
  • method(resourceMethodName), ex: method('byId')
  • with(resourceMethodArguments), ex: with({ id: 1 })
  • status(statusNumber | statusHandler), ex: status(204) or status((request, mock) => 200)
  • headers(responseHeaders), ex: headers({ 'x-header': 'value' })
  • response(responseData | responseHandler), ex: response({ user: { id: 1 } }) or response((request, mock) => ({ user: { id: request.body().id } }))
  • assertObject()
  • assertObjectAsync()

Example using jasmine:

import { forge } from 'mappersmith'
import { install, uninstall, mockClient } from 'mappersmith/test'

describe('Feature', () => {
  beforeEach(() => install())
  afterEach(() => uninstall())

  it('works', (done) => {
    const myManifest = {} // Let's assume I have my manifest here
    const client = forge(myManifest)

    mockClient(client)
      .resource('User')
      .method('all')
      .response({ allUsers: [{id: 1}] })

    // now if I call my resource method, it should return my mock response
    client.User
      .all()
      .then((response) => expect(response.data()).toEqual({ allUsers: [{id: 1}] }))
      .then(done)
  })
})

To mock a failure just use the correct HTTP status, example:

// ...
mockClient(client)
  .resource('User')
  .method('byId')
  .with({ id: 'ABC' })
  .status(422)
  .response({ error: 'invalid ID' })
// ...

The method with accepts the body and headers attributes, example:

// ...
mockClient(client)
  .with({
    id: 'abc',
    headers: { 'x-special': 'value'},
    body: { payload: 1 }
  })
  // ...

It's possible to use a match function to assert params and body, example:

import { m } from 'mappersmith/test'

mockClient(client)
  .with({
    id: 'abc',
    name: m.stringContaining('john'),
    headers: { 'x-special': 'value'},
    body: m.stringMatching(/token=[^&]+&other=true$/)
  })

The assert object can be used to retrieve the requests that went through the created mock, example:

const mock = mockClient(client)
  .resource('User')
  .method('all')
  .response({ allUsers: [{id: 1}] })
  .assertObject()

console.log(mock.mostRecentCall())
console.log(mock.callsCount())
console.log(mock.calls())

The mock object is an instance of MockAssert and exposes three methods:

  • calls(): returns a Request array;
  • mostRecentCall(): returns the last Request made. Returns null if array is empty.
  • callsCount(): returns the number of requests that were made through the mocked client;

Note: The assert object will also be returned in the mockRequest function call.

If you have a middleware with an async request phase use assertObjectAsync to await for the middleware execution, example:

const mock = await mockClient(client)
  .resource('User')
  .method('all')
  .response({ allUsers: [{id: 1}] })
  .assertObjectAsync()

console.log(mock.mostRecentCall())
console.log(mock.callsCount())
console.log(mock.calls())

response and status can accept functions to generate response body or status. This can be useful when you want to return different responses for the same request being made several times.

const generateResponse = () => {
  return (request, mock) => mock.callsCount() === 0
    ? {}
    : { user: { id: 1 } }
}

const mock = mockClient(client)
  .resource('User')
  .method('create')
  .response(generateResponse())

mockRequest

mockRequest offers a low level abstraction, very useful for automations.

It accepts the params: method, url, body and response

It returns an assert object

Example using jasmine:

import { forge } from 'mappersmith'
import { install, uninstall, mockRequest } from 'mappersmith/test'

describe('Feature', () => {
  beforeEach(() => install())
  afterEach(() => uninstall())

  it('works', (done) => {
    mockRequest({
      method: 'get',
      url: 'https://my.api.com/users?someParam=true',
      response: {
        body: { allUsers: [{id: 1}] }
      }
    })

    const myManifest = {} // Let's assume I have my manifest here
    const client = forge(myManifest)

    client.User
      .all()
      .then((response) => expect(response.data()).toEqual({ allUsers: [{id: 1}] }))
      .then(done)
  })
})

A more complete example:

// ...
mockRequest({
  method: 'post',
  url: 'http://example.org/blogs',
  body: 'param1=A&param2=B', // request body
  response: {
    status: 503,
    body: { error: true },
    headers: { 'x-header': 'nope' }
  }
})
// ...

It's possible to use a match function to assert the body and the URL, example:

import { m } from 'mappersmith/test'

mockRequest({
  method: 'post',
  url: m.stringMatching(/example\.org/),
  body: m.anything(),
  response: {
    body: { allUsers: [{id: 1}] }
  }
})

Using the assert object:

const mock = mockRequest({
  method: 'get',
  url: 'https://my.api.com/users?someParam=true',
  response: {
    body: { allUsers: [{id: 1}] }
  }
})

console.log(mock.mostRecentCall())
console.log(mock.callsCount())
console.log(mock.calls())

Match functions

mockClient and mockRequest accept match functions, the available built-in match functions are:

import { m } from 'mappersmith/test'

m.stringMatching(/something/) // accepts a regexp
m.stringContaining('some-string') // accepts a string
m.anything()
m.uuid4()

A match function is a function which returns a boolean, example:

mockClient(client)
  .with({
    id: 'abc',
    headers: { 'x-special': 'value'},
    body: (body) => body === 'something'
  })

Note: mockClient only accepts match functions for body and params mockRequest only accepts match functions for body and url

unusedMocks

unusedMocks can be used to check if there are any unused mocks after each test. It will return count of unused mocks. It can be either unused mockRequest or mockClient.

import { install, uninstall, unusedMocks } from 'mappersmith/test'

describe('Feature', () => {
  beforeEach(() => install())
  afterEach(() => {
    const unusedMocksCount = unusedMocks()
    uninstall()
    if (unusedMocksCount > 0) {
      throw new Error(`There are ${unusedMocksCount} unused mocks`) // fail the test
    }
  })
})

Gateways

Mappersmith has a pluggable transport layer and it includes by default three gateways: xhr, http and fetch. Mappersmith will pick the correct gateway based on the environment you are running (nodejs or the browser).

You can write your own gateway, take a look at XHR for an example. To configure, import the configs object and assign the gateway option, like:

import { configs } from 'mappersmith'
configs.gateway = MyGateway

It's possible to globally configure your gateway through the option gatewayConfigs.

HTTP

When running with node.js you can configure the configure callback to further customize the http/https module, example:

import fs from 'fs'
import https from 'https'
import { configs } from 'mappersmith'

const key = fs.readFileSync('/path/to/my-key.pem')
const cert =  fs.readFileSync('/path/to/my-cert.pem')

configs.gatewayConfigs.HTTP = {
  configure() {
    return {
      agent: new https.Agent({ key, cert })
    }
  }
}

The new configurations will be merged. configure also receives the requestParams as the first argument. Take a look here for more options.

The HTTP gatewayConfigs also provides several callback functions that will be called when various events are emitted on the request, socket, and response EventEmitters. These callbacks can be used as a hook into the event cycle to execute any custom code. For example, you may want to time how long each stage of the request or response takes. These callback functions will receive the requestParams as the first argument.

The following callbacks are supported:

  • onRequestWillStart - This callback is not based on a event emitted by Node but is called just before the request method is called.
  • onRequestSocketAssigned - Called when the 'socket' event is emitted on the request
  • onSocketLookup - Called when the lookup event is emitted on the socket
  • onSocketConnect - Called when the connect event is emitted on the socket
  • onSocketSecureConnect - Called when the secureConnect event is emitted on the socket
  • onResponseReadable - Called when the readable event is emitted on the response
  • onResponseEnd - Called when the end event is emitted on the response
let startTime

configs.gatewayConfigs.HTTP = {
  onRequestWillStart() {
    startTime = Date.now()
  }
  onResponseReadable() {
    console.log('Time to first byte', Date.now() - startTime)
  }
}

XHR

When running in the browser you can configure withCredentials and configure to further customize the XMLHttpRequest object, example:

import { configs } from 'mappersmith'
configs.gatewayConfigs.XHR = {
  withCredentials: true,
  configure(xhr) {
    xhr.ontimeout = () => console.error('timeout!')
  }
}

Take a look here for more options.

Fetch

Mappersmith does not apply any polyfills, it depends on a native fetch implementation to be supported. It is possible to assign the fetch implementation used by Mappersmith:

import { configs } from 'mappersmith'
configs.fetch = fetchFunction

Fetch is not used by default, you can configure it through configs.gateway.

import { FetchGateway } from 'mappersmith/gateway'
import { configs } from 'mappersmith'

configs.gateway = FetchGateway

// Extra configurations, if needed
configs.gatewayConfigs.Fetch = {
  credentials: 'same-origin'
}

Take a look here for more options.

TypeScript

Mappersmith also supports TypeScript (>=3.5). In the following sections there are some common examples for using TypeScript with Mappersmith where it is not too obvious how typings are properly applied.

Create a middleware with TypeScript

To create a middleware using TypeScript you just have to add the Middleware interface to your middleware object:

import type { Middleware } from 'mappersmith'

const MyMiddleware: Middleware = () => ({
  prepareRequest(next) {
    return next().then(request => request.enhance({
      headers: { 'x-special-request': '->' }
    }))
  },

  response(next) {
    return next().then(response => response.enhance({
      headers: { 'x-special-response': '<-' }
    }))
  }
})

Use mockClient with TypeScript

To use the mockClient with proper types you need to pass a typeof your client as generic to the mockClient function:

import { forge } from 'mappersmith'
import { mockClient } from 'mappersmith/test'

const github = forge({
  clientId: 'github',
  host: 'https://status.github.com',
  resources: {
    Status: {
      current: { path: '/api/status.json' },
      messages: { path: '/api/messages.json' },
      lastMessage: { path: '/api/last-message.json' },
    },
  },
})

const mock = mockClient<typeof github>(github)
  .resource('Status')
  .method('current')
  .with({ id: 'abc' })
  .response({ allUsers: [] })
  .assertObject()

console.log(mock.mostRecentCall())
console.log(mock.callsCount())
console.log(mock.calls())

Use mockRequest with Typescript

const mock = mockRequest({
  method: 'get',
  url: 'https://status.github.com/api/status.json',
  response: {
    status: 503,
    body: { error: true },
  }
})

console.log(mock.mostRecentCall())
console.log(mock.callsCount())
console.log(mock.calls())

Development

Node version

This project uses ASDF to manage the node version used via .tool-versions.

Running unit tests:

yarn test:browser
yarn test:node

Running integration tests:

yarn integration-server &
yarn test:browser:integration
yarn test:node:integration

Running all tests

yarn test

Package and release

Package project only

Useful for testing a branch against local projects. Run the build step, and yarn link to the dist/ folder:

yarn publish:prepare

In remote project:

yarn link ../mappersmith/dist

Release

  1. Create a release branch, e.g. git checkout -b release/2.43.0
  2. Update package version and generate an updated CHANGELOG.md:
yarn changeset version
yarn copy:version:src
  1. Merge the PR.
  2. From master: pull the latest changes, and build the dist/ folder which will be published to npm:
yarn publish:prepare
  1. Verify the release works. If you are using npm pack to create a local tarball, delete this file after the verification has been done.
  2. Finally, publish the contents of dist/ folder to npm:
cd dist/
rm *.tgz # do not accidentally publish any tarball
npm publish
  1. Tag the release and push the tags.
git tag 2.43.0
git push --tags

Linting

This project uses prettier and eslint, it is recommended to install extensions in your editor to format on save.

Contributors

Check it out!

https://github.com/tulios/mappersmith/graphs/contributors

License

See LICENSE for more details.

mappersmith's People

Contributors

aitherios avatar antonioalmeida avatar benzaita avatar brenwell avatar capucho avatar danielalves avatar dependabot[bot] avatar grndcherokee avatar hmon avatar icewind1991 avatar jbeuckm avatar johanhenrikssn avatar karthikiyengar avatar kjjg1 avatar kjjgibson avatar klippx avatar martinhelmut avatar meric avatar mvkklarna avatar nevon avatar ollelindeman avatar pirelenito avatar roberto avatar rozzilla avatar techeverri avatar tommygustafsson avatar tulios avatar waleedashraf avatar yurickh avatar yuvke 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  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  avatar  avatar

mappersmith's Issues

Option to change bodyAttr

Hi @tulios

Prior to version 2.0.0 we had something like this:
Mappersmith.forge(Manifest, Gateway, "data")

Is there a way to achieve the same (pass a custom bodyAttr) on version >= 2.0.0?

PS: If possible, can you put together a minimal migration guide?

Split gateway options and implementations

The title is extremely short and may not reflect the importance of this issue, if some one have a better title, please post here.

The idea of this issue came from @meric with the issue #11, he wrote:

I'm not 100% sure that's the best, architecture wise. So far there's the manifest, the forge, the gateway, perhaps there one more layer of abstraction would be useful - Right now there's some code duplication between gateways, to do with serializing data and setting content type and emulateHTTP headers. Some sort of middleware that creates a dictionary representing the request the user wants sent, and the gateways would only be responsible for converting that dictionary into a format for requests that is local to the networking library it uses. This means the gateways no longer all have to implement the same code to interpret the options the client was called with.

Let's use this issue to discuss how to do it.

Content-Length header error

Sorry, but you don't have to set manually "Content-Length" header in Gateway, it causes browser error.

xhr.js:87 Refused to set unsafe header "content-length"

Include a way to assign headers direct from the call

In the issue #20 @CezarLuiz0 asked about a better way to assign headers and I think this is reasonable.

The proposal is:

Client.Photo.save({
  params: { category: 'family' },
  body: {year: 2015, tags: ['party', 'family']},
  headers: {
    'Authorization': 'token MYTOKEN'
  }
})

Why enforcing content-type?

First of all, congratz for the amazing lib.
Ive been trying it in a new project using play framework as the backend, the problem is, with the current gateways i cant send any post data because Mappersmith always concatenate "application/x-www-form-urlencoded; charset=UTF-8;" on the request, this way play cant understand that im trying to send an application/json (even if i put it in the headers rule).

So i basically had to copy/paste the gateway to make it stop enforcing the form content-type. This is obviously a bad solution, so do you have plans to stop enforcing this content-type? You may first check the user header options and only the user didnt add a Content-type then you can put it.

I can even do a PR if you dont have the time to do this.

Thanks

401 in basic auth

We are having an issue and we traced it to mappersmith sending wrong credentials, our back end log. We think the bug is due to assign(originalRequest.requestParams.auth, { password: '***' }) should be assign({}, originalRequest.requestParams.auth, { password: '***' })

api_1       | user:***
api_1       | [2017-10-17T08:37:57:262+0000Z] INFO  -- App : <Hash> {:remote_address=>"172.18.0.1", :remote_user=>nil, :request_method=>"GET", :path=>"/af123e56-2884-4110-a0fb-70f69c8cc6ec/foo", :status=>"401", :request_time=>"0.000961455s", :params=>{}}

Not possible to post with body using VanillaGateWay

Reproduce:

https://github.com/klippx/roger/tree/mappersmith-issue

This commit breaks the express middleware: klippx/roger@79d54e3

If I start the app and post the form using commit klippx/roger@9acdf0a I get this output in backend:

App running at http://localhost:3100/
GET /links.json {}
fetching {links} from graphql
--bodyParser: URLENCODED--, encoding: utf-8
POST /links/create.json { title: '123', url: 'abc' }

 == create-link handler ==
body: { title: '123', url: 'avc' }

But without jquery/jquery gateway I get this:

App running at http://localhost:3100/
GET /links.json {}
fetching {links} from graphql
POST /links/create.json {}

 == create-link handler ==
body: {}
creating new {link} in graphql
    mutation {
      add(title: undefined, url: undefined) {
        id, title, url
      }
    }

As you can see above, it totally skipped the express bodyParser middleware.

Curl is always possible regardless of commit sha:

curl -H "Content-Type: application/json" -X POST -d '{"title":"xyz","url":"http://123"}' http://localhost:3100/data/links/create.json

fetch compatible gateway

Hi @tulios!

I was wandering if there are any gateway implementations using fetch instead of jQuery.ajax? If aren't any, what do I need to implement one?

GlobalErrorHandler breaks precessing response and not executing response-middlewares

I need to transform error response by middlewares, such as normat responses.

GlobalErrorHandler breaks precessing response and not executing ResponseExtractMiddleware, EncodeJsonMiddleware.

middlewares: [
    GlobalErrorHandler,
    ResponseExtractMiddleware,
    EncodeJsonMiddleware,
  ],
const ResponseExtractMiddleware = () => ({
  response(next) {
    return next().then((response) => {
      console.log('ResponseExtractMiddleware->response:63', response);

      return {
        status: response.status(),
        data: response.data().data,
        headers: response.headers(),
      };
    })
  },
});
const CONTENT_TYPE_JSON = 'application/json;charset=utf-8';
const EncodeJsonMiddleware = () => ({
  request(request) {
    try {
      if (request.body()) {
        const content = JSON.stringify(request.body());
        return request.enhance({
          headers: {'Content-Type': CONTENT_TYPE_JSON},
          body: content,
        });
      }
    }
    catch (e) {
      return request;
    }
  }
});

Fail callback should inform the requested URL

fail callback is very useful to deal or log errors but without the requested URL we are assuming that you know which method/url have been called. In some cases we use the same handler for all calls and this, definitely can help.

Include used params in the stats object

The success callback receive data and stats, this stats object could have the params used.

{
  "timeElapsed": 6.745000369846821,
  "timeElapsedHumanized": "6.75 ms",
  "url": "http://host.com/path/subpath.json?param1=true&param2=1",
  "params": {"param1": true, "param2": 1}
}

Example of the need:

React.createClass({

  render: function() {
    return <div />
  },

  componendDidMount: function() {
    API.myMethod({param1: false, param2: 2}, this.handleSuccess);
  },

  handleSuccess: function(data, stats) {
    // I want to use some of the params together with data
    var newState = {};
    newState[stats.params.param2] = data;
    this.setState(newState);
  }

});

I want to operate data using some of the params used in the call, the example is simple and I could have used a local callback and variable for the params but when you have multiple calls and want a single method to handle the data it becomes a problem

setContext does not unset the value when undefined is passed

Hi there!
Thanks for an awesome lib ๐Ÿ‘

I have got into an issue with setContext feature in the following case:

setContext({key: 'value'})
...
setContext({key: undefined})

Second call does not override the value, so it stays 'value'. That could be misleading.
Thoughts?

Apply middleware per resource

I would like to build a middleware that will deserialize my responses into the appropriate response object depending on the resource. To do that, I would need to be able to do something like:

const deserializeInto = require('./middleware/deserialization')
const User = require('./responses/user')

const client = forge({
  resources: {
    User: {
      all: { path: '/users', middleware: [deserializeInto(users => users.map(User.fromResponse))]},
      byId: { path: '/users/{id}', middleware: [deserializeInto(User.fromResponse)]},
    },
  }
})

To do this currently, I would need to apply a global middleware and switch on the request path, which feels very ugly and prone to breakage.

Accepting a JSON Request Body

Thank you for Mappersmith! It's an amazing client ;)

The documentation says that a urlencoded version of the object will be used when sending values in the request body.

I wonder if that's possible to check the content type before creating a urlencoded version. In my case, the API only works with json objects.

Is there any way to send a json object in the request body?

thanks!

Needs additional error handling for onSuccess callback

Hi,

I came across the case that I have an error that is thrown in the code that is executed in the "onSuccess" callback of a mappersmith request.

image

In the vanilla-gateway.js#L85 this error is caught and treated as if it was a mappersmith error. That leads to a very confusing error that is shown in the developer-tools because it looks just like a API request error...

image

There are no hints about the original error that was happening in the onSuccess callback.

I would propose that there is an additional catching of errors that happen in the onSuccess callback like so:

    try {
      this.successCallback(data, extra);
    }
    catch(e) {
      this.failCallback({status: status, args: [e]});
    }

... at vanilla-gateway.js#L80

This way, the original error is passed as the second parameter to the failCallback and can be gracefully handled.

PS: I think the other gateway implementations might be prone to the same behavior, I only debugged the vanilla-gateway though.

Regards

Add timeout support for xhr and http gateways

XHR and HTTP gateways support timeout configurations, it would be nice to be able to set them through the configs object.

import { configs } from 'mappersmith'
configs.gatewayConfigs.XHR = {
  timeout: 1000
}
configs.gatewayConfigs.HTTP = {
  timeout: 1000
}

Do not send malformed requests

The request could be malformed if the header is present however the value of the header is undefined. I guess mappersmith could handle this issues and prevent this kind of requests

Duplicated content-type on vanilla gateway

Im trying to do a POST to my server i configured my gateway as follows:

import Mappersmith from 'mappersmith';
Mappersmith.Env.USE_PROMISES = true;
var manifest = {
 host: 'http://localhost:3000',
 rules: [
    {
        values: {
            gateway: {
                headers: {
                    'Content-Type': 'application/json'
                }
            }
        }
    }
 ],
 resources: {
   CreateMixData: {
     search: 'post:/search',
     data: '/data'
   }
 }
};
export default Mappersmith.forge(manifest);```

and im calling it as:
```js
API.CreateMixData.search({body:JSON.stringify({category:21})})

The problem is, when i look at the call on chrome the content-type is "Content-Type:application/json, application/json" instead of "Content-Type:application/json" , and for this reason my server dont know how to parse it.

The problem happens on any verb except for GET , and i tracked down the problem to the following method on vanilla-gateway:

_performRequest: function(method) {
    var emulateHTTP = this.shouldEmulateHTTP(method);
    var requestMethod = method;
    var request = new XMLHttpRequest();

    this._configureCallbacks(request);

    if (emulateHTTP) {
      this.body = this.body || {};
      if (typeof this.body === 'object') this.body._method = method;
      requestMethod = 'POST';
    }

    request.open(requestMethod, this.url, true);
    if (emulateHTTP) request.setRequestHeader('X-HTTP-Method-Override', method);

    this._setContentTypeHeader(request); // HERE THE CONTENT-TYPE IS SET
    this._setUserDefinedHeaders(request); // HERE THE CONTENT-TYPE IS SET AGAIN (DUPLICATION)

    var args = [];
    if (this.body !== undefined) {
      args.push(Utils.params(this.body));
    }

    request.send.apply(request, args);
  },

Thanks :)

wrong content-type on vanilla gateway

Hi, i was trying to send a request with Content-Type: application/json and i got a response status 500, looking the Content-Type on network request I noticed that all headers doesn't concatenating correctly.

Example: application/x-www-form-urlencoded; charset=UTF-8 application/json
Should be: application/x-www-form-urlencoded; charset=UTF-8; application/json

I prepared a pull request to solve

Best way to set headers?

There is a best way to set headers on pass params?

Example:

Client.Photo.save({
  category: 'family',
  body: {year: 2015, tags: ['party', 'family']},
  headers: {
      'Authorization': 'token MYTOKEN'
  }
})

Thanks!

Before callback

Mappersmith should have a before callback, something like:

var manifest = {
  host: 'http://my.api.com',
  resources: {
    Book: {
      all:  {
        path: '/v1/books.json',
        before: function(stats) {}
      }
    }
  }
}

and if we return false the request is not made.

mappersmith/test does not initialize gateway

Currently, the gateway is not initialized when requiring mappersmith/test. This creates unexpected behaviors for my tests.

Could be solved by requiring index.js file instead of mappersmith.js in test.js.

Accept an array of responses in `mockClient`

It can be really useful if mockClient accepts an array of responses to return in order

mockClient(client)
  .resource('User')
  .method('all')
  .response([
    { allUsers: [{id: 1}] },
    { allUsers: [{id: 1, 2}] },
    { allUsers: [{id: 2, 3, 4}] }
  ])

It will return specific values in order until it reaches the end of the response values list, at which point it will throw an error for all subsequent calls.

New syntax for defining an API call method and path

Evolving issue #1 idea, we could use a simpler syntax for defining an API call method and path:

var Manifest = {
  host: 'http://my.api.com',
  resources: {
    Book: {
      all:  'get:/v1/books.json', // Understands this is the same as using { path: '/v1/books.json' }
      save:  'post:/v1/books.json' // Understands this is the same as using { method: 'POST', path: '/v1/books/save' }
    }
  }
}

Add support for basic auth (Authorization header)

Add easy support to basic auth

const data = { ... }
const body = { username: 'bob', password: 'bob' }
Client.Blog.create({ auth, body })

Like body it should support auth by default and allow a different configuration with authAttr in the manifest definition.

The implementation can use window.btoa but it should provide a polyfill since it's not available in IE<11 (http://caniuse.com/#feat=atob-btoa)

A small implementation can be found here: https://github.com/davidchambers/Base64.js

Include the url in the stats object

The success callback receive data and stats, this stats object could have the url used.

{
  timeElapsed: 6.745000369846821,
  timeElapsedHumanized: '6.75 ms',
  url: 'http://host.com/path/subpath.json?param1=true&param2=1'
}

File Upload

Hi, guys! First of all, awesome work!

I'm trying to figure out how to upload a file using FormData and mappersmith. Any example or idea will be appreciated.

Thanks!

Use faux-jax instead of sinon/nock

faux-jax is isomorphic and will improve the tests for all kinds of gateways. Nowadays we use sinon for browser and nock for server, this change will allow a single solution for both platforms.

api versioning

What is the best practice when dealing w/ api versioning in the path?

  1. interpolate?:
const API = ({ host, version = 'v2.1.0' }) => {
  return forge({
    host,
    resources: {
      Foo: { bar: { path: `/base/${version}/bar` } }
    }
  })
}
  1. some middleware that fills it in as a param?:
const API = ({ host, version = 'v2.1.0' }) => {
  return forge({
    middlewares: [Versioned(version)],
    host,
    resources: {
      Foo: { bar: { path: '/base/{version}/bar' } }
    }
  })
}
  1. have some first-class handling:
const API = ({ host, version = 'v2.1.0' }) => {
  return forge({
    host,
    version,
    resources: {
      Foo: { bar: { path: '/base/{version}/bar' } }
    }
  })
}
  1. have it fixed and expose one API per version, like API1, API2, etc...?
const API210 = ({ host }) => {
  return forge({
    host,
    resources: {
      Foo: { bar: { path: '/base/2.1.0/bar' } }
    }
  })
}

In the first 3 scenarios, I assume one may do some conditionals to deal with differences, which is awful, but in case 4, how to reuse things? Or not reuse at all?

Ideas?

Error in documentation of how to set custom header

In the README this example is given for how to set a custom header on a request

...
rules: [
  ...
  { // This will only be applied when the URL matches the regexp
    match: /\/v1\/books/,
    values: {headers: {'X-MY-HEADER': 'value'}}
  }
]
...

But it looks like you need to do this instead:

...
rules: [
  ...
  { // This will only be applied when the URL matches the regexp
    match: /\/v1\/books/,
    gateway: {
      values: {headers: {'X-MY-HEADER': 'value'}}
    }
  }
]
...

Hook beforeSend

It would be good to have a hook like the beforeSend on Jquery ajax. Sometimes you might want to change some headers dinamically (im passing user auth token on header and this token can change as the user login/logoff my application). This way i could check storage/memory before sending the request for the current token.

Accept processor functions by API call

We could add another configuration key, by API call, with a function to process the request response:

var manifest = {
  host: 'http://my.api.com',
  resources: {
    Book: {
      all:  { 
        path: '/v1/books.json', 
        processor: function( data ){ // i.e: data contains 'results', 'pagination', ...
          return data.results;
        } 
      }
    }
  }
}

Syntatic sugar for GET methods with no parameters

We could use this alternative syntax for GET methods with no parameters:

var Manifest = {
  host: 'http://my.api.com',
  resources: {
    Book: {
      all: '/v1/books.json' // Understands this string is the same as using { path: '/v1/books.json' }
    }
  }
}

rules matcher should use parsed url instead of descriptor path

Rule matches should compare parsed url instead of descriptor path

Here is what i have to do right now:

var manifest = {
  host: 'http://api',
  resources: {
    List: {
      byId: '/{resource_list}/{resource_id}/'
    }
  },
  rules: [
    {
      match: /resource_list/,
      values: {
        gateway: {
          cache: { ttl: 60 * 30 }
        }
      }
    }
  ]
}

But i have 2 different resources list.
It should be awsome if we hame something like this:

Here is what i have to do right now:

var manifest = {
  host: 'http://api',
  resources: {
    List: {
      byId: '/{resource_list}/{resource_id}/'
    }
  },
  rules: [
    {
      match: /watch_history/,
      values: {
        gateway: {
          cache: { ttl: 60 * 30 }
        }
      }
    },
    {
      match: /favorites/,
      values: {
        gateway: {
          cache: { ttl: 60 * 120 }
        }
      }
    }
  ]
}

Browser support

Hi Guys,

Just wondering if you could shed some light in terms of browser support? Would this library be safe to use in IE8?

Thanks...

MockClient should use the same middlewares configured

If a forged client is configured with middlewares (e.g. EncodeJson),
any mocks from that client should also use the same middlewares in the same order.

Current specs failure messages:

[Mappersmith Test] No exact match found for "POST /api/foo" (body: "{"field":"value"}"), partial match with "POST /api/foo" (body: "field=value"), check your mock definition

ClientBuilder isn't using the new configured gateway when config.gateway changes

When using the test lib the mock gateway is never used for the pre forged client since client builder caches the gateway class.

How it should work:

import { install, mockClient } from 'mappersmith/test'
const client = forge(manifest)
client.Resource.method()
// call using XHR gateway

install()
mockClient(client)
  .resource('Resource')
  .method('method')
  .response({})

client.Resource.method()
// call using Mock gateway

Create a 'gateway options' manifest key

We could create a new key with options that would apply to all services matching a regex. For example:

var manifest = {
  host: 'http://my.api.com',
  gatewayOpts: [
    { values: { jsonp: true } },
    { match: /v1\/books\//, values: { headers: { 'X-MY-HEADER': 'book-related' } } }
  ],
  resources: {
    Book: {
      all:  {path: '/v1/books.json'},
      byId: {path: '/v1/books/{id}.json'}
    },
    Photo: {
      byCategory: {path: '/v1/photos/{category}/all.json'}
    }
  }
}

If we do not pass the match key, the option is applied to the gateway on every service request.
If it is passed, only services with a path that match it will have the option.

We should stress that how an option is used vary by gateway implementation. The example above, uses JQueryGateway.

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.