Typescriptify src/services/hypermedia.js
authorFrank Bessou <frank.bessou@logilab.fr>
Thu, 06 Jul 2017 10:12:26 +0200
changeset 254 100978264f6e
parent 253 22f11f2c300d
child 255 ba56a53b0abd
Typescriptify src/services/hypermedia.js
package.json
src/services/hypermedia.js
src/services/hypermedia.ts
--- a/package.json	Thu Jul 06 10:09:57 2017 +0200
+++ b/package.json	Thu Jul 06 10:12:26 2017 +0200
@@ -18,6 +18,7 @@
     "whatwg-fetch": "^0.11.0"
   },
   "devDependencies": {
+    "@types/lodash": "^4.14.68",
     "awesome-typescript-loader": "^3.1.3",
     "babel": "^6.5.2",
     "babel-loader": "^6.2.4",
--- a/src/services/hypermedia.js	Thu Jul 06 10:09:57 2017 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,156 +0,0 @@
-/* global fetch, API_URL, SCRIPT_NAME */
-
-import 'whatwg-fetch';
-import LinkParser from 'http-link-header';
-import {defaultsDeep} from 'lodash/object';
-
-import {mapToSchema} from '../jsonaryutils';
-import {appendPath} from '../utils';
-
-export class HttpHypermediaClient {
-
-    constructor(baseUrl, fetchFunc) {
-        if (baseUrl) {
-            this.baseUrl = baseUrl;
-        } else if (typeof SCRIPT_NAME !== 'undefined') {
-            this.baseUrl = SCRIPT_NAME;
-        } else if (typeof API_URL !== 'undefined') {
-            this.baseUrl = API_URL;
-        }
-        fetchFunc = fetchFunc || fetch;
-        this.fetch = fetchFunc.bind(window);
-    }
-
-    getResource(resourceRoute) {
-        const resource = {url: resourceRoute};
-        // Fetch resource
-        const dataResponsePromise = this.jsonFetchResponse(resource.url);
-        const resourcePromise = dataResponsePromise.then(response => {
-            resource.allowedActions = this.extractAllowedActions(response);
-            resource.links = this.extractLinks(response);
-            const schemaRoute = this.extractSchemaRoute(resource.links);
-            const schemaPromise = this.getSchema(schemaRoute);
-            const dataPromise = response.json().catch(() => null);
-
-            return Promise.all([dataPromise, schemaPromise]).then(
-                ([data, schema]) => {
-                    resource.data = mapToSchema(data, schema);
-                    return resource;
-                });
-        });
-        return resourcePromise;
-    }
-
-    getSchema(url, options = {}) {
-        defaultsDeep(options, {headers: {Accept: 'application/schema+json'}});
-        return this.jsonFetch(url, options);
-    }
-
-    getSubmissionSchema(resource) {
-        let selfLink = resource.data.getLink('self');
-        if (selfLink) {
-            selfLink = selfLink.rawLink;
-        } else {
-            const err = new Error(`no rel="self" link found schema for ${resource.url}`);
-            return Promise.reject(err);
-        }
-        const submissionSchema = selfLink.submissionSchema;
-        if (submissionSchema.hasOwnProperty('$ref')) {
-            return this.getSchema(submissionSchema.$ref);
-        } else {
-            return Promise.resolve(submissionSchema);
-        }
-    }
-
-    extractAllowedActions(response) {
-        if (!response.headers.has('Allow')) {
-            return ['view'];
-        }
-
-        const allowHeader = response.headers.get('Allow');
-        const allowedMethods = allowHeader.split(/[ ,]+/);
-        return allowedMethods.map((method) => {
-            switch(method) {
-                case 'POST':
-                    return 'add';
-                case 'GET':
-                    return 'view';
-                case 'PUT':
-                case 'PATCH':
-                    return 'edit';
-                case 'DELETE':
-                    return 'delete';
-            }
-        });
-    }
-
-    extractSchemaRoute(links) {
-        try {
-            return links.rel('describedby')[0].uri;
-        } catch (e) {
-            throw new Error(`Cannot find 'describedby' link`);
-        }
-    }
-
-    extractLinks(response) {
-        const linkHeader = response.headers.get("Link");
-        if (linkHeader === null) {
-            throw new Error(`"Link" header does not exist on resource's HTTP header`);
-        }
-        return LinkParser.parse(linkHeader);
-    }
-
-    jsonFetchResponse(url, options = {}) {
-        const fullUrl = this.buildUrl(url);
-        defaultsDeep(options, {credentials: 'same-origin',
-            headers: {Accept: 'application/json'}});
-        return this.fetch(fullUrl, options)
-            .then(response => {
-                const contentType = (response.headers.get('content-type') || '').toLowerCase();
-                if(response.status === 204 ||
-                        contentType.indexOf('application/json') >= 0 ||
-                        contentType.indexOf('application/problem+json') >= 0) {
-                    return response;
-                }
-                const method = options.method || 'GET';
-                throw new Error(
-                    `Got "${response.statusText}" from ${method} request at ${fullUrl}`);
-            });
-    }
-
-    submitResource(method, url, data) {
-        const options = {
-            method: method,
-            headers: {
-                'Content-Type': 'application/json',
-            },
-            body: JSON.stringify(data),
-        };
-        return this.jsonFetchResponse(url, options);
-    }
-
-    deleteResource(url) {
-        const options = {
-            method: 'DELETE',
-        };
-        url = this.buildUrl(url);
-        return this.fetch(url, options)
-            .then( response => response.status)
-            .then( status => {
-                if (status !== 204 && status !== 404) {
-                    throw new Error(`Could not delete resource '${url}'`);
-                }
-            });
-
-    }
-
-    buildUrl(path) {
-        return appendPath(this.baseUrl, path);
-    }
-
-    jsonFetch(url, options = {}) {
-        return this.jsonFetchResponse(url, options).then(response => response.json());
-    }
-}
-
-export default new HttpHypermediaClient();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/services/hypermedia.ts	Thu Jul 06 10:12:26 2017 +0200
@@ -0,0 +1,194 @@
+import {Links, parse as parseLinks} from 'http-link-header';
+import {defaultsDeep} from 'lodash';
+import 'whatwg-fetch';
+
+import {mapToSchema} from '../jsonaryutils';
+import {appendPath} from '../utils';
+
+declare var SCRIPT_NAME: string;
+declare var API_URL: string;
+
+type HttpMethod = 'POST'|'GET'|'PUT'|'PATCH'|'DELETE';
+
+export interface Resource {
+    allowedActions: string[];
+    data: any;
+    links: Links;
+    url: string;
+}
+
+export class HttpHypermediaClient {
+
+    private baseUrl: string;
+    private fetch: GlobalFetch['fetch'];
+    constructor(baseUrl?: string, fetchFunc: GlobalFetch['fetch'] = fetch) {
+        if (baseUrl) {
+            this.baseUrl = baseUrl;
+        } else if (typeof SCRIPT_NAME !== 'undefined') {
+            this.baseUrl = SCRIPT_NAME;
+        } else if (typeof API_URL !== 'undefined') {
+            this.baseUrl = API_URL;
+        }
+        this.fetch = fetchFunc.bind(window);
+    }
+
+    public getResource(resourceRoute: string): Promise<Resource> {
+        // Fetch resource
+        const dataResponsePromise = this.jsonFetchResponse(resourceRoute);
+        const resourcePromise = dataResponsePromise.then((response) => {
+            const allowedActions = this.extractAllowedActions(response);
+            const links = this.extractLinks(response);
+            const schemaRoute = this.extractSchemaRoute(links);
+            const schemaPromise = this.getSchema(schemaRoute);
+            const dataPromise = response.json().catch(() => null);
+            return Promise.all([dataPromise, schemaPromise]).then(
+                ([data, schema]: any[]) => {
+                    const wrappedData = mapToSchema(data, schema);
+                    return {
+                        allowedActions,
+                        data: wrappedData,
+                        links,
+                        url: resourceRoute,
+                    };
+                });
+        });
+        return resourcePromise;
+    }
+
+    public getSchema(url: string, options: RequestInit = {}): Promise<any> {
+        defaultsDeep(options, {headers: {Accept: 'application/schema+json'}});
+        return this.jsonFetch(url, options);
+    }
+
+    public getSubmissionSchema(resource: Resource) {
+        let selfLink = resource.data.getLink('self');
+        if (selfLink) {
+            selfLink = selfLink.rawLink;
+        } else {
+            const err = new Error(`no rel="self" link found schema for ${resource.url}`);
+            return Promise.reject(err);
+        }
+        const submissionSchema = selfLink.submissionSchema;
+        if (submissionSchema.hasOwnProperty('$ref')) {
+            return this.getSchema(submissionSchema.$ref);
+        } else {
+            return Promise.resolve(submissionSchema);
+        }
+    }
+
+    public createResource(url: string, data: any) {
+        const options = {
+            body: JSON.stringify(data),
+            headers: {
+                'Content-Type': 'application/json',
+            },
+            method: 'POST',
+        };
+        return this.jsonFetch(url, options);
+    }
+
+    public updateResource(url: string, data: any) {
+        const options = {
+            body: JSON.stringify(data),
+            headers: {
+                'Content-Type': 'application/json',
+            },
+            method: 'PUT',
+        };
+        return this.jsonFetch(url, options);
+    }
+
+    public deleteResource(url: string): Promise<void> {
+        const options = {
+            method: 'DELETE',
+        };
+        url = this.buildUrl(url);
+        return this.fetch(url, options)
+            .then( (response) => response.status)
+            .then( (status) => {
+                if (status !== 204 && status !== 404) {
+                    throw new Error(`Could not delete resource '${url}'`);
+                }
+            });
+
+    }
+
+    public jsonFetchResponse(url: string, options: RequestInit = {}) {
+        const fullUrl = this.buildUrl(url);
+        defaultsDeep(options, {credentials: 'same-origin',
+            headers: {Accept: 'application/json'}});
+        return this.fetch(fullUrl, options)
+            .then((response) => {
+                const contentType = (response.headers.get('content-type') || '').toLowerCase();
+                if (response.status === 204 ||
+                        contentType.indexOf('application/json') >= 0 ||
+                        contentType.indexOf('application/problem+json') >= 0) {
+                    return response;
+                }
+                const method = options.method || 'GET';
+                throw new Error(
+                    `Got "${response.statusText}" from ${method} request at ${fullUrl}`);
+            });
+    }
+
+    public submitResource(method: 'POST'|'PUT', url: string, data: any) {
+        const options = {
+            body: JSON.stringify(data),
+            headers: {
+                'Content-Type': 'application/json',
+            },
+            method,
+        };
+        return this.jsonFetchResponse(url, options);
+    }
+
+    private buildUrl(path: string) {
+        return appendPath(this.baseUrl, path);
+    }
+
+    private jsonFetch(url: string, options: RequestInit = {}) {
+        return this.jsonFetchResponse(url, options).then((response) => response.json());
+    }
+
+    private extractAllowedActions(response: Response): string[] {
+        if (!response.headers.has('Allow')) {
+            return ['view'];
+        }
+
+        const allowHeader = response.headers.get('Allow') || '';
+        const allowedMethods = allowHeader.split(/[ ,]+/)
+            .filter((method) =>
+                        ['POST', 'GET', 'PUT', 'PATCH', 'DELETE'].indexOf(method) > -1) as HttpMethod[];
+        return allowedMethods.map((method) => {
+            switch (method) {
+                case 'POST':
+                    return 'add';
+                case 'GET':
+                    return 'view';
+                case 'PUT':
+                case 'PATCH':
+                    return 'edit';
+                case 'DELETE':
+                    return 'delete';
+            }
+        });
+    }
+
+    private extractSchemaRoute(links: Links) {
+        try {
+            return links.rel('describedby')[0].uri;
+        } catch (e) {
+            throw new Error(`Cannot find 'describedby' link`);
+        }
+    }
+
+    private extractLinks(response: Response) {
+        const linkHeader = response.headers.get('Link');
+        if (linkHeader === null) {
+            throw new Error(`"Link" header does not exist on resource's HTTP header`);
+        }
+        return parseLinks(linkHeader);
+    }
+}
+
+export default new HttpHypermediaClient();