Mappersmith

is a lightweight rest client for node.js and the browser

README

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


  - Commonjs
    - Parameters
    - Body
    - Headers
    - Basic Auth
    - Timeout
    - Binary data
  - Promises
      - Context (deprecated)
      - Optional arguments
        - mockRequest
        - Abort
        - Renew
        - request
      - Global middleware
      - BasicAuth
      - CSRF
      - Duration
      - EncodeJSON
      - GlobalErrorHandler
      - Log
      - Retry
      - Timeout
  - Gateways

## Installation

  1. ```sh
  2. npm install mappersmith --save
  3. ```

or

  1. ```sh
  2. yarn add mappersmith
  3. ```

Browser


Download the tag/latest version from the dist folder.

Build from the source


Install the dependencies

  1. ```sh
  2. yarn
  3. ```

Build

  1. ```sh
  2. yarn build
  3. yarn release # for minified version
  4. ```

## 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:

  1. ``` js
  2. import forge from 'mappersmith'

  3. const github = forge({
  4.   clientId: 'github',
  5.   host: 'https://status.github.com',
  6.   resources: {
  7.     Status: {
  8.       current: { path: '/api/status.json' },
  9.       messages: { path: '/api/messages.json' },
  10.       lastMessage: { path: '/api/last-message.json' }
  11.     }
  12.   }
  13. })

  14. github.Status.lastMessage().then((response) => {
  15.   console.log(`status: ${response.data()}`)
  16. })
  17. ```

## Commonjs

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

  1. ``` js
  2. const forge = require('mappersmith').default
  3. ```

## 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:

  1. ``` js
  2. const client = forge({
  3.   resources: {
  4.     User: {
  5.       all: { path: '/users' },

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

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

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

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

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

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

### Parameters

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

  1. ``` js
  2. client.User
  3.   .all() // https://my.api.com/users
  4.   .then((response) => console.log(response.data()))
  5.   .catch((response) => console.error(response.data()))
  6. ```

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

  1. ``` js
  2. client.User.all({ active: true }) // https://my.api.com/users?active=true
  3. ```

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

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

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).

  1. ``` js
  2. const client = forge({
  3.   host: 'https://custom-host.com',
  4.   parameterEncoder: yourEncodingFunction,
  5.   resources: { ... }
  6. })
  7. ```

### 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"

  1. ``` js
  2. client.User.byGroup() // https://my.api.com/users/groups/general
  3. ```

And, of course, we can override the defaults:

  1. ``` js
  2. client.User.byGroup({ group: 'cool' }) // https://my.api.com/users/groups/cool
  3. ```

### 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.

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

### Body

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

  1. ``` js
  2. client.Blog.create({
  3.   body: {
  4.     title: 'Title',
  5.     tags: ['party', 'launch']
  6.   }
  7. })
  8. ```

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:

  1. ``` js
  2. // ...
  3. {
  4.   create: { method: 'post', path: '/blogs', bodyAttr: 'payload' }
  5. }
  6. // ...

  7. client.Blog.create({
  8.   payload: {
  9.     title: 'Title',
  10.     tags: ['party', 'launch']
  11.   }
  12. })
  13. ```

__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:

  1. ``` js
  2. client.User.all({ headers: { Authorization: 'token 1d1435k' } })
  3. ```

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

  1. ``` js
  2. // ...
  3. {
  4.   all: { path: '/users', headersAttr: 'h' }
  5. }
  6. // ...

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

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

### Basic auth

To define credentials for basic auth use the parameter auth:

  1. ``` js
  2. client.User.all({ auth: { username: 'bob', password: 'bob' } })
  3. ```

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:

  1. ``` js
  2. // ...
  3. {
  4.   all: { path: '/users', authAttr: 'secret' }
  5. }
  6. // ...

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

__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:

  1. ``` js
  2. client.User.all({ timeout: 1000 })
  3. ```

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

  1. ``` js
  2. // ...
  3. {
  4.   all: { path: '/users', timeoutAttr: 'maxWait' }
  5. }
  6. // ...

  7. client.User.all({ maxWait: 500 })
  8. ```

__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:

  1. ``` js
  2. // ...
  3. {
  4.   all: { path: '/users', host: 'http://old-api.com' }
  5. }
  6. // ...

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

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

  1. ``` js
  2. // ...
  3. {
  4.   all: { path: '/users', host: 'http://old-api.com' }
  5. }
  6. // ...

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

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

  1. ``` js
  2. // ...
  3. {
  4.   all: { path: '/users', hostAttr: 'baseUrl' }
  5. }
  6. // ...

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

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

  1. ``` js
  2. const client = forge({
  3.   host: 'https://new-host.com',
  4.   allowResourceHostOverride: true,
  5.   resources: {
  6.     User: {
  7.       all: { path: '/users', host: 'https://old-host.com }
  8.     }
  9.   }
  10. })
  11. ```

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:

  1. ``` js
  2. // ...
  3. {
  4.   all: { path: '/users' }
  5. }
  6. // ...

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

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

  1. ``` js
  2. // ...
  3. {
  4.   all: { path: '/users', pathAttr: '__path' }
  5. }
  6. // ...

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

### 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.

  1. ``` js

  2. // ...
  3. {
  4.   report: { path: '/report.pdf', binary: true }
  5. }
  6. // ...

  7. ```

## 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+):

  1. ``` js
  2. import RSVP from 'rsvp'
  3. import { configs } from 'mappersmith'

  4. configs.Promise = RSVP.Promise
  5. ```

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 aPromise resolving the request. The method enhance can be used to generate a new request based on the previous one.

  1. ``` js
  2. const MyMiddleware = () => ({
  3.   prepareRequest(next) {
  4.     return next().then(request => request.enhance({
  5.       headers: { 'x-special-request': '->' }
  6.     }))
  7.   }
  8. })
  9. ```

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

  1. ``` js
  2. const MyMiddlewareOne = () => ({
  3.   async prepareRequest(next) {
  4.     const request = await next().then(request => request.enhance({}, { message: 'hello from mw1' }))
  5.   }
  6. })

  7. const MyMiddlewareTwo = () => ({
  8.   async prepareRequest(next) {
  9.     const request = await next()
  10.     const { message } = request.getContext()
  11.     // Logs: "hello from mw1"
  12.     console.log(message)
  13.     return request
  14.   }
  15. })
  16. ```

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 aPromise resolving the Response. The method enhance can be used to generate a new response based on the previous one.

  1. ``` js
  2. const MyMiddleware = () => ({
  3.   response(next) {
  4.     return next().then((response) => response.enhance({
  5.       headers: { 'x-special-response': '<-' }
  6.     }))
  7.   }
  8. })
  9. ```

#### 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](context), clientId and mockRequest. Example:

  1. ``` js
  2. const MyMiddleware = ({ resourceName, resourceMethod, context, clientId, mockRequest }) => ({
  3.   /* ... */
  4. })

  5. client.User.all()
  6. // resourceName: 'User'
  7. // resourceMethod: 'all'
  8. // clientId: 'myClient'
  9. // context: {}
  10. // mockRequest: false
  11. ```

##### 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:

  1. ``` js
  2. const MyMiddleware = ({ mockRequest }) => {
  3.   prepareRequest(next) {
  4.     if (mockRequest) {
  5.       ... // executed once for each mocked client that utilises the middleware
  6.     }
  7.     if (!mockRequest) {
  8.       ... // executed once for the matching mock definition
  9.     }
  10.     return next().then(request => request)
  11.   }
  12. }
  13. ```

##### 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:

  1. ``` js
  2. const MyMiddleware = () => {
  3.   prepareRequest(next, abort) {
  4.     return next().then(request =>
  5.       request.header('x-special')
  6.         ? response
  7.         : abort(new Error('"x-special" must be set!'))
  8.     )
  9.   }
  10. }
  11. ```

##### 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:

  1. ``` js
  2. const AccessTokenMiddleware = () => {
  3.   // maybe this is stored elsewhere, here for simplicity
  4.   let accessToken = null

  5.   return () => ({
  6.     request(request) {
  7.       return Promise
  8.         .resolve(accessToken)
  9.         .then((token) => token || fetchAccessToken())
  10.         .then((token) => {
  11.           accessToken = token
  12.           return request.enhance({
  13.             headers: { 'Authorization': `Token ${token}` }
  14.           })
  15.         })
  16.     },
  17.     response(next, renew) {
  18.       return next().catch(response => {
  19.         if (response.status() === 401) { // token expired
  20.           accessToken = null
  21.           return renew()
  22.         }

  23.         return next()
  24.       })
  25.     }
  26.   })
  27. }
  28. ```

Then:

  1. ``` js
  2. const AccessToken = AccessTokenMiddleware()
  3. const client = forge({
  4.   // ...
  5.   middleware: [ AccessToken ],
  6.   // ...
  7. })
  8. ```

"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:

  1. ``` js
  2. import { configs } from 'mappersmith'
  3. configs.maxMiddlewareStackExecutionAllowed = 3
  4. ```

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:

  1. ``` js
  2. const CircuitBreakerMiddleware = () => {
  3.   return () => ({
  4.     response(next, renew, request) {
  5.       // Creating the breaker required some information available only on `request`:
  6.       const breaker = createBreaker({ ..., timeout: request.timeout })
  7.       // Note: `next` is still wrapped:
  8.       return breaker.invoke(createExecutor(next))
  9.     }
  10.   })
  11. }
  12. ```

### 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:

  1. ``` js
  2. const client = forge({
  3.   clientId: 'myClient',
  4.   resources: {
  5.     User: {
  6.       all: {
  7.         // only the `all` resource will include MyMiddleware:
  8.         middleware: [ MyMiddleware ],
  9.         path: '/users'
  10.       }
  11.     }
  12.   }
  13. })
  14. ```

#### Client level middleware

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

  1. ``` js
  2. const client = forge({
  3.   clientId: 'myClient',
  4.   // all resources in this client will include MyMiddleware:
  5.   middleware: [ MyMiddleware ],
  6.   resources: {
  7.     User: {
  8.       all: { path: '/users' }
  9.     }
  10.   }
  11. })
  12. ```

#### Global middleware

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

  1. ``` js
  2. import forge, { configs } from 'mappersmith'

  3. configs.middleware = [MyMiddleware]
  4. // all clients defined from now on will include MyMiddleware
  5. ```

Global middleware can be disabled for specific clients with the option ignoreGlobalMiddleware, e.g:

  1. ``` js
  2. forge({
  3.   ignoreGlobalMiddleware: true,
  4.   // + the usual configurations
  5. })
  6. ```

### Built-in middleware

#### BasicAuth

Automatically configure your requests with basic auth

  1. ``` js
  2. import BasicAuthMiddleware from 'mappersmith/middleware/basic-auth'
  3. const BasicAuth = BasicAuthMiddleware({ username: 'bob', password: 'bob' })

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

  8. client.User.all()
  9. // => header: "Authorization: Basic Ym9iOmJvYg=="
  10. ```

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

  1. ``` js
  2. client.User.all({ auth: { username: 'bill', password: 'bill' } })
  3. // auth will be { username: 'bill', password: 'bill' } instead of { username: 'bob', password: 'bob' }
  4. ```

#### 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;

  1. ``` js
  2. import CSRF from 'mappersmith/middleware/csrf'

  3. const client = forge({
  4.   middleware: [ CSRF('csrfToken', 'x-csrf-token') ],
  5.   /* ... */
  6. })

  7. client.User.all()
  8. ```

#### Duration

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

  1. ``` js
  2. import Duration from 'mappersmith/middleware/duration'

  3. const client = forge({
  4.   middleware: [ Duration ],
  5.   /* ... */
  6. })

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

#### EncodeJson

Automatically encode your objects into JSON

  1. ``` js
  2. import EncodeJson from 'mappersmith/middleware/encode-json'

  3. const client = forge({
  4.   middleware: [ EncodeJson ],
  5.   /* ... */
  6. })

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

#### GlobalErrorHandler

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

  1. ``` js
  2. import GlobalErrorHandler, { setErrorHandler } from 'mappersmith/middleware/global-error-handler'

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

  7. const client = forge({
  8.   middleware: [ GlobalErrorHandler ],
  9.   /* ... */
  10. })

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

  14. // If status != 500
  15. // output:
  16. //   -> global error handler
  17. //   -> my error

  18. // IF status == 500
  19. // output:
  20. //   -> global error handler
  21. ```

#### Log

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

  1. ``` js
  2. import Log from 'mappersmith/middleware/log'

  3. const client = forge({
  4.   middleware: [ Log ],
  5.   /* ... */
  6. })
  7. ```

#### 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.

v2

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.

  1. ``` js
  2. import Retry from 'mappersmith/middleware/retry/v2'

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

  13. const client = forge({
  14.   middleware: [ Retry(retryConfigs) ],
  15.   /* ... */
  16. })
  17. ```

v1 (deprecated)

The v1 retry middleware is now deprecated as it relies on global configuration which can cause issues if you need to have multiple different configurations.

  1. ``` js
  2. import Retry from 'mappersmith/middleware/retry'

  3. const client = forge({
  4.   middleware: [ Retry ],
  5.   /* ... */
  6. })
  7. ```

It's possible to configure the header names and parameters used in the calculation by calling the deprecated setRetryConfigs method.

  1. ``` js
  2. import { setRetryConfigs } from 'mappersmith/middleware/retry'

  3. // Using the default values as an example
  4. setRetryConfigs({
  5.   headerRetryCount: 'X-Mappersmith-Retry-Count',
  6.   headerRetryTime: 'X-Mappersmith-Retry-Time',
  7.   maxRetryTimeInSecs: 5,
  8.   initialRetryTimeInSecs: 0.1,
  9.   factor: 0.2, // randomization factor
  10.   multiplier: 2, // exponential factor
  11.   retries: 5, // max retries
  12.   validateRetry: (response) => response.responseStatus >= 500 // a function that returns true if the request should be retried
  13. })
  14. ```

#### Timeout

Automatically configure your requests with a default timeout

  1. ``` js
  2. import TimeoutMiddleware from 'mappersmith/middleware/timeout'
  3. const Timeout = TimeoutMiddleware(500)

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

  8. client.User.all()
  9. ```

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

  1. ``` js
  2. client.User.all({ timeout: 100 })
  3. // timeout will be 100 instead of 500
  4. ```

### 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 methodenhance can be used to generate a new request based on the previous one.

Example:

  1. ``` js
  2. const MyMiddleware = () => ({
  3.   request(request) {
  4.     return request.enhance({
  5.       headers: { 'x-special-request': '->' }
  6.     })
  7.   },

  8.   response(next) {
  9.     return next().then((response) => response.enhance({
  10.       headers: { 'x-special-response': '<-' }
  11.     }))
  12.   }
  13. })
  14. ```

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

  1. ``` js
  2. const MyMiddleware = () => ({
  3.   request(request) {
  4.     return Promise.resolve(
  5.       request.enhance({
  6.         headers: { 'x-special-token': 'abc123' }
  7.       })
  8.     )
  9.   }
  10. })
  11. ```

## 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__:

  1. ``` js
  2. import { install, uninstall } from 'mappersmith/test'

  3. describe('Feature', () => {
  4.   beforeEach(() => install())
  5.   afterEach(() => uninstall())
  6. })
  7. ```

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__:

  1. ``` js
  2. import forge from 'mappersmith'
  3. import { install, uninstall, mockClient } from 'mappersmith/test'

  4. describe('Feature', () => {
  5.   beforeEach(() => install())
  6.   afterEach(() => uninstall())

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

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

  14.     // now if I call my resource method, it should return my mock response
  15.     client.User
  16.       .all()
  17.       .then((response) => expect(response.data()).toEqual({ allUsers: [{id: 1}] }))
  18.       .then(done)
  19.   })
  20. })
  21. ```

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

  1. ``` js
  2. // ...
  3. mockClient(client)
  4.   .resource('User')
  5.   .method('byId')
  6.   .with({ id: 'ABC' })
  7.   .status(422)
  8.   .response({ error: 'invalid ID' })
  9. // ...
  10. ```

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

  1. ``` js
  2. // ...
  3. mockClient(client)
  4.   .with({
  5.     id: 'abc',
  6.     headers: { 'x-special': 'value'},
  7.     body: { payload: 1 }
  8.   })
  9.   // ...
  10. ```

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

  1. ``` js
  2. import { m } from 'mappersmith/test'

  3. mockClient(client)
  4.   .with({
  5.     id: 'abc',
  6.     name: m.stringContaining('john'),
  7.     headers: { 'x-special': 'value'},
  8.     body: m.stringMatching(/token=[^&]+&other=true$/)
  9.   })
  10. ```

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

  1. ``` js
  2. const mock = mockClient(client)
  3.   .resource('User')
  4.   .method('all')
  5.   .response({ allUsers: [{id: 1}] })
  6.   .assertObject()

  7. console.log(mock.mostRecentCall())
  8. console.log(mock.callsCount())
  9. console.log(mock.calls())
  10. ```

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:

  1. ``` js
  2. const mock = await mockClient(client)
  3.   .resource('User')
  4.   .method('all')
  5.   .response({ allUsers: [{id: 1}] })
  6.   .assertObjectAsync()

  7. console.log(mock.mostRecentCall())
  8. console.log(mock.callsCount())
  9. console.log(mock.calls())
  10. ```

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.

  1. ``` js
  2. const generateResponse = () => {
  3.   return (request, mock) => mock.callsCount() === 0
  4.     ? {}
  5.     : { user: { id: 1 } }
  6. }

  7. const mock = mockClient(client)
  8.   .resource('User')
  9.   .method('create')
  10.   .response(generateResponse())
  11. ```

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__:

  1. ``` js
  2. import forge from 'mappersmith'
  3. import { install, uninstall, mockRequest } from 'mappersmith/test'

  4. describe('Feature', () => {
  5.   beforeEach(() => install())
  6.   afterEach(() => uninstall())

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

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

  17.     client.User
  18.       .all()
  19.       .then((response) => expect(response.data()).toEqual({ allUsers: [{id: 1}] }))
  20.       .then(done)
  21.   })
  22. })
  23. ```

A more complete example:

  1. ``` js
  2. // ...
  3. mockRequest({
  4.   method: 'post',
  5.   url: 'http://example.org/blogs',
  6.   body: 'param1=A&param2=B', // request body
  7.   response: {
  8.     status: 503,
  9.     body: { error: true },
  10.     headers: { 'x-header': 'nope' }
  11.   }
  12. })
  13. // ...
  14. ```

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

  1. ``` js
  2. import { m } from 'mappersmith/test'

  3. mockRequest({
  4.   method: 'post',
  5.   url: m.stringMatching(/example\.org/),
  6.   body: m.anything(),
  7.   response: {
  8.     body: { allUsers: [{id: 1}] }
  9.   }
  10. })
  11. ```

Using the assert object:

  1. ``` js
  2. const mock = mockRequest({
  3.   method: 'get',
  4.   url: 'https://my.api.com/users?someParam=true',
  5.   response: {
  6.     body: { allUsers: [{id: 1}] }
  7.   }
  8. })

  9. console.log(mock.mostRecentCall())
  10. console.log(mock.callsCount())
  11. console.log(mock.calls())
  12. ```

Match functions


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

  1. ``` js
  2. import { m } from 'mappersmith/test'

  3. m.stringMatching(/something/) // accepts a regexp
  4. m.stringContaining('some-string') // accepts a string
  5. m.anything()
  6. m.uuid4()
  7. ```

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

  1. ``` js
  2. mockClient(client)
  3.   .with({
  4.     id: 'abc',
  5.     headers: { 'x-special': 'value'},
  6.     body: (body) => body === 'something'
  7.   })
  8. ```

__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.

  1. ``` js
  2. import { install, uninstall, unusedMocks } from 'mappersmith/test'

  3. describe('Feature', () => {
  4.   beforeEach(() => install())
  5.   afterEach(() => {
  6.     const unusedMocksCount = unusedMocks()
  7.     uninstall()
  8.     if (unusedMocksCount > 0) {
  9.       throw new Error(`There are ${unusedMocksCount} unused mocks`) // fail the test
  10.     }
  11.   })
  12. })
  13. ```

## 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 theconfigs object and assign the gateway option, like:

  1. ``` js
  2. import { configs } from 'mappersmith'
  3. configs.gateway = MyGateway
  4. ```

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:

  1. ``` js
  2. import fs from 'fs'
  3. import https from 'https'
  4. import { configs } from 'mappersmith'

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

  7. configs.gatewayConfigs.HTTP = {
  8.   configure() {
  9.     return {
  10.       agent: new https.Agent({ key, cert })
  11.     }
  12.   }
  13. }
  14. ```

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

  1. ``` js
  2. let startTime

  3. configs.gatewayConfigs.HTTP = {
  4.   onRequestWillStart() {
  5.     startTime = Date.now()
  6.   }
  7.   onResponseReadable() {
  8.     console.log('Time to first byte', Date.now() - startTime)
  9.   }
  10. }
  11. ```

XHR


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

  1. ``` js
  2. import { configs } from 'mappersmith'
  3. configs.gatewayConfigs.XHR = {
  4.   withCredentials: true,
  5.   configure(xhr) {
  6.     xhr.ontimeout = () => console.error('timeout!')
  7.   }
  8. }
  9. ```

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:

  1. ``` js
  2. import { configs } from 'mappersmith'
  3. configs.fetch = fetchFunction
  4. ```

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

  1. ``` js
  2. import FetchGateway from 'mappersmith/gateway/fetch'
  3. import { configs } from 'mappersmith'

  4. configs.gateway = FetchGateway

  5. // Extra configurations, if needed
  6. configs.gatewayConfigs.Fetch = {
  7.   credentials: 'same-origin'
  8. }
  9. ```

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:

  1. ```typescript
  2. import { Middleware } from 'mappersmith'

  3. const MyMiddleware: Middleware = () => ({
  4.   prepareRequest(next) {
  5.     return next().then(request => request.enhance({
  6.       headers: { 'x-special-request': '->' }
  7.     }))
  8.   },

  9.   response(next) {
  10.     return next().then(response => response.enhance({
  11.       headers: { 'x-special-response': '<-' }
  12.     }))
  13.   }
  14. })
  15. ```

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:

  1. ```typescript
  2. import forge from 'mappersmith'
  3. import { mockClient } from 'mappersmith/test'

  4. const github = forge({
  5.   clientId: 'github',
  6.   host: 'https://status.github.com',
  7.   resources: {
  8.     Status: {
  9.       current: { path: '/api/status.json' },
  10.       messages: { path: '/api/messages.json' },
  11.       lastMessage: { path: '/api/last-message.json' },
  12.     },
  13.   },
  14. })

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

  21. console.log(mock.mostRecentCall())
  22. console.log(mock.callsCount())
  23. console.log(mock.calls())
  24. ```

Use mockRequest with Typescript


  1. ```typescript
  2. const mock = mockRequest({
  3.   method: 'get',
  4.   url: 'https://status.github.com/api/status.json',
  5.   response: {
  6.     status: 503,
  7.     body: { error: true },
  8.   }
  9. })

  10. console.log(mock.mostRecentCall())
  11. console.log(mock.callsCount())
  12. console.log(mock.calls())
  13. ```

## Development

Volta


This project uses Volta to manage the node/npm and yarn versions used via package.json.

Running unit tests:


  1. ```sh
  2. yarn test:browser
  3. yarn test:node
  4. ```

Running integration tests:


  1. ```sh
  2. yarn integration-server &
  3. yarn test:browser:integration
  4. yarn test:node:integration
  5. ```

Running all tests


  1. ```sh
  2. yarn test
  3. ```

Compile and release


Compile project only


Useful for testing a branch against local projects. Run the build step of the release script and yarn link:

  1. ```sh
  2. yarn build:project
  3. cd lib
  4. yarn link
  5. ```

Release


  1. ```sh
  2. yarn release
  3. ```

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.