diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5efbd2d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 diff --git a/.gitignore b/.gitignore index 901f7df..ea04946 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ node_modules dist coverage html-report + +*.iml +.idea/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5ce3ac3..399e943 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,14 @@ { "name": "hal-rest-client", - "version": "0.3.1", + "version": "0.3.4", "lockfileVersion": 1, "requires": true, "dependencies": { + "@types/lodash": { + "version": "4.14.92", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.92.tgz", + "integrity": "sha512-cdvY1fyUGYgG7/i07a/sR5PnD6+Z+ljUrD0CNVf0qj645VvEdLNtZPjvCp4siPy3rQ/KRXMfUATIqi3+9x57Sw==" + }, "@types/nock": { "version": "8.2.1", "resolved": "https://registry.npmjs.org/@types/nock/-/nock-8.2.1.tgz", @@ -2277,12 +2282,6 @@ } } }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -2321,6 +2320,12 @@ "function-bind": "1.1.0" } }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, "stringstream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", diff --git a/package.json b/package.json index 95a03af..5e65a50 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "url": "http://github.com/deblockt/hal-rest-client/issues" }, "dependencies": { + "@types/lodash": "^4.14.92", "axios": "^0.15.3", "reflect-metadata": "^0.1.9", "uri-templates": "^0.1.9" diff --git a/src/hal-json-parser.ts b/src/hal-json-parser.ts index 14d4059..481580c 100644 --- a/src/hal-json-parser.ts +++ b/src/hal-json-parser.ts @@ -38,8 +38,8 @@ export class JSONParser { for (const linkKey in json._links) { if ("self" !== linkKey) { const href = this.extractURI(links[linkKey]); - const type = Reflect.getMetadata("halClient:specificType", c.prototype, linkKey) || HalResource; const propKey = halToTs[linkKey] || linkKey; + const type = Reflect.getMetadata("halClient:specificType", c.prototype, propKey) || HalResource; const linkResource = createResource(this.halRestClient, type, href); for (const propInLinkKey of Object.keys(links[linkKey])) { linkResource.prop(propInLinkKey, links[linkKey][propInLinkKey]); diff --git a/src/hal-resource-interface.ts b/src/hal-resource-interface.ts index 6536518..cf13879 100644 --- a/src/hal-resource-interface.ts +++ b/src/hal-resource-interface.ts @@ -18,6 +18,18 @@ export interface IHalResource { */ fetch(forceOrParams: boolean|object): Promise; + /** + * fetch an array of the current resource + * + * Unlike #fetch(boolean|object) this method needs #uri to be set. So it's not possible to fetch a resource in + * advance. + * + * @param {object} params: If the uri is a template link, you can set the parameters. + * @param {IHalResourceConstructor} resource: An optional resource to create the array items of. + * @returns {Promise} Will return an array of resources. + */ + fetchArray(params?: object, resource?: IHalResourceConstructor): Promise; + /** * get or set a prop or a link. * if name is a link. link function is used diff --git a/src/hal-resource.ts b/src/hal-resource.ts index 86c3984..e668ba4 100644 --- a/src/hal-resource.ts +++ b/src/hal-resource.ts @@ -2,6 +2,7 @@ import { DefaultSerializer, IJSONSerializer } from "./hal-json-serializer"; import { IHalResource, IHalResourceConstructor } from "./hal-resource-interface"; import { HalRestClient } from "./hal-rest-client"; import { URI } from "./uri"; +import * as _ from 'lodash'; export class HalResource implements IHalResource { public readonly links = {}; @@ -39,6 +40,13 @@ export class HalResource implements IHalResource { } } + public fetchArray(params?: object, resource?: IHalResourceConstructor): Promise { + return this.restClient.fetchArray( + this.uri.fill(params as object), + resource ? resource.prototype.constructor : this.constructor as IHalResourceConstructor, + ) as Promise; + } + /** * to clear value use null not undefined */ @@ -133,11 +141,8 @@ export class HalResource implements IHalResource { for (const prop of props) { const jsonKey = this.tsProptoHalProd(prop) ; - if (this.props[prop] !== undefined && this.props[prop] !== null && this.props[prop].onInitEnded !== undefined) { - json[jsonKey] = serializer.parseResource(this.props[prop]); - } else { - json[jsonKey] = serializer.parseProp(this.props[prop]); - } + + json[jsonKey] = this.serializeProperty(prop, serializer); } for (const link of links) { @@ -147,4 +152,23 @@ export class HalResource implements IHalResource { return json; } + + private serializeProperty(prop, serializer: IJSONSerializer, arrayItem?: any) { + let result = null; + + const property = arrayItem || this.props[prop]; + + if (!_.isEmpty(property) && _.isFunction(property.onInitEnded)) { + result = serializer.parseResource(property) + } else if (_.isArray(property)) { + result = _(property) + .map(item => this.serializeProperty(prop, serializer, item)) + .toArray() + .value() + } else { + result = serializer.parseProp(property) + } + + return result + } } diff --git a/src/test/model/array-resource.ts b/src/test/model/array-resource.ts new file mode 100644 index 0000000..f821daa --- /dev/null +++ b/src/test/model/array-resource.ts @@ -0,0 +1,17 @@ +import {HalResource} from '../../hal-resource'; +import {HalProperty} from '../../hal-decorator'; + +export class ArrayResourceItem extends HalResource { + + @HalProperty() + public id: Number; + +} + +export class ArrayResource extends HalResource { + + @HalProperty('items', ArrayResourceItem) + public items: ArrayResourceItem[]; + +} + diff --git a/src/test/test-resource-class.ts b/src/test/test-resource-class.ts index 3fcb28b..c0088b1 100644 --- a/src/test/test-resource-class.ts +++ b/src/test/test-resource-class.ts @@ -6,6 +6,7 @@ import { test } from "tape-async"; import { ContactInfos } from "./model/contact-infos"; import { Cyclical, CyclicalList } from "./model/cyclical"; import { Person } from "./model/person"; +import {ArrayResource, ArrayResourceItem} from './model/array-resource'; // mock list response function initTests() { @@ -122,6 +123,20 @@ function initTests() { testNock .get("/cyclicals/refresh") .reply(200, cyclicals); + + const arrayResource = { + _links: { + self: 'http://test.fr/array', + }, + items: [ + {id: 1, _links: {self: {href: 'http://test.fr/array/1'}}}, + {id: 2, _links: {self: {href: 'http://test.fr/array/2'}}}, + ], + }; + + testNock + .get("/arrayResource") + .reply(200, arrayResource); } test("can get single string prop", async (t) => { @@ -267,3 +282,16 @@ test("cyclical property have good class type", async (t) => { t.ok(Array.isArray(cyclicals.cyclicals), "cyclicals is an array"); t.equals(cyclicals.cyclicals[0].property, "name"); }); + +test('fetching resource with array contains valid typed resources', async (t) => { + initTests(); + + const client = createClient('http://test.fr'); + const resource: ArrayResource = await client.fetch('/arrayResource', ArrayResource); + + t.ok(resource instanceof ArrayResource, 'Result has the correct type'); + t.ok(Array.isArray(resource.items), 'Result content is array'); + t.ok(resource.items.length === 2, 'Correct count of array items'); + t.ok(resource.items[0] instanceof ArrayResourceItem, 'Array item has correct type'); + t.ok(resource.items[0].id === 1, 'Correct item returned.'); +}); \ No newline at end of file