diff --git a/README.md b/README.md index ea92b2f..5f81af0 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ halResource.uri.uri // get the uri provided from server halResource.uri.fill({params: "test"}) // fill the templated uri with given parameters ``` -## how to use +## How to use The library provide two access method : 1. use generic object `HalResource` to map service return @@ -48,7 +48,7 @@ const client = createClient(); const client = createClient("http://exemple.com/api"); ``` -to get a resource, you can use fetchResource method. +To get a resource, you can use fetchResource method. ``` ts const resource = await client.fetchResource("http://exemple.com/api/resources/5") @@ -64,21 +64,31 @@ const resourceURI = resource.uri; ``` for a link, on `link` service return ```ts -const linkValue = resource.prop("link_name"); +const link = resource.prop("link_name"); // or -const linkValue = resource.link("link_name"); +const link = resource.link("link_name"); ``` > link attribute type is `HalResource` #### Follow a link -links are made to be followed. So you can simply fetch a link using `fetch` method. +Links are made to be followed. So you can simply fetch a link using `fetch` method. ``` ts -const linkValue = resource.link("link_name"); -await linkValue.fetch(); -const name = linkValue.prop("name"); + +const link = resource.link("link_name"); +await link.fetch(); +const name = link.prop("name"); +``` + +Note that `link()` returns an empty `HalResource` with its `uri` set. You need to call `fetch()` to populate the HalResource. + +The library also supports arrays of links using Array syntax: + +```ts +const link = resource.link("link_name")[0]; +await link.fetch(); +const name = link.prop("name"); ``` -> link return an empty `HalResource`, just `uri` is setted. `fetch` populate the HalResource. #### Follow a templated link @@ -86,17 +96,40 @@ If you link is templated, you can set parameter to fetch to compute fetch URL. ```ts // link "link_name" is a templated link like this // /bookings{?projection} -const linkValue = resource.link("link_name"); -const bookings = await linkValue.fetch(); // fetch /bookings -const bookingsWithName = await linkValue.fetch({projection : "name"}); // fetch /bookings?projection=name +const link = resource.link("link_name"); +const bookings = await link.fetch(); // fetch /bookings +const bookingsWithName = await link.fetch({projection : "name"}); // fetch /bookings?projection=name +``` +```ts // link "link_infos" is like this // /infos{/path*} -const linkValue = resource.link("link_infos"); -const infos = await linkValue.fetch(); // fetch /infos -const infosForFoo = await linkValue.fetch({path: "foo"}); +const link = resource.link("link_infos"); +const infos = await link.fetch(); // fetch /infos +const infosForFoo = await link.fetch({path: "foo"}); ``` +#### Links as props + +Note that named links are synonymous with props: + +```ts +const link = resource.link("link_name"); +const prop = resource.prop("link_name"); +link === prop // true +``` + +This means you can navigate a HAL hierarchy (referencing and fetching) using props alone: + +```ts +// using .prop() +const foo = await resource.prop("foo").fetch(); +const bar = await foo.prop("bar").fetch(); + +// using .links +bar.props === resource.links.foo.links.bar.props // true +``` + #### Update a resource Resource can be updated, an save with a PATCH query. @@ -182,7 +215,7 @@ const resource = await client.fetch("/resource/5", Resource); ``` > fetch return a promise. you can use `then` and `catch` to get result. Otherwise you can use `await` see [this article](https://blog.mariusschulz.com/2016/12/09/typescript-2-1-async-await-for-es3-es5) -read props is simply call object attributs. +Read props is simply call object attributes. ``` ts const name = resource.name; diff --git a/package.json b/package.json index 95a03af..16d5826 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hal-rest-client", "author": "Thomas Deblock ", - "version": "0.3.4", + "version": "0.4.0", "description": "Hal rest client for typescript", "tags": [ "HAL", diff --git a/src/hal-json-parser.ts b/src/hal-json-parser.ts index 14d4059..c1ef823 100644 --- a/src/hal-json-parser.ts +++ b/src/hal-json-parser.ts @@ -32,19 +32,27 @@ export class JSONParser { // get translation between hal-service-name and name on ts class const halToTs = Reflect.getMetadata("halClient:halToTs", c.prototype) || {}; + const processLink = (link, type) => { + const href = this.extractURI(link); + const linkResource = createResource(this.halRestClient, type, href); + for (const propKey of Object.keys(link)) { + linkResource.prop(propKey, link[propKey]); + } + return linkResource; + }; + for (const key in json) { if ("_links" === key) { const links = json._links; 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 type = Reflect.getMetadata("halClient:specificType", c.prototype, linkKey) || HalResource; const propKey = halToTs[linkKey] || linkKey; - const linkResource = createResource(this.halRestClient, type, href); - for (const propInLinkKey of Object.keys(links[linkKey])) { - linkResource.prop(propInLinkKey, links[linkKey][propInLinkKey]); - } - resource.link(propKey, linkResource); + const link = links[linkKey]; + const result = Array.isArray(link) + ? link.map((item) => processLink(item, type)) + : processLink(link, type); + resource.link(propKey, result); } } if (links.self) { diff --git a/src/hal-resource-interface.ts b/src/hal-resource-interface.ts index 6536518..a048c65 100644 --- a/src/hal-resource-interface.ts +++ b/src/hal-resource-interface.ts @@ -31,7 +31,7 @@ export interface IHalResource { * @param name : the link name * @param value : the new resource. If you want reset a link use null and not undefined */ - link(name: string, value ?: IHalResource): IHalResource; + link(name: string, value ?: any): any; /** * function called when object is populated diff --git a/src/hal-resource.ts b/src/hal-resource.ts index 86c3984..7455402 100644 --- a/src/hal-resource.ts +++ b/src/hal-resource.ts @@ -72,7 +72,7 @@ export class HalResource implements IHalResource { /** * to clear value use null not undefined */ - public link(name: string, value ?: IHalResource): HalResource { + public link(name: string, value ?: any): any { if (value !== void 0) { this.links[name] = value; if (this.initEnded) { diff --git a/src/test/test-issue-32.ts b/src/test/test-issue-32.ts new file mode 100644 index 0000000..b7eea0f --- /dev/null +++ b/src/test/test-issue-32.ts @@ -0,0 +1,58 @@ +import { test } from "tape-async"; + +import { createClient, HalResource, resetCache } from "../"; + +import * as nock from "nock"; + +// mock list response +function initTests() { + nock.cleanAll(); + resetCache(); + const project1 = { + _links: { + related: [ + { href: "http://test.fr/projects/1" }, + { href: "http://test.fr/projects/2" }, + { href: "http://test.fr/projects/3" }, + ], + self: { + href: "http://test.fr/projects/1", + }, + }, + test: "test", + }; + + const project2 = { + _links: { + self: { + href: "http://test.fr/projects/2", + }, + }, + }; + + const testNock = nock("http://test.fr/"); + + testNock + .get("/projects/1") + .reply(200, project1); + + testNock + .get("/projects/2") + .reply(200, project2); +} + +test("list of links are resources", async (t) => { + initTests(); + const project = await createClient().fetch("http://test.fr/projects/1", HalResource); + const related = project.link("related"); + t.equals(Array.isArray(related), true); + t.equals(related.every((item) => item instanceof HalResource), true); +}); + +test("list links can be fetched", async (t) => { + initTests(); + const project = await createClient().fetch("http://test.fr/projects/1", HalResource); + const related = project.link("related"); + await related[0].fetch(); + t.equals(related[0].prop("test"), "test"); +});