UNPKG

38 kBJavaScriptView Raw
1/**
2 * @file Defines the BaseGenerator class.
3 *
4 * @author Luke Chavers <luke@c2cschools.com>
5 * @author Kevin Sanders <kevin@c2cschools.com>
6 * @since 5.1.15
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;
14
15/**
16 * Defines the base class for special utilities called "Generators", which
17 * generate various files and artifacts based on service and endpoint
18 * configuration data.
19 *
20 * @abstract
21 * @memberOf Util
22 * @extends Common.BaseClass
23 */
24class BaseGenerator extends BaseClass {
25
26 /**
27 * @inheritDoc
28 */
29 _initialize( cfg ) {
30
31 const me = this;
32
33 // Dependencies
34 const _ = me.$dep( "lodash" );
35
36 // Call parent
37 super._initialize( cfg );
38
39 if ( !_.isPlainObject( cfg ) || !_.isString( cfg.serviceRootPath ) ) {
40
41 throw new Error(
42 "All generators require the 'serviceRootPath' configuration " +
43 "property to be defined when being instantiated."
44 );
45 }
46
47 // Store the absolute path to the target service
48 me._serviceRootPath = cfg.serviceRootPath;
49
50 // Store the service name, if provided. It not provided it will be
51 // determined automatically from the service's package.json name.
52 me._serviceName = cfg.serviceName;
53
54 // Store KmsCrypt config. This is used to decrypt sensitive information
55 // stored in S3 and KMS.
56 me._kmsCryptConfig = cfg.kmsCryptConfig;
57
58 // If a 'configVariant' was defined in the constructor config,
59 // save it...
60 if ( cfg.configVariant !== undefined && cfg.configVariant !== null ) {
61 me._configVariant = cfg.configVariant;
62 }
63
64 // Initialize a logger object
65 me._initLogger();
66
67 }
68
69 /**
70 * The absolute path to the Serverless config directory in
71 * core-microservices, based on the generators current `configVariant`
72 * setting (e.g. `serverless/config/default`).
73 *
74 * @public
75 * @type {string}
76 * @readonly
77 */
78 get commonConfigRoot() {
79
80 const me = this;
81
82 // Dependencies
83 const PATH = me.$dep( "path" );
84
85 return PATH.join(
86 me.toolsLibRoot,
87 "serverless/config",
88 me.configVariant
89 );
90 }
91
92 /**
93 * The absolute path to the common schema directory in
94 * core-microservices (`schema`).
95 *
96 * @public
97 * @type {string}
98 * @readonly
99 */
100 get commonSchemaRoot() {
101
102 const me = this;
103
104 // Dependencies
105 const PATH = me.$dep( "path" );
106
107 return PATH.join( me.toolsLibRoot, "schema" );
108 }
109
110 /**
111 * The absolute path to the common schema definitions directory
112 * in core-microservices (`schema/definitions`).
113 *
114 * @public
115 * @type {string}
116 * @readonly
117 */
118 get commonSchemaDefinitionRoot() {
119
120 const me = this;
121
122 // Dependencies
123 const PATH = me.$dep( "path" );
124
125 return PATH.join( me.commonSchemaRoot, "definitions" );
126 }
127
128 /**
129 * The service name. If not provided explicitly during class initialization,
130 * then it will be determined automatically using the service project's
131 * package.json file.
132 *
133 * @public
134 * @type {string}
135 * @readonly
136 */
137 get serviceName() {
138
139 const me = this;
140
141 // Dependencies
142 const _ = me.$dep( "lodash" );
143 const PATH = me.$dep( "path" );
144
145 if ( _.isNil( me._serviceName ) ) {
146
147 let servicePackagePathAbs = PATH.resolve( me.serviceRootPath, "./package" );
148
149 me._serviceName = require( servicePackagePathAbs ).name;
150 }
151
152 return me._serviceName;
153 }
154
155 /**
156 * The absolute path to the root directory (repo/project root) for
157 * the service that this generator is generating files or artifacts for.
158 *
159 * @public
160 * @type {string}
161 * @readonly
162 */
163 get serviceRootPath() {
164
165 const me = this;
166
167 return me._serviceRootPath;
168 }
169
170 /**
171 * KmsCrypt configuration data. Used to decrypt and apply sensitive
172 * information via S3 and KMS services.
173 *
174 * @public
175 * @type {Object}
176 * @readonly
177 */
178 get kmsCryptConfig() {
179
180 const me = this;
181
182 return me._kmsCryptConfig;
183 }
184
185 /**
186 * The absolute path to the 'endpoints' directory (`lib/endpoints`) for
187 * the service that this generator is generating files or artifacts for.
188 *
189 * @public
190 * @type {string}
191 * @readonly
192 */
193 get serviceEndpointPath() {
194
195 const me = this;
196
197 // Dependencies
198 const PATH = me.$dep( "path" );
199
200 return PATH.join( me.serviceRootPath, "lib", "endpoints" );
201 }
202
203 /**
204 * Provides a way for implementors to specify a different set of
205 * common config/schema/etc that this generator will use when
206 * generating files and artifacts.
207 *
208 * @public
209 * @type {string}
210 * @default "default"
211 */
212 get configVariant() {
213
214 const me = this;
215
216 if ( me._configVariant === undefined || me._configVariant === null ) {
217
218 return "default";
219 }
220
221 return me._configVariant;
222 }
223
224 set configVariant( /** string */ newVal ) {
225
226 const me = this;
227
228 me._configVariant = newVal;
229 }
230
231 // noinspection JSMethodCanBeStatic
232 /**
233 * The path to the root directory of this library (core-microservices).
234 *
235 * @public
236 * @type {string}
237 * @readonly
238 */
239 get toolsLibRoot() {
240
241 const me = this;
242
243 // Dependencies
244 const PATH = me.$dep( "path" );
245
246 return PATH.join( __dirname, "../.." );
247 }
248
249 /**
250 * Reads the contents of a "common file" and returns it as a string.
251 *
252 * @protected
253 * @param {string} relPath - The relative path of the file to read; this
254 * path should be relative to the `commonConfigRoot` path.
255 * @returns {Buffer | string} The file contents.
256 */
257 _readCommonFile( relPath ) {
258
259 const me = this;
260
261 // Dependencies
262 const PATH = me.$dep( "path" );
263 const FS = me.$dep( "fs" );
264
265 // Resolve absolute path
266 let absPath = PATH.join( me.commonConfigRoot, relPath );
267
268 // Read the file and return it
269 return FS.readFileSync( absPath, {
270 encoding: "utf-8",
271 } );
272 }
273
274 /**
275 * Reads the contents of a "common YAML file" (via `loadYamlFile()`)
276 * and returns it as an object.
277 *
278 * @protected
279 * @param {string} relPath - The relative path of the file to read; this
280 * path should be relative to the `commonConfigRoot` path.
281 * @param {?string} [property=null] - When provided, and not null, this
282 * method will return the value of a property from within the loaded
283 * YAML file. If not provided, or NULL, then the root object will be
284 * returned.
285 * @returns {Object} The file contents.
286 */
287 _loadCommonConfigFile( relPath, property ) {
288
289 const me = this;
290
291 // Dependencies
292 const PATH = me.$dep( "path" );
293
294 let absPath = PATH.join( me.commonConfigRoot, relPath );
295
296 return me.loadYamlFile( absPath, property );
297 }
298
299 /**
300 * Reads the contents of a "common schema file" (via `loadYamlFile()`)
301 * and returns it as an object.
302 *
303 * @protected
304 * @param {string} relPath - The relative path of the file to read; this
305 * path should be relative to the `commonSchemaRoot` path.
306 * @param {?string} [property=null] - When provided, and not null, this
307 * method will return the value of a property from within the loaded
308 * YAML file. If not provided, or NULL, then the root object will be
309 * returned.
310 * @returns {Object} The file contents.
311 */
312 _loadCommonSchemaFile( relPath, property ) {
313
314 const me = this;
315
316 // Dependencies
317 const PATH = me.$dep( "path" );
318
319 let absPath = PATH.join( me.commonSchemaRoot, relPath );
320
321 return me.loadYamlFile( absPath, property );
322 }
323
324 // noinspection JSMethodCanBeStatic
325 /**
326 * Reads the contents of a JSON file (via `require()`) and returns
327 * it as an object.
328 *
329 * @protected
330 * @param {string} absPath - The absolute path of the file to read
331 * @param {?string} [property=null] - When provided, and not null, this
332 * method will return the value of a property from within the loaded
333 * JSON file. If not provided, or NULL, then the root object will be
334 * returned.
335 * @returns {Object} The file contents.
336 */
337 _loadJsonFile( absPath, property ) {
338
339 const data = require( absPath );
340
341 if ( property === undefined || property === null ) {
342
343 return data;
344 }
345
346 return data[ property ];
347 }
348
349 /**
350 * Returns the details of a single service endpoint.
351 *
352 * @uses getServiceEndpoints
353 * @public
354 * @param {string} endpointName - The name of the endpoint to return the
355 * details for. This value is case-insensitive.
356 * @param {boolean} [force=false] - When TRUE, the details of all endpoints
357 * will be reloaded, from disk, before this method returns a value.
358 * @returns {?Object} The details of one endpoint or NULL if the
359 * endpoint was not found or is invalid.
360 */
361 getServiceEndpoint( endpointName, force ) {
362
363 const me = this;
364
365 // Dependencies
366 const _ = me.$dep( "lodash" );
367
368 let allEndpoints = me.getServiceEndpoints( force );
369 let ret = null;
370
371 endpointName = endpointName.toLowerCase();
372
373 _.each( allEndpoints, function ( epDetails, epName ) {
374
375 if ( endpointName === epName.toLowerCase() ) {
376
377 ret = epDetails;
378
379 return false;
380 }
381
382 return true;
383 } );
384
385 return ret;
386 }
387
388 /**
389 * Returns the details of all endpoints within the target service.
390 *
391 * @public
392 * @param {boolean} [force=false] - When TRUE, the details of all endpoints
393 * will be reloaded, from disk, before this method returns a value.
394 * @returns {?Object} A plain object containing the details of every
395 * endpoint, keyed by endpoint name.
396 */
397 getServiceEndpoints( force ) {
398
399 const me = this;
400
401 // Dependencies
402 const _ = me.$dep( "lodash" );
403
404 // Param validation/coercion
405 if ( force !== true ) {
406
407 force = false;
408 }
409
410 if ( force || me._endpointDetailsCache === undefined ) {
411
412 // Scan the endpoints directory for files
413 // named 'serverless-function.yml'
414 let pattern = /serverless-function\.yml/;
415
416 let epConfigFiles = me._findFilesWithPattern(
417 me.serviceEndpointPath,
418 pattern
419 );
420
421 // Initialize the return object
422 let endpointDetails = {};
423
424 // Iterate over each endpoint, building each one's details
425 _.each( epConfigFiles, function ( configFileInfo, configFilePath ) {
426
427 // Defer to `_resolveEndpointDetails` for the bulk of the work
428 let epDetails = me._resolveEndpointDetails( configFilePath );
429
430 // Persist the endpoint details to the return object
431 endpointDetails[ epDetails.endpointName ] = epDetails;
432 } );
433
434 // Store the endpoint details in a memory cache
435 me._endpointDetailsCache = endpointDetails;
436 }
437
438 // All done
439 return me._endpointDetailsCache;
440 }
441
442 /**
443 * Resolves the details of a single service endpoint when given
444 * the path to that endpoint's `serverless-function.yml` file.
445 *
446 * @private
447 * @param {string} configPath - The absolute path to an endpoint's
448 * `serverless-function.yml` configuration file.
449 * @returns {Object} The endpoint details.
450 */
451 _resolveEndpointDetails( configPath ) {
452
453 const me = this;
454
455 // Dependencies
456 const PATH = me.$dep( "path" );
457
458 let spl = configPath.split( PATH.sep );
459
460 // Init the return
461 let ret = {
462 functionConfigPath: configPath,
463 };
464
465 // Append config file name
466 ret.functionConfigFilename = spl.pop();
467
468 // Append the config path
469 ret.configRootPath = spl.join( PATH.sep );
470 ret.configDirName = spl.pop();
471
472 // Append the endpoint root directory
473 ret.endpointRoot = spl.join( PATH.sep );
474 ret.endpointRootRel = ret.endpointRoot.replace( me.serviceRootPath, "" );
475
476 // Append the endpoint [class] name
477 ret.endpointName = spl[ spl.length - 1 ];
478
479 // Append the http method
480 ret.httpMethod = spl[ spl.length - 2 ];
481 ret.httpMethodUC = ret.httpMethod.toUpperCase();
482 ret.httpMethodLC = ret.httpMethod.toLowerCase();
483
484 // Resolve the schema directory
485 ret.schemaRootPath = PATH.join( ret.endpointRoot, "schema" );
486
487 // Parameter config
488 ret.parameterConfigPath = PATH.join(
489 ret.schemaRootPath, "Parameters.yml"
490 );
491
492 ret.parameterConfig = me._loadParameterConfig( ret );
493
494 // Path config
495 ret.pathConfigPath = PATH.join(
496 ret.schemaRootPath, "Paths.yml"
497 );
498
499 ret.pathConfig = me._loadPathConfig( ret );
500
501 // Load and parse the function config
502 ret.functionConfig = me._loadEpFunctionConfig( ret );
503
504 // Done
505 return ret;
506 }
507
508 /**
509 * A plain object containing default values for function configurations;
510 * these defaults will be loaded from the common config file,
511 * 'function-defaults.yml', and will be used to fill in any values that
512 * a loaded 'serverless-function.yml' file does not define.
513 *
514 * @public
515 * @type {Object}
516 * @readonly
517 */
518 get functionDefaults() {
519
520 const me = this;
521
522 // Dependencies
523 const _ = me.$dep( "lodash" );
524
525 // Read from cache or load the config file...
526 if (
527 me._functionDefaults === undefined ||
528 me._functionDefaults === null
529 ) {
530
531 me._functionDefaults =
532 me._loadCommonConfigFile( "function-defaults.yml" );
533 }
534
535 // Out of paranoia, we clone the default config
536 // to ensure that it cannot be modified by
537 // subsequent processing.
538 return _.cloneDeep( me._functionDefaults );
539 }
540
541 /**
542 * A plain object containing default values for 'http' events within
543 * function configurations; these defaults will be loaded from the common
544 * config file, 'http-event-defaults.yml', and will be used to fill in any
545 * values that any http events declarations do not define.
546 *
547 * @public
548 * @type {Object}
549 * @readonly
550 */
551 get httpEventDefaults() {
552
553 const me = this;
554
555 // Dependencies
556 const _ = me.$dep( "lodash" );
557
558 // Read from cache or load the config file...
559 if (
560 me._httpEventDefaults === undefined ||
561 me._httpEventDefaults === null
562 ) {
563
564 me._httpEventDefaults =
565 me._loadCommonConfigFile( "http-event-defaults.yml" );
566 }
567
568 // Out of paranoia, we clone the default config
569 // to ensure that it cannot be modified by
570 // subsequent processing.
571 return _.cloneDeep( me._httpEventDefaults );
572 }
573
574 /**
575 * A plain object containing default values for 'http' events that
576 * are automatically created by paths found within an endpoint's Paths.yml
577 * schema definition file.
578 *
579 * @public
580 * @type {Object}
581 * @readonly
582 */
583 get httpEventScaffold() {
584
585 const me = this;
586
587 // Dependencies
588 const _ = me.$dep( "lodash" );
589
590 // Read from cache or load the config file...
591 if (
592 me._httpEventScaffold === undefined ||
593 me._httpEventScaffold === null
594 ) {
595
596 me._httpEventScaffold =
597 me._loadCommonConfigFile( "http-event-scaffold.yml" );
598 }
599
600 // Out of paranoia, we clone the default config
601 // to ensure that it cannot be modified by
602 // subsequent processing.
603 return _.cloneDeep( me._httpEventScaffold );
604 }
605
606 /**
607 * This helper method assists `_resolveEndpointDetails()` by loading
608 * the `serverless-function.yml` configuration file and parsing it
609 * for special variables.
610 *
611 * @private
612 * @param {Object} epDetailsObj - A special endpoint details object that
613 * is constructed by `_resolveEndpointDetails()`
614 * @returns {Object} The endpoint function configuration.
615 */
616 _loadEpFunctionConfig( epDetailsObj ) {
617
618 const me = this;
619
620 // Dependencies
621 const _ = me.$dep( "lodash" );
622
623 let defaults = me.functionDefaults;
624
625 // Initialize an object store that will
626 // be used to allow special variables in the config.
627 let parseVars = {};
628
629 // Param validation
630 if ( !_.isPlainObject( epDetailsObj ) ) {
631
632 throw new Error(
633 "Missing or invalid endpoint details object (epDetailsObj) " +
634 "in BaseGenerator#_loadEpFunctionConfig()."
635 );
636 }
637
638 // Read the serverless-function.yml file...
639 let cfg = me.loadYamlFile( epDetailsObj.functionConfigPath );
640
641 // Apply function config defaults
642 cfg = _.merge( defaults, cfg );
643
644 // Load each endpoint detail property (built prior) into
645 // a special variables object that will allow endpoint data
646 // to be injected into the `serverless-function.yml` file
647 // using special variables (such as `${endpointRoot}`).
648 _.each( epDetailsObj, function ( val, key ) {
649
650 // We only want to allow strings to be injected...
651 if ( _.isString( val ) ) {
652
653 parseVars[ key ] = val;
654 }
655 } );
656
657 // If the provided function has an 'Paths.yml' configuration,
658 // then we will ensure that each path within it is automatically
659 // declared as an 'http' event.
660 cfg = me._addAutomaticEventsToFunction( cfg, epDetailsObj );
661
662 // Apply event defaults
663 cfg = me._applyEventDefaults( cfg );
664
665 // Parse the config object from `serverless-function.yml`, injecting
666 // the endpoint data variables as necessary.
667 cfg = me._parseObjectForVariables( cfg, parseVars );
668
669 // All done
670 return cfg;
671 }
672
673 /**
674 * Applies function-event-level defaults from the Serverless
675 * config templates.
676 *
677 * @private
678 * @param {Object} cfg - A function configuration object (from
679 * `serverless-function.yml`)
680 * @returns {Object} The `cfg` object, with defaults applied.
681 */
682 _applyEventDefaults( cfg ) {
683
684 const me = this;
685
686 // Dependencies
687 const _ = me.$dep( "lodash" );
688
689 // Iterate over each of the function's "events"
690 _.each( cfg.events, function ( eventWrapper ) {
691
692 // Serverless event configs have a somewhat strange
693 // structure, so we have to break it down a bit...
694 _.each( eventWrapper, function ( ev, evType ) {
695
696 let newConfig;
697
698 switch ( evType.toLowerCase() ) {
699
700 case "http":
701
702 newConfig = _.merge( ev, me.httpEventDefaults );
703 break;
704
705 default:
706
707 newConfig = ev;
708 break;
709 }
710
711 // Apply the new config
712 eventWrapper[ evType ] = newConfig;
713 } );
714 } );
715
716 // All done
717 return cfg;
718 }
719
720 /**
721 * This helper method assists `_loadEpFunctionConfig()` by adding automatic
722 * events to endpoint functions based on information gathered from other
723 * sources (such as the endpoint's Paths.yml schema).
724 *
725 * @private
726 * @param {Object} cfg - The function's config.
727 * @param {Object} epDetailsObj - A special endpoint details object that
728 * is constructed by `_resolveEndpointDetails()`
729 * @returns {Object} The function's config (`cfg`), potentially with
730 * events added.
731 */
732 _addAutomaticEventsToFunction( cfg, epDetailsObj ) {
733
734 const me = this;
735
736 // Dependencies
737 const _ = me.$dep( "lodash" );
738
739 // If the endpoint does not have a Paths.yml schema, then
740 // we won't add any events automatically, so we can exit early...
741 if (
742 epDetailsObj.pathConfig === undefined ||
743 epDetailsObj.pathConfig === null
744 ) {
745
746 return cfg;
747 }
748
749 // Ensure that our function config has an 'events' property
750 if ( !_.isArray( cfg.events ) ) {
751
752 cfg.events = [];
753 }
754
755 // Iterate over each path and build the event config
756 _.each( epDetailsObj.pathConfig, function ( pathSchema, pathName ) {
757
758 // Normalize path name...
759 pathName = me._normalizeConfigHttpPath( pathName );
760
761 // We will not create an automatic event if it is already
762 // defined within a http event.
763 if (
764 !me._functionConfigHasPath(
765 cfg, pathName, "http"
766 )
767 ) {
768
769 // Generate the new event
770 let newEvent = me._buildHttpEvent( pathName, pathSchema );
771
772 // Add the automatic event...
773 cfg.events.push( newEvent );
774 }
775 } );
776
777 return cfg;
778 }
779
780 /**
781 * Constructs a 'http' event configuration object for a given
782 * path schema (from within an endpoint's Paths.yml schema)
783 *
784 * @private
785 * @param {string} pathName - The path from a Paths.yml schema file
786 * @param {Object} pathSchema - The full schema for the path.
787 * @returns {Object} A 'http' event...
788 */
789 _buildHttpEvent( pathName, pathSchema ) {
790
791 const me = this;
792
793 // Dependencies
794 const _ = me.$dep( "lodash" );
795
796 // Start with the scaffold...
797 let ret = me.httpEventScaffold;
798
799 // Create a variables object to parse
800 // for path-level variables...
801 let parseVars = {
802 httpPath: pathName,
803 };
804
805 // Find the HTTP method...
806 _.each( pathSchema, function ( methodSchema, methodName ) {
807
808 parseVars.httpMethod = methodName;
809 parseVars.httpMethodLC = methodName.toLowerCase();
810 parseVars.httpMethodUC = methodName.toUpperCase();
811 } );
812
813 // Apply variables to the scaffold
814 ret = me._parseObjectForVariables( ret, parseVars );
815
816 // All done
817 return ret;
818 }
819
820 /**
821 * Checks a function configuration's event property to see if the
822 * provided path has already been defined within it.
823 *
824 * @protected
825 * @param {Object} cfg - The function configuration object.
826 * @param {string} pathName - The path to check for
827 * @param {?string|string[]} [eventType=null] - If passed and non-null, then
828 * the path will only be searched for within events of the specified
829 * type, or types (if an array is passed). If not provided, or NULL,
830 * then all event types will be searched.
831 * @returns {boolean} TRUE if the function config contains the provided
832 * path, or FALSE otherwise.
833 */
834 _functionConfigHasPath( cfg, pathName, eventType ) {
835
836 const me = this;
837
838 // Dependencies
839 const _ = me.$dep( "lodash" );
840
841 let eventTypes = {};
842 let ret = false;
843
844 // Normalize the path...
845 pathName = me._normalizeConfigHttpPath( pathName );
846
847 // Normalize the eventType parameter as
848 // either an object or NULL
849 if ( eventType === undefined || eventType === null ) {
850
851 eventTypes = null;
852
853 } else if ( _.isPlainObject( eventType ) ) {
854
855 // If a plain object is passed, we'll just
856 // use it verbatim...
857 eventTypes = eventType;
858
859 } else if ( _.isArray( eventType ) ) {
860
861 // If an array is passed, we'll convert it an
862 // object for easy indexing/checking.
863 _.each( eventType, function ( et ) {
864
865 et = et.toLowerCase();
866 eventTypes[ et ] = et;
867 } );
868
869 } else if ( _.isString( eventType ) ) {
870
871 // If an string is passed, we'll convert it an
872 // object for easy indexing/checking.
873 eventType = eventType.toLowerCase();
874 eventTypes[ eventType ] = eventType;
875
876 } else {
877
878 // Error on anything else...
879 throw new Error(
880 "Invalid value passed for 'eventType' in " +
881 "BaseGenerator#_functionConfigHasPath()."
882 );
883 }
884
885 // Exit early if the cfg doesn't have any events...
886 if (
887 cfg.events === undefined ||
888 !_.isArray( cfg.events ) ||
889 cfg.events.length === 0
890 ) {
891
892 return ret;
893 }
894
895 // Iterate over each event
896 _.each( cfg.events, function ( evWrapper ) {
897
898 // Serverless event configs have a somewhat strange
899 // structure, so we have to break it down a bit...
900 _.each( evWrapper, function ( ev, evType ) {
901
902 // Types will be compared as case-insensitive
903 evType = evType.toLowerCase();
904
905 // If the path has already been found,
906 // then we can stop iterating...
907 if ( ret === true ) {
908
909 return false;
910 }
911
912 // Only search for paths within certain types of events...
913 if (
914 eventTypes === null ||
915 eventTypes[ evType ] !== undefined
916 ) {
917
918 // Ensure we have a valid path...
919 if ( ev.path !== undefined && _.isString( ev.path ) ) {
920
921 let cfgPath = me._normalizeConfigHttpPath( ev.path );
922
923 // Check the path...
924 if ( cfgPath === pathName ) {
925
926 // Set the return
927 ret = true;
928 }
929 }
930 }
931
932 return true;
933 } );
934 } );
935
936 // Done
937 return ret;
938 }
939
940 // noinspection JSMethodCanBeStatic
941 /**
942 * Ensures that a provided API path is in the appropriate format
943 * to be used within an 'http' event as part of
944 * a Serverless function config.
945 *
946 * One of the main uses for this method is the conversion of OpenAPI
947 * specification style paths into Serverless config style paths.
948 *
949 * @protected
950 * @param {string} pathName - The path name to normalize.
951 * @returns {string} The 'pathName' parameter, after normalization.
952 */
953 _normalizeConfigHttpPath( pathName ) {
954
955 const me = this;
956
957 // Dependencies
958 const _ = me.$dep( "lodash" );
959
960 // Trim
961 pathName = _.trim( pathName, " /" );
962
963 // All done
964 return pathName;
965 }
966
967 // noinspection JSMethodCanBeStatic
968 /**
969 * This helper method assists `_resolveEndpointDetails()` by loading
970 * the `Parameters.yml` schema for a single endpoint, if it exists.
971 *
972 * @private
973 * @param {Object} epDetailsObj - A special endpoint details object that
974 * is constructed by `_resolveEndpointDetails()`
975 * @returns {?Object} The contents of `Parameters.yml` or NULL if the
976 * file was not found for the endpoint specified in `epDetailsObj`.
977 */
978 _loadParameterConfig( epDetailsObj ) {
979
980 const me = this;
981
982 let ret;
983
984 try {
985
986 let tmp = me.loadYamlFile( epDetailsObj.parameterConfigPath );
987
988 if ( tmp.parameters !== undefined && tmp.parameters !== null ) {
989
990 ret = tmp.parameters;
991
992 } else {
993
994 ret = tmp;
995 }
996
997 } catch ( e ) {
998
999 ret = null;
1000 }
1001
1002 return ret;
1003 }
1004
1005 // noinspection JSMethodCanBeStatic
1006 /**
1007 * This helper method assists `_resolveEndpointDetails()` by loading
1008 * the `Paths.yml` schema for a single endpoint, if it exists.
1009 *
1010 * @private
1011 * @param {Object} epDetailsObj - A special endpoint details object that
1012 * is constructed by `_resolveEndpointDetails()`
1013 * @returns {?Object} The contents of `Paths.yml` or NULL if the
1014 * file was not found for the endpoint specified in `epDetailsObj`.
1015 */
1016 _loadPathConfig( epDetailsObj ) {
1017
1018 const me = this;
1019
1020 let ret;
1021
1022 try {
1023
1024 let tmp = me.loadYamlFile( epDetailsObj.pathConfigPath );
1025
1026 if ( tmp.paths !== undefined && tmp.paths !== null ) {
1027
1028 ret = tmp.paths;
1029
1030 } else {
1031
1032 ret = tmp;
1033 }
1034
1035 } catch ( e ) {
1036
1037 ret = null;
1038 }
1039
1040 return ret;
1041 }
1042
1043 /**
1044 * This utility method is one of many methods that facilitate the
1045 * use of special variables (such as `${endpointRoot}`) inside of objects,
1046 * especially objects that are loaded from JSON and YAML files.
1047 *
1048 * Specifically, this method will parse an object, recursively, for
1049 * strings that contain any identifiers that correspond to any of the
1050 * properties defined in the `vars` param object.
1051 *
1052 * This method is, typically, the entry point for variable resolution,
1053 * since relevant JSON and YAML files will be passed, directly, into this
1054 * method.
1055 *
1056 * @protected
1057 * @param {Object} obj - The object to scan for variable identifiers.
1058 * @param {Object} vars - A plain object containing variables that can be
1059 * injected into the object defined by the `obj` param.
1060 * @returns {Object} The final object with all valid variable values
1061 * injected.
1062 */
1063 _parseObjectForVariables( obj, vars ) {
1064
1065 const me = this;
1066
1067 // Dependencies
1068 const _ = me.$dep( "lodash" );
1069
1070 // Initialize the return
1071 let ret = {};
1072
1073 // Iterate over each property in the target object
1074 _.each( obj, function ( val, key ) {
1075
1076 ret[ key ] = me._parseAnyForVariables( val, vars );
1077 } );
1078
1079 return ret;
1080 }
1081
1082 /**
1083 * This utility method is one of many methods that facilitate the
1084 * use of special variables (such as `${endpointRoot}`) inside of objects,
1085 * especially objects that are loaded from JSON and YAML files.
1086 *
1087 * Specifically, this method will evaluate a variable and route processing
1088 * of that variable to more specific methods based on its type.
1089 *
1090 * @protected
1091 * @param {*} target - The variable to scan for variable identifiers.
1092 * @param {Object} vars - A plain object containing variables that can be
1093 * injected into the object defined by the `obj` param.
1094 * @returns {*} The variables passed via the `target` parameter, but with
1095 * valid variable values injected.
1096 */
1097 _parseAnyForVariables( target, vars ) {
1098
1099 const me = this;
1100
1101 // Dependencies
1102 const _ = me.$dep( "lodash" );
1103
1104 if ( _.isPlainObject( target ) ) {
1105
1106 // If `target` is an object, we'll defer to the
1107 // method that parses objects...
1108 return me._parseObjectForVariables( target, vars );
1109
1110 } else if ( _.isArray( target ) ) {
1111
1112 // If `target` is an array, we'll defer to the
1113 // method that parses arrays...
1114 return me._parseArrayForVariables( target, vars );
1115
1116 } else if ( _.isString( target ) ) {
1117
1118 // If `target` is a string, we'll defer to the
1119 // method that parses string...
1120 return me._parseStringForVariables( target, vars );
1121 }
1122
1123 // All other types will be returned verbatim, since
1124 // they cannot be parsed for variable identifiers.
1125 return target;
1126 }
1127
1128 /**
1129 * This utility method is one of many methods that facilitate the
1130 * use of special variables (such as `${endpointRoot}`) inside of objects,
1131 * especially objects that are loaded from JSON and YAML files.
1132 *
1133 * Specifically, this method will parse an array and send each of
1134 * its elements to `_parseAnyForVariables`, where they will be evaluated
1135 * for additional processing.
1136 *
1137 * @protected
1138 * @param {Array} arr - The array to scan for variable identifiers.
1139 * @param {Object} vars - A plain object containing variables that can be
1140 * injected into the object defined by the `obj` param.
1141 * @returns {Array} The final array with all valid variable values injected.
1142 */
1143 _parseArrayForVariables( arr, vars ) {
1144
1145 const me = this;
1146
1147 // Dependencies
1148 const _ = me.$dep( "lodash" );
1149
1150 // Init Return
1151 let ret = [];
1152
1153 // Iterate over each element in the array...
1154 _.each( arr, function ( val ) {
1155
1156 // Defer each element to `_parseAnyForVariables`,
1157 // for additional processing...
1158 ret.push( me._parseAnyForVariables( val, vars ) );
1159 } );
1160
1161 // All done
1162 return ret;
1163 }
1164
1165 /**
1166 * This utility method is one of many methods that facilitate the
1167 * use of special variables (such as `${endpointRoot}`) inside of objects,
1168 * especially objects that are loaded from JSON and YAML files.
1169 *
1170 * Specifically, this method will parse a string for variable identifiers,
1171 * and, when found, will inject the values from the `vars` object.
1172 *
1173 * Since variable identifiers can only be represented as strings, this
1174 * method is the actual injection mechanism for the variable system. All
1175 * of the other methods just allow for recursive processing.
1176 *
1177 * @protected
1178 * @param {string} str - The string to parse for variable identifiers.
1179 * @param {Object} vars - A plain object containing variables that can be
1180 * injected into the object defined by the `obj` param.
1181 * @returns {*} The resolved variable, after variable values have been
1182 * injected.
1183 */
1184 _parseStringForVariables( str, vars ) {
1185
1186 const me = this;
1187
1188 // Dependencies
1189 const _ = me.$dep( "lodash" );
1190
1191 // Init return
1192 let ret = str;
1193
1194 // Iterate over each available variable and check
1195 // to see if the identifier is present in the string.
1196 _.each( vars, function ( varVal, varName ) {
1197
1198 // Resolve the identifier to search for...
1199 // e.g. { a: ... } -> "${a}"
1200 let varIdentifier = "${" + varName + "}";
1201
1202 // If a previous variable has modified the 'type' of
1203 // our return (so that it is no longer a string), then
1204 // we won't process any additional variables.
1205 if ( ret.indexOf !== undefined ) {
1206
1207 if ( _.isString( varVal ) ) {
1208
1209 // If our current variable value is a string, we're going
1210 // to do a search and replace operation...
1211 while ( ret.indexOf( varIdentifier ) !== -1 ) {
1212
1213 // Replace...
1214 ret = ret.replace( varIdentifier, varVal );
1215 }
1216
1217 // If our current variable value is not a string, then
1218 // we'll return the full variable value if its identifier
1219 // is found in the string.
1220
1221 } else if ( ret.indexOf( varIdentifier ) !== -1 ) {
1222
1223 // Return the full value
1224 ret = varVal;
1225
1226 // Stop iteration
1227 return false;
1228 }
1229 }
1230
1231 return true;
1232 } );
1233
1234 // All done
1235 return ret;
1236 }
1237
1238 /**
1239 * Searches for files, recursively, within a given `basePath`, whose
1240 * absolute path matches a given regular expression pattern (`pattern`).
1241 *
1242 * @access protected
1243 * @param {string} basePath - The file system path to search
1244 * @param {RegExp} pattern - The regular expression to test each absolute
1245 * path against.
1246 * @returns {Object} A plain-object, keyed by absolute paths, that
1247 * contains information about each file that was matched/found.
1248 */
1249 _findFilesWithPattern( basePath, pattern ) {
1250
1251 const me = this;
1252
1253 // Dependencies
1254 const _ = me.$dep( "lodash" );
1255
1256 let allFiles = me._getFilesRecursive( basePath );
1257 let ret = {};
1258
1259 _.each( allFiles, function ( item, absPath ) {
1260
1261 if ( pattern.test( absPath ) ) {
1262
1263 ret[ absPath ] = item;
1264 }
1265 } );
1266
1267 return ret;
1268 }
1269
1270 /**
1271 * Returns information about ALL files found after a recursive scan
1272 * of the provided `basePath`.
1273 *
1274 * @access protected
1275 * @uses _getDirContentsRecursive
1276 * @param {string} basePath - The file system path to scan/walk.
1277 * @returns {Object} A plain-object, keyed by absolute paths, that
1278 * contains information about each file within the provided `basePath`.
1279 */
1280 _getFilesRecursive( basePath ) {
1281
1282 const me = this;
1283
1284 // Dependencies
1285 const _ = me.$dep( "lodash" );
1286
1287 // Init return object
1288 let ret = {};
1289
1290 // Get ALL items (files + dirs) for the path
1291 let allItems = me._getDirContentsRecursive( basePath );
1292
1293 // Iterate over each item and, for each item that is not
1294 // a directory, add it to the return.
1295 _.each( allItems, function ( item, absPath ) {
1296
1297 // Ignore directories...
1298 if ( item.isDirectory === false ) {
1299
1300 // Return files...
1301 ret[ absPath ] = item;
1302 }
1303 } );
1304
1305 // All done
1306 return ret;
1307 }
1308
1309 /**
1310 * Returns information about ALL items (including both files and
1311 * directories) found after a recursive scan of the provided
1312 * `absPathToRead`.
1313 *
1314 * @protected
1315 * @param {string} absPathToRead - The file system path to scan/walk.
1316 * @param {?Object} [initialStore=null] - This optional object is used
1317 * to facilitate recursive scanning by providing a single object/ref
1318 * to store all items within. You usually will not need to pass this
1319 * parameter because it will be created, automatically, by the top-most
1320 * call to this method.
1321 * @returns {Object[]} A plain-object, keyed by absolute paths, that
1322 * contains information about each item (files+dirs) within the provided
1323 * `absPathToRead`.
1324 */
1325 _getDirContentsRecursive( absPathToRead, initialStore ) {
1326
1327 const me = this;
1328
1329 // Dependencies
1330 const _ = me.$dep( "lodash" );
1331
1332 // Init return
1333 let ret;
1334
1335 // Create an object store, if one was not provided as a param
1336 if ( initialStore !== undefined && initialStore !== null ) {
1337
1338 ret = initialStore;
1339
1340 } else {
1341
1342 ret = {};
1343 }
1344
1345 // Build the contents of the target directory
1346 let dirContents = me._getDirContents( absPathToRead );
1347
1348 // Add target directory contents to return
1349 _.merge( ret, dirContents );
1350
1351 // Dive deeper into subdirectories
1352 _.each( dirContents, function ( item, absPath ) {
1353
1354 if ( item.isDirectory === true ) {
1355
1356 me._getDirContentsRecursive( absPath, ret );
1357 }
1358 } );
1359
1360 // Done
1361 return ret;
1362 }
1363
1364 /**
1365 * Returns the contents (dirs+files) of a single file system directory.
1366 *
1367 * @protected
1368 * @param {string} absPathToRead - The file system directory to read.
1369 * @returns {Object} An object, keyed by absolute path, that contains
1370 * information about each item (dirs+files) found within the provided
1371 * `absPathToRead`.
1372 */
1373 _getDirContents( absPathToRead ) {
1374
1375 const me = this;
1376
1377 // Dependencies
1378 const _ = me.$dep( "lodash" );
1379 const PATH = me.$dep( "path" );
1380 const FS = me.$dep( "fs" );
1381
1382 // Init the return object
1383 let ret = {};
1384
1385 // Defer to the `fs` module to read the directory
1386 let res = FS.readdirSync( absPathToRead );
1387
1388 // Iterate over each item found by the `fs` module
1389 _.each( res, function ( relItem ) {
1390
1391 // Resolve the absolute path for the item...
1392 let absItem = PATH.join( absPathToRead, relItem );
1393
1394 // Stat the item, which will provide lots
1395 // of useful information about it (especially
1396 // info about whether or not it is a file or dir)
1397 let stat = FS.statSync( absItem );
1398
1399 // Create the info object for the target item
1400 let item = {
1401 path : absItem,
1402 name : relItem,
1403 isDirectory : stat.isDirectory(),
1404 stat : stat,
1405 };
1406
1407 // Add the info for the current
1408 // item to the return object
1409 ret[ absItem ] = item;
1410 } );
1411
1412 // All done
1413 return ret;
1414 }
1415
1416 // --
1417
1418 /**
1419 * A string that is used as the prefix for all event "name" fields within
1420 * the {@link Logging.Logger} object.
1421 *
1422 * @public
1423 * @readonly
1424 * @type {string}
1425 */
1426 get loggerPrefix() {
1427
1428 const me = this;
1429
1430 return "msa.generator." + me.constructor.name;
1431 }
1432
1433 /**
1434 * Initializes a new logger object.
1435 *
1436 * @private
1437 * @returns {void} The new logger object will be stored in the 'logger'
1438 * property.
1439 */
1440 _initLogger() {
1441
1442 const me = this;
1443
1444 let config = me._createLoggerConfig( me.loggerConfigOverrides );
1445
1446 // Instantiate the logger
1447 me.logger = me.$dep( "logger", config );
1448 }
1449
1450 /**
1451 * Creates a logger configuration object the is pre-filled with
1452 * information about this Generator.
1453 *
1454 * @access private
1455 * @returns {Object} A plain configuration object to be passed to
1456 * the constructor of new Logger objects.
1457 */
1458 _createLoggerConfig( loggerConfigOverrides ) {
1459
1460 // Locals
1461 let me = this;
1462
1463 // Setup the initial logger config based on
1464 // information pulled from the context/environment.
1465 return {
1466 application : "core-microservices",
1467 component : me.constructor.name,
1468 namePrefix : me.loggerPrefix,
1469 linear: true
1470 };
1471
1472 }
1473
1474 // <editor-fold desc="--- Paths & Path Management ------------------------">
1475
1476 /**
1477 * @inheritDoc
1478 */
1479 get pathManager() {
1480
1481 // Locals
1482 const me = this;
1483
1484 // Deps
1485 const PATH = me.$dep( "path" );
1486
1487 // Defer to parent/super..
1488 let pm = super.pathManager;
1489
1490 // Attach the microservices lib path (as needed)
1491 if ( !pm.hasPath( "microservicesLib" ) ) {
1492 pm.setPath( "microservicesLib", PATH.join( __dirname, ".." ) );
1493 }
1494
1495 // Attach the service paths (as possible & needed)
1496 if( me.serviceRootPath !== undefined ) {
1497 if ( !pm.hasPath( "serviceRoot" ) ) {
1498 pm.setPath( "serviceRoot", me.serviceRootPath );
1499 }
1500 if ( !pm.hasPath( "serviceLib" ) ) {
1501 pm.setPath( "serviceLib", PATH.join( me.serviceRootPath, "lib" ) );
1502 }
1503
1504 }
1505
1506 // All done..
1507 return pm;
1508
1509 }
1510
1511 /**
1512 * @inheritDoc
1513 */
1514 set pathManager( /** Common.PathManager */ val ) {
1515
1516 // I had to add this setter because I added a getter override.
1517 // If I had not, then `pathManager` would have been
1518 // treated as read-only.
1519
1520 super.pathManager = val;
1521
1522 }
1523
1524
1525 // </editor-fold>
1526
1527}
1528
1529module.exports = BaseGenerator;