@mountainpass/waychaser

    4.0.22 • Public • Published

    waychaser

    Client library for HATEOAS level 3 RESTful APIs that provide hypermedia controls using:

    This isomorphic library is compatible with Node.js 10.x, 12.x and 14.x, Chrome, Firefox, Safari, Edge and even IE. aw yeah!

    License npm npm downloads FOSSA Status

    Build Status BrowserStack Status

    GitHub issues GitHub pull requests

    Quality Coverage

    source code vulnerabilities npm package vulnerabilities

    Conventional Commits code style: prettier

    I love badges

    JavaScript Style Guide

    Node.js
    Node.js
    Chrome
    Chrome
    Firefox
    Firefox
    Safari
    Safari
    Edge
    Edge
    iOS
    iOS
    Android
    Android
    IE
    IE
    10.x, 12.x, 14.x latest version latest version latest version latest version latest version latest version 11

    FOSSA Status

    ToC

    Usage

    Node.js

    npm install @mountainpass/waychaser
    import { WayChaser } from '@mountainpass/waychaser'
    
    //...
    const waychaser = new WayChaser()
    try {
      const apiResource = await waychaser.load(apiUrl)
      // do something with `apiResource`
    } catch (error) {
      // do something with `error`
    }

    Browser

    <script
      type="text/javascript"
      src="https://unpkg.com/@mountainpass/waychaser@4.0.22"
    ></script>
    
    ...
    <script type="text/javascript">
      var waychaser = new window.waychaser.WayChaser()
      waychaser
        .load(apiUrl)
        .then((apiResource) => {
          // do something with `apiResource`
        })
        .catch((error) => {
          // do something with `error`
        });
    </script>

    Getting the response

    WayChaser makes it's http requests using fetch and the Fetch.Response is available via the response property.

    For example

    const responseUrl = apiResource.response.url

    Getting the response body

    WayChaser makes the response body available via the body() async method.

    For example

    const responseUrl = await apiResource.body()

    Requesting linked resources

    Level 3 REST APIs are expected to return links to related resources. WayChaser expects to find these links via RFC 8288 link headers, link-template headers, HAL _link elements or Siren link elements.

    WayChaser provides methods to simplify requesting these linked resources.

    For instance, if the apiResource we loaded above has a next link like any of the following:

    Link header:

    Link: <https://api.waychaser.io/example?p=2>; rel="next";
    

    HAL

    {
      "_links": {
        "next": { "href": "https://api.waychaser.io/example?p=2" }
      }
    }

    Siren

    {
      "links": [
        { "rel": [ "next" ], "href": "https://api.waychaser.io/example?p=2" },
      ]
    }

    then that next page can be retrieved using the following code

    const nextResource = await apiResource.invoke('next')

    You don't need to tell waychaser whether to use Link headers, HAL _links or Siren links; it will figure it out based on the resource's media-type. If the media-type is application/hal+json if will try to parse the links in the _link property of the body. If the media-type is application/vnd.siren+json if will try to parse the links in the link property of the body.

    Regardless of the resource's media-type, it will always try to parse the links in the Link and Link-Template headers.

    Multiple links with the same relationship

    Resources can have multiple links with the same relationship, such as

    HAL

    {
      "_links": {
        "item": [{
          "href": "/first_item",
          "name": "first"
        },{
          "href": "/second_item",
          "name": "second"
        }]
      }
    }

    If you know the name of the resource, then waychaser can load it using the following code

    const firstResource = await apiResource.invoke({ rel: 'item', name: 'first' })

    Forms

    Query forms

    Support for query forms is provided via:

    For instance if our resource has either of the following

    Link-Template header:

    Link-Template: <https://api.waychaser.io/search{?q}>; rel="search";
    

    HAL

    {
      "_links": {
        "search": { "href": "https://api.waychaser.io/search{?q}" }
      }
    }

    Then waychaser can execute a search for "waychaser" with the following code

    const searchResultsResource = await apiResource.invoke('search', {
      q: 'waychaser'
    })

    Path parameter forms

    Support for query forms is provided via:

    For instance if our resource has either of the following

    Link-Template header

    Link-Template: <https://api.waychaser.io/users{/username}>; rel="item";
    

    HAL

    {
      "_links": {
        "item": { "href": "https://api.waychaser.io/users{/username}" }
      }
    }

    Then waychaser can retrieve the user with the username "waychaser" with the following code

    const userResource = await apiResource.invoke('item', {
      username: 'waychaser'
    })

    Request body forms

    Support for request body forms is provided via:

    To support request body forms with link-template headers, waychaser supports three additional parameters in the link-template header:

    • method - used to specify the HTTP method to use
    • params* - used to specify the fields the form expects
    • accept* - used to specify the media-types that can be used to send the body as per, RFC7231 and defaulting to application/x-www-form-urlencoded

    If our resource has either of the following:

    Link-Template header:

    Link-Template: <https://api.waychaser.io/users>; 
      rel="https://waychaser.io/rels/create-user"; 
      method="POST";
      params*=UTF-8'en'%7B%22username%22%3A%7B%7D%7D'
    

    If your wondering what the UTF-8'en'%7B%22username%22%3A%7B%7D%7D' part is, it's just the JSON {"username":{}} encoded as an Extension Attribute as per (RFC8288) Link Headers. Don't worry, libraries like http-link-header can do this encoding for you.

    Siren

    {
      "actions": [
        {
          "name": "https://waychaser.io/rels/create-user",
          "href": "https://api.waychaser.io/users",
          "method": "POST",
          "fields": [
            { "name": "username" }
          ]
        }
      ]
    }

    Then waychaser can create a new user with the username "waychaser" with the following code

    const createUserResultsResource = await apiResource.invoke('https://waychaser.io/rels/create-user', {
      username: 'waychaser'
    })

    NOTE: The URL https://waychaser.io/rels/create-user in the above code is NOT the end-point the form is posted to. That URL is a custom Extension Relation that identifies the semantics of the operation. In the example above, the form will be posted to https://api.waychaser.io/users

    DELETE, POST, PUT, PATCH

    As mentioned above, waychaser supports Link and Link-Template headers that include method properties, to specify the HTTP method the client must use to execute the relationship.

    For instance if our resource has the following link

    Link header:

    Link: <https://api.waychaser.io/example/some-resource>; rel="https://api.waychaser.io/rel/delete"; method="DELETE";
    

    Then the following code

    const deletedResource = await apiResource.invoke('https://waychaser.io/rel/delete')

    will send a HTTP DELETE to https://api.waychaser.io/example/some-resource.

    NOTE: The method property is not part of the specification for Link

    (RFC8288) or Link-Template headers. This means that if you use waychaser with a server that provides this headers and it uses the method property for something else, then you're going to need a custom handler.

    Examples

    HAL

    The following code demonstrates using waychaser with the REST API for AWS API Gateway to download the 'Error' schema from 'test-waychaser' gateway

    import { waychaser, halHandler, MediaTypes } from '@mountainpass/waychaser'
    import fetch from 'isomorphic-fetch'
    import aws4 from 'aws4'
    
    
    // AWS makes us sign each request. This is a fetcher that does that automagically for us.
    /**
     * @param url
     * @param options
     */
    function awsFetch (url, options) {
      const parsedUrl = new URL(url)
      const signedOptions = aws4.sign(
        Object.assign(
          {
            host: parsedUrl.host,
            path: `${parsedUrl.pathname}?${parsedUrl.searchParams}`,
            method: 'GET'
          },
          options
        )
      )
      return fetch(url, signedOptions)
    }
    
    // Now we tell waychaser, to only accept HAL and to use our fetcher.
    const awsWayChaser = waychaser.use(halHandler, MediaTypes.HAL).withFetch(awsFetch)
    
    // now we can load the API
    const api = await waychaser.load(
      'https://apigateway.ap-southeast-2.amazonaws.com/restapis'
    )
    
    // then we can find the gateway we're after
    const gateway = await api.ops
      .filter('item')
      .findInRelated({ name: 'test-waychaser' })
    
    // then we can get the models
    const models = await gateway.invoke(
      'http://docs.aws.amazon.com/apigateway/latest/developerguide/restapi-restapi-models.html'
    )
    
    // then we can find the schema we're after 
    const model = await models.ops
      .filter('item')
      .findInRelated({ name: 'Error' })
    
    // and now we get the schema
    const schema = JSON.parse((await model.body()).schema)

    NOTE: While the above is a legit, and it works (here's the test), for full use of the AWS API Gateway REST API, you're going to need a custom handler.

    This is because HAL links are supposed retrieved using a HTTP GET, but many of the AWS API Gateway REST API links require using POST, PATCH or DELETE HTTP methods.

    But there's nothing in AWS API Gateway links to tell you when to use a different HTTP method. Instead it's communicated out-of-band in AWS API Gateway documentation. If you write a custom handler, please let me know 👍

    Siren

    While admittedly this is a toy example, the following code demonstrates using waychaser to complete the Hypermedia in the Wizard's Tower text-based adventure game.

    But even though it's a game, it shows how waychaser can easily navigate a complex process, including POSTing data and DELETEing resources.

      return waychaser
        .load('http://hyperwizard.azurewebsites.net/hywit/void')
        .then(current =>
          current.invoke('start-adventure', {
            name: 'waychaser',
            class: 'Burglar',
            race: 'waychaser',
            gender: 'Male'
          })
        )
        .then(current => {
          if (current.response.status <= 500) return current.invoke('related')
          else throw new Error('Server Error')
        })
        .then(current => current.invoke('north'))
        .then(current => current.invoke('pull-lever'))
        .then(current =>
          current.invoke({ rel: 'move', title: 'Cross the bridge.' })
        )
        .then(current => current.invoke('move'))
        .then(current => current.invoke('look'))
        .then(current => current.invoke('eat-snacks'))
        .then(current => current.invoke('related'))
        .then(current => current.invoke('north'))
        .then(current => current.invoke('pull-lever'))
        .then(current => current.invoke('look'))
        .then(current => current.invoke('eat-snacks'))
        .then(current => current.invoke('enter'))
        .then(current => current.invoke('answer-skull', { master: 'Edsger' }))
        .then(current => current.invoke('east'))
        .then(current => current.invoke('smash-mirror-1') || current)
        .then(current => current.invoke('related') || current)
        .then(current => current.invoke('smash-mirror-2') || current)
        .then(current => current.invoke('related') || current)
        .then(current => current.invoke('smash-mirror-3') || current)
        .then(current => current.invoke('related') || current)
        .then(current => current.invoke('smash-mirror-4') || current)
        .then(current => current.invoke('related') || current)
        .then(current => current.invoke('smash-mirror-5') || current)
        .then(current => current.invoke('related') || current)
        .then(current => current.invoke('smash-mirror-6') || current)
        .then(current => current.invoke('related') || current)
        .then(current => current.invoke('smash-mirror-7') || current)
        .then(current => current.invoke('related') || current)
        .then(current => current.invoke('look'))
        .then(current => current.invoke('enter-mirror'))
        .then(current => current.invoke('north'))
        .then(current => current.invoke('down'))
        .then(current => current.invoke('take-book-3'))

    Upgrading from 1.x to 2.x

    Removal of Loki

    Loki is no longer use for storing operations and has been replaced with an subclass of Array. We originally introduced Loki it's querying capability, but it turned out to be far to large a dependency.

    Operation count

    Previously you could get the number of operations on a resource by calling

    apiResource.count()

    For 2.x, replace this with

    apiResource.length

    Finding operations

    To find an operation, instead of using

    apiResource.operations.findOne(relationship)
    // or
    apiResource.operations.findOne({ rel: relationship })
    // or
    apiResource.ops.findOne(relationship)
    // or
    apiResource.ops.findOne({ rel: relationship })

    use

    apiResource.operations.find(relationship)
    // or
    apiResource.operations.find({ rel: relationship })
    // or
    apiResource.operations.find(operation => {
      return operation.rel === relationship
    })
    // or
    apiResource.ops.find(relationship)
    // or
    apiResource.ops.find({ rel: relationship })
    // or
    apiResource.ops.find(operation => {
      return operation.rel === relationship
    })

    Additionally when invoking an operation, you can use an array finder function as well. e.g. the following are all equivalent

    await apiResource.invoke(relationship)
    await apiResource.invoke({ rel: relationship })
    await apiResource.invoke(operation => {
      return operation.rel === relationship
    })
    await apiResource.operations.invoke(relationship)
    await apiResource.operations.invoke({ rel: relationship })
    await apiResource.operations.invoke(operation => {
      return operation.rel === relationship
    })
    await apiResource.ops.invoke(relationship)
    await apiResource.ops.invoke({ rel: relationship })
    await apiResource.ops.invoke(operation => {
      return operation.rel === relationship
    })
    await apiResource.operations.find(relationship).invoke()
    await apiResource.operations.find({ rel: relationship }).invoke()
    await apiResource.operations.find(operation => {
      return operation.rel === relationship
    }).invoke()
    await apiResource.ops.find(relationship).invoke()
    await apiResource.ops.find({ rel: relationship }).invoke()
    await apiResource.ops.find(operation => {
      return operation.rel === relationship
    }).invoke()

    NOTE: When findOne could not find an operation, null was returned, whereas when find cannot find an operation it returns undefined

    Upgrading from 2.x to 3.x

    Accept Header

    waychaser now automatically provides an accept header in requests.

    The accept header can be overridden for individual requests, by including an alternate header.accept value in the options parameter when calling the invoke method.

    Handlers

    The use method now expects both a handler and the mediaType it can handle. WayChaser uses the provided mediaTypes to automatically generate the accept request header.

    NOTE: Currently waychaser does use the corresponding content-type header to filter the responses passed to handlers. THIS MAY CHANGE IN THE FUTURE. Handlers should only process responses that match the mediaType provided when they are registered using the use method.

    Error responses

    In 2.x waychaser would throw an Error if response.ok was false. This is no longer the case as some APIs provide hypermedia responses for 4xx and 5xx responses.

    Code like the following

    try {
      return apiResource.invoke(relationship)
    } catch(error) {
      if( error.response ) {
        // handle error response...
      }
      else {
        // handle fetch error
      }
    }

    should be replaced with

    try {
      const resource = await apiResource.invoke(relationship)
      if( resource.response.ok ) {
        return resource
      }
      else {
        // handle error response...
      }
    } catch(error) {
      // handle fetch error
    }

    or if there is no special processing needed for error responses

    try {
      return apiResource.invoke(relationship)
    } catch(error) {
      // handle fetch error
    }

    Invoking missing operations

    In 2.x invoking an operation that didn't exist would throw an error, leading to code like

    const found = apiResource.ops.find(relationship)
    if( found ) {
      return found.invoke()
    }
    else {
      // handle op missing
    }

    In 3.x invoking an operation that doesn't exist returns undefined, allowing for simpler code, as follows

    const resource = await apiResource.invoke(relationship)
    if( resource === undefined ) {
      // handle operation missing 
    }

    or

    return apiResource.invoke(relationship) || //... return a default

    NOTE: When we say it returns undefined we actually mean undefined, NOT a promise the resolves to undefined. This is what makes the ...invoke(rel) || default code possible.

    Handling location headers

    WayChaser 3.x now includes a location header hander, which will create an operation with the related relationship. This allows support for APIs that, when creating a resource (ie using POST), provide a location to the created resource in the response, or APIs that, when updating a resource (ie using PUT or PATCH), provide a location to the updated resource in the response.

    Upgrading from 3.x to 4.x

    Previously WayChaser provided a default instance via the waychaser export. This is no longer the case and you will need to create your own instance using new WayChaser()

    Install

    npm i @mountainpass/waychaser

    DownloadsWeekly Downloads

    365

    Version

    4.0.22

    License

    Apache-2.0

    Unpacked Size

    862 kB

    Total Files

    52

    Last publish

    Collaborators

    • avatar
    • avatar