hapii-js 0.0.0-stage → 1.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/lib/server.js ADDED
@@ -0,0 +1,604 @@
1
+ 'use strict';
2
+
3
+ const Hoek = require('@hapi/hoek');
4
+ const Shot = require('@hapi/shot');
5
+ const Teamwork = require('@hapi/teamwork');
6
+
7
+ const Config = require('./config');
8
+ const Core = require('./core');
9
+ const Cors = require('./cors');
10
+ const Ext = require('./ext');
11
+ const Package = require('../package.json');
12
+ const Route = require('./route');
13
+ const Toolkit = require('./toolkit');
14
+ const Validation = require('./validation');
15
+
16
+
17
+ const internals = {};
18
+
19
+
20
+ exports = module.exports = function (options) {
21
+
22
+ const core = new Core(options);
23
+ return new internals.Server(core);
24
+ };
25
+
26
+
27
+ internals.Server = class {
28
+
29
+ constructor(core, name, parent) {
30
+
31
+ this._core = core;
32
+
33
+ // Public interface
34
+
35
+ this.app = core.app;
36
+ this.auth = core.auth.public(this);
37
+ this.decorations = core.decorations.public;
38
+ this.cache = internals.cache(this);
39
+ this.events = core.events;
40
+ this.info = core.info;
41
+ this.listener = core.listener;
42
+ this.load = core.heavy.load;
43
+ this.methods = core.methods.methods;
44
+ this.mime = core.mime;
45
+ this.plugins = core.plugins;
46
+ this.registrations = core.registrations;
47
+ this.settings = core.settings;
48
+ this.states = core.states;
49
+ this.type = core.type;
50
+ this.version = Package.version;
51
+
52
+ this.realm = {
53
+ _extensions: {
54
+ onPreAuth: new Ext('onPreAuth', core),
55
+ onCredentials: new Ext('onCredentials', core),
56
+ onPostAuth: new Ext('onPostAuth', core),
57
+ onPreHandler: new Ext('onPreHandler', core),
58
+ onPostHandler: new Ext('onPostHandler', core),
59
+ onPreResponse: new Ext('onPreResponse', core),
60
+ onPostResponse: new Ext('onPostResponse', core)
61
+ },
62
+ modifiers: {
63
+ route: {}
64
+ },
65
+ parent: parent ? parent.realm : null,
66
+ plugin: name,
67
+ pluginOptions: {},
68
+ plugins: {},
69
+ _rules: null,
70
+ settings: {
71
+ bind: undefined,
72
+ files: {
73
+ relativeTo: undefined
74
+ }
75
+ },
76
+ validator: null
77
+ };
78
+
79
+ // Decorations
80
+
81
+ for (const [property, method] of core.decorations.server.entries()) {
82
+ this[property] = method;
83
+ }
84
+
85
+ core.registerServer(this);
86
+ }
87
+
88
+ _clone(name) {
89
+
90
+ return new internals.Server(this._core, name, this);
91
+ }
92
+
93
+ bind(context) {
94
+
95
+ Hoek.assert(typeof context === 'object', 'bind must be an object');
96
+ this.realm.settings.bind = context;
97
+ }
98
+
99
+ control(server) {
100
+
101
+ Hoek.assert(server instanceof internals.Server, 'Can only control Server objects');
102
+
103
+ this._core.controlled = this._core.controlled ?? [];
104
+ this._core.controlled.push(server);
105
+ }
106
+
107
+ decoder(encoding, decoder) {
108
+
109
+ return this._core.compression.addDecoder(encoding, decoder);
110
+ }
111
+
112
+ decorate(type, property, method, options = {}) {
113
+
114
+ Hoek.assert(this._core.decorations.public[type], 'Unknown decoration type:', type);
115
+ Hoek.assert(property, 'Missing decoration property name');
116
+ Hoek.assert(typeof property === 'string' || typeof property === 'symbol', 'Decoration property must be a string or a symbol');
117
+
118
+ const propertyName = property.toString();
119
+ Hoek.assert(propertyName[0] !== '_', 'Property name cannot begin with an underscore:', propertyName);
120
+
121
+ const existing = this._core.decorations[type].get(property);
122
+ if (options.extend) {
123
+ Hoek.assert(type !== 'handler', 'Cannot extend handler decoration:', propertyName);
124
+ Hoek.assert(existing, `Cannot extend missing ${type} decoration: ${propertyName}`);
125
+ Hoek.assert(typeof method === 'function', `Extended ${type} decoration method must be a function: ${propertyName}`);
126
+
127
+ method = method(existing);
128
+ }
129
+ else {
130
+ Hoek.assert(existing === undefined, `${type[0].toUpperCase() + type.slice(1)} decoration already defined: ${propertyName}`);
131
+ }
132
+
133
+ if (type === 'handler') {
134
+
135
+ // Handler
136
+
137
+ Hoek.assert(typeof method === 'function', 'Handler must be a function:', propertyName);
138
+ Hoek.assert(!method.defaults || typeof method.defaults === 'object' || typeof method.defaults === 'function', 'Handler defaults property must be an object or function');
139
+ Hoek.assert(!options.extend, 'Cannot extend handler decoration:', propertyName);
140
+ }
141
+ else if (type === 'request') {
142
+
143
+ // Request
144
+
145
+ Hoek.assert(!this._core.Request.reserved.includes(property), 'Cannot override the built-in request interface decoration:', propertyName);
146
+
147
+ if (options.apply) {
148
+ this._core.decorations.requestApply = this._core.decorations.requestApply ?? new Map();
149
+ this._core.decorations.requestApply.set(property, method);
150
+ }
151
+ else {
152
+ this._core.Request.prototype[property] = method;
153
+ }
154
+ }
155
+ else if (type === 'response') {
156
+
157
+ // Response
158
+
159
+ Hoek.assert(!this._core.Response.reserved.includes(property), 'Cannot override the built-in response interface decoration:', propertyName);
160
+ this._core.Response.prototype[property] = method;
161
+ }
162
+ else if (type === 'toolkit') {
163
+
164
+ // Toolkit
165
+
166
+ Hoek.assert(!Toolkit.reserved.includes(property), 'Cannot override the built-in toolkit decoration:', propertyName);
167
+ this._core.toolkit.decorate(property, method);
168
+ }
169
+ else {
170
+
171
+ // Server
172
+
173
+ if (typeof property === 'string') {
174
+ Hoek.assert(!Object.getOwnPropertyNames(internals.Server.prototype).includes(property), 'Cannot override the built-in server interface method:', propertyName);
175
+ }
176
+ else {
177
+ Hoek.assert(!Object.getOwnPropertySymbols(internals.Server.prototype).includes(property), 'Cannot override the built-in server interface method:', propertyName);
178
+ }
179
+
180
+ this._core.instances.forEach((server) => {
181
+
182
+ server[property] = method;
183
+ });
184
+ }
185
+
186
+ this._core.decorations[type].set(property, method);
187
+ this._core.decorations.public[type].push(property);
188
+ }
189
+
190
+ dependency(dependencies, after) {
191
+
192
+ Hoek.assert(this.realm.plugin, 'Cannot call dependency() outside of a plugin');
193
+ Hoek.assert(!after || typeof after === 'function', 'Invalid after method');
194
+
195
+ // Normalize to { plugin: version }
196
+
197
+ if (typeof dependencies === 'string') {
198
+ dependencies = { [dependencies]: '*' };
199
+ }
200
+ else if (Array.isArray(dependencies)) {
201
+ const map = {};
202
+ for (const dependency of dependencies) {
203
+ map[dependency] = '*';
204
+ }
205
+
206
+ dependencies = map;
207
+ }
208
+
209
+ this._core.dependencies.push({ plugin: this.realm.plugin, deps: dependencies });
210
+
211
+ if (after) {
212
+ this.ext('onPreStart', after, { after: Object.keys(dependencies) });
213
+ }
214
+ }
215
+
216
+ encoder(encoding, encoder) {
217
+
218
+ return this._core.compression.addEncoder(encoding, encoder);
219
+ }
220
+
221
+ event(event) {
222
+
223
+ this._core.events.registerEvent(event);
224
+ }
225
+
226
+ expose(key, value, options = {}) {
227
+
228
+ Hoek.assert(this.realm.plugin, 'Cannot call expose() outside of a plugin');
229
+
230
+ let plugin = this.realm.plugin;
231
+ if (plugin[0] === '@' &&
232
+ options.scope !== true) {
233
+
234
+ plugin = plugin.replace(/^@([^/]+)\//, ($0, $1) => {
235
+
236
+ return !options.scope ? '' : `${$1}__`;
237
+ });
238
+ }
239
+
240
+ this._core.plugins[plugin] = this._core.plugins[plugin] ?? {};
241
+
242
+ if (typeof key === 'string') {
243
+ this._core.plugins[plugin][key] = value;
244
+ }
245
+ else {
246
+ Hoek.merge(this._core.plugins[plugin], key);
247
+ }
248
+ }
249
+
250
+ ext(events, method, options) { // (event, method, options) -OR- (events)
251
+
252
+ let promise;
253
+ if (typeof events === 'string') {
254
+ if (!method) {
255
+ const team = new Teamwork.Team();
256
+ method = (request, h) => {
257
+
258
+ team.attend(request);
259
+ return h.continue;
260
+ };
261
+
262
+ promise = team.work;
263
+ }
264
+
265
+ events = { type: events, method, options };
266
+ }
267
+
268
+ events = Config.apply('exts', events);
269
+ for (const event of events) {
270
+ this._ext(event);
271
+ }
272
+
273
+ return promise;
274
+ }
275
+
276
+ _ext(event) {
277
+
278
+ event = Object.assign({}, event); // Shallow cloned
279
+ event.realm = this.realm;
280
+ const type = event.type;
281
+
282
+ if (!this._core.extensions.server[type]) {
283
+
284
+ // Realm route extensions
285
+
286
+ if (event.options.sandbox === 'plugin') {
287
+ Hoek.assert(this.realm._extensions[type], 'Unknown event type', type);
288
+ return this.realm._extensions[type].add(event);
289
+ }
290
+
291
+ // Connection route extensions
292
+
293
+ Hoek.assert(this._core.extensions.route[type], 'Unknown event type', type);
294
+ return this._core.extensions.route[type].add(event);
295
+ }
296
+
297
+ // Server extensions
298
+
299
+ Hoek.assert(!event.options.sandbox, 'Cannot specify sandbox option for server extension');
300
+ Hoek.assert(type !== 'onPreStart' || this._core.phase === 'stopped', 'Cannot add onPreStart (after) extension after the server was initialized');
301
+
302
+ event.server = this;
303
+ this._core.extensions.server[type].add(event);
304
+ }
305
+
306
+ async inject(options) {
307
+
308
+ let settings = options;
309
+ if (typeof settings === 'string') {
310
+ settings = { url: settings };
311
+ }
312
+
313
+ if (!settings.authority ||
314
+ settings.auth ||
315
+ settings.app ||
316
+ settings.plugins ||
317
+ settings.allowInternals !== undefined) { // Can be false
318
+
319
+ settings = Object.assign({}, settings); // options can be reused (shallow cloned)
320
+ delete settings.auth;
321
+ delete settings.app;
322
+ delete settings.plugins;
323
+ delete settings.allowInternals;
324
+
325
+ settings.authority = settings.authority ?? this._core.info.host + ':' + this._core.info.port;
326
+ }
327
+
328
+ Hoek.assert(!options.credentials, 'options.credentials no longer supported (use options.auth)');
329
+
330
+ if (options.auth) {
331
+ Hoek.assert(typeof options.auth === 'object', 'options.auth must be an object');
332
+ Hoek.assert(options.auth.credentials, 'options.auth.credentials is missing');
333
+ Hoek.assert(options.auth.strategy, 'options.auth.strategy is missing');
334
+ }
335
+
336
+ const needle = this._core._dispatch({
337
+ auth: options.auth,
338
+ allowInternals: options.allowInternals,
339
+ app: options.app,
340
+ plugins: options.plugins,
341
+ isInjected: true
342
+ });
343
+
344
+ const res = await Shot.inject(needle, settings);
345
+ const custom = res.raw.res[Config.symbol];
346
+ if (custom) {
347
+ delete res.raw.res[Config.symbol];
348
+
349
+ res.request = custom.request;
350
+
351
+ if (custom.error) {
352
+ throw custom.error;
353
+ }
354
+
355
+ if (custom.result !== undefined) {
356
+ res.result = custom.result;
357
+ }
358
+ }
359
+
360
+ if (res.result === undefined) {
361
+ res.result = res.payload;
362
+ }
363
+
364
+ return res;
365
+ }
366
+
367
+ log(tags, data) {
368
+
369
+ return this._core.log(tags, data);
370
+ }
371
+
372
+ lookup(id) {
373
+
374
+ Hoek.assert(id && typeof id === 'string', 'Invalid route id:', id);
375
+
376
+ const record = this._core.router.ids.get(id);
377
+ if (!record) {
378
+ return null;
379
+ }
380
+
381
+ return record.route.public;
382
+ }
383
+
384
+ match(method, path, host) {
385
+
386
+ Hoek.assert(method && typeof method === 'string', 'Invalid method:', method);
387
+ Hoek.assert(path && typeof path === 'string' && path[0] === '/', 'Invalid path:', path);
388
+ Hoek.assert(!host || typeof host === 'string', 'Invalid host:', host);
389
+
390
+ const match = this._core.router.route(method.toLowerCase(), path, host);
391
+ Hoek.assert(match !== this._core.router.specials.badRequest, 'Invalid path:', path);
392
+ if (match === this._core.router.specials.notFound) {
393
+ return null;
394
+ }
395
+
396
+ return match.route.public;
397
+ }
398
+
399
+ method(name, method, options = {}) {
400
+
401
+ return this._core.methods.add(name, method, options, this.realm);
402
+ }
403
+
404
+ path(relativeTo) {
405
+
406
+ Hoek.assert(relativeTo && typeof relativeTo === 'string', 'relativeTo must be a non-empty string');
407
+ this.realm.settings.files.relativeTo = relativeTo;
408
+ }
409
+
410
+ async register(plugins, options = {}) {
411
+
412
+ if (this.realm.modifiers.route.prefix ||
413
+ this.realm.modifiers.route.vhost) {
414
+
415
+ options = Hoek.clone(options);
416
+ options.routes = options.routes ?? {};
417
+
418
+ options.routes.prefix = (this.realm.modifiers.route.prefix ?? '') + (options.routes.prefix ?? '') || undefined;
419
+ options.routes.vhost = this.realm.modifiers.route.vhost ?? options.routes.vhost;
420
+ }
421
+
422
+ options = Config.apply('register', options);
423
+
424
+ ++this._core.registring;
425
+
426
+ try {
427
+ const items = [].concat(plugins);
428
+ for (let item of items) {
429
+
430
+ /*
431
+ { register, ...attributes }
432
+ { plugin: { register, ...attributes }, options, once, routes }
433
+ { plugin: { plugin: { register, ...attributes } }, options, once, routes } // Required module
434
+ */
435
+
436
+ if (!item.plugin) {
437
+ item = {
438
+ plugin: item
439
+ };
440
+ }
441
+ else if (!item.plugin.register) {
442
+ item = {
443
+ options: item.options,
444
+ once: item.once,
445
+ routes: item.routes,
446
+ plugin: item.plugin.plugin
447
+ };
448
+ }
449
+ else if (typeof item === 'function') {
450
+ item = Object.assign({}, item); // Shallow cloned
451
+ }
452
+
453
+ item = Config.apply('plugin', item);
454
+
455
+ const name = item.plugin.name ?? item.plugin.pkg.name;
456
+ const clone = this._clone(name);
457
+
458
+ clone.realm.modifiers.route.prefix = item.routes.prefix ?? options.routes.prefix;
459
+ clone.realm.modifiers.route.vhost = item.routes.vhost ?? options.routes.vhost;
460
+ clone.realm.pluginOptions = item.options ?? {};
461
+
462
+ // Validate requirements
463
+
464
+ const requirements = item.plugin.requirements;
465
+ Hoek.assert(!requirements.node || Config.versionMatch(process.version, requirements.node), 'Plugin', name, 'requires node version', requirements.node, 'but found', process.version);
466
+ Hoek.assert(!requirements.hapi || Config.versionMatch(this.version, requirements.hapi), 'Plugin', name, 'requires hapi version', requirements.hapi, 'but found', this.version);
467
+
468
+ // Protect against multiple registrations
469
+
470
+ if (this._core.registrations[name]) {
471
+ if (item.plugin.once ||
472
+ item.once ||
473
+ options.once) {
474
+
475
+ continue;
476
+ }
477
+
478
+ Hoek.assert(item.plugin.multiple, 'Plugin', name, 'already registered');
479
+ }
480
+ else {
481
+ this._core.registrations[name] = {
482
+ version: item.plugin.version ?? item.plugin.pkg.version,
483
+ name,
484
+ options: item.options
485
+ };
486
+ }
487
+
488
+ if (item.plugin.dependencies) {
489
+ clone.dependency(item.plugin.dependencies);
490
+ }
491
+
492
+ // Register
493
+
494
+ await item.plugin.register(clone, item.options ?? {});
495
+ }
496
+ }
497
+ finally {
498
+ --this._core.registring;
499
+ }
500
+
501
+ return this;
502
+ }
503
+
504
+ route(options) {
505
+
506
+ Hoek.assert(typeof options === 'object', 'Invalid route options');
507
+
508
+ options = [].concat(options);
509
+ for (const config of options) {
510
+ if (Array.isArray(config.method)) {
511
+ for (const method of config.method) {
512
+ const settings = Object.assign({}, config); // Shallow cloned
513
+ settings.method = method;
514
+ this._addRoute(settings, this);
515
+ }
516
+ }
517
+ else {
518
+ this._addRoute(config, this);
519
+ }
520
+ }
521
+ }
522
+
523
+ _addRoute(config, server) {
524
+
525
+ const route = new Route(config, server); // Do no use config beyond this point, use route members
526
+ const vhosts = [].concat(route.settings.vhost ?? '*');
527
+
528
+ for (const vhost of vhosts) {
529
+ const record = this._core.router.add({ method: route.method, path: route.path, vhost, analysis: route._analysis, id: route.settings.id }, route);
530
+ route.fingerprint = record.fingerprint;
531
+ route.params = record.params;
532
+ }
533
+
534
+ this.events.emit('route', route.public);
535
+ Cors.options(route.public, server);
536
+ }
537
+
538
+ rules(processor, options = {}) {
539
+
540
+ Hoek.assert(!this.realm._rules, 'Server realm rules already defined');
541
+
542
+ const settings = Config.apply('rules', options);
543
+ if (settings.validate) {
544
+ const schema = settings.validate.schema;
545
+ settings.validate.schema = Validation.compile(schema, null, this.realm, this._core);
546
+ }
547
+
548
+ this.realm._rules = { processor, settings };
549
+ }
550
+
551
+ state(name, options) {
552
+
553
+ this.states.add(name, options);
554
+ }
555
+
556
+ table(host) {
557
+
558
+ return this._core.router.table(host);
559
+ }
560
+
561
+ validator(validator) {
562
+
563
+ Hoek.assert(!this.realm.validator, 'Validator already set');
564
+
565
+ this.realm.validator = Validation.validator(validator);
566
+ }
567
+
568
+ start() {
569
+
570
+ return this._core._start();
571
+ }
572
+
573
+ initialize() {
574
+
575
+ return this._core._initialize();
576
+ }
577
+
578
+ stop(options) {
579
+
580
+ return this._core._stop(options);
581
+ }
582
+ };
583
+
584
+
585
+ internals.cache = (plugin) => {
586
+
587
+ const policy = function (options, _segment) {
588
+
589
+ return this._core._cachePolicy(options, _segment, plugin.realm);
590
+ };
591
+
592
+ policy.provision = async (opts) => {
593
+
594
+ const clients = plugin._core._createCache(opts);
595
+
596
+ // Start cache
597
+
598
+ if (['initialized', 'starting', 'started'].includes(plugin._core.phase)) {
599
+ await Promise.all(clients.map((client) => client.start()));
600
+ }
601
+ };
602
+
603
+ return policy;
604
+ };
package/lib/streams.js ADDED
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const Stream = require('stream');
4
+
5
+ const Boom = require('@hapi/boom');
6
+ const Teamwork = require('@hapi/teamwork');
7
+
8
+ const internals = {
9
+ team: Symbol('team')
10
+ };
11
+
12
+
13
+ exports.isStream = function (stream) {
14
+
15
+ const isReadableStream = stream instanceof Stream.Readable;
16
+
17
+ if (!isReadableStream &&
18
+ typeof stream?.pipe === 'function') {
19
+ throw Boom.badImplementation('Cannot reply with a stream-like object that is not an instance of Stream.Readable');
20
+ }
21
+
22
+ if (!isReadableStream) {
23
+ return false;
24
+ }
25
+
26
+ if (stream.readableObjectMode) {
27
+ throw Boom.badImplementation('Cannot reply with stream in object mode');
28
+ }
29
+
30
+ return true;
31
+ };
32
+
33
+
34
+ exports.drain = function (stream) {
35
+
36
+ const team = new Teamwork.Team();
37
+ stream[internals.team] = team;
38
+
39
+ stream.on('readable', internals.read);
40
+ stream.on('error', internals.end);
41
+ stream.on('end', internals.end);
42
+ stream.on('close', internals.end);
43
+
44
+ return team.work;
45
+ };
46
+
47
+
48
+ internals.read = function () {
49
+
50
+ while (this.read()) { }
51
+ };
52
+
53
+
54
+ internals.end = function () {
55
+
56
+ this.removeListener('readable', internals.read);
57
+ this.removeListener('error', internals.end);
58
+ this.removeListener('end', internals.end);
59
+ this.removeListener('close', internals.end);
60
+
61
+ this[internals.team].attend();
62
+ };