├── .npmignore ├── .gitignore ├── superagent.d.ts ├── .babelrc ├── polyfills.js ├── .travis.yml ├── bump.sh ├── tsconfig.json ├── src ├── utils.ts ├── interfaces.d.ts ├── fetch.ts ├── RxRestProxyHandler.ts ├── RxRestConfiguration.ts ├── index.ts └── RxRest.ts ├── rollup.config.js ├── LICENCE ├── tslint.json ├── package.json ├── test ├── typings.ts ├── urlsearchparamspolyfill.js └── index.js ├── typings └── rxrest.d.ts └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | .awcache 4 | lib 5 | coverage 6 | build 7 | -------------------------------------------------------------------------------- /superagent.d.ts: -------------------------------------------------------------------------------- 1 | declare let _default: {} 2 | declare module 'superagent' { 3 | export default _default; 4 | } 5 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | // .babelrc 2 | { 3 | "presets": [ 4 | [ 5 | "env", 6 | { 7 | "modules": false 8 | } 9 | ] 10 | ], 11 | "plugins": [ 12 | "external-helpers" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /polyfills.js: -------------------------------------------------------------------------------- 1 | const {Headers, Response, Request} = require('node-fetch'); 2 | require('./test/urlsearchparamspolyfill.js') 3 | global.Headers = Headers 4 | global.Response = Response 5 | global.Request = Request 6 | global.FormData = require('form-data') 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | node_js: 10 | - 8 11 | - 10 12 | 13 | before_script: 14 | - npm run lint 15 | - npm run build 16 | - npm run bundle 17 | 18 | after_script: 19 | - tsc --noEmit --target es6 --module commonjs test/typings.ts 20 | -------------------------------------------------------------------------------- /bump.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | [[ '' == $1 ]] && echo "Please provide patch, minor, major argument" && exit 1 4 | 5 | npm run lint 6 | npm run build 7 | npm run bundle 8 | npm test 9 | newver=$(npm --no-git-tag-version version $1) 10 | git add -f build package.json package-lock.json 11 | git commit -m $newver 12 | git tag $newver 13 | npm publish 14 | git reset --hard HEAD~1 15 | newver=$(npm --no-git-tag-version version $1) 16 | git add package.json package-lock.json 17 | git commit -m $newver 18 | git push --tags 19 | git push 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "removeComments": false, 4 | "preserveConstEnums": true, 5 | "sourceMap": true, 6 | "declaration": true, 7 | "noImplicitAny": true, 8 | "suppressImplicitAnyIndexErrors": true, 9 | "module": "commonjs", 10 | "target": "es6", 11 | "outDir": "lib/" 12 | }, 13 | "formatCodeOptions": { 14 | "indentSize": 2, 15 | "tabSize": 2 16 | }, 17 | "files": [ 18 | "superagent.d.ts", 19 | "src/fetch.ts", 20 | "src/index.ts" 21 | ], 22 | "awesomeTypescriptLoaderOptions": { 23 | "useBabel": true, 24 | "useCache": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function objectToMap(map: URLSearchParams | Headers, item: any): any { 2 | for (let key in item) { 3 | if (Array.isArray(item[key])) { 4 | for (let i = 0; i < item[key].length; i++) { 5 | map.append(key, item[key][i]) 6 | } 7 | } else { 8 | map.append(key, item[key]) 9 | } 10 | } 11 | 12 | return map 13 | } 14 | 15 | /** 16 | * UUID generator https://gist.github.com/jed/982883 17 | */ 18 | export function uuid(a: any = '', b: any = '') { 19 | for (; a++ < 36; b += a * 51 & 52 ? ( 20 | a ^ 15 ? 8 ^ Math.random() * (a ^ 20 ? 16 : 4) : 4 21 | ).toString(16) : '-') { 22 | // 23 | } 24 | return b 25 | } 26 | -------------------------------------------------------------------------------- /src/interfaces.d.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { RxRestItem } from './index' 3 | 4 | export type BodyParam = RxRestItem|FormData|URLSearchParams|Body|Blob|undefined|Object; 5 | 6 | export interface RequestInterceptor { 7 | (request: Request): Observable|Promise|undefined|Request|void; 8 | } 9 | 10 | export interface ResponseInterceptor { 11 | (body: Body): Observable| 12 | Promise|undefined|Body|void; 13 | } 14 | 15 | export interface ErrorInterceptor { 16 | (response: Response): Observable|void|Response|Promise; 17 | } 18 | 19 | export interface ErrorResponse extends Response { 20 | name: string; 21 | message: string; 22 | } 23 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import ts from 'rollup-plugin-typescript' 2 | import nodeResolve from 'rollup-plugin-node-resolve' 3 | import uglify from 'rollup-plugin-uglify' 4 | import cjs from 'rollup-plugin-commonjs' 5 | import babel from 'rollup-plugin-babel' 6 | 7 | const plugins = [ 8 | ts(), 9 | babel({exclude: 'node_modules/**'}) 10 | ] 11 | 12 | const prod = process.env.NODE_ENV === 'production' || ~process.argv.indexOf('--prod') 13 | const full = prod || ~process.argv.indexOf('--full') 14 | 15 | if (prod) { 16 | plugins.push(uglify()) 17 | } 18 | 19 | if (full) { 20 | plugins.push( 21 | nodeResolve({ 22 | main: true, 23 | jsnext: true, 24 | browser: true 25 | }), 26 | cjs({ 27 | include: 'node_modules/**', 28 | ignore: ['most', 'symbol-observable'] 29 | }) 30 | ) 31 | } 32 | 33 | export default { 34 | input: 'src/index.ts', 35 | output: { 36 | name: 'rxrest', 37 | sourcemap: true, 38 | format: 'cjs', 39 | file: `build/rxrest${full ? '.bundle' : ''}${prod ? '.min' : ''}.js` 40 | }, 41 | plugins: plugins 42 | } 43 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | RxRest a reactive REST utility 2 | Copyright © 2016 Antoine Bluchet 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the "Software"), 6 | to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 16 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 20 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "curly": true, 4 | "eofline": false, 5 | "align": [true, "parameters"], 6 | "class-name": true, 7 | "indent": [true, "spaces"], 8 | "max-line-length": [true, 100], 9 | "no-consecutive-blank-lines": true, 10 | "no-trailing-whitespace": true, 11 | "no-duplicate-variable": true, 12 | "no-var-keyword": true, 13 | "no-empty": true, 14 | "no-unused-expression": true, 15 | "no-unused-variable": true, 16 | "no-use-before-declare": true, 17 | "no-var-requires": true, 18 | "no-require-imports": true, 19 | "one-line": [true, 20 | "check-else", 21 | "check-whitespace", 22 | "check-open-brace"], 23 | "quotemark": [true, 24 | "single", 25 | "avoid-escape"], 26 | "semicolon": false, 27 | "typedef-whitespace": [true, { 28 | "call-signature": "nospace", 29 | "index-signature": "nospace", 30 | "parameter": "nospace", 31 | "property-declaration": "nospace", 32 | "variable-declaration": "nospace" 33 | }], 34 | "whitespace": [true, 35 | "check-branch", 36 | "check-decl", 37 | "check-operator", 38 | "check-separator", 39 | "check-type"] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rxrest", 3 | "version": "6.0.1", 4 | "description": "RxRest a reactive REST utility", 5 | "main": "build/rxrest.js", 6 | "typings": "build/rxrest.d.ts", 7 | "repository": "https://github.com/soyuka/rxrest", 8 | "scripts": { 9 | "lint": "tslint -c tslint.json src/*.ts", 10 | "coverage": "istanbul cover _mocha test/index.js", 11 | "test": "mocha -b", 12 | "build": "tsc", 13 | "bundle": "bash build.sh" 14 | }, 15 | "keywords": [ 16 | "REST", 17 | "Reactive", 18 | "Rxjs" 19 | ], 20 | "author": "", 21 | "license": "MIT", 22 | "devDependencies": { 23 | "awesome-typescript-loader": "^3.5.0", 24 | "babel-core": "^6.26.0", 25 | "babel-plugin-external-helpers": "^6.22.0", 26 | "babel-preset-env": "^1.7.0", 27 | "body-parser": "^1.18.2", 28 | "chai": "^4.1.2", 29 | "chai-spies": "^1.0.0", 30 | "encoding": "^0.1.12", 31 | "express": "^4.16.2", 32 | "form-data": "^2.3.2", 33 | "istanbul": "^0.4.5", 34 | "mocha": "^5.0.3", 35 | "node-fetch": "^2.1.1", 36 | "rollup": "^0.56.5", 37 | "rollup-plugin-babel": "^3.0.3", 38 | "rollup-plugin-commonjs": "^9.0.0", 39 | "rollup-plugin-node-resolve": "^3.0.3", 40 | "rollup-plugin-typescript": "^0.8.1", 41 | "rollup-plugin-uglify": "^3.0.0", 42 | "tslint": "^5.9.1", 43 | "typescript": "2.7.2", 44 | "uglify-es": "^3.3.9" 45 | }, 46 | "dependencies": { 47 | "rxjs": "^6.2.0", 48 | "superagent": "^3.8.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/fetch.ts: -------------------------------------------------------------------------------- 1 | import {ErrorResponse} from './interfaces' 2 | import * as superagent from 'superagent' 3 | import {Observable, Observer, from as fromPromise} from 'rxjs' 4 | import {mergeMap} from 'rxjs/operators' 5 | 6 | export function fetch( 7 | input: string|Request, 8 | init?: RequestInit, 9 | abortCallback?: (req: Request) => void 10 | ): Observable { 11 | 12 | if (!(input instanceof Request)) { 13 | input = new Request(input, init) 14 | } 15 | 16 | let req = superagent[input.method.toLowerCase()](input.url) 17 | 18 | for (let header of input.headers) { 19 | req.set(header[0], header[1]) 20 | } 21 | 22 | return fromPromise(input.text()) 23 | .pipe( 24 | mergeMap(body => { 25 | req.send(body) 26 | 27 | return Observable.create((observer: Observer) => { 28 | req.end(function(err: any, res: any) { 29 | if (err) { 30 | return observer.error(res) 31 | } 32 | 33 | if (res.noContent === true) { 34 | observer.next(new Response()) 35 | return observer.complete() 36 | } 37 | 38 | res.url = (input as Request).url 39 | let response = new Response(res.text, res) 40 | 41 | observer.next(response) 42 | observer.complete() 43 | }) 44 | 45 | return function abort() { 46 | req.abort() 47 | if (abortCallback) { 48 | abortCallback(req) 49 | } 50 | } 51 | }) 52 | }) 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/RxRestProxyHandler.ts: -------------------------------------------------------------------------------- 1 | import {RxRest} from './RxRest' 2 | 3 | export class RxRestProxyHandler implements ProxyHandler> { 4 | private $internal: PropertyKey[] = []; 5 | private $instance: F; 6 | 7 | constructor(target: F) { 8 | this.$instance = target 9 | do { 10 | this.$internal = this.$internal.concat( 11 | Object.getOwnPropertyNames(target), Object.getOwnPropertySymbols(target) 12 | ) 13 | } while (target = Object.getPrototypeOf(target)) 14 | } 15 | 16 | getPrototypeOf(target: any) { 17 | return Object.getPrototypeOf(this.$instance) 18 | } 19 | 20 | defineProperty(target: any, p: PropertyKey, attributes: PropertyDescriptor): boolean { 21 | if (~this.$internal.indexOf(p)) { 22 | return true 23 | } 24 | 25 | Object.defineProperty(target, p, attributes) 26 | return true 27 | } 28 | 29 | deleteProperty(target: any, p: PropertyKey): boolean { 30 | return delete target[p] 31 | } 32 | 33 | set(target: any, p: PropertyKey, value: any, receiver: any): boolean { 34 | 35 | if (~this.$internal.indexOf(p)) { 36 | this.$instance[p] = value 37 | return true 38 | } 39 | 40 | if ((this.$instance as any).$pristine === true && target[p] !== value) { 41 | (this.$instance as any).$pristine = false 42 | } 43 | 44 | target[p] = value 45 | return true 46 | } 47 | 48 | get(target: any, p: PropertyKey, receiver: any): any { 49 | if (~this.$internal.indexOf(p)) { 50 | return this.$instance[p] 51 | } 52 | 53 | return target[p] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/typings.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | import { RxRest, RxRestCollection, RxRestConfiguration, RxRestItem } from '../src/index' 4 | 5 | const configuration = new RxRestConfiguration() 6 | const rxrest = new RxRest(configuration) 7 | 8 | interface Foo { 9 | id: number 10 | } 11 | 12 | interface Bar extends RxRestItem { 13 | id: number 14 | foo: string 15 | } 16 | 17 | const foo: Foo = {id: 1} 18 | 19 | // Retrieve a collection, when the second argument is "true", the stream is Stream 20 | rxrest.all('foos', true).get() 21 | // Adding the RxRestCollection type so that `e.$metadata` is a known property 22 | .observe((e: Foo[] & RxRestCollection) => { 23 | console.log(e.filter(e => {})) 24 | console.log(e.$metadata) 25 | }) 26 | .then((e: Foo[]) => { 27 | }) 28 | 29 | // Retrieve a collection, the stream is Stream 30 | rxrest.all('foos').get() 31 | .observe((e: Foo) => { 32 | console.log(e) 33 | }) 34 | // Still, the promise returns a collection 35 | .then((e: Foo[]) => { 36 | }) 37 | 38 | rxrest.one('foos', 1).get() 39 | .observe((e: Foo) => { 40 | console.log(e) 41 | }) 42 | .then((e: Foo) => { 43 | }) 44 | 45 | // When using fromObject you have to say if you're expecting a collection or an item 46 | let t = (rxrest.fromObject('foos', foo) as RxRestItem) 47 | .get() 48 | .observe((e: Foo) => {}) 49 | .then((e: Foo) => {}) 50 | 51 | let e = (rxrest.fromObject('foos', [foo]) as RxRestCollection) 52 | .get() 53 | .observe((e: Foo) => {}) 54 | .then((e: Foo) => {}) 55 | 56 | 57 | let f = rxrest.one('foos', 1) 58 | // f is Foo 59 | console.log(f.id) 60 | // f is RxRestItem 61 | console.log(f.$fromServer) 62 | 63 | // clone is Foo & RxRestItem 64 | let fclone = f.clone() 65 | console.log(fclone.$fromServer) 66 | console.log(fclone.id) 67 | 68 | // Bar extends rxrestitem 69 | let x: Bar = {id: 1, foo: ''} as Bar 70 | let z = rxrest.fromObject('bars', x) as Bar 71 | console.log(z.clone().plain()) 72 | 73 | -------------------------------------------------------------------------------- /src/RxRestConfiguration.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RequestInterceptor, 3 | ResponseInterceptor, 4 | ErrorInterceptor 5 | } from './interfaces' 6 | import { RxRestItem } from './index' 7 | import { BodyParam } from './interfaces' 8 | import { objectToMap } from './utils' 9 | import { fetch } from './fetch'; 10 | 11 | export interface RequestBodyHandler { 12 | (body: BodyParam): FormData|URLSearchParams|Body|Blob|undefined|string|Promise 13 | } 14 | 15 | export interface ResponseBodyHandler { 16 | (body: Response): Promise 17 | } 18 | 19 | /** 20 | * RxRestConfiguration 21 | */ 22 | export class RxRestConfiguration { 23 | private $baseURL: string 24 | private $headers: Headers = new Headers() 25 | private $queryParams: URLSearchParams = new URLSearchParams() 26 | public identifier: string = 'id' 27 | public requestInterceptors: RequestInterceptor[] = [] 28 | public responseInterceptors: ResponseInterceptor[] = [] 29 | public errorInterceptors: ErrorInterceptor[] = [] 30 | public fetch: any 31 | public abortCallback: (req: Request) => void = () => null 32 | public uuid: boolean = false 33 | 34 | constructor() { 35 | this.fetch = fetch 36 | } 37 | 38 | /** 39 | * requestBodyHandler 40 | * JSONify the body if it's an `RxRestItem` or an `Object` 41 | * 42 | * @param {FormData|URLSearchParams|Body|Blob|undefined} body 43 | * @returns {any} 44 | */ 45 | _requestBodyHandler(body: FormData|URLSearchParams|Body|Blob|undefined): 46 | FormData|URLSearchParams|Body|Blob|undefined|string|Promise { 47 | if (!body) { 48 | return undefined 49 | } 50 | 51 | if (body instanceof FormData || body instanceof URLSearchParams) { 52 | return body 53 | } 54 | 55 | return body instanceof RxRestItem ? body.json() : JSON.stringify(body) 56 | } 57 | 58 | /** 59 | * responseBodyHandler 60 | * transforms the response to a json object 61 | * 62 | * @param {Response} body 63 | * @reject when response is an error 64 | * @returns {Promise} 65 | */ 66 | _responseBodyHandler(body: Response): Promise<{body: any, metadata: any}> { 67 | return body.text() 68 | .then(text => { 69 | return {body: text ? JSON.parse(text) : null, metadata: null} 70 | }) 71 | } 72 | 73 | get responseBodyHandler(): ResponseBodyHandler { 74 | return this._responseBodyHandler 75 | } 76 | 77 | set responseBodyHandler(responseBodyHandler: ResponseBodyHandler) { 78 | this._responseBodyHandler = responseBodyHandler 79 | } 80 | 81 | get requestBodyHandler(): RequestBodyHandler { 82 | return this._requestBodyHandler 83 | } 84 | 85 | set requestBodyHandler(requestBodyHandler: RequestBodyHandler) { 86 | this._requestBodyHandler = requestBodyHandler 87 | } 88 | 89 | /** 90 | * set baseURL 91 | * 92 | * @param {String} base 93 | * @returns 94 | */ 95 | set baseURL(base: string) { 96 | if (base.charAt(base.length - 1) !== '/') { 97 | base += '/' 98 | } 99 | 100 | this.$baseURL = base 101 | } 102 | 103 | /** 104 | * get baseURL 105 | * 106 | * @returns {string} 107 | */ 108 | get baseURL(): string { 109 | return this.$baseURL 110 | } 111 | 112 | /** 113 | * Set global query params 114 | * @param {Object|URLSearchParams} params 115 | */ 116 | set queryParams(params: any) { 117 | if (params instanceof URLSearchParams) { 118 | this.$queryParams = params 119 | return 120 | } 121 | 122 | if (typeof params === 'string') { 123 | this.$queryParams = new URLSearchParams(params) 124 | return 125 | } 126 | 127 | this.$queryParams = objectToMap(new URLSearchParams(), params) 128 | } 129 | 130 | /** 131 | * Get global query params 132 | * @return {URLSearchParams} 133 | */ 134 | get queryParams(): any { 135 | return this.$queryParams 136 | } 137 | 138 | /** 139 | * set global headers 140 | * @param {Object|Headers} params 141 | */ 142 | set headers(params: any) { 143 | if (params instanceof Headers) { 144 | this.$headers = params 145 | return 146 | } 147 | 148 | this.$headers = objectToMap(new Headers(), params) 149 | } 150 | 151 | /** 152 | * Get global headers 153 | * @return Headers 154 | */ 155 | get headers(): any { 156 | return this.$headers 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /typings/rxrest.d.ts: -------------------------------------------------------------------------------- 1 | export as namespace RxRest 2 | export = RxRest 3 | 4 | import { Observable } from 'rxjs' 5 | 6 | declare namespace RxRest { 7 | function fetch(input: string | Request, 8 | init?: RequestOptions, 9 | abortCallback?: (req: Request) => void 10 | ): Observable; 11 | 12 | class RxRest { 13 | constructor (config: RxRestConfiguration); 14 | one(route: string, id?: any): RxRestItem & T; 15 | all(route: string, asIterable?: boolean): RxRestCollection & T[]; 16 | fromObject(route: string, element: T|T[]): (RxRestItem & T)|(RxRestCollection & T[]); 17 | } 18 | 19 | class AbstractRxRest { 20 | constructor (route?: string[], config?: RxRestConfiguration); 21 | private config; 22 | json(): string; 23 | one(route: string, id?: any): RxRestItem; 24 | all(route: string, asIterable?: boolean): RxRestCollection; 25 | asIterable(): this; 26 | fromObject(route: string, element: T|T[]): RxRestItem|RxRestCollection; 27 | post( 28 | body?: BodyParam, 29 | queryParams?: Object|URLSearchParams, 30 | headers?: Object|Headers): Observable; 31 | put( 32 | body?: BodyParam, 33 | queryParams?: Object|URLSearchParams, 34 | headers?: Object|Headers): Observable; 35 | patch( 36 | body?: BodyParam, 37 | queryParams?: Object|URLSearchParams, 38 | headers?: Object|Headers): Observable; 39 | remove(queryParams?: Object|URLSearchParams, headers?: Object|Headers): Observable; 40 | get(queryParams?: Object|URLSearchParams, headers?: Object|Headers): Observable; 41 | head(queryParams?: Object|URLSearchParams, headers?: Object|Headers): Observable; 42 | trace(queryParams?: Object|URLSearchParams, headers?: Object|Headers): Observable; 43 | options(queryParams?: Object|URLSearchParams, headers?: Object|Headers): Observable; 44 | URL: string; 45 | baseURL: string; 46 | identifier: string; 47 | setQueryParams(params: any): AbstractRxRest; 48 | localQueryParams: any; 49 | setHeaders(params: any): AbstractRxRest; 50 | localHeaders: any; 51 | headers: any; 52 | queryParams: any; 53 | readonly requestQueryParams: any; 54 | readonly requestHeaders: any; 55 | requestInterceptors: RequestInterceptor[]; 56 | responseInterceptors: ResponseInterceptor[]; 57 | errorInterceptors: ErrorInterceptor[]; 58 | fetch: any 59 | requestBodyHandler: RequestBodyHandler; 60 | responseBodyHandler: ResponseBodyHandler; 61 | request(method: string, body?: BodyParam): Observable; 62 | $route: string[]; 63 | $fromServer: boolean; 64 | $pristine: boolean; 65 | $uuid: string; 66 | $queryParams: URLSearchParams; 67 | $headers: Headers; 68 | $metadata: any; 69 | $asIterable: boolean; 70 | abortCallback: (req: Request) => void; 71 | } 72 | 73 | class RxRestItem extends AbstractRxRest & T, T> { 74 | $element: T; 75 | save( 76 | queryParams?: Object|URLSearchParams, 77 | headers?: Object|Headers): Observable|RxRestCollection>; 78 | clone(): RxRestItem & T; 79 | plain(): T; 80 | } 81 | 82 | class RxRestCollection 83 | extends AbstractRxRest & T[] & T, T> 84 | implements Iterable> { 85 | length: number; 86 | [Symbol.iterator]: () => Iterator>; 87 | $elements: RxRestItem[]; 88 | getList( 89 | queryParams?: Object|URLSearchParams, 90 | headers?: Object|Headers): Observable|RxRestCollection>; 91 | clone(): RxRestCollection & T[]; 92 | plain(): T[]; 93 | } 94 | 95 | interface RequestInterceptor { 96 | (request: Request): Observable|Promise|undefined|Request|void; 97 | } 98 | 99 | interface ResponseInterceptor { 100 | (body: Body): Observable|Promise|undefined|Body|void; 101 | } 102 | 103 | interface ErrorInterceptor { 104 | (response: Response): Observable|void|Response|Promise; 105 | } 106 | 107 | type BodyParam = RxRestItem|FormData|URLSearchParams|Body|Blob|undefined|Object; 108 | 109 | interface RequestBodyHandler { 110 | (body: BodyParam): FormData|URLSearchParams|Body|Blob|undefined|string|Promise 111 | } 112 | 113 | interface ResponseBodyHandler { 114 | (body: Response): Promise 115 | } 116 | 117 | interface ErrorResponse extends Response { 118 | name: string; 119 | message: string; 120 | } 121 | 122 | interface RequestOptions { 123 | method: string; 124 | headers?: Headers; 125 | body?: Body|Blob|FormData|URLSearchParams|Object; 126 | mode?: string; 127 | credentials?: string; 128 | cache?: string; 129 | redirect?: string; 130 | referrer?: string; 131 | integrity?: string; 132 | } 133 | 134 | class RxRestConfiguration { 135 | constructor(); 136 | baseURL: string; 137 | identifier: string; 138 | uuid: boolean; 139 | requestInterceptors: RequestInterceptor[]; 140 | responseInterceptors: ResponseInterceptor[]; 141 | errorInterceptors: ErrorInterceptor[]; 142 | headers: Headers; 143 | queryParams: URLSearchParams; 144 | fetch: any; 145 | abortCallback: (req: Request) => void; 146 | requestBodyHandler(body: FormData | URLSearchParams | Body | Blob | undefined): 147 | FormData | URLSearchParams | Body | Blob | undefined | string | Promise; 148 | responseBodyHandler(body: Response): Promise; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { RxRestProxyHandler } from './RxRestProxyHandler' 3 | import { RxRest as AbstractRxRest } from './RxRest' 4 | import { RxRestConfiguration } from './RxRestConfiguration'; 5 | 6 | export class RxRestItem extends AbstractRxRest & T, T> { 7 | $element: T = {} as T; 8 | 9 | /** 10 | * constructor 11 | * 12 | * @param {string[]} route 13 | * @param {T} [element] 14 | * @return {Proxy} 15 | */ 16 | constructor(route: string[], element?: T, config?: RxRestConfiguration, 17 | metadata?: any, suffix?: string[]) { 18 | super(config, route, metadata) 19 | 20 | if (element !== undefined) { 21 | this.element = element 22 | } 23 | 24 | if (Array.isArray(suffix)) { 25 | suffix = [].concat.apply([], suffix) 26 | if (suffix.length) { 27 | this.addRoute(suffix.join('/')) 28 | } 29 | } 30 | 31 | const proxy = new Proxy(this.$element, new RxRestProxyHandler, T>(this)) 32 | 33 | return & T> proxy 34 | } 35 | 36 | /** 37 | * save - POST or PUT according to $fromServer value 38 | * 39 | * @param {Object|URLSearchParams} [queryParams] 40 | * @param {Object|Headers} [headers] 41 | * @returns {Observable} 42 | */ 43 | save(queryParams?: Object|URLSearchParams, headers?: Object|Headers): 44 | Observable|RxRestCollection> { 45 | this.queryParams = queryParams 46 | this.headers = headers 47 | 48 | return this.request(this.$fromServer === true ? 'PUT' : 'POST', this) 49 | } 50 | 51 | /** 52 | * set element 53 | * 54 | * @param {T} element 55 | */ 56 | set element(element: T) { 57 | for (let i in element) { 58 | if (i === this.config.identifier && !this.$element[this.config.identifier]) { 59 | this.$route.push('' + element[i]) 60 | } 61 | 62 | this.$element[i] = element[i] 63 | } 64 | } 65 | 66 | /** 67 | * get element 68 | * 69 | * @return {T} 70 | */ 71 | get element(): T { 72 | return this.$element 73 | } 74 | 75 | /** 76 | * get plain object 77 | * 78 | * @return {T} 79 | */ 80 | plain(): T { 81 | return this.element 82 | } 83 | 84 | /** 85 | * Get json string 86 | * @return {string} 87 | */ 88 | json(): string { 89 | return JSON.stringify(this.plain()) 90 | } 91 | 92 | /** 93 | * Clone 94 | * @return {RxRestItem} 95 | */ 96 | clone(): RxRestItem & T { 97 | let route = this.$route 98 | 99 | if (this.$element[this.config.identifier]) { 100 | route = route.slice(0, this.$route.length - 1) 101 | } 102 | 103 | let clone = new RxRestItem(route, this.$element, this.config) 104 | clone.$fromServer = this.$fromServer 105 | return clone as RxRestItem & T 106 | } 107 | } 108 | 109 | export class RxRestCollection extends AbstractRxRest & T[] & T, T> 110 | implements Iterable> { 111 | length: number; 112 | $elements: RxRestItem[] = []; 113 | [index: number]: RxRestItem; 114 | 115 | /** 116 | * constructor 117 | * 118 | * @param {string[]} route 119 | * @param {T[]|RxRestItem[]]} [elements] 120 | * @return {Proxy} 121 | */ 122 | constructor( 123 | route: string[], 124 | elements?: T[]|RxRestItem[], 125 | config?: RxRestConfiguration, 126 | metadata?: any, 127 | asIterable: boolean = true 128 | ) { 129 | super(config, route, metadata) 130 | 131 | if (elements !== undefined) { 132 | this.elements = (elements as any).map((e: any) => 133 | e instanceof RxRestItem ? e.clone() : new RxRestItem(this.$route, e) 134 | ) 135 | } 136 | 137 | this.$asIterable = asIterable 138 | 139 | const proxy = new Proxy(this.$elements, new RxRestProxyHandler, T>(this)) 140 | 141 | return & T[]> proxy 142 | } 143 | 144 | [Symbol.iterator]() { 145 | let index = 0 146 | let elements = this.$elements 147 | 148 | return { 149 | next(): IteratorResult> { 150 | return index < elements.length ? 151 | {value: elements[index++], done: false} : {value: undefined, done: true} 152 | } 153 | } 154 | } 155 | 156 | /** 157 | * getList - fetch a collection 158 | * 159 | * @param {Object|URLSearchParams} [queryParams] 160 | * @param {Object|Headers} [headers] 161 | * @returns {Observable} 162 | */ 163 | getList(queryParams?: Object|URLSearchParams, headers?: Object|Headers): 164 | Observable|RxRestCollection> { 165 | this.queryParams = queryParams 166 | this.headers = headers 167 | 168 | return this.request('GET') 169 | } 170 | 171 | /** 172 | * set elements 173 | * 174 | * @param {T[]} elements 175 | */ 176 | set elements(elements: RxRestItem[]) { 177 | this.$elements = elements 178 | this.length = elements.length 179 | } 180 | 181 | /** 182 | * get elements 183 | * @return {RxRestItem[]} 184 | */ 185 | get elements(): RxRestItem[] { 186 | return this.$elements 187 | } 188 | 189 | /** 190 | * plain 191 | * 192 | * @returns {T[]} 193 | */ 194 | plain(): T[] { 195 | return this.elements.map(e => e.plain()) 196 | } 197 | 198 | /** 199 | * json 200 | * 201 | * @returns {String} 202 | */ 203 | json(): string { 204 | return JSON.stringify(this.plain()) 205 | } 206 | 207 | /** 208 | * clone 209 | * 210 | * @returns {RxRestCollection} 211 | */ 212 | clone(): RxRestCollection & T[] { 213 | return new RxRestCollection( 214 | this.$route, this.$elements, this.config 215 | ) as RxRestCollection & T[] 216 | } 217 | } 218 | 219 | export class RxRest { 220 | constructor(private config: RxRestConfiguration) { 221 | } 222 | 223 | one(route: string, id?: any, ...suffix: string[]): RxRestItem & T { 224 | let r = new AbstractRxRest(this.config) 225 | return r.one.call(r, route, id, suffix) 226 | } 227 | 228 | all(route: string, asIterable: boolean = true): RxRestCollection & T[] { 229 | let r = new AbstractRxRest(this.config) 230 | return r.all.call(r, route, asIterable) 231 | } 232 | 233 | fromObject(route: string, element: T|T[], ...suffix: string[]): 234 | (RxRestItem & T) | (RxRestCollection & T[]) { 235 | let r = new AbstractRxRest(this.config) 236 | return r.fromObject.call(r, route, element, suffix) 237 | } 238 | } 239 | 240 | export { RxRestConfiguration } 241 | -------------------------------------------------------------------------------- /test/urlsearchparamspolyfill.js: -------------------------------------------------------------------------------- 1 | // Browsers may have: 2 | // * No global URL object 3 | // * URL with static methods only - may have a dummy constructor 4 | // * URL with members except searchParams 5 | // * Full URL API support 6 | var origURL = global.URL; 7 | var nativeURL; 8 | try { 9 | if (origURL) { 10 | nativeURL = new global.URL('http://example.com'); 11 | if ('searchParams' in nativeURL) 12 | return; 13 | if (!('href' in nativeURL)) 14 | nativeURL = undefined; 15 | } 16 | } catch (_) {} 17 | 18 | // NOTE: Doesn't do the encoding/decoding dance 19 | function urlencoded_serialize(pairs) { 20 | var output = '', first = true; 21 | pairs.forEach(function (pair) { 22 | var name = encodeURIComponent(pair.name); 23 | var value = encodeURIComponent(pair.value); 24 | if (!first) output += '&'; 25 | output += name + '=' + value; 26 | first = false; 27 | }); 28 | return output.replace(/%20/g, '+'); 29 | } 30 | 31 | // NOTE: Doesn't do the encoding/decoding dance 32 | function urlencoded_parse(input, isindex) { 33 | var sequences = input.split('&'); 34 | if (isindex && sequences[0].indexOf('=') === -1) 35 | sequences[0] = '=' + sequences[0]; 36 | var pairs = []; 37 | sequences.forEach(function (bytes) { 38 | if (bytes.length === 0) return; 39 | var index = bytes.indexOf('='); 40 | if (index !== -1) { 41 | var name = bytes.substring(0, index); 42 | var value = bytes.substring(index + 1); 43 | } else { 44 | name = bytes; 45 | value = ''; 46 | } 47 | name = name.replace(/\+/g, ' '); 48 | value = value.replace(/\+/g, ' '); 49 | pairs.push({ name: name, value: value }); 50 | }); 51 | var output = []; 52 | pairs.forEach(function (pair) { 53 | output.push({ 54 | name: decodeURIComponent(pair.name), 55 | value: decodeURIComponent(pair.value) 56 | }); 57 | }); 58 | return output; 59 | } 60 | 61 | 62 | function URLUtils(url) { 63 | if (nativeURL) 64 | return new origURL(url); 65 | var anchor = document.createElement('a'); 66 | anchor.href = url; 67 | return anchor; 68 | } 69 | 70 | function URLSearchParams(init) { 71 | var $this = this; 72 | this._list = []; 73 | 74 | if (init === undefined || init === null) 75 | init = ''; 76 | 77 | if (Object(init) !== init || (init instanceof URLSearchParams)) 78 | init = String(init); 79 | 80 | if (typeof init === 'string') { 81 | if (init.substring(0, 1) === '?') { 82 | init = init.substring(1); 83 | } 84 | this._list = urlencoded_parse(init); 85 | } 86 | 87 | this._url_object = null; 88 | this._setList = function (list) { if (!updating) $this._list = list; }; 89 | 90 | var updating = false; 91 | this._update_steps = function() { 92 | if (updating) return; 93 | updating = true; 94 | 95 | if (!$this._url_object) return; 96 | 97 | // Partial workaround for IE issue with 'about:' 98 | if ($this._url_object.protocol === 'about:' && 99 | $this._url_object.pathname.indexOf('?') !== -1) { 100 | $this._url_object.pathname = $this._url_object.pathname.split('?')[0]; 101 | } 102 | 103 | $this._url_object.search = urlencoded_serialize($this._list); 104 | 105 | updating = false; 106 | }; 107 | } 108 | 109 | 110 | Object.defineProperties(URLSearchParams.prototype, { 111 | append: { 112 | value: function (name, value) { 113 | this._list.push({ name: name, value: value }); 114 | this._update_steps(); 115 | }, writable: true, enumerable: true, configurable: true 116 | }, 117 | 118 | 'delete': { 119 | value: function (name) { 120 | for (var i = 0; i < this._list.length;) { 121 | if (this._list[i].name === name) 122 | this._list.splice(i, 1); 123 | else 124 | ++i; 125 | } 126 | this._update_steps(); 127 | }, writable: true, enumerable: true, configurable: true 128 | }, 129 | 130 | get: { 131 | value: function (name) { 132 | for (var i = 0; i < this._list.length; ++i) { 133 | if (this._list[i].name === name) 134 | return this._list[i].value; 135 | } 136 | return null; 137 | }, writable: true, enumerable: true, configurable: true 138 | }, 139 | 140 | getAll: { 141 | value: function (name) { 142 | var result = []; 143 | for (var i = 0; i < this._list.length; ++i) { 144 | if (this._list[i].name === name) 145 | result.push(this._list[i].value); 146 | } 147 | return result; 148 | }, writable: true, enumerable: true, configurable: true 149 | }, 150 | 151 | has: { 152 | value: function (name) { 153 | for (var i = 0; i < this._list.length; ++i) { 154 | if (this._list[i].name === name) 155 | return true; 156 | } 157 | return false; 158 | }, writable: true, enumerable: true, configurable: true 159 | }, 160 | 161 | set: { 162 | value: function (name, value) { 163 | var found = false; 164 | for (var i = 0; i < this._list.length;) { 165 | if (this._list[i].name === name) { 166 | if (!found) { 167 | this._list[i].value = value; 168 | found = true; 169 | ++i; 170 | } else { 171 | this._list.splice(i, 1); 172 | } 173 | } else { 174 | ++i; 175 | } 176 | } 177 | 178 | if (!found) 179 | this._list.push({ name: name, value: value }); 180 | 181 | this._update_steps(); 182 | }, writable: true, enumerable: true, configurable: true 183 | }, 184 | 185 | entries: { 186 | value: function() { 187 | var $this = this, index = 0; 188 | return { next: function() { 189 | if (index >= $this._list.length) 190 | return {done: true, value: undefined}; 191 | var pair = $this._list[index++]; 192 | return {done: false, value: [pair.name, pair.value]}; 193 | }}; 194 | }, writable: true, enumerable: true, configurable: true 195 | }, 196 | 197 | keys: { 198 | value: function() { 199 | var $this = this, index = 0; 200 | return { next: function() { 201 | if (index >= $this._list.length) 202 | return {done: true, value: undefined}; 203 | var pair = $this._list[index++]; 204 | return {done: false, value: pair.name}; 205 | }}; 206 | }, writable: true, enumerable: true, configurable: true 207 | }, 208 | 209 | values: { 210 | value: function() { 211 | var $this = this, index = 0; 212 | return { next: function() { 213 | if (index >= $this._list.length) 214 | return {done: true, value: undefined}; 215 | var pair = $this._list[index++]; 216 | return {done: false, value: pair.value}; 217 | }}; 218 | }, writable: true, enumerable: true, configurable: true 219 | }, 220 | 221 | forEach: { 222 | value: function(callback) { 223 | var thisArg = (arguments.length > 1) ? arguments[1] : undefined; 224 | this._list.forEach(function(pair, index) { 225 | callback.call(thisArg, pair.value, pair.name); 226 | }); 227 | 228 | }, writable: true, enumerable: true, configurable: true 229 | }, 230 | 231 | toString: { 232 | value: function () { 233 | return urlencoded_serialize(this._list); 234 | }, writable: true, enumerable: false, configurable: true 235 | } 236 | }); 237 | 238 | if ('Symbol' in global && 'iterator' in global.Symbol) { 239 | Object.defineProperty(URLSearchParams.prototype, global.Symbol.iterator, { 240 | value: URLSearchParams.prototype.entries, 241 | writable: true, enumerable: true, configurable: true}); 242 | } 243 | 244 | function URL(url, base) { 245 | if (!(this instanceof global.URL)) 246 | throw new TypeError("Failed to construct 'URL': Please use the 'new' operator."); 247 | 248 | if (base) { 249 | url = (function () { 250 | if (nativeURL) return new origURL(url, base).href; 251 | 252 | var doc; 253 | // Use another document/base tag/anchor for relative URL resolution, if possible 254 | if (document.implementation && document.implementation.createHTMLDocument) { 255 | doc = document.implementation.createHTMLDocument(''); 256 | } else if (document.implementation && document.implementation.createDocument) { 257 | doc = document.implementation.createDocument('http://www.w3.org/1999/xhtml', 'html', null); 258 | doc.documentElement.appendChild(doc.createElement('head')); 259 | doc.documentElement.appendChild(doc.createElement('body')); 260 | } else if (window.ActiveXObject) { 261 | doc = new window.ActiveXObject('htmlfile'); 262 | doc.write('<\/head><\/body>'); 263 | doc.close(); 264 | } 265 | 266 | if (!doc) throw Error('base not supported'); 267 | 268 | var baseTag = doc.createElement('base'); 269 | baseTag.href = base; 270 | doc.getElementsByTagName('head')[0].appendChild(baseTag); 271 | var anchor = doc.createElement('a'); 272 | anchor.href = url; 273 | return anchor.href; 274 | }()); 275 | } 276 | 277 | // An inner object implementing URLUtils (either a native URL 278 | // object or an HTMLAnchorElement instance) is used to perform the 279 | // URL algorithms. With full ES5 getter/setter support, return a 280 | // regular object For IE8's limited getter/setter support, a 281 | // different HTMLAnchorElement is returned with properties 282 | // overridden 283 | 284 | var instance = URLUtils(url || ''); 285 | 286 | // Detect for ES5 getter/setter support 287 | // (an Object.defineProperties polyfill that doesn't support getters/setters may throw) 288 | var ES5_GET_SET = (function() { 289 | if (!('defineProperties' in Object)) return false; 290 | try { 291 | var obj = {}; 292 | Object.defineProperties(obj, { prop: { 'get': function () { return true; } } }); 293 | return obj.prop; 294 | } catch (_) { 295 | return false; 296 | } 297 | })(); 298 | 299 | var self = ES5_GET_SET ? this : document.createElement('a'); 300 | 301 | 302 | 303 | var query_object = new URLSearchParams( 304 | instance.search ? instance.search.substring(1) : null); 305 | query_object._url_object = self; 306 | 307 | Object.defineProperties(self, { 308 | href: { 309 | get: function () { return instance.href; }, 310 | set: function (v) { instance.href = v; tidy_instance(); update_steps(); }, 311 | enumerable: true, configurable: true 312 | }, 313 | origin: { 314 | get: function () { 315 | if ('origin' in instance) return instance.origin; 316 | return this.protocol + '//' + this.host; 317 | }, 318 | enumerable: true, configurable: true 319 | }, 320 | protocol: { 321 | get: function () { return instance.protocol; }, 322 | set: function (v) { instance.protocol = v; }, 323 | enumerable: true, configurable: true 324 | }, 325 | username: { 326 | get: function () { return instance.username; }, 327 | set: function (v) { instance.username = v; }, 328 | enumerable: true, configurable: true 329 | }, 330 | password: { 331 | get: function () { return instance.password; }, 332 | set: function (v) { instance.password = v; }, 333 | enumerable: true, configurable: true 334 | }, 335 | host: { 336 | get: function () { 337 | // IE returns default port in |host| 338 | var re = {'http:': /:80$/, 'https:': /:443$/, 'ftp:': /:21$/}[instance.protocol]; 339 | return re ? instance.host.replace(re, '') : instance.host; 340 | }, 341 | set: function (v) { instance.host = v; }, 342 | enumerable: true, configurable: true 343 | }, 344 | hostname: { 345 | get: function () { return instance.hostname; }, 346 | set: function (v) { instance.hostname = v; }, 347 | enumerable: true, configurable: true 348 | }, 349 | port: { 350 | get: function () { return instance.port; }, 351 | set: function (v) { instance.port = v; }, 352 | enumerable: true, configurable: true 353 | }, 354 | pathname: { 355 | get: function () { 356 | // IE does not include leading '/' in |pathname| 357 | if (instance.pathname.charAt(0) !== '/') return '/' + instance.pathname; 358 | return instance.pathname; 359 | }, 360 | set: function (v) { instance.pathname = v; }, 361 | enumerable: true, configurable: true 362 | }, 363 | search: { 364 | get: function () { return instance.search; }, 365 | set: function (v) { 366 | if (instance.search === v) return; 367 | instance.search = v; tidy_instance(); update_steps(); 368 | }, 369 | enumerable: true, configurable: true 370 | }, 371 | searchParams: { 372 | get: function () { return query_object; }, 373 | enumerable: true, configurable: true 374 | }, 375 | hash: { 376 | get: function () { return instance.hash; }, 377 | set: function (v) { instance.hash = v; tidy_instance(); }, 378 | enumerable: true, configurable: true 379 | }, 380 | toString: { 381 | value: function() { return instance.toString(); }, 382 | enumerable: false, configurable: true 383 | }, 384 | valueOf: { 385 | value: function() { return instance.valueOf(); }, 386 | enumerable: false, configurable: true 387 | } 388 | }); 389 | 390 | function tidy_instance() { 391 | var href = instance.href.replace(/#$|\?$|\?(?=#)/g, ''); 392 | if (instance.href !== href) 393 | instance.href = href; 394 | } 395 | 396 | function update_steps() { 397 | query_object._setList(instance.search ? urlencoded_parse(instance.search.substring(1)) : []); 398 | query_object._update_steps(); 399 | }; 400 | 401 | return self; 402 | } 403 | 404 | if (origURL) { 405 | for (var i in origURL) { 406 | if (origURL.hasOwnProperty(i) && typeof origURL[i] === 'function') 407 | URL[i] = origURL[i]; 408 | } 409 | } 410 | 411 | global.URL = URL; 412 | global.URLSearchParams = URLSearchParams; 413 | -------------------------------------------------------------------------------- /src/RxRest.ts: -------------------------------------------------------------------------------- 1 | import { RxRestConfiguration } from './RxRestConfiguration' 2 | import { 3 | RequestInterceptor, 4 | ResponseInterceptor, 5 | ErrorInterceptor, 6 | ErrorResponse, 7 | BodyParam 8 | } from './interfaces' 9 | import { RxRestCollection, RxRestItem } from './index' 10 | import { Observable, Observer, from as fromPromise, throwError as _throw, of } from 'rxjs' 11 | import { mergeMap, catchError, concatMap } from 'rxjs/operators' 12 | import { objectToMap, uuid } from './utils' 13 | 14 | // const fromPromise = function(promise: Promise) { 15 | // return Observable.create((observer: Observer) => { 16 | // promise 17 | // .then((v) => { 18 | // observer.next(v) 19 | // observer.complete() 20 | // }) 21 | // .catch(observer.error) 22 | // }) 23 | // } 24 | 25 | export class RxRest { 26 | protected $route: string[] 27 | $fromServer: boolean = false 28 | $asIterable: boolean = true 29 | $queryParams: URLSearchParams = new URLSearchParams() 30 | $headers: Headers = new Headers() 31 | config: RxRestConfiguration 32 | $metadata: any 33 | $pristine: boolean = true 34 | $uuid?: string; 35 | 36 | /** 37 | * constructor 38 | * 39 | * @param {String} [route] the resource route 40 | */ 41 | constructor( 42 | config: RxRestConfiguration = new RxRestConfiguration(), 43 | route?: string[], 44 | metadata?: any 45 | ) { 46 | this.$route = route === undefined ? [] : [...route] 47 | this.config = config 48 | this.$metadata = metadata 49 | if (config.uuid) { 50 | this.$uuid = uuid() 51 | } 52 | } 53 | 54 | protected addRoute(route: string): void { 55 | this.$route.push.apply(this.$route, route.split('/')) 56 | } 57 | 58 | /** 59 | * one 60 | * 61 | * @param {String} route 62 | * @param {any} id 63 | * @returns {RxRestItem} 64 | */ 65 | one(route: string, id?: any, ...suffix: string[]): RxRestItem { 66 | this.addRoute(route) 67 | let o = {} as T 68 | if (id) { 69 | o[this.config.identifier] = id 70 | } 71 | 72 | return new RxRestItem(this.$route, o, this.config, null, suffix) 73 | } 74 | 75 | /** 76 | * all 77 | * 78 | * @param {String} route 79 | * @param {boolean} [asIterable=true] - forces the next request to return an Observable 80 | * instead of emitting multiple events 81 | * @returns {RxRestCollection} 82 | */ 83 | all(route: string, asIterable: boolean = true): RxRestCollection { 84 | this.addRoute(route) 85 | return new RxRestCollection(this.$route, undefined, this.config, null, asIterable) 86 | } 87 | 88 | /** 89 | * asIterable - sets the flag $asIterable 90 | * instead of emitting multiple events 91 | * 92 | * @returns {self} 93 | */ 94 | asIterable(value = true): this { 95 | this.$asIterable = value 96 | return this 97 | } 98 | 99 | /** 100 | * fromObject 101 | * 102 | * @param {String} route 103 | * @param {Object|Object[]} element 104 | * @returns {RxRestItem|RxRestCollection} 105 | */ 106 | fromObject(route: string, element: T|T[], suffix: string[]): 107 | RxRestItem|RxRestCollection { 108 | this.addRoute(route) 109 | if (Array.isArray(element)) { 110 | return new RxRestCollection(this.$route, element, this.config) 111 | } 112 | 113 | return new RxRestItem(this.$route, element, this.config, null, suffix) 114 | } 115 | 116 | /** 117 | * @access private 118 | * @param {BodyParam} body 119 | * @return {BodyParam|RxRestItem} 120 | */ 121 | protected withBody(body: BodyParam) { 122 | return body ? body : this 123 | } 124 | 125 | /** 126 | * post 127 | * 128 | * @param {Body|Blob|FormData|URLSearchParams|Object|RxRestItem} [body] 129 | * @param {Object|URLSearchParams} [queryParams] 130 | * @param {Object|Headers} [headers] 131 | * @returns {Observable} 132 | */ 133 | post(body?: BodyParam, queryParams?: Object|URLSearchParams, headers?: Object|Headers): 134 | Observable { 135 | this.queryParams = queryParams 136 | this.headers = headers 137 | 138 | return this.request('POST', this.withBody(body)) 139 | } 140 | 141 | /** 142 | * remove 143 | * 144 | * @param {Object|URLSearchParams} [queryParams] 145 | * @param {Object|Headers} [headers] 146 | * @returns {Observable} 147 | */ 148 | remove(queryParams?: Object|URLSearchParams, headers?: Object|Headers): 149 | Observable { 150 | this.queryParams = queryParams 151 | this.headers = headers 152 | 153 | return this.request('DELETE') 154 | } 155 | 156 | /** 157 | * get 158 | * 159 | * @param {Object|URLSearchParams} [queryParams] 160 | * @param {Object|Headers} [headers] 161 | * @returns {Observable} 162 | */ 163 | get(queryParams?: Object|URLSearchParams, headers?: Object|Headers): 164 | Observable { 165 | this.queryParams = queryParams 166 | this.headers = headers 167 | 168 | return this.request('GET') 169 | } 170 | 171 | /** 172 | * put 173 | * 174 | * @param {Body|Blob|FormData|URLSearchParams|Object|RxRestItem} [body] 175 | * @param {Object|URLSearchParams} [queryParams] 176 | * @param {Object|Headers} [headers] 177 | * @returns {Observable} 178 | */ 179 | put(body?: BodyParam, queryParams?: Object|URLSearchParams, headers?: Object|Headers): 180 | Observable { 181 | this.queryParams = queryParams 182 | this.headers = headers 183 | 184 | return this.request('PUT', this.withBody(body)) 185 | } 186 | 187 | /** 188 | * patch 189 | * 190 | * @param {Body|Blob|FormData|URLSearchParams|Object|RxRestItem} [body] 191 | * @param {Object|URLSearchParams} [queryParams] 192 | * @param {Object|Headers} [headers] 193 | * @returns {Observable} 194 | */ 195 | patch(body?: BodyParam, queryParams?: Object|URLSearchParams, headers?: Object|Headers): 196 | Observable { 197 | this.queryParams = queryParams 198 | this.headers = headers 199 | 200 | return this.request('PATCH', this.withBody(body)) 201 | } 202 | 203 | /** 204 | * head 205 | * 206 | * @param {Object|URLSearchParams} [queryParams] 207 | * @param {Object|Headers} [headers] 208 | * @returns {Observable} 209 | */ 210 | head(queryParams?: Object|URLSearchParams, headers?: Object|Headers): 211 | Observable { 212 | this.queryParams = queryParams 213 | this.headers = headers 214 | 215 | return this.request('HEAD') 216 | } 217 | 218 | /** 219 | * trace 220 | * 221 | * @param {Object|URLSearchParams} [queryParams] 222 | * @param {Object|Headers} [headers] 223 | * @returns {Observable} 224 | */ 225 | trace(queryParams?: Object|URLSearchParams, headers?: Object|Headers): 226 | Observable { 227 | this.queryParams = queryParams 228 | this.headers = headers 229 | 230 | return this.request('TRACE') 231 | } 232 | 233 | /** 234 | * options 235 | * 236 | * @param {Object|URLSearchParams} [queryParams] 237 | * @param {Object|Headers} [headers] 238 | * @returns {Observable} 239 | */ 240 | options(queryParams?: Object|URLSearchParams, headers?: Object|Headers): 241 | Observable { 242 | this.queryParams = queryParams 243 | this.headers = headers 244 | 245 | return this.request('OPTIONS') 246 | } 247 | 248 | /** 249 | * URL 250 | * 251 | * @returns {string} 252 | */ 253 | get URL(): string { 254 | return `${this.config.baseURL}${this.$route.join('/')}` 255 | } 256 | 257 | /** 258 | * set local query params 259 | * @param {Object|URLSearchParams} params 260 | */ 261 | set queryParams(params: any) { 262 | if (!params) { 263 | return 264 | } 265 | 266 | if (params instanceof URLSearchParams) { 267 | this.$queryParams = params 268 | return 269 | } 270 | 271 | this.$queryParams = objectToMap(new URLSearchParams(), params) 272 | } 273 | 274 | /** 275 | * Sets local query params useful to add params to a custom request 276 | * @param {Object|URLSearchParams} 277 | * @return this 278 | */ 279 | setQueryParams(params: any): this { 280 | this.queryParams = params 281 | return this 282 | } 283 | 284 | /** 285 | * Sets local headers useful to add headers to a custom request 286 | * @param {Object|URLSearchParams} 287 | * @return this 288 | */ 289 | setHeaders(params: any): this { 290 | this.headers = params 291 | return this 292 | } 293 | 294 | /** 295 | * Get local query params 296 | * @return URLSearchParams 297 | */ 298 | get queryParams(): any { 299 | return this.$queryParams 300 | } 301 | 302 | /** 303 | * Get request query params 304 | * Combine local and global query params 305 | * Local query params are overriding global params 306 | * @return {String} 307 | */ 308 | get requestQueryParams(): string { 309 | let params = new URLSearchParams() 310 | 311 | for (let param of this.config.queryParams) { 312 | params.append(param[0], param[1]) 313 | } 314 | 315 | for (let param of this.queryParams) { 316 | params.append(param[0], param[1]) 317 | } 318 | 319 | let str = params.toString() 320 | 321 | if (str.length) { 322 | return '?' + str 323 | } 324 | 325 | return '' 326 | } 327 | 328 | /** 329 | * Set local headers 330 | * @param {Object|Headers} params 331 | */ 332 | set headers(params: any) { 333 | if (!params) { 334 | return 335 | } 336 | 337 | if (params instanceof Headers) { 338 | this.$headers = params 339 | return 340 | } 341 | 342 | this.$headers = objectToMap(new Headers(), params) 343 | } 344 | 345 | /** 346 | * Get local headers 347 | * @return Headers 348 | */ 349 | get headers(): any { 350 | return this.$headers 351 | } 352 | 353 | /** 354 | * get request Headers 355 | * Combine local and global headers 356 | * Local headers are overriding global headers 357 | * 358 | * @returns {Headers} 359 | */ 360 | get requestHeaders(): Headers { 361 | let headers = new Headers() 362 | 363 | for (let header of this.headers) { 364 | headers.append(header[0], header[1]) 365 | } 366 | 367 | for (let header of this.config.headers) { 368 | headers.append(header[0], header[1]) 369 | } 370 | 371 | return headers 372 | } 373 | 374 | /** 375 | * expandInterceptors 376 | * 377 | * @param {RequestInterceptor[]|ResponseInterceptor[]|ErrorInterceptor[]} interceptors 378 | * @returns {Observable} fn 379 | */ 380 | private expandInterceptors( 381 | interceptors: RequestInterceptor[]|ResponseInterceptor[]|ErrorInterceptor[] 382 | ) { 383 | return function(origin: any): Observable { 384 | return (interceptors).reduce( 385 | (obs: Observable, interceptor: any) => 386 | obs.pipe(concatMap(value => { 387 | let result = interceptor(value) 388 | if (result === undefined) { 389 | return of(value) 390 | } 391 | 392 | if (result instanceof Promise) { 393 | return fromPromise(result) 394 | } 395 | 396 | if (result instanceof Observable) { 397 | return result 398 | } 399 | 400 | return of(result) 401 | })), 402 | of(origin) 403 | ) 404 | } 405 | } 406 | 407 | /** 408 | * request 409 | * 410 | * @param {string} method 411 | * @param {RxRestItem|FormData|URLSearchParams|Body|Blob|undefined|Object} [body] 412 | * @returns {Observable} 413 | */ 414 | request(method: string, body?: BodyParam): Observable { 415 | let requestOptions = { 416 | method: method, 417 | headers: this.requestHeaders, 418 | body: this.config.requestBodyHandler(body) as any 419 | } 420 | 421 | let request = new Request(this.URL + this.requestQueryParams, requestOptions) 422 | let stream = > of(request) 423 | .pipe( 424 | mergeMap(this.expandInterceptors(this.config.requestInterceptors)), 425 | mergeMap(request => this.config.fetch(request, null, this.config.abortCallback)), 426 | mergeMap(this.expandInterceptors(this.config.responseInterceptors)), 427 | mergeMap(body => fromPromise(this.config.responseBodyHandler(body))), 428 | mergeMap(({body, metadata}) => { 429 | if (!Array.isArray(body)) { 430 | let item: RxRestItem 431 | if (this instanceof RxRestItem) { 432 | item = this 433 | item.element = body as T 434 | item.$metadata = metadata 435 | } else { 436 | item = new RxRestItem(this.$route, body, this.config, metadata) 437 | } 438 | 439 | item.$fromServer = true 440 | item.$pristine = true 441 | 442 | return Observable.create((observer: Observer>) => { 443 | observer.next(item) 444 | observer.complete() 445 | }) 446 | } 447 | 448 | let collection = new RxRestCollection(this.$route, body.map((e: T) => { 449 | let item = new RxRestItem(this.$route, e, this.config, metadata) 450 | item.$fromServer = true 451 | item.$pristine = true 452 | return item 453 | }), this.config, metadata) 454 | 455 | collection.$pristine = true 456 | 457 | return Observable.create((observer: Observer|RxRestCollection>) => { 458 | if (this.$asIterable) { 459 | observer.next(collection) 460 | } else { 461 | for (let item of collection) { 462 | observer.next(item) 463 | } 464 | } 465 | 466 | observer.complete() 467 | }) 468 | }), 469 | catchError(body => { 470 | return of(body).pipe( 471 | mergeMap(this.expandInterceptors(this.config.errorInterceptors)), 472 | mergeMap((body: ErrorResponse) => _throw(body)) 473 | ) 474 | }) 475 | ) 476 | 477 | return stream 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RxRest [![Build Status](https://travis-ci.org/soyuka/rxrest.svg?branch=master)](https://travis-ci.org/soyuka/rxrest) 2 | ====== 3 | 4 | > A reactive REST utility 5 | 6 | Highly inspirated by [Restangular](https://github.com/mgonto/restangular), this library implements a natural way to interact with a REST API. 7 | 8 | ## Install 9 | 10 | ``` 11 | npm install rxrest --save 12 | ``` 13 | 14 | ## Example 15 | 16 | ```javascript 17 | import { RxRest, RxRestConfig } from 'rxrest' 18 | 19 | const config = new RxRestConfig() 20 | config.baseURL = 'http://localhost/api' 21 | 22 | const rxrest = new RxRest(config) 23 | rxrest.all('cars') 24 | .get() 25 | .subscribe((cars: Car[]) => { 26 | /** 27 | * `cars` is: 28 | * RxRestCollection [ 29 | * RxRestItem { name: 'Polo', id: 1, brand: 'Audi' }, 30 | * RxRestItem { name: 'Golf', id: 2, brand: 'Volkswagen' } 31 | * ] 32 | */ 33 | 34 | cars[0].brand = 'Volkswagen' 35 | 36 | cars[0].save() 37 | .subscribe(result => { 38 | console.log(result) 39 | /** 40 | * outputs: RxRestItem { name: 'Polo', id: 1, brand: 'Volkswagen' } 41 | */ 42 | }) 43 | }) 44 | ``` 45 | 46 | ## Menu 47 | 48 | - [Technical concepts](#technical-concepts) 49 | - [Promise compatibility](#promise-compatibility) 50 | - [One-event Stream instead of multiple events](#one-event-stream-instead-of-multiple-events) 51 | - [Object state (`$fromServer`, `$pristine`, `$uuid`)](#object-state-fromserver-pristine-uuid) 52 | - [Configuration](#configuration) 53 | - [Interceptors](#interceptors) 54 | - [Handlers](#handlers) 55 | - [API](#api) 56 | - [Typings](#typings) 57 | - [Angular 2 configuration example](#angular-2-configuration-example) 58 | 59 | ## Technical concepts 60 | 61 | 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`. 62 | 63 | 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). 64 | It is also familiar with `Body`-like object, as `FormData`, `Response`, `Request` etc. 65 | 66 | This script depends on `superagent` (for a easier XMLHttpRequest usage, compatible in both node and the browser) and `rxjs` for the reactive part. 67 | 68 | [^ Back to menu](#menu) 69 | 70 | ## Promise compatibility 71 | 72 | Just use the `toPromise` utility: 73 | 74 | ```javascript 75 | 76 | rxrest.one('foo') 77 | .get() 78 | .toPromise() 79 | .then(item => { 80 | console.log(item) 81 | }) 82 | ``` 83 | 84 | [^ Back to menu](#menu) 85 | ## One-event Stream instead of multiple events 86 | 87 | Sometimes, you may want RxRest to emit one event per item in the collection: 88 | 89 | To do so, just call `asIterable(false)`: 90 | 91 | ```javascript 92 | rxrest.all('cars') 93 | .asIterable(false) 94 | .get() 95 | // next() is called with every car available 96 | .subscribe((e) => {}) 97 | ``` 98 | 99 | Or use the second argument of `.all` instead of `asIterable`: 100 | 101 | ```javascript 102 | rxrest.all('cars', false) 103 | .get() 104 | // next() is called with every car available 105 | .subscribe((e) => {}) 106 | ``` 107 | 108 | ## Object state (`$fromServer`, `$pristine`, `$uuid`) 109 | 110 | Thanks to the Proxy, we can get metadata informations about the current object and it's state. 111 | 112 | When you instantiate an object, it's `$pristine`. When it gets modified it's dirty: 113 | 114 | ```javascript 115 | const rxrest = new RxRest() 116 | const car = rxrest.one('cars', 1) 117 | 118 | assert(car.$prisine === true) 119 | 120 | car.brand = 'Ford' 121 | 122 | assert(car.$prisine === false) 123 | ``` 124 | 125 | You can also check that the item comes from the server: 126 | 127 | ```javascript 128 | const rxrest = new RxRest() 129 | const car = rxrest.one('cars', 1) 130 | 131 | assert(car.$fromServer === false) // we just instantiated it in the client 132 | 133 | car.save() 134 | .subscribe((car) => { 135 | assert(car.$fromServer === true) //now it's from the server 136 | assert(car.$prisine === true) //it's also pristine! 137 | }) 138 | ``` 139 | 140 | [^ Back to menu](#menu) 141 | ## Configuration 142 | 143 | Setting up `RxRest` is done via `RxRestConfiguration`: 144 | 145 | ```javascript 146 | const config = new RxRestConfiguration() 147 | ``` 148 | 149 | #### `baseURL` 150 | 151 | It is the base url prepending your routes. For example : 152 | 153 | ```javascript 154 | //set the url 155 | config.baseURL = 'http://localhost/api' 156 | 157 | const rxrest = new RxRest(config) 158 | //this will request GET http://localhost/api/cars/1 159 | rxrest.one('cars', 1) 160 | .get() 161 | ``` 162 | 163 | #### `identifier='id'` 164 | 165 | This is the key storing your identifier in your api objects. It defaults to `id`. 166 | 167 | ```javascript 168 | config.identifier = '@id' 169 | 170 | const rxrest = new RxRest(config) 171 | rxrest.one('cars', 1) 172 | 173 | > RxRestItem { '@id': 1 } 174 | ``` 175 | 176 | #### `headers` 177 | 178 | You can set headers through the configuration, but also change them request-wise: 179 | 180 | ```javascript 181 | config.headers 182 | config.headers.set('Authorization', 'foobar') 183 | config.headers.set('Content-Type', 'application/json') 184 | 185 | const rxrest = new RxRest(config) 186 | 187 | // Performs a GET request on /cars/1 with Authorization and an `application/json` content type header 188 | rxrest.one('cars', 1).get() 189 | 190 | // Performs a POST request on /cars with Authorization and an `application/x-www-form-urlencoded` content type header 191 | rxrest.all('cars') 192 | .post(new FormData(), null, {'Content-Type': 'application/x-www-form-urlencoded'}) 193 | ``` 194 | 195 | #### `queryParams` 196 | 197 | You can set query parameters through the configuration, but also change them request-wise: 198 | 199 | ```javascript 200 | config.queryParams.set('bearer', 'foobar') 201 | 202 | const rxrest = new RxRest(config) 203 | 204 | // Performs a GET request on /cars/1?bearer=foobar 205 | rxrest.one('cars', 1).get() 206 | 207 | // Performs a GET request on /cars?bearer=barfoo 208 | rxrest.all('cars') 209 | .get({bearer: 'barfoo'}) 210 | ``` 211 | 212 | #### `uuid` 213 | 214 | 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): 215 | 216 | ```javascript 217 | //set the url 218 | config.uuid = true 219 | 220 | const rxrest = new RxRest(config) 221 | rxrest.one('cars', 1) 222 | .get() 223 | .subscribe((car: Car) => { 224 | console.log(car.$uuid) 225 | }) 226 | ``` 227 | 228 | Also works in a non-`$fromServer` resource: 229 | 230 | ``` 231 | const car = rxrest.fromObject('cars') 232 | console.log(car.$uuid) 233 | ``` 234 | 235 | [^ Back to menu](#menu) 236 | 237 | ## Interceptors 238 | 239 | You can add custom behaviors on every state of the request. In order those are: 240 | 1. Request 241 | 2. Response 242 | 3. Error 243 | 244 | To alter those states, you can add interceptors having the following signature: 245 | 1. `requestInterceptor(request: Request)` 246 | 2. `responseInterceptor(request: Body)` 247 | 3. `errorInterceptor(error: Response)` 248 | 249 | Each of those can return a Stream, a Promise, their initial altered value, or be void (ie: return nothing). 250 | 251 | For example, let's alter the request and the response: 252 | 253 | ```javascript 254 | config.requestInterceptors.push(function(request) { 255 | request.headers.set('foo', 'bar') 256 | }) 257 | 258 | // This alters the body (note that ResponseBodyHandler below is more appropriate to do so) 259 | config.responseInterceptors.push(function(response) { 260 | return response.text( 261 | .then(data => { 262 | data = JSON.parse(data) 263 | data.foo = 'bar' 264 | //We can read the body only once (see Body.bodyUsed), here we return a new Response 265 | return new Response(JSON.stringify(body), response) 266 | }) 267 | }) 268 | 269 | // Performs a GET request with a 'foo' header having `bar` as value 270 | const rxrest = new RxRest(config) 271 | 272 | rxrest.one('cars', 1) 273 | .get() 274 | 275 | > RxRestItem {id: 1, brand: 'Volkswagen', name: 'Polo', foo: 1} 276 | ``` 277 | 278 | [^ Back to menu](#menu) 279 | 280 | ## Handlers 281 | 282 | Handlers allow you to transform the Body before or after a request is issued. 283 | 284 | Those are the default values: 285 | 286 | ```javascript 287 | /** 288 | * This method transforms the requested body to a json string 289 | */ 290 | config.requestBodyHandler = function(body) { 291 | if (!body) { 292 | return undefined 293 | } 294 | 295 | if (body instanceof FormData || body instanceof URLSearchParams) { 296 | return body 297 | } 298 | 299 | return body instanceof RxRestItem ? body.json() : JSON.stringify(body) 300 | } 301 | 302 | /** 303 | * This transforms the response in an Object (ie JSON.parse on the body text) 304 | * should return Promise<{body: any, metadata: any}> 305 | */ 306 | config.responseBodyHandler = function(body) { 307 | return body.text() 308 | .then(text => { 309 | return {body: text ? JSON.parse(text) : null, metadata: null} 310 | }) 311 | } 312 | ``` 313 | 314 | In the `responseBodyHandler`, you can note that we're returning an object containing: 315 | 316 | 1. `body` - the javascript Object or Array that will be transformed in a RxRestItem or RxRestCollection 317 | 2. `metadata` - an API request sometimes gives us metadata (for example pagination metadata), add it here to be able to retrieve `item.$metadata` later 318 | 319 | [^ Back to menu](#menu) 320 | 321 | ## API 322 | 323 | There are two prototypes: 324 | - RxRestItem 325 | - RxRestCollection - an iterable collection of RxRestItem 326 | 327 | ### Available on both RxRestItem and RxRestCollection 328 | 329 | #### `one(route: string, id: any): RxRestItem` 330 | 331 | Creates an RxRestItem on the requested route. 332 | 333 | #### `all(route: string, asIterable: boolean = false): RxRestCollection` 334 | 335 | Creates an RxRestCollection on the requested route 336 | 337 | Note that this allows url composition: 338 | 339 | ```javascript 340 | rxrest.all('cars').one('audi', 1).URL 341 | 342 | > cars/audi/1 343 | ``` 344 | 345 | #### `fromObject(route: string, element: Object|Object[]): RxRestItem|RxRestCollection` 346 | 347 | Depending on whether element is an `Object` or an `Array`, it returns an RxRestItem or an RxRestCollection. 348 | 349 | For example: 350 | 351 | ```javascript 352 | const car = rxrest.fromObject('cars', {id: 1, brand: 'Volkswagen', name: 'Polo'}) 353 | 354 | > RxRestItem {id: 1, brand: 'Volkswagen', name: 'Polo'} 355 | 356 | car.URL 357 | 358 | > cars/1 359 | ``` 360 | 361 | RxRest automagically binds the id in the route, note that the identifier property is configurable. 362 | 363 | #### `get(queryParams?: Object|URLSearchParams, headers?: Object|Headers): Stream` 364 | 365 | Performs a `GET` request, for example: 366 | 367 | ```javascript 368 | rxrest.one('cars', 1).get({brand: 'Volkswagen'}) 369 | .subscribe(e => console.log(e)) 370 | 371 | GET /cars/1?brand=Volkswagen 372 | 373 | > RxRestItem {id: 1, brand: 'Volkswagen', name: 'Polo'} 374 | ``` 375 | 376 | #### `post(body?: BodyParam, queryParams?: Object|URLSearchParams, headers?: Object|Headers): Stream` 377 | 378 | Performs a `POST` request, for example: 379 | 380 | ```javascript 381 | const car = new Car({brand: 'Audi', name: 'A3'}) 382 | rxrest.all('cars').post(car) 383 | .subscribe(e => console.log(e)) 384 | 385 | > RxRestItem {id: 3, brand: 'Audi', name: 'A3'} 386 | ``` 387 | 388 | #### `remove(queryParams?: Object|URLSearchParams, headers?: Object|Headers): Stream` 389 | 390 | Performs a `DELETE` request 391 | 392 | #### `patch(body?: BodyParam, queryParams?: Object|URLSearchParams, headers?: Object|Headers): Stream` 393 | 394 | Performs a `PATCH` request 395 | 396 | #### `head(queryParams?: Object|URLSearchParams, headers?: Object|Headers): Stream` 397 | 398 | Performs a `HEAD` request 399 | 400 | #### `trace(queryParams?: Object|URLSearchParams, headers?: Object|Headers): Stream` 401 | 402 | Performs a `TRACE` request 403 | 404 | #### `request(method: string, body?: BodyParam): Stream` 405 | 406 | This is useful when you need to do a custom request, note that we're adding query parameters and headers 407 | 408 | ```javascript 409 | rxrest.all('cars/1/audi') 410 | .setQueryParams({foo: 'bar'}) 411 | .setHeaders({'Content-Type': 'application/x-www-form-urlencoded'}) 412 | .request('GET') 413 | ``` 414 | 415 | 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. 416 | 417 | #### `json(): string` 418 | 419 | Output a `JSON` string of your RxRest element. 420 | 421 | ```javascript 422 | rxrest.one('cars', 1) 423 | .get() 424 | .subscribe((e: RxRestItem) => console.log(e.json())) 425 | 426 | > {id: 1, brand: 'Volkswagen', name: 'Polo'} 427 | ``` 428 | 429 | #### `plain(): Object|Object[]` 430 | 431 | This gives you the original object (ie: not an instance of RxRestItem or RxRestCollection): 432 | 433 | ```javascript 434 | rxrest.one('cars', 1) 435 | .get() 436 | .subscribe((e: RxRestItem) => console.log(e.plain())) 437 | 438 | > {id: 1, brand: 'Volkswagen', name: 'Polo'} 439 | ``` 440 | 441 | #### `clone(): RxRestItem|RxRestCollection` 442 | 443 | Clones the current instance to a new one. 444 | 445 | ### RxRestCollection 446 | 447 | #### `getList(): Stream` 448 | 449 | Just a reference to Restangular ;). It's an alias to `get()`. 450 | 451 | ### RxRestItem 452 | 453 | #### `save(): RxRestCollection` 454 | 455 | 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. 456 | 457 | [^ Back to menu](#menu) 458 | 459 | ## Typings 460 | 461 | Interfaces: 462 | 463 | ```typescript 464 | import { RxRest, RxRestItem, RxRestConfig } from 'rxrest'; 465 | 466 | const config = new RxRestConfig() 467 | config.baseURL = 'http://localhost' 468 | 469 | interface Car { 470 | id: number; 471 | name: string; 472 | model: string; 473 | } 474 | 475 | const rxrest = new RxRest(config) 476 | 477 | rxrest.one('/cars', 1) 478 | .get() 479 | .subscribe((item: Car) => { 480 | console.log(item.model) 481 | item.model = 'audi' 482 | 483 | item.save() 484 | }) 485 | ``` 486 | 487 | 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: 488 | 489 | ```typescript 490 | interface HydraItem { 491 | '@id': string; 492 | '@context': string; 493 | '@type': string; 494 | } 495 | 496 | interface Car extends HydraItem { 497 | name: string; 498 | model: Model; 499 | color: string; 500 | } 501 | 502 | interface Model extends HydraItem { 503 | name: string; 504 | } 505 | ``` 506 | 507 | To know more about typings and rxrest, please check out [the typings example](https://github.com/soyuka/rxrest/blob/master/test/typings.ts). 508 | 509 | [^ Back to menu](#menu) 510 | 511 | ## Angular 2 configuration example 512 | 513 | First, let's declare our providers: 514 | 515 | ```typescript 516 | import { Injectable, NgModule, Component, OnInit } from '@angular/core' 517 | import { RxRest, RxRestConfiguration } from 'rxrest' 518 | 519 | @Injectable() 520 | export class AngularRxRestConfiguration extends RxRestConfiguration { 521 | constructor() { 522 | super() 523 | this.baseURL = 'localhost/api' 524 | } 525 | } 526 | 527 | @Injectable() 528 | export class AngularRxRest extends RxRest { 529 | constructor(config: RxRestConfiguration) { 530 | super(config) 531 | } 532 | } 533 | 534 | @NgModule({ 535 | providers: [ 536 | {provide: RxRest, useClass: AngularRxRest}, 537 | {provide: RxRestConfiguration, useClass: AngularRxRestConfiguration}, 538 | ] 539 | }) 540 | export class SomeModule { 541 | } 542 | 543 | ``` 544 | 545 | Then, just inject `RxRest`: 546 | 547 | ```typescript 548 | export interface Car { 549 | name: string 550 | } 551 | 552 | @Component({ 553 | template: '
  • {{car.name}}
' 554 | }) 555 | export class FooComponent implements OnInit { 556 | constructor(private rxrest: RxRest) { 557 | } 558 | 559 | ngOnInit() { 560 | this.cars = this.rxrest.all('cars', true).get() 561 | } 562 | } 563 | ``` 564 | 565 | [Full example featuring jwt authentication, errors handling, body parsers for JSON-LD](https://gist.github.com/soyuka/c2e89ebf3c7a33f8d059c567aefd471c) 566 | 567 | [^ Back to menu](#menu) 568 | 569 | ## Test 570 | 571 | Testing can be done using [`rxrest-assert`](https://github.com/soyuka/rxrest-assert). 572 | 573 | ## Licence 574 | 575 | MIT 576 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* global describe it before beforeEach after */ 2 | const chai = require('chai') 3 | const spies = require('chai-spies') 4 | chai.use(spies) 5 | const expect = chai.expect 6 | const express = require('express') 7 | 8 | const {from, of, merge} = require('rxjs') 9 | const {mergeMap, switchMap, delay} = require('rxjs/operators') 10 | const {Headers, Response, Request} = require('node-fetch') 11 | require('./urlsearchparamspolyfill.js') 12 | 13 | global.Headers = Headers 14 | global.Response = Response 15 | global.Request = Request 16 | 17 | global.FormData = require('form-data') 18 | 19 | const {RxRestConfiguration, RxRestItem, RxRestCollection, RxRest} = require('../lib/index.js') 20 | 21 | let temp = new RxRestConfiguration() 22 | const fetch = temp.fetch 23 | let rxrest 24 | let config 25 | let server 26 | 27 | describe('RxRest', function () { 28 | before(function (cb) { 29 | const app = express() 30 | const bodyParser = require('body-parser') 31 | 32 | app.use(bodyParser.json()) 33 | app.use(bodyParser.urlencoded({extended: true})) 34 | 35 | app.get('/test', function (req, res) { 36 | res.json([{foo: req.query.foo, id: 3}]) 37 | }) 38 | 39 | app.get('/test/:id', function (req, res) { 40 | res.json({foo: req.query.foo, id: parseInt(req.params.id)}) 41 | }) 42 | 43 | app.patch('/test/3', function (req, res) { 44 | res.end() 45 | }) 46 | 47 | app.trace('/test/3', function (req, res) { 48 | res.end() 49 | }) 50 | 51 | app.post('/test', function (req, res) { 52 | req.body.method = 'post' 53 | req.body.id = 4 54 | return res.status(201).json(req.body) 55 | }) 56 | 57 | app.put('/test/:id', function (req, res) { 58 | req.body.method = 'put' 59 | return res.status(200).json(req.body) 60 | }) 61 | 62 | app.head('/404', function (req, res) { 63 | res.status(404).send('fail') 64 | }) 65 | 66 | app.delete('/test/:id', function (req, res) { 67 | res.json({'method': 'delete'}) 68 | }) 69 | 70 | app.delete('/foobar', function (req, res) { 71 | res.set('Content-Length', 0).status(204).end() 72 | }) 73 | 74 | app.get('/error', function (req, res) { 75 | res.status(500).send('fail') 76 | }) 77 | 78 | app.get('/empty', function (req, res) { 79 | res.status(200).json([]) 80 | }) 81 | 82 | app.get('/timeout', function (req, res) { 83 | setTimeout(() => { 84 | res.status(504).end() 85 | }, 100) 86 | }) 87 | 88 | app.post('/resource', function (req, res) { 89 | res.status(201).json({id: 1}) 90 | }) 91 | 92 | // force id from above 93 | app.put('/resource/1', function (req, res) { 94 | res.status(200).json({id: 1}) 95 | }) 96 | 97 | server = app.listen(3333, cb) 98 | }) 99 | 100 | beforeEach(function () { 101 | config = new RxRestConfiguration() 102 | config.baseURL = 'http://localhost:3333' 103 | expect(config.baseURL).to.equal('http://localhost:3333/') 104 | config.identifier = 'id' 105 | expect(config.identifier).to.equal('id') 106 | rxrest = new RxRest(config) 107 | }) 108 | 109 | it.skip('should use a new instance', function () { 110 | let i = 0 111 | 112 | return rxrest.all('test') 113 | .get() 114 | .toPromise() 115 | .then((data) => { 116 | data.push(new RxRestItem('test', {id: 5})) 117 | 118 | return from(data.map(item => rxrest.one('test', item.id))) 119 | .pipe( 120 | mergeMap(item => item.get({foo: 'bar'})) 121 | ) 122 | .subscribe(e => { 123 | if (i === 0) { 124 | expect(e.id).to.equal(3) 125 | expect(e.foo).to.equal('bar') 126 | i++ 127 | return 128 | } 129 | 130 | expect(e.foo).to.equal('bar') 131 | expect(e.id).to.equal(5) 132 | }) 133 | }) 134 | }) 135 | 136 | it('should get one', function () { 137 | config.requestInterceptors.push(function (request) { 138 | expect(request.headers.has('Accept')).to.be.true 139 | }) 140 | 141 | return rxrest.one('test', 3) 142 | .get({foo: 'foo'}, {'Accept': 'application/json'}) 143 | .subscribe(item => { 144 | expect(item.$fromServer).to.be.true 145 | expect(item).to.be.an.instanceof(RxRestItem) 146 | expect(item.URL).to.equal('http://localhost:3333/test/3') 147 | expect(item.plain()).to.deep.equal({foo: 'foo', id: 3}) 148 | expect(item).to.have.ownProperty('foo', 'foo') 149 | 150 | item.bar = 'bar' 151 | delete item.foo 152 | 153 | Object.defineProperty(item, 'foobar', { 154 | value: 'foobar', 155 | enumerable: true 156 | }) 157 | 158 | //can't override internal property 159 | Object.defineProperty(item, '$element', { 160 | value: 'foobar', 161 | enumerable: true 162 | }) 163 | 164 | expect(item.plain()).to.deep.equal({bar: 'bar', id: 3, foobar: 'foobar'}) 165 | 166 | let clone = item.clone() 167 | expect(clone.plain()).to.deep.equal({bar: 'bar', id: 3, foobar: 'foobar'}) 168 | expect(clone.$fromServer).to.equal(true) 169 | expect(clone.URL).to.equal('http://localhost:3333/test/3') 170 | }) 171 | }) 172 | 173 | it('should get one with global parameters', function () { 174 | config.queryParams.set('foo', 'bar') 175 | config.headers.set('Accept', 'application/json') 176 | 177 | return rxrest.one('test', 3) 178 | .get() 179 | .subscribe((item) => { 180 | expect(item).to.be.an.instanceof(RxRestItem) 181 | expect(item.URL).to.equal('http://localhost:3333/test/3') 182 | expect(item.plain()).to.deep.equal({foo: 'bar', id: 3}) 183 | expect(item).to.have.ownProperty('foo', 'bar') 184 | }) 185 | }) 186 | 187 | it('should get one with global parameters (from object)', function () { 188 | config.queryParams = {foo: 'bar'} 189 | config.headers = {'Accept': 'application/json'} 190 | 191 | return rxrest.one('test', 3) 192 | .get() 193 | .subscribe((item) => { 194 | expect(item).to.be.an.instanceof(RxRestItem) 195 | expect(item.URL).to.equal('http://localhost:3333/test/3') 196 | expect(item.plain()).to.deep.equal({foo: 'bar', id: 3}) 197 | expect(item).to.have.ownProperty('foo', 'bar') 198 | }) 199 | }) 200 | 201 | it('should get all', function () { 202 | let params = new URLSearchParams() 203 | params.set('foo', 'bar') 204 | 205 | let headers = new Headers() 206 | headers.set('Accept', 'application/json') 207 | 208 | config.requestInterceptors.push(function (request) { 209 | expect(request.headers.has('Accept')).to.be.true 210 | }) 211 | 212 | return rxrest.all('test') 213 | .getList(params, headers) 214 | .toPromise() 215 | .then(function (values) { 216 | expect(values).to.be.an.instanceof(RxRestCollection) 217 | for (let item of values) { 218 | expect(item).to.be.an.instanceof(RxRestItem) 219 | expect(item.URL).to.equal('http://localhost:3333/test/3') 220 | expect(item.$fromServer).to.be.true 221 | } 222 | 223 | expect(values.URL).to.equal('http://localhost:3333/test') 224 | 225 | expect(values.plain()).to.deep.equal([{foo: 'bar', id: 3}]) 226 | expect(values.json()).to.equal(JSON.stringify([{foo: 'bar', id: 3}])) 227 | 228 | let clone = values.clone() 229 | 230 | expect(clone[0].$fromServer).to.be.true 231 | expect(clone.plain()).to.deep.equal([{foo: 'bar', id: 3}]) 232 | }) 233 | }) 234 | 235 | it('should add request interceptor', function () { 236 | let spy = chai.spy(function () {}) 237 | 238 | config.requestInterceptors = [ 239 | function (req) { 240 | spy() 241 | req.foo = 'FOO' 242 | return of(req) 243 | }, 244 | function (req) { 245 | return new Promise((resolve, reject) => { 246 | spy() 247 | expect(req.foo).to.equal('FOO') 248 | req.foo = 'GET' 249 | resolve(req) 250 | }) 251 | }, 252 | function (req) { 253 | spy() 254 | expect(req.method).to.equal('GET') 255 | return new Request(req.url + '?foo=bar') 256 | } 257 | ] 258 | 259 | config.responseInterceptors.push(function (response) { 260 | return response.text() 261 | .then(e => { 262 | let body = JSON.parse(e) 263 | body.bar = 'foo' 264 | spy() 265 | 266 | return new Response(JSON.stringify(body), response) 267 | }) 268 | }) 269 | 270 | return rxrest.one('test', 3) 271 | .get() 272 | .subscribe((value) => { 273 | expect(spy).to.have.been.called.exactly(4) 274 | expect(value.plain()).to.deep.equal({foo: 'bar', id: 3, bar: 'foo'}) 275 | }) 276 | }) 277 | 278 | it('should save a resource', function () { 279 | config.headers.set('Content-Type', 'application/json') 280 | 281 | return rxrest.one('test', 3) 282 | .get() 283 | .pipe( 284 | mergeMap(e => { 285 | e.bar = 'foo' 286 | return e.save() 287 | }) 288 | ) 289 | .subscribe(e => { 290 | expect(e).to.deep.equal({bar: 'foo', id: 3, method: 'put'}) 291 | }) 292 | }) 293 | 294 | it('should save a resource from object', function () { 295 | config.headers.set('Content-Type', 'application/json') 296 | 297 | return rxrest.fromObject('test', {foo: 'bar'}) 298 | .save() 299 | .subscribe(e => { 300 | expect(e).to.deep.equal({foo: 'bar', id: 4, method: 'post'}) 301 | }) 302 | }) 303 | 304 | it('should save a resource by using post', function () { 305 | config.headers.set('Content-Type', 'application/json') 306 | 307 | return rxrest.one('test') 308 | .post({bar: 'foo'}) 309 | .subscribe(e => { 310 | expect(e).to.deep.equal({bar: 'foo', id: 4, method: 'post'}) 311 | }) 312 | }) 313 | 314 | it('should handle error', function () { 315 | let spy = chai.spy(function () {}) 316 | 317 | config.errorInterceptors.push(function (response) { 318 | expect(response.status).to.equal(404) 319 | spy() 320 | }) 321 | 322 | return rxrest.one('404') 323 | .head() 324 | .toPromise() 325 | .catch((e) => { 326 | expect(spy).to.have.been.called 327 | expect(e.status).to.equal(404) 328 | }) 329 | }) 330 | 331 | it('should handle error with promise', function () { 332 | return rxrest.one('404') 333 | .head() 334 | .toPromise() 335 | .then(() => {}) 336 | .catch((e) => { 337 | expect(e.status).to.equal(404) 338 | }) 339 | }) 340 | 341 | it('should create a collection from an array', function () { 342 | config.headers.set('Content-Type', 'application/json') 343 | 344 | rxrest.fromObject('test', [{foo: 'bar', id: 3}, {foo: 'foo', id: 4}]) 345 | .map(e => { 346 | expect(e).to.be.an.instanceof(RxRestItem) 347 | }) 348 | }) 349 | 350 | it('should create a custom request', function () { 351 | rxrest = rxrest.one('test') 352 | rxrest.$route = ['test/3'] 353 | return rxrest.request('GET') 354 | .subscribe(e => { 355 | expect(e).to.be.an.instanceof(RxRestItem) 356 | }) 357 | }) 358 | 359 | it('should get one and put', function () { 360 | return rxrest.one('test', 3) 361 | .setHeaders({'Content-Type': 'application/json'}) 362 | .get() 363 | .toPromise() 364 | .then(e => { 365 | e.foo = 'bar' 366 | return e.put() 367 | .subscribe(function (e) { 368 | expect(e).to.be.an.instanceof(RxRestItem) 369 | expect(e.method).to.equal('put') 370 | expect(e.foo).to.equal('bar') 371 | }) 372 | }) 373 | }) 374 | 375 | it('should change request/response body handlers', function () { 376 | let spy = chai.spy(function () {}) 377 | 378 | config.requestBodyHandler = function (body) { 379 | spy() 380 | return undefined 381 | } 382 | 383 | config.responseBodyHandler = function (body) { 384 | spy() 385 | return body.text() 386 | .then((t) => { 387 | return {body: JSON.parse(t), metadata: 'foo'} 388 | }) 389 | } 390 | 391 | return rxrest.one('test', 3) 392 | .get() 393 | .subscribe(e => { 394 | expect(e).to.be.an.instanceof(RxRestItem) 395 | expect(e.$metadata).to.equal('foo') 396 | expect(spy).to.have.been.called.exactly(2) 397 | }) 398 | }) 399 | 400 | it('should delete and patch/trace one', function (cb) { 401 | return rxrest 402 | .one('test', 3) 403 | .remove() 404 | .subscribe(function (e) { 405 | expect(e).to.be.an.instanceof(RxRestItem) 406 | expect(e.method).to.equal('delete') 407 | merge(e.patch(), e.trace()) 408 | .subscribe(() => {}, () => {}, () => { 409 | cb() 410 | }) 411 | }) 412 | }) 413 | 414 | it('should throw non-request errors', function (cb) { 415 | config.requestInterceptors.push(function (body) { 416 | throw TypeError('fail') 417 | }) 418 | 419 | rxrest 420 | .one('test', 3) 421 | .get() 422 | .toPromise() 423 | .catch(e => { 424 | expect(e).to.be.an.instanceof(TypeError) 425 | cb() 426 | }) 427 | }) 428 | 429 | it('should abort a request', function (cb) { 430 | config.abortCallback = chai.spy() 431 | 432 | let t = rxrest.all('timeout') 433 | 434 | from([0, 1]) 435 | .pipe(delay(10), mergeMap(() => t.get())) 436 | .subscribe(() => cb(new Error('Next called')), 437 | (err) => { 438 | expect(err.status).to.equal(504) 439 | expect(config.abortCallback).to.have.been.called 440 | cb() 441 | } 442 | ) 443 | }) 444 | 445 | it('should chain query params', function (cb) { 446 | let spy = chai.spy(function () {}) 447 | 448 | config.requestInterceptors = [ 449 | function (request) { 450 | spy() 451 | expect(request.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded') 452 | expect(request.method).to.equal('GET') 453 | } 454 | ] 455 | 456 | rxrest.all('test') 457 | .setQueryParams({foo: 'bar'}) 458 | .setHeaders({'Content-Type': 'application/x-www-form-urlencoded'}) 459 | .request('GET') 460 | .subscribe(item => { 461 | expect(item[0].foo).to.equal('bar') 462 | expect(spy).to.have.been.called.exactly(1) 463 | cb() 464 | }) 465 | }) 466 | 467 | it('should use fetch with a string', function (cb) { 468 | fetch('http://localhost:3333/test') 469 | .subscribe(e => { 470 | e.json() 471 | .then(f => { 472 | expect(f).to.deep.equal([{id: 3}]) 473 | cb() 474 | }) 475 | }) 476 | }) 477 | 478 | it('should be promise compatible', function () { 479 | return rxrest.one('test', 3).get() 480 | .toPromise() 481 | }) 482 | 483 | it('should error properly', function (cb) { 484 | fetch('http://localhost:3333/error') 485 | .toPromise() 486 | .catch(e => { 487 | cb() 488 | }) 489 | }) 490 | 491 | it('should create a resource without id', function () { 492 | let item = rxrest.one('test') 493 | item.foo = 'bar' 494 | 495 | return item.save() 496 | .toPromise() 497 | .then(e => { 498 | expect(e).to.have.property('foo', 'bar') 499 | expect(e.$route).to.deep.equal(['test', '4']) 500 | return e 501 | }) 502 | }) 503 | 504 | it('should work when no response (content-length = 0)', function () { 505 | let item = rxrest.one('foobar') 506 | 507 | return item.remove() 508 | .toPromise() 509 | .then((e) => { 510 | return 'ok' 511 | }) 512 | }) 513 | 514 | it('should handle array query parameters', function () { 515 | let item = rxrest.one('foobar') 516 | 517 | item.queryParams = { 518 | 'foo[]': [0, 1] 519 | } 520 | 521 | expect(item.queryParams.toString()).to.equal('foo%5B%5D=0&foo%5B%5D=1') 522 | expect(item.requestQueryParams.toString()).to.equal('?foo%5B%5D=0&foo%5B%5D=1') 523 | }) 524 | 525 | it('should get end when no results', function (cb) { 526 | rxrest.all('empty') 527 | .get() 528 | .subscribe({ 529 | next: () => {}, 530 | error: () => {}, 531 | complete: () => { 532 | cb() 533 | } 534 | }) 535 | }) 536 | 537 | it.skip('should work with rxjs switch map and get end event on empty', function (cb) { 538 | var source = of('v') 539 | .pipe(switchMap(function (x) { 540 | return rxrest.all('empty').get() 541 | })) 542 | 543 | source.subscribe((v) => {}, () => {}, () => cb()) 544 | }) 545 | 546 | it('should get options', function () { 547 | let spy = chai.spy(function () {}) 548 | 549 | config.requestBodyHandler = function (body) { 550 | spy() 551 | return undefined 552 | } 553 | 554 | config.responseBodyHandler = function (body) { 555 | spy() 556 | return body.text() 557 | .then((t) => { 558 | return {body: t} 559 | }) 560 | } 561 | 562 | return rxrest.one('test', 3) 563 | .options() 564 | .subscribe(e => { 565 | expect(e).to.be.an.instanceof(RxRestItem) 566 | expect(spy).to.have.been.called.exactly(2) 567 | }) 568 | }) 569 | 570 | // since v5 it's the default behaviour, next test tests old behaviour 571 | it.skip('should get all as iterable', function () { 572 | let params = new URLSearchParams() 573 | params.set('foo', 'bar') 574 | 575 | let headers = new Headers() 576 | headers.set('Accept', 'application/json') 577 | 578 | config.requestInterceptors.push(function (request) { 579 | expect(request.headers.has('Accept')).to.be.true 580 | }) 581 | 582 | return rxrest.all('test') 583 | .asIterable() 584 | .get(params, headers) 585 | .subscribe((values) => { 586 | expect(values).to.be.an.instanceof(RxRestCollection) 587 | for (let item of values) { 588 | expect(item).to.be.an.instanceof(RxRestItem) 589 | expect(item.URL).to.equal('http://localhost:3333/test/3') 590 | expect(item.$fromServer).to.be.true 591 | } 592 | 593 | expect(values.URL).to.equal('http://localhost:3333/test') 594 | 595 | expect(values.plain()).to.deep.equal([{foo: 'bar', id: 3}]) 596 | expect(values.json()).to.equal(JSON.stringify([{foo: 'bar', id: 3}])) 597 | 598 | let clone = values.clone() 599 | 600 | expect(clone[0].$fromServer).to.be.true 601 | expect(clone.plain()).to.deep.equal([{foo: 'bar', id: 3}]) 602 | }) 603 | }) 604 | 605 | it('should not get all as iterable', function () { 606 | let params = new URLSearchParams() 607 | params.set('foo', 'bar') 608 | 609 | let headers = new Headers() 610 | headers.set('Accept', 'application/json') 611 | 612 | config.requestInterceptors.push(function (request) { 613 | expect(request.headers.has('Accept')).to.be.true 614 | }) 615 | 616 | return rxrest.all('test') 617 | .asIterable(false) 618 | .get(params, headers) 619 | .subscribe((item) => { 620 | expect(item).to.be.an.instanceof(RxRestItem) 621 | expect(item.URL).to.equal('http://localhost:3333/test/3') 622 | expect(item.$fromServer).to.be.true 623 | }) 624 | }) 625 | 626 | // This is default behavior since v5, next test for opposite 627 | it.skip('should get all as iterable (2nd way of doing it)', function () { 628 | let params = new URLSearchParams() 629 | params.set('foo', 'bar') 630 | 631 | let headers = new Headers() 632 | headers.set('Accept', 'application/json') 633 | 634 | config.requestInterceptors.push(function (request) { 635 | expect(request.headers.has('Accept')).to.be.true 636 | }) 637 | 638 | return rxrest.all('test', true) 639 | .get(params, headers) 640 | .subscribe((values) => { 641 | expect(values).to.be.an.instanceof(RxRestCollection) 642 | for (let item of values) { 643 | expect(item).to.be.an.instanceof(RxRestItem) 644 | expect(item.URL).to.equal('http://localhost:3333/test/3') 645 | expect(item.$fromServer).to.be.true 646 | } 647 | 648 | expect(values.URL).to.equal('http://localhost:3333/test') 649 | 650 | expect(values.plain()).to.deep.equal([{foo: 'bar', id: 3}]) 651 | expect(values.json()).to.equal(JSON.stringify([{foo: 'bar', id: 3}])) 652 | 653 | let clone = values.clone() 654 | 655 | expect(clone[0].$fromServer).to.be.true 656 | expect(clone.plain()).to.deep.equal([{foo: 'bar', id: 3}]) 657 | }) 658 | }) 659 | 660 | it('should not get all as iterable (2nd way of doing it)', function () { 661 | let params = new URLSearchParams() 662 | params.set('foo', 'bar') 663 | 664 | let headers = new Headers() 665 | headers.set('Accept', 'application/json') 666 | 667 | config.requestInterceptors.push(function (request) { 668 | expect(request.headers.has('Accept')).to.be.true 669 | }) 670 | 671 | return rxrest.all('test', false) 672 | .get(params, headers) 673 | .subscribe((item) => { 674 | expect(item).to.be.an.instanceof(RxRestItem) 675 | expect(item.URL).to.equal('http://localhost:3333/test/3') 676 | expect(item.$fromServer).to.be.true 677 | }) 678 | }) 679 | 680 | // replicates a bug where second put was /resource/id/id 681 | it('should update', function (done) { 682 | const resource = rxrest.one('resource') 683 | 684 | // POST 685 | resource.save() 686 | .subscribe(() => { 687 | expect(resource.$route).to.deep.equal(['resource', '1']) 688 | // PUT 689 | resource.save() 690 | .subscribe(() => { 691 | expect(resource.$route).to.deep.equal(['resource', '1']) 692 | // PUT 693 | resource.save() 694 | .subscribe(() => { 695 | expect(resource.$route).to.deep.equal(['resource', '1']) 696 | done() 697 | }) 698 | }) 699 | }) 700 | 701 | }) 702 | 703 | it('should check $pristine', function () { 704 | return rxrest.one('test', 3) 705 | .get() 706 | .subscribe((item) => { 707 | expect(item).to.be.an.instanceof(RxRestItem) 708 | expect(item.$pristine).to.be.true 709 | 710 | item.foo = 'bar' 711 | 712 | expect(item.$pristine).to.be.false 713 | expect(item.$fromServer).to.be.true 714 | }) 715 | }) 716 | 717 | it('should not change $pristine if value is the same', function () { 718 | return rxrest.one('test', 3) 719 | .get({foo: 'bar'}) 720 | .subscribe((item) => { 721 | expect(item).to.be.an.instanceof(RxRestItem) 722 | expect(item.$pristine).to.be.true 723 | 724 | item.foo = 'bar' 725 | 726 | expect(item.$pristine).to.be.true 727 | expect(item.$fromServer).to.be.true 728 | }) 729 | }) 730 | 731 | it('should add uuids', function (done) { 732 | const config = new RxRestConfiguration() 733 | config.baseURL = 'http://localhost:3333' 734 | config.identifier = 'id' 735 | config.uuid = true 736 | rxrest = new RxRest(config) 737 | 738 | rxrest.all('test') 739 | .get() 740 | .toPromise() 741 | .then((data) => { 742 | expect(data[0].$uuid).to.be.a.string 743 | expect(data[0].$uuid).not.to.be.undefined 744 | data.unshift(rxrest.fromObject('test', {})) 745 | expect(data[0].$uuid).to.be.a.string 746 | expect(data[0].$uuid).not.to.be.undefined 747 | done() 748 | }) 749 | }) 750 | 751 | it('should have a suffix', function () { 752 | expect(rxrest.one('test', 1, 'suffix').$route).to.deep.equal(['test', '1', 'suffix']) 753 | expect(rxrest.fromObject('test', {id: '1'}, 'suffix', 'state').$route).to.deep.equal(['test', '1', 'suffix', 'state']) 754 | expect(rxrest.one('test', 1, 'suffix', 'state').$route).to.deep.equal(['test', '1', 'suffix', 'state']) 755 | }) 756 | 757 | after(() => { 758 | server.close() 759 | }) 760 | }) 761 | 762 | process.on('unhandledRejection', function(err) { 763 | console.error(err.stack) 764 | process.exit(1) 765 | }) 766 | --------------------------------------------------------------------------------