@modernpoacher/halacious 7.0.3 → 7.0.5

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,48 +1,123 @@
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
9
14
  * entities.
10
15
  *
11
16
  * @param factory
12
- * @param self
13
17
  * @param entity
18
+ * @param self
14
19
  * @param root
15
20
  * @constructor
16
21
  */
17
22
  export class Representation {
18
- constructor (factory, self, entity, root) {
19
- this._halacious = factory._halacious
23
+ constructor (factory, entity, link, root = this) {
24
+ const {
25
+ _halacious: halacious,
26
+ _request: request
27
+ } = factory
28
+
29
+ const {
30
+ href
31
+ } = link
32
+
33
+ const links = {
34
+ self: link
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.entity = entity
39
+ this.self = href
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,169 @@ 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
- // initialize the json entity
86
- const payload = { _links: this._links }
87
- const self = this
182
+ // initialize the entity
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
- if (!key) {
94
- return value
95
- }
96
- if (!self._ignore[key]) {
97
- payload[key] = value
98
- }
194
+ if (!key) return value
195
+
196
+ if (!ignore.has(key)) Reflect.set(object, key, value)
99
197
  })
100
198
 
101
- // 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
- })
199
+ const embedded = this.getEmbedded()
200
+
201
+ const entries = Object.entries(embedded)
202
+ if (entries.length) {
203
+ object._embedded = (
204
+ entries
205
+ .reduce((accumulator, [entryKey, entryValue]) => {
206
+ let currentValue
207
+
208
+ if (entryValue instanceof Representation) {
209
+ currentValue = {}
210
+ } else {
211
+ if (Array.isArray(entryValue)) {
212
+ currentValue = []
213
+ }
214
+ }
215
+
216
+ JSON.stringify(entryValue, (key, value) => {
217
+ if (!key) return value
218
+
219
+ if (!ignore.has(key)) Reflect.set(currentValue, key, value)
220
+ })
221
+
222
+ return (
223
+ Object.assign(accumulator, { [entryKey]: currentValue })
224
+ )
225
+ }, {})
226
+ )
124
227
  }
125
228
 
126
- return payload
229
+ const props = this.getProps()
230
+
231
+ // merge in any extra properties
232
+ return (
233
+ Object.assign(object, Object.fromEntries(Array.from(props.entries()).filter(([key]) => !ignore.has(key))))
234
+ )
127
235
  }
128
236
 
129
237
  /**
130
238
  * Creates a new link and adds it to the _links collection
239
+ *
131
240
  * @param rel
132
241
  * @param link
133
242
  * @return {{} || []} the new link
134
243
  */
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)
244
+ link (relName, link) {
245
+ const halacious = this.getHalacious()
246
+
247
+ const rel = halacious.rel(relName)
248
+ const key = rel.qname()
249
+
250
+ const links = this.getLinks()
251
+
252
+ if (Array.isArray(link)) {
253
+ if (!Reflect.has(links, key)) Reflect.set(links, key, [])
254
+
255
+ return (
256
+ link.map((href) => this.link(relName, href))
257
+ )
143
258
  }
144
259
 
145
260
  // adds the namespace to the top level curie list
146
261
  this.curie(rel.namespace)
147
262
 
148
- link = this._halacious.link(link, this._links.self.href)
149
- link.templated = templatedRE.test(link.href) ? true : undefined
263
+ const href = getHref(getSelf(links)) ?? ''
264
+
265
+ link = halacious.link(link, href)
266
+
267
+ if (TEMPLATE.test(link.href)) link.templated = true
268
+
150
269
  // 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
- }
270
+
271
+ Reflect.set(links, key, (
272
+ Reflect.has(links, key)
273
+ ? [].concat(Reflect.get(links, key), link)
274
+ : link
275
+ ))
158
276
 
159
277
  return link
160
278
  }
161
279
 
162
280
  /**
163
281
  * Resolves a relative path against the representation's self href
282
+ *
164
283
  * @param relativePath
165
284
  * @return {*}
166
285
  */
167
286
  resolve (relativePath) {
168
- return new URI(relativePath)
169
- .absoluteTo(`${this._links.self.href}/`)
170
- .toString()
287
+ const href = getHref(getSelf(this.getLinks())) ?? ''
288
+
289
+ return (
290
+ new URI(relativePath)
291
+ .absoluteTo(href.concat('/'))
292
+ .toString()
293
+ )
171
294
  }
172
295
 
173
296
  /**
@@ -178,7 +301,9 @@ export class Representation {
178
301
  * @return {*}
179
302
  */
180
303
  route (routeName, params) {
181
- return this._halacious.route(routeName, params)
304
+ return (
305
+ this.getHalacious().route(routeName, params)
306
+ )
182
307
  }
183
308
 
184
309
  /**
@@ -188,45 +313,62 @@ export class Representation {
188
313
  * @param {{} || []} entity an object to wrap
189
314
  * @return {entity || []}
190
315
  */
191
- embed (originalRel, self, entity) {
192
- const rel = this._halacious.rel(originalRel)
193
- const qname = rel.qname()
316
+ embed (relName, self, entity) {
317
+ const halacious = this.getHalacious()
318
+
319
+ const rel = halacious.rel(relName)
320
+ const key = rel.qname()
194
321
 
195
322
  this.curie(rel.namespace)
196
323
 
197
- if (_.isArray(entity)) {
198
- const that = this
199
- this._embedded[qname] = []
200
- return entity.map((e) => that.embed(originalRel, self, e), this)
324
+ const embedded = this.getEmbedded()
325
+
326
+ if (Array.isArray(entity)) {
327
+ if (!Reflect.has(embedded, key)) Reflect.set(embedded, key, [])
328
+
329
+ return (
330
+ entity.map((entity) => this.embed(relName, self, entity))
331
+ )
201
332
  }
202
333
 
203
- self = this._halacious.link(self, this._links.self.href)
334
+ const href = getHref(getSelf(this.getLinks())) ?? ''
204
335
 
205
- const embedded = this.factory.create(entity, self, this._root)
336
+ self = halacious.link(self, href)
206
337
 
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
- }
338
+ const root = this.getRoot()
339
+
340
+ const embed = this.factory.create(entity, self, root)
341
+
342
+ Reflect.set(embedded, key, (
343
+ Reflect.has(embedded, key)
344
+ ? [].concat(Reflect.get(embedded, key), embed)
345
+ : embed
346
+ ))
214
347
 
215
- return embedded
348
+ return embed
216
349
  }
217
350
 
218
351
  /**
219
- * Convenience method for embedding an array of entities
352
+ * Convenience method for embedding an entity or array of entities
353
+ *
220
354
  * @param rel
221
355
  * @param self
222
- * @param entities
356
+ * @param arg
223
357
  * @return {Representation}
224
358
  */
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)
359
+ embedCollection (rel, self, arg, ...args) {
360
+ const entities = (
361
+ Array.isArray(arg)
362
+ ? arg
363
+ : [arg].concat(args)
364
+ )
365
+
366
+ entities
367
+ .filter(Boolean)
368
+ .forEach((entity) => {
369
+ this.embed(rel, hoek.clone(self), entity)
370
+ })
371
+
230
372
  return this
231
373
  }
232
374
 
@@ -236,36 +378,8 @@ export class Representation {
236
378
  * @param callback
237
379
  */
238
380
  configure (config, callback) {
239
- this._halacious.configureRepresentation(this, config, callback)
240
- }
241
- }
242
-
243
- /**
244
- * Responsible for creating all hal entities, top level or embedded, needed for a hapi request
245
- * @param halacious a reference to the plugin api
246
- * @param request a hapi request object
247
- * @constructor
248
- */
249
- export class RepresentationFactory {
250
- constructor (halacious, request) {
251
- this._halacious = halacious
252
- this._request = request
253
- }
254
-
255
- /**
256
- * Creates a new hal representation out of a javascript object
257
- * @param {{}=} entity the entity to wrap with a representation. an empty object is created by default
258
- * @param {String || {}=} self the self href or link object. The request's path is used by default
259
- * @param {Representation} root a pointer to the top level representation for adding curied links
260
- * should be expanded into absolute urls
261
- * @return {Representation}
262
- */
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)
381
+ this.getHalacious().configureRepresentation(config, this, callback)
268
382
  }
269
383
  }
270
384
 
271
- export default RepresentationFactory
385
+ export default Representation
@@ -0,0 +1,60 @@
1
+ import Representation from './representation.mjs'
2
+
3
+ /**
4
+ * Responsible for creating all hal entities, top level or embedded, needed for a Hapi request
5
+ *
6
+ * @param halacious a reference to the plugin api
7
+ * @param request a hapi request object
8
+ * @constructor
9
+ */
10
+ export class RepresentationFactory {
11
+ constructor (halacious, request) {
12
+ this._halacious = halacious
13
+ this._request = request
14
+ }
15
+
16
+ getHalacious () {
17
+ return this._halacious
18
+ }
19
+
20
+ getRequest () {
21
+ return this._request
22
+ }
23
+
24
+ getRequestPath () {
25
+ console.log(this.getRequest()?.path)
26
+ return this.getRequest()?.path
27
+ }
28
+
29
+ get halacious () {
30
+ return this._halacious
31
+ }
32
+
33
+ get request () {
34
+ return this._request
35
+ }
36
+
37
+ get requestPath () {
38
+ return this.request?.path
39
+ }
40
+
41
+ /**
42
+ * Creates a new hal representation out of a javascript object
43
+ * @param {{}=} entity the entity to wrap with a representation. an empty object is created by default
44
+ * @param {String || {}=} self the self href or link object. The request's path is used by default
45
+ * @param {Representation} root a pointer to the top level representation for adding curied links
46
+ * should be expanded into absolute urls
47
+ * @return {Representation}
48
+ */
49
+ create (entity = {}, self = this.getRequestPath(), root) {
50
+ const halacious = this.getHalacious()
51
+
52
+ const link = halacious.link(self)
53
+
54
+ return (
55
+ new Representation(this, entity, link, root)
56
+ )
57
+ }
58
+ }
59
+
60
+ export default RepresentationFactory
@@ -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
+ }