UNPKG

31.4 kBJavaScriptView Raw
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"use strict";
12
13const BaseClass = require( "@corefw/common" ).common.BaseClass;
14const ERRORS = require( "../errors" );
15
16/**
17 * The parent class for all endpoints.
18 *
19 * @memberOf Endpoint
20 * @extends Common.BaseClass
21 */
22class 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
1306module.exports = BaseEndpoint;