aws-lambda-middleware
Advanced tools
Comparing version 0.9.1 to 1.0.0
146
index.js
const Middleware = require('./src/Middleware') | ||
const PropTypes = require('./src/PropTypes') | ||
const Validate = require('./src/Validate') | ||
const Message = require('./src/Message') | ||
const common = require('./src/common') | ||
/** ========== addRules ========== */ | ||
/** ========== Default global option ========== */ | ||
Middleware.globalOption({ | ||
//single, simple, full | ||
pathPropNameType: 'simple' | ||
}) | ||
/** ========== PropTypes addRules ========== */ | ||
PropTypes.addRules({ | ||
@@ -92,26 +102,118 @@ //String | ||
}) | ||
} | ||
}) | ||
/** ========== Validate addRules ========== */ | ||
Validate.addRules({ | ||
/** | ||
* @param {String | Array | Object} value | ||
* @param {Object} option { min, max } | ||
*/ | ||
length: { | ||
valid: (value, option, sibling, event) => { | ||
const length = common.isObject(value) ? Object.keys(value).length : (value?.length || 0) | ||
if (common.isObject(option)) { | ||
let isValid = true | ||
if (!common.isEmpty(option.min)) { | ||
isValid = length >= option.min | ||
} | ||
if (isValid && !common.isEmpty(option.max)) { | ||
isValid = length <= option.max | ||
} | ||
return isValid | ||
} else { | ||
return length == option | ||
} | ||
}, | ||
message: `length of '{{propName}}'{{#unless max}} can be from {{#unless min}}{{min}} {{/unless}}~ {{max}}{{/unless}}{{#less max}} must be {{option}}{{/less}}` | ||
}, | ||
//Array | ||
get array () { | ||
return PropTypes.makeRule({ | ||
validType: (value, isDefaultValue) => { | ||
return Array.isArray(value) | ||
}, | ||
validRequired: (value) => { | ||
return value.length > 0 | ||
/** | ||
* @param {Number | Int} value | ||
* @param {Int} option | ||
*/ | ||
min: { | ||
valid: (value, option, sibling, event) => { | ||
const val = common.isNumber(value) ? value : 0 | ||
return val >= option | ||
}, | ||
message: `'{{propName}}' can be from {{min}}` | ||
}, | ||
/** | ||
* @param {Number | Int} value | ||
* @param {Int} option | ||
*/ | ||
max: { | ||
valid: (value, option, sibling, event) => { | ||
const val = common.isNumber(value) ? value : 0 | ||
return val <= option | ||
}, | ||
message: `'{{propName}}' can be up to {{max}}` | ||
}, | ||
/** | ||
* @param {Number | Int | String | Boolean} value | ||
* @param {Array} option [1, 2] | ||
*/ | ||
or: { | ||
valid: (value, option, sibling, event) => { | ||
const opt = Array.isArray(option) ? option : [] | ||
return opt.includes(value) | ||
}, | ||
message: `'{{propName}}' can only have values {{option}}` | ||
}, | ||
/** | ||
* @param {String} value | ||
*/ | ||
digit: { | ||
valid: (value, option, sibling, event) => { | ||
return /^[0-9]+$/.test(value) | ||
}, | ||
message: `'{{propName}}' can only be the string 0-9` | ||
}, | ||
/** | ||
* @param {String} value | ||
* @param {String} option upper, lower | ||
*/ | ||
alphabet: { | ||
valid: (value, option, sibling, event) => { | ||
let reg = /^[a-z]+$/i | ||
if (option === 'upper') { | ||
reg = /^[A-Z]+$/ | ||
} else if (option === 'lower') { | ||
reg = /^[a-z]+$/ | ||
} | ||
}) | ||
return reg.test(value) | ||
}, | ||
message: `'{{propName}}' can only contain {{#unless option}}{{option}} {{/unless}}alphabets` | ||
}, | ||
//Object | ||
get object () { | ||
return PropTypes.makeRule({ | ||
validType: (value, isDefaultValue) => { | ||
return common.isObject(value) | ||
}, | ||
validRequired: (value) => { | ||
return !common.isEmpty(value) | ||
/** | ||
* alphabets + 0-9 | ||
* @param {String} value | ||
* @param {String} option upper, lower | ||
*/ | ||
alphaDigit: { | ||
valid: (value, option, sibling, event) => { | ||
let reg = /^[a-z0-9]+$/i | ||
if (option === 'upper') { | ||
reg = /^[A-Z0-9]+$/ | ||
} else if (option === 'lower') { | ||
reg = /^[a-z0-9]+$/ | ||
} | ||
}) | ||
return reg.test(value) | ||
}, | ||
message: `'{{propName}}' can only contain {{#unless option}}{{option}} {{/unless}}alphabets and 0-9` | ||
} | ||
@@ -121,4 +223,8 @@ }) | ||
exports.common = common | ||
exports.Message = Message | ||
exports.Middleware = Middleware | ||
exports.PropTypes = PropTypes | ||
exports.common = common | ||
exports.Validate = Validate | ||
//short constant | ||
exports.Prop = PropTypes |
{ | ||
"name": "aws-lambda-middleware", | ||
"version": "0.9.1", | ||
"version": "1.0.0", | ||
"engines": { | ||
"node": ">=8.3.0" | ||
"node": ">=12.0.0" | ||
}, | ||
@@ -12,6 +12,8 @@ "description": "AWS Lambda Middleware", | ||
"eslint-plugin-node": "^11.1.0", | ||
"eslint-plugin-import": "^2.27.5" | ||
"eslint-plugin-import": "^2.29.1" | ||
}, | ||
"scripts": { | ||
"test": "node ./test/run.js" | ||
"validate": "node ./test/validate.js", | ||
"testCase": "node ./test/testCase.js", | ||
"message": "node ./test/message.js" | ||
}, | ||
@@ -26,3 +28,6 @@ "repository": { | ||
"middleware", | ||
"validate", | ||
"validation", | ||
"lambda middleware", | ||
"lambda validation", | ||
"lambda payload 2.0" | ||
@@ -29,0 +34,0 @@ ], |
333
README.md
@@ -10,4 +10,8 @@ # aws-lambda-middleware | ||
It is implemented as lightly as possible to reduce the burden when running Lambda. | ||
> Lambda Payload 2.0 supported. | ||
> **🚀 v1.0 added features** | ||
> A Validate function that is easy to expand and use has been added, and deep data of arrays and objects can now be processed. | ||
> | ||
> It is compatible even in environments other than lambda. (node express etc.) | ||
| ||
@@ -24,3 +28,3 @@ | ||
```js | ||
const { Middleware, PropTypes } = require('aws-lambda-middleware') | ||
const { Middleware, Prop } = require('aws-lambda-middleware') | ||
@@ -30,5 +34,10 @@ | ||
queryStringParameters: { | ||
username: PropTypes.string.isRequired, | ||
age: PropTypes.integer, | ||
photos: PropTypes.array.default([]) | ||
username: Prop.string.required(), | ||
age: Prop.integer, | ||
friends: [ | ||
{ | ||
name: Prop.string.length({ max: 20 }), | ||
gender: Prop.string.or(['male', 'female']) | ||
} | ||
] | ||
} | ||
@@ -52,204 +61,80 @@ }).add(async (event, context, prevData) => { | ||
## Options | ||
You can set global options and cluster options. | ||
Setting priority is `globalOption < clusterOption < callbackResult` | ||
You can set global options and cluster options. | ||
> You can set options such as `trim`. | ||
### callbackData: *{Object}* | ||
> Common data applied during callback | ||
[📖 Options detail docs](docs/OPTIONS.md) | ||
```js | ||
const { Middleware, PropTypes } = require('aws-lambda-middleware') | ||
## Middleware | ||
You can simply apply Middleware in Lambda. | ||
Middleware.globalOption({ | ||
callbackData: { | ||
headers: { 'Access-Control-Allow-Origin': '*' } | ||
} | ||
}) | ||
``` | ||
[📖 Middleware detail docs](docs/MIDDLEWEAR.md) | ||
### bodyParser: *{Function}* | ||
> Common event.body parser | ||
## PropTypes | ||
Checks and corrects the data types of request parameters. | ||
> `PropTypes` and `Prop` are the same object. | ||
Currently, event.body parser supports `Content-Type` : `application/json`, `application/x-www-form-urlencoded`. | ||
The query string parser supports the following formats (application/x-www-form-urlencoded): | ||
``` | ||
'foo=1&foo=&foo=3&name=jim&profile[age]=20' | ||
'foo[]=1&foo[]=&foo[]=3&name=jim&profile[age]=20' | ||
'foo[2]=3&foo[1]=&foo[0]=1&name=jim&profile[age]=20' | ||
'foo[2]=3&foo[1]=&foo[0]=1&name=jim&profile[age]=20' | ||
[📖 PropTypes detail docs](docs/PROP_TYPES.md) | ||
//only parse up to 2 depth | ||
//return { foo: [ '1', '', '3' ], name: 'jim', profile: { age: '20' } } | ||
``` | ||
If you want to use another type of body parser, you can apply it at this point. | ||
## Validate | ||
It only verifies the validity of the request parameter value. | ||
> You can use it by adding custom rules. | ||
```js | ||
const { Middleware, PropTypes, common } = require('aws-lambda-middleware') | ||
const qs = require('qs') | ||
[📖 Validate detail docs](docs/VALIDATE.md) | ||
Middleware.globalOption({ | ||
bodyParser: (event = {}) => { | ||
const contentType = common.getHeader(event, 'Content-Type') | ||
if (/application\/x-www-form-urlencoded/i.test(contentType)) { | ||
event.body = qs.parse(event.body) | ||
} | ||
} | ||
}) | ||
``` | ||
| ||
Cluster option can be applied to each middleware. | ||
```js | ||
const { Middleware, PropTypes } = require('aws-lambda-middleware') | ||
The rules added to PropTypes and Validate are written in one line and used. | ||
exports.handler = new Middleware({ | ||
callbackData: { | ||
headers: { 'Access-Control-Allow-Origin': '*' } | ||
}, | ||
bodyParser: (event = {}) => { | ||
//code | ||
}, | ||
trim: true | ||
}) | ||
``` | ||
<img src="https://github.com/blaxk/aws-lambda-middleware/assets/16889775/519de528-3cf3-4c70-9695-c9c1f72e81ee" alt="code" style="max-width: 300px;"> | ||
### trim: *{Boolean}* | ||
> When the Trim option is set, whitespaces are removed from both ends of the parameter string. | ||
| ||
The trim option can be applied in three forms. | ||
```js | ||
const { Middleware, PropTypes } = require('aws-lambda-middleware') | ||
## ⚠️ Upgrading from v0.9 to v1.0 | ||
//Apply trim to all parameters for which PropType is set | ||
Middleware.globalOption({ | ||
trim: true | ||
}) | ||
### 1. `object` and `array` expressions | ||
`object` and `array` are designated as reserved prop name, so the rule cannot be overwritten. | ||
//Trim option is applied per handler function | ||
exports.handler = new Middleware({ | ||
trim: true | ||
}).add({ | ||
queryStringParameters: { | ||
username: PropTypes.string.isRequired, | ||
//Apply trim to each parameter (highest priority) | ||
age: { | ||
propType: PropTypes.integer, | ||
trim: false | ||
} | ||
} | ||
}) | ||
[⚠️ Reserved prop names](docs/RESERVED_PROPS.md) | ||
### 2. Option settings for each PropTypes | ||
trim settings for each PropTypes use `.option()`. | ||
```js | ||
{ | ||
param: Prop.string.option({ trim: false }) | ||
} | ||
``` | ||
| ||
### 3. `PropTypes` and `Prop` | ||
The abbreviated `Prop` can be used instead of the `PropTypes`. | ||
`PropTypes` can still be used as well. | ||
## Middleware | ||
### 4. `.isRequired` has been replaced by `.required()`. | ||
`.isRequired` is also compatible, but not recommended. | ||
### Middleware | ||
> constructor | ||
### 5. Parameter type of PropTypes.*.default() function | ||
When dynamically setting the default value of PropTypes, the parameter type has been changed to `named parameters`. | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| clusterOption | *Object* | middleware options | | ||
### add(handler) : *{Middleware}* | ||
> Add Flow handler & ProType rules | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| handler | *Function* | @param *{Object}* `event` Lambda event (converted data type)<br>@param *{Object}* `context` Lambda context<br>@param *{Object}* `prevData` Previous handler return data| | ||
| handler | *Object* | PropTypes rules | | ||
<br/> | ||
**v0.9** | ||
```js | ||
exports.handler = new Middleware().add(async (event, context, prevData) => { | ||
if (event.source === 'serverless-plugin-warmup') { | ||
//If Promise.reject() is returned, execute Lambda handler callback(null, rejectValue) without executing next handler | ||
return Promise.reject('Lambda is warm!') | ||
} | ||
}).add({ | ||
//PropTypes do not need to be added as a first flow | ||
body: { | ||
username: PropTypes.string.isRequired | ||
} | ||
}).add(async (event, context, prevData) => { | ||
//code | ||
return { | ||
statusCode: 200, | ||
body: JSON.stringify({ | ||
message: 'success' | ||
}) | ||
} | ||
}) | ||
Prop.*.default((event) => {}) | ||
``` | ||
#### Example | ||
**v1.0** | ||
```js | ||
exports.handler = new Middleware().add({ | ||
queryStringParameters: { | ||
age: PropTypes.integer.isRequired | ||
}, | ||
pathParameters: { | ||
groupId: PropTypes.integer.isRequired | ||
} | ||
}).add(async (event, context, prevData) => { | ||
const query = event.queryStringParameters | ||
if (query.age > 20) { | ||
return { | ||
myName: 'jone' | ||
} | ||
} else { | ||
return Promise.reject({ | ||
statusCode: 404, | ||
body: JSON.stringify({ | ||
message: 'not found' | ||
}) | ||
}) | ||
} | ||
}).add(async (event, context, prevData) => { | ||
const pathParam = event.pathParameters | ||
console.log(prevData.myName) // 'jone' | ||
return { | ||
statusCode: 200, | ||
body: JSON.stringify({ | ||
message: 'success' | ||
}) | ||
} | ||
}) | ||
Prop.*.default(({ event }) => {}) | ||
``` | ||
| ||
[📖 PropTypes > Support methods](docs/PROP_TYPES.md?tab=readme-ov-file#support-methods) | ||
## PropTypes | ||
Parameter PropTypes validater | ||
### 6. Interpreting `object` and `array` expressions | ||
The interpretation of Object and Array expressions has been changed from `validate only when value exists` to `required validation`. | ||
When setting the body as shown below, the returned status depends, so check the `item` document in `PropTypes > Support methods`. | ||
### Support Types | ||
[📖 PropTypes > Support methods](docs/PROP_TYPES.md?tab=readme-ov-file#support-methods) | ||
| Type | Description | | ||
| --- | --- | | ||
| string | String | | ||
| number | Number or Numberic string | | ||
| integer | Integer or Integeric string | | ||
| bool | Boolean or Boolean string | | ||
| array | Array, isRequired = array.length > 0 | | ||
| object | Object, isRequired = Object.length > 0 | | ||
<br/> | ||
```js | ||
exports.handler = new Middleware().add({ | ||
//Validate child property of Lambda event (queryStringParameters, body, pathParameters ...) | ||
queryStringParameters: { | ||
//Type + Required | ||
username: PropTypes.string.isRequired, | ||
//Only Type (Do not check when there is empty value) | ||
age: PropTypes.integer, | ||
//Type + Set the value that is replaced when the request value is empty | ||
photos: PropTypes.array.default([]), | ||
//The value returned by the function can be set as the default value. | ||
startTime: PropTypes.number.default(event => Date.now()) | ||
body: { | ||
myId: Prop.string | ||
} | ||
@@ -259,94 +144,16 @@ }) | ||
### addRules(rules) | ||
> In addition to the basic rules, new rules can be added. | ||
> Adding with the same type name overrides the existing rule. | ||
> This setting is applied globally. | ||
**v0.9** | ||
Even if the body of the request parameter is an empty Object or has no value, `status = 200` is returned. | ||
| Param | Type | Description | | ||
| --- | --- | --- | | ||
| rules | *Object* | - | | ||
<br/> | ||
**v1.0** | ||
If the body of the request parameter is an empty Object or has no value, `status = 400` is returned. | ||
In order to `validate only when value exists` for the body, you must also set PropTypes on the body. | ||
```js | ||
const { Middleware, PropTypes } = require('aws-lambda-middleware') | ||
PropTypes.addRules({ | ||
//It overrides the existing string rule. | ||
get number () { | ||
return PropTypes.makeRule({ | ||
/** | ||
* Valid function to check data type | ||
* @param {*} value | ||
* @param {Boolean} isDefaultValue Returns true when validating the value type set as the default. | ||
* */ | ||
validType: (value, isDefaultValue) => { | ||
if (!isDefaultValue && typeof value === 'string') { | ||
return /^-*[0-9]*[\.]*[0-9]+$/.test(value) && !/^0[0-9]+/.test(value) && !/^-0[0-9]+/.test(value) && !(value.length === 1 && value === '-') | ||
} else { | ||
return typeof value === 'number' | ||
} | ||
}, | ||
//Valid function to check if it is required | ||
validRequired: (value) => { | ||
return !isNaN(value) | ||
}, | ||
//A function that converts the value of Paramers when it is incorrectly converted to a string. (Set only when necessary) | ||
convert: (value) => { | ||
if (typeof value === 'string') { | ||
return Number(value) | ||
} else { | ||
return value | ||
} | ||
} | ||
}) | ||
}, | ||
//Multiple settings are possible at once | ||
get string () { | ||
return PropTypes.makeRule({ | ||
validType: (value, isDefaultValue) => { | ||
return ... | ||
}, | ||
validRequired: (value) => { | ||
return ... | ||
}, | ||
convert: (value) => { | ||
return ... | ||
} | ||
}) | ||
} | ||
exports.handler = new Middleware().add({ | ||
body: Prop.object.item({ | ||
myId: Prop.string | ||
}) | ||
}) | ||
``` | ||
| ||
## Node Version Compatibility | ||
Node.js ^8.3.0 | ||
| ||
## Changelog | ||
#### 0.9.1 | ||
- Added trim option | ||
- Fixed a bug where "convert" was not executed when applying PropTypes.*.default | ||
#### 0.8.4 | ||
- PropTypes.*.default, Added ability to set the value returned from a method as a default value. | ||
- Validate value type set as default | ||
- Fixed a bug PropTypes.addRules | ||
- body parser improvements | ||
#### 0.7.1 | ||
- Added PropTypes.*.default method | ||
- Added PropTypes.object | ||
#### 0.6.1 | ||
- Lambda Payload 2.0 support | ||
- Added Lambda error log | ||
#### 0.5.3 | ||
- Fixed a bug PropTypes.boo.isRequired | ||
#### 0.5.2 | ||
- Added and modify body parser options |
@@ -5,6 +5,15 @@ const { URLSearchParams } = require('url') | ||
const Common = { | ||
RESERVED_PROPS: Object.freeze([ | ||
'addRules', 'makeRule', 'isEmpty', 'default', 'isRequired', 'option', 'array', 'object', | ||
'required', 'valid', 'item', 'items' | ||
]), | ||
isObject: (value) => { | ||
return Object.prototype.toString.call(value) === '[object Object]' | ||
return Object.prototype.toString.call(value) === '[object Object]' && !value?._isRule | ||
}, | ||
isNumber: (value) => { | ||
return typeof value === 'number' | ||
}, | ||
isEmpty: (value, isTypeData) => { | ||
@@ -14,16 +23,15 @@ let result = true | ||
if (Array.isArray(value)) { | ||
result = isTypeData ? false : value.length === 0 | ||
result = isTypeData ? false : !value.length | ||
} else if (Common.isObject(value)) { | ||
if (isTypeData) { | ||
result = false | ||
result = !value | ||
} else { | ||
for (const key in value) { | ||
result = false | ||
break | ||
} | ||
result = !(value && Object.keys(value).length) | ||
} | ||
} else if (typeof value === 'number') { | ||
result = isNaN(value) | ||
} else if (typeof value === 'boolean' || value) { | ||
} else if (typeof value === 'boolean') { | ||
result = false | ||
} else { | ||
result = !value | ||
} | ||
@@ -38,2 +46,26 @@ | ||
//Object, Array, String, Number, NaN, Null, Function, Date, Boolean, RegExp, Error, Undefined | ||
type: (value, lowerCase) => { | ||
let result = Object.prototype.toString.call(value) | ||
result = result.match(/^\[\W*object\W+([a-zA-Z]+)\W*\]$/) | ||
if (result && result.length > 1) { | ||
result = result[1] | ||
} | ||
if (result === 'Number') { | ||
if (isNaN(value)) { | ||
result = 'NaN' | ||
} | ||
} else if (result === 'Object') { | ||
if (undefined === value) { | ||
result = 'Undefined' | ||
} else if (null === value) { | ||
result = 'Null' | ||
} | ||
} | ||
return lowerCase ? result.toLowerCase() : result | ||
}, | ||
/** | ||
@@ -55,3 +87,3 @@ * get header | ||
clone: (value) => { | ||
let result; | ||
let result | ||
@@ -71,2 +103,10 @@ if (Array.isArray(value) || Common.isObject(value)) { | ||
info: (...arg) => { | ||
console.info('[aws-lambda-middleware]', ...arg) | ||
}, | ||
warn: (...arg) => { | ||
console.warn('[aws-lambda-middleware]', ...arg) | ||
}, | ||
error: (...arg) => { | ||
@@ -95,5 +135,7 @@ console.error('[aws-lambda-middleware]', ...arg) | ||
for (const [propName, value] of searchParams) { | ||
if (/([^\[\]]+)(\[.*\])/i.test(propName)) { | ||
const name = RegExp.$1 | ||
const depthAry = RegExp.$2.match(/\[[^\[\]]*\]/g) | ||
const match = Common.matchProp(propName) | ||
if (match) { | ||
const name = match.name | ||
const depthAry = match.value.match(/\[[^\[\]]*\]/g) | ||
const depthLength = depthAry.length | ||
@@ -108,3 +150,3 @@ | ||
//object | ||
if (!params.hasOwnProperty(name)) { | ||
if (!Object.hasOwn(params, name)) { | ||
params[name] = {} | ||
@@ -116,3 +158,3 @@ } | ||
//array | ||
if (!params.hasOwnProperty(name)) { | ||
if (!Object.hasOwn(params, name)) { | ||
params[name] = [] | ||
@@ -127,3 +169,3 @@ } | ||
//array | ||
if (params.hasOwnProperty(propName)) { | ||
if (Object.hasOwn(params, propName)) { | ||
if (Array.isArray(params[propName])) { | ||
@@ -158,2 +200,19 @@ params[propName].push(value) | ||
} | ||
}, | ||
/** | ||
* @param {String} propName | ||
* @returns {Object} { name, value } || null | ||
*/ | ||
matchProp (propName) { | ||
const matchAry = propName.match(/([^\[\]]+)(\[.*\])/i) | ||
if (matchAry?.length) { | ||
return { | ||
name: matchAry[1], | ||
value: matchAry[2], | ||
} | ||
} else { | ||
return null | ||
} | ||
} | ||
@@ -163,2 +222,2 @@ } | ||
module.exports = Common | ||
module.exports = Object.freeze(Common) |
@@ -0,4 +1,5 @@ | ||
const PropTypes = require('./PropTypes') | ||
const common = require('./common') | ||
let globalOptions = {} | ||
const _globalOptions = {} | ||
@@ -14,3 +15,5 @@ | ||
if (common.isObject(options)) { | ||
globalOptions = options | ||
for (const key in options) { | ||
_globalOptions[key] = options[key] | ||
} | ||
} else { | ||
@@ -35,2 +38,3 @@ common.error('The globalOptions type is available only for objects.') | ||
this._handler.add = this.add.bind(this) | ||
this._handler.valid = this.valid.bind(this) | ||
} | ||
@@ -60,95 +64,125 @@ | ||
/** ========== Private Methods ========== */ | ||
//lambda handler | ||
async _handler (event = {}, context = {}, callback) { | ||
const evt = this._parseEvent(event) | ||
const flowLength = this._flows.length | ||
let prevData = {} | ||
/** | ||
* add orgin lambda handler | ||
* @param {Function} func | ||
*/ | ||
handler (func) { | ||
if (typeof func === 'function') { | ||
this._flows.push({ | ||
type: 'lambda-handler', | ||
handler: func | ||
}) | ||
} | ||
for (const i in this._flows) { | ||
const flow = this._flows[i] | ||
return this._handler | ||
} | ||
try { | ||
if (flow.type === 'handler') { | ||
prevData = await flow.handler(evt, context, prevData) | ||
} else { | ||
prevData = await this._validPropTypes(evt, flow.props) | ||
} | ||
/** | ||
* Validate data with "PropTypeRule + ValidateRule" set in Middleware | ||
* @param {Object} data Data to be validated | ||
* @returns {Object} { status, message } | ||
* status = none, valid, invalid, error | ||
*/ | ||
valid (data) { | ||
//none, valid, invalid, error | ||
let status = 'none' | ||
let error = '' | ||
//last flow callback | ||
if (flowLength - 1 == i) { | ||
callback(null, common.isObject(prevData) ? { | ||
...globalOptions.callbackData, | ||
...this._options.callbackData, | ||
...prevData | ||
} : prevData) | ||
if (common.isObject(data)) { | ||
for (const i in this._flows) { | ||
const flow = this._flows[i] | ||
break | ||
if (flow.type === 'props') { | ||
try { | ||
error = this._validError(data, flow.props) | ||
if (error) { | ||
status = 'invalid' | ||
} | ||
} catch (err) { | ||
error = err?.stack || err?.message || 'valid error!' | ||
status = 'error' | ||
} | ||
} | ||
} catch (error) { | ||
common.error(error) | ||
if (common.isError(error)) { | ||
callback(error) | ||
} else { | ||
callback(null, common.isObject(error) ? { | ||
...globalOptions.callbackData, | ||
...this._options.callbackData, | ||
...error | ||
} : error) | ||
} | ||
break | ||
if (error) break | ||
} | ||
if (!error) { | ||
status = 'valid' | ||
} | ||
} | ||
return { | ||
status, | ||
message: error | ||
} | ||
} | ||
async _validPropTypes (event, propGroup) { | ||
let errorMsg = event.middlewareBodyParseError || '' | ||
/** ========== Private Methods ========== */ | ||
//lambda handler | ||
async _handler (event = {}, context = {}, callback) { | ||
const evt = this._parseEvent(event) | ||
const flowLength = this._flows.length | ||
let prevData = {} | ||
if (!errorMsg && common.isObject(propGroup)) { | ||
for (const groupKey in propGroup) { | ||
const propTypeRules = propGroup[groupKey] | ||
if (evt.middlewareBodyParseError) { | ||
common.error(evt.middlewareBodyParseError) | ||
callback(null, { | ||
..._globalOptions.callbackData, | ||
...this._options.callbackData, | ||
statusCode: 400, | ||
body: JSON.stringify({ | ||
message: evt.middlewareBodyParseError | ||
}) | ||
}) | ||
} else { | ||
for (const i in this._flows) { | ||
const flow = this._flows[i] | ||
const isLambdaHandler = flow.type === 'lambda-handler' | ||
for (const propName in propTypeRules) { | ||
const propTypeRule = this._getPropTypeRule(propTypeRules[propName]) | ||
const rule = propTypeRule.rule | ||
let val = common.isObject(event[groupKey]) ? event[groupKey][propName] : undefined | ||
try { | ||
if (flow.type === 'props') { | ||
prevData = await this._validation(evt, flow.props) | ||
} else if (flow.type === 'handler') { | ||
prevData = await flow.handler(evt, context, prevData) | ||
} else if (isLambdaHandler) { | ||
prevData = await flow.handler(evt, context, callback) | ||
} | ||
//trim option | ||
if (val && typeof val === 'string') { | ||
event[groupKey][propName] = val = this._trim(val, propTypeRule.options) | ||
//last flow callback | ||
if (flowLength - 1 == i) { | ||
if (!isLambdaHandler) callback(null, common.isObject(prevData) ? { | ||
..._globalOptions.callbackData, | ||
...this._options.callbackData, | ||
...prevData | ||
} : prevData) | ||
break | ||
} | ||
if (typeof rule._invalid === 'function') { | ||
errorMsg = rule._invalid(propName, val) | ||
} catch (error) { | ||
if (common.isError(error)) { | ||
common.error(error) | ||
if (!isLambdaHandler) callback(error) | ||
} else { | ||
// errorMsg = 'You have set propTypes that are not supported.' | ||
common.info(error) | ||
if (!isLambdaHandler) callback(null, common.isObject(error) ? { | ||
..._globalOptions.callbackData, | ||
...this._options.callbackData, | ||
...error | ||
} : error) | ||
} | ||
if (errorMsg) { | ||
break | ||
} else { | ||
//set value & convert | ||
if (common.isObject(event[groupKey])) { | ||
const hasProp = event[groupKey].hasOwnProperty(propName) | ||
if (common.isEmpty(val) && rule._default) { | ||
try { | ||
const defaultVal = await rule._default(propName, event) | ||
event[groupKey][propName] = defaultVal | ||
} catch (error) { | ||
errorMsg = error | ||
break | ||
} | ||
} else if (hasProp && !common.isEmpty(val) && typeof rule._convert === 'function') { | ||
event[groupKey][propName] = rule._convert(val) | ||
} | ||
} | ||
} | ||
break | ||
} | ||
} | ||
} | ||
} | ||
//propType and validate | ||
async _validation (event, props) { | ||
const errorMsg = this._validError(event, props) | ||
if (errorMsg) { | ||
@@ -165,3 +199,174 @@ return Promise.reject({ | ||
} | ||
/** | ||
* All request data is validated using PropTypeRule and ValidateRule and an error message is returned. | ||
* @param {Object} event | ||
* @param {Object} ruleGroup | ||
* @returns {String} | ||
*/ | ||
_validError (event, ruleGroup) { | ||
let error = '' | ||
if (common.isObject(event) && common.isObject(ruleGroup)) { | ||
//propType check | ||
for (const propName in ruleGroup) { | ||
error = this._validProps(propName, ruleGroup, event, event, [propName]) | ||
if (error) break | ||
} | ||
if (!error) { | ||
//validate check | ||
for (const propName in ruleGroup) { | ||
error = this._validValidates(propName, ruleGroup, event, event, [propName]) | ||
if (error) break | ||
} | ||
} | ||
} | ||
return error | ||
} | ||
/** | ||
* recursive call function | ||
* @param {String} propName | ||
* @param {Object} rules | ||
* @param {*} sibling | ||
* @param {Object} event | ||
* @param {Array} pathPropNames | ||
* @returns {String} | ||
*/ | ||
_validProps (propName, rules, sibling, event, pathPropNames = []) { | ||
let error = '' | ||
const propTypeRule = this._getRule(rules[propName]) | ||
if (propTypeRule) { | ||
error = propTypeRule._validPropRules(propName, sibling, event, { | ||
...this._getEtcOption(), | ||
pathPropNames | ||
}) | ||
//When a rule item exists inside propTypeRule | ||
if (!error && propTypeRule._props.item) { | ||
const value = propTypeRule._toValue(propName, sibling) | ||
const item = propTypeRule._getItem(propName, sibling, event) | ||
if (item && !common.isEmpty(value)) { | ||
if (common.isObject(item)) { | ||
for (const key in item) { | ||
error = this._validProps(key, item, value, event, [...pathPropNames, key]) | ||
if (error) break | ||
} | ||
} else if (Array.isArray(item)) { | ||
const arryItems = this._makeArrayItems(value, item) | ||
//array all value | ||
for (const i in value) { | ||
error = this._validProps(i, arryItems, value, event, [...pathPropNames, i]) | ||
if (error) break | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return error | ||
} | ||
/** | ||
* recursive call function | ||
*/ | ||
_validValidates (propName, rules, sibling, event, pathPropNames = []) { | ||
let error = '' | ||
const propTypeRule = this._getRule(rules[propName]) | ||
if (propTypeRule) { | ||
error = propTypeRule._validValidateRules(propName, sibling, event, { | ||
...this._getEtcOption(), | ||
pathPropNames | ||
}) | ||
//When a rule item exists inside validateRule | ||
if (!error && propTypeRule._props.item) { | ||
const value = propTypeRule._toValue(propName, sibling) | ||
const item = propTypeRule._getItem(propName, sibling, event) | ||
if (item && !common.isEmpty(value)) { | ||
if (common.isObject(item)) { | ||
for (const key in item) { | ||
error = this._validValidates(key, item, value, event, [...pathPropNames, key]) | ||
if (error) break | ||
} | ||
} else if (Array.isArray(item)) { | ||
const arryItems = this._makeArrayItems(value, item) | ||
//array all value | ||
for (const i in value) { | ||
error = this._validValidates(i, arryItems, value, event, [...pathPropNames, i]) | ||
if (error) break | ||
} | ||
} | ||
} | ||
} | ||
} | ||
return error | ||
} | ||
_getRule (propTypeRule) { | ||
let result = null | ||
if (propTypeRule) { | ||
if (propTypeRule._isRule) { | ||
result = propTypeRule | ||
} else if (common.isObject(propTypeRule) || Array.isArray(propTypeRule)) { | ||
//Object and Array create default PropTypes | ||
//"required" must be present to maintain compatibility with versions prior to v1.0 | ||
const rule = Array.isArray(propTypeRule) ? PropTypes.array.required() : PropTypes.object.required() | ||
if (!common.isEmpty(propTypeRule)) { | ||
rule.item(propTypeRule) | ||
} | ||
result = rule | ||
} | ||
} | ||
return result | ||
} | ||
_makeArrayItems (values, items) { | ||
const result = [] | ||
const itemLength = items.length | ||
for (const i in values) { | ||
result.push(items[i % itemLength]) | ||
} | ||
return result | ||
} | ||
_getEtcOption () { | ||
let isTrim = !!_globalOptions.trim | ||
let pathPropNameType = _globalOptions.pathPropNameType | ||
let ignoreFirstPathPropNames = _globalOptions.ignoreFirstPathPropNames || [] | ||
if (!common.isEmpty(this._options.trim)) { | ||
isTrim = !!this._options.trim | ||
} | ||
if (!common.isEmpty(this._options.pathPropNameType)) { | ||
pathPropNameType = this._options.pathPropNameType | ||
} | ||
if (Array.isArray(this._options.ignoreFirstPathPropNames)) { | ||
ignoreFirstPathPropNames = this._options.ignoreFirstPathPropNames | ||
} | ||
return { | ||
isTrim, | ||
pathPropNameType, | ||
ignoreFirstPathPropNames | ||
} | ||
} | ||
_parseEvent (event = {}) { | ||
@@ -176,4 +381,4 @@ /** | ||
this._options.bodyParser(event) | ||
} else if (typeof globalOptions.bodyParser === 'function') { | ||
globalOptions.bodyParser(event) | ||
} else if (typeof _globalOptions.bodyParser === 'function') { | ||
_globalOptions.bodyParser(event) | ||
} else { | ||
@@ -198,40 +403,2 @@ common.bodyParser(event) | ||
} | ||
// @returns {Object} { rule, options } | ||
_getPropTypeRule (propTypeRuleData) { | ||
const result = { | ||
rule: {}, | ||
options: {} | ||
} | ||
if (propTypeRuleData && common.isObject(propTypeRuleData)) { | ||
if (propTypeRuleData._isRule) { | ||
result.rule = propTypeRuleData | ||
} else { | ||
for (const key in propTypeRuleData) { | ||
if (key === 'propType') { | ||
if (propTypeRuleData.propType._isRule) { | ||
result.rule = propTypeRuleData.propType | ||
} | ||
} else { | ||
result.options[key] = propTypeRuleData[key] | ||
} | ||
} | ||
} | ||
} | ||
return result | ||
} | ||
_trim (val, propTypeRuleOptions = {}) { | ||
let isTrim = !!globalOptions.trim | ||
if (!common.isEmpty(propTypeRuleOptions.trim)) { | ||
isTrim = propTypeRuleOptions.trim | ||
} else if (!common.isEmpty(this._options.trim)) { | ||
isTrim = this._options.trim | ||
} | ||
return isTrim ? val.trim() : val | ||
} | ||
} | ||
@@ -238,0 +405,0 @@ |
@@ -0,3 +1,5 @@ | ||
const ValidateRule = require('./ValidateRule') | ||
const common = require('./common') | ||
/** | ||
@@ -8,3 +10,3 @@ * PropTypes | ||
* PropTypes.string.default('200') | ||
* PropTypes.string.isRequired | ||
* PropTypes.string.required() | ||
*/ | ||
@@ -14,9 +16,42 @@ const PropTypes = { | ||
/** ========== Public Methods ========== */ | ||
//Array | ||
get array () { | ||
return PropTypes.makeRule({ | ||
propType: 'array', | ||
validType: (value, isDefaultValue) => { | ||
return Array.isArray(value) | ||
}, | ||
validRequired: (value) => { | ||
return !common.isEmpty(value) | ||
} | ||
}) | ||
}, | ||
//Object | ||
get object () { | ||
return PropTypes.makeRule({ | ||
propType: 'object', | ||
validType: (value, isDefaultValue) => { | ||
return common.isObject(value) | ||
}, | ||
validRequired: (value) => { | ||
return !common.isEmpty(value) | ||
} | ||
}) | ||
}, | ||
/** | ||
* add propType rules | ||
* @param {Object} obj | ||
*/ | ||
addRules (obj) { | ||
// Object.setPrototypeOf(PropTypes, obj) | ||
for (const key in obj) { | ||
if (!['addRules', 'makeRule'].includes(key)) { | ||
PropTypes[key] = obj[key] | ||
PropTypes[key]._type = key | ||
if (/^_/.test(key) || common.RESERVED_PROPS.includes(key)) { | ||
common.error(`'${key}' is a reserved word and cannot be added to the rule.`) | ||
} else if (Object.hasOwn(ValidateRule.prototype, key)) { | ||
common.error(`'${key}' rule cannot be added because it overlaps with the validate rule.`) | ||
} else { | ||
const prop = Object.getOwnPropertyDescriptor(obj, key) | ||
Object.defineProperty(PropTypes, key, prop) | ||
} | ||
@@ -26,86 +61,20 @@ } | ||
makeRule ({ validType, validRequired, convert } = {}) { | ||
const invalid = (propName, value, isDefaultValue) => { | ||
let isEmpty = false | ||
/** | ||
* Make propType rule | ||
* @param {Object} rule | ||
* - {Function} validType | ||
* - {Function} validRequired | ||
* - {Function} convert | ||
* @returns {Object} | ||
*/ | ||
makeRule ({ propType, validType, validRequired, convert } = {}) { | ||
return new ValidateRule({ propType, validType, validRequired, convert }) | ||
}, | ||
if (isDefaultValue) { | ||
isEmpty = !(['boolean', 'number', 'string', 'undefined'].includes(typeof value) || value) | ||
} else { | ||
isEmpty = common.isEmpty(value) | ||
} | ||
if (typeof validType === 'function' && !isEmpty && !validType(value, isDefaultValue)) { | ||
return `invalid parameter type '${propName}'` | ||
} | ||
} | ||
const Rule = { | ||
_invalid: invalid, | ||
_convert: convert, | ||
get isRequired () { | ||
return { | ||
_invalid: (propName, value) => { | ||
if (typeof validType === 'function' && typeof validRequired === 'function') { | ||
if (common.isEmpty(value)) { | ||
return `required parameter '${propName}'` | ||
} else if (!validType(value)) { | ||
return `invalid parameter type '${propName}'` | ||
} else if (!validRequired(value)) { | ||
return `required parameter '${propName}'` | ||
} | ||
} | ||
}, | ||
_convert: convert, | ||
_required: true, | ||
_type: this._type, | ||
_isRule: true | ||
} | ||
}, | ||
/** | ||
* Set the value that is replaced when the request value is empty | ||
* @param {*} val | ||
*/ | ||
default: (val) => { | ||
const defaultVal = val | ||
return { | ||
_invalid: invalid, | ||
_convert: convert, | ||
_default: async (propName, event) => { | ||
let value | ||
//get default value | ||
if (typeof defaultVal === 'function') { | ||
try { | ||
value = defaultVal(event) | ||
} catch (error) { | ||
const errMsg = `'${propName}' default function execution error` | ||
common.error(`${errMsg}:`, error) | ||
return Promise.reject(errMsg) | ||
} | ||
} else { | ||
value = defaultVal | ||
} | ||
//valid type | ||
if (invalid(propName, value, true)) { | ||
const invalidMsg = `'${propName}' default value type error` | ||
common.error(`${invalidMsg}, -value:`, value, ' -type:', typeof value) | ||
return Promise.reject(invalidMsg) | ||
} else { | ||
return common.clone(value) | ||
} | ||
}, | ||
_type: Rule._type, | ||
_isRule: true | ||
} | ||
}, | ||
_type: this._type, | ||
_isRule: true | ||
} | ||
return Rule | ||
/** | ||
* @param {*} val | ||
* @returns {Boolean} | ||
*/ | ||
isEmpty (val) { | ||
return common.isEmpty(val) | ||
} | ||
@@ -112,0 +81,0 @@ } |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
41507
11
1246
1
155
1