UNPKG

21.8 kBJavaScriptView Raw
1/**
2 * @file Defines the Request 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 * Represents an API request.
18 *
19 * @memberOf Request
20 * @extends Common.BaseClass
21 */
22class Request extends BaseClass {
23
24 /**
25 * @inheritDoc
26 */
27 _initialize( cfg ) {
28
29 // const me = this;
30
31 // Call parent
32 super._initialize( cfg );
33 }
34
35 /**
36 * Validate the request.
37 *
38 * @returns {Promise<Request.Request>} This request.
39 */
40 validate() {
41
42 const me = this;
43
44 // Dependencies
45 const BB = me.$dep( "bluebird" );
46
47 return BB.try( function () {
48
49 return me._validateParameters();
50
51 } ).then( function () {
52
53 return me._validateBody();
54
55 } ).then( function () {
56
57 return me;
58 } );
59 }
60
61 /**
62 * The {@link ExecutionContext.BaseExecutionContext} that built this
63 * Request object.
64 *
65 * @public
66 * @type {?ExecutionContext.BaseExecutionContext}
67 * @default null
68 */
69 get context() {
70
71 const me = this;
72
73 return me.getConfigValue( "context", null );
74 }
75
76 set context( /** ?ExecutionContext.BaseExecutionContext */ val ) {
77
78 const me = this;
79
80 val.$adopt( me );
81 me.setConfigValue( "context", val );
82 }
83
84 /**
85 * The request body data passed in by the client.
86 *
87 * @public
88 * @returns {Object|null} Request body.
89 */
90 get body() {
91
92 const me = this;
93
94 return me.getConfigValue( "body", null );
95 }
96
97 set body( val ) {
98
99 const me = this;
100
101 me.setConfigValue( "body", val );
102 }
103
104 /**
105 * Get the raw parameter data passed in by the client, which does not
106 * include any schema information about the parameter.
107 *
108 * @public
109 * @returns {Object|null} Raw parameters.
110 */
111 getRawParameters() {
112
113 const me = this;
114
115 return me.getConfigValue( "rawParameters", null );
116 }
117
118 /**
119 * Set the raw parameter data passed in by the client, which does not
120 * include any schema information about the parameter.
121 *
122 * @public
123 * @param {Object|null} rawParameters - Raw parameters.
124 * @returns {Promise<void>} Empty promise.
125 */
126 setRawParameters( rawParameters ) {
127
128 const me = this;
129
130 // Dependencies
131 const BB = me.$dep( "bluebird" );
132
133 me.setConfigValue( "rawParameters", rawParameters );
134
135 // FIXME: breaks response formatting for error responses
136 // return BB.try( function () {
137 //
138 // return me._validateParameters();
139 //
140 // } ).then( function () {
141 //
142 // return BB.resolve();
143 // } );
144
145 return BB.resolve();
146 }
147
148 /**
149 * The fully resolved parameter information, which includes schema
150 * information and the current value of each parameter.
151 *
152 * This method will return ALL parameters, even if the client did not
153 * specify a value for it and the parameter does not have a default
154 * value.
155 *
156 * @public
157 * @type {?Object}
158 * @readonly
159 * @default null
160 */
161 get parameters() {
162
163 const me = this;
164
165 return me.getConfigValue( "parameters", null );
166 }
167
168 /**
169 * The fully resolved parameter information, which includes schema
170 * information and the current value of each parameter.
171 *
172 * This method will only return parameters that have values, which includes
173 * parameters that had values passed in by the client OR those that were
174 * not passed in by the client but have default values defined.
175 *
176 * @public
177 * @type {?Object}
178 * @readonly
179 * @default null
180 */
181 get parametersWithValues() {
182
183 const me = this;
184
185 // Dependencies
186 const _ = me.$dep( "lodash" );
187
188 let params = me.parameters;
189 let ret = {};
190
191 if ( params === null ) {
192
193 return null;
194 }
195
196 _.each( params, function ( p, key ) {
197
198 if ( p.hasValue ) {
199
200 ret[ key ] = p;
201 }
202 } );
203
204 return ret;
205 }
206
207 /**
208 * A plain object containing parameters and their values.
209 *
210 * This method will only return parameters that have values, which includes
211 * parameters that had values passed in by the client OR those that were
212 * not passed in by the client but have default values defined.
213 *
214 * @public
215 * @type {Object}
216 * @readonly
217 * @default {}
218 */
219 get parameterValues() {
220
221 const me = this;
222
223 // Dependencies
224 const _ = me.$dep( "lodash" );
225
226 let params = me.parametersWithValues;
227 let ret = {};
228
229 _.each( params, function ( v, k ) {
230
231 ret[ k ] = v.value;
232 } );
233
234 return ret;
235 }
236
237 /**
238 * Returns the value of the specified parameter or NULL.
239 *
240 * @param {string} name - Parameter name.
241 * @returns {*} The parameter value or NULL if the parameter does not exist.
242 */
243 getParameterValue( name ) {
244
245 const me = this;
246
247 let params = me.parameters;
248
249 let param = params[ name ];
250
251 if ( param ) {
252
253 return param.value;
254 }
255
256 return null;
257 }
258
259 /**
260 * Request offset parameter value.
261 *
262 * @readonly
263 * @returns {number} Request offset parameter value.
264 */
265 get offset() {
266
267 const me = this;
268
269 let pageNumber = me.pageNumber;
270 let pageSize = me.pageSize;
271
272 let offset = ( pageNumber - 1 ) * pageSize;
273
274 if ( offset < 0 ) {
275
276 offset = 0;
277 }
278
279 return offset;
280 }
281
282 /**
283 * Request limit parameter value.
284 *
285 * @readonly
286 * @returns {number} Request limit parameter value.
287 */
288 get limit() {
289
290 const me = this;
291
292 return me.pageSize;
293 }
294
295 /**
296 * Page number parameter value.
297 *
298 * @readonly
299 * @returns {number} Page number parameter value.
300 */
301 get pageNumber() {
302
303 const me = this;
304
305 return me.getParameterValue( "pageNumber" );
306 }
307
308 /**
309 * Page size parameter value.
310 *
311 * @readonly
312 * @returns {number} Page size parameter value.
313 */
314 get pageSize() {
315
316 const me = this;
317
318 return me.getParameterValue( "pageSize" );
319 }
320
321 /**
322 * Sort parameter value.
323 *
324 * @readonly
325 * @returns {number} Sort parameter value.
326 */
327 get sort() {
328
329 const me = this;
330
331 return me.getParameterValue( "sort" );
332 }
333
334 /**
335 * The endpoint that this Request was submitted to.
336 *
337 * @public
338 * @type {?Endpoint.BaseEndpoint}
339 * @default null
340 * @readonly
341 */
342 get endpoint() {
343
344 const me = this;
345
346 if ( me.context === null ) {
347
348 return null;
349 }
350
351 return me.context.endpoint;
352 }
353
354 /**
355 * The request schema for the endpoint that this request was submitted to.
356 *
357 * @public
358 * @returns {Promise<Object|null>} Request schema.
359 */
360 getSchema() {
361
362 const me = this;
363
364 // Dependencies
365 const BB = require( "bluebird" );
366
367 if ( me.endpoint === null ) {
368
369 return BB.resolve( null );
370 }
371
372 return me.endpoint.getRequestSchema();
373 }
374
375 /**
376 * The parameter portion of the request schema.
377 *
378 * @public
379 * @returns {Promise<Object|null>} Parameter schema.
380 */
381 getParameterSchema() {
382
383 const me = this;
384
385 // Dependencies
386 const BB = require( "bluebird" );
387
388 return BB.try( function () {
389
390 return me.getSchema();
391
392 } ).then( function ( schema ) {
393
394 if ( schema && schema.properties && schema.properties.parameters ) {
395
396 return schema.properties.parameters;
397 }
398
399 return BB.resolve( null );
400 } );
401 }
402
403 /**
404 * The body portion of the request schema.
405 *
406 * @public
407 * @returns {Promise<Object|null>} Body schema.
408 */
409 getBodySchema() {
410
411 const me = this;
412
413 // Dependencies
414 const BB = require( "bluebird" );
415
416 return BB.try( function () {
417
418 return me.getSchema();
419
420 } ).then( function ( schema ) {
421
422 if ( schema && schema.properties && schema.properties.body ) {
423
424 return schema.properties.body;
425 }
426
427 return BB.resolve( null );
428 } );
429 }
430
431 /**
432 * The primary model of the endpoint that this request was submitted to.
433 *
434 * @public
435 * @type {?Model.BaseModel}
436 * @readonly
437 * @default null
438 */
439 get model() {
440
441 const me = this;
442
443 if ( me.endpoint === null ) {
444
445 return null;
446 }
447
448 return me.endpoint.model;
449 }
450
451 /**
452 * The encoded session token used in this Request. This will either be
453 * passed in by the client or generated (for some special purpose) by the
454 * {@link Session.SessionManager}.
455 *
456 * @public
457 * @type {?string}
458 * @default null
459 */
460 get token() {
461
462 const me = this;
463
464 return me.getConfigValue( "token", null );
465 }
466
467 set token( /** ?string */ val ) {
468
469 const me = this;
470
471 me.setConfigValue( "token", val );
472 }
473
474 /**
475 * The decoded data from the session token used in this Request. This will
476 * either be passed in by the client or generated (for some special purpose)
477 * by the {@link Session.SessionManager}.
478 *
479 * @public
480 * @type {?string}
481 * @default null
482 */
483 get tokenData() {
484
485 const me = this;
486
487 return me.getConfigValue( "tokenData", null );
488 }
489
490 set tokenData( /** ?string */ val ) {
491
492 const me = this;
493
494 me.setConfigValue( "tokenData", val );
495 }
496
497 /**
498 * The username of the client making the request, which is extracted from
499 * the session token.
500 *
501 * @public
502 * @type {?string}
503 * @default null
504 * @readonly
505 */
506 get username() {
507
508 const me = this;
509
510 if ( me.tokenData === null ) {
511
512 return null;
513 }
514
515 return me.tokenData.data.username;
516 }
517
518 /**
519 * The userId (which is a UUID) of the client making the request, which is
520 * extracted from the session token.
521 *
522 * @public
523 * @type {?string}
524 * @default null
525 * @readonly
526 */
527 get userId() {
528
529 const me = this;
530
531 if ( me.tokenData === null ) {
532
533 return null;
534 }
535
536 return me.tokenData.userId;
537 }
538
539 /**
540 * The personId (which is a UUID) of the client making the request, which is
541 * extracted from the session token.
542 *
543 * @public
544 * @type {?string}
545 * @default null
546 * @readonly
547 */
548 get personId() {
549
550 const me = this;
551
552 if ( me.tokenData === null ) {
553
554 return null;
555 }
556
557 return me.tokenData.data.personId;
558 }
559
560 /**
561 * The sessionId (which is a UUID) of the client's session, which is
562 * extracted from the session token.
563 *
564 * @public
565 * @type {?string}
566 * @default null
567 * @readonly
568 */
569 get sessionId() {
570
571 const me = this;
572
573 if ( me.tokenData === null ) {
574
575 return null;
576 }
577
578 return me.tokenData.data.sessionId;
579 }
580
581 /**
582 * The namespace of the client's session, which is extracted from the
583 * session token.
584 *
585 * Currently, this is not used, but exists for future compatibility with
586 * a planned extension of the MSA architecture that will make it more
587 * versatile and reusable.
588 *
589 * @public
590 * @type {?string}
591 * @default null
592 * @readonly
593 */
594 get namespace() {
595
596 const me = this;
597
598 if ( me.tokenData === null ) {
599
600 return "default";
601 }
602
603 return me.tokenData.data.ns;
604 }
605
606 // noinspection JSUnusedGlobalSymbols
607 /**
608 * The version of the session token's format, which is extracted from the
609 * session token.
610 *
611 * @public
612 * @type {?number}
613 * @default null
614 * @readonly
615 */
616 get tokenVersion() {
617
618 const me = this;
619
620 if ( me.tokenData === null ) {
621
622 return null;
623 }
624
625 return me.tokenData.data.v;
626 }
627
628 // noinspection JSUnusedGlobalSymbols
629 /**
630 * The ip address of the requesting client, which is extracted from the
631 * session token.
632 *
633 * Because this value is extracted from the session token, the ip address
634 * will be that of the client that created the session, which is not
635 * necessarily the same ip address as the client making this, specific,
636 * request.
637 *
638 * @public
639 * @type {?string}
640 * @default null
641 * @readonly
642 */
643 get tokenClientIp() {
644
645 const me = this;
646
647 if ( me.tokenData === null ) {
648
649 return null;
650 }
651
652 return me.tokenData.sourceIp;
653 }
654
655 // noinspection JSUnusedGlobalSymbols
656 /**
657 * Session flags for the current session, which is extracted from the
658 * session token.
659 *
660 * Session flags are used in special processing logic, especially logic
661 * related to access control. For example, tokens with the "system" flag
662 * will (for the most part) bypass ACM checks.
663 *
664 * Session flags are also persisted to each log event emitted by endpoints,
665 * which can be useful for various types of analysis.
666 *
667 * @public
668 * @type {string[]}
669 * @default []
670 * @readonly
671 */
672 get tokenFlags() {
673
674 const me = this;
675
676 if ( me.tokenData === null ) {
677
678 return [ "no-token" ];
679 }
680
681 return me.tokenData.data.flags;
682 }
683
684 /**
685 * This method will convert the rawParameters, which is a plain one
686 * dimensional object, into a more standardized object that includes
687 * schema information. The information added by this process may
688 * be useful to other validators and parameter parsers that exist
689 * in other parts of the application.
690 *
691 * @private
692 * @returns {Promise<void>} This method stores its results internally as
693 * the 'parameters' property.
694 */
695 _normalizeRawParameters() {
696
697 const me = this;
698
699 // Dependencies
700 const BB = me.$dep( "bluebird" );
701 const TIPE = me.$dep( "tipe" );
702 const _ = me.$dep( "lodash" );
703
704 return BB.try( function () {
705
706 return BB.all( [
707 me.getParameterSchema(),
708 me.getRawParameters(),
709 ] );
710
711 } ).then( function ( [ schema, params ] ) {
712
713 let normalized = {};
714
715 // Skip normalization if we're missing the
716 // parameters or the schema...
717 if ( params === null || schema === null ) {
718
719 return;
720 }
721
722 if ( schema.properties === undefined ) {
723
724 return;
725 }
726
727 // Iterate over the parameters defined within the schema.
728 _.each( schema.properties, function ( paramSchema, paramKey ) {
729
730 let p = normalized[ paramKey ] = {
731 hasValue : false,
732 value : null,
733 provided : false,
734 hasDefault : false,
735 defaultValue : null,
736 key : paramKey,
737 schema : paramSchema,
738 };
739
740 // Create a few meta fields related to defaults
741 if ( p.schema.default !== undefined ) {
742
743 p.hasDefault = true;
744 p.defaultValue = paramSchema.default;
745 }
746
747 // Apply the param value, if it was provided...
748 if ( params[ paramKey ] !== undefined ) {
749
750 p.provided = true;
751 p.value = params[ paramKey ];
752 p.hasValue = true;
753 }
754
755 // Apply defaults, as applicable
756 if ( !p.hasValue && p.hasDefault ) {
757
758 p.value = p.defaultValue;
759 p.hasValue = true;
760 }
761
762 // Additional parsing for certain field types...
763 if ( p.hasValue ) {
764
765 if ( TIPE( p.schema.format ) === "string" ) {
766
767 switch ( p.schema.format.toLowerCase() ) {
768
769 case "uuid":
770 case "uuid-plus":
771 me._normalizeUuidParamValue( p );
772 break;
773
774 default:
775 break;
776 }
777 }
778
779 if ( TIPE( p.schema.type ) === "string" ) {
780
781 switch ( p.schema.type.toLowerCase() ) {
782
783 case "array":
784 me._normalizeArrayParamValue( p );
785 break;
786
787 case "integer":
788 me._normalizeIntegerParamValue( p );
789 break;
790
791 default:
792 break;
793 }
794 }
795 }
796 } );
797
798 // Persist
799 me.setConfigValue( "parameters", normalized );
800 } );
801 }
802
803 _normalizeArrayParamValue( paramInfoObject ) {
804
805 const me = this;
806
807 // Dependencies
808 const TIPE = me.$dep( "tipe" );
809 const _ = me.$dep( "lodash" );
810
811 // FIXME: should parameters already be desearialized by the API gateway before being processed by the endpoint?
812 //
813 // let val = paramInfoObject.value;
814 //
815 // console.log(paramInfoObject);
816 //
817 // switch ( TIPE( val ) ) {
818 //
819 // case "string":
820 //
821 // // FIXME: how to handle escaped commas? CSV format?
822 // val = val.split( "," );
823 // break;
824 //
825 // default:
826 //
827 // // Do nothing
828 //
829 // break;
830 // }
831 //
832 // paramInfoObject.value = val;
833
834 let items = paramInfoObject.schema.items;
835
836 _.each( items, function ( item ) {
837
838 let format = item.format;
839
840 if ( TIPE( format ) === "string" ) {
841
842 switch ( format.toLowerCase() ) {
843
844 case "uuid":
845 case "uuid-plus":
846 me._normalizeUuidParamValue( paramInfoObject );
847 break;
848
849 default:
850 break;
851 }
852 }
853 } );
854 }
855
856 /**
857 * This is a helper method for the `#_normalizeRawParameters()` method,
858 * which normalizes the value for UUID fields, when they'r provided.
859 *
860 * @private
861 * @param {Object} paramInfoObject - An object describing a parameter, its
862 * schema, and its value.
863 * @returns {void} All modifications are made ByRef
864 */
865 _normalizeUuidParamValue( paramInfoObject ) {
866
867 const me = this;
868
869 // Dependencies
870 const _ = me.$dep( "lodash" );
871 // const ERRORS = me.$dep( "errors" );
872
873 let val = paramInfoObject.value;
874 let final = [];
875 let isUuidPlus = false;
876
877 // Check for "plus" designation, which
878 // will loosen the validation rules to
879 // allow for non-uuid values.
880 if ( paramInfoObject.schema.format === "uuid-plus" ) {
881
882 isUuidPlus = true;
883
884 // For UUID plus fields, it may also be helpful
885 // for us to keep track of the values that are
886 // valid UUIDs.
887 paramInfoObject.uuidsAt = [];
888 }
889
890 // Check for commas...
891 if ( val.indexOf( "," ) !== -1 ) {
892
893 val = val.split( "," );
894
895 } else {
896
897 // No commas, but...
898 // We're going to force it to an array anyway,
899 // so that the next steps will be easier.
900 val = [ val ];
901 }
902
903 // Make all values unique...
904 val = _.uniq( val );
905
906 // Normalize each UUID
907 _.each( val, function ( unparsed ) {
908
909 // Trimming couldn't hurt...
910 unparsed = _.trim( unparsed );
911
912 // Ignore blanks...
913 if ( unparsed !== "" ) {
914
915 // Create a parsed value,
916 // ...which should be all lower case
917 let parsed = unparsed.toLowerCase();
918
919 // ...and without non-uuid characters.
920 parsed = parsed.replace( /[^a-f0-9]/g, "" );
921
922 // Validate...
923 if ( parsed.length !== 32 ) {
924
925 // This isn't a valid UUID.
926 // Let's see if we're allowing them.
927
928 if ( isUuidPlus ) {
929
930 // We're allowing non-uuids, so, we'll
931 // just try to preserve this value as it
932 // was before we parsed anything.
933 final.push( unparsed );
934
935 } else {
936
937 // We're not allowing non-UUID values, so
938 // this is a validation error...
939 throw new ERRORS.common.InvalidParameterError(
940 "Invalid request parameter '" + paramInfoObject.key + "': " +
941 "Invalid string format (expected UUID)"
942 );
943 }
944
945 } else {
946
947 // This looks like a proper UUID (though there is,
948 // admittedly, a small chance for screw-ups here,
949 // I will address it later...)
950
951 // todo: scrutinize the value more closely to ensure that it is a UUID
952
953 // Insert dashes...
954 parsed =
955 parsed.substr( 0, 8 ) + "-" +
956 parsed.substr( 8, 4 ) + "-" +
957 parsed.substr( 12, 4 ) + "-" +
958 parsed.substr( 16, 4 ) + "-" +
959 parsed.substr( 20 );
960
961 // Track this as a valid UUID
962 if ( isUuidPlus ) {
963
964 paramInfoObject.uuidsAt.push( final.length );
965 }
966
967 final.push( parsed );
968 }
969 }
970 } );
971
972 // If we have an empty array after processing, then we'll
973 // count this parameter as not being provided...
974 if ( final.length === 0 ) {
975
976 paramInfoObject.hasValue = false;
977 paramInfoObject.value = null;
978 paramInfoObject.provided = false;
979 paramInfoObject.uuidsAt = [];
980
981 } else if ( final.length === 1 ) {
982
983 // If there's only one value, we'll convert it back to a string
984 paramInfoObject.value = final[ 0 ];
985
986 } else {
987
988 // Otherwise, just store it...
989 paramInfoObject.value = final;
990 }
991 }
992
993 // noinspection JSMethodCanBeStatic
994 /**
995 * This is a helper method for the `#_normalizeRawParameters()` method,
996 * which normalizes the value for integer fields, when they'r provided.
997 *
998 * @private
999 * @param {Object} paramInfoObject - An object describing a parameter, its
1000 * schema, and its value.
1001 * @returns {void} All modifications are made ByRef
1002 */
1003 _normalizeIntegerParamValue( paramInfoObject ) {
1004
1005 let val = paramInfoObject.value;
1006
1007 // First, cast to a string...
1008 val = String( val );
1009
1010 // Remove decimals (if they exist)
1011 if ( val.indexOf( "." ) !== -1 ) {
1012
1013 let spl = val.split( "." );
1014
1015 val = spl[ 0 ];
1016 }
1017
1018 // Remove invalid characters
1019 val = val.replace( /[^0-9]/g, "" );
1020
1021 // Cast back to a number
1022 val = parseInt( val, 10 );
1023
1024 // Save it
1025 paramInfoObject.value = val;
1026 }
1027
1028 /**
1029 * This is the main entry point for parameter validation
1030 * against a predefined request schema. This method will be called
1031 * whenever the 'rawParameters' of this Request object are updated.
1032 *
1033 * @private
1034 * @throws UnrecognizedParameterError, MissingParameterError,
1035 * InvalidParameterError
1036 * @see http://json-schema.org/latest/json-schema-validation.html
1037 * @returns {Promise<void>} This method (or its subsidiaries) will throw
1038 * errors if parameter validation fails; otherwise, nothing is returned.
1039 */
1040 _validateParameters() {
1041
1042 const me = this;
1043
1044 // Dependencies
1045 const BB = me.$dep( "bluebird" );
1046 const Validator = me.$dep( "util/Validator" );
1047
1048 return BB.try( function () {
1049
1050 // Normalize first...
1051 return me._normalizeRawParameters();
1052
1053 } ).then( function () {
1054
1055 return me.getParameterSchema();
1056
1057 } ).then( function ( schema ) {
1058
1059 if ( !schema ) {
1060
1061 return;
1062 }
1063
1064 // Create a Validator object
1065 let validator = new Validator(
1066 {
1067 data : me.parameterValues,
1068 schema : schema,
1069 skipIfNull : true,
1070 }
1071 );
1072
1073 // Execute it...
1074 validator.validate();
1075 } );
1076 }
1077
1078 /**
1079 * This is the main entry point for body validation against a predefined
1080 * request schema.
1081 *
1082 * @private
1083 * @see http://json-schema.org/latest/json-schema-validation.html
1084 * @returns {Promise<void>} This method (or its subsidiaries) will throw
1085 * errors if body validation fails; otherwise, nothing is returned.
1086 */
1087 _validateBody() {
1088
1089 const me = this;
1090
1091 // Dependencies
1092 const BB = me.$dep( "bluebird" );
1093 const Validator = me.$dep( "util/Validator" );
1094
1095 return BB.try( function () {
1096
1097 return me.getBodySchema();
1098
1099 } ).then( function ( schema ) {
1100
1101 if ( !schema ) {
1102
1103 return;
1104 }
1105
1106 // Create a Validator object
1107 let validator = new Validator(
1108 {
1109 data : me.body,
1110 schema : schema,
1111 skipIfNull : true,
1112 }
1113 );
1114
1115 // Execute it...
1116 validator.validate();
1117 } );
1118 }
1119}
1120
1121module.exports = Request;