Giter Club home page Giter Club logo

client's Introduction

Siren.js Client

Node Package Build Workflow standard-readme compliant License Contributing

Siren API client library for JavaScript

This library handles much of the boilerplate, protocol-level details around interacting with a Siren API. Here are the things it can do:

  • Parse and validate Siren representations
  • Follow a Link (or any URL)
  • Submit an Action
    • Customize Field validation and serialization
  • Resolve a SubEntity
  • Traverse an Entity via the Visitor pattern
  • Crawl a Siren API

Table of Contents

Install

npm install @siren-js/client

Usage

import { follow, parse, resolve, submit } from '@siren-js/client';

// follow API entry point
let response = await follow('https://api.example.com/entry-point');
// parse the response as Siren
let entity = await parse(response);

// find the first 'item' sub-entity
const itemSubEntity = entity.entities.find((subEntity) => subEntity.rel.includes('item'));
if (itemSubEntity != null) {
  // resolve the sub-entity to a full entity
  entity = await resolve(itemSubEntity);
}

// find the first 'next' link
const nextLink = entity.links.find((link) => link.rel.includes('next'));
if (nextLink != null) {
  // follow the 'next' link, if present, and parse as a Siren entity
  entity = await follow(nextLink).then(parse);
}

// find the 'edit' action
const editAction = entity.getAction('edit');
if (editAction != null) {
  // find the 'quantity' field in the 'edit' action
  const quantityField = editAction.getField('quantity');
  if (quantityField != null) {
    // set the 'quantity' field value
    quantityField.value = 69;
  }
  // submit the action and parse the response as Siren
  response = await submit(editAction).then(parse);
}

Development

# setup Node.js
$ nvm use

# test with Jest
$ npm test
# run tests in watch mode
$ npm run test:watch
# run tests with coverage
$ npm run test:cov

# compile TypeScript code
$ npm run compile

# lint with ESLint
$ npm run lint
# automatically fix lint issues where possible
$ npm run lint:fix

# format files with Prettier
$ npm run format
# check files for proper formatting
$ npm run format:check

# build the library (compile, lint, format check)
$ npm run build:lib

# generate docs with TypeDoc
$ npm run build:docs

API

See our docs.

Maintainer

@dillonredding

Contributing

See our contribution guidelines.

PRs accepted.

If editing the README, please conform to the standard-readme specification.

License

MIT ยฉ 2021 Dillon Redding

client's People

Contributors

dillonredding avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

Forkers

shadowman777

client's Issues

Support for application/json in the default serializer

The default serializer could support serializing an Action with type 'application/json'. The resulting serialization would be a JSON object whose entries are the name-value pairs from each Field in the Action.

For example, these fields:

[
  { "name": "foo", "value": "bar" },
  { "name": "baz", "value": 42 },
  { "name": "qux", "value": true }
]

would serialize as:

{
  "foo": "bar",
  "baz": 42,
  "qux": true
}

Alternatively, the fields could simply be sent as is (as an array), but that scenario seems less desirable since the field's may contain a lot of metadata that the server does not need.

Simplify Default Field Serialization

In hindsight, borrowing from HTML's entry list construction and name-value pair conversion for serializing fields in action submission is overkill and introduces artifacts (e.g., newline normalization) that are likely only the result of HTML's historical baggage. I recommend this be simplified so that a Field's value is the thing sent to the server. Since value's type is parameterized, the serialization can be driven by that. For example, when type is date and value is a Date object, serialize to YYYY-MM-DD. When type is file, value should be of type File, or if multiple is set to true, it should be File[].

Revisit Checkbox Fields

Currently, checkbox fields work similar to HTML.

{
  "type": "checkbox",
  "name": "foo",
  "value": "bar",
  "checked": false
}

In the above Field example, foo=bar will only be sent to the server if checked is truthy. Otherwise, foo is not sent at all (see here). However, this requires clients to understand and utilize the checked extension for submission. I think we should abandon the need for the extension and send the field's value regardless. The checkedness can be tracked elsewhere and used to update the field's value (e.g., to true/false).

Basically, checkbox fields should have the type Field<boolean>.

Multipart Form Data Support

It would be nice to have support for multipart/form-data actions. However, due to limitations in node-fetch (namely v2.6.1, the version used by cross-fetch), this is difficult to do in a cross-platform manner.

The docs for node-fetch recommend using form-data, however, it can't handle File objects from @web-std/file, which are what we use for file fields.

Need to weigh a few options:

  1. Extend the FormData class from form-data. This could cause issues with browser clients.
  2. Contribute to form-data to add support for File-like objects.
  3. Find another package to support FormData in Node.
  4. Implement our own cross-platform FormData class. Not sure if multipart would/could be auto-detected.
  5. Manually serialize to a multipart/form-data string according to Section 5.1 of RFC 2046.

Add a populate action utility

Action should have a populate method that accepts an object, and sets the value of each field according the values in the object.

const field1 = new Field();
field1.name = 'foo';

const field2 = new Field();
field2.name = 'baz';

const field3 = new Field();
field3.name = 'qux';
field3.value = true;

const action = new Action();
action.fields = [field1, field2, field3];

action.populate({
  foo: 'bar',
  baz: 42,
  qux: undefined // specify undefined to remove a value from a field
});

field1.value; //=> 'bar'
field2.value; //=> 42
field3.value; //=> undefined

TypeError: Reflect.getMetadata is not a function

Getting the following error when trying to use the client:

Uncaught TypeError: Reflect.getMetadata is not a function

Looks like reflect-metadata needs to be moved from devDepenedencies to dependencies in package.json.

Support Client-Side Validation

When submitting an action, there should be an option to enable/customize client-side validation.

abstract class ValidationResult;

class PositiveValidationResult extends ValidationResult;

class NegativeValidationResult extends ValidationResult {
  invalidFields: Field[];
}

type Validator = (fields: Field[]) => ValidationResult;

// custom validator
await submit(action, {
  validator: (fields: Field[]): ValidationResult => {
    const invalidFields: Field[] = [];
    // validate fields (e.g., based on `type`)
    return invalidFields.length > 0
      ? new NegativeValidationResult(invalidFields)
      : new PositiveValidationResult();
  }
});

// disable validation
await submit(action, { validate: false });

By default, we could provide a default validator that functions similar to HTML's client-side validation.

Allow Users to Traverse an Entity

An Entity could easily be traversed by implementing the Visitor pattern, allowing operations to be added to an entity graph without modifying the implementation. I believe this would also make implementing a Siren API crawler (#15) almost trivial.

Similar to this Java example, we could have something like the following:

export interface SirenElementVisitor {
  // need individual methods since JS/TS doesn't support overloading
  visitAction(action: Action): void;
  visitEmbeddedEntity(embeddedEntity: EmbeddedEntity): void;
  visitEmbeddedLink(embeddedLink: EmbeddedLink): void;
  visitEntity(entity: Entity): void;
  visitField(field: Field): void;
  visitLink(link: Link): void;
}

// each model class implements this interface
interface SirenElement {
  accept(visitor: SirenElementVisitor): void;
}

This requires each element to know how to traverse is child elements (not necessarily a bad thing). One advantage with this approach is that polymorphism handles distinguishing sub-entity types (no instanceof checks ๐Ÿ˜Œ).

Alternatively, instead of implementing SirenElement in each model, we could have a traverse function that understands how to traverse an entity.

function traverse(entity: Entity, visitor: SirenElementVisitor);

This fits well with the other top-level methods we have and has all the traversal logic, but the downside here is that we either need to manually distinguish between sub-entity types or have a generic visitSubEntity method, which will likely always have the boilerplate

if (subEntity instanceof EmbeddedLink) /* ... */ else /* ... */

The primary disadvantage of traverse is that you always have to start at the entity level, preventing users from (e.g.) creating an action-populating and submitting visitor.

Need to think more about the pros/cons of either approach, especially in regards to how async visits are handled (e.g., visitAction may call submit).

Type Error in React and Angular

When using v0.3.0 in a React or Angular app, the following type error occurs:

TypeError: Cannot destructure property 'ReadableStream' of 'web_streams_polyfill__WEBPACK_IMPORTED_MODULE_1___default.a' as it is undefined.

Option to follow EmbeddedEntity on resolution

Might be nice for the resolve function to include an option to follow the self link of an EmbeddedEntity and parse the result, especially for cases when the EmbeddedEntity may only be a partial representation (i.e., some properties, links, etc. may be missing that would be present in the full, top-level representation).

Submit doesn't handle header name-value pairs array

Given an action that would generate a request body, the following:

submit(action, {
  requestInit: {
    headers: [['Foo', 'bar']]
  }
});

generates the following request:

POST /foo HTTP/1.1
Content-Type: application/x-www-form-urlencoded
0: Foo,bar

...

Absorb @siren-js/core

In an attempt to separate client and server concerns, this library should absorb (and simplify) the parse/validate logic of @siren-js/core since a server should not need to perform those operations, and if they do, that likely means the server also acts as a client and should therefore use is library. From there we can write a library better tailored to Siren servers and deprecate @siren-js/core.

Support Relative URLs

The follow and submit functions should support relative URLs by passing a baseUrl option.

const baseUrl = new URL('http://api.example.com');
const response = await follow(link, { baseUrl }); // requires breaking change

Support more element searching

Might be convenient to have more methods for searching the elements in an entity.

entity.findLink({ rel: ["up"] });
//=> returns the first link with an `up` link relation, or `null`

entity.filterLinks({ class: ["person"] });
//=> returns array of `person` links (may be empty)

Things to consider:

  1. Method names (find* and filter* vs. get*(s))
  2. What happens when (e.g.) rel and class are provided (conjunction?)
  3. What happens when multiple rel/class/etc. values are provided (disjunction?)

Support Object Field Values

There is currently support for primitive, Date, and array field values, but I would consider support for object values. The main question is around serialization.

We might be able to borrow some ideas from how the OpenAPI spec handles different styles of parameter serialization. In the current implementation, we treat array values as an exploded form (style=form, explode=true). For example, this field:

{
  "name": "foo",
  "value": ["bar", 42]
}

Results in foo=bar&foo=42 (assuming application/x-www-form-urlencoded). However, with an object field:

{
  "name": "foo",
  "value": {
    "bar": "baz",
    "qux": 42
  }
}

We'd get bar=baz&qux=42 and lose the field name. This also runs the risk of unintentionally adding values to other fields with the same name. Not ideal.

So, the other viable options are unexploded form (style=form, explode=false) and deep object (style=deepObject, explode=true).

Treating objects as an unexploded form provides a concise serialization, but wouldn't be able to support nested objects (is that even something we need?). In the example above, we'd get foo=bar,baz,qux,42. On the other hand, the "deep object" approach effectively turns each entry into its own "parameter" and (theoretically) accounts for nested objects. In this case, we'd get foo[bar]=baz&foo[qux]=42.

I think the "deep object" strategy is more intuitive (at least with how arrays work) and gives us the most mileage. Perhaps we could follow up with a serialization option to select or even customize the object serialization strategy.

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.