Skip to content

Commit

Permalink
feat(Schema): Set new types for Schema definition
Browse files Browse the repository at this point in the history
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
  • Loading branch information
sebelga committed Apr 11, 2018
1 parent 351538b commit ad51508
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 26 deletions.
24 changes: 17 additions & 7 deletions lib/helpers/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) {
Expand Down Expand Up @@ -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';
}

Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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);

Expand All @@ -247,7 +257,7 @@ const validate = (entityData, schema, entityKind) => {
}
}

// ... valid prop Value
// ... is valid prop Value?
if (error === null &&
schemaHasProperty &&
!isEmpty &&
Expand All @@ -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) {
Expand Down
16 changes: 10 additions & 6 deletions lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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<T = any> {
[propName: string]: SchemaPathDefinition;
Expand Down
8 changes: 8 additions & 0 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
220 changes: 207 additions & 13 deletions test/helpers/validation-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
});
});

0 comments on commit ad51508

Please sign in to comment.