@63klabs/cache-data 1.3.9 → 1.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -7
- package/CONTRIBUTING.md +56 -3
- package/README.md +5 -5
- package/package.json +14 -26
- package/src/lib/dao-cache.js +25 -34
- package/src/lib/dao-endpoint.js +68 -38
- package/src/lib/tools/{APIRequest.class.js → ApiRequest.class.js} +164 -51
- package/src/lib/tools/CachedParametersSecrets.classes.js +10 -10
- package/src/lib/tools/ClientRequest.class.js +174 -25
- package/src/lib/tools/Connections.classes.js +5 -5
- package/src/lib/tools/Response.class.js +25 -0
- package/src/lib/tools/generic.response.html.js +8 -113
- package/src/lib/tools/generic.response.js +73 -0
- package/src/lib/tools/generic.response.json.js +5 -135
- package/src/lib/tools/generic.response.rss.js +10 -114
- package/src/lib/tools/generic.response.text.js +5 -115
- package/src/lib/tools/generic.response.xml.js +10 -114
- package/src/lib/tools/index.js +19 -8
- package/src/lib/utils/ValidationExecutor.class.js +8 -4
- package/src/lib/utils/ValidationMatcher.class.js +13 -1
- package/AGENTS.md +0 -1107
|
@@ -401,6 +401,7 @@ class ClientRequest extends RequestInfo {
|
|
|
401
401
|
|
|
402
402
|
/* Validation system */
|
|
403
403
|
#validationMatchers = {};
|
|
404
|
+
#validationReason = { isValid: true, statusCode: 200, messages: [] };
|
|
404
405
|
|
|
405
406
|
/* The request data */
|
|
406
407
|
#props = {};
|
|
@@ -470,6 +471,10 @@ class ClientRequest extends RequestInfo {
|
|
|
470
471
|
bodyParameters: {},
|
|
471
472
|
bodyPayload: this.#event?.body || null, // from body
|
|
472
473
|
client: {
|
|
474
|
+
ip: this.getClientIp(),
|
|
475
|
+
userAgent: this.getClientUserAgent(),
|
|
476
|
+
origin: this.getClientOrigin(),
|
|
477
|
+
referrer: this.getClientReferrer(),
|
|
473
478
|
isAuthenticated: this.isAuthenticated(),
|
|
474
479
|
isGuest: this.isGuest(),
|
|
475
480
|
authorizations: this.getAuthorizations(),
|
|
@@ -698,7 +703,7 @@ class ClientRequest extends RequestInfo {
|
|
|
698
703
|
* Parameter validations
|
|
699
704
|
* @returns {{
|
|
700
705
|
* pathParameters?: object,
|
|
701
|
-
*
|
|
706
|
+
* queryStringParameters?: object,
|
|
702
707
|
* headerParameters?: object,
|
|
703
708
|
* cookieParameters?: object,
|
|
704
709
|
* bodyParameters?: object
|
|
@@ -755,25 +760,129 @@ class ClientRequest extends RequestInfo {
|
|
|
755
760
|
*/
|
|
756
761
|
#validate() {
|
|
757
762
|
|
|
758
|
-
|
|
763
|
+
const reasons = [];
|
|
764
|
+
let statusCode = 200;
|
|
765
|
+
|
|
766
|
+
// Referrer check
|
|
767
|
+
const referrerValid = this.isAuthorizedReferrer();
|
|
768
|
+
if (!referrerValid) {
|
|
769
|
+
reasons.push("Forbidden");
|
|
770
|
+
statusCode = this.#upgradeStatusCode(statusCode, 403);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Authentication check
|
|
774
|
+
const authFailed = this.hasNoAuthorization();
|
|
775
|
+
if (authFailed) {
|
|
776
|
+
reasons.push("Unauthorized");
|
|
777
|
+
statusCode = this.#upgradeStatusCode(statusCode, 401);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// Parameter checks - collect invalid parameter names from each
|
|
781
|
+
const pathResult = this.#hasValidPathParameters();
|
|
782
|
+
const queryResult = this.#hasValidQueryStringParameters();
|
|
783
|
+
const headerResult = this.#hasValidHeaderParameters();
|
|
784
|
+
const cookieResult = this.#hasValidCookieParameters();
|
|
785
|
+
const bodyResult = this.#hasValidBodyParameters();
|
|
786
|
+
|
|
787
|
+
// Collect invalid parameter messages
|
|
788
|
+
const paramResults = [pathResult, queryResult, headerResult, cookieResult, bodyResult];
|
|
789
|
+
for (const result of paramResults) {
|
|
790
|
+
if (result.invalidParams) {
|
|
791
|
+
for (const paramName of result.invalidParams) {
|
|
792
|
+
reasons.push(`Invalid parameter: ${paramName}`);
|
|
793
|
+
statusCode = this.#upgradeStatusCode(statusCode, 400);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
759
797
|
|
|
760
|
-
//
|
|
761
|
-
|
|
798
|
+
// Handle invalid JSON body
|
|
799
|
+
if (bodyResult.invalidBody) {
|
|
800
|
+
reasons.push("Invalid request body");
|
|
801
|
+
statusCode = this.#upgradeStatusCode(statusCode, 400);
|
|
802
|
+
}
|
|
762
803
|
|
|
763
|
-
//
|
|
804
|
+
// Compute combined valid boolean from all check results
|
|
805
|
+
const valid = referrerValid && !authFailed
|
|
806
|
+
&& pathResult.isValid && queryResult.isValid && headerResult.isValid
|
|
807
|
+
&& cookieResult.isValid && bodyResult.isValid;
|
|
808
|
+
|
|
809
|
+
// Preserve backwards compatibility
|
|
764
810
|
super._isValid = valid;
|
|
765
811
|
|
|
812
|
+
// Populate validation reason
|
|
813
|
+
this.#validationReason = {
|
|
814
|
+
isValid: valid,
|
|
815
|
+
statusCode: valid ? 200 : statusCode,
|
|
816
|
+
messages: valid ? [] : reasons
|
|
817
|
+
};
|
|
818
|
+
|
|
766
819
|
};
|
|
767
820
|
|
|
821
|
+
/**
|
|
822
|
+
* Returns the higher-priority HTTP status code between two candidates.
|
|
823
|
+
* Priority order: 401 > 403 > 400 > 200.
|
|
824
|
+
*
|
|
825
|
+
* @private
|
|
826
|
+
* @param {number} current - The current status code
|
|
827
|
+
* @param {number} candidate - The candidate status code to compare
|
|
828
|
+
* @returns {number} The status code with higher priority
|
|
829
|
+
*/
|
|
830
|
+
#upgradeStatusCode(current, candidate) {
|
|
831
|
+
const priority = { 401: 3, 403: 2, 400: 1, 200: 0 };
|
|
832
|
+
return (priority[candidate] || 0) > (priority[current] || 0) ? candidate : current;
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Returns a structured validation result object describing why the request
|
|
837
|
+
* passed or failed validation. The object includes the validation status,
|
|
838
|
+
* an appropriate HTTP status code, and descriptive messages identifying
|
|
839
|
+
* each failure.
|
|
840
|
+
*
|
|
841
|
+
* A new object is returned on each call to prevent external mutation of
|
|
842
|
+
* internal state.
|
|
843
|
+
*
|
|
844
|
+
* @returns {{ isValid: boolean, statusCode: number, messages: Array<string> }}
|
|
845
|
+
* A new object on each call containing:
|
|
846
|
+
* - isValid: whether the request passed all validation checks
|
|
847
|
+
* - statusCode: the appropriate HTTP status code (200, 400, 401, or 403)
|
|
848
|
+
* - messages: array of descriptive failure messages (empty when valid)
|
|
849
|
+
* @example
|
|
850
|
+
* // Valid request
|
|
851
|
+
* const reason = clientRequest.getValidationReason();
|
|
852
|
+
* // { isValid: true, statusCode: 200, messages: [] }
|
|
853
|
+
*
|
|
854
|
+
* @example
|
|
855
|
+
* // Invalid request with bad parameters
|
|
856
|
+
* const reason = clientRequest.getValidationReason();
|
|
857
|
+
* // { isValid: false, statusCode: 400, messages: ["Invalid parameter: limit"] }
|
|
858
|
+
*/
|
|
859
|
+
getValidationReason() {
|
|
860
|
+
return {
|
|
861
|
+
isValid: this.#validationReason.isValid,
|
|
862
|
+
statusCode: this.#validationReason.statusCode,
|
|
863
|
+
messages: [...this.#validationReason.messages]
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
|
|
768
867
|
/**
|
|
769
868
|
* Validate parameters using ValidationMatcher and ValidationExecutor.
|
|
770
869
|
*
|
|
771
870
|
* This method implements the core parameter validation logic:
|
|
772
871
|
* 1. Uses ValidationMatcher to find the best matching validation rule (4-tier priority)
|
|
773
872
|
* 2. Uses ValidationExecutor to execute validation with appropriate interface (single or multi-parameter)
|
|
774
|
-
* 3. Extracts validated parameters and returns them
|
|
873
|
+
* 3. Extracts ALL validated parameters specified in the matching rule and returns them
|
|
775
874
|
* 4. Respects excludeParamsWithNoValidationMatch flag (default: true)
|
|
776
875
|
*
|
|
876
|
+
* When a validation rule matches and validation passes, ALL parameters specified in rule.params
|
|
877
|
+
* are extracted from clientParameters and included in the returned params object. This ensures
|
|
878
|
+
* that multi-parameter validation rules (e.g., validating query?param1,param2 together) correctly
|
|
879
|
+
* extract all validated parameters, not just the one that triggered the rule match.
|
|
880
|
+
*
|
|
881
|
+
* For single-parameter validation with multi-placeholder routes (e.g., users/{userId}/posts/{id}):
|
|
882
|
+
* - ValidationMatcher returns validateParam field indicating which parameter to validate
|
|
883
|
+
* - ValidationExecutor validates only that parameter with single-parameter interface
|
|
884
|
+
* - This method extracts ALL parameters from rule.params array (e.g., both userId and id)
|
|
885
|
+
*
|
|
777
886
|
* @private
|
|
778
887
|
* @param {Object} paramValidations - Parameter validation configuration (may include BY_ROUTE, BY_METHOD, and global validations)
|
|
779
888
|
* @param {Object} clientParameters - Parameters from the request (path, query, header, or cookie parameters)
|
|
@@ -791,12 +900,25 @@ class ClientRequest extends RequestInfo {
|
|
|
791
900
|
|
|
792
901
|
let rValue = {
|
|
793
902
|
isValid: true,
|
|
794
|
-
params: {}
|
|
903
|
+
params: {},
|
|
904
|
+
invalidParams: []
|
|
795
905
|
}
|
|
796
906
|
|
|
797
|
-
if (clientParameters
|
|
907
|
+
if (clientParameters) {
|
|
798
908
|
// >! Check excludeParamsWithNoValidationMatch flag (default: true)
|
|
799
909
|
const excludeUnmatched = ClientRequest.#validations.parameters?.excludeParamsWithNoValidationMatch !== false;
|
|
910
|
+
|
|
911
|
+
// >! When no validation rules exist for this parameter type,
|
|
912
|
+
// >! pass through all parameters if excludeUnmatched is false
|
|
913
|
+
if (!paramValidations) {
|
|
914
|
+
if (!excludeUnmatched) {
|
|
915
|
+
rValue.params = { ...clientParameters };
|
|
916
|
+
}
|
|
917
|
+
return rValue;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// >! Track which parameters have been validated to avoid duplicate validation
|
|
921
|
+
const validatedParams = new Set();
|
|
800
922
|
|
|
801
923
|
// >! Create normalized parameter map for validation execution
|
|
802
924
|
// >! Include ALL parameter types so multi-parameter validations can access them
|
|
@@ -841,12 +963,20 @@ class ClientRequest extends RequestInfo {
|
|
|
841
963
|
normalizedParams[normalizedKey] = value;
|
|
842
964
|
}
|
|
843
965
|
|
|
966
|
+
// >! Collect valid params separately so we can clear them if any fail
|
|
967
|
+
const collectedParams = {};
|
|
968
|
+
|
|
844
969
|
// Use a for...of loop instead of forEach for better control flow
|
|
845
970
|
for (const [key, value] of Object.entries(clientParameters)) {
|
|
846
971
|
// >! Preserve existing parameter key normalization
|
|
847
972
|
const paramKey = key.replace(/^\/|\/$/g, '');
|
|
848
973
|
const paramValue = value;
|
|
849
974
|
|
|
975
|
+
// >! Skip parameters that have already been validated
|
|
976
|
+
if (validatedParams.has(paramKey)) {
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
|
|
850
980
|
// >! Use ValidationMatcher to find the best matching validation rule
|
|
851
981
|
const rule = validationMatcher.findValidationRule(paramKey);
|
|
852
982
|
|
|
@@ -856,22 +986,41 @@ class ClientRequest extends RequestInfo {
|
|
|
856
986
|
const isValid = ValidationExecutor.execute(rule.validate, rule.params, normalizedParams);
|
|
857
987
|
|
|
858
988
|
if (isValid) {
|
|
859
|
-
|
|
989
|
+
// >! Extract ALL parameters specified in rule.params when validation passes
|
|
990
|
+
// >! This fixes the bug where only the current paramKey was added
|
|
991
|
+
for (const ruleParamName of rule.params) {
|
|
992
|
+
// >! Find the parameter value in clientParameters
|
|
993
|
+
// >! Use normalized key matching to handle case differences
|
|
994
|
+
const normalizedRuleParam = ruleParamName.replace(/^\/|\/$/g, '');
|
|
995
|
+
|
|
996
|
+
// >! Search for matching parameter in clientParameters
|
|
997
|
+
for (const [clientKey, clientValue] of Object.entries(clientParameters)) {
|
|
998
|
+
const normalizedClientKey = clientKey.replace(/^\/|\/$/g, '');
|
|
999
|
+
|
|
1000
|
+
if (normalizedClientKey === normalizedRuleParam) {
|
|
1001
|
+
collectedParams[clientKey] = clientValue;
|
|
1002
|
+
// >! Mark this parameter as validated to avoid duplicate validation
|
|
1003
|
+
validatedParams.add(normalizedClientKey);
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
860
1008
|
} else {
|
|
861
1009
|
// >! Maintain existing logging for invalid parameters
|
|
862
1010
|
DebugAndLog.warn(`Invalid parameter: ${paramKey} = ${paramValue}`);
|
|
863
1011
|
rValue.isValid = false;
|
|
864
|
-
rValue.
|
|
865
|
-
// >! Ensure early exit on validation failure
|
|
866
|
-
return rValue;
|
|
1012
|
+
rValue.invalidParams.push(paramKey);
|
|
867
1013
|
}
|
|
868
1014
|
} else if (!excludeUnmatched) {
|
|
869
1015
|
// No validation rule found, but excludeUnmatched is false
|
|
870
1016
|
// Include parameter without validation
|
|
871
|
-
|
|
1017
|
+
collectedParams[paramKey] = paramValue;
|
|
872
1018
|
}
|
|
873
1019
|
// If excludeUnmatched is true and no rule found, skip parameter (existing behavior)
|
|
874
1020
|
}
|
|
1021
|
+
|
|
1022
|
+
// >! If any parameter failed, clear params (preserves existing behavior)
|
|
1023
|
+
rValue.params = rValue.isValid ? collectedParams : {};
|
|
875
1024
|
}
|
|
876
1025
|
return rValue;
|
|
877
1026
|
}
|
|
@@ -889,13 +1038,13 @@ class ClientRequest extends RequestInfo {
|
|
|
889
1038
|
* const isValid = #hasValidPathParameters();
|
|
890
1039
|
*/
|
|
891
1040
|
#hasValidPathParameters() {
|
|
892
|
-
const { isValid, params } = this.#hasValidParameters(
|
|
1041
|
+
const { isValid, params, invalidParams } = this.#hasValidParameters(
|
|
893
1042
|
ClientRequest.getParameterValidations()?.pathParameters,
|
|
894
1043
|
this.#event?.pathParameters,
|
|
895
1044
|
this.#validationMatchers.pathParameters
|
|
896
1045
|
);
|
|
897
1046
|
this.#props.pathParameters = params;
|
|
898
|
-
return isValid;
|
|
1047
|
+
return { isValid, invalidParams };
|
|
899
1048
|
}
|
|
900
1049
|
|
|
901
1050
|
/**
|
|
@@ -923,13 +1072,13 @@ class ClientRequest extends RequestInfo {
|
|
|
923
1072
|
const paramValidations = ClientRequest.getParameterValidations();
|
|
924
1073
|
const queryValidations = paramValidations?.queryStringParameters || paramValidations?.queryParameters;
|
|
925
1074
|
|
|
926
|
-
const { isValid, params } = this.#hasValidParameters(
|
|
1075
|
+
const { isValid, params, invalidParams } = this.#hasValidParameters(
|
|
927
1076
|
queryValidations,
|
|
928
1077
|
qs,
|
|
929
1078
|
this.#validationMatchers.queryStringParameters
|
|
930
1079
|
);
|
|
931
1080
|
this.#props.queryStringParameters = params;
|
|
932
|
-
return isValid;
|
|
1081
|
+
return { isValid, invalidParams };
|
|
933
1082
|
}
|
|
934
1083
|
|
|
935
1084
|
/**
|
|
@@ -977,23 +1126,23 @@ class ClientRequest extends RequestInfo {
|
|
|
977
1126
|
const camelCaseKey = key.toLowerCase().replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
978
1127
|
headers[camelCaseKey] = this.#event.headers[key];
|
|
979
1128
|
}
|
|
980
|
-
const { isValid, params } = this.#hasValidParameters(
|
|
1129
|
+
const { isValid, params, invalidParams } = this.#hasValidParameters(
|
|
981
1130
|
ClientRequest.getParameterValidations()?.headerParameters,
|
|
982
1131
|
headers,
|
|
983
1132
|
this.#validationMatchers.headerParameters
|
|
984
1133
|
);
|
|
985
1134
|
this.#props.headerParameters = params;
|
|
986
|
-
return isValid;
|
|
1135
|
+
return { isValid, invalidParams };
|
|
987
1136
|
}
|
|
988
1137
|
|
|
989
1138
|
#hasValidCookieParameters() {
|
|
990
|
-
const { isValid, params } = this.#hasValidParameters(
|
|
1139
|
+
const { isValid, params, invalidParams } = this.#hasValidParameters(
|
|
991
1140
|
ClientRequest.getParameterValidations()?.cookieParameters,
|
|
992
1141
|
this.#event?.cookie,
|
|
993
1142
|
this.#validationMatchers.cookieParameters
|
|
994
1143
|
);
|
|
995
1144
|
this.#props.cookieParameters = params;
|
|
996
|
-
return isValid;
|
|
1145
|
+
return { isValid, invalidParams };
|
|
997
1146
|
}
|
|
998
1147
|
|
|
999
1148
|
/**
|
|
@@ -1028,12 +1177,12 @@ class ClientRequest extends RequestInfo {
|
|
|
1028
1177
|
error?.stack
|
|
1029
1178
|
);
|
|
1030
1179
|
this.#props.bodyParameters = {};
|
|
1031
|
-
return false;
|
|
1180
|
+
return { isValid: false, invalidParams: [], invalidBody: true };
|
|
1032
1181
|
}
|
|
1033
1182
|
}
|
|
1034
1183
|
|
|
1035
1184
|
// >! Use existing validation framework with body validation matcher
|
|
1036
|
-
const { isValid, params } = this.#hasValidParameters(
|
|
1185
|
+
const { isValid, params, invalidParams } = this.#hasValidParameters(
|
|
1037
1186
|
ClientRequest.getParameterValidations()?.bodyParameters,
|
|
1038
1187
|
bodyObject,
|
|
1039
1188
|
this.#validationMatchers.bodyParameters
|
|
@@ -1041,7 +1190,7 @@ class ClientRequest extends RequestInfo {
|
|
|
1041
1190
|
|
|
1042
1191
|
// >! Store validated parameters
|
|
1043
1192
|
this.#props.bodyParameters = params;
|
|
1044
|
-
return isValid;
|
|
1193
|
+
return { isValid, invalidParams };
|
|
1045
1194
|
}
|
|
1046
1195
|
|
|
1047
1196
|
|
|
@@ -1252,7 +1401,7 @@ class ClientRequest extends RequestInfo {
|
|
|
1252
1401
|
* Get the _processed_ request properties. These are the properties that
|
|
1253
1402
|
* the ClientRequest object took from the event sent to Lambda, validated,
|
|
1254
1403
|
* supplemented, and makes available to controllers.
|
|
1255
|
-
* @returns {{ method: string, path: string, pathArray: string[], resource: string, resourceArray[], pathParameters: {}, queryStringParameters: {}, headerParameters: {}, cookieParameters: {}, bodyPayload: string, client: {isAuthenticated: boolean, isGuest: boolean, authorizations: string[], roles: string[]}, deadline: number, calcMsToDeadline: number}
|
|
1404
|
+
* @returns {{ method: string, path: string, pathArray: string[], resource: string, resourceArray[], pathParameters: {}, queryStringParameters: {}, headerParameters: {}, cookieParameters: {}, bodyParameters: {}, bodyPayload: string, client: {ip: string, userAgent: string, origin: string, referrer: string, isAuthenticated: boolean, isGuest: boolean, authorizations: string[], roles: string[]}, deadline: number, calcMsToDeadline: number}
|
|
1256
1405
|
*/
|
|
1257
1406
|
getProps() {
|
|
1258
1407
|
return this.#props;
|
|
@@ -32,7 +32,7 @@ const { safeClone } = require('./utils');
|
|
|
32
32
|
*
|
|
33
33
|
* // Use with endpoint requests
|
|
34
34
|
* const dbConn = connections.get('database');
|
|
35
|
-
* const result = await endpoint.
|
|
35
|
+
* const result = await endpoint.send(dbConn);
|
|
36
36
|
*/
|
|
37
37
|
class Connections {
|
|
38
38
|
|
|
@@ -132,7 +132,7 @@ class Connections {
|
|
|
132
132
|
* The Connection object provides the base for requests but does not carry
|
|
133
133
|
* the request. myConnection.get() will return an object (associative array)
|
|
134
134
|
* that can then be used to generate and submit a request to a DAO class or
|
|
135
|
-
*
|
|
135
|
+
* ApiRequest object.
|
|
136
136
|
* You can store and manage multiple connections using the Connections object.
|
|
137
137
|
* @example
|
|
138
138
|
* // Create a simple connection
|
|
@@ -145,7 +145,7 @@ class Connections {
|
|
|
145
145
|
*
|
|
146
146
|
* // Get connection object for use with endpoint
|
|
147
147
|
* const connObj = apiConnection.get();
|
|
148
|
-
* const users = await endpoint.
|
|
148
|
+
* const users = await endpoint.send(connObj);
|
|
149
149
|
*
|
|
150
150
|
* @example
|
|
151
151
|
* // Create connection with authentication
|
|
@@ -581,7 +581,7 @@ class ConnectionAuthentication {
|
|
|
581
581
|
* request.addParameter('page', 1);
|
|
582
582
|
*
|
|
583
583
|
* // Make the request
|
|
584
|
-
* const result = await endpoint.
|
|
584
|
+
* const result = await endpoint.send(request);
|
|
585
585
|
*
|
|
586
586
|
* @example
|
|
587
587
|
* // Build a request dynamically in a DAO
|
|
@@ -597,7 +597,7 @@ class ConnectionAuthentication {
|
|
|
597
597
|
* 'Content-Type': 'application/json'
|
|
598
598
|
* });
|
|
599
599
|
*
|
|
600
|
-
* return await endpoint.
|
|
600
|
+
* return await endpoint.send(request);
|
|
601
601
|
* }
|
|
602
602
|
* }
|
|
603
603
|
*/
|
|
@@ -525,6 +525,31 @@ class Response {
|
|
|
525
525
|
}
|
|
526
526
|
};
|
|
527
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Sets a message or messages property on the JSON response body.
|
|
530
|
+
* If the body is not a JSON object, this method is a no-op.
|
|
531
|
+
* Does not alter the status code or headers.
|
|
532
|
+
*
|
|
533
|
+
* @param {string|Array<string>} message - A single message string or array of message strings
|
|
534
|
+
* @returns {void}
|
|
535
|
+
* @example
|
|
536
|
+
* // Single message
|
|
537
|
+
* response.setMessage("Invalid parameter: limit");
|
|
538
|
+
* // body becomes: { ...existingBody, message: "Invalid parameter: limit" }
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* // Multiple messages
|
|
542
|
+
* response.setMessage(["Invalid parameter: limit", "Invalid parameter: offset"]);
|
|
543
|
+
* // body becomes: { ...existingBody, messages: ["Invalid parameter: limit", "Invalid parameter: offset"] }
|
|
544
|
+
*/
|
|
545
|
+
setMessage = (message) => {
|
|
546
|
+
if (Array.isArray(message)) {
|
|
547
|
+
this.addToJsonBody({ messages: message });
|
|
548
|
+
} else {
|
|
549
|
+
this.addToJsonBody({ message: message });
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
|
|
528
553
|
/**
|
|
529
554
|
* Converts the response to a plain object.
|
|
530
555
|
*
|
|
@@ -1,121 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
const { createGenericResponseModule } = require("./generic.response");
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
"Content-Type": contentType
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
html = (title, body) => {
|
|
3
|
+
const html = (title, body) => {
|
|
8
4
|
return `<html><head><title>${title}</title></head><body>${body}</body></html>`;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
response200 = {
|
|
12
|
-
statusCode: 200,
|
|
13
|
-
headers: headers,
|
|
14
|
-
body: html("200 OK", "<p>Success</p>")
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
response400 = {
|
|
18
|
-
statusCode: 400,
|
|
19
|
-
headers: headers,
|
|
20
|
-
body: html("400 Bad Request", "<p>Bad Request</p>")
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
response401 = {
|
|
24
|
-
statusCode: 401,
|
|
25
|
-
headers: headers,
|
|
26
|
-
body: html("401 Unauthorized", "<p>Unauthorized</p>")
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
response403 = {
|
|
30
|
-
statusCode: 403,
|
|
31
|
-
headers: headers,
|
|
32
|
-
body: html("403 Forbidden", "<p>Forbidden</p>")
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
response404 = {
|
|
36
|
-
statusCode: 404,
|
|
37
|
-
headers: headers,
|
|
38
|
-
body: html("404 Not Found", "<p>Not Found</p>")
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
response405 = {
|
|
42
|
-
statusCode: 405,
|
|
43
|
-
headers: headers,
|
|
44
|
-
body: html("405 Method Not Allowed", "<p>Method Not Allowed</p>")
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
response408 = {
|
|
48
|
-
statusCode: 408,
|
|
49
|
-
headers: headers,
|
|
50
|
-
body: html("408 Request Timeout", "<p>Request Timeout</p>")
|
|
51
5
|
};
|
|
52
6
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
body: html("418 I'm a teapot", "<p>I'm a teapot</p>")
|
|
7
|
+
const HTML_TITLE_MAP = {
|
|
8
|
+
200: "OK",
|
|
9
|
+
500: "Error"
|
|
57
10
|
};
|
|
58
11
|
|
|
59
|
-
|
|
60
|
-
statusCode: 427,
|
|
61
|
-
headers: headers,
|
|
62
|
-
body: html("427 Too Many Requests", "<p>Too Many Requests</p>")
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
response500 = {
|
|
66
|
-
statusCode: 500,
|
|
67
|
-
headers: headers,
|
|
68
|
-
body: html("500 Error", "<p>Internal Server Error</p>")
|
|
69
|
-
};
|
|
12
|
+
const htmlBodyFormatter = (statusCode, message) => html(statusCode + " " + (HTML_TITLE_MAP[statusCode] || message), "<p>" + message + "</p>");
|
|
70
13
|
|
|
71
|
-
|
|
72
|
-
*
|
|
73
|
-
* @param {number|string} statusCode
|
|
74
|
-
* @returns {{statusCode: number, headers: object, body: Array|Object|string}}
|
|
75
|
-
*/
|
|
76
|
-
response = function (statusCode) {
|
|
77
|
-
// convert to int
|
|
78
|
-
statusCode = parseInt(statusCode, 10);
|
|
79
|
-
|
|
80
|
-
switch (statusCode) {
|
|
81
|
-
case 200:
|
|
82
|
-
return this.response200;
|
|
83
|
-
case 400:
|
|
84
|
-
return this.response400;
|
|
85
|
-
case 401:
|
|
86
|
-
return this.response401;
|
|
87
|
-
case 403:
|
|
88
|
-
return this.response403;
|
|
89
|
-
case 404:
|
|
90
|
-
return this.response404;
|
|
91
|
-
case 405:
|
|
92
|
-
return this.response405;
|
|
93
|
-
case 408:
|
|
94
|
-
return this.response408;
|
|
95
|
-
case 418:
|
|
96
|
-
return this.response418;
|
|
97
|
-
case 427:
|
|
98
|
-
return this.response427;
|
|
99
|
-
case 500:
|
|
100
|
-
return this.response500;
|
|
101
|
-
default:
|
|
102
|
-
return this.response500;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
14
|
+
const mod = createGenericResponseModule("text/html; charset=utf-8", htmlBodyFormatter);
|
|
105
15
|
|
|
106
|
-
module.exports = {
|
|
107
|
-
contentType,
|
|
108
|
-
headers,
|
|
109
|
-
html,
|
|
110
|
-
response200,
|
|
111
|
-
response400,
|
|
112
|
-
response401,
|
|
113
|
-
response403,
|
|
114
|
-
response404,
|
|
115
|
-
response405,
|
|
116
|
-
response408,
|
|
117
|
-
response418,
|
|
118
|
-
response427,
|
|
119
|
-
response500,
|
|
120
|
-
response
|
|
121
|
-
}
|
|
16
|
+
module.exports = { ...mod, html };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized generic response module factory.
|
|
3
|
+
*
|
|
4
|
+
* Encapsulates the shared STATUS_CODE_MAP, response object generation, and
|
|
5
|
+
* response() lookup function used by all five format-specific generic response
|
|
6
|
+
* files (HTML, JSON, RSS, Text, XML).
|
|
7
|
+
*
|
|
8
|
+
* @module generic.response
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Map of HTTP status codes to their default message strings.
|
|
13
|
+
*
|
|
14
|
+
* @type {Object.<number, string>}
|
|
15
|
+
*/
|
|
16
|
+
const STATUS_CODE_MAP = {
|
|
17
|
+
200: "Success",
|
|
18
|
+
400: "Bad Request",
|
|
19
|
+
401: "Unauthorized",
|
|
20
|
+
403: "Forbidden",
|
|
21
|
+
404: "Not Found",
|
|
22
|
+
405: "Method Not Allowed",
|
|
23
|
+
408: "Request Timeout",
|
|
24
|
+
418: "I'm a teapot",
|
|
25
|
+
427: "Too Many Requests",
|
|
26
|
+
500: "Internal Server Error"
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Create a complete generic response module for a given content type and body formatter.
|
|
31
|
+
*
|
|
32
|
+
* Iterates over STATUS_CODE_MAP, calls bodyFormatter(statusCode, message) for each
|
|
33
|
+
* entry, and builds the response objects. Attaches a response() function that parses
|
|
34
|
+
* the status code to an integer and looks up the matching response object, falling
|
|
35
|
+
* back to response500 for unknown codes.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} contentType - MIME content type string (e.g., "application/json")
|
|
38
|
+
* @param {function(number, string): *} bodyFormatter - Function that transforms (statusCode, message) into format-specific body
|
|
39
|
+
* @returns {{contentType: string, headers: Object, response200: Object, response400: Object, response401: Object, response403: Object, response404: Object, response405: Object, response408: Object, response418: Object, response427: Object, response500: Object, response: function(number|string): Object}}
|
|
40
|
+
* @example
|
|
41
|
+
* const { createGenericResponseModule } = require("./generic.response");
|
|
42
|
+
*
|
|
43
|
+
* const mod = createGenericResponseModule("application/json", (statusCode, message) => ({ message }));
|
|
44
|
+
* console.log(mod.response200.body); // { message: "Success" }
|
|
45
|
+
* console.log(mod.response(404).statusCode); // 404
|
|
46
|
+
*/
|
|
47
|
+
function createGenericResponseModule(contentType, bodyFormatter) {
|
|
48
|
+
const headers = { "Content-Type": contentType };
|
|
49
|
+
|
|
50
|
+
const mod = {
|
|
51
|
+
contentType: contentType,
|
|
52
|
+
headers: headers
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
for (const code in STATUS_CODE_MAP) {
|
|
56
|
+
const statusCode = parseInt(code, 10);
|
|
57
|
+
const message = STATUS_CODE_MAP[code];
|
|
58
|
+
mod["response" + statusCode] = {
|
|
59
|
+
statusCode: statusCode,
|
|
60
|
+
headers: headers,
|
|
61
|
+
body: bodyFormatter(statusCode, message)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
mod.response = function (statusCode) {
|
|
66
|
+
statusCode = parseInt(statusCode, 10);
|
|
67
|
+
return this["response" + statusCode] || this.response500;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return mod;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { createGenericResponseModule };
|