@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.
package/src/plugin.mjs CHANGED
@@ -1,59 +1,77 @@
1
+ /**
2
+ * @typedef {import('@modernpoacher/halacious').HalaciousTypes.Representation} Representation
3
+ */
4
+
1
5
  import joi from 'joi'
2
6
  import * as boom from '@hapi/boom'
3
7
  import * as hoek from '@hapi/hoek'
4
- import _ from 'lodash'
5
- import fs from 'node:fs'
8
+ import {
9
+ readdirSync,
10
+ readFileSync
11
+ } from 'node:fs'
6
12
  import path from 'node:path'
13
+ import util from 'node:util'
14
+ import url from 'node:url'
15
+ import _ from 'lodash'
7
16
  import pug from 'pug'
8
- import util from 'util'
9
17
  import async from 'async'
10
18
  import {
11
19
  marked
12
20
  } from 'marked'
13
- import url from 'url'
21
+ import debug from 'debug'
14
22
  import URI from 'urijs'
15
23
  import * as urlTemplate from 'url-template'
16
- import Negotiator from 'negotiator'
17
24
  import URITemplate from 'urijs/src/URITemplate.js'
18
- import IAM from '#where-am-i'
19
- import RepresentationFactory from './representation.mjs'
20
25
 
21
- const HAL_MIME_TYPE = 'application/hal+json'
22
-
23
- const REG = /{([^{}]+)}|([^{}]+)/g
24
-
25
- const PKG = JSON.parse(fs.readFileSync(path.join(IAM, './package.json')))
26
-
27
- function reach (object, path) {
28
- const parts = path ? path.split('.') : []
26
+ import PKG from '#package' assert { type: 'json' }
27
+ import IAM from '#where-am-i'
29
28
 
30
- for (let i = 0; i < parts.length && !_.isUndefined(object); i++) {
31
- object = object[parts[i]]
32
- }
29
+ import RepresentationFactory from './representation.mjs'
33
30
 
34
- return object
31
+ import {
32
+ getRepresentationEntity,
33
+ getRepresentationSelfHref,
34
+ getRepresentationRequest,
35
+ getRepresentationRequestPath,
36
+ getRouteSettingsPluginsHal,
37
+ getRouteSettingsPluginsHalApi,
38
+ getRouteSettingsPluginsHalQuery,
39
+ getRouteSettingsIsInternal,
40
+ getRequestRoute,
41
+ getRequestHeadersAccept,
42
+ getRequestServer,
43
+ getRequestResponse,
44
+ getRequestResponseSource,
45
+ getRequestResponseHeadersLocation,
46
+ getResponseVariety,
47
+ getResponseStatusCode,
48
+ isResponseStatusCodeInSuccessRange,
49
+ getPath,
50
+ hasPath,
51
+ getName,
52
+ hasName,
53
+ getPrefix,
54
+ getRels,
55
+ hasRels,
56
+ getAbsolute,
57
+ getQuery,
58
+ getIgnore,
59
+ getMediaType,
60
+ getTemplateContext,
61
+ isRelativePath
62
+ } from './utils.mjs'
63
+
64
+ function sortByName ({ name: alpha }, { name: omega }) {
65
+ return (
66
+ alpha.localeCompare(omega)
67
+ )
35
68
  }
36
69
 
37
- /**
38
- * evaluates and flattens deep expressions (e.g. '/{foo.a.b}') into a single level context object: {'foo.a.b': value}
39
- * so that it may be used by url-template library
40
- * @param template
41
- * @param ctx
42
- * @return {{}}
43
- */
44
- function flattenContext (template, ctx) {
45
- let arr
46
- const result = {}
70
+ const HAL_MIME_TYPE = 'application/hal+json'
47
71
 
48
- while ((arr = REG.exec(template)) !== null) {
49
- if (arr[1]) {
50
- const value = reach(ctx, arr[1])
51
- result[arr[1]] = value && value.toString()
52
- }
53
- }
72
+ const log = debug('@modernpoacher/halacious')
54
73
 
55
- return result
56
- }
74
+ log('`@modernpoacher/halacious` is awake')
57
75
 
58
76
  const optionsSchema = joi.object({
59
77
  absolute: joi.boolean().default(false),
@@ -80,268 +98,164 @@ const optionsSchema = joi.object({
80
98
  marked: joi.object().default({})
81
99
  })
82
100
 
83
- /**
84
- * Registers plugin routes and an "api" object with the hapi server.
85
- * @param server
86
- * @param opts
87
- * @param next
88
- */
89
- export const plugin = {
90
- pkg: PKG,
91
-
92
- async register (server, opts) {
93
- let settings = opts
94
-
95
- const { error, value } = optionsSchema.validate(opts)
96
-
97
- if (error) throw error
98
-
99
- settings = value
100
-
101
- marked.setOptions(settings.marked)
102
-
103
- const selection = settings.apiServerLabel
104
- ? server.select(settings.apiServerLabel)
105
- : server
106
-
107
- const internals = {}
108
-
109
- // for tracking down namespaces
110
- internals.byName = {}
111
- internals.byPrefix = {}
112
-
113
- // valid rel options
114
- internals.relSchema = joi.object({
115
- // the rel name, will default to file's basename if available
116
- name: joi.string().required(),
117
-
118
- // a path to the rel's documentation in html or markdown
119
- file: joi.string().optional(),
120
-
121
- // a short textual description
122
- description: joi.string().optional(),
123
-
124
- // returns the qualified name of the rel (including the namespace)
125
- qname: joi
126
- .function()
127
- .optional()
128
- .default(function () {
129
- return function () {
130
- return this.namespace
131
- ? util.format('%s:%s', this.namespace.prefix, this.name)
132
- : this.name
133
- }
134
- })
135
- }).unknown()
136
-
137
- // valid namespace options
138
- internals.nsSchema = joi.object({
139
- // the namespace name, will default to dir basename if available
140
- name: joi.string().required(),
141
-
142
- // a path to a directory containing rel descriptors. all rels will automatically be added
143
- dir: joi.string().optional(),
144
-
145
- // the namespace prefix for shorthand rel addressing (e.g. 'prefix:relname')
146
- prefix: joi.string().optional().default(joi.ref('name')),
147
-
148
- // a short description
149
- description: joi.string().optional(),
150
-
151
- // a map of rel objects, keyed by name
152
- rels: joi.object().optional(),
101
+ // valid rel options
102
+ const relSchema = joi.object({
103
+ // the rel name, will default to file's basename if available
104
+ name: joi.string().required(),
105
+
106
+ // a path to the rel's documentation in html or markdown
107
+ file: joi.string().optional(),
108
+
109
+ // a short textual description
110
+ description: joi.string().optional(),
111
+
112
+ // returns the qualified name of the rel (including the namespace)
113
+ qname: joi
114
+ .function()
115
+ .optional()
116
+ .default(function getQName () {
117
+ return function qname () {
118
+ return this.namespace
119
+ ? util.format('%s:%s', getPrefix(this.namespace), this.name)
120
+ : this.name
121
+ }
122
+ })
123
+ }).unknown()
153
124
 
154
- // validates and adds a rel to the namespace
155
- rel: joi
156
- .function()
157
- .optional()
158
- .default(function () {
159
- return function (rel) {
160
- this.rels = this.rels || {}
125
+ // valid namespace options
126
+ const namespaceSchema = joi.object({
127
+ // the namespace name, will default to dir basename if available
128
+ name: joi.string().required(),
161
129
 
162
- if (_.isString(rel)) rel = { name: rel }
130
+ // a path to a directory containing rel descriptors. all rels will automatically be added
131
+ dir: joi.string().optional(),
163
132
 
164
- rel.name =
165
- rel.name ||
166
- (rel.file && path.basename(rel.file, path.extname(rel.file)))
133
+ // the namespace prefix for shorthand rel addressing (e.g. 'prefix:relname')
134
+ prefix: joi.string().optional().default(joi.ref('name')),
167
135
 
168
- const { error, value } = internals.relSchema.validate(rel)
169
- if (error) throw error
136
+ // a short description
137
+ description: joi.string().optional(),
170
138
 
171
- rel = value
139
+ // a map of rel objects, keyed by name
140
+ rels: joi.object().optional(),
172
141
 
173
- this.rels[rel.name] = rel
174
- rel.namespace = this
175
- return this
176
- }
177
- }),
178
-
179
- // synchronously scans a directory for rel descriptors and adds them to the namespace
180
- scanDirectory: joi
181
- .function()
182
- .optional()
183
- .default(function () {
184
- return function (directory) {
185
- const files = fs.readdirSync(directory)
186
- files.forEach(function (file) {
187
- this.rel({ file: path.join(directory, file) })
188
- }, this)
189
-
190
- return this
191
- }
192
- })
193
- })
142
+ // validates and adds a rel to the namespace
143
+ rel: joi
144
+ .function()
145
+ .optional()
146
+ .default(function getRel () {
147
+ return function rel (rel) {
148
+ if (!hasRels(this)) this.rels = {}
194
149
 
195
- internals.filter = function (request) {
196
- return _.get(request.route.settings, 'plugins.hal', true)
197
- }
150
+ if (_.isString(rel)) rel = { name: rel }
198
151
 
199
- /**
200
- * Returns a list of all registered namespaces sorted by name
201
- * @return {*}
202
- */
203
- internals.namespaces = function () {
204
- return _.sortBy(_.values(internals.byName), 'name')
205
- }
152
+ if (!hasName(rel)) rel.name = (rel.file && path.basename(rel.file, path.extname(rel.file)))
206
153
 
207
- /**
208
- * Validates and adds a new namespace configuration
209
- * @param namespace the namespace config
210
- * @return {*} a new namespace object
211
- */
212
- internals.namespaces.add = function (namespace) {
213
- // if only dir is specified
214
- namespace.name =
215
- namespace.name || (namespace.dir && path.basename(namespace.dir))
154
+ const { error, value } = relSchema.validate(rel)
155
+ if (error) throw error
216
156
 
217
- // fail fast if the namespace isnt valid
218
- const { error, value } = internals.nsSchema.validate(namespace)
157
+ rel = value
219
158
 
220
- if (error) throw error
159
+ const relName = getName(rel)
160
+ const rels = getRels(this)
221
161
 
222
- namespace = value
162
+ Reflect.set(rels, relName, rel)
223
163
 
224
- // would prefer to initialize w/ joi but it keeps a static reference to the value for some reason
225
- namespace.rels = {}
164
+ rel.namespace = this
226
165
 
227
- if (namespace.dir) {
228
- namespace.scanDirectory(namespace.dir)
166
+ return this
229
167
  }
168
+ }),
169
+
170
+ // synchronously scans a directory for rel descriptors and adds them to the namespace
171
+ scanDirectory: joi
172
+ .function()
173
+ .optional()
174
+ .default(function getScanDirectory () {
175
+ return function scanDirectory (directory) {
176
+ const files = readdirSync(directory)
177
+
178
+ files
179
+ .forEach((file) => {
180
+ this.rel({ file: path.join(directory, file) })
181
+ })
230
182
 
231
- // index and return
232
- internals.byName[namespace.name] = namespace
233
- internals.byPrefix[namespace.prefix] = namespace
234
-
235
- return namespace
236
- }
183
+ return this
184
+ }
185
+ })
186
+ })
237
187
 
238
- /**
239
- * Removes one or all registered namespaces. Mainly used for testing
240
- * @param {String=} namespace the namespace to remove. a falsy value will remove all namespaces
241
- */
242
- internals.namespaces.remove = function (namespace) {
243
- let ns
188
+ // see http://tools.ietf.org/html/draft-kelly-json-hal-06#section-8.2
189
+ const linkSchema = joi.object({
190
+ href: joi.alternatives([joi.string(), joi.function()]).required(),
191
+ templated: joi.boolean().optional(),
192
+ title: joi.string().optional(),
193
+ type: joi.string().optional(),
194
+ deprecation: joi.string().optional(),
195
+ name: joi.string().optional(),
196
+ profile: joi.string().optional(),
197
+ hreflang: joi.string().optional()
198
+ })
244
199
 
245
- if (!namespace) {
246
- internals.byName = {}
247
- internals.byPrefix = {}
248
- } else {
249
- ns = internals.byName[namespace]
250
- if (ns) {
251
- delete internals.byName[namespace]
252
- delete internals.byPrefix[namespace.prefix]
253
- }
254
- }
200
+ /**
201
+ * Wraps callback functions
202
+ *
203
+ * @param {(err: Error | null, any) => void} done
204
+ * @return {(err: Error | null, any) => void}
205
+ */
206
+ function wrap (done, representation) {
207
+ return function wrap (err, result) {
208
+ if (err instanceof Error) {
209
+ done(err)
210
+ } else {
211
+ done(null, result || representation)
255
212
  }
213
+ }
214
+ }
256
215
 
257
- /**
258
- * Looks up a specific namespace
259
- * @param namespace
260
- * @return {*}
261
- */
262
- internals.namespace = function (namespace) {
263
- return internals.byName[namespace]
264
- }
216
+ /**
217
+ * Registers plugin routes and an "api" object with the hapi server
218
+ *
219
+ * @param server
220
+ * @param opts
221
+ * @param {(err: Error | null, any) => void} done
222
+ */
223
+ export const plugin = {
224
+ pkg: PKG,
265
225
 
266
- /**
267
- * Sorts and returns all rels by namespace
268
- * @return {*}
269
- */
270
- internals.rels = function () {
271
- let rels = []
272
- _.values(internals.byName).forEach((ns) => {
273
- rels = rels.concat(_.values(ns.rels) || [])
274
- })
275
- return _.sortBy(rels, 'name')
276
- }
226
+ async register (server, opts) {
227
+ const { error, value: settings } = optionsSchema.validate(opts)
277
228
 
278
- /**
279
- * Adds a new rel configuration to a namespace
280
- * @param {String} namespace the namespace name
281
- * @param rel the rel configuration
282
- * @return the new rel
283
- */
284
- internals.rels.add = function (namespace, rel) {
285
- const ns = internals.byName[namespace]
286
- if (!ns) throw new Error(`Invalid namespace ${namespace}`)
287
- ns.rel(rel)
288
- return ns.rels[rel.name]
289
- }
229
+ if (error) throw error
290
230
 
291
- /**
292
- * Looks up a rel under a given namespace
293
- * @param {String} namespace the namespace name
294
- * @param {String} name the rel name
295
- * @return {*} the rel or undefined if not found
296
- */
297
- internals.rel = function (namespace, name) {
298
- let parts, ns, rel
231
+ marked.setOptions(settings.marked)
299
232
 
300
- if (!name) {
301
- // for shorthand namespace:rel notation
302
- if (namespace.indexOf(':') > 0) {
303
- parts = namespace.split(':')
304
- ns = internals.byPrefix[parts[0]]
305
- name = parts[1]
306
- }
307
- } else {
308
- ns = internals.byName[namespace]
309
- }
233
+ const selection = settings.apiServerLabel
234
+ ? server.select(settings.apiServerLabel)
235
+ : server
310
236
 
311
- // namespace is valid, check for rel
312
- if (ns) {
313
- if (ns.rels[name]) {
314
- // rel has been defined
315
- rel = ns.rels[name]
316
- } else if (!settings.strict) {
317
- // lazily create the rel
318
- ns.rel({ name })
319
- rel = ns.rels[name]
320
- } else {
321
- // could be a typo, fail fast to let the developer know
322
- throw new Error(`No such rel: "${namespace}"`)
323
- }
324
- } else {
325
- // could be globally qualified (e.g. 'self')
326
- const { error, value } = internals.relSchema.validate({ name: namespace })
327
- if (error) throw error
237
+ const byName = new Map()
238
+ const byPrefix = new Map()
328
239
 
329
- rel = value
330
- }
240
+ // keeps found routes in a cache
241
+ const routeCache = new Map()
331
242
 
332
- return rel
243
+ const internals = {
244
+ byName,
245
+ byPrefix
333
246
  }
334
247
 
335
248
  /**
336
249
  * Route handler for /rels
250
+ *
337
251
  * @type {{handler: handler}}
338
252
  */
339
- internals.namespacesRoute = function (relsAuth) {
253
+ function getRelsRouteConfig (auth) {
340
254
  return {
341
- auth: relsAuth,
342
- handler (req, h) {
255
+ auth,
256
+ handler ({ path }, h) {
343
257
  return h.view('namespaces', {
344
- path: req.path,
258
+ path,
345
259
  namespaces: internals.namespaces()
346
260
  })
347
261
  }
@@ -350,432 +264,322 @@ export const plugin = {
350
264
 
351
265
  /**
352
266
  * Route handler for /rels/{namespace}/{rel}
267
+ *
353
268
  * @type {{handler: handler}}
354
269
  */
355
- internals.relRoute = function (relsAuth) {
270
+ function getRelRouteConfig (auth) {
356
271
  return {
357
- auth: relsAuth,
358
- handler (req, h) {
359
- const rel = internals.rel(req.params.namespace, req.params.rel)
272
+ auth,
273
+ handler ({ params: { namespace: namespaceName, rel: relName } }, h) {
274
+ const rel = internals.rel(namespaceName, relName)
275
+
360
276
  if (!rel) {
361
- console.warn('rel not found!')
277
+ log(`Invalid rel "${namespaceName}" ("${relName}")`)
362
278
  throw boom.notFound()
363
279
  }
364
280
 
365
- if (rel.file) {
366
- const data = fs.readFileSync(rel.file)
281
+ const {
282
+ file
283
+ } = rel
284
+
285
+ if (file) {
286
+ const buffer = readFileSync(file)
287
+
367
288
  if (settings.relsTemplate) {
368
289
  return h.view('rel', {
369
290
  rel,
370
- relData: marked(data.toString())
291
+ relData: marked(buffer.toString())
371
292
  })
372
293
  }
373
- return marked(data.toString())
374
- }
375
- return h.view('rel', { rel })
376
- }
377
- }
378
- }
379
-
380
- // see http://tools.ietf.org/html/draft-kelly-json-hal-06#section-8.2
381
- internals.linkSchema = joi.object({
382
- href: joi.alternatives([joi.string(), joi.function()]).required(),
383
- templated: joi.boolean().optional(),
384
- title: joi.string().optional(),
385
- type: joi.string().optional(),
386
- deprecation: joi.string().optional(),
387
- name: joi.string().optional(),
388
- profile: joi.string().optional(),
389
- hreflang: joi.string().optional()
390
- })
391
-
392
- internals.isRelativePath = function (path) {
393
- return (
394
- path &&
395
- (path.substring(0, 2) === './' || path.substring(0, 3) === '../')
396
- )
397
- }
398
294
 
399
- /**
400
- * Resolves a name
401
- * @param link
402
- * @param relativeTo
403
- */
404
- internals.link = function (link, relativeTo) {
405
- relativeTo = relativeTo && relativeTo.split('?')[0]
406
- link =
407
- _.isFunction(link) || _.isString(link)
408
- ? { href: link }
409
- : hoek.clone(link)
410
- const { error, value } = internals.linkSchema.validate(link)
411
-
412
- if (error) throw error
413
- link = value
295
+ return marked(buffer.toString())
296
+ }
414
297
 
415
- if (
416
- relativeTo &&
417
- (internals.isRelativePath(link.href) || settings.absolute)
418
- ) {
419
- try {
420
- link.href = new URI(link.href)
421
- .absoluteTo(`${relativeTo}/`)
422
- .toString()
423
- } catch (e) {
424
- console.warn(e)
425
- return link
298
+ return h.view('rel', { rel })
426
299
  }
427
300
  }
428
- return link
429
301
  }
430
302
 
431
- // keeps found routes in a cache
432
- internals.routeCache = {}
433
-
434
303
  /**
435
304
  * Locates a named route. This feature may not belong here
305
+ *
436
306
  * @param routeName
437
307
  * @return {*}
438
308
  */
439
- internals.locateRoute = function (routeName) {
440
- if (internals.routeCache[routeName]) {
441
- return internals.routeCache[routeName].path
309
+ function getRouteFromRouteCache (routeName) {
310
+ if (!routeCache.has(routeName)) {
311
+ routeCache.set(routeName, (
312
+ server
313
+ .table()
314
+ .find((route) => (
315
+ getName(getRouteSettingsPluginsHal(route)) === routeName
316
+ ))
317
+ ))
442
318
  }
443
319
 
444
- const routes = server.table()
445
-
446
- routes.forEach((route) => {
447
- if (
448
- route.settings.plugins.hal &&
449
- route.settings.plugins.hal.name === routeName
450
- ) {
451
- internals.routeCache[routeName] = route
452
- }
453
- })
454
-
455
- return internals.routeCache[routeName]
456
- }
457
-
458
- /**
459
- * Locates a named route and expands templated parameters
460
- * @param routeId
461
- * @param params
462
- * @return String the expanded path to the named route
463
- */
464
- internals.route = function (routeId, params) {
465
- const route = server.lookup(routeId) || internals.locateRoute(routeId)
466
- if (!route) throw new Error(`No route found with id or name ${routeId}`)
467
- const href = _.template(route.path, {
468
- interpolate: /{([\s\S]+?)(?:\?|\*\d*)??}/g
469
- })(
470
- _.mapValues(params, (val) =>
471
- typeof val !== 'object' ? encodeURIComponent(val) : val
472
- )
320
+ return (
321
+ routeCache.get(routeName)
473
322
  )
474
- const query = hoek.reach(route.settings, 'plugins.hal.query')
475
- return query ? href + query : href
476
- }
477
-
478
- /**
479
- * Returns the documentation link to a namespace
480
- * @param request
481
- * @param namespace
482
- * @return {*}
483
- */
484
- internals.namespaceUrl = function (request, namespace) {
485
- const path = [settings.relsPath, namespace.name].join('/')
486
-
487
- if (settings.absolute) {
488
- return internals.buildUrl(request, path)
489
- }
490
-
491
- return path
492
323
  }
493
324
 
494
325
  /**
495
326
  * Configures a representation with parameters specified by a hapi route config. The configuration object may
496
327
  * include 'links', 'embedded', and 'prepare' properties.
497
- * @param {Representation} rep the representation to configure
498
- * @param {{}} config the config object
499
- * @param callback
328
+ *
329
+ * @param {Record<string, any>} config the config object
330
+ * @param {Representation} representation the representation to configure
331
+ * @param {(err: Error | null, any) => void} done
500
332
  */
501
- internals.configureRepresentation = function configureRepresentation (
502
- rep,
333
+ function configureRepresentation (
503
334
  config,
504
- callback
335
+ representation,
336
+ done
505
337
  ) {
506
- const resolveHref = function (href, ctx) {
338
+ function getHref (href, context) {
507
339
  return _.isFunction(href)
508
- ? href(rep, ctx)
509
- : urlTemplate.parseTemplate(href).expand(flattenContext(href, ctx))
340
+ ? href(representation, context)
341
+ : urlTemplate.parseTemplate(href).expand(getTemplateContext(href, context))
510
342
  }
511
343
 
512
- try {
513
- const { entity } = rep
344
+ /**
345
+ * @param {Record<string, any>} config the config object
346
+ * @param {Representation} representation the representation to configure
347
+ * @param {(err: Error | null, any) => void} done
348
+ */
349
+ function prepareEntity (representation, done) {
350
+ const { prepare } = config
351
+
352
+ if (_.isFunction(prepare)) {
353
+ prepare(representation, wrap(done, representation))
354
+ } else {
355
+ done(null, representation)
356
+ }
357
+ }
514
358
 
359
+ /**
360
+ * @param {Record<string, any>} config the config object
361
+ * @param {Representation} representation the representation to configure
362
+ * @param {(err: Error | null, any) => void} done
363
+ */
364
+ function cascadeEntity (config, representation, done) {
365
+ representation.ignore(getIgnore(config))
366
+
367
+ // cascade the async config functions
368
+ prepareEntity(representation, done)
369
+ }
370
+
371
+ try {
515
372
  // shorthand prepare function
516
373
  if (_.isFunction(config)) config = { prepare: config }
517
374
 
518
- // configure links
519
- _.forEach(config.links, (link, rel) => {
520
- const repLink = internals.link(link, rep.self.href)
521
- repLink.href = resolveHref(repLink.href, entity)
522
- rep.link(rel, repLink)
375
+ if (Reflect.has(config, 'links')) {
376
+ const links = Reflect.get(config, 'links')
523
377
 
524
- // grab query options
525
- if (config.query) {
526
- repLink.href += config.query
527
- }
528
- })
378
+ const entity = getRepresentationEntity(representation)
529
379
 
530
- /**
531
- * Wraps callback functions to support next(rep) instead of next(null, rep)
532
- * @param callback
533
- * @return {Function}
534
- */
535
- const wrap = function (callback) {
536
- return function (err, result) {
537
- if (err instanceof Error) {
538
- callback(err)
539
- } else {
540
- callback(null, result || rep)
541
- }
542
- }
543
- }
380
+ Object.entries(links)
381
+ .forEach(([rel, link]) => {
382
+ const representationLink = internals.link(link, getRepresentationSelfHref(representation))
383
+ representationLink.href = getHref(representationLink.href, entity)
384
+ representation.link(rel, representationLink)
544
385
 
545
- /**
546
- * Looks for an asynchronous prepare method for programmatic configuration of the outbound hal entity. As with
547
- * toHal(), the prepare method can modify the existing rep or create an entirely new one.
548
- * @param rep
549
- * @param callback
550
- */
551
- const prepareEntity = function (rep, callback) {
552
- if (_.isFunction(config.prepare)) {
553
- config.prepare(rep, wrap(callback))
554
- } else {
555
- callback(null, rep)
556
- }
386
+ // grab query options
387
+ if (config.query) {
388
+ representationLink.href += config.query
389
+ }
390
+ })
557
391
  }
558
392
 
559
- // configure embedded declarations. each rel entry is also a representation config object
560
- async.each(
561
- Object.keys(config.embedded || {}),
562
- (rel, cb) => {
563
- const embed = config.embedded[rel]
393
+ if (Reflect.has(config, 'embedded')) {
394
+ // configure embedded declarations. each rel entry is also a representation config object
395
+ const embedded = Reflect.get(config, 'embedded')
564
396
 
565
- // assume that arrays should be embedded as a collection
566
- if (!embed.path) {
567
- throw new Error(
568
- `Error in route ${rep.request.path}: "embedded" route configuration property requires a path`
569
- )
570
- }
571
- let embedded = hoek.reach(entity, embed.path)
572
- if (!embedded) return cb()
397
+ const entity = getRepresentationEntity(representation)
573
398
 
574
- // force the embed array to be inialized. no self rel is necessary
575
- if (_.isArray(embedded)) rep.embed(rel, null, [])
399
+ async.each(
400
+ Object.entries(embedded),
401
+ ([rel, route], done) => {
402
+ // assume that arrays should be embedded as a collection
403
+ if (!hasPath(route)) {
404
+ throw new Error(
405
+ `Invalid route "${getRepresentationRequestPath(representation)}": "embedded" route configuration property requires a path`
406
+ )
407
+ }
576
408
 
577
- // force into an array for iterating
578
- embedded = [].concat(embedded)
409
+ let embedded = hoek.reach(entity, getPath(route))
579
410
 
580
- // embedded reps probably also shouldnt appear in the object payload
581
- rep.ignore(embed.path)
411
+ if (!embedded) return done()
582
412
 
583
- async.each(
584
- embedded,
585
- (item, acb) => {
586
- const link = internals.link(
587
- resolveHref(embed.href, { self: entity, item }),
588
- rep.self.href
589
- )
413
+ // force the embed array to be inialized. no self rel is necessary
414
+ if (_.isArray(embedded)) representation.embed(rel, null, [])
590
415
 
591
- // create the embedded representation from the possibly templated href
592
- let embeddedRep = rep.embed(rel, link, item)
593
-
594
- embeddedRep = _.isArray(embeddedRep)
595
- ? embeddedRep
596
- : [embeddedRep]
597
- // recursively process its links/embedded declarations
598
- async.each(
599
- embeddedRep,
600
- (e, bcb) => {
601
- internals.halifyAndConfigure(e, embed, bcb)
602
- },
603
- acb
604
- )
605
- },
606
- cb
607
- )
608
- },
609
- (err) => {
610
- if (err) return callback(err)
416
+ // force into an array for iterating
417
+ embedded = [].concat(embedded)
611
418
 
612
- rep.ignore(config.ignore)
419
+ // embedded reps probably also shouldnt appear in the object payload
420
+ representation.ignore(getPath(route))
613
421
 
614
- // cascade the async config functions
615
- prepareEntity(rep, callback)
616
- }
617
- )
618
- } catch (e) {
619
- callback(e)
620
- }
621
- }
422
+ async.each(
423
+ embedded,
424
+ (item, done) => {
425
+ const link = internals.link(
426
+ getHref(route.href, { self: entity, item }),
427
+ getRepresentationSelfHref(representation)
428
+ )
622
429
 
623
- internals.halifyAndConfigure = function (rep, config, callback) {
624
- /**
625
- * Wraps callback functions to support next(rep) instead of next(null, rep)
626
- * @param callback
627
- * @return {Function}
628
- */
629
- const wrap = function (callback) {
630
- return function (err, result) {
631
- if (err instanceof Error) {
632
- callback(err)
633
- } else {
634
- callback(null, result || rep)
635
- }
430
+ // create the embedded representation from the possibly templated href
431
+ let embedded = representation.embed(rel, link, item)
432
+
433
+ // force into an array for iterating
434
+ embedded = [].concat(embedded)
435
+
436
+ // recursively process its links/embedded declarations
437
+ async.each(
438
+ embedded,
439
+ (representation, done) => {
440
+ transformRepresentationEntity(route, representation, done)
441
+ },
442
+ done
443
+ )
444
+ },
445
+ done
446
+ )
447
+ },
448
+ (err) => {
449
+ if (err) return done(err)
450
+
451
+ return (
452
+ cascadeEntity(config, representation, done)
453
+ )
454
+ }
455
+ )
456
+ } else {
457
+ return (
458
+ cascadeEntity(config, representation, done)
459
+ )
636
460
  }
461
+ } catch (e) {
462
+ done(e)
637
463
  }
464
+ }
638
465
 
466
+ function transformRepresentationEntity (
467
+ config,
468
+ representation,
469
+ done
470
+ ) {
639
471
  /**
640
- * Looks for a toHal(representation, next) method on the entity. If found, it is called asynchronously. The method may modify the
641
- * representation or pass back a completely new representation by calling next(newRep)
642
- * @param callback
472
+ * Looks for a toHal(representation, done) method on the entity. If found, it is called asynchronously. The method may modify the
473
+ * representation or pass back a completely new representation by calling done(newRep)
474
+ *
475
+ * @param {(err: Error | null, any) => void} done
643
476
  */
644
- const convertEntity = function (callback) {
645
- if (_.isFunction(rep.entity.toHal)) {
646
- rep.entity.toHal(rep, wrap(callback))
477
+ function transformEntity (done) {
478
+ const { toHal } = getRepresentationEntity(representation)
479
+
480
+ if (_.isFunction(toHal)) {
481
+ toHal(representation, wrap(done, representation))
647
482
  } else {
648
- callback(null, rep)
483
+ done(null, representation)
649
484
  }
650
485
  }
651
486
 
652
- const configureEntity = function (rep, callback) {
653
- internals.configureRepresentation(rep, config, callback)
654
- }
655
-
656
- async.waterfall([convertEntity, configureEntity], callback)
657
- }
658
-
659
- /**
660
- * Selects the media type based on the request's Accept header and a ranked ordering of configured
661
- * media types.
662
- * @param mediaTypes
663
- * @param request
664
- * @return {*}
665
- */
666
- internals.getMediaType = function (mediaTypes, request) {
667
- return new Negotiator(request).mediaType(
668
- _.isArray(mediaTypes) ? mediaTypes : [mediaTypes]
487
+ async.waterfall(
488
+ [
489
+ transformEntity,
490
+ (representation, done) => {
491
+ configureRepresentation(config, representation, done)
492
+ }
493
+ ],
494
+ done
669
495
  )
670
496
  }
671
497
 
672
498
  /**
673
- * Expands the url path to include protocol://server:port
674
- * @param request
675
- * @param path
676
- * @param search
677
- * @return {*}
678
- */
679
- internals.buildUrl = function (request, path, search) {
680
- return url.format({
681
- host: settings.host || request.headers.host,
682
- hostname: settings.hostname || request.info.host,
683
- port: settings.port || request.info.port,
684
- pathname: path,
685
- protocol: settings.protocol || request.info.protocol || 'http',
686
- search
687
- })
688
- }
689
-
690
- /**
691
- * Expands the query string template, if present, using query parameter values in the request.
499
+ * Expands the query string template, if present, using query parameter values in the request
500
+ *
692
501
  * @param request
693
502
  * @param queryTemplate
694
- * @param { boolean } absolute whether the link should be expanded to include the server
503
+ * @param {boolean} isAbsolute whether the link should be expanded to include the server
695
504
  * @return {*}
696
505
  */
697
- internals.getRequestPath = function (request, queryTemplate, absolute) {
698
- let uriTemplate
699
-
700
- const path = absolute
701
- ? internals.buildUrl(request, request.path)
702
- : request.path
506
+ function getRequestPath (request, queryTemplate, isAbsolute) {
507
+ const path = isAbsolute
508
+ ? internals.buildUrl(request, getPath(request))
509
+ : getPath(request)
703
510
 
704
511
  if (queryTemplate) {
705
- uriTemplate = new URITemplate(path + queryTemplate)
706
- return uriTemplate.expand(request.query)
512
+ const uriTemplate = new URITemplate(path + queryTemplate)
513
+ return (
514
+ uriTemplate.expand(request.query)
515
+ )
707
516
  }
517
+
708
518
  return path
709
519
  }
710
520
 
711
521
  /**
712
- * Resolves a relative url. Borrowed from hapi
522
+ * Resolves to a relative URL
523
+ *
713
524
  * @param request
714
- * @param uri
715
- * @param absolute
525
+ * @param {string} uri
526
+ * @param {boolean} isAbsolute
716
527
  * @return {*}
717
528
  */
718
- internals.location = function (request, uri, absolute) {
719
- const isAbsolute = uri.match(/^\w+:\/\//)
529
+ function getRelativeUrl (request, uri, isAbsolute) {
530
+ let relativeUrl = null
531
+ let queryParams = null
720
532
 
721
- let path = isAbsolute ? uri : (uri.charAt(0) === '/' ? '' : '/') + uri
722
- let search = null
723
-
724
- if (isAbsolute) {
725
- path = uri
533
+ if (uri.match(/^\w+:\/\//)) {
534
+ relativeUrl = uri
726
535
  } else {
727
- const parts = uri.split('?')
728
- path = (parts[0].charAt(0) === '/' ? '' : '/') + parts[0]
729
- if (parts.length > 1) {
730
- // eslint-disable-next-line prefer-destructuring
731
- search = parts[1]
732
- }
733
- }
536
+ const [
537
+ route = '',
538
+ query = null
539
+ ] = uri.split('?')
734
540
 
735
- if (absolute) {
736
- path = internals.buildUrl(request, path, search)
541
+ relativeUrl = route.startsWith('/') ? route : '/' + route
542
+ queryParams = query || queryParams
737
543
  }
738
- return path
739
- }
740
544
 
741
- internals.successfulResponseCode = function (statusCode) {
742
- return statusCode >= 200 && statusCode < 300
743
- }
545
+ if (isAbsolute) {
546
+ relativeUrl = internals.buildUrl(request, relativeUrl, queryParams)
547
+ }
744
548
 
745
- internals.isSourceEligible = function (source) {
746
- return _.isObject(source) && !_.isArray(source)
549
+ return relativeUrl
747
550
  }
748
551
 
749
- internals.isAcceptHeaderValid = function (request) {
750
- const accept = request.headers.accept || ''
751
-
552
+ function isAcceptHeaderValid (request) {
752
553
  return (
753
554
  !settings.requireHalJsonAcceptHeader ||
754
- accept.toLowerCase().indexOf(HAL_MIME_TYPE) >= 0
555
+ (getRequestHeadersAccept(request) ?? '').toLowerCase().includes(HAL_MIME_TYPE)
755
556
  )
756
557
  }
757
558
 
758
- internals.isRequestEligible = function (request) {
759
- // hapi 9/10 routes can be marked internal only
559
+ function isSourceEligible (source) {
560
+ return _.isObject(source) && !_.isArray(source)
561
+ }
562
+
563
+ function isRequestEligible (request) {
760
564
  return (
761
- !request.route.settings.isInternal &&
762
- internals.isAcceptHeaderValid(request) &&
565
+ !getRouteSettingsIsInternal(getRequestRoute(request)) && // hapi 9/10 routes can be marked internal only
566
+ isAcceptHeaderValid(request) &&
763
567
  internals.filter(request)
764
568
  )
765
569
  }
766
570
 
767
- internals.isResponseEligible = function (response) {
571
+ function isResponseEligible (response) {
768
572
  return (
769
- response.variety === 'plain' &&
770
- internals.successfulResponseCode(response.statusCode)
573
+ getResponseVariety(response) === 'plain' &&
574
+ isResponseStatusCodeInSuccessRange(getResponseStatusCode(response))
771
575
  )
772
576
  }
773
577
 
774
- internals.shouldHalify = function (request) {
578
+ function isEligible (request) {
775
579
  return (
776
- internals.isRequestEligible(request) &&
777
- internals.isResponseEligible(request.response) &&
778
- internals.isSourceEligible(request.response.source)
580
+ isRequestEligible(request) &&
581
+ isResponseEligible(getRequestResponse(request)) &&
582
+ isSourceEligible(getRequestResponseSource(request))
779
583
  )
780
584
  }
781
585
 
@@ -784,85 +588,367 @@ export const plugin = {
784
588
  * the plugins.hal.api configuration settings. This function is exported for convenience if the developer wishes to
785
589
  * define his or her own api handler in order to include metadata in the payload
786
590
  *
787
- * @param absolute
788
- * @param rep
789
- * @param next
591
+ * @param {boolean} isAbsolute
592
+ * @param {Representation} representation
593
+ * @param {(err: Error | null, any) => void} done
790
594
  */
791
- internals.apiLinker = function (absolute, rep, next) {
595
+ function toHal (isAbsolute, representation, done) {
596
+ const request = getRepresentationRequest(representation)
597
+
792
598
  // grab the routing table and iterate
793
- const req = rep.request
599
+ const server = getRequestServer(request)
794
600
 
795
- const routes = req.server.table()
601
+ server.table()
602
+ .forEach((route) => {
603
+ const api = getRouteSettingsPluginsHalApi(route)
796
604
 
797
- for (let i = 0; i < routes.length; i++) {
798
- const route = routes[i]
605
+ if (api) {
606
+ let {
607
+ path: uri = ''
608
+ } = route
799
609
 
800
- const halConfig = route.settings.plugins.hal || {}
610
+ if (isAbsolute) {
611
+ uri = internals.buildUrl(request, uri)
612
+ }
801
613
 
802
- if (halConfig.api) {
803
- const rel = halConfig.api
804
- let href = routes[i].path
614
+ const query = getRouteSettingsPluginsHalQuery(route)
805
615
 
806
- if (absolute) {
807
- href = internals.buildUrl(rep.request, href)
808
- }
616
+ // grab query options
617
+ if (query) {
618
+ uri += query
619
+ }
809
620
 
810
- // grab query options
811
- if (halConfig.query) {
812
- href += halConfig.query
621
+ representation.link(api, uri)
813
622
  }
623
+ })
814
624
 
815
- rep.link(rel, href)
816
- }
817
- }
818
- next()
625
+ done()
819
626
  }
820
627
 
821
628
  /**
822
629
  * Creates an auto api route configuration
823
- * @param absolute
824
- * @param apiAuth
825
- * @return {{auth: *, handler: handler, plugins: {hal: apiLinker}}}
630
+ *
631
+ * @param auth
632
+ * @param {boolean} isAbsolute
633
+ * @return {{auth: *, handler: handler, plugins: {hal: toHal}}}
826
634
  */
827
- internals.apiRouteConfig = function (absolute, apiAuth) {
635
+ function getApiRouteConfig (auth, isAbsolute) {
828
636
  return {
829
- auth: apiAuth,
637
+ auth,
830
638
  handler (req, h) {
831
639
  return h.response({}).type(HAL_MIME_TYPE)
832
640
  },
833
641
  plugins: {
834
- hal: internals.apiLinker.bind(null, absolute)
642
+ hal: toHal.bind(null, isAbsolute)
835
643
  }
836
644
  }
837
645
  }
838
646
 
839
647
  /**
840
648
  * Creates a redirector to redirect the browser from /api to /api/
841
- * @param apiUrl
842
- * @param apiAuth
649
+ *
650
+ * @param auth
651
+ * @param {string} url
843
652
  * @return {{auth: *, handler: handler}}
844
653
  */
845
- internals.apiRedirectConfig = function (apiUrl, apiAuth) {
654
+ function getApiRedirectConfig (auth, url) {
846
655
  return {
847
- auth: apiAuth,
656
+ auth,
848
657
  handler (req, h) {
849
- return h.redirect(`${apiUrl}/`)
658
+ return h.redirect(url.concat('/'))
659
+ }
660
+ }
661
+ }
662
+
663
+ /**
664
+ * Returns namespaces sorted by name
665
+ *
666
+ * @return {*}
667
+ */
668
+ internals.namespaces = function namespaces () {
669
+ return (
670
+ Array
671
+ .from(byName.values())
672
+ .sort(sortByName)
673
+ )
674
+ }
675
+
676
+ /**
677
+ * Validates and adds a new namespace configuration
678
+ *
679
+ * @param namespace the namespace config
680
+ * @return {*} a new namespace object
681
+ */
682
+ internals.namespaces.add = function add (namespace) {
683
+ // if only dir is specified
684
+ if (!hasName(namespace)) namespace.name = (namespace.dir && path.basename(namespace.dir))
685
+
686
+ // fail fast if the namespace isnt valid
687
+ const { error, value } = namespaceSchema.validate(namespace)
688
+
689
+ if (error) throw error
690
+
691
+ namespace = value
692
+
693
+ // would prefer to initialize w/ joi but it keeps a static reference to the value for some reason
694
+ namespace.rels = {}
695
+
696
+ if (namespace.dir) {
697
+ namespace.scanDirectory(namespace.dir)
698
+ }
699
+
700
+ // index and return
701
+ const name = getName(namespace)
702
+ const prefix = getPrefix(namespace)
703
+
704
+ byName
705
+ .set(name, namespace)
706
+ byPrefix
707
+ .set(prefix, namespace)
708
+
709
+ return namespace
710
+ }
711
+
712
+ /**
713
+ * Removes one or all registered namespaces. Mainly used for testing
714
+ *
715
+ * @param {string} name the namespace to remove. a falsy value will remove all namespaces
716
+ */
717
+ internals.namespaces.remove = function remove (name) {
718
+ if (!name) {
719
+ byName
720
+ .clear()
721
+ byPrefix
722
+ .clear()
723
+ } else {
724
+ if (byName.has(name)) {
725
+ const namespace = byName.get(name)
726
+
727
+ const prefix = getPrefix(namespace)
728
+
729
+ byName
730
+ .delete(name)
731
+ byPrefix
732
+ .delete(prefix)
733
+ }
734
+ }
735
+ }
736
+
737
+ /**
738
+ * Looks up a specific namespace
739
+ *
740
+ * @param namespace
741
+ * @return {*}
742
+ */
743
+ internals.namespace = function namespace (name) {
744
+ return (
745
+ byName.get(name)
746
+ )
747
+ }
748
+
749
+ /**
750
+ * Returns namespace rels sorted by name
751
+ *
752
+ * @return {*}
753
+ */
754
+ internals.rels = function rels () {
755
+ return (
756
+ Array
757
+ .from(byName.values())
758
+ .reduce((accumulator, { rels = {} }) => accumulator.concat(Object.values(rels)), [])
759
+ .sort(sortByName)
760
+ )
761
+ }
762
+
763
+ /**
764
+ * Adds a new rel configuration to a namespace
765
+ *
766
+ * @param {string} name the namespace name
767
+ * @param rel the rel configuration
768
+ * @return the new rel
769
+ */
770
+ internals.rels.add = function add (name, rel) {
771
+ if (!byName.has(name)) throw new Error(`Invalid namespace "${name}"`)
772
+
773
+ const namespace = byName.get(name)
774
+
775
+ namespace.rel(rel)
776
+
777
+ const relName = getName(rel)
778
+ const rels = getRels(namespace)
779
+
780
+ return (
781
+ Reflect.get(rels, relName)
782
+ )
783
+ }
784
+
785
+ /**
786
+ * Looks up a rel under a given namespace
787
+ * @param {string} namespaceName the namespace name
788
+ * @param {string} relName the rel name
789
+ * @return {*} the rel or undefined if not found
790
+ */
791
+ internals.rel = function rel (namespaceName, relName) {
792
+ if (!namespaceName) throw new Error(`Invalid rel "${namespaceName}"`)
793
+
794
+ let namespace
795
+ let rel
796
+
797
+ if (!relName) {
798
+ // for shorthand namespace:rel notation
799
+ if (namespaceName.includes(':')) {
800
+ const [prefix, name] = namespaceName.split(':')
801
+ namespace = byPrefix.get(prefix)
802
+ relName = name
803
+ }
804
+ } else {
805
+ namespace = byName.get(namespaceName)
806
+ }
807
+
808
+ // namespace is valid, check for rel
809
+ if (namespace) {
810
+ const rels = getRels(namespace)
811
+
812
+ if (!Reflect.has(rels, relName)) {
813
+ if (!settings.strict) {
814
+ // lazily create the rel
815
+ namespace.rel({ name: relName })
816
+ } else {
817
+ // could be a typo, fail fast to let the developer know
818
+ throw new Error(`Invalid rel "${namespaceName}" ("${relName}")`)
819
+ }
820
+ }
821
+
822
+ rel = Reflect.get(rels, relName)
823
+ } else {
824
+ // could be globally qualified (e.g. 'self')
825
+ const { error, value } = relSchema.validate({ name: namespaceName })
826
+ if (error) throw error
827
+
828
+ rel = value
829
+ }
830
+
831
+ return rel
832
+ }
833
+
834
+ /**
835
+ * Resolves a name
836
+ *
837
+ * @param link
838
+ * @param relativeTo
839
+ */
840
+ internals.link = function link (link, relativeTo) {
841
+ link = (
842
+ _.isFunction(link) || _.isString(link)
843
+ ? { href: link }
844
+ : hoek.clone(link)
845
+ )
846
+
847
+ const { error, value } = linkSchema.validate(link)
848
+
849
+ if (error) throw error
850
+
851
+ link = value
852
+
853
+ relativeTo = (relativeTo ?? '').split('?').shift().trim()
854
+
855
+ if (
856
+ relativeTo &&
857
+ (isRelativePath(link.href) || settings.absolute)
858
+ ) {
859
+ try {
860
+ link.href = (
861
+ new URI(link.href)
862
+ .absoluteTo(relativeTo.concat('/'))
863
+ .toString()
864
+ )
865
+ } catch (e) {
866
+ log(`Invalid URL "${link.href}"`)
867
+ return link
850
868
  }
851
869
  }
870
+
871
+ return link
872
+ }
873
+
874
+ /**
875
+ * Locates a named route and expands templated parameters
876
+ *
877
+ * @param routeName
878
+ * @param params
879
+ * @return String the expanded path to the named route
880
+ */
881
+ internals.route = function route (routeName, params) {
882
+ const route = server.lookup(routeName) || getRouteFromRouteCache(routeName)
883
+ if (!route) throw new Error(`Invalid route "${routeName}"`)
884
+
885
+ const i = /{([\s\S]+?)(?:\?|\*\d*)??}/g
886
+ const c = Object.fromEntries(Object.entries(params).map(([key, value]) => [key, typeof value !== 'object' ? encodeURIComponent(value) : value]))
887
+
888
+ const href = _.template(getPath(route), { interpolate: i })(c)
889
+
890
+ const query = getRouteSettingsPluginsHalQuery(route) // hoek.reach(route.settings, 'plugins.hal.query')
891
+
892
+ if (query) {
893
+ return href + query
894
+ }
895
+
896
+ return href
897
+ }
898
+
899
+ /**
900
+ * Returns the documentation link to a namespace
901
+ *
902
+ * @param request
903
+ * @param namespace
904
+ * @return {*}
905
+ */
906
+ internals.namespaceUrl = function namespaceUrl (request, namespace) {
907
+ const path = [settings.relsPath, getName(namespace)].join('/')
908
+
909
+ if (settings.absolute) {
910
+ return internals.buildUrl(request, path)
911
+ }
912
+
913
+ return path
914
+ }
915
+
916
+ internals.filter = function filter (request) {
917
+ return getRouteSettingsPluginsHal(getRequestRoute(request)) // _.get(getRouteSettings(getRequestRoute(request)), 'plugins.hal', true)
918
+ }
919
+
920
+ /**
921
+ * Expands the url path to include protocol://server:port
922
+ *
923
+ * @param request
924
+ * @param {string} pathname
925
+ * @param {string | void} search
926
+ * @return {*}
927
+ */
928
+ internals.buildUrl = function buildUrl (request, pathname, search) {
929
+ return url.format({
930
+ protocol: settings.protocol || request.info.protocol || 'http',
931
+ hostname: settings.hostname || request.info.host,
932
+ pathname,
933
+ host: settings.host || request.headers.host,
934
+ port: settings.port || request.info.port,
935
+ search
936
+ })
852
937
  }
853
938
 
854
939
  /**
855
- * Assigns a filter function to test routes before applying the hal interceptor.
856
- * @param filterFn
940
+ * Assigns a filter function to test routes before applying the hal interceptor
941
+ *
942
+ * @param filter
857
943
  */
858
- internals.setFilter = function (filterFn) {
859
- const { error } = joi.function().validate(filterFn)
944
+ internals.setFilter = function setFilter (filter) {
945
+ const { error } = joi.function().validate(filter)
860
946
  if (error) throw error
861
947
 
862
- internals.filter = filterFn
948
+ internals.filter = filter
863
949
  }
864
950
 
865
- internals.setUrlBuilder = function (urlBuilder) {
951
+ internals.setUrlBuilder = function setUrlBuilder (urlBuilder) {
866
952
  const { error } = joi.function().validate(urlBuilder)
867
953
  if (error) throw error
868
954
 
@@ -876,58 +962,61 @@ export const plugin = {
876
962
  link: internals.link,
877
963
  rels: internals.rels,
878
964
  rel: internals.rel,
879
- resolve: internals.resolve,
880
965
  route: internals.route,
881
- apiLinker: internals.apiLinker,
966
+ toHal,
882
967
  filter: internals.setFilter,
883
968
  urlBuilder: internals.setUrlBuilder,
884
- configureRepresentation: internals.configureRepresentation
969
+ configureRepresentation
885
970
  }
886
971
 
887
972
  /**
888
- * A hapi lifecycle method that looks for the application/hal+json accept header and wraps the response entity into a
973
+ * A Hapi lifecycle method that looks for the application/hal+json accept header and wraps the response entity into a
889
974
  * HAL representation
975
+ *
890
976
  * @param request
891
977
  * @param h
892
978
  */
893
- internals.postHandler = function (request, h) {
894
- let rf, halConfig, entity, rep, self
895
- const mediaType = internals.getMediaType(settings.mediaTypes, request)
896
- let absolute
897
-
898
- if (mediaType && internals.shouldHalify(request)) {
899
- halConfig = request.route.settings.plugins.hal || {}
900
-
901
- // all new representations for the request will be built by this guy
902
- rf = new RepresentationFactory(api, request)
903
-
904
- entity = request.response.source
905
-
906
- absolute = halConfig.absolute || settings.absolute
979
+ internals.postHandler = function postHandler (request, h) {
980
+ if (isEligible(request)) {
981
+ const mediaType = getMediaType(request, settings.mediaTypes)
982
+ if (mediaType) {
983
+ const config = getRouteSettingsPluginsHal(getRequestRoute(request))
984
+
985
+ // e.g. honor the location header if it has been set using response.created(...) or response.location(...)
986
+ const location = getRequestResponseHeadersLocation(request)
987
+ const isAbsolute = Boolean(getAbsolute(config) || getAbsolute(settings))
988
+
989
+ const self = (
990
+ location
991
+ ? getRelativeUrl(request, location, isAbsolute)
992
+ : getRequestPath(request, getQuery(config), isAbsolute)
993
+ )
907
994
 
908
- // e.g. honor the location header if it has been set using response.created(...) or response.location(...)
909
- const { location } = request.response.headers
910
- self = location
911
- ? internals.location(request, location, absolute)
912
- : internals.getRequestPath(request, halConfig.query, absolute)
995
+ // all new representations for the request will be built by this guy
996
+ const representationFactory = new RepresentationFactory(api, request)
997
+ const representation = representationFactory.create(getRequestResponseSource(request), self)
913
998
 
914
- rep = rf.create(entity, self)
999
+ return (
1000
+ new Promise((resolve, reject) => {
1001
+ transformRepresentationEntity(config, representation, (err, representation) => {
1002
+ if (err) return reject(err)
915
1003
 
916
- return new Promise((resolve, reject) => {
917
- internals.halifyAndConfigure(rep, halConfig, (err, rep) => {
918
- if (err) {
919
- return reject(err)
920
- }
1004
+ const response = getRequestResponse(request)
921
1005
 
922
- // send back what they asked for, as plain object
923
- // so validation can be done correctly
924
- request.response.source = rep.toJSON()
925
- request.response.type(mediaType)
1006
+ // send back what they asked for (as plain object
1007
+ // so validation can be done correctly)
1008
+ response.source = representation.toJSON()
1009
+ response.type(mediaType)
926
1010
 
927
- return resolve(h.continue)
928
- })
929
- })
1011
+ return (
1012
+ resolve(h.continue)
1013
+ )
1014
+ })
1015
+ })
1016
+ )
1017
+ }
930
1018
  }
1019
+
931
1020
  return h.continue
932
1021
  }
933
1022
 
@@ -940,24 +1029,27 @@ export const plugin = {
940
1029
  // bind the API handler to api root + '/'. Ending with '/' is necessary for resolving relative links on the client
941
1030
  selection.route({
942
1031
  method: 'GET',
943
- path: `${settings.apiPath}/`,
944
- config: internals.apiRouteConfig(settings.absolute, settings.apiAuth)
1032
+ path: settings.apiPath.concat('/'),
1033
+ config: getApiRouteConfig(
1034
+ settings.apiAuth,
1035
+ settings.absolute
1036
+ )
945
1037
  })
946
1038
 
947
1039
  // set up a redirect to api root + '/'
948
- if (settings.apiPath.length > 0) {
1040
+ if (settings.apiPath) {
949
1041
  selection.route({
950
1042
  method: 'GET',
951
1043
  path: settings.apiPath,
952
- config: internals.apiRedirectConfig(
953
- settings.apiPath,
954
- settings.apiAuth
1044
+ config: getApiRedirectConfig(
1045
+ settings.apiAuth,
1046
+ settings.apiPath
955
1047
  )
956
1048
  })
957
1049
  }
958
1050
  }
959
1051
 
960
- internals.preStart = function (server) {
1052
+ internals.preStart = function preStart (server) {
961
1053
  if (_.isFunction(server.views)) {
962
1054
  server.views({
963
1055
  engines: {
@@ -966,15 +1058,17 @@ export const plugin = {
966
1058
  path: path.join(IAM, './views'),
967
1059
  isCached: false
968
1060
  })
1061
+
969
1062
  server.route({
970
- method: 'get',
1063
+ method: 'GET',
971
1064
  path: settings.relsPath,
972
- config: internals.namespacesRoute(settings.relsAuth)
1065
+ config: getRelsRouteConfig(settings.relsAuth)
973
1066
  })
1067
+
974
1068
  server.route({
975
- method: 'get',
1069
+ method: 'GET',
976
1070
  path: `${settings.relsPath}/{namespace}/{rel}`,
977
- config: internals.relRoute(settings.relsAuth)
1071
+ config: getRelRouteConfig(settings.relsAuth)
978
1072
  })
979
1073
  }
980
1074
  }