From ad515083a7b187ebe75f0cc29287b8f24a028b1f Mon Sep 17 00:00:00 2001 From: sebelga Date: Wed, 11 Apr 2018 07:50:47 +0200 Subject: [PATCH] feat(Schema): Set new types for Schema definition The 'string', 'int', 'boolean' types have been replaced by their constructor js types: String, Number, Boolean... The old types are still supported but will be deprecated in the future so you should update your Schema's types. See the docs: https://sebelga.gitbooks.io/gstore-node/content/schema/type_validation.html --- lib/helpers/validation.js | 24 +++- lib/index.d.ts | 16 ++- lib/schema.js | 8 ++ test/helpers/validation-test.js | 220 ++++++++++++++++++++++++++++++-- 4 files changed, 242 insertions(+), 26 deletions(-) diff --git a/lib/helpers/validation.js b/lib/helpers/validation.js index 8d405e2..352d824 100755 --- a/lib/helpers/validation.js +++ b/lib/helpers/validation.js @@ -44,13 +44,18 @@ const errorToObject = error => ( const validatePropType = (value, propType, prop) => { let valid; let ref; + let type = propType; + if (typeof propType === 'function') { + type = propType.name.toLowerCase(); + } - switch (propType) { + switch (type) { case 'string': /* eslint valid-typeof: "off" */ valid = typeof value === 'string'; ref = 'string.base'; break; + case 'date': case 'datetime': valid = isValidDate(value); ref = 'datetime.base'; @@ -59,6 +64,7 @@ const validatePropType = (value, propType, prop) => { valid = is.array(value); ref = 'array.base'; break; + case 'number': case 'int': { const isIntInstance = value.constructor.name === 'Int'; if (isIntInstance) { @@ -97,7 +103,11 @@ const validatePropType = (value, propType, prop) => { } default: /* eslint valid-typeof: "off" */ - valid = typeof value === propType; + if (Array.isArray(value)) { + valid = false; + } else { + valid = typeof value === type; + } ref = 'prop.type'; } @@ -205,7 +215,7 @@ const validate = (entityData, schema, entityKind) => { } if (!skip) { - // ... is allowed + // ... is allowed? if (!schemaHasProperty) { error = new gstoreErrors.ValidationError( gstoreErrors.errorCodes.ERR_PROP_NOT_ALLOWED, @@ -220,7 +230,7 @@ const validate = (entityData, schema, entityKind) => { // return; } - // ...is required + // ...is required? isRequired = schemaHasProperty && {}.hasOwnProperty.call(schema.paths[prop], 'required') && schema.paths[prop].required === true; @@ -238,7 +248,7 @@ const validate = (entityData, schema, entityKind) => { errors.push(errorToObject(error)); } - // ... valid prop Type + // ... is valid prop Type? if (schemaHasProperty && !isEmpty && {}.hasOwnProperty.call(schema.paths[prop], 'type')) { error = validatePropType(propertyValue, propertyType, prop); @@ -247,7 +257,7 @@ const validate = (entityData, schema, entityKind) => { } } - // ... valid prop Value + // ... is valid prop Value? if (error === null && schemaHasProperty && !isEmpty && @@ -258,7 +268,7 @@ const validate = (entityData, schema, entityKind) => { } } - // ... value in range + // ... is value in range? if (schemaHasProperty && !isEmpty && {}.hasOwnProperty.call(schema.paths[prop], 'values') && schema.paths[prop].values.indexOf(propertyValue) < 0) { diff --git a/lib/index.d.ts b/lib/index.d.ts index 3021e9a..878bf01 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -99,6 +99,14 @@ declare namespace GstoreNode { properties: {[P in keyof T]: SchemaPathDefinition }, options?: SchemaOptions); + /** + * Custom Schema Types + */ + static Types: { + Double: 'double', + GeoPoint: 'geoPoint', + } + /** * Schema paths * @@ -139,11 +147,6 @@ declare namespace GstoreNode { set(cb: (propName: string) => void): void; } - /** - * Set global configuration for shortcut Queries - * @param shortcutQuery Name of the shortcut Query - * @param options Additional configuration - */ queries(shortcutQuery: 'list', options: ShortcutQueryOptions): void; /** @@ -607,7 +610,8 @@ declare namespace GstoreNode { global?: boolean; } - type PropType = 'string' | 'int' | 'double' | 'boolean' | 'datetime' | 'array' | 'object' | 'geoPoint' | 'buffer'; + type PropType = 'string' | 'int' | 'double' | 'boolean' | 'datetime' | 'array' | 'object' | 'geoPoint' | 'buffer' | + NumberConstructor | StringConstructor | ObjectConstructor | ArrayConstructor | BooleanConstructor | DateConstructor | typeof Buffer; interface SchemaPath { [propName: string]: SchemaPathDefinition; diff --git a/lib/schema.js b/lib/schema.js index f36b2be..1954a24 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -159,6 +159,14 @@ class Schema { } } +/** + * Static properties + */ +Schema.Types = { + Double: 'double', + GeoPoint: 'geoPoint', +}; + /** * Merge options passed with the default option for Schemas * @param options diff --git a/test/helpers/validation-test.js b/test/helpers/validation-test.js index 7a6ccb9..8349683 100755 --- a/test/helpers/validation-test.js +++ b/test/helpers/validation-test.js @@ -24,32 +24,39 @@ const customValidationFunction = (obj, validator, min, max) => { return false; }; -describe('Validation', () => { +/** + * These are the new Types for the Schemas + * To be backward compatible, the old types ('int', 'string') are still supported. + * Once they will be deprecated we can delete the Validation (old Types) below. + */ +describe('Validation (new Types)', () => { let schema; - const validate = entityData => validation.validate(entityData, schema, 'MyEntityKind'); + const validate = entityData => ( + validation.validate(entityData, schema, 'MyEntityKind') + ); beforeEach(() => { schema = new Schema({ - name: { type: 'string' }, - lastname: { type: 'string' }, - age: { type: 'int' }, - birthday: { type: 'datetime' }, + name: { type: String }, + lastname: { type: String }, + age: { type: Number }, + birthday: { type: Date }, 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' }, + modified: { type: Boolean }, + tags: { type: Array }, + prefs: { type: Object }, + price: { type: Schema.Types.Double }, + icon: { type: Buffer }, + location: { type: Schema.Types.GeoPoint }, color: { validate: 'isHexColor' }, type: { values: ['image', 'video'] }, customFieldWithEmbeddedEntity: { - type: 'object', + type: Object, validate: { rule: customValidationFunction, args: [4, 10], @@ -232,9 +239,11 @@ describe('Validation', () => { it('--> object', () => { const { error } = validate({ prefs: { check: true } }); const { error: error2 } = validate({ prefs: 'string' }); + const { error: error3 } = validate({ prefs: [123] }); expect(error).equal(null); expect(error2.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); + expect(error3.errors[0].code).equal(errorCodes.ERR_PROP_TYPE); }); it('--> geoPoint', () => { @@ -506,3 +515,188 @@ describe('Joi Validation', () => { expect(value2.unknownProp).equal('abc'); }); }); + +/** + * These Types will be deprecated. + */ +describe('Validation (old Types)', () => { + 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('--> 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); + }); +});