@modernpoacher/halacious 6.3.0 → 7.0.0
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/README.md +364 -326
- package/examples/api-root.cjs +1 -1
- package/examples/auto-namespace.cjs +1 -1
- package/package.json +26 -29
- package/src/index.mjs +0 -1
- package/src/plugin.mjs +74 -79
- package/src/representation.mjs +1 -2
- package/test/plugin.spec.mjs +11 -11
- package/test/representation.spec.mjs +44 -16
package/README.md
CHANGED
|
@@ -4,10 +4,11 @@ A HAL processor for Hapi.
|
|
|
4
4
|
|
|
5
5
|
## Campatibility
|
|
6
6
|
|
|
7
|
-
Compatible with Hapi
|
|
7
|
+
Compatible with Hapi v21.
|
|
8
8
|
|
|
9
9
|
## Overview
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
_Halacious_ is a plugin for the HapiJS web application server that makes **HATEOASIFYING** your app ridiculously
|
|
11
12
|
easy. When paired with a well-aged HAL client-side library, you will feel the warmth of loose API coupling and the feeling
|
|
12
13
|
of moral superiorty as you rid your SPA of hard-coded api links.
|
|
13
14
|
|
|
@@ -16,6 +17,7 @@ secret sauce. Halacious embraces Hapi's configuration-centric approach to applic
|
|
|
16
17
|
be accomplished without writing any code at all.
|
|
17
18
|
|
|
18
19
|
## Features
|
|
20
|
+
|
|
19
21
|
- Dead-simple namespace/rel registration system that encourages you to document your API as you go
|
|
20
22
|
- Automatic api root REST endpoint generation that instantly gives you links to all top-level API endpoints
|
|
21
23
|
- Automatic rel documentation site generator so that your fully resolved rel names actually, you know, point somewhere.
|
|
@@ -27,162 +29,181 @@ be accomplished without writing any code at all.
|
|
|
27
29
|
- Bunches of unit tests
|
|
28
30
|
|
|
29
31
|
## Getting Started
|
|
32
|
+
|
|
30
33
|
Start by npm installing Halicious into your Hapi project folder:
|
|
31
34
|
|
|
32
|
-
```
|
|
35
|
+
```shell
|
|
33
36
|
npm install @modernpoacher/halacious --save
|
|
34
37
|
```
|
|
35
38
|
|
|
36
39
|
or
|
|
37
40
|
|
|
38
|
-
```
|
|
41
|
+
```shell
|
|
39
42
|
yarn add @modernpoacher/halacious --dev
|
|
40
43
|
```
|
|
41
44
|
|
|
42
45
|
Register the plugin with the app server
|
|
43
|
-
```javascript
|
|
44
|
-
const hapi = require('@hapi/hapi');
|
|
45
|
-
const halacious = require('#halacious');
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
```javascript
|
|
48
|
+
import hapi from '@hapi/hapi'
|
|
49
|
+
import halacious from '#halacious'
|
|
48
50
|
|
|
49
51
|
async function init () {
|
|
50
|
-
|
|
52
|
+
const server = hapi.server({ port: 8080 })
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
await server.register(halacious)
|
|
55
|
+
|
|
56
|
+
server.route({
|
|
57
|
+
method: 'get',
|
|
58
|
+
path: '/hello/{name}',
|
|
59
|
+
handler (req) {
|
|
60
|
+
return { message: 'Hello, '+req.params.name }
|
|
61
|
+
}
|
|
62
|
+
})
|
|
59
63
|
|
|
60
|
-
|
|
64
|
+
await server.start()
|
|
61
65
|
|
|
62
|
-
|
|
66
|
+
console.info('Server started at %s', server.info.uri)
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
init()
|
|
69
|
+
init()
|
|
66
70
|
```
|
|
71
|
+
|
|
67
72
|
Launch the server:
|
|
68
|
-
|
|
73
|
+
|
|
74
|
+
```shell
|
|
69
75
|
node ./examples/hello-world
|
|
70
76
|
```
|
|
71
77
|
|
|
72
78
|
Make a request
|
|
73
|
-
|
|
79
|
+
|
|
80
|
+
```shell
|
|
74
81
|
curl -H 'Accept: application/hal+json' http://localhost:8080/hello/world
|
|
75
82
|
```
|
|
76
83
|
|
|
77
84
|
See the response
|
|
78
|
-
|
|
85
|
+
|
|
86
|
+
```json
|
|
79
87
|
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
"_links": {
|
|
89
|
+
"self": {
|
|
90
|
+
"href": "/hello/world"
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"message": "Hello, world"
|
|
86
94
|
}
|
|
87
95
|
```
|
|
96
|
+
|
|
88
97
|
## Linking
|
|
98
|
+
|
|
89
99
|
Links may be declared directly within the route config.
|
|
100
|
+
|
|
90
101
|
```javascript
|
|
91
102
|
server.route({
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
103
|
+
method: 'get',
|
|
104
|
+
path: '/users/{userId}',
|
|
105
|
+
handler (req) {
|
|
106
|
+
return { id: req.params.userId, name: 'User ' + req.params.userId, googlePlusId: '107835557095464780852' }
|
|
107
|
+
},
|
|
108
|
+
config: {
|
|
109
|
+
plugins: {
|
|
110
|
+
hal: {
|
|
111
|
+
links: {
|
|
112
|
+
'home': 'http://plus.google.com/{googlePlusId}'
|
|
113
|
+
},
|
|
114
|
+
ignore: 'googlePlusId' // remove the id property from the response
|
|
115
|
+
}
|
|
106
116
|
}
|
|
107
|
-
}
|
|
108
|
-
|
|
117
|
+
}
|
|
118
|
+
})
|
|
109
119
|
```
|
|
120
|
+
|
|
121
|
+
```shell
|
|
110
122
|
curl -H 'Accept: application/hal+json' http://localhost:8080/users/100
|
|
111
123
|
```
|
|
124
|
+
|
|
112
125
|
will produce:
|
|
113
|
-
|
|
126
|
+
|
|
127
|
+
```json
|
|
114
128
|
{
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
},
|
|
119
|
-
"home": {
|
|
120
|
-
"href": "http://plus.google.com/107835557095464780852"
|
|
121
|
-
}
|
|
129
|
+
"_links": {
|
|
130
|
+
"self": {
|
|
131
|
+
"href": "/users/1234"
|
|
122
132
|
},
|
|
123
|
-
"
|
|
124
|
-
|
|
133
|
+
"home": {
|
|
134
|
+
"href": "http://plus.google.com/107835557095464780852"
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
"id": "100",
|
|
138
|
+
"name": "User 1234"
|
|
125
139
|
}
|
|
126
140
|
```
|
|
127
141
|
|
|
128
142
|
## Embedding
|
|
143
|
+
|
|
129
144
|
HAL allows you to conserve bandwidth by optionally embedding link payloads in the original request. Halacious will
|
|
130
145
|
automatically convert nested objects into embedded HAL representations (if you ask nicely).
|
|
131
146
|
|
|
132
147
|
```javascript
|
|
133
148
|
server.route({
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
}
|
|
149
|
+
method: 'get',
|
|
150
|
+
path: '/users/{userId}',
|
|
151
|
+
handler (req) {
|
|
152
|
+
return {
|
|
153
|
+
id: req.params.userId,
|
|
154
|
+
name: 'User ' + req.params.userId,
|
|
155
|
+
boss: {
|
|
156
|
+
id: 1234,
|
|
157
|
+
name: 'Boss Man'
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
config: {
|
|
162
|
+
plugins: {
|
|
163
|
+
hal: {
|
|
164
|
+
embedded: {
|
|
165
|
+
'boss': {
|
|
166
|
+
path: 'boss', // the property name of the object to embed
|
|
167
|
+
href: '../{item.id}'
|
|
168
|
+
}
|
|
156
169
|
}
|
|
170
|
+
}
|
|
157
171
|
}
|
|
158
|
-
}
|
|
159
|
-
|
|
172
|
+
}
|
|
173
|
+
})
|
|
160
174
|
```
|
|
175
|
+
|
|
176
|
+
```shell
|
|
161
177
|
curl -H 'Accept: application/hal+json' http://localhost:8080/users/100
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
will produce:
|
|
162
181
|
|
|
182
|
+
```json
|
|
163
183
|
{
|
|
164
|
-
|
|
184
|
+
"_links": {
|
|
185
|
+
"self": {
|
|
186
|
+
"href": "/users/100"
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
"id": "100",
|
|
190
|
+
"name": "User 100",
|
|
191
|
+
"_embedded": {
|
|
192
|
+
"boss": {
|
|
193
|
+
"_links": {
|
|
165
194
|
"self": {
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
"id": "100",
|
|
170
|
-
"name": "User 100",
|
|
171
|
-
"_embedded": {
|
|
172
|
-
"boss": {
|
|
173
|
-
"_links": {
|
|
174
|
-
"self": {
|
|
175
|
-
"href": "/users/1234"
|
|
176
|
-
}
|
|
177
|
-
},
|
|
178
|
-
"id": 1234,
|
|
179
|
-
"name": "Boss Man"
|
|
195
|
+
"href": "/users/1234"
|
|
180
196
|
}
|
|
197
|
+
},
|
|
198
|
+
"id": 1234,
|
|
199
|
+
"name": "Boss Man"
|
|
181
200
|
}
|
|
201
|
+
}
|
|
182
202
|
}
|
|
183
203
|
```
|
|
184
204
|
|
|
185
205
|
## Programmatic configuration of HAL representations
|
|
206
|
+
|
|
186
207
|
You may find the need to take the wheel on occasion and directly configure outbound representions. For example,
|
|
187
208
|
some links may be conditional on potentially asynchronous criteria. Fortunately, Halacious provides two ways to do this:
|
|
188
209
|
|
|
@@ -190,130 +211,131 @@ some links may be conditional on potentially asynchronous criteria. Fortunately,
|
|
|
190
211
|
2. By implementing a `toHal()` method directly on a wrapped entity.
|
|
191
212
|
|
|
192
213
|
In either case, the method signature is the same: `fn(rep, callback)` where
|
|
214
|
+
|
|
193
215
|
- `rep` - a representation object with the following properties and functions:
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
the following properties (See see http://tools.ietf.org/html/draft-kelly-json-hal-06#section-8.2 for more information):
|
|
204
|
-
|
|
205
|
-
- `templated`
|
|
206
|
-
- `title`
|
|
207
|
-
- `type`
|
|
208
|
-
- `deprecation`
|
|
209
|
-
- `name`
|
|
210
|
-
- `profile`
|
|
211
|
-
- `hreflang`
|
|
212
|
-
- `embed(rel, self, entity)` - adds an entity to the representation's `_embedded` collection with the supplied rel link relation and self href, returning a new representation
|
|
216
|
+
- `factory` - a factory reference for creating new representations. The factory object implements one method:
|
|
217
|
+
- `create(entity, selfHref)` - wraps entity with a new Hal representation, whose self link will point to selfHref
|
|
218
|
+
- `request` - the originating hapi request
|
|
219
|
+
- `self` - a shortcut to the representation's self link
|
|
220
|
+
- `entity` - the original wrapped entity
|
|
221
|
+
- `prop(name, value)` - manually adds a name/value pair to the representation
|
|
222
|
+
- `merge(object)` - merges the properties of another object into the representation
|
|
223
|
+
- `ignore(...propertyNames)` - prevents fields from being included in the response
|
|
224
|
+
- `link(relName, href)` - adds a new link to the `_links` collection, returning the new link. Link objects support
|
|
225
|
+
the following properties (See see http://tools.ietf.org/html/draft-kelly-json-hal-06#section-8.2 for more information): - `href` - `templated` - `title` - `type` - `deprecation` - `name` - `profile` - `hreflang`
|
|
226
|
+
- `embed(rel, self, entity)` - adds an entity to the representation's `_embedded` collection with the supplied rel link relation and self href, returning a new representation
|
|
213
227
|
object for further configuration.
|
|
214
228
|
- `callback([err], [representation])` - an asynchronous callback function to be called when configuration of the hal entity
|
|
215
|
-
is complete. Most of the time this function should be called with no arguments. Only pass arguments if there has been
|
|
216
|
-
an error or if a completely new representation has been created with `rep.factory.create()`.
|
|
229
|
+
is complete. Most of the time this function should be called with no arguments. Only pass arguments if there has been
|
|
230
|
+
an error or if a completely new representation has been created with `rep.factory.create()`.
|
|
217
231
|
|
|
218
232
|
#### Example 1: A `prepare()` function declared in the route descriptor.
|
|
233
|
+
|
|
219
234
|
```javascript
|
|
220
235
|
server.route({
|
|
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
|
-
rep.ignore('items');
|
|
247
|
-
// dont forget to call next!
|
|
248
|
-
next();
|
|
249
|
-
}
|
|
236
|
+
method: 'get',
|
|
237
|
+
path: '/users',
|
|
238
|
+
handler (req) {
|
|
239
|
+
return {
|
|
240
|
+
start: 0,
|
|
241
|
+
count: 2,
|
|
242
|
+
limit: 2,
|
|
243
|
+
items: [
|
|
244
|
+
{ id: 100, firstName: 'Brad', lastName: 'Leupen', googlePlusId: '107835557095464780852'},
|
|
245
|
+
{ id: 101, firstName: 'Mark', lastName: 'Zuckerberg'}
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
config: {
|
|
250
|
+
plugins: {
|
|
251
|
+
hal: {
|
|
252
|
+
// you can also assign this function directly to the hal property above as a shortcut
|
|
253
|
+
prepare (rep, next) {
|
|
254
|
+
rep.entity.items.forEach(function (item) {
|
|
255
|
+
const embed = rep.embed('item', './' + item.id, item)
|
|
256
|
+
if (item.googlePlusId) {
|
|
257
|
+
embed.link('home', 'http://plus.google.com/' + item.googlePlusId)
|
|
258
|
+
embed.ignore('googlePlusId')
|
|
250
259
|
}
|
|
260
|
+
})
|
|
261
|
+
rep.ignore('items')
|
|
262
|
+
// dont forget to call next!
|
|
263
|
+
next()
|
|
251
264
|
}
|
|
265
|
+
}
|
|
252
266
|
}
|
|
253
|
-
}
|
|
254
|
-
|
|
267
|
+
}
|
|
268
|
+
})
|
|
255
269
|
```
|
|
270
|
+
|
|
271
|
+
```shell
|
|
256
272
|
curl -H 'Accept: application/hal+json' http://localhost:8080/users
|
|
273
|
+
```
|
|
257
274
|
|
|
275
|
+
will produce:
|
|
276
|
+
|
|
277
|
+
```json
|
|
258
278
|
{
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
},
|
|
264
|
-
"start": 0,
|
|
265
|
-
"count": 2,
|
|
266
|
-
"limit": 2,
|
|
267
|
-
"_embedded": {
|
|
268
|
-
"item": [
|
|
269
|
-
{
|
|
270
|
-
"_links": {
|
|
271
|
-
"self": {
|
|
272
|
-
"href": "/users/100"
|
|
273
|
-
},
|
|
274
|
-
"home": {
|
|
275
|
-
"href": "http://plus.google.com/107835557095464780852"
|
|
276
|
-
}
|
|
277
|
-
},
|
|
278
|
-
"id": 100,
|
|
279
|
-
"firstName": "Brad",
|
|
280
|
-
"lastName": "Leupen"
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
"_links": {
|
|
284
|
-
"self": {
|
|
285
|
-
"href": "/users/101"
|
|
286
|
-
}
|
|
287
|
-
},
|
|
288
|
-
"id": 101,
|
|
289
|
-
"firstName": "Mark",
|
|
290
|
-
"lastName": "Zuckerberg"
|
|
291
|
-
}
|
|
292
|
-
]
|
|
279
|
+
"_links": {
|
|
280
|
+
"self": {
|
|
281
|
+
"href": "/users"
|
|
293
282
|
}
|
|
283
|
+
},
|
|
284
|
+
"start": 0,
|
|
285
|
+
"count": 2,
|
|
286
|
+
"limit": 2,
|
|
287
|
+
"_embedded": {
|
|
288
|
+
"item": [
|
|
289
|
+
{
|
|
290
|
+
"_links": {
|
|
291
|
+
"self": {
|
|
292
|
+
"href": "/users/100"
|
|
293
|
+
},
|
|
294
|
+
"home": {
|
|
295
|
+
"href": "http://plus.google.com/107835557095464780852"
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
"id": 100,
|
|
299
|
+
"firstName": "Brad",
|
|
300
|
+
"lastName": "Leupen"
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
"_links": {
|
|
304
|
+
"self": {
|
|
305
|
+
"href": "/users/101"
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
"id": 101,
|
|
309
|
+
"firstName": "Mark",
|
|
310
|
+
"lastName": "Zuckerberg"
|
|
311
|
+
}
|
|
312
|
+
]
|
|
313
|
+
}
|
|
294
314
|
}
|
|
295
315
|
```
|
|
316
|
+
|
|
296
317
|
#### Example 2: Implementing `toHal()` on a domain entity:
|
|
318
|
+
|
|
297
319
|
```javascript
|
|
298
320
|
function User(id, firstName, lastName, googlePlusId) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
321
|
+
this.id = id
|
|
322
|
+
this.firstName = firstName
|
|
323
|
+
this.lastName = lastName
|
|
324
|
+
this.googlePlusId = googlePlusId
|
|
303
325
|
}
|
|
304
326
|
|
|
305
327
|
User.prototype.toHal = function(rep, next) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
328
|
+
if (this.googlePlusId) {
|
|
329
|
+
rep.link('home', 'http://plus.google.com/' + this.googlePlusId)
|
|
330
|
+
rep.ignore('googlePlusId')
|
|
331
|
+
}
|
|
332
|
+
next()
|
|
333
|
+
}
|
|
312
334
|
|
|
313
335
|
server.route({
|
|
314
336
|
method: 'get',
|
|
315
337
|
path: '/users',
|
|
316
|
-
handler
|
|
338
|
+
handler (req) {
|
|
317
339
|
return {
|
|
318
340
|
start: 0,
|
|
319
341
|
count: 2,
|
|
@@ -322,7 +344,7 @@ server.route({
|
|
|
322
344
|
new User(100, 'Brad', 'Leupen', '107835557095464780852'),
|
|
323
345
|
new User(101, 'Mark', 'Zuckerberg')
|
|
324
346
|
]
|
|
325
|
-
}
|
|
347
|
+
}
|
|
326
348
|
},
|
|
327
349
|
config: {
|
|
328
350
|
plugins: {
|
|
@@ -336,121 +358,130 @@ server.route({
|
|
|
336
358
|
}
|
|
337
359
|
}
|
|
338
360
|
}
|
|
339
|
-
})
|
|
361
|
+
})
|
|
340
362
|
```
|
|
341
363
|
|
|
342
364
|
## The HAL route configuration object
|
|
365
|
+
|
|
343
366
|
The `config.plugins.hal` route configuration object takes the following format:
|
|
367
|
+
|
|
344
368
|
- A function `fn(rep, next)` - for purely programmatic control over the representation
|
|
345
|
-
or
|
|
369
|
+
or
|
|
346
370
|
- An object with the following properties:
|
|
347
|
-
|
|
348
|
-
in the root api resource's _links collection.
|
|
349
|
-
|
|
350
|
-
|
|
371
|
+
- `api` - an optional top level api rel name to assign to this route. Setting a value will cause this route to be included
|
|
372
|
+
in the root api resource's \_links collection.
|
|
373
|
+
- `prepare(rep, next)` - an optional prepare function
|
|
374
|
+
- `ignore` - A String or array of strings containing the names of properties to remove from the output. Can be used
|
|
351
375
|
to remove reduntant information from the response
|
|
352
|
-
|
|
376
|
+
- `query` - An RFC 6570 compatible query string that should be communicated to your clients. See: http://tools.ietf.org/html/rfc6570.
|
|
353
377
|
Example: `{?q*,start,limit}`. These parameters will be included in top level api links. They will also be included in self links if supplied in the request.
|
|
354
378
|
Query parameters that are not included in the template, such as runtime tokens, will be excluded from the self href.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
- `absolute` - a boolean true/false. if true, hrefs for this representation will include protocal, server, and port.
|
|
368
|
-
Default: false
|
|
379
|
+
- `links` - An object whose keys are rel names and whose values are href strings or link objects that contain
|
|
380
|
+
at least an `href` property. Hrefs may be absolute or relative to the representation's self link. Hrefs may also contain
|
|
381
|
+
`{expression}` template expressions, which are resolved against the wrapped entity.
|
|
382
|
+
- `embedded` An object whose keys are rel names and whose values are configuration objects with:
|
|
383
|
+
- `path` - a path expression to evaluate against the wrapped entity to derive the object to embed.
|
|
384
|
+
- `href` - a Function, String or link object that will be used to define the entity's self relation. Like links,
|
|
385
|
+
embedded href's may also be templated. Unlike links, embedded href templates have access to two state variables: - `self` - the parent entity - `item` - the child entity
|
|
386
|
+
- `links`
|
|
387
|
+
- `embedded` (recursively evaluated)
|
|
388
|
+
- `prepare(rep, next)`
|
|
389
|
+
- `absolute` - a boolean true/false. if true, hrefs for this representation will include protocal, server, and port.
|
|
390
|
+
Default: false
|
|
369
391
|
|
|
370
392
|
## Namespaces and Rels
|
|
393
|
+
|
|
371
394
|
So far, we have not done a real good job in our examples defining our link relations. Unless registered with the IANA,
|
|
372
395
|
link relations should really be unique URLs that resolve to documentation regarding their semantics. Halacious will
|
|
373
396
|
happily let you be lazy but its much better if we do things the Right Way.
|
|
374
397
|
|
|
375
398
|
### Manually creating a namespace
|
|
399
|
+
|
|
376
400
|
Halacious exposes its api to your Hapi server so that you may configure it at runtime like so:
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
401
|
+
|
|
402
|
+
```javascript
|
|
403
|
+
import hapi from '@hapi/hapi'
|
|
404
|
+
import halacious from '#halacious'
|
|
380
405
|
|
|
381
406
|
async function init () {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
407
|
+
const server = hapi.server({ port: 8080 })
|
|
408
|
+
|
|
409
|
+
await server.register(halacious)
|
|
410
|
+
|
|
411
|
+
const namespace = server.plugins.halacious.namespaces.add({ name: 'mycompany', description: 'My Companys namespace', prefix: 'mco'})
|
|
412
|
+
namespace.rel({ name: 'users', description: 'a collection of users' })
|
|
413
|
+
namespace.rel({ name: 'user', description: 'a single user' })
|
|
414
|
+
namespace.rel({ name: 'boss', description: 'a users boss' })
|
|
415
|
+
|
|
416
|
+
server.route({
|
|
417
|
+
method: 'get',
|
|
418
|
+
path: '/users/{userId}',
|
|
419
|
+
handler (req) {
|
|
420
|
+
return { id: req.params.userId, name: 'User ' + req.params.userId, bossId: 200 }
|
|
421
|
+
},
|
|
422
|
+
config: {
|
|
423
|
+
plugins: {
|
|
424
|
+
hal: {
|
|
425
|
+
links: {
|
|
426
|
+
'mco:boss': '../{bossId}'
|
|
427
|
+
},
|
|
428
|
+
ignore: 'bossId'
|
|
404
429
|
}
|
|
405
|
-
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
})
|
|
406
433
|
}
|
|
407
434
|
|
|
408
435
|
init()
|
|
409
436
|
```
|
|
410
|
-
|
|
411
|
-
to use shorthand rel names while preserving their uniqueness. Without the curie, the 'mco:boss' rel key would be expanded
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
+
|
|
438
|
+
Now, when we access the server we see a new type of link in the `_links` collection, `curies`. The curies link provides a mechanism to use shorthand rel names while preserving their uniqueness. Without the curie, the 'mco:boss' rel key would be expanded to read `/rels/mycompany/boss`
|
|
439
|
+
|
|
440
|
+
```shell
|
|
441
|
+
curl -H 'Accept: application/hal+json' http://localhost:8080/users/100
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
will produce:
|
|
445
|
+
|
|
446
|
+
```json
|
|
447
|
+
{
|
|
448
|
+
"_links": {
|
|
449
|
+
"self": {
|
|
450
|
+
"href": "/users/100"
|
|
451
|
+
},
|
|
452
|
+
"curies": [
|
|
453
|
+
{
|
|
454
|
+
"name": "mco",
|
|
455
|
+
"href": "/rels/mycompany/{rel}",
|
|
456
|
+
"templated": true
|
|
457
|
+
}
|
|
458
|
+
],
|
|
459
|
+
"mco:boss": {
|
|
460
|
+
"href": "/users/200"
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
"id": "100",
|
|
464
|
+
"name": "User 100"
|
|
465
|
+
}
|
|
466
|
+
```
|
|
437
467
|
|
|
438
468
|
### Creating a namespace from a folder of documentated rels
|
|
469
|
+
|
|
439
470
|
In our examples folder, we have created a folder `rels/mycompany` containing markdown documents for all of the rels in our
|
|
440
471
|
company's namespace. We can suck all these into the system in one fell swoop:
|
|
441
472
|
|
|
442
473
|
```javascript
|
|
443
|
-
|
|
444
|
-
|
|
474
|
+
import hapi from '@hapi/hapi'
|
|
475
|
+
import halacious from '#halacious'
|
|
445
476
|
|
|
446
477
|
async function init () {
|
|
447
|
-
|
|
478
|
+
const server = hapi.server({ port: 8080 })
|
|
448
479
|
|
|
449
|
-
|
|
480
|
+
await server.register(halacious)
|
|
450
481
|
|
|
451
|
-
|
|
482
|
+
server.plugins.halacious.namespaces.add({ dir: __dirname + '/rels/mycompany', prefix: 'mco' })
|
|
452
483
|
|
|
453
|
-
|
|
484
|
+
await server.start()
|
|
454
485
|
}
|
|
455
486
|
|
|
456
487
|
init()
|
|
@@ -459,59 +490,61 @@ init()
|
|
|
459
490
|
Ideally these documents should provide your api consumer enough semantic information to navigate your api.
|
|
460
491
|
|
|
461
492
|
## Rels documentation
|
|
493
|
+
|
|
462
494
|
Halacious includes an (extremely) barebones namespace / rel navigator for users to browse your documentation.
|
|
463
495
|
The server binds to the `/rels` path on your server by default.
|
|
464
496
|
|
|
465
|
-
_Note: Hapi 9 / 10 users must install and configure the vision views plugin to enable this feature.
|
|
497
|
+
\_Note: Hapi 9 / 10 users must install and configure the vision views plugin to enable this feature.
|
|
466
498
|
|
|
467
499
|
## Automatic /api root
|
|
500
|
+
|
|
468
501
|
Discoverability is a key tenant of any hypermedia system. HAL requires that only the root API url be known to clients of your
|
|
469
502
|
application, from which all other urls may be derived via rel names. If you want, Halacious will create this root api
|
|
470
503
|
route for you automatically. All you need to do is to identify which resources to include by using the `api` route
|
|
471
504
|
configuration option. For example:
|
|
472
505
|
|
|
473
506
|
```javascript
|
|
474
|
-
|
|
475
|
-
|
|
507
|
+
import hapi from '@hapi/hapi'
|
|
508
|
+
import halacious from '#halacious'
|
|
476
509
|
|
|
477
510
|
async function init () {
|
|
478
|
-
|
|
511
|
+
const server = hapi.server({ port: 8080 })
|
|
479
512
|
|
|
480
|
-
|
|
513
|
+
await server.register(halacious)
|
|
481
514
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
515
|
+
const namespace = server.plugins.halacious.namespaces.add({ name: 'mycompany', description: 'My Companys namespace', prefix: 'mco'})
|
|
516
|
+
namespace.rel({ name: 'users', description: 'a collection of users' })
|
|
517
|
+
namespace.rel({ name: 'user', description: 'a single user' })
|
|
485
518
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
}
|
|
519
|
+
server.route({
|
|
520
|
+
method: 'get',
|
|
521
|
+
path: '/users',
|
|
522
|
+
handler () {
|
|
523
|
+
return {}
|
|
524
|
+
},
|
|
525
|
+
config: {
|
|
526
|
+
plugins: {
|
|
527
|
+
hal: {
|
|
528
|
+
api: 'mco:users'
|
|
498
529
|
}
|
|
499
|
-
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
})
|
|
500
533
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
}
|
|
512
|
-
}
|
|
534
|
+
server.route({
|
|
535
|
+
method: 'get',
|
|
536
|
+
path: '/users/{userId}',
|
|
537
|
+
handler () {
|
|
538
|
+
return {}
|
|
539
|
+
},
|
|
540
|
+
config: {
|
|
541
|
+
plugins: {
|
|
542
|
+
hal: {
|
|
543
|
+
api: 'mco:user'
|
|
513
544
|
}
|
|
514
|
-
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
})
|
|
515
548
|
}
|
|
516
549
|
|
|
517
550
|
init()
|
|
@@ -519,35 +552,40 @@ init()
|
|
|
519
552
|
|
|
520
553
|
will auto-create the following api root:
|
|
521
554
|
|
|
522
|
-
```
|
|
555
|
+
```shell
|
|
523
556
|
curl -H 'Accept: application/hal+json' http://localhost:8080/api/
|
|
557
|
+
```
|
|
524
558
|
|
|
559
|
+
will produce:
|
|
560
|
+
|
|
561
|
+
```json
|
|
525
562
|
{
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}
|
|
563
|
+
"_links": {
|
|
564
|
+
"self": {
|
|
565
|
+
"href": "/api/"
|
|
566
|
+
},
|
|
567
|
+
"curies": [
|
|
568
|
+
{
|
|
569
|
+
"name": "mco",
|
|
570
|
+
"href": "/rels/mycompany/{rel}",
|
|
571
|
+
"templated": true
|
|
572
|
+
}
|
|
573
|
+
],
|
|
574
|
+
"mco:users": {
|
|
575
|
+
"href": "/users"
|
|
576
|
+
},
|
|
577
|
+
"mco:user": {
|
|
578
|
+
"href": "/users/{userId}",
|
|
579
|
+
"templated": true
|
|
544
580
|
}
|
|
581
|
+
}
|
|
545
582
|
}
|
|
546
583
|
```
|
|
547
584
|
|
|
548
585
|
## Plugin Options
|
|
586
|
+
|
|
549
587
|
- `strict` - setting this to `true` will cause an exception to be thrown when referencing unregistered local rel. Setting this
|
|
550
|
-
to true will help catch typos during development. Default: `false`
|
|
588
|
+
to true will help catch typos during development. Default: `false`
|
|
551
589
|
- `relsPath` - the route path to the rels documentation root. Default: `/rels`
|
|
552
590
|
- `relsAuth` - the hapi authentication setting to use for the documentation routes. Default: `false`
|
|
553
591
|
- `relsTemplate` - a boolean true/false. if true, rels documentation uses the default template. Default: `true`
|
|
@@ -557,7 +595,7 @@ to true will help catch typos during development. Default: `false`
|
|
|
557
595
|
- `apiServerLabel` - when set, Halacious will select for a specific server to route the api root.
|
|
558
596
|
- `mediaTypes` - an array of media types that will trigger the hal processor to modify the response (e.g. `['application/json',
|
|
559
597
|
'application/hal+json']`). the media types are checked in order. if any match the accept header parameters, then the
|
|
560
|
-
response will be halified and the media type of the response will be set to the first winner. Default: `['application/hal+json']`
|
|
598
|
+
response will be halified and the media type of the response will be set to the first winner. Default: `['application/hal+json']`
|
|
561
599
|
- `absolute` - a boolean true/false. if true, all hrefs will include the protocol, server, and port. Default: false
|
|
562
600
|
- `host` - a hostname+port string for all absolute link urls
|
|
563
601
|
- `hostname` - a string hostname for all absolute link urls (only used if host is blank)
|