-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
284 lines (251 loc) · 8.59 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
'use strict';
var uriTemplates = require('uri-templates');
/**
* Module for performing HTTP GET en PUT requests for HAL resources.
*
* Its main use is to embed linked resources, even when the server returns only the links.
*
* @module hally
*/
/**
* A link to another resource.
*
* @typedef {Object} Link
* @property {string} href - The reference of the target resource; a URI or a URI Template.
* @property {boolean} [templated] - Indicates whether the href is a URI Template.
* @property {string} [type] - The media type of the target resource.
* @property {string} [deprecation] - A URL to information about the deprecation of the link.
* @property {string} [name] - A secondary key for selecting links that share the same relation type.
* @property {string} [profile] - The profile of the target resource; a URI.
* @property {string} [title] - A human-readable identification of the link.
* @property {string} [hreflang] - The language of the target resource.
*/
/**
* A HAL resource.
*
* Although the _links and _embedded properties are optional according to the RFC,
* this module always creates them to make traversal simpler.
*
* @typedef {Object} Hal
* @property {Object.<string, Link|Link[]>} _links - Links to related resources.
* @property {Object.<string, Hal|Hal[]>} _embedded - Embedded Hal resources.
*/
/**
* Follow a link relation and return the URI of the target resource(s).
*
* If the resource has no links with the relation type but does contains an
* embedded resource (or resources), the self link of the embedded resource(s)
* is used.
*
* @param {Hal} resource the subject resource
* @param {string} rel the link relation type
* @param {Object.<string, Object>} [params] parameters to expand the target href URI Template with
* @returns {string|string[]|null} the target URI(s)
*/
function linkHref(resource, rel, params) {
var link = resource._links[rel];
if (!link) {
// Fall through
} else if (!Array.isArray(link)) {
return resolveUri(link.href, params);
} else {
return link.map(function (l) {
return resolveUri(l.href, params);
});
}
var embedded = resource._embedded[rel];
if (!embedded) {
// Fall through
} else if (!Array.isArray(embedded)) {
return embedded._links.self.href;
} else {
return embedded.map(function (e) {
return e._links.self.href;
});
}
return null;
}
/**
* Either pass through a URI unchanged, or resolve a URI Template if parameters are given.
*
* @param {string} uri The URI or URI Template
* @param {Object.<string, Object>} [params] URI Template parameters
* @return {string} the resulting URI
*/
function resolveUri(uri, params) {
if (uri && params) {
uri = uriTemplates(uri).fillFromObject(params);
}
return uri;
}
/**
* A resource context contains for every URI:
* - undefined if it has not been requested, or
* - a promise of a resource if it has been requested, or
* - a resource if the request has completed.
*
* @typedef {Object.<string, Hal|Promise<Hal>>} Context;
*/
/**
* Add a HAL resource to the context.
*
* @param {Context} context the resource context
* @param {Hal} resource the HAL resource
*/
function addToContext(context, resource) {
context[resource._links.self.href] = resource;
// Make sure _embedded exists so users can safely write "resource._embedded[rel]"
if (!('_embedded' in resource)) {
resource._embedded = {};
}
// Also add any embedded resources
Object.keys(resource._embedded).forEach(function (rel) {
var embeds = resource._embedded[rel];
embeds = Array.isArray(embeds) ? embeds : [embeds];
embeds.forEach(function (embed) {
addToContext(context, embed);
});
});
}
/**
* An embed request is an object containing information about what HAL
* resources to embed. The resources are embedded even if they were
* linked but not embedded by the server.
*
* The embed request key is a relation type that should be embedded, the
* (optional) value the embed request(s) for the embedded resources.
*
* @typedef {Object.<string, EmbedRequest|null>} EmbedRequest
*/
/**
* Fetch a HAL resource.
*
* @param {string} uri - The resource URI.
* @param {Object} opts - A fetch options object to be used with any GET request for linked resources.
* @param {EmbedRequest[]} embeds - Embed requests for the resource.
* @param {Context} context - The resource context to store resources in.
*
* @returns {Promise<Hal>} A promise that resolves to the HAL resource.
*/
function fetchHalJson(uri, opts, embeds, context) {
var promise;
if (uri in context) {
promise = Promise.resolve(context[uri]);
} else {
promise = fetch(uri, opts)
.then(function (response) {
return response.json();
})
.then(function (resource) {
addToContext(context, resource);
return resource;
});
}
context[uri] = promise;
return promise.then(function (resource) {
return fetchAndEmbedLinks(resource, opts, embeds, context);
});
}
/**
* For all embed requests, get the linked resources and embed them.
*
* @param {Hal} resource - The HAL resource to process.
* @param {Object} opts - A fetch options object to be used with any GET request for linked resources.
* @param {EmbedRequest|null} embeds - The embed requests.
* @param {Context} context - The resources context. Makes sure each resource is requested only once.
*
* @return {Promise<Hal>} A promise that resolve to the resource after all resources are embedded.
*/
function fetchAndEmbedLinks(resource, opts, embeds, context) {
if (!embeds) embeds = {};
var embedPromises = Object.keys(embeds).map(function (rel) {
return fetchAndEmbedLink(resource, opts, rel, embeds[rel], context);
})
// var embedPromises = embeds.map(function (embed) {
// return fetchAndEmbedLink(resource, opts, embed, context);
// })
return Promise.all(embedPromises)
.then(function (/* ignore embedding result */) {
return resource;
});
}
/**
* Get linked resources and embed them.
*
* @param {Hal} resource - The HAL resource to process.
* @param {Object} opts - A fetch options object to be used with any GET request for linked resources.
* @param {string} rel - The link relation to embed.
* @param {EmbedRequest|null} embeds - Embed request for the related resource.
* @param {Context} context - The resources context. Makes sure each resource is requested only once.
*
* @return {Promise<Hal>} A promise that resolve to the resource after all resources are embedded.
*/
function fetchAndEmbedLink(resource, opts, rel, embeds, context) {
var hrefs = linkHref(resource, rel);
if (!hrefs) {
// Link relation does not exist, skip
return;
}
var linkedResourcesPromise;
if (Array.isArray(hrefs)) {
linkedResourcesPromise = Promise.all(hrefs.map(function (href) {
return fetchHalJson(href, opts, embeds, context);
}));
} else {
linkedResourcesPromise = fetchHalJson(hrefs, opts, embeds, context);
}
return linkedResourcesPromise.then(function (linkedResources) {
resource._embedded[rel] = linkedResources;
return resource;
});
}
/**
* Convert a HAL resource to its resource state, i.e. return a copy with '_links' and '_embedded' removed.
*
* @param {Hal} resource - The HAL resource.
*
* @returns {Object} The resource state.
*/
function toState(resource) {
var data = {};
Object.keys(resource).forEach(function (key) {
if (key !== '_links' && key !== '_embedded') {
data[key] = resource[key];
}
})
return data;
}
/**
* Convert a HAL resource to a fetch body, i.e. the stringified JSON with '_links' and '_embedded' removed.
*
* @param {Hal} resource - The HAL resource.
*
* @returns {string} The fetch body.
*/
function stateBody(resource) {
return JSON.stringify(toState(resource));
}
/**
* Perform an HTTP GET request for a HAL resource and ensure certain linked resources are embedded.
*
* @param {Object} opts - A fetch options object to be used with any GET request for linked resources.
* @param {EmbedRequest} [embeds] - Embed request(s) for linked resources. If absent, 'opts.embeds' is used.
*
* @returns {Promise<Hal>} A promise that resolves to the resource after all resources are embedded.
*/
function halJson(opts, embeds) {
if (!embeds) embeds = opts.embeds;
return function (response) {
return response.json().then(function (resource) {
var context = {};
addToContext(context, resource);
return fetchAndEmbedLinks(resource, opts, embeds, context);
})
}
}
module.exports = {
halJson: halJson,
linkHref: linkHref,
stateBody: stateBody,
toState: toState
}