@modernpoacher/halacious 7.0.3 → 7.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,13 @@
1
- import _ from 'lodash'
2
1
  import * as hoek from '@hapi/hoek'
3
2
  import URI from 'urijs'
4
3
 
5
- const templatedRE = /{*}/
4
+ import {
5
+ getSelf,
6
+ getHref,
7
+ getPrefix
8
+ } from './utils.mjs'
9
+
10
+ const TEMPLATE = /{*}/
6
11
 
7
12
  /**
8
13
  * A HAL wrapper interface around an entity. Provides an api for adding new links and recursively embedding child
@@ -15,34 +20,104 @@ const templatedRE = /{*}/
15
20
  * @constructor
16
21
  */
17
22
  export class Representation {
18
- constructor (factory, self, entity, root) {
19
- this._halacious = factory._halacious
23
+ constructor (factory, self, entity, root = this) {
24
+ const {
25
+ _halacious: halacious,
26
+ _request: request
27
+ } = factory
28
+
29
+ const {
30
+ href
31
+ } = self
32
+
33
+ const links = {
34
+ self
35
+ }
36
+
20
37
  this.factory = factory
21
- this.request = factory._request
22
- this._root = root || this
23
- this.self = self.href
24
- this._links = { self }
38
+ this.self = href
39
+ this.entity = entity
40
+ this._root = root
41
+ this._halacious = halacious
42
+ this._request = request
43
+ this._links = links
25
44
  this._embedded = {}
26
45
  this._namespaces = {}
27
- this._props = {}
28
- this._ignore = {}
29
- this.entity = entity
46
+ this._props = new Map()
47
+ this._ignore = new Set()
48
+ }
49
+
50
+ getHalacious () {
51
+ return this._halacious
52
+ }
53
+
54
+ getRequest () {
55
+ return this._request
56
+ }
57
+
58
+ getRoot () {
59
+ return this._root
60
+ }
61
+
62
+ getLinks () {
63
+ return this._links
64
+ }
65
+
66
+ getEmbedded () {
67
+ return this._embedded
68
+ }
69
+
70
+ getNamespaces () {
71
+ return this._namespaces
72
+ }
73
+
74
+ getProps () {
75
+ return this._props
76
+ }
77
+
78
+ getIgnore () {
79
+ return this._ignore
80
+ }
81
+
82
+ get halacious () {
83
+ return this._halacious
84
+ }
85
+
86
+ get request () {
87
+ return this._request
30
88
  }
31
89
 
32
90
  /**
33
91
  * Adds a namespace to the 'curie' link collection. all curies in a response, top level or nested, should be declared
34
92
  * in the top level _links collection. a reference '_root' is kept to the top level representation for this purpose
93
+ *
35
94
  * @param namespace
36
95
  */
37
96
  curie (namespace) {
38
- if (namespace && !this._root._namespaces[namespace.prefix]) {
39
- this._root._namespaces[namespace.prefix] = namespace
40
- this._root._links.curies = this._root._links.curies || []
41
- this._root._links.curies.push({
42
- name: namespace.prefix,
43
- href: `${this._halacious.namespaceUrl(this.request, namespace)}/{rel}`,
97
+ if (namespace) {
98
+ const root = this.getRoot()
99
+
100
+ const namespaces = root.getNamespaces()
101
+
102
+ const prefix = getPrefix(namespace)
103
+
104
+ if (Reflect.has(namespaces, prefix)) return
105
+
106
+ Reflect.set(namespaces, prefix, namespace)
107
+
108
+ const links = root.getLinks()
109
+
110
+ const curies = Reflect.get(links, 'curies') ?? []
111
+
112
+ const request = this.getRequest()
113
+
114
+ const namespaceUrl = this.getHalacious().namespaceUrl(request, namespace)
115
+
116
+ Reflect.set(links, 'curies', curies.concat({
117
+ name: prefix,
118
+ href: `${namespaceUrl}/{rel}`,
44
119
  templated: true
45
- })
120
+ }))
46
121
  }
47
122
  }
48
123
 
@@ -53,121 +128,173 @@ export class Representation {
53
128
  * @return {Representation}
54
129
  */
55
130
  prop (name, value) {
56
- this._props[name] = value
131
+ this.getProps()
132
+ .set(name, value)
133
+
57
134
  return this
58
135
  }
59
136
 
60
137
  /**
61
- * Merges an object's properties into the custom properties collection.
138
+ * Merges an object's properties into the custom properties collection
139
+ *
62
140
  * @param prop
63
141
  */
64
142
  merge (prop) {
65
- hoek.merge(this._props, prop)
143
+ const props = this.getProps()
144
+
145
+ Object
146
+ .entries(prop)
147
+ .forEach(([name, value]) => {
148
+ props.set(name, value)
149
+ })
150
+
151
+ return this
66
152
  }
67
153
 
68
154
  /**
69
155
  * @param {...String || String[]} args properties to ignore
70
156
  * @return {Representation}
71
157
  */
72
- ignore (...args) {
73
- const props = _.isArray(args[0]) ? args[0] : args
74
- props.forEach(function (prop) {
75
- this._ignore[prop] = true
76
- }, this)
158
+ ignore (arg, ...args) {
159
+ const props = (
160
+ Array.isArray(arg)
161
+ ? arg
162
+ : [arg].concat(args)
163
+ )
164
+
165
+ const ignore = this.getIgnore()
166
+
167
+ props
168
+ .filter(Boolean)
169
+ .forEach((prop) => {
170
+ ignore.add(prop)
171
+ })
172
+
77
173
  return this
78
174
  }
79
175
 
80
176
  /**
81
- * Prepares the representation for JSON serialization.
177
+ * Prepares the representation for JSON serialization
178
+ *
82
179
  * @return {{}}
83
180
  */
84
181
  toJSON () {
85
182
  // initialize the json entity
86
- const payload = { _links: this._links }
87
- const self = this
183
+ const object = { _links: this.getLinks() }
88
184
 
89
185
  // copy all target properties in the entity using JSON.stringify(). if the entity has a .toJSON() implementation,
90
186
  // it will be called. properties on the ignore list will not be copied
91
- const { entity } = this
187
+ const {
188
+ entity
189
+ } = this
190
+
191
+ const ignore = this.getIgnore()
192
+
92
193
  JSON.stringify(entity, (key, value) => {
93
194
  if (!key) {
94
195
  return value
95
196
  }
96
- if (!self._ignore[key]) {
97
- payload[key] = value
98
- }
197
+
198
+ if (!ignore.has(key)) Reflect.set(object, key, value)
99
199
  })
100
200
 
201
+ const props = this.getProps()
202
+
101
203
  // merge in any extra properties
102
- _.assign(payload, this._props)
103
-
104
- const embeddedKeys = _.keys(this._embedded)
105
- if (embeddedKeys.length > 0) {
106
- payload._embedded = {}
107
-
108
- const self = this
109
- embeddedKeys.forEach((embedKey) => {
110
- if (self._embedded[embedKey] instanceof Representation) {
111
- payload._embedded[embedKey] = {}
112
- } else if (self._embedded[embedKey] instanceof Array) {
113
- payload._embedded[embedKey] = []
114
- }
115
-
116
- JSON.stringify(self._embedded[embedKey], (key, value) => {
117
- if (!key) {
118
- return value
119
- }
120
-
121
- payload._embedded[embedKey][key] = value
122
- })
123
- })
204
+ Object.assign(object, Object.fromEntries(Array.from(props.entries()).filter(([key]) => !ignore.has(key))))
205
+
206
+ const embedded = this.getEmbedded()
207
+
208
+ const entries = Object.entries(embedded)
209
+ if (entries.length) {
210
+ object._embedded = (
211
+ entries
212
+ .reduce((accumulator, [entryKey, entryValue]) => {
213
+ let currentValue
214
+
215
+ if (entryValue instanceof Representation) {
216
+ currentValue = {}
217
+ } else {
218
+ if (Array.isArray(entryValue)) {
219
+ currentValue = []
220
+ }
221
+ }
222
+
223
+ JSON.stringify(entryValue, (key, value) => {
224
+ if (!key) {
225
+ return value
226
+ }
227
+
228
+ if (!ignore.has(key)) Reflect.set(currentValue, key, value)
229
+ })
230
+
231
+ return (
232
+ Object.assign(accumulator, { [entryKey]: currentValue })
233
+ )
234
+ }, {})
235
+ )
124
236
  }
125
237
 
126
- return payload
238
+ return object
127
239
  }
128
240
 
129
241
  /**
130
242
  * Creates a new link and adds it to the _links collection
243
+ *
131
244
  * @param rel
132
245
  * @param link
133
246
  * @return {{} || []} the new link
134
247
  */
135
- link (originalRel, link) {
136
- const rel = this._halacious.rel(originalRel)
137
- const qname = rel.qname()
138
-
139
- if (_.isArray(link)) {
140
- const that = this
141
- this._links[qname] = []
142
- return link.map((l) => that.link(originalRel, l), this)
248
+ link (relName, link) {
249
+ const halacious = this.getHalacious()
250
+
251
+ const rel = halacious.rel(relName)
252
+ const key = rel.qname()
253
+
254
+ const links = this.getLinks()
255
+
256
+ if (Array.isArray(link)) {
257
+ if (!Reflect.has(links, key)) Reflect.set(links, key, [])
258
+
259
+ return (
260
+ link.map((href) => this.link(relName, href))
261
+ )
143
262
  }
144
263
 
145
264
  // adds the namespace to the top level curie list
146
265
  this.curie(rel.namespace)
147
266
 
148
- link = this._halacious.link(link, this._links.self.href)
149
- link.templated = templatedRE.test(link.href) ? true : undefined
267
+ const href = getHref(getSelf(links)) ?? ''
268
+
269
+ link = halacious.link(link, href)
270
+
271
+ if (TEMPLATE.test(link.href)) link.templated = true
272
+
150
273
  // e.g. 'mco:rel'
151
- if (!this._links[qname]) {
152
- this._links[qname] = link
153
- } else if (_.isArray(this._links[qname])) {
154
- this._links[qname].push(link)
155
- } else {
156
- this._links[qname] = [this._links[qname], link]
157
- }
274
+
275
+ Reflect.set(links, key, (
276
+ Reflect.has(links, key)
277
+ ? [].concat(Reflect.get(links, key), link)
278
+ : link
279
+ ))
158
280
 
159
281
  return link
160
282
  }
161
283
 
162
284
  /**
163
285
  * Resolves a relative path against the representation's self href
286
+ *
164
287
  * @param relativePath
165
288
  * @return {*}
166
289
  */
167
290
  resolve (relativePath) {
168
- return new URI(relativePath)
169
- .absoluteTo(`${this._links.self.href}/`)
170
- .toString()
291
+ const href = getHref(getSelf(this.getLinks())) ?? ''
292
+
293
+ return (
294
+ new URI(relativePath)
295
+ .absoluteTo(href.concat('/'))
296
+ .toString()
297
+ )
171
298
  }
172
299
 
173
300
  /**
@@ -178,7 +305,9 @@ export class Representation {
178
305
  * @return {*}
179
306
  */
180
307
  route (routeName, params) {
181
- return this._halacious.route(routeName, params)
308
+ return (
309
+ this.getHalacious().route(routeName, params)
310
+ )
182
311
  }
183
312
 
184
313
  /**
@@ -188,45 +317,62 @@ export class Representation {
188
317
  * @param {{} || []} entity an object to wrap
189
318
  * @return {entity || []}
190
319
  */
191
- embed (originalRel, self, entity) {
192
- const rel = this._halacious.rel(originalRel)
193
- const qname = rel.qname()
320
+ embed (relName, self, entity) {
321
+ const halacious = this.getHalacious()
322
+
323
+ const rel = halacious.rel(relName)
324
+ const key = rel.qname()
194
325
 
195
326
  this.curie(rel.namespace)
196
327
 
197
- if (_.isArray(entity)) {
198
- const that = this
199
- this._embedded[qname] = []
200
- return entity.map((e) => that.embed(originalRel, self, e), this)
328
+ const embedded = this.getEmbedded()
329
+
330
+ if (Array.isArray(entity)) {
331
+ if (!Reflect.has(embedded, key)) Reflect.set(embedded, key, [])
332
+
333
+ return (
334
+ entity.map((entity) => this.embed(relName, self, entity))
335
+ )
201
336
  }
202
337
 
203
- self = this._halacious.link(self, this._links.self.href)
338
+ const href = getHref(getSelf(this.getLinks())) ?? ''
204
339
 
205
- const embedded = this.factory.create(entity, self, this._root)
340
+ self = halacious.link(self, href)
206
341
 
207
- if (!this._embedded[qname]) {
208
- this._embedded[qname] = embedded
209
- } else if (_.isArray(this._embedded[qname])) {
210
- this._embedded[qname].push(embedded)
211
- } else {
212
- this._embedded[qname] = [this._embedded[qname], embedded]
213
- }
342
+ const root = this.getRoot()
343
+
344
+ const embed = this.factory.create(entity, self, root)
345
+
346
+ Reflect.set(embedded, key, (
347
+ Reflect.has(embedded, key)
348
+ ? [].concat(Reflect.get(embedded, key), embed)
349
+ : embed
350
+ ))
214
351
 
215
- return embedded
352
+ return embed
216
353
  }
217
354
 
218
355
  /**
219
- * Convenience method for embedding an array of entities
356
+ * Convenience method for embedding an entity or array of entities
357
+ *
220
358
  * @param rel
221
359
  * @param self
222
- * @param entities
360
+ * @param arg
223
361
  * @return {Representation}
224
362
  */
225
- embedCollection (rel, self, entities) {
226
- entities = _.isArray(entities) ? entities : [entities]
227
- entities.forEach(function (entity) {
228
- this.embed(rel, hoek.clone(self), entity)
229
- }, this)
363
+ embedCollection (rel, self, arg, ...args) {
364
+ const entities = (
365
+ Array.isArray(arg)
366
+ ? arg
367
+ : [arg].concat(args)
368
+ )
369
+
370
+ entities
371
+ .filter(Boolean)
372
+ .forEach((entity) => {
373
+ this.embed(rel, hoek.clone(self), entity)
374
+ })
375
+
230
376
  return this
231
377
  }
232
378
 
@@ -236,12 +382,13 @@ export class Representation {
236
382
  * @param callback
237
383
  */
238
384
  configure (config, callback) {
239
- this._halacious.configureRepresentation(this, config, callback)
385
+ this.getHalacious().configureRepresentation(config, this, callback)
240
386
  }
241
387
  }
242
388
 
243
389
  /**
244
- * Responsible for creating all hal entities, top level or embedded, needed for a hapi request
390
+ * Responsible for creating all hal entities, top level or embedded, needed for a Hapi request
391
+ *
245
392
  * @param halacious a reference to the plugin api
246
393
  * @param request a hapi request object
247
394
  * @constructor
@@ -252,6 +399,30 @@ export class RepresentationFactory {
252
399
  this._request = request
253
400
  }
254
401
 
402
+ getHalacious () {
403
+ return this._halacious
404
+ }
405
+
406
+ getRequest () {
407
+ return this._request
408
+ }
409
+
410
+ getRequestPath () {
411
+ return this.getRequest()?.path
412
+ }
413
+
414
+ get halacious () {
415
+ return this._halacious
416
+ }
417
+
418
+ get request () {
419
+ return this._request
420
+ }
421
+
422
+ get requestPath () {
423
+ return this.request?.path
424
+ }
425
+
255
426
  /**
256
427
  * Creates a new hal representation out of a javascript object
257
428
  * @param {{}=} entity the entity to wrap with a representation. an empty object is created by default
@@ -260,11 +431,14 @@ export class RepresentationFactory {
260
431
  * should be expanded into absolute urls
261
432
  * @return {Representation}
262
433
  */
263
- create (entity, self, root) {
264
- entity = entity || {}
265
- self = self || (this._request && this._request.path)
266
- self = this._halacious.link(self)
267
- return new Representation(this, self, entity, root)
434
+ create (entity = {}, self = this.getRequestPath(), root) {
435
+ const halacious = this.getHalacious()
436
+
437
+ const link = halacious.link(self)
438
+
439
+ return (
440
+ new Representation(this, link, entity, root)
441
+ )
268
442
  }
269
443
  }
270
444
 
@@ -0,0 +1,20 @@
1
+ function reduce (accumulator, fieldName) {
2
+ return (
3
+ Reflect.has(accumulator, fieldName)
4
+ ? Reflect.get(accumulator, fieldName)
5
+ : accumulator
6
+ )
7
+ }
8
+
9
+ /**
10
+ * Previously `reach`
11
+ */
12
+ export default function getFieldValueByPath (path = '', context = {}) {
13
+ const fieldNames = String(path ?? '').split('.')
14
+
15
+ return (
16
+ fieldNames
17
+ .filter(Boolean)
18
+ .reduce(reduce, context)
19
+ )
20
+ }
@@ -0,0 +1,15 @@
1
+ import Negotiator from 'negotiator'
2
+
3
+ /**
4
+ * Selects the media type based on the request's Accept header and a ranked ordering of configured
5
+ * media types
6
+ *
7
+ * @param request
8
+ * @param mediaTypes
9
+ * @return {*}
10
+ */
11
+ export default function getMediaType (request, mediaTypes) {
12
+ return (
13
+ new Negotiator(request).mediaType([].concat(mediaTypes))
14
+ )
15
+ }
@@ -0,0 +1,32 @@
1
+ import getFieldValueByPath from './getFieldValueByPath.mjs'
2
+
3
+ const REG = /{([^{}]+)}|([^{}]+)/g
4
+
5
+ /**
6
+ * Evaluates and flattens deep expressions (e.g. '/{foo.a.b}') into a single level context object: {'foo.a.b': value}
7
+ * so that it may be used by url-template library
8
+ *
9
+ * @param template
10
+ * @param context
11
+ * @return {{}}
12
+ */
13
+ export default function getTemplateContext (template, context) {
14
+ const templateContext = {}
15
+ let matches
16
+
17
+ while ((matches = REG.exec(template))) {
18
+ if (Array.isArray(matches)) {
19
+ const [
20
+ , // input,
21
+ path
22
+ ] = matches
23
+
24
+ if (path) {
25
+ const fieldValue = getFieldValueByPath(path, context)
26
+ if (fieldValue) Reflect.set(templateContext, path, fieldValue.toString())
27
+ }
28
+ }
29
+ }
30
+
31
+ return templateContext
32
+ }