From 528da24fcff21bb19937b87e96dca63d85da8c5b Mon Sep 17 00:00:00 2001 From: Bas Kiers Date: Fri, 26 Apr 2019 15:44:07 +0200 Subject: [PATCH] fix: added default options when Joi is enabled --- lib/entity.js | 4 ++-- lib/helpers/validation.js | 9 ++------- lib/index.d.ts | 17 +++++++++++++++-- lib/model.js | 14 +++++++++----- lib/schema.js | 40 ++++++++++++++++++++++++++++++++++++--- test/model-test.js | 16 ++++++++++++++++ 6 files changed, 81 insertions(+), 19 deletions(-) diff --git a/lib/entity.js b/lib/entity.js index 33d97f0..ca087fa 100644 --- a/lib/entity.js +++ b/lib/entity.js @@ -372,13 +372,13 @@ function parseId(self, id) { function buildEntityData(self, data) { const { schema } = self; - const isJoiSchema = !is.undef(schema._joi); + const isJoiSchema = schema.isJoi; let entityData; // If Joi schema, get its default values if (isJoiSchema) { - const { error, value } = schema._joi.validate(data); + const { error, value } = schema.validateJoi(data); if (!error) { entityData = Object.assign({}, value); diff --git a/lib/helpers/validation.js b/lib/helpers/validation.js index e594b0e..a5cc86b 100755 --- a/lib/helpers/validation.js +++ b/lib/helpers/validation.js @@ -192,15 +192,10 @@ const validate = (entityData, schema, entityKind, datastore) => { const props = Object.keys(entityData); const totalProps = Object.keys(entityData).length; - const isJoi = !is.undef(schema._joi); - if (isJoi) { + if (schema.isJoi) { // We leave the validation to Joi - const joiOptions = schema.options.joi.options || {}; - joiOptions.stripUnknown = {}.hasOwnProperty.call(joiOptions, 'stripUnknown') - ? joiOptions.stripUnknown - : schema.options.explicitOnly !== false; - return schema._joi.validate(entityData, joiOptions); + return schema.validateJoi(entityData); } for (let i = 0; i < totalProps; i += 1) { diff --git a/lib/index.d.ts b/lib/index.d.ts index c1bbc50..59da46a 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -184,6 +184,19 @@ export class Schema { method: string, callback: funcReturningPromise | funcReturningPromise[] ): void; + + /** + * Executes joi.validate on given data. If schema does not have a joi config object data is returned + * + * @param {*} data The data to sanitize + * @returns {*} The data sanitized + */ + validateJoi(data: { [propName: string]: any }): Validation<{ [P in keyof T]: T[P] }>; + + /** + * Checks if the schema has a joi config object. + */ + readonly isJoi: boolean; } export interface Model { @@ -844,9 +857,9 @@ export interface QueryFindAroundOptions extends QueryOptions { showKey?: boolean; } -export interface Validation { +export interface Validation { error: ValidationError; - value: any; + value: T; } declare class GstoreError extends Error { diff --git a/lib/model.js b/lib/model.js index bda6a42..f4279d9 100755 --- a/lib/model.js +++ b/lib/model.js @@ -644,20 +644,24 @@ class Model extends Entity { return null; } - const isJoiSchema = !is.undef(schema._joi); + const isJoiSchema = schema.isJoi; let sanitized; let joiOptions; if (isJoiSchema) { - sanitized = schema._joi.validate(data).value; + const { error, value } = schema.validateJoi(data); + if (!error) { + sanitized = Object.assign({}, value); + } joiOptions = schema.options.joi.options || {}; - } else { + } + if (sanitized === undefined) { sanitized = Object.assign({}, data); } const isSchemaExplicitOnly = isJoiSchema - ? !joiOptions.allowUnknown - : schema.options.explicitOnly !== false; + ? joiOptions.stripUnknown + : schema.options.explicitOnly === true; const isWriteDisabled = options.disabled.includes('write'); const hasSchemaRefProps = Boolean(schema.__meta.refProps); diff --git a/lib/schema.js b/lib/schema.js index a9c19c2..d78c946 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -10,6 +10,7 @@ const Joi = optional('joi'); const { queries } = require('./constants'); const VirtualType = require('./virtualType'); +const { ValidationError, errorCodes } = require('./errors'); const IS_QUERY_HOOK = { update: true, @@ -90,7 +91,7 @@ class Schema { // }); if (options) { - this._joi = buildJoiSchema(properties, options.joi); + this._joi = buildJoiSchema(properties, this.options.joi); } } @@ -165,6 +166,23 @@ class Schema { } return this.virtuals[propName]; } + + validateJoi(entityData) { + if (!this.isJoi) { + return { + error: new ValidationError( + errorCodes.ERR_GENERIC, + 'Schema does not have a joi configuration object' + ), + value: entityData, + }; + } + return this._joi.validate(entityData, this.options.joi.options || {}); + } + + get isJoi() { + return !is.undef(this._joi); + } } /** @@ -183,21 +201,37 @@ Schema.Types = { function defaultOptions(options) { const optionsDefault = { validateBeforeSave: true, + explicitOnly: true, queries: { readAll: false, format: queries.formats.JSON, }, }; options = extend(true, {}, optionsDefault, options); + if (options.joi) { + const joiOptionsDefault = { + options: { + allowUnknown: options.explicitOnly !== true, + }, + }; + if (is.object(options.joi)) { + options.joi = extend(true, {}, joiOptionsDefault, options.joi); + } else { + options.joi = Object.assign({}, joiOptionsDefault); + } + if (!Object.prototype.hasOwnProperty.call(options.joi.options, 'stripUnknown')) { + options.joi.options.stripUnknown = options.joi.options.allowUnknown !== true; + } + } return options; } function buildJoiSchema(schema, joiConfig) { - if (is.undef(joiConfig)) { + if (!is.object(joiConfig)) { return undefined; } - const hasExtra = is.object(joiConfig) && is.object(joiConfig.extra); + const hasExtra = is.object(joiConfig.extra); const joiKeys = {}; Object.keys(schema).forEach((k) => { diff --git a/test/model-test.js b/test/model-test.js index 767b628..aab0c8b 100755 --- a/test/model-test.js +++ b/test/model-test.js @@ -208,6 +208,22 @@ describe('Model', () => { assert.isDefined(entityData.unknown); }); + it('should return the same value object from Model.sanitize and Entity.validate in Joi schema', () => { + schema = new Schema({ + foo: { joi: Joi.object({ bar: Joi.any() }).required() }, + createdOn: { joi: Joi.date().default(() => new Date('01-01-2019'), 'static createdOn date') }, + }, { joi: true }); + GstoreModel = gstore.model('BlogJoi', schema, gstore); + + const data = { foo: { unknown: 123 } }; + const entityData = GstoreModel.sanitize(data); + const { value: validationData, error: validationError } = new GstoreModel(data).validate(); + + assert.isUndefined(entityData.foo.unknown); + assert.isNull(validationError); + assert.deepEqual(entityData, validationData); + }); + it('should preserve the datastore.KEY', () => { const key = GstoreModel.key(123); let data = { foo: 'bar' };