From 3f1faf113e28af4cf0c0c9e23aaeabddbeebcaf8 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Tue, 24 Nov 2015 18:51:48 +0100 Subject: [PATCH] add baseModel plugin with some tests --- test/api/v3/unit/libs/baseModel.test.js | 59 +++++++++++++++++++++++++ website/src/libs/api-v3/baseModel.js | 53 ++++++++++++++++++++++ website/src/models/user.js | 14 +++--- 3 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 test/api/v3/unit/libs/baseModel.test.js create mode 100644 website/src/libs/api-v3/baseModel.js diff --git a/test/api/v3/unit/libs/baseModel.test.js b/test/api/v3/unit/libs/baseModel.test.js new file mode 100644 index 0000000000..9cab299d82 --- /dev/null +++ b/test/api/v3/unit/libs/baseModel.test.js @@ -0,0 +1,59 @@ +import baseModel from '../../../../../website/src/libs/api-v3/baseModel'; + +describe('Base model plugin', () => { + let schema = { + add () { + return true; + }, + statics: {}, + options: {}, + pre () { + return true; + }, + }; + + beforeEach(() => { + sandbox.stub(schema, 'add'); + }); + + it('adds a _id field to the schema', () => { + baseModel(schema); + + expect(schema.add).to.be.calledWith(sinon.match({ + _id: sinon.match.object, + })); + }); + + it('can add timestamps fields', () => { + baseModel(schema, {timestamps: true}); + + expect(schema.add).to.be.calledTwice; + }); + + it('can sanitize input objects', () => { + baseModel(schema, { + noSet: ['noUpdateForMe'] + }); + + expect(schema.statics.sanitize).to.exist; + let sanitized = schema.statics.sanitize({ok: true, noUpdateForMe: true}); + + expect(sanitized).to.have.property('ok'); + expect(sanitized).to.have.property('noUpdateForMe'); + expect(sanitized.noUpdateForMe).to.equal(undefined); + }); + + it('can make fields private', () => { + baseModel(schema, { + private: ['amPrivate'] + }); + + expect(schema.options.toObject.transform).to.exist; + let objToTransform = {ok: true, amPrivate: true}; + let privatized = schema.options.toObject.transform({}, objToTransform); + + expect(objToTransform).to.have.property('ok'); + expect(objToTransform).to.have.property('amPrivate'); + expect(objToTransform.amPrivate).to.equal(undefined); + }); +}); diff --git a/website/src/libs/api-v3/baseModel.js b/website/src/libs/api-v3/baseModel.js new file mode 100644 index 0000000000..c5bc964af8 --- /dev/null +++ b/website/src/libs/api-v3/baseModel.js @@ -0,0 +1,53 @@ +import _ from 'lodash'; +import { uuid } from '../../../../common'; +import validator from 'validator'; + +export default function baseModel (schema, options = {}) { + schema.add({ + _id: { + type: String, + default: uuid.v4, + validate: [validator.isUUID, 'Invalid uuid.'], // TODO check for UUID version + }, + }); + + if (options.timestamps) { + schema.add({ + createdAt: { + type: Date, + default: Date.now, + }, + updatedAt: { + type: Date, + default: Date.now, + }, + }); + } + + if (options.timestamps) { + schema.pre('save', function updateUpdatedAt (next) { + if (!this.isNew) this.updatedAt = Date.now(); + next(); + }); + } + + let noSetFields = ['createdAt', 'updatedAt']; + let privateFields = ['__v']; + + if (Array.isArray(options.noSet)) noSetFields.push(...options.noSet); + schema.statics.sanitize = function sanitize (objToSanitize = {}) { + noSetFields.forEach((fieldPath) => { + _.set(objToSanitize, fieldPath, undefined); // TODO decide wheter to use delete here + }); + + return objToSanitize; + }; + + if (Array.isArray(options.private)) privateFields.push(...options.private); + if (!schema.options.toObject) schema.options.toObject = {}; + schema.options.toObject.transform = function transformToObject (doc, plainObj) { + privateFields.forEach((fieldPath) => { + _.set(plainObj, fieldPath, undefined); // TODO decide wheter to use delete here + }); + }; +} diff --git a/website/src/models/user.js b/website/src/models/user.js index 0462dd5ab1..c67e5f69e1 100644 --- a/website/src/models/user.js +++ b/website/src/models/user.js @@ -5,19 +5,13 @@ import _ from 'lodash'; import validator from 'validator'; import moment from 'moment'; import TaskSchemas from './task'; +import baseModel from '../libs/api-v3/baseModel'; // import {model as Challenge} from './challenge'; let Schema = mongoose.Schema; // User schema definition export let schema = new Schema({ - // The user _id, stored as a string - // TODO validation - _id: { - type: String, - default: shared.uuid, - }, - // TODO validation apiToken: { type: String, default: shared.uuid, @@ -480,6 +474,12 @@ export let schema = new Schema({ minimize: false, // So empty objects are returned }); +schema.plugin(baseModel, { + noSet: ['_id', 'apikey', 'auth.blocked', 'auth.timestamps', 'lastCron', 'auth.local.hashed_password', 'auth.local.salt'], + private: ['auth.local.hashed_password', 'auth.local.salt'], +}); + + schema.methods.deleteTask = function deleteTask (tid) { this.ops.deleteTask({params: {id: tid}}, () => {}); // TODO remove this whole method, since it just proxies, and change all references to this method };