1 | /**
|
2 | * @file Defines the BaseEndpoint class.
|
3 | *
|
4 | * @author Luke Chavers <luke@c2cschools.com>
|
5 | * @author Kevin Sanders <kevin@c2cschools.com>
|
6 | * @since 5.0.0
|
7 | * @license See LICENSE.md for details about licensing.
|
8 | * @copyright 2017 C2C Schools, LLC
|
9 | */
|
10 |
|
11 | ;
|
12 |
|
13 | const BaseClass = require( "@corefw/common" ).common.BaseClass;
|
14 | const ERRORS = require( "../errors" );
|
15 |
|
16 | /**
|
17 | * The parent class for all endpoints.
|
18 | *
|
19 | * @memberOf Endpoint
|
20 | * @extends Common.BaseClass
|
21 | */
|
22 | class BaseEndpoint extends BaseClass {
|
23 |
|
24 | // <editor-fold desc="--- Construction & Initialization ------------------">
|
25 |
|
26 | /**
|
27 | * @inheritDoc
|
28 | */
|
29 | _initialize( cfg ) {
|
30 |
|
31 | const me = this;
|
32 |
|
33 | // Call parent
|
34 | super._initialize( cfg );
|
35 |
|
36 | // If a model object was passed in the
|
37 | // config, we'll need to "adopt" it.
|
38 | if ( cfg.model !== undefined ) {
|
39 |
|
40 | me.$adopt( cfg.model );
|
41 | }
|
42 | }
|
43 |
|
44 | // </editor-fold>
|
45 |
|
46 | // <editor-fold desc="--- Fundamental Endpoint Properties ----------------">
|
47 |
|
48 | /**
|
49 | * A shortened string representation of the fundamental endpoint "type".
|
50 | * The value for this property is usually provided as a static string
|
51 | * from within the constructors of child classes.
|
52 | *
|
53 | * @public
|
54 | * @type {?string}
|
55 | * @default null
|
56 | */
|
57 | get endpointType() {
|
58 |
|
59 | const me = this;
|
60 |
|
61 | return me.getConfigValue( "endpointType", null );
|
62 | }
|
63 |
|
64 | // noinspection JSUnusedGlobalSymbols
|
65 | set endpointType( /** ?string */ val ) {
|
66 |
|
67 | const me = this;
|
68 |
|
69 | me.setConfigValue( "endpointType", val );
|
70 | }
|
71 |
|
72 | /**
|
73 | * The name of the default {@link Response.BaseResponse} class to use.
|
74 | * The value for this property is usually provided as a static string
|
75 | * from within the constructors of child classes.
|
76 | *
|
77 | * @public
|
78 | * @type {?string}
|
79 | * @default null
|
80 | * @readonly
|
81 | */
|
82 | get defaultSuccessResponse() {
|
83 |
|
84 | const me = this;
|
85 |
|
86 | return me.getConfigValue( "defaultSuccessResponse", null );
|
87 | }
|
88 |
|
89 | // </editor-fold>
|
90 |
|
91 | // <editor-fold desc="--- Fundamental Paths & Path Management ------------">
|
92 |
|
93 | /**
|
94 | * An absolute filesystem path to the endpoint's directory.
|
95 | *
|
96 | * @throws {Errors.SourcePathNotDefinedError} If the value for this property
|
97 | * is requested but not defined (NULL).
|
98 | * @public
|
99 | * @type {?string}
|
100 | * @default null
|
101 | * @readonly
|
102 | */
|
103 | get endpointPath() {
|
104 |
|
105 | const me = this;
|
106 |
|
107 | let val = me.getConfigValue( "endpointPath", null );
|
108 |
|
109 | if ( val === null ) {
|
110 |
|
111 | throw new ERRORS.SourcePathNotDefinedError(
|
112 | "Endpoints MUST define their 'Endpoint Root Path' using the " +
|
113 | "'endpointPath' configuration property during construction."
|
114 | );
|
115 | }
|
116 |
|
117 | return val;
|
118 | }
|
119 |
|
120 | /**
|
121 | * An absolute filesystem path to the file that defines the endpoint class.
|
122 | *
|
123 | * @throws {Errors.SourcePathNotDefinedError} If the value for this property
|
124 | * is requested but not defined (NULL).
|
125 | * @public
|
126 | * @type {?string}
|
127 | * @default null
|
128 | * @readonly
|
129 | */
|
130 | get handlerPath() {
|
131 |
|
132 | const me = this;
|
133 |
|
134 | let val = me.getConfigValue( "handlerPath", null );
|
135 |
|
136 | if ( val === null ) {
|
137 |
|
138 | throw new ERRORS.SourcePathNotDefinedError(
|
139 | "Endpoints MUST define their 'Endpoint Handler Path' using " +
|
140 | "the 'handlerPath' configuration property during construction."
|
141 | );
|
142 | }
|
143 |
|
144 | return val;
|
145 | }
|
146 |
|
147 | /**
|
148 | * An absolute filesystem path to the root directory of the project
|
149 | * containing the endpoint (the service repository).
|
150 | *
|
151 | * @public
|
152 | * @type {string}
|
153 | * @readonly
|
154 | */
|
155 | get projectPath() {
|
156 |
|
157 | const me = this;
|
158 |
|
159 | return me.getConfigValue( "projectPath", me._resolveProjectPath );
|
160 | }
|
161 |
|
162 | // noinspection JSUnusedGlobalSymbols
|
163 | /**
|
164 | * Initializes a PathManager object and attaches it to this endpoint.
|
165 | * PathManagers are used throughout the project by most classes, but they're
|
166 | * usually inherited. Endpoints are the actual creators/source for path
|
167 | * managers, and, so, they need to create their own.
|
168 | *
|
169 | * @private
|
170 | * @returns {Common.PathManager} The path manager is instantiated and then
|
171 | * stored in the 'pathManager' property.
|
172 | */
|
173 | initPathManager() {
|
174 |
|
175 | const me = this;
|
176 |
|
177 | // Dependencies...
|
178 | const PATH = me.$dep( "path" );
|
179 |
|
180 | let pm = me.getConfigValue( "pathManager" );
|
181 |
|
182 | if ( !pm ) {
|
183 |
|
184 | pm = super.initPathManager();
|
185 | }
|
186 |
|
187 | if ( !pm.hasPath( "microservicesLib" ) ) {
|
188 |
|
189 | pm.setPath( "microservicesLib", PATH.join( __dirname, ".." ) );
|
190 | }
|
191 |
|
192 | // // Dependencies...
|
193 | // const PATH = me.$dep( "path" );
|
194 | //
|
195 | // // We'll only ever need one...
|
196 | // if ( me.hasConfigValue( "pathManager" ) ) {
|
197 | //
|
198 | // return me.pathManager;
|
199 | // }
|
200 | //
|
201 | // // Instantiate the new path manager
|
202 | // let pm = super.initPathManager();
|
203 |
|
204 | // pm.setPath( "microservicesLib", PATH.join( __dirname, ".." ) );
|
205 |
|
206 | // Add the core/fundamental paths...
|
207 | pm.setPath( "project", me.projectPath );
|
208 | pm.setPath( "endpoint", me.endpointPath );
|
209 | pm.setPath( "endpointHandler", me.handlerPath );
|
210 |
|
211 | // Add the sub-paths
|
212 | pm.setSubPath(
|
213 | "endpointSchema",
|
214 | "endpoint",
|
215 | "schema"
|
216 | );
|
217 |
|
218 | pm.setSubPath(
|
219 | "endpoints",
|
220 | "project",
|
221 | "endpoints"
|
222 | );
|
223 |
|
224 | pm.setSubPath(
|
225 | "projectLib",
|
226 | "project",
|
227 | "lib"
|
228 | );
|
229 |
|
230 | pm.setSubPath(
|
231 | "models",
|
232 | "projectLib",
|
233 | "models"
|
234 | );
|
235 |
|
236 | // Schemas
|
237 | pm.setSubPath(
|
238 | "parameterSchema",
|
239 | "endpointSchema",
|
240 | "Parameters.yml"
|
241 | );
|
242 | pm.setSubPath(
|
243 | "requestBodySchema",
|
244 | "endpointSchema",
|
245 | "RequestBody.yml"
|
246 | );
|
247 | pm.setSubPath(
|
248 | "pathSchema",
|
249 | "endpointSchema",
|
250 | "Paths.yml"
|
251 | );
|
252 |
|
253 | pm.setSubPath(
|
254 | "responseExample",
|
255 | "endpointSchema",
|
256 | "SuccessExample.yml"
|
257 | );
|
258 |
|
259 | pm.setSubPath(
|
260 | "responseSchema",
|
261 | "endpointSchema",
|
262 | "SuccessResponse.yml"
|
263 | );
|
264 |
|
265 | return pm;
|
266 | }
|
267 |
|
268 | /**
|
269 | * Resolves the root path for the project that defines the endpoint by
|
270 | * assuming that all endpoints are defined within a root directory named
|
271 | * 'endpoints'.
|
272 | *
|
273 | * @private
|
274 | * @returns {string} An absolute filesystem path.
|
275 | */
|
276 | _resolveProjectPath() {
|
277 |
|
278 | const me = this;
|
279 |
|
280 | let pathSpl = me.endpointPath.split( "lib/endpoints" );
|
281 |
|
282 | return pathSpl[ 0 ];
|
283 | }
|
284 |
|
285 | // </editor-fold>
|
286 |
|
287 | // <editor-fold desc="--- Request, Response, and Parameter Schemas -------">
|
288 |
|
289 | // -- parameter schema --
|
290 |
|
291 | /**
|
292 | * The absolute path to the parameter schema for this endpoint, which is
|
293 | * used for validation within certain contexts.
|
294 | *
|
295 | * @public
|
296 | * @throws {Errors.MissingParameterSchemaError} If the path to the response
|
297 | * schema is requested but not defined.
|
298 | * @type {string}
|
299 | * @readonly
|
300 | */
|
301 | get parameterSchemaPath() {
|
302 |
|
303 | const me = this;
|
304 |
|
305 | if ( !me.pathManager.hasPath( "parameterSchema" ) ) {
|
306 |
|
307 | throw new ERRORS.MissingParameterSchemaError(
|
308 | "All endpoints MUST have a 'Parameter Schema Path' defined."
|
309 | );
|
310 | }
|
311 |
|
312 | return me.pathManager.getPath( "parameterSchema" );
|
313 | }
|
314 |
|
315 | /**
|
316 | * The absolute path to the request body schema for this endpoint, which is
|
317 | * used for validation within certain contexts.
|
318 | *
|
319 | * @public
|
320 | * @throws {Errors.MissingRequestBodySchemaError} If the path to the
|
321 | * response schema is requested but not defined.
|
322 | * @type {string}
|
323 | * @readonly
|
324 | */
|
325 | get requestBodySchemaPath() {
|
326 |
|
327 | const me = this;
|
328 |
|
329 | if ( !me.pathManager.hasPath( "requestBodySchema" ) ) {
|
330 |
|
331 | throw new ERRORS.MissingRequestBodySchemaError(
|
332 | "All endpoints MUST have a 'Request Body Schema Path' defined."
|
333 | );
|
334 | }
|
335 |
|
336 | return me.pathManager.getPath( "requestBodySchema" );
|
337 | }
|
338 |
|
339 | /**
|
340 | * Get a schema representing the parameters within a valid request, in JSON
|
341 | * Schema object format.
|
342 | *
|
343 | * @public
|
344 | * @throws {Errors.MissingParameterSchemaError} If the schema is requested
|
345 | * but is not defined.
|
346 | * @returns {Promise<Object>} Parameter schema.
|
347 | */
|
348 | getParameterSchema() {
|
349 |
|
350 | const me = this;
|
351 |
|
352 | // Dependencies
|
353 | const BB = me.$dep( "bluebird" );
|
354 |
|
355 | if ( me.hasConfigValue( "parameterSchema" ) ) {
|
356 |
|
357 | return BB.resolve(
|
358 | me.getConfigValue( "parameterSchema" )
|
359 | );
|
360 | }
|
361 |
|
362 | return me._loadParameterSchema()
|
363 | .then( function ( parameterSchema ) {
|
364 |
|
365 | me.setConfigValue( "parameterSchema", parameterSchema );
|
366 |
|
367 | return parameterSchema;
|
368 | } );
|
369 | }
|
370 |
|
371 | /**
|
372 | * Get a schema representing the request body within a valid request, in
|
373 | * JSON Schema object format.
|
374 | *
|
375 | * @public
|
376 | * @throws {Errors.MissingRequestBodySchemaError} If the schema is requested
|
377 | * but is not defined.
|
378 | * @returns {Promise<Object>} Parameter schema.
|
379 | */
|
380 | getRequestBodySchema() {
|
381 |
|
382 | const me = this;
|
383 |
|
384 | // Dependencies
|
385 | const BB = me.$dep( "bluebird" );
|
386 |
|
387 | return BB.resolve( null );
|
388 | }
|
389 |
|
390 | /**
|
391 | * Set a schema representing the parameters within a valid request, in JSON
|
392 | * Schema object format.
|
393 | *
|
394 | * @public
|
395 | * @param {Object} parameterSchema - Parameter schema.
|
396 | * @returns {void}
|
397 | */
|
398 | setParameterSchema( parameterSchema ) {
|
399 |
|
400 | const me = this;
|
401 |
|
402 | me.setConfigValue( "parameterSchema", parameterSchema );
|
403 | }
|
404 |
|
405 | /**
|
406 | * Loads the parameter schema (from a file) using the `parameterSchemaPath`.
|
407 | *
|
408 | * @private
|
409 | * @throws {Errors.MissingParameterSchemaError} If the schema could not be
|
410 | * loaded (for any reason).
|
411 | * @returns {Promise<Object>} The loaded parameter schema.
|
412 | */
|
413 | _loadParameterSchema() {
|
414 |
|
415 | const me = this;
|
416 |
|
417 | // Dependencies
|
418 | const BB = me.$dep( "bluebird" );
|
419 |
|
420 | return BB.try( function () {
|
421 |
|
422 | let projectPath = me.projectPath;
|
423 |
|
424 | /** @type Util.SchemaGenerator */
|
425 | let SchemaGenerator = require( "../util/SchemaGenerator" );
|
426 |
|
427 | let schemaGenerator = new SchemaGenerator( {
|
428 | serviceRootPath: projectPath,
|
429 | } );
|
430 |
|
431 | return schemaGenerator.buildSchema( me.parameterSchemaPath );
|
432 |
|
433 | } ).catch( function ( err ) {
|
434 |
|
435 | throw new ERRORS.MissingParameterSchemaError(
|
436 | err,
|
437 | "Failed to load the parameter schema."
|
438 | );
|
439 | } );
|
440 | }
|
441 |
|
442 | // -- request schema --
|
443 |
|
444 | /**
|
445 | * Creates a request schema by parsing and evaluating the endpoint's
|
446 | * parameter schema.
|
447 | *
|
448 | * Note: Originally we had a tangible request schema, but after considering
|
449 | * it, it seemed rather redundant since it mostly just reiterated the
|
450 | * endpoint's parameter schema (which is used, directly, in service-level
|
451 | * OpenAPI specs). So, I refactored the code to build the request schema
|
452 | * dynamically, using the parameter schema, in order to get past some
|
453 | * code that depends on a request schema. This may be temporary, and we
|
454 | * may need to go back to having a full request schema... but, this should
|
455 | * work for now.
|
456 | *
|
457 | * -- Luke, 11/14/17
|
458 | *
|
459 | * @private
|
460 | * @returns {Promise<Object>} The created request schema.
|
461 | */
|
462 | _createRequestSchema() {
|
463 |
|
464 | const me = this;
|
465 |
|
466 | // Dependencies
|
467 | const BB = me.$dep( "bluebird" );
|
468 |
|
469 | return BB.try( function () {
|
470 |
|
471 | return BB.all( [
|
472 | me.getParameterSchema(),
|
473 | me.getRequestBodySchema(),
|
474 | ] );
|
475 |
|
476 | } ).then( function ( [ parameterSchema, requestBodySchema ] ) {
|
477 |
|
478 | // Init return
|
479 | let requestSchema = {
|
480 | "$id" : "#" + me.operationId + "Request",
|
481 | "type" : "object",
|
482 | "description" : "A request for the " + me.operationId +
|
483 | " endpoint/operation.",
|
484 | "properties": {},
|
485 | };
|
486 |
|
487 | // Add request body schema if available
|
488 |
|
489 | if ( requestBodySchema ) {
|
490 |
|
491 | requestSchema.properties.body = requestBodySchema;
|
492 | }
|
493 |
|
494 | // Add parameter schema if available
|
495 |
|
496 | if ( parameterSchema ) {
|
497 |
|
498 | me._addParamSchema( requestSchema, parameterSchema );
|
499 | }
|
500 |
|
501 | return requestSchema;
|
502 | } );
|
503 | }
|
504 |
|
505 | /**
|
506 | * Converts an OpenAPI parameter into a JSON Schema object and
|
507 | * injects it into the specified parameter schema.
|
508 | *
|
509 | * @param {Object} requestSchema - Request schema object.
|
510 | * @param {Object} parameterSchema - OpenAPI formatted parameter schema.
|
511 | * @returns {void}
|
512 | * @private
|
513 | */
|
514 | _addParamSchema( requestSchema, parameterSchema ) {
|
515 |
|
516 | const me = this;
|
517 |
|
518 | // Dependencies
|
519 | const _ = me.$dep( "lodash" );
|
520 |
|
521 | requestSchema.properties.parameters = {
|
522 | "type" : "object",
|
523 | "properties" : {},
|
524 | "required" : [],
|
525 | };
|
526 |
|
527 | // Iterate over each parameter and add it to the request schema
|
528 |
|
529 | _.each( parameterSchema, function ( param ) {
|
530 |
|
531 | param = _.clone( param );
|
532 |
|
533 | let paramName = param.name;
|
534 | let paramSchema = param.schema;
|
535 | let paramDescription = param.description;
|
536 | let paramRequired = param.required || false;
|
537 |
|
538 | paramSchema.description = paramDescription;
|
539 |
|
540 | requestSchema.properties.parameters.properties[ paramName ] =
|
541 | paramSchema;
|
542 |
|
543 | if ( paramRequired ) {
|
544 |
|
545 | requestSchema.properties.parameters.required.push( paramName );
|
546 | }
|
547 | } );
|
548 | }
|
549 |
|
550 | /**
|
551 | * A schema representing a valid request, in JSON Schema object format.
|
552 | *
|
553 | * @public
|
554 | * @returns {Promise<Object>} Request schema.
|
555 | * @readonly
|
556 | */
|
557 | getRequestSchema() {
|
558 |
|
559 | const me = this;
|
560 |
|
561 | // Dependencies
|
562 | const BB = me.$dep( "bluebird" );
|
563 |
|
564 | if ( me.hasConfigValue( "requestSchema" ) ) {
|
565 |
|
566 | return BB.resolve(
|
567 | me.getConfigValue( "requestSchema" )
|
568 | );
|
569 | }
|
570 |
|
571 | return me._createRequestSchema()
|
572 | .then( function ( requestSchema ) {
|
573 |
|
574 | me.setConfigValue( "requestSchema", requestSchema );
|
575 |
|
576 | return requestSchema;
|
577 | } );
|
578 | }
|
579 |
|
580 | // -- response schema --
|
581 |
|
582 | /**
|
583 | * The absolute path to the response schema for this endpoint, which is
|
584 | * used for _successful_ response validation within certain contexts.
|
585 | *
|
586 | * @public
|
587 | * @throws {Errors.MissingResponseSchemaError} If the path to the response
|
588 | * schema is requested but not defined.
|
589 | * @type {string}
|
590 | * @readonly
|
591 | */
|
592 | get responseSchemaPath() {
|
593 |
|
594 | const me = this;
|
595 |
|
596 | if ( !me.pathManager.hasPath( "responseSchema" ) ) {
|
597 |
|
598 | throw new ERRORS.MissingResponseSchemaError(
|
599 | "All endpoints MUST have a 'Response Schema Path' defined."
|
600 | );
|
601 | }
|
602 |
|
603 | return me.pathManager.getPath( "responseSchema" );
|
604 | }
|
605 |
|
606 | /**
|
607 | * Get a schema representing a valid response, in JSON Schema object format.
|
608 | *
|
609 | * @public
|
610 | * @throws {Errors.MissingResponseSchemaError} If the schema is requested
|
611 | * but is not defined.
|
612 | * @returns {Promise<Object>} Response schema.
|
613 | */
|
614 | getResponseSchema() {
|
615 |
|
616 | const me = this;
|
617 |
|
618 | // Dependencies
|
619 | const BB = me.$dep( "bluebird" );
|
620 |
|
621 | if ( me.hasConfigValue( "responseSchema" ) ) {
|
622 |
|
623 | return BB.resolve(
|
624 | me.getConfigValue( "responseSchema" )
|
625 | );
|
626 | }
|
627 |
|
628 | return me._loadResponseSchema()
|
629 | .then( function ( responseSchema ) {
|
630 |
|
631 | me.setConfigValue( "responseSchema", responseSchema );
|
632 |
|
633 | return responseSchema;
|
634 | } );
|
635 | }
|
636 |
|
637 | /**
|
638 | * Set a schema representing a valid response, in JSON Schema object format.
|
639 | *
|
640 | * @public
|
641 | * @param {Promise<Object>} responseSchema - Response schema.
|
642 | * @throws {Errors.MissingResponseSchemaError} If the schema is requested
|
643 | * but is not defined.
|
644 | * @returns {void}
|
645 | */
|
646 | setResponseSchema( responseSchema ) {
|
647 |
|
648 | const me = this;
|
649 |
|
650 | me.setConfigValue( "responseSchema", responseSchema );
|
651 | }
|
652 |
|
653 | /**
|
654 | * Loads the response schema (from a file) using the `responseSchemaPath`.
|
655 | *
|
656 | * @private
|
657 | * @throws {Errors.MissingResponseSchemaError} If the schema could not be
|
658 | * loaded (for any reason).
|
659 | * @returns {Promise<Object>} The loaded response schema.
|
660 | */
|
661 | _loadResponseSchema() {
|
662 |
|
663 | const me = this;
|
664 |
|
665 | // const BB = me.$dep( "bluebird" );
|
666 |
|
667 | let projectPath = me.projectPath;
|
668 |
|
669 | let SchemaGenerator = require( "../util/SchemaGenerator" );
|
670 |
|
671 | let schemaGenerator = new SchemaGenerator( {
|
672 | serviceRootPath: projectPath,
|
673 | } );
|
674 |
|
675 | return schemaGenerator.buildSchema( me.responseSchemaPath )
|
676 | .catch( function ( err ) {
|
677 |
|
678 | throw new ERRORS.MissingResponseSchemaError(
|
679 | err,
|
680 | "Failed to load the response schema."
|
681 | );
|
682 | } );
|
683 | }
|
684 |
|
685 | // </editor-fold>
|
686 |
|
687 | // <editor-fold desc="--- Main Execution/Handler Logic -------------------">
|
688 |
|
689 | /**
|
690 | * This is the main entry point for endpoint execution.
|
691 | *
|
692 | * @public
|
693 | * @param {Object} event - The request information, including parameters,
|
694 | * headers, and other variables related to the request. The contents and
|
695 | * structure of this object will vary by run-time environment.
|
696 | * @param {Object} [context] - Context information, from the outside. The
|
697 | * contents and structure of this object will vary by run-time
|
698 | * environment.
|
699 | * @param {Function} [callback] - An optional callback that will be called
|
700 | * after the endpoint has executed successfully (or failed with an
|
701 | * error)
|
702 | * @returns {Promise<Object>} A promise that encompasses the entire
|
703 | * endpoint execution chain and every sub-process within.
|
704 | */
|
705 | execute( event, context, callback ) {
|
706 |
|
707 | const me = this;
|
708 |
|
709 | const BB = me.$dep( "bluebird" );
|
710 |
|
711 | return BB.try( function () {
|
712 |
|
713 | // Initialize the session manager
|
714 | return me._initSessionManager();
|
715 |
|
716 | } ).then( function () {
|
717 |
|
718 | return me._prepForExecution( event, context, callback );
|
719 |
|
720 | } ).then( function ( executionContext ) {
|
721 |
|
722 | // Refuse to execute if the environment could not be resolved.
|
723 | // (We still needed the GenericExecutionContext for a graceful exit)
|
724 | if ( me.environment === "Generic" ) {
|
725 |
|
726 | throw new ERRORS.EnvironmentResolutionError(
|
727 | "Failed to resolve environment information."
|
728 | );
|
729 | }
|
730 |
|
731 | // Log the start of it
|
732 | me.$log( "info", "Endpoint execution started." );
|
733 |
|
734 | // Execute
|
735 | return executionContext.invokeEndpointHandler();
|
736 |
|
737 | // Note:
|
738 | // The call above will, basically, ask the execution context to call
|
739 | // the #handle method on this endpoint. While this may seem a little
|
740 | // odd and redundant, we need the handle() method to be executed
|
741 | // from _within_ the execution context so that context specific logic
|
742 | // can be applied to handler success and error output.
|
743 | } );
|
744 | }
|
745 |
|
746 | /**
|
747 | * This is the first stage of the request processing. The goal of
|
748 | * this method is to prepare for execution by creating an "ExecutionContext"
|
749 | * that can be used to gather request and run-time information that will be
|
750 | * needed by the execution logic ("#handle") later on.
|
751 | *
|
752 | * @private
|
753 | * @param {Object} event - The request information, including parameters,
|
754 | * headers, and other variables related to the request. The contents and
|
755 | * structure of this object will vary by run-time environment.
|
756 | * @param {Object} [context] - Context information, from the outside. The
|
757 | * contents and structure of this object will vary by run-time
|
758 | * environment.
|
759 | * @param {Function} [callback] - An optional callback that will be called
|
760 | * after the endpoint has executed successfully (or failed with an
|
761 | * error)
|
762 | * @returns {Promise<ExecutionContext.BaseExecutionContext>} The
|
763 | * environment specific execution context.
|
764 | */
|
765 | _prepForExecution( event, context, callback ) {
|
766 |
|
767 | const me = this;
|
768 |
|
769 | // Flag that allows suspend node process with non-empty event loop
|
770 | // http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html
|
771 | // TODO: provide more information on why this is important
|
772 | context.callbackWaitsForEmptyEventLoop = false;
|
773 |
|
774 | // Convert the incoming parameters into a
|
775 | // config object, which can be more-easily
|
776 | // passed around and augmented.
|
777 | let cfg = {
|
778 | endpoint : me,
|
779 | initialContextData : {
|
780 | event : event,
|
781 | context : context,
|
782 | callback : callback,
|
783 | package : me.packageInfo,
|
784 | },
|
785 | };
|
786 |
|
787 | // Determine our run-time environment.
|
788 | let env = me._resolveEnvironment( cfg.initialContextData );
|
789 |
|
790 | // Create an "Execution Context" object that
|
791 | // is specific to our run-time environment.
|
792 |
|
793 | let executionContext = me.$spawn(
|
794 | "microservicesLib",
|
795 | "context/" + env + "ExecutionContext",
|
796 | cfg
|
797 | );
|
798 |
|
799 | return executionContext.prepare();
|
800 | }
|
801 |
|
802 | /**
|
803 | * This method represents the second stage of processing for the endpoint,
|
804 | * which is the actual fulfillment of the endpoint's function/purpose.
|
805 | * This method is public because it is called from within an "Execution
|
806 | * Context" object, which represents a combination of the run-time
|
807 | * environment and the request.
|
808 | *
|
809 | * @public
|
810 | * @param {Request.Request} request - A fully resolved request object that
|
811 | * describes the request being made to the endpoint.
|
812 | * @returns {Promise<Object>} The endpoint response.
|
813 | */
|
814 | handle( request ) {
|
815 |
|
816 | const me = this;
|
817 |
|
818 | // Dependencies
|
819 | const BB = me.$dep( "bluebird" );
|
820 |
|
821 | let endpointType = me.endpointType;
|
822 | let model = me.model;
|
823 | let modelMethod = model[ endpointType ].bind( model );
|
824 |
|
825 | // Do endpoint-level parameter parsing, coercion,
|
826 | // normalization, and validation...
|
827 | me._parseParameters( request );
|
828 |
|
829 | // Log pre-session
|
830 | me.$log( "debug", "Endpoint execution started." );
|
831 |
|
832 | // Enter the promised land...
|
833 | return BB.try( function () {
|
834 |
|
835 | // Validate the session/token...
|
836 | return me._validateSession( request );
|
837 |
|
838 | } ).then( function () {
|
839 |
|
840 | me.$log(
|
841 | "info",
|
842 | "Session validation succeeded; executing model operation."
|
843 | );
|
844 |
|
845 | // Defer to the model
|
846 | // for further processing.
|
847 | return modelMethod( request );
|
848 | } );
|
849 | }
|
850 |
|
851 | // </editor-fold>
|
852 |
|
853 | // <editor-fold desc="--- Model ------------------------------------------">
|
854 |
|
855 | /**
|
856 | * A fully instantiated model object that represents the endpoints
|
857 | * primary model.
|
858 | *
|
859 | * @public
|
860 | * @type {Model.BaseModel}
|
861 | * @readonly
|
862 | */
|
863 | get model() {
|
864 |
|
865 | const me = this;
|
866 |
|
867 | return me.getConfigValue( "model", me._initPrimaryModelObject );
|
868 | }
|
869 |
|
870 | /**
|
871 | * The name of the primary model for this endpoint. Endpoints usually
|
872 | * define a value for this in their constructor.
|
873 | *
|
874 | * @public
|
875 | * @throws {Errors.ModelRequiredError} If the primary model has not been
|
876 | * defined.
|
877 | * @type {string}
|
878 | * @readonly
|
879 | */
|
880 | get modelName() {
|
881 |
|
882 | const me = this;
|
883 |
|
884 | let val = me.getConfigValue( "modelName", null );
|
885 |
|
886 | // All endpoints MUST have a .modelName
|
887 | if ( val === null ) {
|
888 |
|
889 | throw new ERRORS.ModelRequiredError(
|
890 | "Endpoints MUST be associated with a valid data Model. " +
|
891 | "Please ensure that this endpoint handler class is being " +
|
892 | "instantiated with a configuration that either passes a " +
|
893 | "'modelName' setting or passes a pre-instantiated data " +
|
894 | "model class as the 'model' setting."
|
895 | );
|
896 | }
|
897 |
|
898 | return val;
|
899 | }
|
900 |
|
901 | // noinspection JSUnusedGlobalSymbols
|
902 | /**
|
903 | * The name of the primary model for this endpoint, but pluralized
|
904 | * (e.g. "People").
|
905 | *
|
906 | * @public
|
907 | * @throws {Errors.ModelRequiredError} If the primary model has not been
|
908 | * defined.
|
909 | * @type {string}
|
910 | * @readonly
|
911 | */
|
912 | get pluralModelName() {
|
913 |
|
914 | const me = this;
|
915 |
|
916 | // Dependencies
|
917 | const _ = me.$dep( "lodash" );
|
918 |
|
919 | return _.pluralize( me.modelName );
|
920 | }
|
921 |
|
922 | /**
|
923 | * Instantiates this endpoint's primary Model object. This method is
|
924 | * called, exclusively, by the 'model' property's getter when the model
|
925 | * object is requested for the first time.
|
926 | *
|
927 | * @private
|
928 | * @returns {Model.BaseModel} The primary model object.
|
929 | */
|
930 | _initPrimaryModelObject() {
|
931 |
|
932 | const me = this;
|
933 |
|
934 | // Spawn the model object
|
935 | let mdl = me._initModelObject( me.modelName );
|
936 |
|
937 | // Persist the model object
|
938 | me.setConfigValue( "model", mdl );
|
939 |
|
940 | return mdl;
|
941 | }
|
942 |
|
943 | /**
|
944 | * Instantiates a model object and returns it.
|
945 | *
|
946 | * @private
|
947 | * @param {string} modelName - The name of the model to spawn.
|
948 | * @returns {Model.BaseModel} The instantiated model object.
|
949 | */
|
950 | _initModelObject( modelName ) {
|
951 |
|
952 | const me = this;
|
953 |
|
954 | return me.$spawn( "models", modelName + "/" + modelName );
|
955 | }
|
956 |
|
957 | // </editor-fold>
|
958 |
|
959 | // <editor-fold desc="--- Environment & Execution Context ----------------">
|
960 |
|
961 | /**
|
962 | * The name of the current run-time environment. This property is used to
|
963 | * instantiate a {@link ExecutionContext.BaseExecutionContext} object during
|
964 | * endpoint execution.
|
965 | *
|
966 | * @public
|
967 | * @type {string}
|
968 | * @default "Generic"
|
969 | */
|
970 | get environment() {
|
971 |
|
972 | const me = this;
|
973 |
|
974 | return me.getConfigValue( "environment", "Generic" );
|
975 | }
|
976 |
|
977 | set environment( /** string */ val ) {
|
978 |
|
979 | const me = this;
|
980 |
|
981 | me.setConfigValue( "environment", val );
|
982 | }
|
983 |
|
984 | /**
|
985 | * Whenever possible, invocation sources should provide endpoints with the
|
986 | * name of the environment. In the cases where that is not possible,
|
987 | * however, endpoints will need to try to figure out what environment
|
988 | * they are being executed within. This method is the entry point for
|
989 | * that resolution logic; it is called, automatically, if the endpoint
|
990 | * was not provided an 'environment' value.
|
991 | *
|
992 | * Final: This method is marked as 'final' because the environment should
|
993 | * always resolve in the same way, regardless of the endpoint type or
|
994 | * request context.
|
995 | *
|
996 | * @private
|
997 | * @param {?Endpoint.ContextData} contextData - Invocation context data.
|
998 | * @returns {string} The name of the resolved environment (e.g.
|
999 | * "AagExecution"), which will correspond, directly, to the prefix of
|
1000 | * an ExecutionContext class name.
|
1001 | */
|
1002 | _resolveEnvironment( contextData ) {
|
1003 |
|
1004 | const me = this;
|
1005 |
|
1006 | // DEBUG
|
1007 | // me.$log( "info", contextData );
|
1008 |
|
1009 | // Dependencies
|
1010 | const _ = me.$dep( "lodash" );
|
1011 | const TIPE = me.$dep( "tipe" );
|
1012 |
|
1013 | // We can skip this if the
|
1014 | // environment is already set...
|
1015 | if ( me.hasConfigValue( "environment" ) ) {
|
1016 |
|
1017 | me.$log(
|
1018 | "info",
|
1019 | "Environment type was provided: " + me.environment
|
1020 | );
|
1021 |
|
1022 | return me.environment;
|
1023 | }
|
1024 |
|
1025 | // Ensure we have a contextData object
|
1026 | if ( TIPE( contextData ) !== "object" ) {
|
1027 |
|
1028 | contextData = {};
|
1029 | }
|
1030 |
|
1031 | if ( TIPE( contextData.event ) !== "object" ) {
|
1032 |
|
1033 | contextData.event = {};
|
1034 | }
|
1035 |
|
1036 | if ( TIPE( contextData.context ) !== "object" ) {
|
1037 |
|
1038 | contextData.context = {};
|
1039 | }
|
1040 |
|
1041 | // Check for recognizable patterns...
|
1042 | if ( process.env.TRAVIS_JOB_NUMBER !== undefined ) {
|
1043 |
|
1044 | // Identified Travis-CI
|
1045 | me.environment = "MochaCi";
|
1046 |
|
1047 | } else if ( contextData.event.isOffline === true ) {
|
1048 |
|
1049 | // Identified Serverless Offline
|
1050 | me.environment = "ServerlessOffline";
|
1051 |
|
1052 | } else if ( _.get( contextData.event, "requestContext.apiId" ) !== undefined ) {
|
1053 |
|
1054 | // Identified AAG
|
1055 | me.environment = "Aag";
|
1056 |
|
1057 | } else if ( contextData.context.invokeid !== undefined ) {
|
1058 |
|
1059 | // Identified AAG
|
1060 | me.environment = "LambdaInvoke";
|
1061 |
|
1062 | } else {
|
1063 |
|
1064 | // Couldn't figure it out...
|
1065 | me.environment = "Generic";
|
1066 | }
|
1067 |
|
1068 | me.$log( "info", "Environment type was resolved: " + me.environment );
|
1069 |
|
1070 | return me.environment;
|
1071 | }
|
1072 |
|
1073 | // </editor-fold>
|
1074 |
|
1075 | // <editor-fold desc="--- Sessions & Tokens ------------------------------">
|
1076 |
|
1077 | /**
|
1078 | * Defines whether or not the endpoint requires clients to have a valid
|
1079 | * "Session Token" when executing this endpoint. Most endpoints _should_
|
1080 | * require session tokens. The only known exceptions, at this time, are
|
1081 | * endpoints that create or update session tokens, such as `POST /Sessions`.
|
1082 | *
|
1083 | * The value for this property is usually set by endpoints within their
|
1084 | * constructor.
|
1085 | *
|
1086 | * @public
|
1087 | * @type {boolean}
|
1088 | * @default true
|
1089 | */
|
1090 | get requireSession() {
|
1091 |
|
1092 | const me = this;
|
1093 |
|
1094 | return me.getConfigValue( "requireSession", true );
|
1095 | }
|
1096 |
|
1097 | // noinspection JSUnusedGlobalSymbols
|
1098 | set requireSession( /** boolean */ val ) {
|
1099 |
|
1100 | const me = this;
|
1101 |
|
1102 | me.setConfigValue( "requireSession", val );
|
1103 | }
|
1104 |
|
1105 | /**
|
1106 | * Returns a {@link Session.SessionManager} object, which can be used to
|
1107 | * manage the current user session and its token.
|
1108 | *
|
1109 | * @public
|
1110 | * @type {Session.SessionManager}
|
1111 | */
|
1112 | get sessionManager() {
|
1113 |
|
1114 | const me = this;
|
1115 |
|
1116 | return me.getConfigValue( "sessionManager", me._initSessionManager );
|
1117 | }
|
1118 |
|
1119 | // noinspection JSUnusedGlobalSymbols
|
1120 | set sessionManager( /** Session.SessionManager */ val ) {
|
1121 |
|
1122 | const me = this;
|
1123 |
|
1124 | me.setConfigValue( "sessionManager", val );
|
1125 | me.$adopt( val );
|
1126 | }
|
1127 |
|
1128 | /**
|
1129 | * Initializes a SessionManager object and attaches it to this endpoint.
|
1130 | *
|
1131 | * This method is called, exclusively, by the `constructor`.
|
1132 | *
|
1133 | * @private
|
1134 | * @returns {void} The session manager is instantiated and then stored
|
1135 | * in the 'sessionManager' property.
|
1136 | */
|
1137 | _initSessionManager() {
|
1138 |
|
1139 | const me = this;
|
1140 |
|
1141 | // We'll only ever need one...
|
1142 | if ( me.hasConfigValue( "sessionManager" ) ) {
|
1143 |
|
1144 | return;
|
1145 | }
|
1146 |
|
1147 | // Spawn the session manager
|
1148 | let sm = me.$spawn( "microservicesLib", "session/SessionManager", {
|
1149 | endpoint: me,
|
1150 | } );
|
1151 |
|
1152 | // Persist the session manager
|
1153 | me.setConfigValue( "sessionManager", sm );
|
1154 | }
|
1155 |
|
1156 | /**
|
1157 | * This method invokes the SessionManager to validate the session/token.
|
1158 | *
|
1159 | * @private
|
1160 | * @param {Request.Request} request - A fully resolved request object that
|
1161 | * describes the request being made to the endpoint.
|
1162 | * @returns {Promise<Request.Request>} A promise, resolved with the
|
1163 | * provided 'Request' object, unless session validation fails. If
|
1164 | * validation fails, then errors will be THROWN and should be caught
|
1165 | * higher up in the chain.
|
1166 | */
|
1167 | _validateSession( request ) {
|
1168 |
|
1169 | const me = this;
|
1170 |
|
1171 | return me.sessionManager.validateRequest( request );
|
1172 | }
|
1173 |
|
1174 | // </editor-fold>
|
1175 |
|
1176 | // <editor-fold desc="--- Abstract Methods -------------------------------">
|
1177 |
|
1178 | /**
|
1179 | * Applies endpoint-specific parameter parsing. This method is called,
|
1180 | * exclusively, by an ExecutionContext object.
|
1181 | *
|
1182 | * If any parameter is found to be invalid, child methods should
|
1183 | * THROW errors with the appropriate status code.
|
1184 | *
|
1185 | * Child methods may, also, freely modify the request object, as needed,
|
1186 | * if coercion (as opposed to rejection) is desired.
|
1187 | *
|
1188 | * @abstract
|
1189 | * @private
|
1190 | * @param {Request.Request} request - A Request object.
|
1191 | * @returns {void} Child methods should either THROW errors or modify the
|
1192 | * request object byRef.
|
1193 | */
|
1194 | _parseParameters( request ) { // eslint-disable-line no-unused-vars
|
1195 |
|
1196 | // FIXME: should private methods encourage overriding?
|
1197 | // FIXME: should this be declared as @template instead of @abstract?
|
1198 |
|
1199 | // The default behavior is to do nothing.
|
1200 | // Child classes should override this method
|
1201 | // if they need special parameter parsing.
|
1202 | }
|
1203 |
|
1204 | // noinspection JSUnusedGlobalSymbols
|
1205 | /**
|
1206 | * <this is a while away>
|
1207 | *
|
1208 | * Applies endpoint-specific access verification. This method is called,
|
1209 | * exclusively, by an ExecutionContext object.
|
1210 | *
|
1211 | * If access is denied, child methods should THROW errors with
|
1212 | * the appropriate status code.
|
1213 | *
|
1214 | * Alternatively, child methods may modify the request if complete
|
1215 | * request denial is not warranted.
|
1216 | *
|
1217 | * @abstract
|
1218 | * @protected
|
1219 | * @param {Request.Request} request - A Request object.
|
1220 | * @returns {void} Child methods should THROW if access is denied, or they
|
1221 | * may modify the request object byRef.
|
1222 | */
|
1223 | checkAccess( request ) {
|
1224 |
|
1225 | // The default behavior is to allow execution.
|
1226 | // Child classes should override this method
|
1227 | // if they need to apply ACM logic.
|
1228 | }
|
1229 |
|
1230 | // </editor-fold>
|
1231 |
|
1232 | // Misc / Needs Work:
|
1233 |
|
1234 | /**
|
1235 | * The contents of the endpoint service's package.json file.
|
1236 | *
|
1237 | * @public
|
1238 | * @type {Object}
|
1239 | * @readonly
|
1240 | * @todo Remove this or move it to BaseExecutionContext.
|
1241 | */
|
1242 | get packageInfo() {
|
1243 |
|
1244 | const me = this;
|
1245 |
|
1246 | let cur = me.getConfigValue( "packageInfo", null );
|
1247 |
|
1248 | if ( cur === null ) {
|
1249 |
|
1250 | let pm = me.pathManager;
|
1251 | let pkgPath = pm.join( "project", "package.json" );
|
1252 |
|
1253 | cur = require( pkgPath );
|
1254 | me.setConfigValue( "packageInfo", cur );
|
1255 | }
|
1256 |
|
1257 | return cur;
|
1258 | }
|
1259 |
|
1260 | /**
|
1261 | * The name of the endpoint's service (from package.json).
|
1262 | *
|
1263 | * @public
|
1264 | * @type {string}
|
1265 | * @readonly
|
1266 | * @todo Remove this or move it to BaseExecutionContext.
|
1267 | */
|
1268 | get serviceName() {
|
1269 |
|
1270 | const me = this;
|
1271 |
|
1272 | return me.packageInfo.name;
|
1273 | }
|
1274 |
|
1275 | /**
|
1276 | * The current version of the endpoint's service repo (from package.json).
|
1277 | *
|
1278 | * @public
|
1279 | * @type {string}
|
1280 | * @readonly
|
1281 | * @todo Remove this or move it to BaseExecutionContext.
|
1282 | */
|
1283 | get serviceVersion() {
|
1284 |
|
1285 | const me = this;
|
1286 |
|
1287 | return me.packageInfo.version;
|
1288 | }
|
1289 |
|
1290 | /**
|
1291 | * The operationId for the current endpoint, which is its class name.
|
1292 | *
|
1293 | * @public
|
1294 | * @type {string}
|
1295 | * @readonly
|
1296 | * @todo Decide if this should be moved to BaseExecutionContext.
|
1297 | */
|
1298 | get operationId() {
|
1299 |
|
1300 | const me = this;
|
1301 |
|
1302 | return me.constructor.name;
|
1303 | }
|
1304 | }
|
1305 |
|
1306 | module.exports = BaseEndpoint;
|