UNPKG

rxrest

Version:

RxRest a reactive REST utility

576 lines (398 loc) 14.7 kB
RxRest [![Build Status](https://travis-ci.org/soyuka/rxrest.svg?branch=master)](https://travis-ci.org/soyuka/rxrest) ====== > A reactive REST utility Highly inspirated by [Restangular](https://github.com/mgonto/restangular), this library implements a natural way to interact with a REST API. ## Install ``` npm install rxrest --save ``` ## Example ```javascript import { RxRest, RxRestConfig } from 'rxrest' const config = new RxRestConfig() config.baseURL = 'http://localhost/api' const rxrest = new RxRest(config) rxrest.all('cars') .get() .subscribe((cars: Car[]) => { /** * `cars` is: * RxRestCollection [ * RxRestItem { name: 'Polo', id: 1, brand: 'Audi' }, * RxRestItem { name: 'Golf', id: 2, brand: 'Volkswagen' } * ] */ cars[0].brand = 'Volkswagen' cars[0].save() .subscribe(result => { console.log(result) /** * outputs: RxRestItem { name: 'Polo', id: 1, brand: 'Volkswagen' } */ }) }) ``` ## Menu - [Technical concepts](#technical-concepts) - [Promise compatibility](#promise-compatibility) - [One-event Stream instead of multiple events](#one-event-stream-instead-of-multiple-events) - [Object state (`$fromServer`, `$pristine`, `$uuid`)](#object-state-fromserver-pristine-uuid) - [Configuration](#configuration) - [Interceptors](#interceptors) - [Handlers](#handlers) - [API](#api) - [Typings](#typings) - [Angular 2 configuration example](#angular-2-configuration-example) ## Technical concepts This library uses a [`fetch`-like](https://developer.mozilla.org/en-US/docs/Web/API/GlobalFetch) library to perform HTTP requests. It has the same api as fetch but uses XMLHttpRequest so that requests have a cancellable ability! It also makes use of [`Proxy`](https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Proxy) and implements an [`Iterator`](https://developer.mozilla.org/fr/docs/Web/JavaScript/Guide/iterateurs_et_generateurs) on `RxRestCollection`. Because it uses fetch, the RxRest library uses it's core concepts. It will add an `Object` compatibility layer to [URLSearchParams](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/URLSearchParams) for query parameters and [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers). It is also familiar with `Body`-like object, as `FormData`, `Response`, `Request` etc. This script depends on `superagent` (for a easier XMLHttpRequest usage, compatible in both node and the browser) and `rxjs` for the reactive part. <sup>[^ Back to menu](#menu)</sup> ## Promise compatibility Just use the `toPromise` utility: ```javascript rxrest.one('foo') .get() .toPromise() .then(item => { console.log(item) }) ``` <sup>[^ Back to menu](#menu)</sup> ## One-event Stream instead of multiple events Sometimes, you may want RxRest to emit one event per item in the collection: To do so, just call `asIterable(false)`: ```javascript rxrest.all('cars') .asIterable(false) .get() // next() is called with every car available .subscribe((e) => {}) ``` Or use the second argument of `.all` instead of `asIterable`: ```javascript rxrest.all('cars', false) .get() // next() is called with every car available .subscribe((e) => {}) ``` ## Object state (`$fromServer`, `$pristine`, `$uuid`) Thanks to the Proxy, we can get metadata informations about the current object and it's state. When you instantiate an object, it's `$pristine`. When it gets modified it's dirty: ```javascript const rxrest = new RxRest() const car = rxrest.one('cars', 1) assert(car.$prisine === true) car.brand = 'Ford' assert(car.$prisine === false) ``` You can also check that the item comes from the server: ```javascript const rxrest = new RxRest() const car = rxrest.one('cars', 1) assert(car.$fromServer === false) // we just instantiated it in the client car.save() .subscribe((car) => { assert(car.$fromServer === true) //now it's from the server assert(car.$prisine === true) //it's also pristine! }) ``` <sup>[^ Back to menu](#menu)</sup> ## Configuration Setting up `RxRest` is done via `RxRestConfiguration`: ```javascript const config = new RxRestConfiguration() ``` #### `baseURL` It is the base url prepending your routes. For example : ```javascript //set the url config.baseURL = 'http://localhost/api' const rxrest = new RxRest(config) //this will request GET http://localhost/api/cars/1 rxrest.one('cars', 1) .get() ``` #### `identifier='id'` This is the key storing your identifier in your api objects. It defaults to `id`. ```javascript config.identifier = '@id' const rxrest = new RxRest(config) rxrest.one('cars', 1) > RxRestItem { '@id': 1 } ``` #### `headers` You can set headers through the configuration, but also change them request-wise: ```javascript config.headers config.headers.set('Authorization', 'foobar') config.headers.set('Content-Type', 'application/json') const rxrest = new RxRest(config) // Performs a GET request on /cars/1 with Authorization and an `application/json` content type header rxrest.one('cars', 1).get() // Performs a POST request on /cars with Authorization and an `application/x-www-form-urlencoded` content type header rxrest.all('cars') .post(new FormData(), null, {'Content-Type': 'application/x-www-form-urlencoded'}) ``` #### `queryParams` You can set query parameters through the configuration, but also change them request-wise: ```javascript config.queryParams.set('bearer', 'foobar') const rxrest = new RxRest(config) // Performs a GET request on /cars/1?bearer=foobar rxrest.one('cars', 1).get() // Performs a GET request on /cars?bearer=barfoo rxrest.all('cars') .get({bearer: 'barfoo'}) ``` #### `uuid` It tells RxRest to add an uuid to every resource. This is great if you need a unique identifier that's not related to the data of a collection (useful in forms): ```javascript //set the url config.uuid = true const rxrest = new RxRest(config) rxrest.one('cars', 1) .get() .subscribe((car: Car) => { console.log(car.$uuid) }) ``` Also works in a non-`$fromServer` resource: ``` const car = rxrest.fromObject('cars') console.log(car.$uuid) ``` <sup>[^ Back to menu](#menu)</sup> ## Interceptors You can add custom behaviors on every state of the request. In order those are: 1. Request 2. Response 3. Error To alter those states, you can add interceptors having the following signature: 1. `requestInterceptor(request: Request)` 2. `responseInterceptor(request: Body)` 3. `errorInterceptor(error: Response)` Each of those can return a Stream, a Promise, their initial altered value, or be void (ie: return nothing). For example, let's alter the request and the response: ```javascript config.requestInterceptors.push(function(request) { request.headers.set('foo', 'bar') }) // This alters the body (note that ResponseBodyHandler below is more appropriate to do so) config.responseInterceptors.push(function(response) { return response.text( .then(data => { data = JSON.parse(data) data.foo = 'bar' //We can read the body only once (see Body.bodyUsed), here we return a new Response return new Response(JSON.stringify(body), response) }) }) // Performs a GET request with a 'foo' header having `bar` as value const rxrest = new RxRest(config) rxrest.one('cars', 1) .get() > RxRestItem<Car> {id: 1, brand: 'Volkswagen', name: 'Polo', foo: 1} ``` <sup>[^ Back to menu](#menu)</sup> ## Handlers Handlers allow you to transform the Body before or after a request is issued. Those are the default values: ```javascript /** * This method transforms the requested body to a json string */ config.requestBodyHandler = function(body) { if (!body) { return undefined } if (body instanceof FormData || body instanceof URLSearchParams) { return body } return body instanceof RxRestItem ? body.json() : JSON.stringify(body) } /** * This transforms the response in an Object (ie JSON.parse on the body text) * should return Promise<{body: any, metadata: any}> */ config.responseBodyHandler = function(body) { return body.text() .then(text => { return {body: text ? JSON.parse(text) : null, metadata: null} }) } ``` In the `responseBodyHandler`, you can note that we're returning an object containing: 1. `body` - the javascript Object or Array that will be transformed in a RxRestItem or RxRestCollection 2. `metadata` - an API request sometimes gives us metadata (for example pagination metadata), add it here to be able to retrieve `item.$metadata` later <sup>[^ Back to menu](#menu)</sup> ## API There are two prototypes: - RxRestItem - RxRestCollection - an iterable collection of RxRestItem ### Available on both RxRestItem and RxRestCollection #### `one(route: string, id: any): RxRestItem` Creates an RxRestItem on the requested route. #### `all(route: string, asIterable: boolean = false): RxRestCollection` Creates an RxRestCollection on the requested route Note that this allows url composition: ```javascript rxrest.all('cars').one('audi', 1).URL > cars/audi/1 ``` #### `fromObject(route: string, element: Object|Object[]): RxRestItem|RxRestCollection` Depending on whether element is an `Object` or an `Array`, it returns an RxRestItem or an RxRestCollection. For example: ```javascript const car = rxrest.fromObject('cars', {id: 1, brand: 'Volkswagen', name: 'Polo'}) > RxRestItem<Car> {id: 1, brand: 'Volkswagen', name: 'Polo'} car.URL > cars/1 ``` RxRest automagically binds the id in the route, note that the identifier property is configurable. #### `get(queryParams?: Object|URLSearchParams, headers?: Object|Headers): Stream<RxRestItem|RxRestCollection>` Performs a `GET` request, for example: ```javascript rxrest.one('cars', 1).get({brand: 'Volkswagen'}) .subscribe(e => console.log(e)) GET /cars/1?brand=Volkswagen > RxRestItem<Car> {id: 1, brand: 'Volkswagen', name: 'Polo'} ``` #### `post(body?: BodyParam, queryParams?: Object|URLSearchParams, headers?: Object|Headers): Stream<RxRestItem|RxRestCollection>` Performs a `POST` request, for example: ```javascript const car = new Car({brand: 'Audi', name: 'A3'}) rxrest.all('cars').post(car) .subscribe(e => console.log(e)) > RxRestItem<Car> {id: 3, brand: 'Audi', name: 'A3'} ``` #### `remove(queryParams?: Object|URLSearchParams, headers?: Object|Headers): Stream<RxRestItem|RxRestCollection>` Performs a `DELETE` request #### `patch(body?: BodyParam, queryParams?: Object|URLSearchParams, headers?: Object|Headers): Stream<RxRestItem|RxRestCollection>` Performs a `PATCH` request #### `head(queryParams?: Object|URLSearchParams, headers?: Object|Headers): Stream<RxRestItem|RxRestCollection>` Performs a `HEAD` request #### `trace(queryParams?: Object|URLSearchParams, headers?: Object|Headers): Stream<RxRestItem|RxRestCollection>` Performs a `TRACE` request #### `request(method: string, body?: BodyParam): Stream<RxRestItem|RxRestCollection>` This is useful when you need to do a custom request, note that we're adding query parameters and headers ```javascript rxrest.all('cars/1/audi') .setQueryParams({foo: 'bar'}) .setHeaders({'Content-Type': 'application/x-www-form-urlencoded'}) .request('GET') ``` This will do a `GET` request on `cars/1/audi?foo=bar` with a `Content-Type` header having a `application/x-www-form-urlencoded` value. #### `json(): string` Output a `JSON` string of your RxRest element. ```javascript rxrest.one('cars', 1) .get() .subscribe((e: RxRestItem<Car>) => console.log(e.json())) > {id: 1, brand: 'Volkswagen', name: 'Polo'} ``` #### `plain(): Object|Object[]` This gives you the original object (ie: not an instance of RxRestItem or RxRestCollection): ```javascript rxrest.one('cars', 1) .get() .subscribe((e: RxRestItem<Car>) => console.log(e.plain())) > {id: 1, brand: 'Volkswagen', name: 'Polo'} ``` #### `clone(): RxRestItem|RxRestCollection` Clones the current instance to a new one. ### RxRestCollection #### `getList(): Stream<RxRestCollection>` Just a reference to Restangular ;). It's an alias to `get()`. ### RxRestItem #### `save(): RxRestCollection` Do a `POST` or a `PUT` request according to whether the resource came from the server or not. This is due to an internal property `fromServer`, which is set when parsing the request result. <sup>[^ Back to menu](#menu)</sup> ## Typings Interfaces: ```typescript import { RxRest, RxRestItem, RxRestConfig } from 'rxrest'; const config = new RxRestConfig() config.baseURL = 'http://localhost' interface Car { id: number; name: string; model: string; } const rxrest = new RxRest(config) rxrest.one<Car>('/cars', 1) .get() .subscribe((item: Car) => { console.log(item.model) item.model = 'audi' item.save() }) ``` If you work with [Hypermedia-Driven Web APIs (Hydra)](http://www.markus-lanthaler.com/hydra/), you can extend a default typing for you items to avoid repetitions: ```typescript interface HydraItem<T> { '@id': string; '@context': string; '@type': string; } interface Car extends HydraItem<Car> { name: string; model: Model; color: string; } interface Model extends HydraItem<Model> { name: string; } ``` To know more about typings and rxrest, please check out [the typings example](https://github.com/soyuka/rxrest/blob/master/test/typings.ts). <sup>[^ Back to menu](#menu)</sup> ## Angular 2 configuration example First, let's declare our providers: ```typescript import { Injectable, NgModule, Component, OnInit } from '@angular/core' import { RxRest, RxRestConfiguration } from 'rxrest' @Injectable() export class AngularRxRestConfiguration extends RxRestConfiguration { constructor() { super() this.baseURL = 'localhost/api' } } @Injectable() export class AngularRxRest extends RxRest { constructor(config: RxRestConfiguration) { super(config) } } @NgModule({ providers: [ {provide: RxRest, useClass: AngularRxRest}, {provide: RxRestConfiguration, useClass: AngularRxRestConfiguration}, ] }) export class SomeModule { } ``` Then, just inject `RxRest`: ```typescript export interface Car { name: string } @Component({ template: '<ul><li *ngFor="let car of cars | async">{{car.name}}</li></ul>' }) export class FooComponent implements OnInit { constructor(private rxrest: RxRest) { } ngOnInit() { this.cars = this.rxrest.all<Car>('cars', true).get() } } ``` [Full example featuring jwt authentication, errors handling, body parsers for JSON-LD](https://gist.github.com/soyuka/c2e89ebf3c7a33f8d059c567aefd471c) <sup>[^ Back to menu](#menu)</sup> ## Test Testing can be done using [`rxrest-assert`](https://github.com/soyuka/rxrest-assert). ## Licence MIT