swagger-tools
Advanced tools
Comparing version
@@ -86,2 +86,18 @@ /* | ||
module.exports.getErrorCount = function getErrorCount (results) { | ||
var errors = 0; | ||
if (results) { | ||
errors = results.errors.length; | ||
_.each(results.apiDeclarations, function (adResults) { | ||
if (adResults) { | ||
errors += adResults.errors.length; | ||
} | ||
}); | ||
} | ||
return errors; | ||
}; | ||
/** | ||
@@ -145,4 +161,6 @@ * Returns the proper specification based on the human readable version. | ||
var printErrorsOrWarnings = function printErrorsOrWarnings (header, entries, indent) { | ||
console.error(header); | ||
console.error(); | ||
if (header) { | ||
console.error(header + ':'); | ||
console.error(); | ||
} | ||
@@ -153,7 +171,9 @@ _.each(entries, function (entry) { | ||
if (entry.inner) { | ||
printErrorsOrWarnings (header, entry.inner, indent + 2); | ||
printErrorsOrWarnings (undefined, entry.inner, indent + 2); | ||
} | ||
}); | ||
console.error(); | ||
if (header) { | ||
console.error(); | ||
} | ||
}; | ||
@@ -168,3 +188,3 @@ var errorCount = 0; | ||
printErrorsOrWarnings('API Errors:', results.errors, 2); | ||
printErrorsOrWarnings('API Errors', results.errors, 2); | ||
} | ||
@@ -175,3 +195,3 @@ | ||
printErrorsOrWarnings('API Warnings:', results.warnings, 2); | ||
printErrorsOrWarnings('API Warnings', results.warnings, 2); | ||
} | ||
@@ -190,3 +210,3 @@ | ||
printErrorsOrWarnings(' API Declaration (' + name + ') Errors:', adResult.errors, 4); | ||
printErrorsOrWarnings(' API Declaration (' + name + ') Errors', adResult.errors, 4); | ||
} | ||
@@ -197,3 +217,3 @@ | ||
printErrorsOrWarnings(' API Declaration (' + name + ') Warnings:', adResult.warnings, 4); | ||
printErrorsOrWarnings(' API Declaration (' + name + ') Warnings', adResult.warnings, 4); | ||
} | ||
@@ -200,0 +220,0 @@ }); |
@@ -32,2 +32,3 @@ /* | ||
var SparkMD5 = require('spark-md5'); | ||
var swaggerConverter = require('swagger-converter'); | ||
var traverse = require('traverse'); | ||
@@ -229,7 +230,12 @@ var validators = require('./validators'); | ||
var handleValidationError = function handleValidationError (results, callback) { | ||
var err = new Error('The Swagger document is invalid and model composition is not possible'); | ||
var err = new Error('The Swagger document(s) are invalid'); | ||
err.errors = results.errors; | ||
err.failedValidation = true; | ||
err.warnings = results.warnings; | ||
if (results.apiDeclarations) { | ||
err.apiDeclarations = results.apiDeclarations; | ||
} | ||
callback(err); | ||
@@ -1158,3 +1164,3 @@ }; | ||
return callback(err); | ||
} else if (helpers.formatResults(results)) { | ||
} else if (helpers.getErrorCount(results) > 0) { | ||
return handleValidationError(results, callback); | ||
@@ -1175,3 +1181,3 @@ } | ||
if (helpers.formatResults(results)) { | ||
if (helpers.getErrorCount(results) > 0) { | ||
return handleValidationError(results, callback); | ||
@@ -1364,3 +1370,3 @@ } | ||
return callback(err); | ||
} else if (helpers.formatResults(results)) { | ||
} else if (helpers.getErrorCount(results) > 0) { | ||
return handleValidationError(results, callback); | ||
@@ -1376,3 +1382,65 @@ } | ||
/** | ||
* Converts the Swagger 1.2 documents to a Swagger 2.0 document. | ||
* | ||
* @param {object} resourceListing - The Swagger Resource Listing | ||
* @param {object[]} [apiDeclarations] - The array of Swagger API Declarations | ||
* @param {boolean=false} [skipValidation] - Whether or not to skip validation | ||
* @param {resultCallback} callback - The result callback | ||
* | ||
* @returns the converted Swagger document | ||
* | ||
* @throws Error if the arguments provided are not valid | ||
*/ | ||
Specification.prototype.convert = function (resourceListing, apiDeclarations, skipValidation, callback) { | ||
var doConvert = function doConvert (resourceListing, apiDeclarations) { | ||
callback(undefined, swaggerConverter(resourceListing, apiDeclarations)); | ||
}.bind(this); | ||
if (this.version !== '1.2') { | ||
throw new Error('Specification#convert only works for Swagger 1.2'); | ||
} | ||
// Validate arguments | ||
if (_.isUndefined(resourceListing)) { | ||
throw new Error('resourceListing is required'); | ||
} else if (!_.isPlainObject(resourceListing)) { | ||
throw new TypeError('resourceListing must be an object'); | ||
} | ||
// API Declarations are optional because swagger-converter was written to support it | ||
if (_.isUndefined(apiDeclarations)) { | ||
apiDeclarations = []; | ||
} | ||
if (!_.isArray(apiDeclarations)) { | ||
throw new TypeError('apiDeclarations must be an array'); | ||
} | ||
if (arguments.length < 4) { | ||
callback = arguments[arguments.length - 1]; | ||
} | ||
if (_.isUndefined(callback)) { | ||
throw new Error('callback is required'); | ||
} else if (!_.isFunction(callback)) { | ||
throw new TypeError('callback must be a function'); | ||
} | ||
if (skipValidation === true) { | ||
doConvert(resourceListing, apiDeclarations); | ||
} else { | ||
this.validate(resourceListing, apiDeclarations, function (err, results) { | ||
if (err) { | ||
return callback(err); | ||
} else if (helpers.getErrorCount(results) > 0) { | ||
return handleValidationError(results, callback); | ||
} | ||
doConvert(resourceListing, apiDeclarations); | ||
}); | ||
} | ||
}; | ||
module.exports.v1 = module.exports.v1_2 = new Specification('1.2'); // jshint ignore:line | ||
module.exports.v2 = module.exports.v2_0 = new Specification('2.0'); // jshint ignore:line |
@@ -160,21 +160,31 @@ /* | ||
/** | ||
* Validates the request's content type (when necessary). | ||
* Validates the request or response content type (when necessary). | ||
* | ||
* @param {string[]} gConsumes - The valid consumes at the API scope | ||
* @param {string[]} oConsumes - The valid consumes at the operation scope | ||
* @param {object} req - The request | ||
* @param {string[]} gPOrC - The valid consumes at the API scope | ||
* @param {string[]} oPOrC - The valid consumes at the operation scope | ||
* @param {object} reqOrRes - The request or response | ||
* | ||
* @throws Error if the content type is invalid | ||
*/ | ||
module.exports.validateContentType = function validateContentType (gConsumes, oConsumes, req) { | ||
module.exports.validateContentType = function validateContentType (gPOrC, oPOrC, reqOrRes) { | ||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1 | ||
var contentType = req.headers['content-type'] || 'application/octet-stream'; | ||
var consumes = _.union(oConsumes, gConsumes); | ||
var isResponse = typeof reqOrRes.end === 'function'; | ||
var contentType = isResponse ? reqOrRes.getHeader('content-type') : reqOrRes.headers['content-type']; | ||
var pOrC = _.union(gPOrC, oPOrC); | ||
if (!contentType) { | ||
if (isResponse) { | ||
contentType = 'text/plain'; | ||
} else { | ||
contentType = 'application/octet-stream'; | ||
} | ||
} | ||
// Get only the content type | ||
contentType = contentType.split(';')[0]; | ||
// Validate content type (Only for POST/PUT per HTTP spec) | ||
if (consumes.length > 0 && ['POST', 'PUT'].indexOf(req.method) !== -1 && consumes.indexOf(contentType) === -1) { | ||
throw new Error('Invalid content type (' + contentType + '). These are valid: ' + consumes.join(', ')); | ||
if (pOrC.length > 0 && (isResponse ? | ||
true : | ||
['POST', 'PUT'].indexOf(reqOrRes.method) !== -1) && pOrC.indexOf(contentType) === -1) { | ||
throw new Error('Invalid content type (' + contentType + '). These are valid: ' + pOrC.join(', ')); | ||
} | ||
@@ -443,2 +453,5 @@ }; | ||
break; | ||
case 'void': | ||
result = _.isUndefined(val); | ||
break; | ||
} | ||
@@ -451,3 +464,5 @@ } | ||
throwErrorWithCode('INVALID_TYPE', | ||
'Not a valid ' + (_.isUndefined(format) ? '' : format + ' ') + type + ': ' + val); | ||
type !== 'void' ? | ||
'Not a valid ' + (_.isUndefined(format) ? '' : format + ' ') + type + ': ' + val : | ||
'Void does not allow a value'); | ||
} | ||
@@ -492,9 +507,16 @@ }; | ||
}; | ||
var type = schema.type; | ||
// Resolve the actual schema object | ||
schema = resolveSchema(schema); | ||
if (!type) { | ||
if (!schema.schema) { | ||
type = 'void'; | ||
} else { | ||
schema = resolveSchema(schema); | ||
type = schema.type || 'object'; | ||
} | ||
} | ||
try { | ||
// Always perform this check even if there is no value | ||
if (schema.type === 'array') { | ||
if (type === 'array') { | ||
validateArrayType(schema); | ||
@@ -515,13 +537,13 @@ } | ||
if (schema.type === 'array') { | ||
if (type === 'array') { | ||
if (!_.isUndefined(schema.items)) { | ||
validateTypeAndFormat(val, schema.type === 'array' ? schema.items.type : schema.type, | ||
schema.type === 'array' && schema.items.format ? | ||
validateTypeAndFormat(val, type === 'array' ? schema.items.type : type, | ||
type === 'array' && schema.items.format ? | ||
schema.items.format : | ||
schema.format); | ||
} else { | ||
validateTypeAndFormat(val, schema.type, schema.format); | ||
validateTypeAndFormat(val, type, schema.format); | ||
} | ||
} else { | ||
validateTypeAndFormat(val, schema.type, schema.format); | ||
validateTypeAndFormat(val, type, schema.format); | ||
} | ||
@@ -533,3 +555,3 @@ | ||
// Validate maximum | ||
validateMaximum(val, schema.maximum, schema.type, schema.exclusiveMaximum); | ||
validateMaximum(val, schema.maximum, type, schema.exclusiveMaximum); | ||
@@ -547,3 +569,3 @@ | ||
// Validate minimum | ||
validateMinimum(val, schema.minimum, schema.type, schema.exclusiveMinimum); | ||
validateMinimum(val, schema.minimum, type, schema.exclusiveMinimum); | ||
@@ -550,0 +572,0 @@ // Validate minItems |
@@ -31,7 +31,6 @@ /* | ||
var send400 = helpers.send400; | ||
var spec = require('../../lib/helpers').getSpec('1.2'); | ||
var validators = require('../../lib/validators'); | ||
/** | ||
* Middleware for using Swagger information to validate API requests prior to sending the request to the route handler. | ||
* Middleware for using Swagger information to validate API requests/responses. | ||
* | ||
@@ -41,5 +40,12 @@ * This middleware also requires that you use the swagger-metadata middleware before this middleware. This middleware | ||
* | ||
* @param {object} [options] - The middleware options | ||
* @param {boolean} [options.validateResponse=false] - Whether or not to validate responses | ||
* | ||
* @returns the middleware function | ||
*/ | ||
exports = module.exports = function swaggerValidatorMiddleware () { | ||
exports = module.exports = function swaggerValidatorMiddleware (options) { | ||
if (_.isUndefined(options)) { | ||
options = {}; | ||
} | ||
return function swaggerValidator (req, res, next) { | ||
@@ -53,2 +59,7 @@ var operation = req.swagger ? req.swagger.operation : undefined; | ||
// If necessary, override 'res.send' | ||
if (options.validateResponse === true) { | ||
helpers.wrapEnd('1.2', req, res, next); | ||
} | ||
// Validate the request | ||
@@ -60,3 +71,2 @@ try { | ||
async.map(operation.parameters, function (parameter, oCallback) { | ||
var isModel = helpers.isModelParameter('1.2', parameter); | ||
var val; | ||
@@ -76,32 +86,4 @@ | ||
validators.validateSchemaConstraints('1.2', parameter, paramPath, val); | ||
helpers.validateValue(req, parameter, paramPath, val, oCallback); | ||
if (isModel) { | ||
async.map(parameter.type === 'array' ? val : [val], function (aVal, callback) { | ||
spec.validateModel(req.swagger.apiDeclaration, | ||
'#/models/' + (parameter.items ? | ||
parameter.items.type || parameter.items.$ref : | ||
parameter.type), | ||
aVal, callback); | ||
}, function (err, allResults) { | ||
if (!err) { | ||
_.each(allResults, function (results) { | ||
if (results) { | ||
err = new Error('Failed schema validation'); | ||
err.code = 'SCHEMA_VALIDATION_FAILED'; | ||
err.errors = results.errors; | ||
err.failedValidation = true; | ||
return false; | ||
} | ||
}); | ||
} | ||
oCallback(err); | ||
}); | ||
} else { | ||
oCallback(); | ||
} | ||
paramIndex++; | ||
@@ -108,0 +90,0 @@ }, function (err) { |
@@ -129,2 +129,3 @@ /* | ||
return function swaggerMetadata (req, res, next) { | ||
var method = req.method.toLowerCase(); | ||
var path = parseurl(req).pathname; | ||
@@ -149,5 +150,6 @@ var match; | ||
if (_.isPlainObject(pathMetadata.operations[req.method.toLowerCase()])) { | ||
metadata.operation = pathMetadata.operations[req.method.toLowerCase()].operation; | ||
metadata.operationParameters = pathMetadata.operations[req.method.toLowerCase()].parameters || []; | ||
if (_.isPlainObject(pathMetadata.operations[method])) { | ||
metadata.operation = pathMetadata.operations[method].operation; | ||
metadata.operationParameters = pathMetadata.operations[method].parameters || []; | ||
metadata.operationPath = ['paths', pathMetadata.apiPath, method]; | ||
metadata.security = metadata.operation.security || metadata.swaggerObject.security || []; | ||
@@ -154,0 +156,0 @@ } |
@@ -34,3 +34,3 @@ /* | ||
/** | ||
* Middleware for using Swagger information to validate API requests prior to sending the request to the route handler. | ||
* Middleware for using Swagger information to validate API requests/responses. | ||
* | ||
@@ -40,5 +40,12 @@ * This middleware also requires that you use the swagger-metadata middleware before this middleware. This middleware | ||
* | ||
* @param {object} [options] - The middleware options | ||
* @param {boolean} [options.validateResponse=false] - Whether or not to validate responses | ||
* | ||
* @returns the middleware function | ||
*/ | ||
exports = module.exports = function swaggerValidatorMiddleware () { | ||
exports = module.exports = function swaggerValidatorMiddleware (options) { | ||
if (_.isUndefined(options)) { | ||
options = {}; | ||
} | ||
return function swaggerValidator (req, res, next) { | ||
@@ -51,2 +58,7 @@ var operation = req.swagger ? req.swagger.operation : undefined; | ||
// If necessary, override 'res.send' | ||
if (options.validateResponse === true) { | ||
helpers.wrapEnd('2.0', req, res, next); | ||
} | ||
// Validate the request | ||
@@ -59,3 +71,2 @@ try { | ||
var parameter = paramMetadata.schema; | ||
var isModel = helpers.isModelParameter('2.0', parameter); | ||
var val; | ||
@@ -75,19 +86,3 @@ | ||
validators.validateSchemaConstraints('2.0', parameter, paramPath, val); | ||
if (isModel) { | ||
async.map(parameter.type === 'array' ? val : [val], function (aVal, callback) { | ||
try { | ||
validators.validateAgainstSchema(parameter.schema, val); | ||
} catch (err) { | ||
return callback(err); | ||
} | ||
return callback(); | ||
}, function (err) { | ||
oCallback(err); | ||
}); | ||
} else { | ||
oCallback(); | ||
} | ||
helpers.validateValue(req, parameter, paramPath, val, oCallback); | ||
}, function (err) { | ||
@@ -94,0 +89,0 @@ if (err) { |
@@ -28,7 +28,9 @@ /* | ||
var _ = require('lodash'); | ||
var async = require('async'); | ||
var fs = require('fs'); | ||
var helpers = require('../lib/helpers'); | ||
var parseurl = require('parseurl'); | ||
var path = require('path'); | ||
var validators = require('../lib/validators'); | ||
var helpers = require('../lib/helpers'); | ||
var operationVerbs = [ | ||
@@ -48,2 +50,33 @@ 'DELETE', | ||
var isModelParameter = module.exports.isModelParameter = function isModelParameter (version, param) { | ||
var spec = helpers.getSpec(version); | ||
var isModel = false; | ||
switch (version) { | ||
case '1.2': | ||
if (!_.isUndefined(param.type) && isModelType(spec, param.type)) { | ||
isModel = true; | ||
} else if (param.type === 'array' && isModelType(spec, param.items ? | ||
param.items.type || param.items.$ref : | ||
undefined)) { | ||
isModel = true; | ||
} | ||
break; | ||
case '2.0': | ||
if (param.type === 'object' || !param.type) { | ||
isModel = true; | ||
} else if (!_.isUndefined(param.schema) && (param.schema.type === 'object' || !_.isUndefined(param.schema.$ref))) { | ||
isModel = true; | ||
} | ||
// 2.0 does not allow arrays of models in the same way Swagger 1.2 does | ||
break; | ||
} | ||
return isModel; | ||
}; | ||
/** | ||
@@ -315,33 +348,2 @@ * Returns an Express style path for the Swagger path. | ||
var isModelParameter = module.exports.isModelParameter = function isModelParameter (version, param) { | ||
var spec = helpers.getSpec(version); | ||
var isModel = false; | ||
switch (version) { | ||
case '1.2': | ||
if (!_.isUndefined(spec, param.type) && isModelType(spec, param.type)) { | ||
isModel = true; | ||
} else if (param.type === 'array' && isModelType(spec, param.items ? | ||
param.items.type || param.items.$ref : | ||
undefined)) { | ||
isModel = true; | ||
} | ||
break; | ||
case '2.0': | ||
if (param.type === 'object' || !param.type) { | ||
isModel = true; | ||
} else if (!_.isUndefined(param.schema) && (param.schema.type === 'object' || !_.isUndefined(param.schema.$ref))) { | ||
isModel = true; | ||
} | ||
// 2.0 does not allow arrays of models in the same way Swagger 1.2 does | ||
break; | ||
} | ||
return isModel; | ||
}; | ||
module.exports.getParameterValue = function getParameterValue (version, parameter, pathKeys, match, req) { | ||
@@ -356,2 +358,3 @@ var defaultVal = version === '1.2' ? parameter.defaultValue : parameter.default; | ||
case 'form': | ||
case 'formData': | ||
if (!req.body) { | ||
@@ -473,1 +476,143 @@ throw new Error('Server configuration error: req.body is not defined but is required'); | ||
}; | ||
var validateValue = module.exports.validateValue = | ||
function validateValue (req, schema, path, val, callback) { | ||
var document = req.swagger.apiDeclaration || req.swagger.swaggerObject; | ||
var version = req.swagger.apiDeclaration ? '1.2' : '2.0'; | ||
var isModel = isModelParameter(version, schema); | ||
var spec = helpers.getSpec(version); | ||
try { | ||
validators.validateSchemaConstraints(version, schema, path, val); | ||
} catch (err) { | ||
return callback(err); | ||
} | ||
if (isModel) { | ||
if (_.isString(val)) { | ||
try { | ||
val = JSON.parse(val); | ||
} catch (err) { | ||
err.failedValidation = true; | ||
err.message = 'Value expected to be an array/object but is not'; | ||
throw err; | ||
} | ||
} | ||
async.map(schema.type === 'array' ? val : [val], function (aVal, oCallback) { | ||
if (version === '1.2') { | ||
spec.validateModel(document, '#/models/' + (schema.items ? | ||
schema.items.type || schema.items.$ref : | ||
schema.type), aVal, oCallback); | ||
} else { | ||
try { | ||
validators.validateAgainstSchema(schema.schema ? schema.schema : schema, val); | ||
oCallback(); | ||
} catch (err) { | ||
oCallback(err); | ||
} | ||
} | ||
}, function (err, allResults) { | ||
if (!err) { | ||
_.each(allResults, function (results) { | ||
if (results && helpers.getErrorCount(results) > 0) { | ||
err = new Error('Failed schema validation'); | ||
err.code = 'SCHEMA_VALIDATION_FAILED'; | ||
err.errors = results.errors; | ||
err.warnings = results.warnings; | ||
err.failedValidation = true; | ||
return false; | ||
} | ||
}); | ||
} | ||
callback(err); | ||
}); | ||
} else { | ||
callback(); | ||
} | ||
}; | ||
module.exports.wrapEnd = function wrapEnd (version, req, res, next) { | ||
var operation = req.swagger.operation; | ||
var originalEnd = res.end; | ||
var vPath = _.cloneDeep(req.swagger.operationPath); | ||
res.end = function end (data, encoding) { | ||
var schema = operation; | ||
var val = data; | ||
// Replace 'res.end' with the original | ||
res.end = originalEnd; | ||
// If the data is a buffer, convert it to a string so we can parse it prior to validation | ||
if (val instanceof Buffer) { | ||
val = data.toString(encoding); | ||
} | ||
try { | ||
// Validate the content type | ||
try { | ||
validators.validateContentType(req.swagger.apiDeclaration ? | ||
req.swagger.apiDeclaration.produces : | ||
req.swagger.swaggerObject.produces, | ||
operation.produces, res); | ||
} catch (err) { | ||
err.failedValidation = true; | ||
throw err; | ||
} | ||
if (_.isUndefined(schema.type)) { | ||
if (schema.schema) { | ||
schema = schema.schema; | ||
} else if (version === '1.2') { | ||
schema = _.find(operation.responseMessages, function (responseMessage, index) { | ||
if (responseMessage.code === res.statusCode) { | ||
vPath.push(['responseMessages', index.toString()]); | ||
return true; | ||
} | ||
}); | ||
if (!_.isUndefined(schema)) { | ||
schema = schema.responseModel; | ||
} | ||
} else { | ||
schema = _.find(operation.responses, function (response, code) { | ||
if (code === res.statusCode.toString()) { | ||
vPath.push(['responses', code]); | ||
return true; | ||
} | ||
}); | ||
} | ||
} | ||
validateValue(req, schema, vPath, val, | ||
function (err) { | ||
if (err) { | ||
throw err; | ||
} | ||
// 'res.end' requires a Buffer or String so if it's not one, create a String | ||
if (!(data instanceof Buffer) && !_.isString(data)) { | ||
data = JSON.stringify(data); | ||
} | ||
res.end(data, encoding); | ||
}); | ||
} catch (err) { | ||
if (err.failedValidation) { | ||
err.message = 'Response validation failed: ' + err.message.charAt(0).toLowerCase() + err.message.substring(1); | ||
} | ||
return next(err); | ||
} | ||
}; | ||
}; |
{ | ||
"name": "swagger-tools", | ||
"version": "0.7.0", | ||
"version": "0.7.1", | ||
"description": "Various tools for using and integrating with Swagger.", | ||
@@ -67,2 +67,3 @@ "main": "index.js", | ||
"superagent": "^0.21.0", | ||
"swagger-converter": "0.0.5", | ||
"traverse": "^0.6.6", | ||
@@ -69,0 +70,0 @@ "yamljs": "^0.2.1", |
@@ -22,3 +22,5 @@ The project provides various tools for integrating and interacting with Swagger. This project is in its infancy but | ||
* Simple CLI for validating Swagger documents | ||
* Simple CLI | ||
* Validate Swagger document(s) | ||
* Convert Swagger 1.2 documents to Swagger 2.0 | ||
* Schema validation: For the file(s) supported by the Swagger specification, ensure they pass structural validation | ||
@@ -32,5 +34,6 @@ based on the [JSON Schema][json-schema] associated with that version of the specification _(Browser and Node)_ | ||
* Connect middleware for using Swagger resource documents for pre-route validation _(Node only)_ | ||
* Validate the request Content-Type based on the operation's `consumes` value(s) | ||
* Validate the request/response Content-Type based on the operation's `consumes/produces` value(s) | ||
* Validate the request parameter types | ||
* Validate the request parameter values | ||
* Validate the response values | ||
@@ -37,0 +40,0 @@ ## Installation |
Sorry, the diff of this file is not supported yet
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
851685
1%17231
1.16%66
4.76%14
7.69%+ Added
+ Added