From b10ec4798a841a59446a0eb1280aeb57b390aae1 Mon Sep 17 00:00:00 2001 From: sebelga Date: Fri, 13 Oct 2017 07:39:56 +0200 Subject: [PATCH 1/4] BREAKING CHANGE: New validation with error codes feature(error handling): moved validation to its own helper file. feat(error handling): test assertions in its own file Add "then" and "catch" Promise chain to validate() response Update all tests --- lib/error.js | 52 ---- lib/errors.js | 106 ++++++++ lib/helper.js | 6 - lib/helpers/index.js | 10 + lib/helpers/validation.js | 303 +++++++++++++++++++++ lib/model.js | 223 +--------------- package.json | 8 +- test/entity-test.js | 0 test/error-test.js | 193 +++++++++----- test/helpers/queryhelpers-test.js | 2 +- test/helpers/validation-test.js | 401 ++++++++++++++++++++++++++++ test/model-test.js | 428 ++---------------------------- yarn.lock | 186 ++++--------- 13 files changed, 1036 insertions(+), 882 deletions(-) delete mode 100644 lib/error.js create mode 100755 lib/errors.js delete mode 100644 lib/helper.js create mode 100644 lib/helpers/index.js create mode 100755 lib/helpers/validation.js mode change 100644 => 100755 lib/model.js mode change 100644 => 100755 package.json mode change 100644 => 100755 test/entity-test.js mode change 100644 => 100755 test/error-test.js create mode 100755 test/helpers/validation-test.js mode change 100644 => 100755 test/model-test.js diff --git a/lib/error.js b/lib/error.js deleted file mode 100644 index 3206335..0000000 --- a/lib/error.js +++ /dev/null @@ -1,52 +0,0 @@ -/* eslint no-use-before-define: "off" */ - -'use strict'; - -class GstoreError extends Error { - constructor(msg) { - super(); - this.constructor.captureStackTrace(this); - - this.message = msg; - this.name = 'GstoreError'; - } - - static get ValidationError() { - return class extends ValidationError {}; - } - - static get ValidatorError() { - return class extends ValidatorError {}; - } -} - -class ValidationError extends GstoreError { - constructor(instance) { - if (instance && instance.constructor.entityKind) { - super(`${instance.constructor.entityKind} validation failed`); - } else if (instance && instance.constructor.name === 'Object') { - super(instance); - } else { - super('Validation failed'); - } - this.name = 'ValidationError'; - } -} - -class ValidatorError extends GstoreError { - constructor(data) { - if (data && data.constructor.name === 'Object') { - data.errorName = data.errorName || 'Wrong format'; - super(data); - } else { - super('Value validation failed'); - } - this.name = 'ValidatorError'; - } -} - -module.exports = { - GstoreError, - ValidationError, - ValidatorError, -}; diff --git a/lib/errors.js b/lib/errors.js new file mode 100755 index 0000000..a0bb47b --- /dev/null +++ b/lib/errors.js @@ -0,0 +1,106 @@ +/* eslint no-use-before-define: "off" */ + +'use strict'; + +const util = require('util'); +const is = require('is'); + +const errorCodes = { + ERR_GENERIC: 'ERR_GENERIC', + ERR_VALIDATION: 'ERR_VALIDATION', + ERR_PROP_TYPE: 'ERR_PROP_TYPE', + ERR_PROP_VALUE: 'ERR_PROP_VALUE', + ERR_PROP_NOT_ALLOWED: 'ERR_PROP_NOT_ALLOWED', + ERR_PROP_REQUIRED: 'ERR_PROP_REQUIRED', + ERR_PROP_IN_RANGE: 'ERR_PROP_IN_RANGE', +}; + +const message = (text, ...args) => util.format(text, ...args); + +const messages = { + ERR_GENERIC: 'An error occured', + ERR_VALIDATION: entityKind => message('The entity data does not validate against the "%s" Schema', entityKind), + ERR_PROP_TYPE: (prop, type) => message('Property "%s" must be a %s', prop, type), + ERR_PROP_VALUE: (value, prop) => message('"%s" is not a valid value for property "%s"', value, prop), + ERR_PROP_NOT_ALLOWED: (prop, entityKind) => ( + message('Property "%s" is not allowed for entityKind "%s"', prop, entityKind) + ), + ERR_PROP_REQUIRED: prop => message('Property "%s" is required but no value has been provided', prop), + ERR_PROP_IN_RANGE: (prop, range) => message('Property "%s" must be one of [%s]', prop, range && range.join(', ')), +}; + +class GstoreError extends Error { + constructor(code, msg, args) { + if (code && code in messages) { + if (is.function(messages[code])) { + msg = messages[code](...args.messageParams); + } else { + msg = messages[code]; + } + } + + if (!msg) { + msg = messages.ERR_GENERIC; + } + + super(msg, code, args); + this.name = 'GstoreError'; + this.message = msg; + this.code = code || errorCodes.ERR_GENERIC; + + if (args) { + Object.keys(args).forEach((k) => { + if (k !== 'messageParams') { + this[k] = args[k]; + } + }); + } + + Error.captureStackTrace(this, this.constructor); + } + + static get TypeError() { + return class extends TypeError {}; + } + + static get ValueError() { + return class extends ValueError {}; + } + + static get ValidationError() { + return class extends ValidationError {}; + } +} + +class ValidationError extends GstoreError { + constructor(...args) { + super(...args); + this.name = 'ValidationError'; + Error.captureStackTrace(this, this.constructor); + } +} + +class TypeError extends GstoreError { + constructor(...args) { + super(...args); + this.name = 'TypeError'; + Error.captureStackTrace(this, this.constructor); + } +} + +class ValueError extends GstoreError { + constructor(...args) { + super(...args); + this.name = 'ValueError'; + Error.captureStackTrace(this, this.constructor); + } +} + +module.exports = { + GstoreError, + ValidationError, + TypeError, + ValueError, + message, + errorCodes, +}; diff --git a/lib/helper.js b/lib/helper.js deleted file mode 100644 index 4484285..0000000 --- a/lib/helper.js +++ /dev/null @@ -1,6 +0,0 @@ - -'use strict'; - -const queryHelpers = require('./helpers/queryhelpers'); - -exports.QueryHelpers = queryHelpers; diff --git a/lib/helpers/index.js b/lib/helpers/index.js new file mode 100644 index 0000000..acac7e1 --- /dev/null +++ b/lib/helpers/index.js @@ -0,0 +1,10 @@ + +'use strict'; + +const queryHelpers = require('./queryhelpers'); +const validation = require('./validation'); + +module.exports = { + queryHelpers, + validation, +}; diff --git a/lib/helpers/validation.js b/lib/helpers/validation.js new file mode 100755 index 0000000..f3f0469 --- /dev/null +++ b/lib/helpers/validation.js @@ -0,0 +1,303 @@ +'use strict'; + +const moment = require('moment'); +const validator = require('validator'); +const is = require('is'); +const Joi = require('joi'); + +const gstoreErrors = require('../errors'); + +const isValidDate = (value) => { + if (value.constructor.name !== 'Date' && + (typeof value !== 'string' || + !value.match(/\d{4}-\d{2}-\d{2}([ ,T])?(\d{2}:\d{2}:\d{2})?(\.\d{1,3})?/) || + !moment(value).isValid())) { + return false; + } + return true; +}; + +const isInt = n => Number(n) === n && n % 1 === 0; + +const isFloat = n => Number(n) === n && n % 1 !== 0; + +const isValueEmpty = v => ( + v === null || v === undefined || (typeof v === 'string' && v.trim().length === 0) +); + +const isValidLngLat = (data) => { + const validLatitude = (isInt(data.latitude) || isFloat(data.latitude)) + && data.latitude >= -90 && data.latitude <= 90; + const validLongitude = (isInt(data.longitude) || isFloat(data.longitude)) + && data.longitude >= -180 && data.longitude <= 180; + + return validLatitude && validLongitude; +}; + +const errorToObject = error => ( + { + code: error.code, + type: error.type, + message: error.message, + } +); + +const validatePropType = (value, propType, prop) => { + let valid; + let ref; + + switch (propType) { + case 'string': + /* eslint valid-typeof: "off" */ + valid = typeof value === 'string'; + ref = 'string.base'; + break; + case 'datetime': + valid = isValidDate(value); + ref = 'datetime.base'; + break; + case 'array': + valid = is.array(value); + ref = 'array.base'; + break; + case 'int': { + const isIntInstance = value.constructor.name === 'Int'; + if (isIntInstance) { + valid = !isNaN(parseInt(value.value, 10)); + } else { + valid = isInt(value); + } + ref = 'int.base'; + break; + } + case 'double': { + const isIntInstance = value.constructor.name === 'Double'; + if (isIntInstance) { + valid = isFloat(parseFloat(value.value, 10)) || + isInt(parseFloat(value.value, 10)); + } else { + valid = isFloat(value) || isInt(value); + } + ref = 'double.base'; + break; + } + case 'buffer': + valid = value instanceof Buffer; + ref = 'buffer.base'; + break; + case 'geoPoint': { + if (is.object(value) && Object.keys(value).length === 2 + && {}.hasOwnProperty.call(value, 'longitude') + && {}.hasOwnProperty.call(value, 'latitude')) { + valid = isValidLngLat(value); + } else { + valid = value.constructor.name === 'GeoPoint'; + } + ref = 'geopoint.base'; + break; + } + default: + /* eslint valid-typeof: "off" */ + valid = typeof value === propType; + ref = 'prop.type'; + } + + if (!valid) { + return new gstoreErrors.TypeError( + gstoreErrors.errorCodes.ERR_PROP_TYPE, + null, + { ref, messageParams: [prop, propType], property: prop } + ); + } + + return null; +}; + +const validatePropValue = (value, validationRule, propType, prop) => { + /** + * Validator.js only works with string values + * let's make sure we are working with a string. + */ + const strValue = typeof value !== 'string' && !is.object(value) ? String(value) : value; + + /** + * By default, the args only contain the value being validated + * and the string, matching the simple case. + */ + let validationArgs = [strValue]; + let validationFn = null; + + /** + * If the validate property is an object, then it's assumed that + * it contains the 'rule' property, which will be the new + * validationRule's value. + * If the 'args' prop was passed then we concat them to + * 'validationArgs'. + */ + + let _is = is; + + if (typeof validationRule === 'object') { + const args = validationRule.args; + validationRule = validationRule.rule; + + if (typeof validationRule === 'function') { + validationFn = validationRule; + validationArgs.push(validator); + } else { + validationFn = validator[validationRule]; + } + + validationArgs = validationArgs.concat(args || []); + } else { + validationFn = validator[validationRule]; + } + + if (!validationFn.apply(validator, validationArgs)) { + return new gstoreErrors.ValueError( + gstoreErrors.errorCodes.ERR_PROP_VALUE, + null, + { type: 'prop.validator', messageParams: [value, prop], property: prop } + ); + } + + return null; +}; + +const validate = (entityData, schema, entityKind) => { + const errors = []; + + let prop; + let skip; + let schemaHasProperty; + let propertyType; + let propertyValue; + let isEmpty; + let isRequired; + let error; + + const props = Object.keys(entityData); + const totalProps = Object.keys(entityData).length; + + for (let i = 0; i < totalProps; i += 1) { + prop = props[i]; + skip = false; + error = null; + schemaHasProperty = {}.hasOwnProperty.call(schema.paths, prop); + propertyType = schemaHasProperty ? schema.paths[prop].type : null; + propertyValue = entityData[prop]; + isEmpty = isValueEmpty(propertyValue); + + if (typeof propertyValue === 'string') { + propertyValue = propertyValue.trim(); + } + + if ({}.hasOwnProperty.call(schema.virtuals, prop)) { + // Virtual, remove it and skip the rest + delete entityData[prop]; + skip = true; + } else if (!schemaHasProperty && schema.options.explicitOnly === false) { + // No more validation, key does not exist but it is allowed + skip = true; + } + + if (!skip) { + // ... is allowed + if (!schemaHasProperty) { + error = new gstoreErrors.ValidationError( + gstoreErrors.errorCodes.ERR_PROP_NOT_ALLOWED, + null, + { + type: 'prop.not.allowed', + messageParams: [prop, entityKind], + property: prop, + } + ); + errors.push(errorToObject(error)); + // return; + } + + // ...is required + isRequired = schemaHasProperty && + {}.hasOwnProperty.call(schema.paths[prop], 'required') && + schema.paths[prop].required === true; + + if (isRequired && isEmpty) { + error = new gstoreErrors.ValueError( + gstoreErrors.errorCodes.ERR_PROP_REQUIRED, + null, + { + type: 'prop.required', + messageParams: [prop], + property: prop, + } + ); + errors.push(errorToObject(error)); + } + + // ... valid prop Type + if (schemaHasProperty && !isEmpty && {}.hasOwnProperty.call(schema.paths[prop], 'type')) { + error = validatePropType(propertyValue, propertyType, prop); + + if (error) { + errors.push(errorToObject(error)); + } + } + + // ... valid prop Value + if (error === null && schemaHasProperty && !isEmpty && {}.hasOwnProperty.call(schema.paths[prop], 'validate')) { + error = validatePropValue(propertyValue, schema.paths[prop].validate, propertyType, prop); + if (error) { + errors.push(errorToObject(error)); + } + } + + // ... value in range + if (schemaHasProperty && !isEmpty && + {}.hasOwnProperty.call(schema.paths[prop], 'values') && + schema.paths[prop].values.indexOf(propertyValue) < 0) { + error = new gstoreErrors.ValueError( + gstoreErrors.errorCodes.ERR_PROP_IN_RANGE, + null, + { type: 'value.range', messageParams: [prop, schema.paths[prop].values], property: prop } + ); + + if (error) { + errors.push(errorToObject(error)); + } + } + } + } + + let validationError = null; + + if (Object.keys(errors).length > 0) { + validationError = new gstoreErrors.ValidationError( + gstoreErrors.errorCodes.ERR_VALIDATION, + null, + { errors, messageParams: [entityKind] } + ); + } + + return { + error: validationError, + value: entityData, + then: (resolve, reject) => { + if (validationError) { + return Promise.resolve(reject(validationError)); + } + + return Promise.resolve(resolve(entityData)); + }, + catch: (reject) => { + if (validationError) { + return Promise.resolve(reject(validationError)); + } + return undefined; + }, + }; +}; + +module.exports = { + validate, +}; diff --git a/lib/model.js b/lib/model.js old mode 100644 new mode 100755 index 2ec9103..2062223 --- a/lib/model.js +++ b/lib/model.js @@ -4,19 +4,16 @@ /* * Module dependencies. */ -const moment = require('moment'); const is = require('is'); const arrify = require('arrify'); const extend = require('extend'); const hooks = require('promised-hooks'); const ds = require('@google-cloud/datastore')(); -const validator = require('validator'); const Entity = require('./entity'); const datastoreSerializer = require('./serializer').Datastore; const utils = require('./utils'); -const queryHelpers = require('./helper').QueryHelpers; -const GstoreError = require('./error.js'); +const { queryHelpers, validation } = require('./helpers'); class Model extends Entity { static compile(kind, schema, gstore) { @@ -840,9 +837,9 @@ class Model extends Entity { options = args.length > 1 && args[1] !== null ? args[1] : {}; extend(defaultOptions, options); - const errors = validateEntityData(); - if (errors) { - return cb(errors); + const error = validateEntityData(); + if (error) { + return cb(error); } validateMethod(defaultOptions.method); @@ -877,12 +874,8 @@ class Model extends Entity { function validateEntityData() { if (_this.schema.options.validateBeforeSave) { - const validate = _this.validate(); - - if (!validate.success) { - delete validate.success; - return validate.errors[Object.keys(validate.errors)[0]]; - } + const { error } = _this.validate(); + return error; } return undefined; @@ -957,209 +950,9 @@ class Model extends Entity { } validate() { - const errors = {}; - const self = this; - const { schema } = this; - - let skip; - let schemaHasProperty; - let propertyType; - let propertyValue; - let isValueEmpty; - let isRequired; - - Object.keys(this.entityData).forEach((k) => { - skip = false; - schemaHasProperty = {}.hasOwnProperty.call(schema.paths, k); - propertyType = schemaHasProperty ? schema.paths[k].type : null; - propertyValue = self.entityData[k]; - isValueEmpty = valueIsEmpty(propertyValue); - - if (typeof propertyValue === 'string') { - propertyValue = propertyValue.trim(); - } - - if ({}.hasOwnProperty.call(schema.virtuals, k)) { - // Virtual, remove it and skip the rest - delete self.entityData[k]; - skip = true; - } - - // Properties dict - if (!schemaHasProperty && schema.options.explicitOnly === false) { - // No more validation, key does not exist but it is allowed - skip = true; - } - - if (!skip && !schemaHasProperty) { - errors.properties = new Error(`Property not allowed { ${k} } for ${this.entityKind} Entity`); - } - - // Properties type - if (!skip && schemaHasProperty && !isValueEmpty && {}.hasOwnProperty.call(schema.paths[k], 'type')) { - let typeValid = true; - - if (propertyType === 'datetime') { - // Validate datetime "format" - const error = validateDateTime(propertyValue, k); - if (error !== null) { - errors.datetime = error; - } - } else { - if (propertyType === 'array') { - // Array - typeValid = is.array(propertyValue); - } else if (propertyType === 'int') { - // Integer - const isIntInstance = propertyValue.constructor.name === 'Int'; - if (isIntInstance) { - typeValid = !isNaN(parseInt(propertyValue.value, 10)); - } else { - typeValid = isInt(propertyValue); - } - } else if (propertyType === 'double') { - // Double - const isIntInstance = propertyValue.constructor.name === 'Double'; - - if (isIntInstance) { - typeValid = isFloat(parseFloat(propertyValue.value, 10)) || - isInt(parseFloat(propertyValue.value, 10)); - } else { - typeValid = isFloat(propertyValue) || isInt(propertyValue); - } - } else if (propertyType === 'buffer') { - // Double - typeValid = propertyValue instanceof Buffer; - } else if (propertyType === 'geoPoint') { - // GeoPoint - if (is.object(propertyValue) && Object.keys(propertyValue).length === 2 - && {}.hasOwnProperty.call(propertyValue, 'longitude') - && {}.hasOwnProperty.call(propertyValue, 'latitude')) { - typeValid = validateLngLat(propertyValue); - } else { - typeValid = propertyValue.constructor.name === 'GeoPoint'; - } - } else { - // Other - /* eslint valid-typeof: "off" */ - typeValid = typeof propertyValue === propertyType; - } - - if (!typeValid) { - errors[k] = new GstoreError.ValidationError({ - message: `Data type error for ' + ${k}`, - }); - } - } - } - - // ----------------- - // Value Validation - // ----------------- - - // ...Required - isRequired = schemaHasProperty && - {}.hasOwnProperty.call(schema.paths[k], 'required') && - schema.paths[k].required === true; - - if (!skip && isRequired && isValueEmpty) { - errors[k] = new GstoreError.ValidatorError({ - errorName: 'Required', - message: `Property { ${k} } is required`, - }); - } - - // ...Wrong format - if ( - !skip && schemaHasProperty && !isValueEmpty && - {}.hasOwnProperty.call(schema.paths[k], 'validate') - ) { - /** - * By default, the args only contain the value being validated - * and the string, matching the simple case. - */ - let validationArgs = [propertyValue]; - let validationRule = schema.paths[k].validate; - let validationFn = null; - - /** - * If the validate property is an object, then it's assumed that - * it contains the 'rule' property, which will be the new - * validationRule's value. - * If the 'args' prop was passed then we concat them to - * 'validationArgs'. - */ - if (typeof validationRule === 'object') { - validationRule = schema.paths[k].validate.rule; - - if (typeof validationRule === 'function') { - validationFn = validationRule; - validationArgs.push(validator); - } else { - validationFn = validator[validationRule]; - } + const { entityData, schema, entityKind } = this; - validationArgs = validationArgs.concat(schema.paths[k].validate.args || []); - } else { - validationFn = validator[validationRule]; - } - - if (!validationFn.apply(validator, validationArgs)) { - errors[k] = new GstoreError.ValidatorError({ - message: `Wrong format for property { ${k} }`, - }); - } - } - - // ...Preset values - if (!skip && schemaHasProperty && !isValueEmpty && - {}.hasOwnProperty.call(schema.paths[k], 'values') && - schema.paths[k].values.indexOf(propertyValue) < 0) { - errors[k] = new Error(`Value not allowed for ${k}. It must be one of: ${schema.paths[k].values}`); - } - }); - - const payload = Object.keys(errors).length > 0 ? { success: false, errors } : { success: true }; - - return payload; - - // ----------- - - function validateDateTime(value, k) { - if (value.constructor.name !== 'Date' && - (typeof value !== 'string' || - !value.match(/\d{4}-\d{2}-\d{2}([ ,T])?(\d{2}:\d{2}:\d{2})?(\.\d{1,3})?/) || - !moment(value).isValid())) { - return { - error: 'Wrong format', - message: `Wrong date format for ${k}`, - }; - } - return null; - } - - function isInt(n) { - return Number(n) === n && n % 1 === 0; - } - - function isFloat(n) { - return Number(n) === n && n % 1 !== 0; - } - - function valueIsEmpty(v) { - return v === null || - v === undefined || - (typeof v === 'string' && v.trim().length === 0); - } - - function validateLngLat(data) { - const validLatitude = (isInt(data.latitude) || isFloat(data.latitude)) - && data.latitude >= -90 && data.latitude <= 90; - const validLongitude = (isInt(data.longitude) || isFloat(data.longitude)) - && data.longitude >= -180 && data.longitude <= 180; - - return validLatitude && validLongitude; - } + return validation.validate(entityData, schema, entityKind); } } diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 50d4cd6..abd8dfe --- a/package.json +++ b/package.json @@ -49,9 +49,10 @@ "arrify": "^1.0.1", "extend": "^3.0.0", "is": "^3.2.1", + "joi": "^11.3.4", "moment": "^2.18.1", "promised-hooks": "^1.1.0", - "validator": "^8.2.0" + "validator": "^9.0.0" }, "devDependencies": { "babel-cli": "^6.9.0", @@ -63,10 +64,9 @@ "eslint-plugin-import": "^2.7.0", "eslint-plugin-mocha": "^4.11.0", "istanbul": "^0.4.3", - "mocha": "^3.5.3", + "mocha": "^4.0.1", "mocha-lcov-reporter": "^1.2.0", "nconf": "^0.8.4", - "sinon": "^4.0.0", - "sinon-as-promised": "^4.0.3" + "sinon": "^4.0.0" } } diff --git a/test/entity-test.js b/test/entity-test.js old mode 100644 new mode 100755 diff --git a/test/error-test.js b/test/error-test.js old mode 100644 new mode 100755 index d0fa969..1709cef --- a/test/error-test.js +++ b/test/error-test.js @@ -2,94 +2,167 @@ 'use strict'; const chai = require('chai'); +const util = require('util'); const gstore = require('../'); -const { GstoreError } = require('../lib/error'); +const errors = require('../lib/errors'); const Model = require('../lib/model'); const Schema = require('../lib/schema'); -const { ValidationError, ValidatorError } = GstoreError; +const { GstoreError, TypeError, ValueError, message } = errors; const { expect, assert } = chai; -describe('Datastools Errors', () => { - it('should extend Error', () => { - expect(GstoreError.prototype.name).equal('Error'); +const doSomethingBad = (code) => { + code = code || 'ERR_GENERIC'; + throw new GstoreError(code); +}; + +describe('message()', () => { + it('should return string passed', () => { + expect(message('My message')).equal('My message'); }); - it('should set properties in constructor', () => { - const error = new GstoreError('Something went wrong'); + it('should return string passed with arguments', () => { + expect(message('Hello %s %s', 'John', 'Snow')).equal('Hello John Snow'); + expect(message('Age: %d years old', 27)).equal('Age: 27 years old'); + }); +}); - expect(error.message).equal('Something went wrong'); - expect(error.name).equal('GstoreError'); +describe('GstoreError', () => { + it('should create a custom Error', () => { + try { + doSomethingBad(); + } catch (e) { + expect(e.name).equal('GstoreError'); + expect(e instanceof GstoreError); + expect(e instanceof Error); + + // The error should be recognized by Node.js' util#isError + expect(util.isError(e)).equal(true); + assert.isDefined(e.stack); + expect(e.toString()).equal('GstoreError: An error occured'); + + // The stack should start with the default error message formatting + expect(e.stack.split('\n')[0]).equal('GstoreError: An error occured'); + + // The first stack frame should be the function where the error was thrown. + expect(e.stack.split('\n')[1].indexOf('doSomethingBad')).equal(7); + + // The error code should be set + expect(e.code).equal('ERR_GENERIC'); + } + }); + + it('should fall back to generic if no message passed', () => { + const func = () => { + throw new GstoreError('UNKNOWN_CODE'); + }; + + try { + func(); + } catch (e) { + expect(e.toString()).equal('GstoreError: An error occured'); + } }); it('should have static errors', () => { + assert.isDefined(GstoreError.TypeError); + assert.isDefined(GstoreError.ValueError); assert.isDefined(GstoreError.ValidationError); - assert.isDefined(GstoreError.ValidatorError); }); }); -describe('ValidationError', () => { - it('should extend Error', () => { - expect(ValidationError.prototype.name).equal('Error'); - }); - - it('should return error data passed in param', () => { - const errorData = { - code: 400, - message: 'Something went really bad', +describe('TypeError', () => { + it('should create a TypeError', () => { + const throwTypeError = (code) => { + code = code || 'ERR_GENERIC'; + throw new TypeError(code); }; - const error = new ValidationError(errorData); - expect(error.message).equal(errorData); - }); + try { + throwTypeError(); + } catch (e) { + expect(e.name).equal('TypeError'); + expect(e instanceof TypeError); + expect(e instanceof GstoreError); + expect(e instanceof Error); - it('should return "{entityKind} validation failed" if called with entity instance', () => { - const entityKind = 'Blog'; - const schema = new Schema({}); - const ModelInstance = Model.compile(entityKind, schema, gstore); - const model = new ModelInstance({}); - const error = new ValidationError(model); + // The error should be recognized by Node.js' util#isError + // expect(util.isError(e)).equal(true); + // assert.isDefined(e.stack); + // expect(e.toString()).equal('GstoreError: An error occured'); - expect(error.message).equal(`${entityKind} validation failed`); - }); + // // The stack should start with the default error message formatting + // expect(e.stack.split('\n')[0]).equal('GstoreError: An error occured'); - it('should return "Validation failed" if called without param', () => { - const error = new ValidationError(); + // // The first stack frame should be the function where the error was thrown. + // expect(e.stack.split('\n')[1].indexOf('doSomethingBad')).equal(7); - expect(error.message).equal('Validation failed'); + // // The error code should be set + // expect(e.code).equal('ERR_GENERIC'); + } }); -}); -describe('ValidatorError', () => { - it('should extend Error', () => { - expect(ValidatorError.prototype.name).equal('Error'); - }); + // it('should extend Error', () => { + // expect(ValidationError.prototype.name).equal('Error'); + // }); - it('should return error data passed in param', () => { - const errorData = { - code: 400, - message: 'Something went really bad', - }; - const error = new ValidatorError(errorData); + // it('should return error data passed in param', () => { + // const errorData = { + // code: 400, + // message: 'Something went really bad', + // }; + // const error = new ValidationError(errorData); - expect(error.message.errorName).equal('Wrong format'); - expect(error.message.message).equal(errorData.message); - }); + // expect(error.message).equal(errorData); + // }); - it('should set error name passed in param', () => { - const errorData = { - code: 400, - errorName: 'Required', - message: 'Something went really bad', - }; - const error = new ValidatorError(errorData); + // it('should return "{entityKind} validation failed" if called with entity instance', () => { + // const entityKind = 'Blog'; + // const schema = new Schema({}); + // const ModelInstance = Model.compile(entityKind, schema, gstore); + // const model = new ModelInstance({}); + // const error = new ValidationError(model); - expect(error.message.errorName).equal(errorData.errorName); - }); + // expect(error.message).equal(`${entityKind} validation failed`); + // }); - it('should return "Validation failed" if called without param', () => { - const error = new ValidatorError(); + // it('should return "Validation failed" if called without param', () => { + // const error = new ValidationError(); - expect(error.message).equal('Value validation failed'); - }); + // expect(error.message).equal('Validation failed'); + // }); +}); + +describe('ValidatorError', () => { + // it('should extend Error', () => { + // expect(ValidatorError.prototype.name).equal('Error'); + // }); + + // it('should return error data passed in param', () => { + // const errorData = { + // code: 400, + // message: 'Something went really bad', + // }; + // const error = new ValidatorError(errorData); + + // expect(error.message.errorName).equal('Wrong format'); + // expect(error.message.message).equal(errorData.message); + // }); + + // it('should set error name passed in param', () => { + // const errorData = { + // code: 400, + // errorName: 'Required', + // message: 'Something went really bad', + // }; + // const error = new ValidatorError(errorData); + + // expect(error.message.errorName).equal(errorData.errorName); + // }); + + // it('should return "Validation failed" if called without param', () => { + // const error = new ValidatorError(); + + // expect(error.message).equal('Value validation failed'); + // }); }); diff --git a/test/helpers/queryhelpers-test.js b/test/helpers/queryhelpers-test.js index 881eb32..993c337 100644 --- a/test/helpers/queryhelpers-test.js +++ b/test/helpers/queryhelpers-test.js @@ -3,7 +3,7 @@ const chai = require('chai'); const sinon = require('sinon'); const ds = require('@google-cloud/datastore')(); -const queryHelpers = require('../../lib/helper').QueryHelpers; +const { queryHelpers } = require('../../lib/helpers'); const { expect } = chai; diff --git a/test/helpers/validation-test.js b/test/helpers/validation-test.js new file mode 100755 index 0000000..e03d476 --- /dev/null +++ b/test/helpers/validation-test.js @@ -0,0 +1,401 @@ +'use strict'; + +const chai = require('chai'); +const sinon = require('sinon'); +const is = require('is'); +const Joi = require('joi'); + +const gstore = require('../../'); +const { Schema } = require('../../lib'); +const gstoreErrors = require('../../lib/errors'); +const { validation } = require('../../lib/helpers'); + +const ds = require('../mocks/datastore')({ + namespace: 'com.mydomain', +}); + +const { expect, assert } = chai; +const { errorCodes } = gstoreErrors; + +const customValidationFunction = (obj, validator, min, max) => { + if ('embeddedEntity' in obj) { + const { value } = obj.embeddedEntity; + return validator.isNumeric(value.toString()) && (value >= min) && (value <= max); + } + + return false; +}; + +describe('Validation', () => { + let schema; + + const validate = entityData => validation.validate(entityData, schema, 'MyEntityKind'); + + beforeEach(() => { + schema = new Schema({ + name: { type: 'string' }, + lastname: { type: 'string' }, + age: { type: 'int' }, + birthday: { type: 'datetime' }, + street: {}, + website: { validate: 'isURL' }, + email: { validate: 'isEmail' }, + ip: { validate: { rule: 'isIP', args: [4] } }, + ip2: { validate: { rule: 'isIP' } }, // no args passed + modified: { type: 'boolean' }, + tags: { type: 'array' }, + prefs: { type: 'object' }, + price: { type: 'double' }, + icon: { type: 'buffer' }, + location: { type: 'geoPoint' }, + color: { validate: 'isHexColor' }, + type: { values: ['image', 'video'] }, + customFieldWithEmbeddedEntity: { + type: 'object', + validate: { + rule: customValidationFunction, + args: [4, 10], + }, + }, + }); + + schema.virtual('fullname').get(() => { }); + }); + + it('should return an object with an "error" and "value" properties', () => { + const entityData = { name: 'John' }; + + const { error, value } = validate(entityData); + + assert.isDefined(error); + expect(value).equal(entityData); + }); + + it('should return a Promise and resolve with the entityData', () => { + const entityData = { name: 'John' }; + + return validate(entityData).then((value) => { + expect(value).equal(entityData); + return Promise.resolve('test'); + }) + .catch(() => {}) + .then((response) => { + expect(response).equal('test'); + }); + }); + + it('should return a Promise and reject with the error', () => { + const entityData = { name: 123 }; + + return validate(entityData).then(() => { + }, (error) => { + expect(error.name).equal('ValidationError'); + expect(error.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + }); + }); + + it('should return a Promise catch with the error', () => { + const entityData = { name: 123 }; + + return validate(entityData).catch((error) => { + expect(error.name).equal('ValidationError'); + expect(error.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + return Promise.resolve('test'); + }).then((response) => { + // Just to make sure we can chain Promises + expect(response).equal('test'); + }); + }); + + it('properties passed ok', () => { + const { error } = validate({ name: 'John', lastname: 'Snow' }); + + expect(error).equal(null); + }); + + it('properties passed ko', () => { + const { error } = validate({ unknown: 123 }); + + expect(error.errors[0].code).equal(gstoreErrors.errorCodes.ERR_PROP_NOT_ALLOWED); + }); + + it('should remove virtuals before validating', () => { + const { error } = validate({ fullname: 'John Snow' }); + + expect(error).equal(null); + }); + + it('accept unkwown properties when "explicityOnly" set to false', () => { + schema = new Schema({ name: { type: 'string' } }, { explicitOnly: false }); + + const { error } = validate({ unknown: 123 }); + + expect(error).equal(null); + }); + + it('required property', () => { + schema = new Schema({ + name: { type: 'string' }, + email: { type: 'string', required: true }, + }); + + const { error } = validate({ name: 'John Snow', email: '' }); + const { error: error2 } = validate({ name: 'John Snow', email: ' ' }); + const { error: error3 } = validate({ name: 'John Snow', email: null }); + + expect(error.errors[0].code).equal(gstoreErrors.errorCodes.ERR_PROP_REQUIRED); + expect(error2.errors[0].code).equal(gstoreErrors.errorCodes.ERR_PROP_REQUIRED); + expect(error3.errors[0].code).equal(gstoreErrors.errorCodes.ERR_PROP_REQUIRED); + }); + + it('don\'t validate empty value', () => { + const { error } = validate({ email: undefined }); + const { error: error2 } = validate({ email: null }); + const { error: error3 } = validate({ email: '' }); + + expect(error).equal(null); + expect(error2).equal(null); + expect(error3).equal(null); + }); + + it('no type validation', () => { + const { error } = validate({ street: 123 }); + const { error: error2 } = validate({ street: '123' }); + const { error: error3 } = validate({ street: true }); + + expect(error).equal(null); + expect(error2).equal(null); + expect(error3).equal(null); + }); + + it('--> string', () => { + const { error } = validate({ name: 123 }); + + expect(error).not.equal(null); + expect(error.errors[0].code).equal(gstoreErrors.errorCodes.ERR_PROP_TYPE); + }); + + it('--> number', () => { + const { error } = validate({ age: 'string' }); + + expect(error.errors[0].code).equal(gstoreErrors.errorCodes.ERR_PROP_TYPE); + }); + + it('--> int', () => { + const { error } = validate({ age: ds.int('7') }); + const { error: error2 } = validate({ age: ds.int(7) }); + const { error: error3 } = validate({ age: 7 }); + const { error: error4 } = validate({ age: ds.int('string') }); + const { error: error5 } = validate({ age: 'string' }); + const { error: error6 } = validate({ age: '7' }); + + expect(error).equal(null); + expect(error2).equal(null); + expect(error3).equal(null); + expect(error4.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + expect(error5.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + expect(error6.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + }); + + it('--> double', () => { + const { error } = validate({ price: ds.double('1.2') }); + const { error: error2 } = validate({ price: ds.double(7.0) }); + const { error: error3 } = validate({ price: 7 }); + const { error: error4 } = validate({ price: 7.59 }); + const { error: error5 } = validate({ price: ds.double('str') }); + const { error: error6 } = validate({ price: 'string' }); + const { error: error7 } = validate({ price: '7' }); + + expect(error).equal(null); + expect(error2).equal(null); + expect(error3).equal(null); + expect(error4).equal(null); + expect(error5.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + expect(error6.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + expect(error7.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + }); + + it('--> buffer', () => { + const { error } = validate({ icon: Buffer.from('\uD83C\uDF69') }); + const { error: error2 } = validate({ icon: 'string' }); + + expect(error).equal(null); + expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + }); + + it('--> boolean', () => { + const { error } = validate({ modified: true }); + const { error: error2 } = validate({ modified: 'string' }); + + expect(error).equal(null); + expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + }); + + it('--> object', () => { + const { error } = validate({ prefs: { check: true } }); + const { error: error2 } = validate({ prefs: 'string' }); + + expect(error).equal(null); + expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + }); + + it('--> geoPoint', () => { + // datastore geoPoint + const { error } = validate({ + location: ds.geoPoint({ + latitude: 40.6894, + longitude: -74.0447, + }), + }); + + // valid geo object + const { error: error2 } = validate({ + location: { + latitude: 40.68942342541, + longitude: -74.044743654572, + }, + }); + + const { error: error3 } = validate({ location: 'string' }); + const { error: error4 } = validate({ location: true }); + const { error: error5 } = validate({ location: { longitude: 999, latitude: 'abc' } }); + const { error: error6 } = validate({ location: { longitude: 40.6895 } }); + const { error: error7 } = validate({ location: { longitude: '120.123', latitude: '40.12345678' } }); + + expect(error).equal(null); + expect(error2).equal(null); + expect(error3.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + expect(error4.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + expect(error5.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + expect(error6.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + expect(error7.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + }); + + it('--> array ok', () => { + const { error } = validate({ tags: [] }); + + expect(error).equal(null); + }); + + it('--> array ko', () => { + const { error } = validate({ tags: {} }); + const { error: error2 } = validate({ tags: 'string' }); + const { error: error3 } = validate({ tags: 123 }); + + expect(error.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + expect(error3.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + }); + + it('--> date ok', () => { + const { error } = validate({ birthday: '2015-01-01' }); + const { error: error2 } = validate({ birthday: new Date() }); + + expect(error).equal(null); + expect(error2).equal(null); + }); + + it('--> date ko', () => { + const { error } = validate({ birthday: '01-2015-01' }); + const { error: error2 } = validate({ birthday: '01-01-2015' }); + const { error: error3 } = validate({ birthday: '2015/01/01' }); + const { error: error4 } = validate({ birthday: '01/01/2015' }); + const { error: error5 } = validate({ birthday: 12345 }); // No number allowed + const { error: error6 } = validate({ birthday: 'string' }); + + expect(error.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + expect(error3.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + expect(error4.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + expect(error5.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + expect(error6.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + }); + + it('--> isURL ok', () => { + const { error } = validate({ website: 'http://google.com' }); + const { error: error2 } = validate({ website: 'google.com' }); + + expect(error).equal(null); + expect(error2).equal(null); + }); + + it('--> isURL ko', () => { + const { error } = validate({ website: 'domain.k' }); + const { error: error2 } = validate({ website: 123 }); + + expect(error.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); + expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); + }); + + it('--> isEmail ok', () => { + const { error } = validate({ email: 'john@snow.com' }); + + expect(error).equal(null); + }); + + it('--> isEmail ko', () => { + const { error } = validate({ email: 'john@snow' }); + const { error: error2 } = validate({ email: 'john@snow.' }); + const { error: error3 } = validate({ email: 'john@snow.k' }); + const { error: error4 } = validate({ email: 'johnsnow.com' }); + + expect(error.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); + expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); + expect(error3.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); + expect(error4.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); + }); + + it('--> is IP ok', () => { + const { error } = validate({ ip: '127.0.0.1' }); + const { error: error2 } = validate({ ip2: '127.0.0.1' }); + + expect(error).equal(null); + expect(error2).equal(null); + }); + + it('--> is IP ko', () => { + const { error } = validate({ ip: 'fe80::1c2e:f014:10d8:50f5' }); + const { error: error2 } = validate({ ip: '1.1.1' }); + + expect(error.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); + expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); + }); + + it('--> is HexColor', () => { + const { error } = validate({ color: '#fff' }); + const { error: error2 } = validate({ color: 'white' }); + + expect(error).equal(null); + expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); + }); + + it('--> is customFieldWithEmbeddedEntity ok', () => { + const { error } = validate({ + customFieldWithEmbeddedEntity: { + embeddedEntity: { + value: 6, + }, + }, + }); + + expect(error).equal(null); + }); + + it('--> is customFieldWithEmbeddedEntity ko', () => { + const { error } = validate({ + customFieldWithEmbeddedEntity: { + embeddedEntity: { + value: 2, + }, + }, + }); + + expect(error.errors[0].code).equal(errorCodes.ERR_PROP_VALUE); + }); + + it('--> only accept value in range of values', () => { + const { error } = validate({ type: 'other' }); + + expect(error.errors[0].code).equal(errorCodes.ERR_PROP_IN_RANGE); + }); +}); diff --git a/test/model-test.js b/test/model-test.js old mode 100644 new mode 100755 index 6704b96..3282768 --- a/test/model-test.js +++ b/test/model-test.js @@ -5,6 +5,7 @@ const chai = require('chai'); const sinon = require('sinon'); const is = require('is'); +const gstoreErrors = require('../lib/errors'); const { expect, assert } = chai; const ds = require('./mocks/datastore')({ @@ -18,16 +19,7 @@ const gstore = require('../'); const Entity = require('../lib/entity'); const { Schema } = require('../lib'); const datastoreSerializer = require('../lib/serializer').Datastore; -const queryHelpers = require('../lib/helper').QueryHelpers; - -function customValidationFunction(obj, validator, min, max) { - if ('embeddedEntity' in obj) { - const { value } = obj.embeddedEntity; - return validator.isNumeric(value.toString()) && (value >= min) && (value <= max); - } - - return false; -} +const { queryHelpers, validation } = require('../lib/helpers'); let Model = require('../lib/model'); @@ -62,15 +54,6 @@ describe('Model', () => { price: { type: 'double', write: false }, icon: { type: 'buffer' }, location: { type: 'geoPoint' }, - color: { validate: 'isHexColor' }, - type: { values: ['image', 'video'] }, - customFieldWithEmbeddedEntity: { - type: 'object', - validate: { - rule: customValidationFunction, - args: [4, 10], - }, - }, }); schema.virtual('fullname').get(() => { }); @@ -1406,7 +1389,7 @@ describe('Model', () => { return model.save().catch((err) => { assert.isDefined(err); expect(ds.save.called).equal(false); - expect(err.message.indexOf('Property not allowed')).equal(0); + expect(err.code).equal(gstoreErrors.errorCodes.ERR_VALIDATION); }); }); @@ -1659,404 +1642,25 @@ describe('Model', () => { }); describe('validate()', () => { - it('properties passed ok', () => { - const model = new ModelInstance({ name: 'John', lastname: 'Snow' }); - - const valid = model.validate(); - expect(valid.success).equal(true); - }); - - it('properties passed ko', () => { - const model = new ModelInstance({ unknown: 123 }); - - const valid = model.validate(); - - expect(valid.success).equal(false); - }); - - it('should remove virtuals', () => { - const model = new ModelInstance({ fullname: 'John Snow' }); - - const valid = model.validate(); - - expect(valid.success).equal(true); - assert.isUndefined(model.entityData.fullname); - }); - - it('accept unkwown properties', () => { - schema = new Schema({ name: { type: 'string' } }, { explicitOnly: false }); - ModelInstance = Model.compile('Blog', schema, gstore); - const model = new ModelInstance({ unknown: 123 }); - - const valid = model.validate(); - - expect(valid.success).equal(true); - }); - - it('required property', () => { - schema = new Schema({ - name: { type: 'string' }, - email: { type: 'string', required: true }, - }); - - ModelInstance = Model.compile('Blog', schema, gstore); - - const model = new ModelInstance({ name: 'John Snow' }); - const model2 = new ModelInstance({ name: 'John Snow', email: '' }); - const model3 = new ModelInstance({ name: 'John Snow', email: ' ' }); - const model4 = new ModelInstance({ name: 'John Snow', email: null }); - - const valid = model.validate(); - const valid2 = model2.validate(); - const valid3 = model3.validate(); - const valid4 = model4.validate(); - - expect(valid.success).equal(false); - expect(valid2.success).equal(false); - expect(valid3.success).equal(false); - expect(valid4.success).equal(false); - }); - - it('don\'t validate empty value', () => { - const model = new ModelInstance({ email: undefined }); - const model2 = new ModelInstance({ email: null }); - const model3 = new ModelInstance({ email: '' }); - - const valid = model.validate(); - const valid2 = model2.validate(); - const valid3 = model3.validate(); - - expect(valid.success).equal(true); - expect(valid2.success).equal(true); - expect(valid3.success).equal(true); - }); - - it('no type validation', () => { - const model = new ModelInstance({ street: 123 }); - const model2 = new ModelInstance({ street: '123' }); - const model3 = new ModelInstance({ street: true }); - - const valid = model.validate(); - const valid2 = model2.validate(); - const valid3 = model3.validate(); - - expect(valid.success).equal(true); - expect(valid2.success).equal(true); - expect(valid3.success).equal(true); - }); - - it('--> string', () => { - const model = new ModelInstance({ name: 123 }); - - const valid = model.validate(); - - expect(valid.success).equal(false); - }); - - it('--> number', () => { - const model = new ModelInstance({ age: 'string' }); - - const valid = model.validate(); - - expect(valid.success).equal(false); - }); - - it('--> int', () => { - const model = new ModelInstance({ age: ds.int('str') }); - const valid = model.validate(); - - const model2 = new ModelInstance({ age: ds.int('7') }); - const valid2 = model2.validate(); - - const model3 = new ModelInstance({ age: ds.int(7) }); - const valid3 = model3.validate(); - - const model4 = new ModelInstance({ age: 'string' }); - const valid4 = model4.validate(); - - const model5 = new ModelInstance({ age: '7' }); - const valid5 = model5.validate(); - - const model6 = new ModelInstance({ age: 7 }); - const valid6 = model6.validate(); - - expect(valid.success).equal(false); - expect(valid2.success).equal(true); - expect(valid3.success).equal(true); - expect(valid4.success).equal(false); - expect(valid5.success).equal(false); - expect(valid6.success).equal(true); - }); - - it('--> double', () => { - const model = new ModelInstance({ price: ds.double('str') }); - const valid = model.validate(); - - const model2 = new ModelInstance({ price: ds.double('1.2') }); - const valid2 = model2.validate(); - - const model3 = new ModelInstance({ price: ds.double(7.0) }); - const valid3 = model3.validate(); - - const model4 = new ModelInstance({ price: 'string' }); - const valid4 = model4.validate(); - - const model5 = new ModelInstance({ price: '7' }); - const valid5 = model5.validate(); - - const model6 = new ModelInstance({ price: 7 }); - const valid6 = model6.validate(); - - const model7 = new ModelInstance({ price: 7.59 }); - const valid7 = model7.validate(); - - expect(valid.success).equal(false); - expect(valid2.success).equal(true); - expect(valid3.success).equal(true); - expect(valid4.success).equal(false); - expect(valid5.success).equal(false); - expect(valid6.success).equal(true); - expect(valid7.success).equal(true); - }); - - it('--> buffer', () => { - const model = new ModelInstance({ icon: 'string' }); - const valid = model.validate(); - - const model2 = new ModelInstance({ icon: Buffer.from('\uD83C\uDF69') }); - const valid2 = model2.validate(); - - expect(valid.success).equal(false); - expect(valid2.success).equal(true); - }); - - it('--> boolean', () => { - const model = new ModelInstance({ modified: 'string' }); - - const valid = model.validate(); - - expect(valid.success).equal(false); - }); - - it('--> object', () => { - const model = new ModelInstance({ prefs: { check: true } }); - - const valid = model.validate(); - - expect(valid.success).equal(true); - }); - - it('--> geoPoint', () => { - const model = new ModelInstance({ location: 'string' }); - const valid = model.validate(); - - // datastore geoPoint - const model2 = new ModelInstance({ - location: ds.geoPoint({ - latitude: 40.6894, - longitude: -74.0447, - }), - }); - const valid2 = model2.validate(); - - // valid geo object - const model3 = new ModelInstance({ - location: { - latitude: 40.68942342541, - longitude: -74.044743654572, - }, - }); - const valid3 = model3.validate(); - - // other tests - const model4 = new ModelInstance({ location: true }); - const valid4 = model4.validate(); - - const model5 = new ModelInstance({ location: { longitude: 999, latitude: 'abc' } }); - const valid5 = model5.validate(); - - const model6 = new ModelInstance({ location: { longitude: 40.6895 } }); - const valid6 = model6.validate(); - - const model7 = new ModelInstance({ location: { longitude: '120.123', latitude: '40.12345678' } }); - const valid7 = model7.validate(); - - expect(valid.success).equal(false); - expect(valid2.success).equal(true); - expect(valid3.success).equal(true); - expect(valid4.success).equal(false); - expect(valid5.success).equal(false); - expect(valid6.success).equal(false); - expect(valid7.success).equal(false); - }); - - it('--> array ok', () => { - const model = new ModelInstance({ tags: [] }); - - const valid = model.validate(); - - expect(valid.success).equal(true); - }); - - it('--> array ko', () => { - const model = new ModelInstance({ tags: {} }); - const model2 = new ModelInstance({ tags: 'string' }); - const model3 = new ModelInstance({ tags: 123 }); - - const valid = model.validate(); - const valid2 = model2.validate(); - const valid3 = model3.validate(); - - expect(valid.success).equal(false); - expect(valid2.success).equal(false); - expect(valid3.success).equal(false); - }); - - it('--> date ok', () => { - const model = new ModelInstance({ birthday: '2015-01-01' }); - const model2 = new ModelInstance({ birthday: new Date() }); - - const valid = model.validate(); - const valid2 = model2.validate(); - - expect(valid.success).equal(true); - expect(valid2.success).equal(true); - }); - - it('--> date ko', () => { - const model = new ModelInstance({ birthday: '01-2015-01' }); - const model2 = new ModelInstance({ birthday: '01-01-2015' }); - const model3 = new ModelInstance({ birthday: '2015/01/01' }); - const model4 = new ModelInstance({ birthday: '01/01/2015' }); - const model5 = new ModelInstance({ birthday: 12345 }); // No number allowed - const model6 = new ModelInstance({ birthday: 'string' }); - - const valid = model.validate(); - const valid2 = model2.validate(); - const valid3 = model3.validate(); - const valid4 = model4.validate(); - const valid5 = model5.validate(); - const valid6 = model6.validate(); - - expect(valid.success).equal(false); - expect(valid2.success).equal(false); - expect(valid3.success).equal(false); - expect(valid4.success).equal(false); - expect(valid5.success).equal(false); - expect(valid6.success).equal(false); - }); - - it('--> is URL ok', () => { - const model = new ModelInstance({ website: 'http://google.com' }); - const model2 = new ModelInstance({ website: 'google.com' }); - - const valid = model.validate(); - const valid2 = model2.validate(); - - expect(valid.success).equal(true); - expect(valid2.success).equal(true); - }); - - it('--> is URL ko', () => { - const model = new ModelInstance({ website: 'domain.k' }); - - const valid = model.validate(); - - expect(valid.success).equal(false); - }); - - it('--> is EMAIL ok', () => { - const model = new ModelInstance({ email: 'john@snow.com' }); - - const valid = model.validate(); - - expect(valid.success).equal(true); - }); - - it('--> is EMAIL ko', () => { - const model = new ModelInstance({ email: 'john@snow' }); - const model2 = new ModelInstance({ email: 'john@snow.' }); - const model3 = new ModelInstance({ email: 'john@snow.k' }); - const model4 = new ModelInstance({ email: 'johnsnow.com' }); - - const valid = model.validate(); - const valid2 = model2.validate(); - const valid3 = model3.validate(); - const valid4 = model4.validate(); - - expect(valid.success).equal(false); - expect(valid2.success).equal(false); - expect(valid3.success).equal(false); - expect(valid4.success).equal(false); - }); - - it('--> is IP ok', () => { - const model = new ModelInstance({ ip: '127.0.0.1' }); - const model2 = new ModelInstance({ ip2: '127.0.0.1' }); - - const valid = model.validate(); - const valid2 = model2.validate(); - - expect(valid.success).equal(true); - expect(valid2.success).equal(true); - }); - - it('--> is IP ko', () => { - const model = new ModelInstance({ ip: 'fe80::1c2e:f014:10d8:50f5' }); - const model2 = new ModelInstance({ ip: '1.1.1' }); - - const valid = model.validate(); - const valid2 = model2.validate(); - - expect(valid.success).equal(false); - expect(valid2.success).equal(false); - }); - - it('--> is HexColor', () => { - const model = new ModelInstance({ color: '#fff' }); - const model2 = new ModelInstance({ color: 'white' }); - - const valid = model.validate(); - const valid2 = model2.validate(); - - expect(valid.success).equal(true); - expect(valid2.success).equal(false); - }); - - it('--> is customFieldWithEmbeddedEntity ok', () => { - const model = new ModelInstance({ - customFieldWithEmbeddedEntity: { - embeddedEntity: { - value: 6, - }, - }, - }); - - const valid = model.validate(); - - expect(valid.success).equal(true); + beforeEach(() => { + sinon.spy(validation, 'validate'); }); - it('--> is customFieldWithEmbeddedEntity ko', () => { - const model = new ModelInstance({ - customFieldWithEmbeddedEntity: { - embeddedEntity: { - value: 2, - }, - }, - }); - - const valid = model.validate(); - - expect(valid.success).equal(false); + afterEach(() => { + validation.validate.restore(); }); - it('and only accept value in default values', () => { - const model = new ModelInstance({ type: 'other' }); + it('should call "Validation" helper passing entityData, Schema & entityKind', () => { + schema = new Schema({ name: { type: 'string' } }); + ModelInstance = gstore.model('TestValidate', schema); + const model = new ModelInstance({ name: 'John' }); - const valid = model.validate(); + const { error } = model.validate(); - expect(valid.success).equal(false); + assert.isDefined(error); + expect(validation.validate.getCall(0).args[0]).equal(model.entityData); + expect(validation.validate.getCall(0).args[1]).equal(schema); + expect(validation.validate.getCall(0).args[2]).equal(model.entityKind); }); }); }); diff --git a/yarn.lock b/yarn.lock index ee65b64..163e746 100644 --- a/yarn.lock +++ b/yarn.lock @@ -932,13 +932,7 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" -commander@2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" - dependencies: - graceful-readlink ">= 1.0.0" - -commander@^2.11.0, commander@^2.9.0: +commander@2.11.0, commander@^2.11.0, commander@^2.9.0: version "2.11.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" @@ -990,13 +984,6 @@ create-error-class@^3.0.2: dependencies: capture-stack-trace "^1.0.0" -create-thenable@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/create-thenable/-/create-thenable-1.0.2.tgz#e2031720ccc9575d8cfa31f5c146e762a80c0534" - dependencies: - object.omit "~2.0.0" - unique-concat "~0.2.2" - cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -1023,9 +1010,9 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -debug@2.6.8: - version "2.6.8" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" dependencies: ms "2.0.0" @@ -1085,11 +1072,7 @@ detect-indent@^4.0.0: dependencies: repeating "^2.0.0" -diff@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" - -diff@^3.1.0: +diff@3.3.1, diff@^3.1.0: version "3.3.1" resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" @@ -1516,14 +1499,14 @@ glob-parent@^2.0.0: dependencies: is-glob "^2.0.0" -glob@7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" +glob@7.1.2, glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.0.2" + minimatch "^3.0.4" once "^1.3.0" path-is-absolute "^1.0.0" @@ -1537,17 +1520,6 @@ glob@^5.0.15: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.3, glob@^7.0.5, glob@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - globals@^9.17.0, globals@^9.18.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" @@ -1634,13 +1606,9 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.4: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" -"graceful-readlink@>= 1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" - -growl@1.9.2: - version "1.9.2" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" +growl@1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" grpc@^1.2, grpc@^1.3.1: version "1.6.0" @@ -1982,6 +1950,12 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" +isemail@3.x.x: + version "3.0.0" + resolved "https://registry.yarnpkg.com/isemail/-/isemail-3.0.0.tgz#c89a46bb7a3361e1759f8028f9082488ecce3dff" + dependencies: + punycode "2.x.x" + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -2015,6 +1989,14 @@ istanbul@^0.4.3: which "^1.1.1" wordwrap "^1.0.0" +joi@^11.3.4: + version "11.3.4" + resolved "https://registry.yarnpkg.com/joi/-/joi-11.3.4.tgz#c25fc2598c3847865f92c51b5249b519af3e51cb" + dependencies: + hoek "4.x.x" + isemail "3.x.x" + topo "2.x.x" + js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" @@ -2067,10 +2049,6 @@ json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" -json3@3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" - json5@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" @@ -2162,41 +2140,10 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" -lodash._baseassign@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" - dependencies: - lodash._basecopy "^3.0.0" - lodash.keys "^3.0.0" - -lodash._basecopy@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" - -lodash._basecreate@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz#1bc661614daa7fc311b7d03bf16806a0213cf821" - -lodash._getnative@^3.0.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" - -lodash._isiterateecall@^3.0.0: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" - lodash.cond@^4.3.0: version "4.5.2" resolved "https://registry.yarnpkg.com/lodash.cond/-/lodash.cond-4.5.2.tgz#f471a1da486be60f6ab955d17115523dd1d255d5" -lodash.create@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lodash.create/-/lodash.create-3.1.1.tgz#d7f2849f0dbda7e04682bb8cd72ab022461debe7" - dependencies: - lodash._baseassign "^3.0.0" - lodash._basecreate "^3.0.0" - lodash._isiterateecall "^3.0.0" - lodash.flatten@^4.2.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" @@ -2205,22 +2152,6 @@ lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" -lodash.isarguments@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - -lodash.isarray@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" - -lodash.keys@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" - dependencies: - lodash._getnative "^3.0.0" - lodash.isarguments "^3.0.0" - lodash.isarray "^3.0.0" - lodash.noop@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-3.0.1.tgz#38188f4d650a3a474258439b96ec45b32617133c" @@ -2330,22 +2261,20 @@ mocha-lcov-reporter@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/mocha-lcov-reporter/-/mocha-lcov-reporter-1.3.0.tgz#469bdef4f8afc9a116056f079df6182d0afb0384" -mocha@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d" +mocha@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.0.1.tgz#0aee5a95cf69a4618820f5e51fa31717117daf1b" dependencies: browser-stdout "1.3.0" - commander "2.9.0" - debug "2.6.8" - diff "3.2.0" + commander "2.11.0" + debug "3.1.0" + diff "3.3.1" escape-string-regexp "1.0.5" - glob "7.1.1" - growl "1.9.2" + glob "7.1.2" + growl "1.10.3" he "1.1.1" - json3 "3.3.2" - lodash.create "3.1.1" mkdirp "0.5.1" - supports-color "3.1.2" + supports-color "4.4.0" modelo@^4.2.0: version "4.2.0" @@ -2367,7 +2296,7 @@ nan@^2.3.0, nan@^2.6.2: version "2.7.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" -native-promise-only@^0.8.1, native-promise-only@~0.8.1: +native-promise-only@^0.8.1: version "0.8.1" resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11" @@ -2466,7 +2395,7 @@ object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" -object.omit@^2.0.0, object.omit@~2.0.0: +object.omit@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" dependencies: @@ -2693,6 +2622,10 @@ pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" +punycode@2.x.x: + version "2.1.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.0.tgz#5f863edc89b96db09074bad7947bf09056ca4e7d" + punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" @@ -3014,13 +2947,6 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" -sinon-as-promised@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/sinon-as-promised/-/sinon-as-promised-4.0.3.tgz#c0545b1685fd813588a4ed697012487ed11d151b" - dependencies: - create-thenable "~1.0.0" - native-promise-only "~0.8.1" - sinon@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/sinon/-/sinon-4.0.0.tgz#a54a5f0237aa1dd2215e5e81c89b42b50c4fdb6b" @@ -3180,11 +3106,11 @@ stubs@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" -supports-color@3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" +supports-color@4.4.0, supports-color@^4.0.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" dependencies: - has-flag "^1.0.0" + has-flag "^2.0.0" supports-color@^2.0.0: version "2.0.0" @@ -3196,12 +3122,6 @@ supports-color@^3.1.0: dependencies: has-flag "^1.0.0" -supports-color@^4.0.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" - dependencies: - has-flag "^2.0.0" - table@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/table/-/table-4.0.1.tgz#a8116c133fac2c61f4a420ab6cdf5c4d61f0e435" @@ -3263,6 +3183,12 @@ to-fast-properties@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" +topo@2.x.x: + version "2.0.2" + resolved "https://registry.yarnpkg.com/topo/-/topo-2.0.2.tgz#cd5615752539057c0dc0491a621c3bc6fbe1d182" + dependencies: + hoek "4.x.x" + tough-cookie@~2.3.0, tough-cookie@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" @@ -3330,10 +3256,6 @@ uid-number@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" -unique-concat@~0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/unique-concat/-/unique-concat-0.2.2.tgz#9210f9bdcaacc5e1e3929490d7c019df96f18712" - user-home@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" @@ -3359,9 +3281,9 @@ validate-npm-package-license@^3.0.1: spdx-correct "~1.0.0" spdx-expression-parse "~1.0.0" -validator@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/validator/-/validator-8.2.0.tgz#3c1237290e37092355344fef78c231249dab77b9" +validator@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-9.0.0.tgz#6c1ef955e007af704adea86ae8a76da84a6c172e" verror@1.10.0: version "1.10.0" From 33bef2584b3d85230f88740b0763b12b92f354cc Mon Sep 17 00:00:00 2001 From: sebelga Date: Wed, 18 Oct 2017 06:42:01 +0200 Subject: [PATCH 2/4] feat(Joi schema): support for Joi schema definition and validation feat(joi schema): Validation done with Joi feat(feat(joi schema): Default Value when creating entity --- lib/entity.js | 50 +++++++++++++--------- lib/helpers/validation.js | 21 +++++---- lib/model.js | 8 ++-- lib/schema.js | 35 +++++++++++++++ test/entity-test.js | 36 ++++++++++++++++ test/error-test.js | 5 +-- test/helpers/validation-test.js | 76 ++++++++++++++++++++++++++++++--- test/model-test.js | 1 + test/schema-test.js | 20 ++++++++- 9 files changed, 209 insertions(+), 43 deletions(-) diff --git a/lib/entity.js b/lib/entity.js index 6eb114b..874a3ed 100644 --- a/lib/entity.js +++ b/lib/entity.js @@ -27,7 +27,7 @@ class Entity { } // create entityData from data passed - this.entityData = buildEntityData(this, data); + this.entityData = buildEntityData(this, data || {}); // wrap entity with hook methods hooks.wrap(this); @@ -161,29 +161,40 @@ function createKey(self, id, ancestors, namespace) { function buildEntityData(self, data) { const { schema } = self; - const entityData = {}; + const isJoiSchema = !is.undef(schema._joi); + + let entityData = {}; if (data) { + // If Joi schema, get its default values + if (isJoiSchema) { + const { error, value } = schema._joi.validate(data); + + if (!error) { + entityData = Object.assign({}, value); + } + } + Object.keys(data).forEach((k) => { entityData[k] = data[k]; }); } - // set default values & excludedFromIndex Object.keys(schema.paths).forEach((k) => { - const schemaProperty = schema.paths[k]; + const prop = schema.paths[k]; const hasValue = {}.hasOwnProperty.call(entityData, k); - const isOptional = {}.hasOwnProperty.call(schemaProperty, 'optional') && schemaProperty.optional !== false; - const isRequired = {}.hasOwnProperty.call(schemaProperty, 'required') && schemaProperty.required === true; + const isOptional = {}.hasOwnProperty.call(prop, 'optional') && prop.optional !== false; + const isRequired = {}.hasOwnProperty.call(prop, 'required') && prop.required === true; - if (!hasValue && !isOptional) { + // Set Default Values + if (!isJoiSchema && !hasValue && !isOptional) { let value = null; - if ({}.hasOwnProperty.call(schemaProperty, 'default')) { - if (typeof schemaProperty.default === 'function') { - value = schemaProperty.default(); + if ({}.hasOwnProperty.call(prop, 'default')) { + if (typeof prop.default === 'function') { + value = prop.default(); } else { - value = schemaProperty.default; + value = prop.default; } } @@ -193,23 +204,24 @@ function buildEntityData(self, data) { * then execute the handler for that shortcut */ value = defaultValues.__handler__(value); - } else if (value === null && {}.hasOwnProperty.call(schemaProperty, 'values') && !isRequired) { - // Default to first value of the allowed values is **not** required - [value] = schemaProperty.values; + } else if (value === null && {}.hasOwnProperty.call(prop, 'values') && !isRequired) { + // Default to first value of the allowed values if **not** required + [value] = prop.values; } entityData[k] = value; } - if (schemaProperty.excludeFromIndexes === true) { + // Set excludeFromIndexes + if (prop.excludeFromIndexes === true) { self.excludeFromIndexes.push(k); - } else if (!is.boolean(schemaProperty.excludeFromIndexes)) { + } else if (!is.boolean(prop.excludeFromIndexes)) { // For embedded entities we can set which properties are excluded from indexes // by passing a string | array of properties - const excludeFromIndexes = arrify(schemaProperty.excludeFromIndexes); + const excludeFromIndexes = arrify(prop.excludeFromIndexes); let excludedProps; - if (schemaProperty.type === 'array') { + if (prop.type === 'array') { excludedProps = excludeFromIndexes.map(excluded => `${k}[].${excluded}`); } else { excludedProps = excludeFromIndexes.map(excluded => `${k}.${excluded}`); @@ -219,7 +231,7 @@ function buildEntityData(self, data) { } }); - // add Symbol Key to data + // add Symbol Key to the entityData entityData[self.gstore.ds.KEY] = self.entityKey; return entityData; diff --git a/lib/helpers/validation.js b/lib/helpers/validation.js index f3f0469..1d0d104 100755 --- a/lib/helpers/validation.js +++ b/lib/helpers/validation.js @@ -3,7 +3,6 @@ const moment = require('moment'); const validator = require('validator'); const is = require('is'); -const Joi = require('joi'); const gstoreErrors = require('../errors'); @@ -135,10 +134,8 @@ const validatePropValue = (value, validationRule, propType, prop) => { * 'validationArgs'. */ - let _is = is; - if (typeof validationRule === 'object') { - const args = validationRule.args; + const { args } = validationRule; validationRule = validationRule.rule; if (typeof validationRule === 'function') { @@ -178,6 +175,13 @@ const validate = (entityData, schema, entityKind) => { const props = Object.keys(entityData); const totalProps = Object.keys(entityData).length; + const isJoi = !is.undef(schema._joi); + + if (isJoi) { + // We leave the validation to Joi + const joiOptions = schema.options.joi.options; + return schema._joi.validate(entityData, joiOptions); + } for (let i = 0; i < totalProps; i += 1) { prop = props[i]; @@ -245,7 +249,10 @@ const validate = (entityData, schema, entityKind) => { } // ... valid prop Value - if (error === null && schemaHasProperty && !isEmpty && {}.hasOwnProperty.call(schema.paths[prop], 'validate')) { + if (error === null && + schemaHasProperty && + !isEmpty && + {}.hasOwnProperty.call(schema.paths[prop], 'validate')) { error = validatePropValue(propertyValue, schema.paths[prop].validate, propertyType, prop); if (error) { errors.push(errorToObject(error)); @@ -262,9 +269,7 @@ const validate = (entityData, schema, entityKind) => { { type: 'value.range', messageParams: [prop, schema.paths[prop].values], property: prop } ); - if (error) { - errors.push(errorToObject(error)); - } + errors.push(errorToObject(error)); } } } diff --git a/lib/model.js b/lib/model.js index 2062223..d725d82 100755 --- a/lib/model.js +++ b/lib/model.js @@ -837,7 +837,8 @@ class Model extends Entity { options = args.length > 1 && args[1] !== null ? args[1] : {}; extend(defaultOptions, options); - const error = validateEntityData(); + const { error } = validateEntityData(); + if (error) { return cb(error); } @@ -874,11 +875,10 @@ class Model extends Entity { function validateEntityData() { if (_this.schema.options.validateBeforeSave) { - const { error } = _this.validate(); - return error; + return _this.validate(); } - return undefined; + return {}; } function validateMethod(method) { diff --git a/lib/schema.js b/lib/schema.js index 01c0da7..80d4b94 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -2,6 +2,8 @@ 'use strict'; const extend = require('extend'); +const is = require('is'); +const Joi = require('joi'); const queries = require('./queries'); const VirtualType = require('./virtualType'); @@ -81,6 +83,10 @@ class Schema { // defaultMiddleware.forEach((m) => { // self[m.kind](m.hook, m.fn); // }); + + if (options) { + this._joi = buildJoiSchema(obj, options.joi); + } } /** @@ -184,4 +190,33 @@ function defaultOptions(options) { return options; } +function buildJoiSchema(schema, joiConfig) { + if (is.undef(joiConfig)) { + return undefined; + } + + const hasExtra = is.object(joiConfig) && is.object(joiConfig.extra); + const rawJoiSchema = {}; + + Object.keys(schema).forEach((k) => { + if ({}.hasOwnProperty.call(schema[k], 'joi')) { + rawJoiSchema[k] = schema[k].joi; + } + }); + + let joiSchema = Joi.object(rawJoiSchema); + let args; + + if (hasExtra) { + Object.keys(joiConfig.extra).forEach((k) => { + if (is.function(joiSchema[k])) { + args = joiConfig.extra[k]; + joiSchema = joiSchema[k].apply(joiSchema, args); + } + }); + } + + return joiSchema; +} + module.exports = Schema; diff --git a/test/entity-test.js b/test/entity-test.js index 093dd33..dd3c634 100755 --- a/test/entity-test.js +++ b/test/entity-test.js @@ -2,6 +2,8 @@ const chai = require('chai'); const sinon = require('sinon'); +const Joi = require('joi'); + const ds = require('./mocks/datastore')(); const datastoreSerializer = require('../lib/serializer').Datastore; const { Schema } = require('../lib'); @@ -97,6 +99,40 @@ describe('Entity', () => { expect(entity.entityData.availableValuesRequired).equal(null); }); + it('should set values from Joi schema', () => { + const generateFullName = context => ( + `${context.name} ${context.lastname}` + ); + + schema = new Schema({ + name: { joi: Joi.string() }, + lastname: { joi: Joi.string().default('Jagger') }, + fullname: { joi: Joi.string().default(generateFullName, 'generated fullname') }, + }, { joi: true }); + + ModelInstance = gstore.model('EntityKind', schema); + + const user = new ModelInstance({ name: 'Mick' }); + + expect(user.entityData.lastname).equal('Jagger'); + expect(user.entityData.fullname).equal('Mick Jagger'); + }); + + it('should not set default if Joi validation does not pass', () => { + schema = new Schema({ + name: { joi: Joi.string().default('test').required() }, + lastname: { joi: Joi.string().default('Jagger') }, + age: { joi: Joi.number() }, + }, { joi: true }); + + ModelInstance = gstore.model('EntityKind', schema); + + const user = new ModelInstance({ age: 77 }); + + expect(user.age).equal(77); + assert.isUndefined(user.entityData.lastname); + }); + it('should call handler for default values in gstore.defaultValues constants', () => { sinon.spy(gstore.defaultValues, '__handler__'); schema = new Schema({ diff --git a/test/error-test.js b/test/error-test.js index 1709cef..f7729df 100755 --- a/test/error-test.js +++ b/test/error-test.js @@ -3,12 +3,9 @@ const chai = require('chai'); const util = require('util'); -const gstore = require('../'); const errors = require('../lib/errors'); -const Model = require('../lib/model'); -const Schema = require('../lib/schema'); -const { GstoreError, TypeError, ValueError, message } = errors; +const { GstoreError, TypeError, message } = errors; const { expect, assert } = chai; const doSomethingBad = (code) => { diff --git a/test/helpers/validation-test.js b/test/helpers/validation-test.js index e03d476..304a948 100755 --- a/test/helpers/validation-test.js +++ b/test/helpers/validation-test.js @@ -1,11 +1,8 @@ 'use strict'; const chai = require('chai'); -const sinon = require('sinon'); -const is = require('is'); const Joi = require('joi'); -const gstore = require('../../'); const { Schema } = require('../../lib'); const gstoreErrors = require('../../lib/errors'); const { validation } = require('../../lib/helpers'); @@ -78,10 +75,10 @@ describe('Validation', () => { expect(value).equal(entityData); return Promise.resolve('test'); }) - .catch(() => {}) - .then((response) => { - expect(response).equal('test'); - }); + .catch(() => {}) + .then((response) => { + expect(response).equal('test'); + }); }); it('should return a Promise and reject with the error', () => { @@ -399,3 +396,68 @@ describe('Validation', () => { expect(error.errors[0].code).equal(errorCodes.ERR_PROP_IN_RANGE); }); }); + +describe('Joi Validation', () => { + let schema; + + const validate = entityData => validation.validate(entityData, schema, 'MyEntityKind'); + + beforeEach(() => { + schema = new Schema({ + name: { joi: Joi.string().required() }, + color: { joi: Joi.valid('a', 'b') }, + birthyear: { joi: Joi.number().integer().min(1900).max(2013) }, + email: { joi: Joi.string().email() }, + }, { + joi: true, + }); + }); + + it('should validate with Joi', () => { + const { error } = validate({ name: 123 }); + const { error: error2 } = validate({ name: 'John', color: 'c' }); + const { error: error3 } = validate({ name: 'John', birthyear: 1877 }); + const { error: error4 } = validate({ name: 'John', email: 'abc' }); + const { error: error5 } = validate({ name: 'John', unknownProp: 'abc' }); + + expect(error).not.equal(null); + expect(error2.details[0].type).equal('any.allowOnly'); + expect(error3.details[0].type).equal('number.min'); + expect(error4.details[0].type).equal('string.email'); + expect(error5.details[0].type).equal('object.allowUnknown'); + }); + + it('should accept extra validation on top of the schema', () => { + schema = new Schema({ + name: { joi: Joi.string() }, + lastname: { joi: Joi.string() }, + }, { + joi: { + extra: { + with: ['name', 'lastname'], + unknownMethod: 'shouldBeIgnored', + }, + }, + }); + + const { error } = validate({ name: 'John' }); + + expect(error.details[0].type).equal('object.with'); + }); + + it('should accept an "option" object', () => { + schema = new Schema({ + name: { joi: Joi.string().required() }, + }, { + joi: { + options: { + allowUnknown: true, + }, + }, + }); + + const { error } = validate({ name: 'John', unknownProp: 'abc' }); + + expect(error).equal(null); + }); +}); diff --git a/test/model-test.js b/test/model-test.js index 3282768..2d25c44 100755 --- a/test/model-test.js +++ b/test/model-test.js @@ -6,6 +6,7 @@ const sinon = require('sinon'); const is = require('is'); const gstoreErrors = require('../lib/errors'); + const { expect, assert } = chai; const ds = require('./mocks/datastore')({ diff --git a/test/schema-test.js b/test/schema-test.js index c834e9e..00973bc 100644 --- a/test/schema-test.js +++ b/test/schema-test.js @@ -1,7 +1,9 @@ 'use strict'; -const gstore = require('../'); const chai = require('chai'); +const Joi = require('joi'); + +const gstore = require('../'); const { Schema } = require('../lib'); const { expect, assert } = chai; @@ -184,4 +186,20 @@ describe('Schema', () => { assert.isDefined(schema.shortcutQueries.list); expect(schema.shortcutQueries.list).to.equal(listQuerySettings); }); + + describe('Joi', () => { + let schema; + + beforeEach(() => { + schema = new Schema({ + name: { joi: Joi.string().required() }, + }, { + joi: true, + }); + }); + + it('should build Joi schema', () => { + assert.isDefined(schema._joi); + }); + }); }); From a80c673e1e0ca6df1b3388c76abcc88430a18bc5 Mon Sep 17 00:00:00 2001 From: sebelga Date: Thu, 19 Oct 2017 07:31:02 +0200 Subject: [PATCH 3/4] BREAKING CHANGE: #59 Allow multi instances of gstore bound to separate namespaces --- lib/helpers/validation.js | 4 ++- lib/index.js | 42 ++++++++++++++++++++------ lib/model.js | 45 ++++++++++++++++++---------- test/entity-test.js | 11 +++++-- test/helpers/validation-test.js | 18 ++++++++++-- test/index-test.js | 47 +++++++++++++++++++++++------- test/model-test.js | 39 +++++++++++++++++++++++-- test/schema-test.js | 5 ++-- test/serializers/datastore-test.js | 4 +-- 9 files changed, 168 insertions(+), 47 deletions(-) diff --git a/lib/helpers/validation.js b/lib/helpers/validation.js index 1d0d104..0d1159a 100755 --- a/lib/helpers/validation.js +++ b/lib/helpers/validation.js @@ -179,7 +179,9 @@ const validate = (entityData, schema, entityKind) => { if (isJoi) { // We leave the validation to Joi - const joiOptions = schema.options.joi.options; + const joiOptions = schema.options.joi.options || {}; + joiOptions.stripUnknown = {}.hasOwnProperty.call(joiOptions, 'stripUnknown') ? + joiOptions.stripUnknown : schema.options.explicitOnly !== false; return schema._joi.validate(entityData, joiOptions); } diff --git a/lib/index.js b/lib/index.js index ae77e8a..4bf3532 100644 --- a/lib/index.js +++ b/lib/index.js @@ -11,6 +11,9 @@ const datastoreSerializer = require('./serializer').Datastore; const pkg = require('../package.json'); +const gstoreInstances = {}; +const gstoreDefaultNamespace = 'com.github.gstore-node'; + class Gstore { constructor() { this.models = {}; @@ -22,14 +25,6 @@ class Gstore { this._pkgVersion = pkg.version; } - // Connect to Google Datastore instance - connect(ds) { - if (ds.constructor.name !== 'Datastore') { - throw new Error('A Datastore instances required on connect'); - } - this._ds = ds; - } - /** * Defines a Model and retreives it * @param name @@ -118,6 +113,15 @@ class Gstore { return this._ds.save.apply(this._ds, args); } + // Connect to Google Datastore instance + connect(ds) { + if (!ds.constructor || ds.constructor.name !== 'Datastore') { + throw new Error('No @google-cloud/datastore instance provided.'); + } + + this._ds = ds; + } + /** * Expose the defaultValues constants */ @@ -134,4 +138,24 @@ class Gstore { } } -module.exports = new Gstore(); +const instanceManager = (config) => { + if (!is.undef(config) && !is.object(config)) { + throw new Error('Wrong config format for gstore'); + } else if (config && !{}.hasOwnProperty.call(config, 'namespace')) { + throw new Error('Missing "namespace" property on config'); + } + + const namespace = config ? config.namespace : gstoreDefaultNamespace; + + if ({}.hasOwnProperty.call(gstoreInstances, namespace)) { + return gstoreInstances[namespace]; + } + + const gstore = new Gstore(); + + gstoreInstances[namespace] = gstore; + + return gstore; +}; + +module.exports = instanceManager; diff --git a/lib/model.js b/lib/model.js index d725d82..7cdcef3 100755 --- a/lib/model.js +++ b/lib/model.js @@ -15,6 +15,25 @@ const datastoreSerializer = require('./serializer').Datastore; const utils = require('./utils'); const { queryHelpers, validation } = require('./helpers'); +const sanitize = (data, schema) => { + if (!is.object(data)) { + return null; + } + + const newData = Object.assign({}, data); + + Object.keys(data).forEach((k) => { + if (schema.options.explicitOnly !== false && + (!{}.hasOwnProperty.call(schema.paths, k) || schema.paths[k].write === false)) { + delete newData[k]; + } else if (newData[k] === 'null') { + newData[k] = null; + } + }); + + return newData; +}; + class Model extends Entity { static compile(kind, schema, gstore) { const ModelInstance = class extends Model { }; @@ -764,20 +783,7 @@ class Model extends Entity { * @param data : userData */ static sanitize(data) { - if (!is.object(data)) { - return null; - } - - Object.keys(data).forEach((k) => { - if (this.schema.options.explicitOnly !== false && - (!{}.hasOwnProperty.call(this.schema.paths, k) || this.schema.paths[k].write === false)) { - delete data[k]; - } else if (data[k] === 'null') { - data[k] = null; - } - }); - - return data; + return sanitize(data, this.schema); } /** @@ -950,7 +956,16 @@ class Model extends Entity { } validate() { - const { entityData, schema, entityKind } = this; + const { schema, entityKind } = this; + let { entityData } = this; + + /** + * If not a Joi schema, we sanitize before + * Joi is going to do it for us + */ + if (is.undef(schema._joi)) { + entityData = sanitize(entityData, schema); + } return validation.validate(entityData, schema, entityKind); } diff --git a/test/entity-test.js b/test/entity-test.js index dd3c634..17d5e20 100755 --- a/test/entity-test.js +++ b/test/entity-test.js @@ -4,10 +4,13 @@ const chai = require('chai'); const sinon = require('sinon'); const Joi = require('joi'); -const ds = require('./mocks/datastore')(); +const ds = require('@google-cloud/datastore')({ + namespace: 'com.mydomain', + apiEndpoint: 'http://localhost:8080', +}); const datastoreSerializer = require('../lib/serializer').Datastore; -const { Schema } = require('../lib'); -const gstore = require('../lib'); +const gstore = require('../lib')(); +const { Schema } = require('../lib')(); const { expect, assert } = chai; gstore.connect(ds); @@ -133,6 +136,8 @@ describe('Entity', () => { assert.isUndefined(user.entityData.lastname); }); + // it('should sanitize') + it('should call handler for default values in gstore.defaultValues constants', () => { sinon.spy(gstore.defaultValues, '__handler__'); schema = new Schema({ diff --git a/test/helpers/validation-test.js b/test/helpers/validation-test.js index 304a948..a48b6bb 100755 --- a/test/helpers/validation-test.js +++ b/test/helpers/validation-test.js @@ -3,7 +3,6 @@ const chai = require('chai'); const Joi = require('joi'); -const { Schema } = require('../../lib'); const gstoreErrors = require('../../lib/errors'); const { validation } = require('../../lib/helpers'); @@ -11,6 +10,8 @@ const ds = require('../mocks/datastore')({ namespace: 'com.mydomain', }); +const { Schema } = require('../../lib')(); + const { expect, assert } = chai; const { errorCodes } = gstoreErrors; @@ -409,7 +410,7 @@ describe('Joi Validation', () => { birthyear: { joi: Joi.number().integer().min(1900).max(2013) }, email: { joi: Joi.string().email() }, }, { - joi: true, + joi: { options: { stripUnknown: false } }, }); }); @@ -460,4 +461,17 @@ describe('Joi Validation', () => { expect(error).equal(null); }); + + it('should set "stripUnknown" according to "explicitOnly" setting', () => { + schema = new Schema({ name: { joi: Joi.string() } }, { explicitOnly: false }); + const schema2 = new Schema({ name: { joi: Joi.string() } }); + + const { error, value } = validate({ name: 'John', unknownProp: 'abc' }); + const { error: error2, value: value2 } = validation.validate({ name: 'John', unknownProp: 'abc' }, schema2, ''); + + expect(error).equal(null); + expect(value.unknownProp).equal('abc'); + expect(error2).not.equal(null); + expect(value2.unknownProp).equal('abc'); + }); }); diff --git a/test/index-test.js b/test/index-test.js index 0fe0b29..a786a12 100644 --- a/test/index-test.js +++ b/test/index-test.js @@ -1,6 +1,11 @@ 'use strict'; +/** + * Make sure that we are starting from a fresh gstore instance + */ +delete require.cache[require.resolve('../lib')]; + const chai = require('chai'); const sinon = require('sinon'); @@ -11,8 +16,8 @@ const ds = require('@google-cloud/datastore')({ apiEndpoint: 'http://localhost:8080', }); -const gstore = require('../lib'); -const { Schema } = require('../lib'); +const gstore = require('../lib')(); +const { Schema } = require('../lib')(); const pkg = require('../package.json'); const Transaction = require('./mocks/transaction'); @@ -53,6 +58,7 @@ describe('gstore-node', () => { it('should save ds instance', () => { gstore.connect(ds); expect(gstore.ds).to.equal(ds); + expect(gstore.ds.packageJson.name).equal('@google-cloud/datastore'); }); it('should throw an error if ds passed on connect is not a Datastore instance', () => { @@ -154,14 +160,7 @@ describe('gstore-node', () => { expect(gstore.version).equal(version); }); - it('should return the datastore instance', () => { - gstore.connect(ds); - - expect(gstore.ds).equal(ds); - }); - it('should create shortcut of datastore.transaction', () => { - gstore.connect(ds); sinon.spy(ds, 'transaction'); const trans = gstore.transaction(); @@ -173,7 +172,6 @@ describe('gstore-node', () => { describe('save() alias', () => { beforeEach(() => { sinon.stub(ds, 'save').resolves(); - gstore.connect(ds); }); afterEach(() => { @@ -229,4 +227,33 @@ describe('gstore-node', () => { expect(func).to.throw('No entities passed'); }); }); + + describe('multi instances', () => { + it('should cache instances', () => { + /* eslint-disable global-require */ + const cached = require('../lib')(); + const gstore2 = require('../lib')({ namespace: 'com.mydomain2' }); + const cached2 = require('../lib')({ namespace: 'com.mydomain2' }); + + expect(cached).equal(gstore); + expect(gstore2).not.equal(gstore); + expect(cached2).equal(gstore2); + }); + + it('should throw Error if wrong config', () => { + const func1 = () => { + require('../lib')(0); + }; + const func2 = () => { + require('../lib')({}); + }; + const func3 = () => { + require('../lib')('namespace'); + }; + + expect(func1).throw(); + expect(func2).throw(); + expect(func3).throw(); + }); + }); }); diff --git a/test/model-test.js b/test/model-test.js index 2d25c44..f724e8c 100755 --- a/test/model-test.js +++ b/test/model-test.js @@ -4,6 +4,7 @@ const chai = require('chai'); const sinon = require('sinon'); const is = require('is'); +const Joi = require('joi'); const gstoreErrors = require('../lib/errors'); @@ -16,9 +17,9 @@ const ds = require('./mocks/datastore')({ const Transaction = require('./mocks/transaction'); const Query = require('./mocks/query'); -const gstore = require('../'); +const gstore = require('../')(); const Entity = require('../lib/entity'); -const { Schema } = require('../lib'); +const { Schema } = require('../lib')(); const datastoreSerializer = require('../lib/serializer').Datastore; const { queryHelpers, validation } = require('../lib/helpers'); @@ -193,6 +194,13 @@ describe('Model', () => { expect(data).equal(null); }); + + it('should not mutate the entityData passed', () => { + const data = { name: 'John' }; + const data2 = ModelInstance.sanitize(data); + + expect(data2).not.equal(data); + }); }); describe('key()', () => { @@ -1659,9 +1667,34 @@ describe('Model', () => { const { error } = model.validate(); assert.isDefined(error); - expect(validation.validate.getCall(0).args[0]).equal(model.entityData); + expect(validation.validate.getCall(0).args[0]).deep.equal(model.entityData); expect(validation.validate.getCall(0).args[1]).equal(schema); expect(validation.validate.getCall(0).args[2]).equal(model.entityKind); }); + + it('should sanitize data', () => { + schema = new Schema({ name: { type: 'string' }, createdOn: { write: false } }); + ModelInstance = gstore.model('TestValidate', schema); + const model = new ModelInstance({ name: 'John', unknown: 123, createdOn: '1900-12-25' }); + + const schemaJoi = new Schema({ + name: { joi: Joi.string() }, + createdOn: { joi: Joi.date().strip() }, + }, { joi: true }); + + const ModelInstance2 = gstore.model('TestValidate2', schemaJoi); + const model2 = new ModelInstance2({ name: 'John', unknown: 123, createdOn: '1900-12-25' }); + + const { error, value } = model.validate(); + const { error: error2, value: value2 } = model2.validate(); + + assert.isUndefined(value.createdOn); + assert.isUndefined(value.unknown); + expect(error).equal(null); + + assert.isUndefined(value2.createdOn); + assert.isUndefined(value2.unknown); + expect(error2).equal(null); + }); }); }); diff --git a/test/schema-test.js b/test/schema-test.js index 00973bc..ecbfe30 100644 --- a/test/schema-test.js +++ b/test/schema-test.js @@ -3,8 +3,8 @@ const chai = require('chai'); const Joi = require('joi'); -const gstore = require('../'); -const { Schema } = require('../lib'); +const gstore = require('../')(); +const { Schema } = require('../lib')(); const { expect, assert } = chai; @@ -193,6 +193,7 @@ describe('Schema', () => { beforeEach(() => { schema = new Schema({ name: { joi: Joi.string().required() }, + notJoi: { type: 'string' }, }, { joi: true, }); diff --git a/test/serializers/datastore-test.js b/test/serializers/datastore-test.js index c960c18..b3e5fd1 100644 --- a/test/serializers/datastore-test.js +++ b/test/serializers/datastore-test.js @@ -5,8 +5,8 @@ const ds = require('../mocks/datastore')({ namespace: 'com.mydomain', }); -const gstore = require('../../lib'); -const { Schema } = require('../../lib'); +const gstore = require('../../lib')(); +const { Schema } = require('../../lib')(); const datastoreSerializer = require('../../lib/serializer').Datastore; const { expect, assert } = chai; From 36efe8d5178f58ad90adfe8d2585ecac15343aab Mon Sep 17 00:00:00 2001 From: sebelga Date: Thu, 19 Oct 2017 07:37:37 +0200 Subject: [PATCH 4/4] Prepare release v2.0.0 Add "Model.sanitize()" with Joi.strip() fix(validate()): remove sanitising + update README.md fix(global save): bug when saving inside transaction --- README.md | 32 ++++++++++++++++++++++++-------- lib/index.js | 2 +- lib/model.js | 16 ++++++---------- package.json | 2 +- test/model-test.js | 36 +++++++++++------------------------- 5 files changed, 43 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 6e927a1..4d72674 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Its main features are: - **shortcuts** queries - pre & post **middleware** (hooks) - **custom methods** on entity instances +- :tada: **NEW** Joi schema definition/validation (since v2.0.0) This library is in active development, please report any issue you might find. @@ -33,7 +34,7 @@ Import gstore-node and @google-cloud/datastore and configure your project. For the information on how to configure @google-cloud/datastore [read the docs here](https://googlecloudplatform.github.io/google-cloud-node/#/docs/datastore/master/datastore). ```js -const gstore = require('gstore-node'); +const gstore = require('gstore-node')(); const datastore = require('@google-cloud/datastore')({ projectId: 'my-google-project-id', }); @@ -57,7 +58,7 @@ The [complete documentation of gstore-node](https://sebelga.gitbooks.io/gstore-n Initialize gstore-node in your server file ```js // server.js -const gstore = require('gstore-node'); +const gstore = require('gstore-node')(); const datastore = require('@google-cloud/datastore')({ projectId: 'my-google-project-id', }); @@ -115,6 +116,21 @@ const userSchema = new Schema({ }, }); +// Or with **Joi** schema definition +const userSchema = new Schema({ + firstname: { joi: Joi.string().required() }, + email: { joi: Joi.string().email() }, + password: { joi: Joi.string() }, + ... +}, { + joi: { + extra: { + // validates that when "email" is present, "password" must be too + when: ['email', 'password'], + }, + } +); + /** * List entities query shortcut */ @@ -183,7 +199,7 @@ const getUsers(req ,res) { .then((entities) => { res.json(entities); }) - .catch(err => res.status(500).json(err)); + .catch(err => res.status(400).json(err)); } const getUser(req, res) { @@ -192,7 +208,7 @@ const getUser(req, res) { .then((entity) => { res.json(entity.plain()); }) - .catch(err => res.status(500).json(err)); + .catch(err => res.status(400).json(err)); } const createUser(req, res) { @@ -206,13 +222,13 @@ const createUser(req, res) { .catch((err) => { // If there are any validation error on the schema // they will be in this error object - res.status(500).json(err); + res.status(400).json(err); }) } const updateUser(req, res) { const userId = +req.params.id; - const entityData = User.sanitize(req.body); // ex: { email: 'john@snow.com' } + const entityData = User.sanitize(req.body); // { email: 'john@snow.com' } /** * This will fetch the entity, merge the data and save it back to the Datastore @@ -224,7 +240,7 @@ const updateUser(req, res) { .catch((err) => { // If there are any validation error on the schema // they will be in this error object - res.status(500).json(err); + res.status(400).json(err); }) } @@ -234,7 +250,7 @@ const deleteUser(req, res) { .then((response) => { res.json(response); }) - .catch(err => res.status(500).json(err)); + .catch(err => res.status(400).json(err)); } module.exports = { diff --git a/lib/index.js b/lib/index.js index 4bf3532..ebad85d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -106,7 +106,7 @@ class Gstore { if (args.length > 1 && !is.fn(args[1])) { // Save inside a transaction - return transaction.save(entities); + return transaction.save(args[0]); } // We forward the call to google-datastore diff --git a/lib/model.js b/lib/model.js index 7cdcef3..9cc5ca4 100755 --- a/lib/model.js +++ b/lib/model.js @@ -20,6 +20,11 @@ const sanitize = (data, schema) => { return null; } + if (!is.undef(schema._joi)) { + const { value } = schema._joi.validate(data); + return value; + } + const newData = Object.assign({}, data); Object.keys(data).forEach((k) => { @@ -956,16 +961,7 @@ class Model extends Entity { } validate() { - const { schema, entityKind } = this; - let { entityData } = this; - - /** - * If not a Joi schema, we sanitize before - * Joi is going to do it for us - */ - if (is.undef(schema._joi)) { - entityData = sanitize(entityData, schema); - } + const { entityData, schema, entityKind } = this; return validation.validate(entityData, schema, entityKind); } diff --git a/package.json b/package.json index abd8dfe..a209c2f 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstore-node", - "version": "1.5.0", + "version": "2.0.0", "description": "gstore-node is a Google Datastore Entity Models tools", "main": "index.js", "scripts": { diff --git a/test/model-test.js b/test/model-test.js index f724e8c..bf90bf4 100755 --- a/test/model-test.js +++ b/test/model-test.js @@ -201,6 +201,17 @@ describe('Model', () => { expect(data2).not.equal(data); }); + + it('should sanitize with Joi.strip()', () => { + schema = new Schema({ + createdOn: { joi: Joi.strip() }, + }, { joi: true }); + ModelInstance = gstore.model('BlogJoi', schema, gstore); + + const entityData = ModelInstance.sanitize({ createdOn: 'abc' }); + + assert.isUndefined(entityData.createdOn); + }); }); describe('key()', () => { @@ -1671,30 +1682,5 @@ describe('Model', () => { expect(validation.validate.getCall(0).args[1]).equal(schema); expect(validation.validate.getCall(0).args[2]).equal(model.entityKind); }); - - it('should sanitize data', () => { - schema = new Schema({ name: { type: 'string' }, createdOn: { write: false } }); - ModelInstance = gstore.model('TestValidate', schema); - const model = new ModelInstance({ name: 'John', unknown: 123, createdOn: '1900-12-25' }); - - const schemaJoi = new Schema({ - name: { joi: Joi.string() }, - createdOn: { joi: Joi.date().strip() }, - }, { joi: true }); - - const ModelInstance2 = gstore.model('TestValidate2', schemaJoi); - const model2 = new ModelInstance2({ name: 'John', unknown: 123, createdOn: '1900-12-25' }); - - const { error, value } = model.validate(); - const { error: error2, value: value2 } = model2.validate(); - - assert.isUndefined(value.createdOn); - assert.isUndefined(value.unknown); - expect(error).equal(null); - - assert.isUndefined(value2.createdOn); - assert.isUndefined(value2.unknown); - expect(error2).equal(null); - }); }); });