From 27b663facf2e2c6ff0313e855ad91885f403e63c Mon Sep 17 00:00:00 2001 From: sebelga Date: Sat, 5 Nov 2016 19:23:38 +0100 Subject: [PATCH] Add Promises Support Model.get() with promise fix data back from promise is a array of data update pre / post hooks with promised-hooks entity.save() as promised. Fixed tests update() tests now with promises Update delete() and query() to support Promises added ESLINT update deleteAll, findOne & findAround method with Promise support updated entity to support Promise ESLINT library fix eslint finished test coverage update README.md update package.json remove pretest lint (breaks travis build on node 4) --- .eslintrc.json | 25 +- .travis.yml | 8 +- README.md | 409 +++-- index.js | 2 + lib/entity.js | 374 +++-- lib/error.js | 39 +- lib/error/validation.js | 18 - lib/error/validator.js | 17 - lib/helper.js | 4 +- lib/helpers/defaultValues.js | 14 +- lib/helpers/queryhelpers.js | 14 +- lib/index.js | 187 ++- lib/model.js | 1591 ++++++++++--------- lib/schema.js | 365 +++-- lib/serializer.js | 2 +- lib/serializers/datastore.js | 26 +- lib/utils.js | 66 +- lib/virtualType.js | 20 +- package.json | 37 +- test/.eslintrc | 6 + test/entity-test.js | 415 ++--- test/error-test.js | 90 +- test/error/validation-test.js | 41 - test/error/validator-test.js | 40 - test/helpers/defaultValues.js | 21 +- test/helpers/queryhelpers-test.js | 91 +- test/index-test.js | 96 +- test/mocks/datastore.js | 47 +- test/mocks/query.js | 28 + test/mocks/transaction.js | 18 + test/model-test.js | 2297 ++++++++++++++-------------- test/schema-test.js | 188 +-- test/serializers/datastore-test.js | 79 +- test/utils-test.js | 47 +- test/virtualType-test.js | 67 +- 35 files changed, 3543 insertions(+), 3246 deletions(-) delete mode 100644 lib/error/validation.js delete mode 100644 lib/error/validator.js create mode 100644 test/.eslintrc delete mode 100644 test/error/validation-test.js delete mode 100644 test/error/validator-test.js create mode 100644 test/mocks/query.js create mode 100644 test/mocks/transaction.js diff --git a/.eslintrc.json b/.eslintrc.json index 5ba10fc..14452a6 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,25 @@ { "parserOptions": { "ecmaVersion": 6, - "sourceType": "module" - } -} \ No newline at end of file + "sourceType": "script" + }, + "root": true, + "env": { + "node": true + }, + "extends": "airbnb-base", + "plugins": [ + "mocha" + ], + "rules": { + // enable additional rules + "indent": ["error", 4, {"SwitchCase": 1}], + "no-use-before-define": ["error", {"functions": false}], + "prefer-rest-params": "off", + "prefer-spread": "off", + "no-underscore-dangle": "off", + "no-param-reassign": "off", + "max-len": ["error", 120], + "mocha/no-exclusive-tests": "off" + } +} diff --git a/.travis.yml b/.travis.yml index ed4866d..e7e2069 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,4 +10,10 @@ node_js: branches: only: - master - - /^release\/.*$/ \ No newline at end of file + - /^release\/.*$/ + +script: + - npm test + +after_success: + - npm run coveralls \ No newline at end of file diff --git a/README.md b/README.md index efdb4d6..428d881 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ [![Coverage Status](https://coveralls.io/repos/github/sebelga/gstore-node/badge.svg?branch=master)](https://coveralls.io/github/sebelga/gstore-node?branch=master) gstore-node is a Google Datastore entities modeling library for Node.js inspired by Mongoose and built on top of the **[google-cloud-node](https://github.com/GoogleCloudPlatform/google-cloud-node)** library. +:new: gstore supports **Promises**! (> v0.8.0) + Its main features are: - explicit **Schema declaration** for entities @@ -94,13 +96,13 @@ sometimes lead to a lot of duplicate code to **validate** the properties passed ``` ### Getting started -For info on how to configure gcloud [read the docs here](https://googlecloudplatform.github.io/google-cloud-node/#/docs/datastore/0.5.0/datastore). +For info on how to configure the gcloud datastore [read the docs here](https://googlecloudplatform.github.io/google-cloud-node/#/docs/datastore/0.5.0/datastore). ```js -var ds = require('@google-cloud/datastore')(); - +var datastore = require('@google-cloud/datastore')(); var gstore = require('gstore-node'); -gstore.connect(ds); + +gstore.connect(datastore); ``` #### Aliases @@ -163,6 +165,17 @@ var entitySchema = new Schema({ ... }); ``` + +You can also define an **Array of valid values** for a properties. +If you then try to save an entity with a different value it won't validate and won't be saved in the Datastore. + +```js +var entitySchema = new Schema({ + color : {values: ['#ffffff', '#ff6000', '#000000'}, + ... +}); +``` + ### Other properties options #### optional By default if a property value is not defined it will be set to null or to its default value (if any). If you don't want this behaviour you can set it as *optional* and if no value is passed, this property will not be saved in the Datastore. @@ -170,7 +183,7 @@ By default if a property value is not defined it will be set to null or to its d #### default You can set a default value for the properties. -If you need to set the default value for a *datetime* property to the current time of the request there is a **special** constant for that. `gstore.defaultValues.NOW` +If you need to set the default value for a **datetime** property to the **current time of the request** there is a special default value for that. `gstore.defaultValues.NOW` ``` var schema = new Schema({ @@ -246,6 +259,7 @@ var entitySchema = new Schema({ ``` ### Schema options + #### validateBeforeSave (default true) To disable any validation before save/update, set it to false @@ -379,9 +393,9 @@ This method accepts the following parameters: - namespace (optional) - transaction (optional) - options (optional) -- callback +- callback (optional, if not passed a **Promise** is returned) -Returns: an entity **instance**. +Returns ---> an entity **instance**. ```js var blogPostSchema = new gstore.Schema({...}); @@ -439,6 +453,15 @@ transaction.run(function(err) { }); ``` +If no callback is passed, it will return a Promise + +```js +BlogPost.get(123).then((data) => { + const entity = data[0]; + console.log(entity.plain()); +}); +``` + **options** parameter The options object parameter has a **preserveOrder** property (default to false). Useful when an array of IDs is passed and you want to preserve the order of those ids in the results. @@ -466,9 +489,9 @@ The update() method has the following parameters - namespace (optional) - transaction (optional) - options (optional) -- callback +- callback (optional, if not passed a **Promise** is returned) -Returns: an entity **instance**. +Returns ---> an entity **instance**. ```js // ... @@ -528,9 +551,17 @@ BlogPost.update(123, data, null, null, null, {replace:true}, function(err, entit }); ``` +If no callback is passed, it will return a Promise + +```js +BlogPost.update(123, data).then((data) => { + const entity = data[0]; + console.log(entity.plain()); +}); +``` #### Delete() -You can delete an entity by calling `Model.delete(...args)`. +You can delete an entity by calling `Model.delete(...args)`. This method accepts the following parameters - id : the id to delete. Can also be an **array** of ids @@ -538,21 +569,23 @@ This method accepts the following parameters - namespace (optional) - transaction (optional) - key (optional) Can also be an **array** of keys -- callback +- callback (optional, if not passed a **Promise** is returned) -The callback has a "success" properties that is set to true if an entity has been deleted or false if not. +The response of the callback has a "success" properties that is set to true if an entity has been deleted or false if not. ```js var BlogPost = gstore.model('BlogPost'); -BlogPost.delete(123, function(err, success, apiResponse) { +BlogPost.delete(123, function(err, response) { if (err) { // deal with err } - if (!success) { + if (!response.success) { console.log('No entity deleted. The id provided didn\'t return any entity'); } + + // The response has a *key* property with the entity keys that have been deleted (single or Array) }); // With an array of ids @@ -567,9 +600,10 @@ BlogPost.delete(null, null, null, null, key, function(err, success, apiResponse) // Transaction // ----------- -// The same method can be executed from inside a transaction -// Important: you need to execute done() from the callback as gstore needs to execute -// the "pre" hooks before deleting the entity +/* The same method can be executed inside a transaction + * Important!: if you have "pre" middelware set fot delete, then you must *resolve* + * the Promise before commiting the transaction +*/ var transaction = gstore.transaction(); @@ -578,14 +612,25 @@ transaction.run(function(err) { // handle error return; } - - BlogPost.delete(123, null, null, transaction); - - transaction.commit(function(err) { + + // example 1 (in sync when there are no "pre" middleware) + BlogPost.delete(123, null, null, transaction); + + transaction.commit(function(err) { if (err) { // handle error } - }); + }); + + // example 2 (with "pre" middleware to execute first) + BlogPost.delete(123, null, null, transaction) + .then(() => { + transaction.commit(function(err) { + if (err) { + // handle error + } + }); + }); }); ``` @@ -756,7 +801,12 @@ blogPostEntity.entityData.title = 'My third blog post'; // blogPostEntity.title #### Save() -After the instantiation of a Model, you can persist its data to the Datastore with `entity.save(transaction /*optional*/, callback)` +After the instantiation of a Model, you can persist its data to the Datastore with `entity.save(...args)` +This method accepts the following parameters + +- transaction (optional). Will execute the save operation inside this transaction +- callback (optional, if not passed a **Promise** is returned) + ```js var gstore = require('gstore-node'); @@ -785,25 +835,57 @@ blogPostEntity.save(function(err) { /* * From inside a transaction */ - +var user = new User({name:'john'}); var transaction = gstore.transaction(); -transaction.run(function(err) { - if (err) { - // handle error - return; - } +transaction.run().then(() => { + + // See note below to avoid nesting Promises + return user.save(transaction).then(() => { + return transaction.commit().then((data) => { + const apiResponse = data[0]; + ... + }); + }); +}).catch((err) => { + // handle error + ... + }); - var user = new User({name:'john'}); // user could also come from a get() - user.save(transaction); +``` - transaction.commit(function(err) { - if (err) { - // handle error - } - }); -}); +Note on **saving inside a Transaction** +By default, the entity data is validated before being saved in the Datastore (you can desactivate this behavious by setting [validateBeforeSave](#validateBeforeSave) to false in the Schema definition). The validation middleware is async, which means that to be able to save inside a transaction and at the same time validate before, you need to resolve the *save* method before being able to commit the transaction. +A solution to avoid this is to **manually validate** before saving and then desactivate the "pre" middelwares by setting **preHooksEnabled** to false on the entity. +**Important**: This solution will bypass any other middleware that you might have defined on "save" in your Schema. +```js +var user = new User({name:'john'}); +var transaction = gstore.transaction(); + +transaction.run().then() => { + User.get(123, null, null, transaction).then((data) => { + const user = data[0]; + user.email = 'abc@def.com'; + const valid = user.validate(); + + if (!valid) { + // exit the transaction; + } + + // disable pre middleware(s) + user.preHooksEnabled = false; + + // save inside transaction + user.save(transaction); + + // ... more transaction operations + + transaction.commit().then(() => { + ... + }); + }); +}); ``` @@ -853,18 +935,27 @@ commentSchema.pre('save', function(next){ ##### datastoreEntity() -In case you need at any moment to fetch the entity data from the Datastore, this method will do just that right on the entity instance. +In case you need at any moment to fetch the entity **data** from Goolge Datastore, this method will do just that right on the entity instance. ```js var user = new User({name:'John'}); user.save(function(err) { - // userEntity is an *gstore* entity instance of a User Model + // the scope *this* is a gstore entity instance of a User Model this.datastoreEntity(function(err, entity){ console.log(entity.get('name')); // 'John' }); }); + +// or with a Promise... +user.save().then(function() { + this.datastoreEntity().then((data){ + const entity = data[0]; + console.log(entity.name); // 'John' + }); +}); + ``` ##### validate() @@ -922,7 +1013,7 @@ var query = User.query() .start(nextPageCursor); ``` -**namespace** +**namespace** Model.query() takes an optional namespace parameter if needed. ```js @@ -931,6 +1022,21 @@ var query = User.query('com.domain-dev') ... ``` +If no callback is passed, a **Promise** is returned + +```js +var query = User.query() + .filter('name', '=', 'John'); + +query.run().then((result) => { + const response = result[0]; + + // response contains both the entities and a nextPageCursor for pagination + var entities = response.entities; + var nextPageCursor = response.nextPageCursor; // not present if no more results +}); +``` + ### list() Shortcut for listing the entities. For complete control (pagination, start, end...) use the above gcloud queries. List queries are meant to quickly list entities with predefined settings. @@ -1046,6 +1152,16 @@ var newSettings = { BlogPost.list(newSettings, ...); ``` +If no callback is passed, a **Promise** is returned + +```js +BlogPost.list(/*settings*/).then((data) => { + const entities = data[0]; + console.log(entities); +}); +``` + + ### findOne() ```js User.findOne({prop1:value, prop2:value2}, ancestors /*optional*/, namespace /*optional*/, callback); @@ -1066,6 +1182,17 @@ User.findOne({email:'john@snow.com'}, function(err, entity) { ``` +If no callback is passed, a **Promise** is returned + +```js +User.findOne({email:'john@snow.com'}).then((data) => { + const entity = data[0]; + + console.log(entity.plain()); + console.log(entity.get('name')); // or directly entity.name; +}); +``` + ### findAround() `Model.findAround(property, value, settings, callback)` @@ -1085,6 +1212,15 @@ User.findAround('lastname', 'Jagger', {before:10}, function(err, entities){ ``` +If no callback is passed, a **Promise** is returned + +```js +BlogPost.findAround('publishedOn', '2016-03-01', {after:20}).then((data) => { + const entities = data[0]; + ... +}); +``` + ### deleteAll() ```js BlogPost.deleteAll(ancestors /*optional*/, namespace /*optional*/, callback) @@ -1099,20 +1235,31 @@ BlogPost.deleteAll(function(err, result){ }); // With ancestors path and namespace -BlogPost.deleteAll(['Grandpa', 1234, 'Dad', 'keyname'], 'com.new-domain.dev', function(err) {...}) +BlogPost.deleteAll(['Grandpa', 1234, 'Dad', 'keyname'], 'com.new-domain.dev', function(err) {...}); ``` +If no callback is passed, a **Promise** is returned +```js +BlogPost.deleteAll(['Grandpa', 1234, 'Dad', 'keyname'], 'com.new-domain.dev').then(() => { + ... +}); +``` ## Middleware (Hooks) -Middleware or 'Hooks' are functions that are executed right before or right after a specific action on an entity. -For now, hooks are available for the following methods +Middleware or 'Hooks' are functions that are executed right before or right after a specific action on an entity. +Hooks are available for the following methods + +- Entity.save() (also executed on Model.**update()**) +- Model.delete() +- Model.findOne() +- On your custom methods -- save (are also executed on Model.**update()**) -- delete +:exclamation: Breaking change since v0.8.0. Your hooks must return a Promise **and** you must use the new "Promise" version of the methods (=> not passing a callback). ### Pre hooks -Each pre hook has a "**next**" parameter that you have to call at the end of your function in order to run the next "pre" hook or execute to. +The middleware that you declare receives the original parameter(s) passed to the method. You can modify them in your **resolve** passing an object with an **__override** property containing the new parameter(s) for the target method (be careful though... with great power comes great responsibility!). See example below. +If you **reject** the Promise in a "pre" middleware, the target function is not executed. A common use case would be to hash a user's password before saving it into the Datastore. @@ -1129,101 +1276,164 @@ var userSchema = new Schema({ userSchema.pre('save', hashPassword); -function hashPassword(next) { +function hashPassword() { + // scope *this* is the entity instance var _this = this; - var password = this.get('password'); + var password = this.get('password'); // or this.password (virtual property) if (!password) { - return next(); + // nothing to hash... exit + return Promise.resolve(); } - - bcrypt.genSalt(5, function (err, salt) { - if (err) return next(err); - - bcrypt.hash(password, salt, null, function (err, hash) { - if (err) return next(err); - _this.set('password', hash); - - // don't forget to call next() - next(); - }); + + return new Promise((resolve, reject) => { + bcrypt.genSalt(5, function (err, salt) { + if (err) { + return reject(err); + }; + bcrypt.hash(password, salt, null, function (err, hash) { + if (err) { + return reject(err); + }; + _this.set('password', hash); // or _this.password = hash; + return resolve(); + }); + }); }); } ... // Then when you create a new user and save it (or when updating it) -// its password will automatically be hashed +// the password will automatically be hashed + var User = gstore.model('User'); var user = new User({username:'john', password:'mypassword'}); + user.save(function(err, entity) { console.log(entity.get('password')); - // $2a$05$Gd/7OGVnMyTDnaGC3QfEwuQ1qmjifli3MvjcP7UGFHAe2AuGzne5. + // $7a$01$Gd/7OGVnMyTDnaGC3QfEwuQ1qmjifli3MvjcP7UGFHAe2AuGzne5. }); ``` **Note** -The pre('delete') hook has its scope set on the entity to be deleted. **Except** when an *Array* of ids is passed when calling Model.delete(). +The pre('delete') hook has its scope set on the entity to be deleted. **Except** when an *Array* of ids to delete is passed. ```js -blogSchema.pre('delete', function(next) { +blogSchema.pre('delete', function() { console.log(this.entityKey); // the datastore entity key to be deleted // By default this.entityData is not present because // the entity is *not* fetched from the Datastore. - // You can call this.datastoreEntity() here (see the Entity section) + // You could call this.datastoreEntity() here (see the Entity section) // to fetch the data from the Datastore and do any other logic - // before calling next() + // before resolving your middlewware + + // Access arguments passed + const args = Array.prototype.slice(arguments); + console.log(args[0]); // 1234 (from call below) + + // Here you would override the id to delete! At your own risk... + // The Array passed in __override are the parameter(s) for the target function + return Promise.resolve({ __override: [1235] }); }); + +BlogPost.delete(1234).then(() => {...}); +``` + +You can also pass an **Array** of middleware to execute + +```js +function middleware1() { + // Return a Promise + return Promise.resolve(); +} + +function middleware2() { + return Promise.resolve(); +} + +userSchema.pre('save', [middleware1, middleware2]); + +var user = new User({username:'john', password:'mypassword'}); +user.save().then((result) => { ... }); ``` ### Post hooks -Post are defined the same way as pre hooks. The only difference is that there is no "next" function to call. +Post are defined the same way as pre hooks. The main difference is that if you reject the Promise because of an error, the original method still resolves but the response is now an object with 2 properties. The **result** and **errorsPostHook** containing possible post hooks error(s). ```js var schema = new Schema({username:{...}}); schema.post('save', function(){ var email = this.get('email'); // do anything needed, maybe send an email of confirmation? + + // If there is any error you'll reject your middleware + return Promise.reject({ code:500, message: 'Houston something went really wrong.' }); +}); + +// .... + +var user = new User({ name: 'John' }); + +user.save().then((data) => { + // You should only do this check if you have post hooks that can fail + const entity = data.errorsPostHook ? data[0].result : data[0]; + + if (data.errorsPostHook) { + console.log(data.errorsPostHook[0].message); // 'Houston something went really wrong.' + } }); + ``` **Note** -The post('delete') hook does not have its scope mapped to the entity as it is not retrieved. But the hook has a first argument with the key(s) that have been deleted. +The post('delete') hook does not have its scope mapped to the entity as it is not fetched from the datastore. Althought the *data* argument of the hook contain the key(s) of the entitie(s) deleted. ```js -schema.post('delete', function(keys){ - // keys can be one Key or an array of entity Keys that have been deleted. +schema.post('delete', function(data){ + // data[1] can be one Key or an array of entity Keys that have been deleted. + return Promise.resolve(); }); ``` +You can also pass an **Array** of middleware to execute + +```js +function middleware1() { + return Promise.resolve(); +} + +function middleware2() { + return Promise.resolve(); +} + +userSchema.post('save', [middleware1, middleware2]); + +var user = new User({username:'john', password:'mypassword'}); +user.save().then((result) => { ... }); +``` ### Transactions and Hooks -When you save or delete an entity from inside a transaction, gstore adds an extra **execPostHooks()** method to the transaction. +When you save or delete an entity from inside a transaction, gstore adds an **execPostHooks()** method to the transaction instance. If the transaction succeeds and you have any post('save') or post('delete') hooks on any of the entities modified during the transaction you need to call this method to execute them. ```js - var transaction = gstore.transaction(); -transaction.run(function(err) { - if (err) { - // handle error - return; - } - - var user = new User({name:'john'}); // user could also come from a get() +transaction.run().then(() => { + var user = new User({name:'john'}); + user.preHooksEnabled = false; // disable "pre" hooks (see entity section) user.save(transaction); BlogPost.delete(123, null, null, transaction); - transaction.commit(function(err) { - if (err) { - // handle error - return; - } - transaction.execPostHooks(); + transaction.commit().then((data) => { + transaction.execPostHooks().then(() => { + const apiResponse = data[0]; + // all done! + }); }); }); @@ -1236,7 +1446,7 @@ Custom methods can be attached to entities instances. ```js var blogPostSchema = new Schema({title:{}}); -// Custom method to query all "child" Text entities +// Custom method to retrieve all children Text entities blogPostSchema.methods.texts = function(cb) { var query = this.model('Text') .query() @@ -1252,20 +1462,22 @@ blogPostSchema.methods.texts = function(cb) { ... -// You can then call it on all instances of BlogPost -var blogPost = new BlogPost({title:'My Title'}); -blgoPost.texts(function(err, texts) { - console.log(texts); // texts entities; +// You can then call it on an entity instance of BlogPost +BlogPost.get(123).then((data) => { + const blogEntity = data[0]; + blogEntity.texts(function(err, texts) { + console.log(texts); // texts entities; + }); }); ``` -Note that entities instances can also access other models through `entity.model('MyModel')`. *Denormalization* can then easily be done with a custom method: +Note how entities instances can access other models through `entity.model('OtherModel')`. *Denormalization* can then easily be done with a custom method: ```js -... -// custom getImage() method on the User Schema +// Add custom "getImage()" method on the User Schema userSchema.methods.getImage = function(cb) { // Any type of query can be done here + // note this.get('imageIdx') could also be accessed by virtual property: this.imageIdx return this.model('Image').get(this.get('imageIdx'), cb); }; ... @@ -1275,6 +1487,17 @@ user.getImage(function(err, imageEntity) { user.set('profilePict', imageEntity.get('url')); user.save(function(err){...}); }); + +// Or with Promises +userSchema.methods.getImage = function() { + return this.model('Image').get(this.imageIdx); +}; +... +var user = new User({name:'John', imageIdx:1234}); +user.getImage().then((data) => { + const imageEntity = data[0]; + ... +}); ``` ## Credits diff --git a/index.js b/index.js index 9eaf523..411f6f9 100644 --- a/index.js +++ b/index.js @@ -3,4 +3,6 @@ * */ +'use strict'; + module.exports = require('./lib/'); diff --git a/lib/entity.js b/lib/entity.js index 807ff9d..ec574c7 100644 --- a/lib/entity.js +++ b/lib/entity.js @@ -1,239 +1,233 @@ -(function() { - 'use strict'; - /* - * Module dependencies. - */ - var async = require('async'); - var is = require('is'); - var extend = require('extend'); - var EventEmitter = require('events').EventEmitter; - var hooks = require('hooks-fixed'); - - var datastoreSerializer = require('./serializer').Datastore; - const defaultValues = require('./helpers/defaultValues'); - var GstoreError = require('./error.js'); - - class Entity extends EventEmitter { - constructor(data, id, ancestors, namespace, key) { - super(); - this.className = 'Entity'; - this.setMaxListeners(0); - - this.schema = this.constructor.schema; - this.excludeFromIndexes = []; - - if (key) { - if(key.constructor.name === 'Key') { - this.entityKey = key; - } else { - throw new Error('Entity Key must be an instance of gcloud Key'); - } - } else { - this.entityKey = createKey(this, id, ancestors, namespace); - } +'use strict'; - // add entityData from data passed - this.entityData = buildEntityData(this, data); +const is = require('is'); +const hooks = require('promised-hooks'); - // Adding 'pre', 'post' and 'hooks' method to our Entity (hooks-fixed) - Object.keys(hooks).forEach((k) => { - this[k] = hooks[k]; - }); +const utils = require('./utils'); +const datastoreSerializer = require('./serializer').Datastore; +const defaultValues = require('./helpers/defaultValues'); - registerHooksFromSchema(this); - } +class Entity { + constructor(data, id, ancestors, namespace, key) { + this.className = 'Entity'; - plain (options) { - options = typeof options === 'undefined' ? {} : options; + this.schema = this.constructor.schema; + this.excludeFromIndexes = []; - if (typeof options !== 'undefined' && !is.object(options)) { - throw new Error('Options must be an Object'); + if (key) { + if (key.constructor.name === 'Key') { + this.entityKey = key; + } else { + throw new Error('Entity Key must be an instance of gcloud Key'); } - let readAll = options.hasOwnProperty('readAll') ? options.readAll : false; - let virtuals = options.hasOwnProperty('virtuals') ? options.virtuals : false; + } else { + this.entityKey = createKey(this, id, ancestors, namespace); + } - if (virtuals) { - this.addVirtuals(this.entityData); - } + // create entityData from data passed + this.entityData = buildEntityData(this, data); - var data = datastoreSerializer.fromDatastore.call(this, this.entityData, readAll); + // wrap entity with hook methods + hooks.wrap(this); - return data; - }; + // add middleware defined on Schena + registerHooksFromSchema(this); + } - get (path) { - if (this.schema.virtuals.hasOwnProperty(path)) { - return this.schema.virtuals[path].applyGetters(this.entityData); - } - return this.entityData[path]; - } + plain(options) { + options = typeof options === 'undefined' ? {} : options; - set (path, value) { - if (this.schema.virtuals.hasOwnProperty(path)) { - return this.schema.virtuals[path].applySetters(value, this.entityData); - } - this.entityData[path] = value; + if (typeof options !== 'undefined' && !is.object(options)) { + throw new Error('Options must be an Object'); } + const readAll = {}.hasOwnProperty.call(options, 'readAll') ? options.readAll : false; + const virtuals = {}.hasOwnProperty.call(options, 'virtuals') ? options.virtuals : false; - /** - * Return a Model from Gstore - * @param name : model name - */ - model(name) { - return this.constructor.gstore.model(name); + if (virtuals) { + this.addVirtuals(this.entityData); } - // Fetch entity from Datastore - datastoreEntity(cb) { - let _this = this; - this.gstore.ds.get(this.entityKey, (err, entity) => { - if (err) { - return cb(err); - } - - if (!entity) { - return cb({ - code: 404, - message: 'Entity not found' - }); - } - - _this.entityData = entity; - cb(null, _this); - }); - } + const data = datastoreSerializer.fromDatastore.call(this, this.entityData, readAll); - addVirtuals() { - let virtuals = this.schema.virtuals; - let entityData = this.entityData; + return data; + } - Object.keys(virtuals).forEach((k) => { - if (entityData.hasOwnProperty(k)) { - virtuals[k].applySetters(entityData[k], entityData); - } else { - virtuals[k].applyGetters(entityData); - } - }); + get(path) { + if ({}.hasOwnProperty.call(this.schema.virtuals, path)) { + return this.schema.virtuals[path].applyGetters(this.entityData); + } + return this.entityData[path]; + } - return this.entityData; + set(path, value) { + if ({}.hasOwnProperty.call(this.schema.virtuals, path)) { + return this.schema.virtuals[path].applySetters(value, this.entityData); } + + this.entityData[path] = value; + return this; } - // Private - // ------- - function createKey(self, id, ancestors, namespace) { - let hasAncestors = typeof ancestors !== 'undefined' && ancestors !== null && is.array(ancestors); + /** + * Return a Model from Gstore + * @param name : model name + */ + model(name) { + return this.constructor.gstore.model(name); + } - /* - /* Create copy of ancestors to avoid mutating the Array - */ - if (hasAncestors) { - ancestors = ancestors.slice(); - } + /** + * Fetch entity from Datastore + * + * @param {Function} cb Callback + */ + datastoreEntity(cb) { + const _this = this; - let path; - if (id) { - if (is.string(id)) { - id = isFinite(id) ? parseInt(id, 10) : id; - } else if (!is.number(id)) { - throw new Error('id must be a string or a number'); - } - path = hasAncestors ? ancestors.concat([self.entityKind, id]) : [self.entityKind, id]; - } else { - if (hasAncestors) { - ancestors.push(self.entityKind); + return this.gstore.ds.get(this.entityKey).then(onSuccess, onError); + + // ------------------------ + + function onSuccess(result) { + const datastoreEntity = result ? result[0] : null; + + if (!datastoreEntity) { + return cb({ + code: 404, + message: 'Entity not found', + }); } - path = hasAncestors ? ancestors : self.entityKind; + + _this.entityData = datastoreEntity; + return cb(null, _this); } - if (namespace && !is.array(path)) { - path = [path]; + function onError(err) { + return cb(err); } - return namespace ? self.gstore.ds.key({namespace:namespace, path:path}) : self.gstore.ds.key(path); } - function buildEntityData(self, data) { - var schema = self.schema; - var entityData = {}; + addVirtuals() { + const virtuals = this.schema.virtuals; + const entityData = this.entityData; - if (data) { - Object.keys(data).forEach(function (k) { - entityData[k] = data[k]; - }); - } - - //set default values & excludedFromIndex - Object.keys(schema.paths).forEach((k) => { - if (!entityData.hasOwnProperty(k) && (!schema.paths[k].hasOwnProperty('optional') || schema.paths[k].optional === false)) { - let value = schema.paths[k].hasOwnProperty('default') ? schema.paths[k].default : null; - - if (({}).hasOwnProperty.call(defaultValues.__map__, value)) { - /** - * If default value is in the gstore.defaultValue map - * then execute the handler for that shortcut - */ - value = defaultValues.__handler__(value); - } else if (value === null && schema.paths[k].hasOwnProperty('values')) { - value = schema.paths[k].values[0]; - } - - entityData[k] = value; - } - if (schema.paths[k].excludeFromIndexes === true) { - self.excludeFromIndexes.push(k); + Object.keys(virtuals).forEach((k) => { + if ({}.hasOwnProperty.call(entityData, k)) { + virtuals[k].applySetters(entityData[k], entityData); + } else { + virtuals[k].applyGetters(entityData); } }); - // add Symbol Key to data - entityData[self.gstore.ds.KEY] = self.entityKey; + return this.entityData; + } +} + +// Private +// ------- +function createKey(self, id, ancestors, namespace) { + const hasAncestors = typeof ancestors !== 'undefined' && ancestors !== null && is.array(ancestors); - return entityData; + /* + /* Create copy of ancestors to avoid mutating the Array + */ + if (hasAncestors) { + ancestors = ancestors.slice(); } - function registerHooksFromSchema(self) { - var queue = self.schema && self.schema.callQueue; - if (!queue.length) { - return self; + let path; + if (id) { + if (is.string(id)) { + id = isFinite(id) ? parseInt(id, 10) : id; + } else if (!is.number(id)) { + throw new Error('id must be a string or a number'); + } + path = hasAncestors ? ancestors.concat([self.entityKind, id]) : [self.entityKind, id]; + } else { + if (hasAncestors) { + ancestors.push(self.entityKind); } + path = hasAncestors ? ancestors : self.entityKind; + } - var toWrap = queue.reduce(function(seed, pair) { - var args = [].slice.call(pair[1]); - var pointCut = pair[0] === 'on' ? 'post' : args[0]; + if (namespace && !is.array(path)) { + path = [path]; + } + return namespace ? self.gstore.ds.key({ namespace, path }) : self.gstore.ds.key(path); +} - if (!(pointCut in seed)) { - seed[pointCut] = {post: [], pre: []}; - } +function buildEntityData(self, data) { + const schema = self.schema; + const entityData = {}; - if (pair[0] === 'on') { - seed.post.push(args); - } else { - seed[pointCut].pre.push(args); + if (data) { + Object.keys(data).forEach((k) => { + entityData[k] = data[k]; + }); + } + + // set default values & excludedFromIndex + Object.keys(schema.paths).forEach((k) => { + const schemaProperty = schema.paths[k]; + + if (!{}.hasOwnProperty.call(entityData, k) && + (!{}.hasOwnProperty.call(schemaProperty, 'optional') || schemaProperty.optional === false)) { + let value = {}.hasOwnProperty.call(schemaProperty, 'default') ? schemaProperty.default : null; + + if (({}).hasOwnProperty.call(defaultValues.__map__, value)) { + /** + * If default value is in the gstore.defaultValue hashTable + * then execute the handler for that shortcut + */ + value = defaultValues.__handler__(value); + } else if (value === null && {}.hasOwnProperty.call(schemaProperty, 'values')) { + value = schemaProperty.values[0]; } - return seed; - }, {post: []}); + entityData[k] = value; + } + if (schemaProperty.excludeFromIndexes === true) { + self.excludeFromIndexes.push(k); + } + }); + + // add Symbol Key to data + entityData[self.gstore.ds.KEY] = self.entityKey; - // 'post' hooks - toWrap.post.forEach(function(args) { - self.on.apply(self, args); - }); - delete toWrap.post; + return entityData; +} - Object.keys(toWrap).forEach(function(pointCut) { - if (!self[pointCut]) { - return; - } - toWrap[pointCut].pre.forEach(function(args) { - args[0] = pointCut; - let fn = args.pop(); - args.push(fn.bind(self)); - self.pre.apply(self, args); - }); - }); +function registerHooksFromSchema(self) { + const callQueue = self.schema.callQueue.entity; + if (!Object.keys(callQueue).length) { return self; } - module.exports = exports = Entity; -})(); + Object.keys(callQueue).forEach(addHooks); + + // --------------------------------------- + + function addHooks(method) { + if (!self[method]) { + return; + } + + // Add Pre hooks + callQueue[method].pres.forEach((fn) => { + self.pre(method, fn); + }); + + // Add Pre hooks + callQueue[method].post.forEach((fn) => { + self.post(method, fn); + }); + } + return self; +} + +// Promisify Entity methods +Entity.prototype.datastoreEntity = utils.promisify(Entity.prototype.datastoreEntity); + +module.exports = exports = Entity; diff --git a/lib/error.js b/lib/error.js index 1af2420..8534d79 100644 --- a/lib/error.js +++ b/lib/error.js @@ -1,22 +1,53 @@ +/* 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'; + this.name = 'GstoreError'; } static get ValidationError() { - return require('./error/validation'); + return class extends ValidationError {}; } static get ValidatorError() { - return require('./error/validator'); + 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 = exports = GstoreError; +module.exports = { + GstoreError, + ValidationError, + ValidatorError, +}; diff --git a/lib/error/validation.js b/lib/error/validation.js deleted file mode 100644 index 03ada0d..0000000 --- a/lib/error/validation.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -var GstoreError = require('../error.js'); - -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'; - } -} - -module.exports = exports = ValidationError; diff --git a/lib/error/validator.js b/lib/error/validator.js deleted file mode 100644 index bd9d82f..0000000 --- a/lib/error/validator.js +++ /dev/null @@ -1,17 +0,0 @@ -'use strict'; - -var GstoreError = require('../error.js'); - -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 = exports = ValidatorError; diff --git a/lib/helper.js b/lib/helper.js index 7cbf0bd..4484285 100644 --- a/lib/helper.js +++ b/lib/helper.js @@ -1,4 +1,6 @@ -var queryHelpers = require('./helpers/queryhelpers'); +'use strict'; + +const queryHelpers = require('./helpers/queryhelpers'); exports.QueryHelpers = queryHelpers; diff --git a/lib/helpers/defaultValues.js b/lib/helpers/defaultValues.js index ac80042..76ac50b 100644 --- a/lib/helpers/defaultValues.js +++ b/lib/helpers/defaultValues.js @@ -1,6 +1,10 @@ 'use strict'; const NOW = 'CURRENT_DATETIME'; +const timeNow = () => new Date(); +const map = { + CURRENT_DATETIME: timeNow, +}; const handler = (value) => { if (({}).hasOwnProperty.call(map, value)) { @@ -10,16 +14,8 @@ const handler = (value) => { return null; }; -const timeNow = () => { - return new Date(); -}; - -const map = { - 'CURRENT_DATETIME' : timeNow -}; - module.exports = { NOW, __handler__: handler, - __map__: map + __map__: map, }; diff --git a/lib/helpers/queryhelpers.js b/lib/helpers/queryhelpers.js index 898d3d6..3a05424 100644 --- a/lib/helpers/queryhelpers.js +++ b/lib/helpers/queryhelpers.js @@ -1,6 +1,6 @@ 'use strict'; -var is = require('is'); +const is = require('is'); function buildFromOptions(query, options, ds) { if (!query || query.constructor.name !== 'Query') { @@ -20,7 +20,11 @@ function buildFromOptions(query, options, ds) { options.order = [options.order]; } options.order.forEach((order) => { - query.order(order.property, {descending:order.hasOwnProperty('descending') ? order.descending : false}); + query.order(order.property, + { + descending: {}.hasOwnProperty.call(order, 'descending') ? + order.descending : false, + }); }); } @@ -37,7 +41,7 @@ function buildFromOptions(query, options, ds) { if (options.filters) { if (!is.array(options.filters)) { - throw new Error ('Wrong format for filters option'); + throw new Error('Wrong format for filters option'); } if (!is.array(options.filters[0])) { @@ -50,7 +54,7 @@ function buildFromOptions(query, options, ds) { // if it is, we execute it. let value = filter[filter.length - 1]; value = is.fn(value) ? value() : value; - let f = filter.slice(0, -1).concat([value]); + const f = filter.slice(0, -1).concat([value]); query.filter.apply(query, f); }); @@ -65,5 +69,5 @@ function buildFromOptions(query, options, ds) { } module.exports = { - buildFromOptions : buildFromOptions + buildFromOptions, }; diff --git a/lib/index.js b/lib/index.js index f06344e..451f325 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,116 +1,113 @@ -(function() { - 'use strict'; - - const is = require('is'); - const utils = require('./utils'); - const Schema = require('./schema'); - const Model = require('./model'); - const defaultValues = require('./helpers/defaultValues'); - - const pkg = require('../package.json'); - - class Gstore { - constructor() { - this.models = {}; - this.modelSchemas = {}; - this.options = {}; - this.Schema = Schema; +'use strict'; + +const is = require('is'); +const Schema = require('./schema'); +const Model = require('./model'); +const defaultValues = require('./helpers/defaultValues'); + +const pkg = require('../package.json'); + +class Gstore { + constructor() { + this.models = {}; + this.modelSchemas = {}; + this.options = {}; + this.Schema = Schema; + this._defaultValues = defaultValues; + 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; + } - // Set 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 + * @param schema + * @param skipInit + */ + model(name, schema, skipInit) { + if (is.object(schema) && !(schema.instanceOfSchema)) { + schema = new Schema(schema); } - /** - * Defines a Model and retreives it - * @param name - * @param schema - * @param skipInit - */ - model(name, schema, skipInit) { - if (is.object(schema) && !(schema.instanceOfSchema)) { - schema = new Schema(schema); - } + let options; + if (skipInit && is.object(skipInit)) { + options = skipInit; + skipInit = true; + } else { + options = {}; + } - var options; - if (skipInit && is.object(skipInit)) { - options = skipInit; - skipInit = true; + // look up schema in cache + if (!this.modelSchemas[name]) { + if (schema) { + // cache it so we only apply plugins once + this.modelSchemas[name] = schema; } else { - options = {}; - } - - // look up schema in cache - if (!this.modelSchemas[name]) { - if (schema) { - // cache it so we only apply plugins once - this.modelSchemas[name] = schema; - } else { - throw new Error('Schema ' + name + ' missing'); - } + throw new Error(`Schema ${name} missing`); } + } - var model; - - // we might be passing a different schema for - // an existing model name. in this case don't read from cache. - if (this.models[name] && options.cache !== false) { - if (schema && schema.instanceOfSchema && schema !== this.models[name].schema) { - throw new Error('Trying to override ' + name + ' Model Schema'); - } - return this.models[name]; + // we might be passing a different schema for + // an existing model name. in this case don't read from cache. + if (this.models[name] && options.cache !== false) { + if (schema && schema.instanceOfSchema && schema !== this.models[name].schema) { + throw new Error(`Trying to override ${name} Model Schema`); } + return this.models[name]; + } - model = Model.compile(name, schema, this); + const model = Model.compile(name, schema, this); - if (!skipInit) { - model.init(); - } + // if (!skipInit) { + // model.init(); + // } - if (options.cache === false) { - return model; - } + if (options.cache === false) { + return model; + } - this.models[name] = model; + this.models[name] = model; - return this.models[name]; - } + return this.models[name]; + } - /** - * Alias to gcloud datastore Transaction method - */ - transaction() { - return this._ds.transaction(); - } + /** + * Alias to gcloud datastore Transaction method + */ + transaction() { + return this._ds.transaction(); + } - /** - * Return an array of model names created on this instance of Gstore - * @returns {Array} - */ - modelNames() { - var names = Object.keys(this.models); - return names; - } + /** + * Return an array of model names created on this instance of Gstore + * @returns {Array} + */ + modelNames() { + const names = Object.keys(this.models); + return names; + } - /** - * Expose the defaultValues constants - */ - get defaultValues() { - return defaultValues; - } + /** + * Expose the defaultValues constants + */ + get defaultValues() { + return this._defaultValues; + } - get version() { - return pkg.version; - } + get version() { + return this.pkgVersion; + } - get ds() { - return this._ds; - } + get ds() { + return this._ds; } +} - module.exports = exports = new Gstore(); -})(); +module.exports = exports = new Gstore(); diff --git a/lib/model.js b/lib/model.js index 60bdb6a..1410d5d 100644 --- a/lib/model.js +++ b/lib/model.js @@ -1,297 +1,311 @@ -(function() { - 'use strict'; - - /* - * Module dependencies. - */ - const moment = require('moment'); - const async = require('async'); - const is = require('is'); - const arrify = require('arrify'); - const extend = require('extend'); - 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'); - - class Model extends Entity{ - constructor (data, id, ancestors, namespace, key) { - super(data, id, ancestors, namespace, key); - } - - static compile(kind, schema, gstore) { - var ModelInstance = class extends Model { - constructor (data, id, ancestors, namespace, key) { - super(data, id, ancestors, namespace, key); - } - static init() { - } - }; - ModelInstance.schema = schema; - applyMethods(ModelInstance.prototype, schema); - applyStatics(ModelInstance, schema); - - ModelInstance.prototype.entityKind = ModelInstance.entityKind = kind; - ModelInstance.hooks = schema.s.hooks.clone(); - ModelInstance.prototype.gstore = ModelInstance.gstore = gstore; - - Object.keys(schema.paths) - .filter((key) => schema.paths.hasOwnProperty(key)) - .forEach((key) => Object.defineProperty(ModelInstance.prototype, key, { - get: function() { - return this.entityData[key]; - }, - set: function(new_value) { - this.entityData[key] = new_value; - } - })); +'use strict'; + +/* +* 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'); + +class Model extends Entity { + static compile(kind, schema, gstore) { + const ModelInstance = class extends Model {}; + hooks.wrap(ModelInstance); + + ModelInstance.schema = schema; + ModelInstance.registerHooksFromSchema(); - return ModelInstance; - } + /** + * Add schema "custom" methods on the prototype + * to be accesible from Entity instances + */ + applyMethods(ModelInstance.prototype, schema); + applyStatics(ModelInstance, schema); - static get(id, ancestors, namespace, transaction, options, cb) { - let _this = this; - let args = arrayArguments(arguments); - let multiple = is.array(id); + ModelInstance.prototype.entityKind = ModelInstance.entityKind = kind; + ModelInstance.prototype.gstore = ModelInstance.gstore = gstore; - cb = args.pop(); - id = parseId(id); - ancestors = args.length > 1 ? args[1] : undefined; - namespace = args.length > 2 ? args[2] : undefined; - transaction = args.length > 3 ? args[3] : undefined; - options = args.length > 4 ? args[4] : {}; + /** + * Create virtual properties (getters and setters for entityData object) + */ + Object.keys(schema.paths) + .filter(key => ({}.hasOwnProperty.call(schema.paths, key))) + .forEach(key => Object.defineProperty(ModelInstance.prototype, key, { + get: function getProp() { return this.entityData[key]; }, + set: function setProp(newValue) { + this.entityData[key] = newValue; + }, + })); + + return ModelInstance; + } - let key = this.key(id, ancestors, namespace); + /** + * Pass all the "pre" and "post" hooks from schema to + * the current ModelInstance + */ + static registerHooksFromSchema() { + const self = this; + const callQueue = this.schema.callQueue.model; - if (transaction) { - if (transaction.constructor.name !== 'Transaction') { - throw Error('Transaction needs to be a gcloud Transaction'); - } - return transaction.get(key, onEntity); - } + if (!Object.keys(callQueue).length) { + return this; + } - this.gstore.ds.get(key, onEntity); + Object.keys(callQueue).forEach(addHooks); - //////////////////// + return self; - function onEntity(err, entity) { - if (err) { - return cb(err); - } + // -------------------------------------- - if (!entity) { - return cb({ - code : 404, - message: _this.entityKind + ' {' + id.toString() + '} not found' - }); - } + function addHooks(method) { + // Add Pre hooks + callQueue[method].pres.forEach((fn) => { + self.pre(method, fn); + }); - if (!multiple) { - entity = [entity]; - } + // Add Post hooks + callQueue[method].post.forEach((fn) => { + self.post(method, fn); + }); + } + } - entity = entity.map((e) => { - return _this.__model(e, null, null, null, e[_this.gstore.ds.KEY]); - }); + /** + * Get and entity from the Datastore + */ + static get(id, ancestors, namespace, transaction, options, cb) { + const _this = this; + const args = Array.prototype.slice.apply(arguments); + const multiple = is.array(id); - if (multiple && options.preserveOrder) { - entity.sort(function(a, b){ - return id.indexOf(a.entityKey.id) - id.indexOf(b.entityKey.id); - }); - } + cb = args.pop(); + id = parseId(id); + ancestors = args.length > 1 ? args[1] : undefined; + namespace = args.length > 2 ? args[2] : undefined; + transaction = args.length > 3 ? args[3] : undefined; + options = args.length > 4 ? args[4] : {}; + + const key = this.key(id, ancestors, namespace); - cb(null, multiple ? entity : entity[0]); + if (transaction) { + if (transaction.constructor.name !== 'Transaction') { + throw Error('Transaction needs to be a gcloud Transaction'); } + return transaction.get(key) + .then(onEntity, onError); } - static update(id, data, ancestors, namespace, transaction, options, cb) { - var _this = this; - - var error; - var entityUpdated; - var infoTransaction; + return this.gstore.ds.get(key) + .then(onEntity, onError); - let args = arrayArguments(arguments); + // ----------------------------------------------------- - cb = args.pop(); - id = parseId(id); - ancestors = args.length > 2 ? args[2] : undefined; - namespace = args.length > 3 ? args[3] : undefined; - transaction = args.length > 4 ? args[4] : undefined; - options = args.length > 5 ? args[5] : undefined; + function onEntity(data) { + if (data.length === 0) { + return cb({ + code: 404, + message: `${_this.entityKind} { ${id.toString()} } not found`, + }); + } - let key = this.key(id, ancestors, namespace); + let entity = data[0]; - /** - * If options.replace is set to true we don't fetch the entity - * and save the data directly to the specified key, replacing any previous data. - */ - if (options && options.replace === true) { - return save(key, data, null, cb); + if (!multiple) { + entity = [entity]; } - if (typeof transaction === 'undefined' || transaction === null) { - transaction = this.gstore.ds.transaction(); - transaction.run(function(err){ - if (err) { - return onTransaction(err); - } else { - getInTransaction(transaction, function(err) { - if (err) { - onTransaction(err); - } else { - transaction.commit(onTransaction); - } - }); - } - }); - } else { - if (transaction.constructor.name !== 'Transaction') { - throw Error('Transaction needs to be a gcloud Transaction'); - } - getInTransaction(transaction, onTransaction); + entity = entity.map(e => _this.__model(e, null, null, null, e[_this.gstore.ds.KEY])); + + if (multiple && options.preserveOrder) { + entity.sort((a, b) => id.indexOf(a.entityKey.id) - id.indexOf(b.entityKey.id)); } - /////////////////// + const response = multiple ? entity : entity[0]; + return cb(null, response); + } - function getInTransaction(transaction, done) { - transaction.get(key, (err, entity) => { - if (err) { - error = err; - return done(err); - } + function onError(err) { + return cb(err); + } + } - if (!entity) { - error = { - code : 404, - message: 'Entity {' + id.toString() + '} to update not found' - }; - transaction.rollback(done); - return; - } + static update(id, data, ancestors, namespace, transaction, options, cb) { + this.__hooksEnabled = true; - extend(false, entity, data); + const _this = this; + const args = Array.prototype.slice.apply(arguments); - save(entity[_this.gstore.ds.KEY], entity, transaction, done); - }); - } + let entityUpdated; + let error = {}; + let infoTransaction = {}; - function save(key, data, transaction, done) { - let model = _this.__model(data, null, null, null, key); + cb = args.pop(); + id = parseId(id); + ancestors = args.length > 2 ? args[2] : undefined; + namespace = args.length > 3 ? args[3] : undefined; + transaction = args.length > 4 ? args[4] : undefined; + options = args.length > 5 ? args[5] : undefined; - if (transaction === null) { - // we need to pass an empty object instead of null for a bug related with pre hooks that does - // not allow null as first parameter - transaction = {}; - } + const key = this.key(id, ancestors, namespace); + const override = options && options.replace === true; - model.save(transaction, {op:'update'}, (err, entity, info) => { - if (err) { - error = err; - if (!transaction || is.object(transaction) && Object.keys(transaction).length === 0) { - return done(err); - } else { - transaction.rollback(done); - return; - } - } + /** + * If options.replace is set to true we don't fetch the entity + * and save the data directly to the specified key, overriding any previous data. + */ + if (override) { + return saveEntity({ key, data }) + .then(onEntityUpdated, onUpdateError); + } - entityUpdated = entity; - infoTransaction = info; - done(null, entity); - }); - } + if (typeof transaction === 'undefined' || transaction === null) { + transaction = this.gstore.ds.transaction(); - function onTransaction(transactionError, apiResponse) { - if (transactionError || error) { - cb(transactionError || error); - } else { - _this.prototype.emit('update'); - apiResponse = typeof apiResponse === 'undefined' ? {} : apiResponse; - extend(apiResponse, infoTransaction); - cb(null, entityUpdated, apiResponse); - } - } + return transaction.run().then((transactionData) => { + transaction = transactionData[0]; + return getAndUpdate(); + }, err => cb(err)); } - static query(namespace, transaction) { - let _this = this; + if (transaction.constructor.name !== 'Transaction') { + throw Error('Transaction needs to be a gcloud Transaction'); + } + + return getAndUpdate(); - let query = initQuery(this, namespace, transaction); + // --------------------------------------------------------- + + function getAndUpdate() { + return getEntity() + .then(saveEntity) + .then(onEntityUpdated, onUpdateError); + } - query.run = function(options, cb) { - let args = Array.prototype.slice.apply(arguments); - cb = args.pop(); + function getEntity() { + return new Promise((resolve, reject) => { + return transaction.get(key).then(onEntity, onGetError); - options = args.length > 0 ? args[0] : {}; - options = extend(true, {}, _this.schema.options.queries, options); - _this.gstore.ds.runQuery(query, onQuery); + function onEntity(getData) { + const entity = getData[0]; - //////////////////// + if (typeof entity === 'undefined') { + error = { + code: 404, + message: `Entity { ${id.toString()} } to update not found`, + }; - function onQuery(err, entities, info) { - if (err) { - return cb(err); + return reject(error); } - // Add id property to entities and suppress properties - // where "read" setting is set to false - entities = entities.map((entity) => { - return datastoreSerializer.fromDatastore.call(_this, entity, options.readAll); - }); + extend(false, entity, data); - var response = { - entities : entities + const result = { + key: entity[_this.gstore.ds.KEY], + data: entity, }; - if (info.moreResults !== ds.NO_MORE_RESULTS) { - response.nextPageCursor = info.endCursor; - } + return resolve(result); + } - cb(null, response); + function onGetError(err) { + error = err; + reject(error); } - }; + }); + } + + function saveEntity(getData) { + const entityKey = getData.key; + const entityData = getData.data; + const model = _this.__model(entityData, null, null, null, entityKey); - return query; + return model.save(transaction, { op: 'update' }); } - static list(options, cb) { - var _this = this; - let args = arrayArguments(arguments); + function onEntityUpdated(updateData) { + entityUpdated = updateData[0]; + infoTransaction = updateData[1]; - cb = args.pop(); - options = args.length > 0 ? args[0] : {}; + if (transaction) { + return transaction.commit().then(onTransactionSuccess); + } - if(this.schema.shortcutQueries.hasOwnProperty('list')) { - options = extend({}, this.schema.shortcutQueries.list, options); + return onTransactionSuccess(); + } + + function onUpdateError(err) { + error = err; + if (transaction) { + return transaction.rollback().then(onTransactionError); } - let query = initQuery(this, options.namespace); + return onTransactionError([err]); + } - // Build query from options passed - query = queryHelpers.buildFromOptions(query, options, this.gstore.ds); + function onTransactionSuccess(transactionData) { + const apiResponse = transactionData ? transactionData[0] : {}; + extend(apiResponse, infoTransaction); - // merge options inside entities option - options = extend({}, this.schema.options.queries, options); + return cb(null, entityUpdated, apiResponse); + } - this.gstore.ds.runQuery(query, (err, entities, info) => { - if (err) { - return cb(err); - } + function onTransactionError(transactionData) { + const apiResponse = transactionData ? transactionData[0] : {}; + extend(apiResponse, error); + return cb(apiResponse); + } + } + + /** + * Initialize a query for the current Entity Kind of the Model + * + * @param {String} namespace Namespace for the Query + * @param {Object} transaction The transactioh to execute the query in (optional) + * + * @returns {Object} The query to be run + */ + static query(namespace, transaction) { + const _this = this; + const query = initQuery(this, namespace, transaction); + + // keep a reference to original run() method + query.__originalRun = query.run; + + query.run = function runQuery(options, cb) { + const args = Array.prototype.slice.apply(arguments); + cb = args.pop(); + + options = args.length > 0 ? args[0] : {}; + options = extend(true, {}, _this.schema.options.queries, options); + + return this.__originalRun.call(this).then(onQuery).catch(onError); + + // ----------------------------------------------- + + function onQuery(data) { + let entities = data[0]; + const info = data[1]; // Add id property to entities and suppress properties // where "read" setting is set to false - entities = entities.map((entity) => { - return datastoreSerializer.fromDatastore.call(_this, entity, options.readAll); - }); + entities = entities.map(entity => datastoreSerializer.fromDatastore.call(_this, + entity, + options.readAll)); - var response = { - entities : entities + const response = { + entities, }; if (info.moreResults !== ds.NO_MORE_RESULTS) { @@ -299,670 +313,747 @@ } cb(null, response); - }); + } + + function onError(err) { + return cb(err); + } + }; + + query.run = utils.promisify(query.run); + + return query; + } + + static list(options, cb) { + const _this = this; + const args = Array.prototype.slice.apply(arguments); + + cb = args.pop(); + options = args.length > 0 ? args[0] : {}; + + /** + * If global options set in schema, we extend the current it with passed options + */ + if ({}.hasOwnProperty.call(this.schema.shortcutQueries, 'list')) { + options = extend({}, this.schema.shortcutQueries.list, options); } - static delete(id, ancestors, namespace, transaction, key, cb) { - let _this = this; - let args = arrayArguments(arguments); - let multiple = is.array(id); + let query = initQuery(this, options.namespace); - cb = args.pop(); - id = parseId(id); - ancestors = args.length > 1 ? args[1]: undefined; - namespace = args.length > 2 ? args[2]: undefined; - transaction = args.length > 3 ? args[3]: undefined; - key = args.length > 4 ? args[4]: undefined; + // Build Datastore query from options passed + query = queryHelpers.buildFromOptions(query, options, this.gstore.ds); - if (!key) { - key = this.key(id, ancestors, namespace); - } else { - multiple = is.array(key); - } + // merge options inside entities option + options = extend({}, this.schema.options.queries, options); - if (transaction && transaction.constructor.name !== 'Transaction') { - throw Error('Transaction needs to be a gcloud Transaction'); - } + return query.run().then(onSuccess, onError); - /** - * If it is a transaction, we create a hooks.post array to be executed - * after transaction succeeds if needed with transaction.execPostHooks() - */ - if (transaction) { - this.hooksTransaction(transaction); - } + // ---------------------------------------- - /** - * Call pre hooks, then delete, then post hooks - */ - async.series([pre, executeDelete, post], allDone); + function onSuccess(queryData) { + let entities = queryData[0]; + const info = queryData[1]; - ////////// + // Add id property to entities and suppress properties + // where "read" setting is set to false + entities = entities.map(entity => datastoreSerializer.fromDatastore.call(_this, entity, options.readAll)); - function pre(callback) { - let entity; + const response = { + entities, + }; - if (!multiple) { - entity = _this.__model(null, id, ancestors, namespace, key); - } - return _this.hooks.execPre('delete', entity ? entity : null, callback); + if (info.moreResults !== ds.NO_MORE_RESULTS) { + response.nextPageCursor = info.endCursor; } - function executeDelete(callback) { - if (!transaction) { - _this.gstore.ds.delete(key, onDelete); - } else { - transaction.delete(key); - transaction.addHook('post', function() { - _this.hooks.execPost('delete', _this, [key], () => {}); - }); + return cb(null, response); + } - return cb(); - } + function onError(err) { + return cb(err); + } + } - function onDelete(err, apiRes) { - if (err) { - return callback(err); - } + static delete(id, ancestors, namespace, transaction, key, cb) { + this.__hooksEnabled = true; - if (apiRes) { - apiRes.success = apiRes.indexUpdates > 0; - } + const args = Array.prototype.slice.apply(arguments); - callback(null, apiRes); - } + cb = args.pop(); + id = parseId(id); + ancestors = args.length > 1 ? args[1] : undefined; + namespace = args.length > 2 ? args[2] : undefined; + transaction = args.length > 3 ? args[3] : undefined; + key = args.length > 4 ? args[4] : undefined; + + if (!key) { + key = this.key(id, ancestors, namespace); + } + + if (transaction && transaction.constructor.name !== 'Transaction') { + throw Error('Transaction needs to be a gcloud Transaction'); + } + + /** + * If it is a transaction, we create a hooks.post array that will be executed + * when transaction succeeds by calling transaction.execPostHooks() ---> returns a Promise + */ + if (transaction) { + // disable (post) hooks, to only trigger them if transaction succeeds + this.__hooksEnabled = false; + this.hooksTransaction(transaction, this.__posts ? this.__posts.delete : undefined); + transaction.delete(key); + return cb(); + } + + return this.gstore.ds.delete(key).then(onDelete, onError); + + // ------------------------------------------------------- + + function onDelete(results) { + const response = results ? results[0] : {}; + response.key = key; + + let success; + if (typeof response.indexUpdates !== 'undefined') { + success = response.indexUpdates > 0; } + return cb(null, success, response); + } - function post(callback) { - return _this.hooks.execPost('delete', _this, [key], callback); + function onError(err) { + return cb(err); + } + } + + static deleteAll(ancestors, namespace, cb) { + const _this = this; + const args = Array.prototype.slice.apply(arguments); + + cb = args.pop(); + ancestors = args.length > 0 ? args[0] : undefined; + namespace = args.length > 1 ? args[1] : undefined; + + const query = initQuery(this, namespace); + + if (ancestors) { + query.hasAncestor(this.gstore.ds.key(ancestors.slice())); + } + + return query.run().then(onEntities, onError); + + // ------------------------------------------------ + + function onEntities(data) { + const entities = data[0]; + if ((_this.__pres && {}.hasOwnProperty.call(_this.__pres, 'delete'))) { + // We execute delete in serie (chaining Promises) --> so we call each possible pre & post hooks + return entities.reduce(chainPromise, Promise.resolve()) + .then(onEntitiesDeleted, onError); } - function allDone(err, results) { - if (err) { - return cb(err); - } - let response = results[1]; - return cb(null, response); + const keys = entities.map(entity => entity[_this.gstore.ds.KEY]); + return _this.delete.call(_this, null, null, null, null, keys).then(onEntitiesDeleted, onError); + + // ------------------------------------ + + function chainPromise(promise, entity) { + return promise.then(_this.delete.call(_this, null, null, null, null, entity[_this.gstore.ds.KEY])); } } - static deleteAll(ancestors, namespace, cb) { - var _this = this; + function onError(err) { + return cb(err); + } - let args = arrayArguments(arguments); + function onEntitiesDeleted() { + return cb(null, { + success: true, + message: `All ${_this.entityKind} deleted successfully.`, + }); + } + } - cb = args.pop(); - ancestors = args.length > 0 ? args[0] : undefined; - namespace = args.length > 1 ? args[1] : undefined; + static findOne(params, ancestors, namespace, cb) { + this.__hooksEnabled = true; - let query = initQuery(this, namespace); + const _this = this; + const args = Array.prototype.slice.apply(arguments); - if (ancestors) { - query.hasAncestor(this.gstore.ds.key(ancestors.slice())); - } + cb = args.pop(); + ancestors = args.length > 1 ? args[1] : undefined; + namespace = args.length > 2 ? args[2] : undefined; - this.gstore.ds.runQuery(query, (err, entities) => { - if (err) { - return cb(err); - } - if (_this.hooks._pres.hasOwnProperty('delete') || _this.hooks._posts.hasOwnProperty('delete')) { - // We execute delete in serie, calling each pre / post hooks - async.eachSeries(entities, function deleteEntity(entity, cb) { - _this.delete.call(_this, null, null, null, null, entity[_this.gstore.ds.KEY], cb); - }, onEntitiesDeleted); - } else { - // No pre or post hooks so we can delete them all at once - let keys = entities.map((entity) => { - return entity[_this.gstore.ds.KEY]; - }); - _this.delete.call(_this, null, null, null, null, keys, onEntitiesDeleted); - } + if (!is.object(params)) { + return cb({ + code: 400, + message: 'Params have to be passed as object', }); + } - ////////// + const query = initQuery(this, namespace); + query.limit(1); - function onEntitiesDeleted(err) { - if (err) { - return cb(err); - } - cb(null, { - success: true, - message: 'All ' + _this.entityKind + ' deleted successfully.' - }); - } + Object.keys(params).forEach((k) => { + query.filter(k, params[k]); + }); + + if (ancestors) { + query.hasAncestor(this.gstore.ds.key(ancestors.slice())); } - static findOne(params, ancestors, namespace, cb) { - let _this = this; - let args = arrayArguments(arguments); + return query.run().then(onSuccess, onError); - cb = args.pop(); + // ----------------------------------------- - ancestors = args.length > 1 ? args[1] : undefined; - namespace = args.length > 2 ? args[2] : undefined; + function onSuccess(queryData) { + const entities = queryData ? queryData[0] : null; + let entity = entities && entities.length > 0 ? entities[0] : null; - if (!is.object(params)) { + if (!entity) { return cb({ - code : 400, - message : 'Params have to be passed as object' + code: 404, + message: `${_this.entityKind} not found`, }); } - let query = initQuery(this, namespace); - - query.limit(1); + entity = _this.__model(entity, null, null, null, entity[_this.gstore.ds.KEY]); + return cb(null, entity); + } - Object.keys(params).forEach((k) => { - query.filter(k, params[k]); - }); + function onError(err) { + return cb(err); + } + } - if (ancestors) { - query.hasAncestor(this.gstore.ds.key(ancestors.slice())); - } + static findAround(property, value, options, namespace, cb) { + const _this = this; + const args = Array.prototype.slice.apply(arguments); + cb = args.pop(); - this.hooks.execPre('findOne', _this, () => { - // Pre methods done - _this.gstore.ds.runQuery(query, (err, entities) => { - if (err) { - return cb(err); - } + if (args.length < 3) { + return cb({ + code: 400, + message: 'Argument missing', + }); + } - let entity = entities && entities.length > 0 ? entities[0] : null; + property = args[0]; + value = args[1]; + options = args[2]; + namespace = args.length > 3 ? args[3] : undefined; - if (!entity) { - return cb({ - code: 404, - message: _this.entityKind + ' not found' - }); - } else { - entity = _this.__model(entity, null, null, null, entity[_this.gstore.ds.KEY]); - } + if (!is.object(options)) { + return cb({ + code: 400, + message: 'Options pased has to be an object', + }); + } - _this.hooks.execPost('findOne', null, [], () => { - // all post hooks are done - cb(null, entity); - }); - }); + if (!{}.hasOwnProperty.call(options, 'after') && !{}.hasOwnProperty.call(options, 'before')) { + return cb({ + code: 400, + message: 'You must set "after" or "before" in options', }); } - static findAround(property, value, options, namespace, cb) { - var _this = this; + if ({}.hasOwnProperty.call(options, 'after') && {}.hasOwnProperty.call(options, 'before')) { + return cb({ + code: 400, + message: 'You must chose between after or before', + }); + } - let args = arrayArguments(arguments); + const query = initQuery(this, namespace); + const op = options.after ? '>' : '<'; + const descending = !!options.after; - cb = args.pop(); + query.filter(property, op, value); + query.order(property, { descending }); + query.limit(options.after ? options.after : options.before); - if (args.length < 3) { - return cb({ - code : 400, - message : 'Argument missing' - }); - } + return query.run().then(onSuccess, onError); - property = args[0]; - value = args[1]; - options = args[2]; - namespace = args.length > 3 ? args[3] : undefined; + // -------------------------- - if (!is.object(options)) { - return cb({ - code : 400, - message : 'Options pased has to be an object' - }); - } + function onSuccess(queryData) { + let entities = queryData[0]; - if (!options.hasOwnProperty('after') && !options.hasOwnProperty('before')) { - return cb({ - code : 400, - message : 'You must set "after" or "before" in options' - }); - } + // Add id property to entities and suppress properties + // where "read" setting is set to false + entities = entities.map(entity => datastoreSerializer.fromDatastore.call(_this, entity, options.readAll)); - if (options.hasOwnProperty('after') && options.hasOwnProperty('before')) { - return cb({ - code : 400, - message : 'You must chose between after or before' - }); - } + return cb(null, entities); + } - let query = initQuery(this, namespace); + function onError(err) { + return cb(err); + } + } - let op = options.after ? '>' : '<'; - let descending = options.after ? false : true; + /** + * Generate one or an Array of Google Datastore entity keys + * based on the current entity kind + * + * @param {Number|String|Array} ids Id of the entity(ies) + * @param {Array} ancestors Ancestors path (otional) + * @namespace {String} namespace The namespace where to store the entity + */ + static key(ids, ancestors, namespace) { + const _this = this; + const keys = []; - query.filter(property, op, value); - query.order(property, {descending: descending}); - query.limit(options.after ? options.after : options.before); + let multiple = false; - this.gstore.ds.runQuery(query, (err, entities) => { - if (err) { - return cb(err); - } + if (typeof ids !== 'undefined' && ids !== null) { + multiple = is.array(ids); - // Add id property to entities and suppress properties - // where "read" setting is set to false - entities = entities.map((entity) => { - return datastoreSerializer.fromDatastore.call(_this, entity, options.readAll); - }); + if (!multiple) { + ids = [ids]; + } - cb(null, entities); + ids.forEach((id) => { + const key = getKey(id); + keys.push(key); }); - }; - - static key(ids, ancestors, namespace) { - let _this = this; - let multiple = false; + } else { + const key = getKey(null); + keys.push(key); + } - let keys = []; + return multiple ? keys : keys[0]; - if (typeof ids !== 'undefined' && ids !== null) { - multiple = is.array(ids); + // ---------------------------------------- - if (!multiple) { - ids = [ids]; - } + function getKey(id) { + const path = getPath(id); + let key; - ids.forEach((id) => { - let key = getKey(id, ancestors, namespace); - keys.push(key); + if (typeof namespace !== 'undefined' && namespace !== null) { + key = _this.gstore.ds.key({ + namespace, + path, }); } else { - let key = getKey(null, ancestors, namespace); - keys.push(key); + key = _this.gstore.ds.key(path); } + return key; + } - return multiple ? keys : keys[0]; - - //////////////////// + function getPath(id) { + let path = [_this.entityKind]; - function getKey(id, ancestors, namespace) { - let path = getPath(id, ancestors); - let key; - if (typeof namespace !== 'undefined' && namespace !== null) { - key = _this.gstore.ds.key({ - namespace : namespace, - path : path - }); - } else { - key = _this.gstore.ds.key(path); - } - return key; + if (typeof id !== 'undefined' && id !== null) { + id = parseId(id); + path.push(id); } - function getPath(id, ancestors) { - let path = [_this.entityKind]; + if (ancestors && is.array(ancestors)) { + path = ancestors.concat(path); + } - if (typeof id !== 'undefined' && id !== null) { - id = parseId(id); - path.push(id); - } + return path; + } + } - if (ancestors && is.array(ancestors)) { - path = ancestors.concat(path); - } + /** + * Add "post" hooks to a transaction + */ + static hooksTransaction(transaction, postHooks) { + postHooks = arrify(postHooks); - return path; - } + if (!{}.hasOwnProperty.call(transaction, 'hooks')) { + transaction.hooks = { + post: [], + }; } - static hooksTransaction(transaction) { - var _this = this; + postHooks.forEach(hook => transaction.hooks.post.push(hook)); - if (!transaction.hasOwnProperty('hooks')) { - transaction.hooks = { - post:[] - }; + transaction.execPostHooks = function executePostHooks() { + if (this.hooks.post) { + return this.hooks.post.reduce((promise, hook) => promise.then(hook), Promise.resolve()); + } - transaction.addHook = function(type, fn) { - this.hooks[type].push(fn.bind(_this)); - }; + return Promise.resolve(); + }; + } - transaction.execPostHooks = executePostHooks.bind(transaction); + /** + * Dynamic properties (in non explicitOnly Schemas) are indexes by default + * This method allows to exclude from indexes those properties if needed + * @param properties {Array} or {String} + * @param cb + */ + static excludeFromIndexes(properties) { + if (!is.array(properties)) { + properties = arrify(properties); + } - function executePostHooks() { - this.hooks.post.forEach(function(fn) { - fn.call(_this); - }); - } + properties.forEach((p) => { + if (!{}.hasOwnProperty.call(this.schema.paths, p)) { + this.schema.path(p, { optional: true, excludeFromIndexes: true }); + } else { + this.schema.paths[p].excludeFromIndexes = true; } + }); + } + + /** + * Sanitize user data before saving to Datastore + * @param data : userData + */ + static sanitize(data) { + if (!is.object(data)) { + return null; } - /** - * Dynamic properties (in non explicitOnly Schemas) are indexes by default - * This method allows to exclude from indexes those properties if needed - * @param properties {Array} or {String} - * @param cb - */ - static excludeFromIndexes(properties) { - if (!is.array(properties)) { - properties = arrify(properties); + Object.keys(data).forEach((k) => { + if (!{}.hasOwnProperty.call(this.schema.paths, k) || this.schema.paths[k].write === false) { + delete data[k]; + } else if (data[k] === 'null') { + data[k] = null; } - properties.forEach((p) => { - if (!this.schema.paths.hasOwnProperty(p)) { - this.schema.path(p, {optional:true, excludeFromIndexes: true}); - } else { - this.schema.paths[p].excludeFromIndexes = true; - } - }); + }); + + return data; + } + + /** + * Creates an entity instance of a Model + * @param data (entity data) + * @param id + * @param ancestors + * @param namespace + * @param key (gcloud entity Key) + * @returns {Entity} Entity --> Model instance + * @private + */ + static __model(data, id, ancestors, namespace, key) { + const M = this.compile(this.entityKind, this.schema, this.gstore); + return new M(data, id, ancestors, namespace, key); + } + + /** + * Helper to change the function scope for a hook if necessary + * + * @param {String} hook The name of the hook (save, delete...) + * @param {Array} args The arguments passed to the original method + */ + static __scopeHook(hook, args) { + const _this = this; + + switch (hook) { + case 'delete': + return deleteScope(); + default: + return undefined; } - /** - * Sanitize user data before saving to Datastore - * @param data : userData - */ - static sanitize(data) { - if(!is.object(data)) { - return null; - } + function deleteScope() { + let id = args[0]; + const multiple = is.array(id); + id = parseId(id); - Object.keys(data).forEach((k) => { - if (!this.schema.paths.hasOwnProperty(k) || this.schema.paths[k].write === false) { - delete data[k]; - } else { - if (data[k] === 'null') { - data[k] = null; - } - } - }); + const ancestors = args.length > 1 ? args[1] : undefined; + const namespace = args.length > 2 ? args[2] : undefined; + const key = args.length > 4 ? args[4] : undefined; - return data; + return multiple ? null : _this.__model(null, id, ancestors, namespace, key); } + } + + save(transaction, options, cb) { + this.__hooksEnabled = true; + + const _this = this; + const args = Array.prototype.slice.apply(arguments); + const saveOptions = { + op: 'save', + }; + + cb = args.pop(); + transaction = args.length > 0 ? args[0] : undefined; + options = args.length > 1 && args[1] !== null ? args[1] : {}; /** - * Creates an entity instance of a Model - * @param data (entity data) - * @param id - * @param ancestors - * @param namespace - * @param key (gcloud entity Key) - * @returns {Entity} Entity --> Model instance - * @private + * In a transaction we don't need to pass a callback. In case we pass a Transaction + * without callback we need to delete the callback and set the transaction accordingly */ - static __model(data, id, ancestors, namespace, key) { - let M = this.compile(this.entityKind, this.schema, this.gstore); - return new M(data, id, ancestors, namespace, key); + if (!transaction && !is.fn(cb) && cb.constructor && cb.constructor.name === 'Transaction') { + transaction = cb; + cb = undefined; } - save (transaction, options, cb) { - let _this = this; - let args = arrayArguments(arguments); - let saveOptions = { - op:'save' - }; - - cb = args.pop(); - transaction = args.length > 0 ? args[0] : undefined; - options = args.length > 1 && args[1] !== null ? args[1] : {}; + /** + * If it is a transaction, we create a hooks.post array that will be executed + * when transaction succeeds by calling transaction.execPostHooks() (returns a Promises) + */ + if (transaction) { + // disable (post) hooks, we will only trigger them on transaction succceed + this.__hooksEnabled = false; + this.constructor.hooksTransaction(transaction, this.__posts ? this.__posts.save : undefined); + } - /* - * Fix when passing {} as transaction (pre hooks does not allow null value as first argument...) - */ - if (is.object(transaction) && Object.keys(transaction).length === 0) { - transaction = null; - } + extend(saveOptions, options); - /** - * In a transaction we don't need to pass a callback. In case we pass a Transaction - * without callback we need to delete the callback and set the transaction accordingly - */ - if (!transaction && !is.fn(cb) && cb.constructor && cb.constructor.name === 'Transaction') { - transaction = cb; - cb = undefined; - } + /** + * If the schema has a modifiedOn property we automatically + * update its value to the current dateTime + */ + if ({}.hasOwnProperty.call(this.schema.paths, 'modifiedOn')) { + this.entityData.modifiedOn = new Date(); + } - /** - * If it is a transaction, we create a hooks.post array to be executed - * after transaction succeeds if needed with transaction.execPostHooks() - */ - if (transaction) { - this.constructor.hooksTransaction(transaction); - } + const entity = { + key: this.entityKey, + data: datastoreSerializer.toDatastore(this.entityData, this.excludeFromIndexes), + }; - extend(saveOptions, options); + const info = { + op: saveOptions.op, + }; - if (this.schema.paths.hasOwnProperty('modifiedOn')) { - this.entityData.modifiedOn = new Date(); - } + if (!transaction) { + return this.gstore.ds.save(entity).then(onSuccess, onError); + } - var entity = { - key : this.entityKey, - data : datastoreSerializer.toDatastore(this.entityData, this.excludeFromIndexes) - }; + if (transaction.constructor.name !== 'Transaction') { + throw Error('Transaction needs to be a gcloud Transaction'); + } - let info = { - op : saveOptions.op - }; + transaction.save(entity); - if (!transaction) { - this.gstore.ds.save(entity, (err) => { - if (err) { - return cb(err); - } + if (cb) { + return cb(null, _this, info, transaction); + } - _this.emit('save'); + return true; - cb(null, _this, info); - }); - } else { - if (transaction.constructor.name !== 'Transaction') { - throw Error('Transaction needs to be a gcloud Transaction'); - } + // --------------------------------------------- - transaction.save(entity); - transaction.addHook('post', function() { - _this.emit('save'); - }); - if (cb) { - cb(null, _this, info); - } - } + function onSuccess() { + return cb(null, _this, info); } - validate(cb) { - let errors = {}; - const self = this; - const schema = this.schema; - - let skip; - let schemaHasProperty; - let propertyValue; - let isValueEmpty; - let isRequired; + function onError(err) { + return cb(err); + } + } - Object.keys(this.entityData).forEach((k) => { - skip = false; - schemaHasProperty = schema.paths.hasOwnProperty(k); - propertyValue = self.entityData[k]; - isValueEmpty = valueIsEmpty(propertyValue); + validate(cb) { + const errors = {}; + const self = this; + const schema = this.schema; + + let skip; + let schemaHasProperty; + let propertyValue; + let isValueEmpty; + let isRequired; + + Object.keys(this.entityData).forEach((k) => { + skip = false; + schemaHasProperty = {}.hasOwnProperty.call(schema.paths, k); + propertyValue = self.entityData[k]; + isValueEmpty = valueIsEmpty(propertyValue); + + if (typeof propertyValue === 'string') { + propertyValue = propertyValue.trim(); + } - 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; + } - if (schema.virtuals.hasOwnProperty(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; + } - // 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`); + } - 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; - // Properties type - if (!skip && schemaHasProperty && !isValueEmpty && schema.paths[k].hasOwnProperty('type')) { - var typeValid = true; - if (schema.paths[k].type === 'datetime') { - // Validate datetime "format" - let error = validateDateTime(propertyValue, k); - if (error !== null) { - errors.datetime = error; - } - } else { - if (schema.paths[k].type === 'array') { - // Array - typeValid = is.array(propertyValue); - } else if (schema.paths[k].type === 'int') { - // Integer - let isIntInstance = propertyValue.constructor.name === 'Int'; - if (isIntInstance) { - typeValid = !isNaN(parseInt(propertyValue.value)); - } else { - typeValid = isInt(propertyValue); - } - } else if (schema.paths[k].type === 'double') { - // Double - let 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 (schema.paths[k].type === 'buffer') { - // Double - typeValid = propertyValue instanceof Buffer; - } else if (schema.paths[k].type === 'geoPoint') { - // GeoPoint - typeValid = propertyValue.constructor.name === 'GeoPoint'; + if (schema.paths[k].type === 'datetime') { + // Validate datetime "format" + const error = validateDateTime(propertyValue, k); + if (error !== null) { + errors.datetime = error; + } + } else { + if (schema.paths[k].type === 'array') { + // Array + typeValid = is.array(propertyValue); + } else if (schema.paths[k].type === 'int') { + // Integer + const isIntInstance = propertyValue.constructor.name === 'Int'; + if (isIntInstance) { + typeValid = !isNaN(parseInt(propertyValue.value, 10)); } else { - // Other - typeValid = typeof propertyValue === schema.paths[k].type; + typeValid = isInt(propertyValue); } + } else if (schema.paths[k].type === 'double') { + // Double + const isIntInstance = propertyValue.constructor.name === 'Double'; - if (!typeValid) { - errors[k] = new GstoreError.ValidationError({ - message: 'Data type error for ' + k - }); + if (isIntInstance) { + typeValid = isFloat(parseFloat(propertyValue.value, 10)) || + isInt(parseFloat(propertyValue.value, 10)); + } else { + typeValid = isFloat(propertyValue) || isInt(propertyValue); } + } else if (schema.paths[k].type === 'buffer') { + // Double + typeValid = propertyValue instanceof Buffer; + } else if (schema.paths[k].type === 'geoPoint') { + // GeoPoint + typeValid = propertyValue.constructor.name === 'GeoPoint'; + } else { + // Other + /* eslint valid-typeof: "off" */ + typeValid = typeof propertyValue === schema.paths[k].type; } - } - - // Value Validation - // ...Required - isRequired = schemaHasProperty && schema.paths[k].hasOwnProperty('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 && schema.paths[k].hasOwnProperty('validate') && self.entityData[k] && self.entityData[k] !== '' && self.entityData[k] !== null) { - if (!validator[schema.paths[k].validate](self.entityData[k])) { - errors[k] = new GstoreError.ValidatorError({ - message: 'Wrong format for property {' + k + '}' + if (!typeValid) { + errors[k] = new GstoreError.ValidationError({ + message: `Data type error for ' + ${k}`, }); } } + } - // Preset values - if (!skip && schemaHasProperty && schema.paths[k].hasOwnProperty('values') && self.entityData[k] !== '') { - if (schema.paths[k].values.indexOf(self.entityData[k]) < 0) { - errors[k] = new Error('Value not allowed for ' + k + '. It must be in the range: ' + schema.paths[k].values); - } - } - }); + // ----------------- + // Value Validation + // ----------------- - if (cb) { - var payload = Object.keys(errors).length > 0 ? {success:false, errors:errors} : {success:true}; - cb(payload); - } else { - return Object.keys(errors).length > 0 ? {success:false, errors:errors} : {success:true}; - } + // ...Required + isRequired = schemaHasProperty && + {}.hasOwnProperty.call(schema.paths[k], 'required') && + schema.paths[k].required === true; - 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; + if (!skip && isRequired && isValueEmpty) { + errors[k] = new GstoreError.ValidatorError({ + errorName: 'Required', + message: `Property { ${k} } is required`, + }); } - function isInt(n){ - return Number(n) === n && n % 1 === 0; + // ...Wrong format + if (!skip && schemaHasProperty && !isValueEmpty && + {}.hasOwnProperty.call(schema.paths[k], 'validate') && + !validator[schema.paths[k].validate](propertyValue)) { + errors[k] = new GstoreError.ValidatorError({ + message: `Wrong format for property { ${k} }`, + }); } - function isFloat(n){ - return Number(n) === n && n % 1 !== 0; + // ...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}`); } + }); - function valueIsEmpty(v) { - return v === null || - v === undefined || - typeof v === 'string' && v.trim().length === 0; + if (cb) { + const payload = Object.keys(errors).length > 0 ? { success: false, errors } : { success: true }; + return cb(payload); + } + return Object.keys(errors).length > 0 ? { success: false, errors } : { success: true }; + + 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; } - } - /** - * - * @param self {Model} - * @param schema {Schema} - * @returns {Model} - */ - function applyMethods (self, schema) { - Object.keys(schema.methods).forEach((method) => { - self[method] = schema.methods[method]; - }); - return self; - } + function isInt(n) { + return Number(n) === n && n % 1 === 0; + } - function applyStatics(Model, schema) { - Object.keys(schema.statics).forEach((method) => { - if (typeof Model[method] !== 'undefined') { - throw new Error('`' + method + '` already declared as static.'); - } - Model[method] = schema.statics[method]; - }); - return Model; - } + function isFloat(n) { + return Number(n) === n && n % 1 !== 0; + } - function arrayArguments(args) { - let a = []; - for (let i = 0, l = args.length; i < l; i++) { - a.push(args[i]); + function valueIsEmpty(v) { + return v === null || + v === undefined || + (typeof v === 'string' && v.trim().length === 0); } - return a; } - - function parseId(id) { - return isFinite(id) ? parseInt(id, 10) : id; +} + +/** + * + * @param self {Model} + * @param schema {Schema} + * @returns {Model} + */ +function applyMethods(self, schema) { + Object.keys(schema.methods).forEach((method) => { + self[method] = schema.methods[method]; + }); + return self; +} + +function applyStatics(self, schema) { + Object.keys(schema.statics).forEach((method) => { + if (typeof self[method] !== 'undefined') { + throw new Error(`${method} already declared as static.`); + } + self[method] = schema.statics[method]; + }); + return self; +} + +function parseId(id) { + return isFinite(id) ? parseInt(id, 10) : id; +} + +function initQuery(self, namespace, transaction) { + if (transaction && transaction.constructor.name !== 'Transaction') { + throw Error('Transaction needs to be a gcloud Transaction'); } - function initQuery(self, namespace, transaction) { - if (transaction && transaction.constructor.name !== 'Transaction') { - throw Error('Transaction needs to be a gcloud Transaction'); - } - let createQueryArgs = [self.entityKind]; + const createQueryArgs = [self.entityKind]; - if (namespace) { - createQueryArgs.unshift(namespace); - } + if (namespace) { + createQueryArgs.unshift(namespace); + } - if (transaction) { - return transaction.createQuery.apply(transaction, createQueryArgs); - } else { - return self.gstore.ds.createQuery.apply(self.gstore.ds, createQueryArgs); - } + if (transaction) { + return transaction.createQuery.apply(transaction, createQueryArgs); } - module.exports = exports = Model; -})(); + return self.gstore.ds.createQuery.apply(self.gstore.ds, createQueryArgs); +} + +// Promisify Model methods +Model.get = utils.promisify(Model.get); +Model.update = utils.promisify(Model.update); +Model.delete = utils.promisify(Model.delete); +Model.list = utils.promisify(Model.list); +Model.deleteAll = utils.promisify(Model.deleteAll); +Model.findAround = utils.promisify(Model.findAround); +Model.findOne = utils.promisify(Model.findOne); +Model.prototype.save = utils.promisify(Model.prototype.save); +module.exports = exports = Model; diff --git a/lib/schema.js b/lib/schema.js index b86a349..bf78fe2 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -1,215 +1,204 @@ -(function() { - 'use strict'; - - var extend = require('extend'); - var utils = require('./utils'); - var Kareem = require('kareem'); - - var GstoreError = require('./error.js'); - var VirtualType = require('./virtualType'); - - class Schema { - constructor(obj, options) { - var self = this; - - this.instanceOfSchema = true; - this.methods = {}; - this.statics = {}; - this.virtuals = {}; - this.shortcutQueries = {}; - this.paths = {}; - this.callQueue = []; - this.options = defaultOptions(options); - - this.s = { - hooks: new Kareem(), - queryHooks: IS_QUERY_HOOK - }; - Object.keys(obj).forEach((k) => { - if (reserved[k]) { - throw new Error('`' + k + '` may not be used as a schema pathname'); - } +'use strict'; + +const extend = require('extend'); +const VirtualType = require('./virtualType'); + +const IS_QUERY_HOOK = { + update: true, + delete: true, + findOne: true, +}; + +const reserved = { + constructor: true, + ds: true, + gstore: true, + entityKey: true, + entityData: true, + className: true, + domain: true, + excludeFromIndexes: true, + emit: true, + on: true, + once: true, + listeners: true, + removeListener: true, + errors: true, + init: true, + isModified: true, + isNew: true, + get: true, + modelName: true, + save: true, + schema: true, + set: true, + toObject: true, + validate: true, + hook: true, + pre: true, + post: true, + removePre: true, + removePost: true, + _pres: true, + _posts: true, + _events: true, + _eventsCount: true, + _lazySetupHooks: true, + _maxListeners: true, +}; + +const defaultMiddleware = [ + /* Validate Schema Middleware */ + { + kind: 'pre', + hook: 'save', + fn() { + const _this = this; + return new Promise((resolve, reject) => ( + // Validate + _this.validate((result) => { + if (!result.success) { + delete result.success; + return reject(result.errors[Object.keys(result.errors)[0]]); + } - // if (!obj[k].hasOwnProperty('type')) { - // obj[k].type = 'string'; - // } + return resolve(); + }) + )); + }, + }, +]; + +class Schema { + constructor(obj, options) { + const self = this; + + this.instanceOfSchema = true; + this.methods = {}; + this.statics = {}; + this.virtuals = {}; + this.shortcutQueries = {}; + this.paths = {}; + this.callQueue = { + model: {}, + entity: {}, + }; + this.options = defaultOptions(options); - self.paths[k] = obj[k]; - }); + Object.keys(obj).forEach((k) => { + if (reserved[k]) { + throw new Error(`${k} is reserved and can not be used as a schema pathname`); + } + + self.paths[k] = obj[k]; + }); - defaultMiddleware.forEach(function(m) { - self[m.kind](m.hook, !!m.isAsync, m.fn); + if (this.options.validateBeforeSave) { + defaultMiddleware.forEach((m) => { + self[m.kind](m.hook, m.fn); }); } + } - /** - * Allow to add custom methods to a Schema - * @param name can be a method name or an object of functions - * @param fn (optional, the function to execute) - */ - method (name, fn) { - let self = this; - if (typeof name !== 'string') { - if (typeof name !== 'object') { - return; - } - Object.keys(name).forEach(k => { - if (typeof name[k] === 'function') { - self.methods[k] = name[k]; - } - }); - } else if (typeof fn === 'function') { - this.methods[name] = fn; + /** + * Allow to add custom methods to a Schema + * @param name can be a method name or an object of functions + * @param fn (optional, the function to execute) + */ + method(name, fn) { + const self = this; + if (typeof name !== 'string') { + if (typeof name !== 'object') { + return; } + Object.keys(name).forEach((k) => { + if (typeof name[k] === 'function') { + self.methods[k] = name[k]; + } + }); + } else if (typeof fn === 'function') { + this.methods[name] = fn; } + } - /** - * Add a default queries settings - * @param type - * @param settings - */ - queries (type, settings) { - this.shortcutQueries[type] = settings; - } - - /** - * Set or Get a path - * @param path - * @param obj - */ - path (path, obj) { - if (typeof obj === 'undefined') { - if (this.paths[path]) { - return this.paths[path]; - } else { - return undefined; - } - } + /** + * Add a default queries settings + * @param type + * @param settings + */ + queries(type, settings) { + this.shortcutQueries[type] = settings; + } - if (reserved[path]) { - throw new Error('`' + path + '` may not be used as a schema pathname'); + /** + * Set or Get a path + * @param path + * @param obj + */ + path(path, obj) { + if (typeof obj === 'undefined') { + if (this.paths[path]) { + return this.paths[path]; } + return undefined; + } - this.paths[path] = obj; - return this; + if (reserved[path]) { + throw new Error(`${path} is reserved and can not be used as a schema pathname`); } - pre () { - var hook = arguments[0]; + this.paths[path] = obj; + return this; + } - if (IS_QUERY_HOOK[hook]) { - this.s.hooks.pre.apply(this.s.hooks, arguments); - return this; - } - return this.queue('pre', arguments); - } + pre(hook, fn) { + const queue = IS_QUERY_HOOK[hook] ? this.callQueue.model : this.callQueue.entity; - post(method, fn) { - if (IS_QUERY_HOOK[method]) { - this.s.hooks.post.apply(this.s.hooks, arguments); - return this; - } - return this.queue('on', [method, fn]); + if (!{}.hasOwnProperty.call(queue, hook)) { + queue[hook] = { + pres: [], + post: [], + }; } - queue(fn, args) { - /** - * TODO: try to put 'save' pre before validate...... - */ - this.callQueue.push([fn, args]); - } + return queue[hook].pres.push(fn); + } - virtual(name) { - if (!this.virtuals.hasOwnProperty(name)) { - this.virtuals[name] = new VirtualType(name); - } - return this.virtuals[name]; + post(hook, fn) { + const queue = IS_QUERY_HOOK[hook] ? this.callQueue.model : this.callQueue.entity; + + if (!{}.hasOwnProperty.call(queue, hook)) { + queue[hook] = { + pres: [], + post: [], + }; } - } - /** - * Merge options passed with the default option for Schemas - * @param options - */ - function defaultOptions(options) { - let optionsDefault = { - validateBeforeSave:true, - queries : { - readAll : false - } - }; - options = extend(true, {}, optionsDefault, options); - return options; + return queue[hook].post.push(fn); } - const defaultMiddleware = [ - /* Validate Schema Middleware */ - { - kind:'pre', - hook:'save', - fn: function(next) { - var shouldValidate = this.schema.options.validateBeforeSave; - // Validate - if (shouldValidate) { - this.validate((result) => { - if (!result.success) { - delete result.success; - next(result.errors[Object.keys(result.errors)[0]]); - } else { - next(); - } - }); - } else { - next(); - } - } + virtual(name) { + if (!{}.hasOwnProperty.call(this.virtuals, name)) { + this.virtuals[name] = new VirtualType(name); } - ]; - const IS_QUERY_HOOK = { - update : true, - delete : true, - findOne : true - }; - - const reserved = { - constructor:true, - ds:true, - gstore:true, - entityKey:true, - entityData:true, - className:true, - domain:true, - excludeFromIndexes:true, - emit:true, - on:true, - once:true, - listeners:true, - removeListener:true, - errors:true, - init:true, - isModified:true, - isNew:true, - get:true, - modelName:true, - save:true, - schema:true, - set:true, - toObject:true, - validate:true, - hook:true, - pre:true, - post:true, - removePre:true, - removePost:true, - _pres:true, - _posts:true, - _events:true, - _eventsCount:true, - _lazySetupHooks:true, - _maxListeners:true + return this.virtuals[name]; + } +} + +/** + * Merge options passed with the default option for Schemas + * @param options + */ +function defaultOptions(options) { + const optionsDefault = { + validateBeforeSave: true, + queries: { + readAll: false, + }, }; + options = extend(true, {}, optionsDefault, options); + return options; +} - module.exports = exports = Schema; -})(); - +module.exports = exports = Schema; diff --git a/lib/serializer.js b/lib/serializer.js index 213deed..ae1841b 100644 --- a/lib/serializer.js +++ b/lib/serializer.js @@ -1,5 +1,5 @@ 'use strict'; -var datastoreSerializer = require('./serializers/datastore'); +const datastoreSerializer = require('./serializers/datastore'); exports.Datastore = datastoreSerializer; diff --git a/lib/serializers/datastore.js b/lib/serializers/datastore.js index 4a07466..bb00a54 100644 --- a/lib/serializers/datastore.js +++ b/lib/serializers/datastore.js @@ -1,16 +1,17 @@ 'use strict'; function toDatastore(obj, nonIndexed) { - nonIndexed = nonIndexed || []; - var results = []; - Object.keys(obj).forEach(function (k) { + nonIndexed = nonIndexed || []; + const results = []; + + Object.keys(obj).forEach((k) => { if (obj[k] === undefined) { return; } results.push({ - name : k, - value : obj[k], - excludeFromIndexes: nonIndexed.indexOf(k) !== -1 + name: k, + value: obj[k], + excludeFromIndexes: nonIndexed.indexOf(k) !== -1, }); }); return results; @@ -18,23 +19,24 @@ function toDatastore(obj, nonIndexed) { function fromDatastore(entity, readAll) { readAll = typeof readAll === 'undefined' ? false : readAll; + const schema = this.schema; const KEY = this.gstore.ds.KEY; const entityKey = entity[KEY]; - let data = { - id: idFromKey(entityKey) + const data = { + id: idFromKey(entityKey), }; data[KEY] = entityKey; Object.keys(entity).forEach((k) => { - if (readAll || !schema.paths.hasOwnProperty(k) || schema.paths[k].read !== false) { + if (readAll || !{}.hasOwnProperty.call(schema.paths, k) || schema.paths[k].read !== false) { data[k] = entity[k]; } }); return data; - ///////// + // ---------------------- function idFromKey(key) { return key.path[key.path.length - 1]; @@ -42,6 +44,6 @@ function fromDatastore(entity, readAll) { } module.exports = { - toDatastore: toDatastore, - fromDatastore: fromDatastore + toDatastore, + fromDatastore, }; diff --git a/lib/utils.js b/lib/utils.js index 47af99f..460a092 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,17 +1,63 @@ 'use strict'; +const is = require('is'); + /** - * Create shallow copy of option - * @param defaultOptions - * @param options + * Wraps a callback style function to conditionally return a promise. + * Utility function taken from the "google-cloud-node library" + * Credits: Dave Gramlich + * + * @param {function} originalMethod - The method to promisify. + * @return {function} wrapped */ -exports.options = function(defaultOptions, options) { - options = options || {}; +const promisify = (originalMethod) => { + if (originalMethod.__promisified) { + return originalMethod; + } + + const wrapper = function wrapper() { + const args = Array.prototype.slice.call(arguments); + const hasCallback = is.fn(args[args.length - 1]); + const context = this; + + // If the only argument passed is a Transaction object, don't return a Promise + const inTransaction = ifSyncTransaction(context, originalMethod); + + if (hasCallback || inTransaction) { + return originalMethod.apply(context, args); + } + + return new Promise((resolve, reject) => { + args.push(function callback() { + const callbackArgs = Array.prototype.slice.call(arguments); + const err = callbackArgs.shift(); + + if (err) { + return reject(err); + } - Object.keys(defaultOptions).forEach((k) => { - if (!options.hasOwnProperty(k)) { - options[k] = defaultOptions[k]; + return resolve(callbackArgs); + }); + + return originalMethod.apply(context, args); + }); + + // ----------------------------- + + function ifSyncTransaction(scope, fn) { + const hasPreHooks = scope.preHooksEnabled !== false && + scope.__pres && + {}.hasOwnProperty.call(scope.__pres, fn.name); + const onlyTransactionArg = !!args[0] && args[0].constructor && args[0].constructor.name === 'Transaction'; + + return !hasPreHooks && onlyTransactionArg; } - }); - return options; + }; + + wrapper.__promisified = true; + return wrapper; +}; + +module.exports = { + promisify, }; diff --git a/lib/virtualType.js b/lib/virtualType.js index 9de3f42..a440052 100644 --- a/lib/virtualType.js +++ b/lib/virtualType.js @@ -1,16 +1,16 @@ 'use strict'; -var is = require('is'); +const is = require('is'); class VirtualType { constructor(name, options) { - this.name = name; - this.getter = null; - this.setter = null; + this.name = name; + this.getter = null; + this.setter = null; this.options = options || {}; } - get (fn) { + get(fn) { if (!is.fn(fn)) { throw new Error('You need to pass a function to virtual get'); } @@ -18,7 +18,7 @@ class VirtualType { return this; } - set (fn) { + set(fn) { if (!is.fn(fn)) { throw new Error('You need to pass a function to virtual set'); } @@ -30,18 +30,18 @@ class VirtualType { if (this.getter === null) { return null; } - let v = this.getter.call(scope); + const v = this.getter.call(scope); scope[this.name] = v; return v; } applySetters(value, scope) { if (this.setter === null) { - return; + return null; } - let v = this.setter.call(scope, value); + const v = this.setter.call(scope, value); return v; } } -module.exports = VirtualType; \ No newline at end of file +module.exports = VirtualType; diff --git a/package.json b/package.json index f371c60..b0a07b5 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,11 @@ { "name": "gstore-node", - "version": "0.7.2", + "version": "0.8.0", "description": "gstore-node is a Google Datastore Entity Models tools", "main": "index.js", "scripts": { + "lint": "./node_modules/eslint/bin/eslint.js ./lib && eslint ./test", + "pretest": "npm run lint", "test": "mocha test --recursive", "coverage": "istanbul cover _mocha -- -R spec --recursive", "coveralls": "istanbul cover _mocha --report lcovonly -- -R spec --recursive && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" @@ -22,28 +24,45 @@ "url": "https://github.com/sebelga/gstore-node.git" }, "author": "Sébastien Loix", + "contributors": [ + { + "name": "Sébastien Loix", + "email": "sabee77@gmail.com", + "url": "https://github.com/sebelga" + }, + { + "name": "Micah Allen", + "url": "https://github.com/micaww" + }, + { + "name": "jfbenckhuijsen", + "url": "https://github.com/jfbenckhuijsen" + } + ], "license": "ISC", "dependencies": { "@google-cloud/datastore": "^0.5.0", "arrify": "^1.0.1", - "async": "^2.0.0-rc.5", "extend": "^3.0.0", - "hooks-fixed": "^1.1.0", "is": "^3.1.0", - "kareem": "^1.1.0", - "moment": "^2.13.0", - "regexp-clone": "0.0.1", - "validator": "^5.2.0" + "moment": "^2.16.0", + "promised-hooks": "^1.1.0", + "validator": "^6.1.0" }, "devDependencies": { "babel-cli": "^6.9.0", "babel-preset-es2015": "^6.9.0", "chai": "^3.5.0", "coveralls": "^2.11.9", + "eslint": "^3.10.0", + "eslint-config-airbnb-base": "^10.0.1", + "eslint-plugin-import": "^2.2.0", + "eslint-plugin-mocha": "^4.7.0", "istanbul": "^0.4.3", - "mocha": "^2.4.5", + "mocha": "^3.1.2", "mocha-lcov-reporter": "^1.2.0", "nconf": "^0.8.4", - "sinon": "^1.17.4" + "sinon": "^1.17.4", + "sinon-as-promised": "^4.0.2" } } diff --git a/test/.eslintrc b/test/.eslintrc new file mode 100644 index 0000000..f22b7c0 --- /dev/null +++ b/test/.eslintrc @@ -0,0 +1,6 @@ +{ + "env": { + "node": true, + "mocha": true + } +} \ No newline at end of file diff --git a/test/entity-test.js b/test/entity-test.js index 2be8556..2d6ad8c 100644 --- a/test/entity-test.js +++ b/test/entity-test.js @@ -1,102 +1,89 @@ -/*jshint -W030 */ 'use strict'; -const chai = require('chai'); -const expect = chai.expect; -const sinon = require('sinon'); -const extend = require('extend'); - +const chai = require('chai'); +const sinon = require('sinon'); const ds = require('./mocks/datastore')(); const datastoreSerializer = require('../lib/serializer').Datastore; const Schema = require('../lib').Schema; -const Model = require('../lib/model'); const gstore = require('../lib'); + +require('sinon-as-promised'); + +const expect = chai.expect; +const assert = chai.assert; gstore.connect(ds); describe('Entity', () => { - let clock; let schema; let ModelInstance; - beforeEach(function() { - clock = sinon.useFakeTimers(); - - gstore.models = {}; + beforeEach(() => { + gstore.models = {}; gstore.modelSchemas = {}; - gstore.options = {}; + gstore.options = {}; schema = new Schema({ - name : {type: 'string'}, - lastname: {type:'string'}, - password: {type: 'string', read: false} + name: { type: 'string' }, + lastname: { type: 'string' }, + password: { type: 'string', read: false }, }); - schema.virtual('fullname').get(function() { - return this.name + ' ' + this.lastname; + schema.virtual('fullname').get(function getFullName() { + return `${this.name} ${this.lastname}`; }); - schema.virtual('fullname').set(function(name) { - var split = name.split(' '); - this.name = split[0]; + schema.virtual('fullname').set(function setFullName(name) { + const split = name.split(' '); + this.name = split[0]; this.lastname = split[1]; }); ModelInstance = gstore.model('User', schema); - sinon.stub(ds, 'save', (entity, cb) => { - cb(null, entity); - }); + sinon.stub(ds, 'save').resolves(); }); - afterEach(function() { + afterEach(() => { ds.save.restore(); - clock.restore(); }); - describe('intantiate', function() { - it('should initialized properties', (done) => { - let model = gstore.model('BlogPost', schema); - - let entity = new model({}, 'keyid'); + describe('intantiate', () => { + it('should initialized properties', () => { + const entity = new ModelInstance({}, 'keyid'); - expect(entity.entityData).to.exist; - expect(entity.entityKey).to.exist; - expect(entity.schema).to.exist; + assert.isDefined(entity.entityData); + assert.isDefined(entity.entityKey); + assert.isDefined(entity.schema); + assert.isDefined(entity.pre); + assert.isDefined(entity.post); expect(entity.excludeFromIndexes).deep.equal([]); - expect(entity.pre).to.exist; - expect(entity.post).to.exist; - - done(); }); it('should add data passed to entityData', () => { - let model = gstore.model('BlogPost', schema); - - let entity = new model({name:'John'}); - + const entity = new ModelInstance({ name: 'John' }); expect(entity.entityData.name).to.equal('John'); }); it('should not add any data if nothing is passed', () => { schema = new Schema({ - name : {type: 'string', optional:true} + name: { type: 'string', optional: true }, }); - let model = gstore.model('BlogPost', schema); + ModelInstance = gstore.model('BlogPost', schema); - let entity = new model(); + const entity = new ModelInstance(); expect(Object.keys(entity.entityData).length).to.equal(0); }); - it ('should set default values or null from schema', () => { + it('should set default values or null from schema', () => { schema = new Schema({ - name:{type:'string', default:'John'}, - lastname: {type: 'string'}, - email:{optional:true} + name: { type: 'string', default: 'John' }, + lastname: { type: 'string' }, + email: { optional: true }, }); - let model = gstore.model('BlogPost', schema); + ModelInstance = gstore.model('BlogPost', schema); - let entity = new model({}); + const entity = new ModelInstance({}); expect(entity.entityData.name).equal('John'); expect(entity.entityData.lastname).equal(null); @@ -104,38 +91,37 @@ describe('Entity', () => { }); it('should call handler for default values in gstore.defaultValues constants', () => { - clock.restore(); sinon.spy(gstore.defaultValues, '__handler__'); schema = new Schema({ - createdOn:{type:'dateTime', default:gstore.defaultValues.NOW} + createdOn: { type: 'dateTime', default: gstore.defaultValues.NOW }, }); - let model = gstore.model('BlogPost', schema); - - let entity = new model({}); + ModelInstance = gstore.model('BlogPost', schema); + const entity = new ModelInstance({}); - expect(gstore.defaultValues.__handler__.calledOnce).be.true; + expect(gstore.defaultValues.__handler__.calledOnce).equal(true); + return entity; }); - it ('should not add default to optional properties', () => { + it('should not add default to optional properties', () => { schema = new Schema({ - name:{type:'string'}, - email:{optional:true} + name: { type: 'string' }, + email: { optional: true }, }); - let model = gstore.model('BlogPost', schema); + ModelInstance = gstore.model('BlogPost', schema); - let entity = new model({}); + const entity = new ModelInstance({}); expect(entity.entityData.email).equal(undefined); }); - it ('should its array of excludeFromIndexes', () => { + it('should its array of excludeFromIndexes', () => { schema = new Schema({ - name : {excludeFromIndexes:true}, - lastname: {excludeFromIndexes:true} + name: { excludeFromIndexes: true }, + lastname: { excludeFromIndexes: true }, }); - let model = gstore.model('BlogPost', schema); + ModelInstance = gstore.model('BlogPost', schema); - let entity = new model({name:'John'}); + const entity = new ModelInstance({ name: 'John' }); expect(entity.excludeFromIndexes).deep.equal(['name', 'lastname']); }); @@ -146,7 +132,7 @@ describe('Entity', () => { beforeEach(() => { sinon.spy(ds, 'key'); - Model = gstore.model('BlogPost', schema); + Model = gstore.model('BlogPost', schema); }); afterEach(() => { @@ -154,46 +140,47 @@ describe('Entity', () => { }); it('---> with a full Key (String keyname passed)', () => { - var entity = new Model({}, 'keyid'); + const entity = new Model({}, 'keyid'); expect(entity.entityKey.kind).equal('BlogPost'); expect(entity.entityKey.name).equal('keyid'); }); it('---> with a full Key (String with including numbers)', () => { - var entity = new Model({}, '123:456'); + const entity = new Model({}, '123:456'); expect(entity.entityKey.name).equal('123:456'); }); - it ('---> with a full Key (Integer keyname passed)', () => { - var entity = new Model({}, 123); + it('---> with a full Key (Integer keyname passed)', () => { + const entity = new Model({}, 123); expect(entity.entityKey.id).equal(123); }); - it ('---> with a full Key ("string" Integer keyname passed)', () => { - var entity = new Model({}, '123'); + it('---> with a full Key ("string" Integer keyname passed)', () => { + const entity = new Model({}, '123'); expect(entity.entityKey.id).equal(123); }); - it ('---> throw error is id passed is not string or number', () => { - let fn = () => { - var entity = new Model({}, {}); + it('---> throw error is id passed is not string or number', () => { + const fn = () => { + const entity = new Model({}, {}); + return entity; }; expect(fn).throw(Error); }); it('---> with a partial Key (auto-generated id)', () => { - var model = new Model({}); + const model = new Model({}); expect(model.entityKey.kind).to.deep.equal('BlogPost'); }); it('---> with an ancestor path (auto-generated id)', () => { - var entity = new Model({}, null, ['Parent', 1234]); + const entity = new Model({}, null, ['Parent', 1234]); expect(entity.entityKey.parent.kind).equal('Parent'); expect(entity.entityKey.parent.id).equal(1234); @@ -201,7 +188,7 @@ describe('Entity', () => { }); it('---> with an ancestor path (manual id)', () => { - var entity = new Model({}, 'entityKind', ['Parent', 1234]); + const entity = new Model({}, 'entityKind', ['Parent', 1234]); expect(entity.entityKey.parent.kind).equal('Parent'); expect(entity.entityKey.parent.id).equal(1234); @@ -210,22 +197,23 @@ describe('Entity', () => { }); it('---> with a namespace', () => { - let model = new Model({}, null, null, 'com.otherdomain'); + const model = new Model({}, null, null, 'com.otherdomain'); expect(model.entityKey.namespace).equal('com.otherdomain'); }); it('---> with a gcloud Key', () => { - var key = ds.key('BlogPost', 1234); + const key = ds.key('BlogPost', 1234); - var entity = new Model({}, null, null, null, key); + const entity = new Model({}, null, null, null, key); expect(entity.entityKey).equal(key); }); it('---> throw error if key is not instance of Key', () => { function fn() { - new Model({}, null, null, null, {}); + const entity = new Model({}, null, null, null, {}); + return entity; } expect(fn).to.throw(); @@ -239,102 +227,97 @@ describe('Entity', () => { beforeEach(() => { spyOn = { - fnHookPre: (next) => {next();}, - fnHookPost: () => {} + fnHookPre: () => Promise.resolve(), + fnHookPost: () => Promise.resolve(1234), }; + + sinon.spy(spyOn, 'fnHookPre'); + sinon.spy(spyOn, 'fnHookPost'); + }); + + afterEach(() => { + spyOn.fnHookPost.restore(); + spyOn.fnHookPre.restore(); }); - it('should call pre hooks before saving', (done) => { - var save = sinon.spy(spyOn, 'fnHookPre'); - schema.pre('save', save); - Model = gstore.model('BlogPost', schema); - entity = new Model({name:'John'}); + it('should call pre hooks before saving and override arguments', () => { + schema.pre('save', spyOn.fnHookPre); + Model = gstore.model('BlogPost', schema); + entity = new Model({ name: 'John' }); - entity.save(function() { - done(); + return entity.save().then(() => { + expect(spyOn.fnHookPre.callCount).to.equal(1); }); - - clock.tick(50); - expect(save.callCount).to.equal(1); - save.restore(); }); it('should call pre and post hooks on custom method', () => { - var preNewMethod = sinon.spy(spyOn, 'fnHookPre'); - var postNewMethod = sinon.spy(spyOn, 'fnHookPost'); - schema.method('newmethod', function() { - this.emit('newmethod'); - return true; + schema.method('newmethod', () => Promise.resolve()); + schema.pre('newmethod', spyOn.fnHookPre); + schema.post('newmethod', spyOn.fnHookPost); + Model = gstore.model('BlogPost', schema); + entity = new Model({ name: 'John' }); + + return entity.newmethod().then(() => { + expect(spyOn.fnHookPre.callCount).to.equal(1); + expect(spyOn.fnHookPost.callCount).to.equal(1); }); - schema.pre('newmethod', preNewMethod); - schema.post('newmethod', postNewMethod); - Model = gstore.model('BlogPost', schema); - entity = new Model({name:'John'}); - - entity.newmethod(); - - expect(preNewMethod.callCount).to.equal(1); - expect(postNewMethod.callCount).to.equal(1); - preNewMethod.restore(); - postNewMethod.restore(); }); - it('should call post hooks after saving', () => { - let save = sinon.spy(spyOn, 'fnHookPost'); - schema.post('save', save); - Model = gstore.model('BlogPost', schema); + it('should call post hooks after saving and override resolve', () => { + schema.post('save', spyOn.fnHookPost); + Model = gstore.model('BlogPost', schema); entity = new Model({}); - entity.save(() => { - expect(spyOn.fnHookPost.called).be.true; - save.restore(); + return entity.save().then((result) => { + expect(spyOn.fnHookPost.called).equal(true); + expect(result).equal(1234); }); }); - it('should not do anything if no hooks on schema', function() { - schema.callQueue = []; - Model = gstore.model('BlogPost', schema); - entity = new Model({name:'John'}); + it('should not do anything if no hooks on schema', () => { + schema.callQueue = { model: {}, entity: {} }; + Model = gstore.model('BlogPost', schema); + entity = new Model({ name: 'John' }); - expect(entity._pres).not.exist; - expect(entity._posts).not.exist; + assert.isUndefined(entity.__pres); + assert.isUndefined(entity.__posts); }); it('should not register unknown methods', () => { - schema.callQueue = []; - schema.pre('unknown', () => {}); - Model = gstore.model('BlogPost', schema); + schema.callQueue = { model: {}, entity: {} }; + schema.pre('unknown', () => { }); + Model = gstore.model('BlogPost', schema); entity = new Model({}); - expect(entity._pres).not.exist; - expect(entity._posts).not.exist; + assert.isUndefined(entity.__pres); + assert.isUndefined(entity.__posts); }); }); }); - describe('get / set', function() { - var user; + describe('get / set', () => { + let user; - beforeEach(function() { - user = new ModelInstance({name:'John', lastname:'Snow'}); + beforeEach(() => { + user = new ModelInstance({ name: 'John', lastname: 'Snow' }); }); - it ('should get an entityData property', function() { - let name = user.get('name'); + it('should get an entityData property', () => { + const name = user.get('name'); expect(name).equal('John'); }); it('should return virtual', () => { - let fullname = user.get('fullname'); + const fullname = user.get('fullname'); expect(fullname).equal('John Snow'); }); - it ('should set an entityData property', function() { + it('should set an entityData property', () => { user.set('name', 'Gregory'); - let name = user.get('name'); + const name = user.get('name'); expect(name).equal('Gregory'); }); @@ -346,12 +329,12 @@ describe('Entity', () => { }); it('should get data on entity properties from the entity data', () => { - let model = gstore.model('BlogPost', schema); + ModelInstance = gstore.model('BlogPost', schema); - let entity = new model({ + const entity = new ModelInstance({ name: 'Jane', lastname: 'Does', - password: 'JanesPassword' + password: 'JanesPassword', }); expect(entity.name).to.equal('Jane'); @@ -360,12 +343,12 @@ describe('Entity', () => { }); it('should reflect changes to entity properties in the entity data', () => { - let model = gstore.model('BlogPost', schema); + ModelInstance = gstore.model('BlogPost', schema); - let entity = new model({ + const entity = new ModelInstance({ name: 'Jane', lastname: 'Does', - password: 'JanesPassword' + password: 'JanesPassword', }); entity.name = 'John'; @@ -378,67 +361,64 @@ describe('Entity', () => { }); }); - describe('plain()', function() { - beforeEach(function() { - sinon.spy(datastoreSerializer, 'fromDatastore') + describe('plain()', () => { + beforeEach(() => { + sinon.spy(datastoreSerializer, 'fromDatastore'); }); - afterEach(function() { + afterEach(() => { datastoreSerializer.fromDatastore.restore(); }); it('should throw an error is options is not of type Object', () => { - let fn = () => { - var model = new ModelInstance({name:'John'}); - let output = model.plain(true); - } + const fn = () => { + const model = new ModelInstance({ name: 'John' }); + model.plain(true); + }; expect(fn).throw(Error); }); it('should call datastoreSerializer "fromDatastore"', () => { - var model = new ModelInstance({name:'John', password:'test'}); - var entityKey = model.entityKey; - var entityData = model.entityData; + const model = new ModelInstance({ name: 'John', password: 'test' }); + const entityData = model.entityData; - let output = model.plain(); + const output = model.plain(); expect(datastoreSerializer.fromDatastore.getCall(0).args[0]).deep.equal(entityData); expect(datastoreSerializer.fromDatastore.getCall(0).args[1]).equal(false); - expect(output.password).not.exist; + assert.isUndefined(output.password); }); it('should call datastoreSerializer "fromDatastore" passing readAll parameter', () => { - var model = new ModelInstance({name:'John', password:'test'}); + const model = new ModelInstance({ name: 'John', password: 'test' }); - let output = model.plain({readAll:true}); + const output = model.plain({ readAll: true }); expect(datastoreSerializer.fromDatastore.getCall(0).args[1]).equal(true); - expect(output.password).exist; + assert.isDefined(output.password); }); it('should add virtuals', () => { - var model = new ModelInstance({name:'John'}); + const model = new ModelInstance({ name: 'John' }); sinon.spy(model, 'addVirtuals'); - let output = model.plain({virtuals:true}); + model.plain({ virtuals: true }); - expect(model.addVirtuals.called).be.true; + expect(model.addVirtuals.called).equal(true); }); - }); - describe('datastoreEntity()', function() { - it('should get the data from the Datastore and merge it into the entity', function() { - let mockData = {name:'John'}; - sinon.stub(ds, 'get', function(key, cb) { - cb(null, mockData); - }); + describe('datastoreEntity()', () => { + it('should get the data from the Datastore and merge it into the entity', () => { + const mockData = { name: 'John' }; + sinon.stub(ds, 'get').resolves([mockData]); - var model = new ModelInstance({}); + const model = new ModelInstance({}); - model.datastoreEntity((err, entity) => { - expect(ds.get.called).be.true; + return model.datastoreEntity().then((result) => { + const entity = result[0]; + expect(ds.get.called).equal(true); expect(ds.get.getCall(0).args[0]).equal(model.entityKey); expect(entity.className).equal('Entity'); expect(entity.entityData).equal(mockData); @@ -447,56 +427,79 @@ describe('Entity', () => { }); }); - it ('should return 404 not found if no entity returned', () => { - sinon.stub(ds, 'get', function(key, cb) { - cb(null); - }); + it('should return 404 not found if no entity returned', () => { + sinon.stub(ds, 'get').resolves([]); - var model = new ModelInstance({}); + const model = new ModelInstance({}); - model.datastoreEntity((err, entity) => { + return model.datastoreEntity().catch((err) => { expect(err.code).equal(404); expect(err.message).equal('Entity not found'); ds.get.restore(); }); }); - it ('should deal with error while fetching the entity', function() { - let error = {code:500, message:'Something went bad'}; - sinon.stub(ds, 'get', function(key, cb) { - cb(error); + it('should return 404 not found if no entity returned (2)', () => { + sinon.stub(ds, 'get').resolves(); + + const model = new ModelInstance({}); + + return model.datastoreEntity().catch((err) => { + expect(err.code).equal(404); + ds.get.restore(); }); + }); + + it('should deal with error while fetching the entity', () => { + const error = { code: 500, message: 'Something went bad' }; + sinon.stub(ds, 'get').rejects(error); - var model = new ModelInstance({}); + const model = new ModelInstance({}); - model.datastoreEntity((err) => { + return model.datastoreEntity().catch((err) => { expect(err).equal(error); ds.get.restore(); }); }); + + it('should still work with a callback', () => { + const mockData = { name: 'John' }; + sinon.stub(ds, 'get').resolves([mockData]); + + const model = new ModelInstance({}); + + return model.datastoreEntity((err, entity) => { + expect(ds.get.called).equal(true); + expect(ds.get.getCall(0).args[0]).equal(model.entityKey); + expect(entity.className).equal('Entity'); + expect(entity.entityData).equal(mockData); + + ds.get.restore(); + }); + }); }); describe('model()', () => { it('should be able to return model instances', () => { - let imageSchema = new Schema({}); - let ImageModel = gstore.model('Image', imageSchema); + const imageSchema = new Schema({}); + const ImageModel = gstore.model('Image', imageSchema); - let blog = new ModelInstance({}); + const blog = new ModelInstance({}); expect(blog.model('Image')).equal(ImageModel); }); it('should be able to execute methods from other model instances', () => { - let imageSchema = new Schema({}); - let ImageModel = gstore.model('Image', imageSchema); - let mockEntities = [{key : ds.key(['BlogPost', 1234])}]; + const imageSchema = new Schema({}); + const ImageModel = gstore.model('Image', imageSchema); + const mockEntities = [{ key: ds.key(['BlogPost', 1234]) }]; sinon.stub(ImageModel, 'get', (cb) => { cb(null, mockEntities[0]); }); - let blog = new ModelInstance({}); + const blog = new ModelInstance({}); blog.model('Image').get((err, entity) => { expect(entity).equal(mockEntities[0]); @@ -505,25 +508,25 @@ describe('Entity', () => { }); describe('addVirtuals()', () => { - var model; - var User; + let model; + let User; beforeEach(() => { - var schema = new Schema({firstname:{}, lastname:{}}); + schema = new Schema({ firstname: {}, lastname: {} }); - schema.virtual('fullname').get(function() { - return this.firstname + ' ' + this.lastname; + schema.virtual('fullname').get(function getFullName() { + return `${this.firstname} ${this.lastname}`; }); - schema.virtual('fullname').set(function(name) { - let split = name.split(' '); + schema.virtual('fullname').set(function setFullName(name) { + const split = name.split(' '); this.firstname = split[0]; - this.lastname = split[1]; + this.lastname = split[1]; }); User = gstore.model('Client', schema); - model = new User({firstname:'John', lastname:'Snow'}); + model = new User({ firstname: 'John', lastname: 'Snow' }); }); it('should create virtual (get) setting scope to entityData', () => { @@ -533,14 +536,14 @@ describe('Entity', () => { }); it('should Not override', () => { - model = new User({firstname:'John', lastname:'Snow', fullname:'Jooohn'}); + model = new User({ firstname: 'John', lastname: 'Snow', fullname: 'Jooohn' }); model.addVirtuals(); expect(model.entityData.fullname).equal('Jooohn'); }); it('should read and parse virtual (set)', () => { - model = new User({fullname:'John Snow'}); + model = new User({ fullname: 'John Snow' }); model.addVirtuals(); @@ -549,7 +552,7 @@ describe('Entity', () => { }); it('should override existing', () => { - model = new User({firstname:'Peter', fullname:'John Snow'}); + model = new User({ firstname: 'Peter', fullname: 'John Snow' }); model.addVirtuals(); diff --git a/test/error-test.js b/test/error-test.js index 8e99af6..3c212d9 100644 --- a/test/error-test.js +++ b/test/error-test.js @@ -1,25 +1,97 @@ -var chai = require('chai'); -var expect = chai.expect; +'use strict'; -var GstoreError = require('../lib/error'); +const chai = require('chai'); +const gstore = require('../'); +const GstoreError = require('../lib/error').GstoreError; +const Model = require('../lib/model'); +const Schema = require('../lib/schema'); -describe('Datastools Errors', () => { - "use strict"; +const ValidationError = GstoreError.ValidationError; +const ValidatorError = GstoreError.ValidatorError; +const expect = chai.expect; +const assert = chai.assert; - it ('should extend Error', () => { +describe('Datastools Errors', () => { + it('should extend Error', () => { expect(GstoreError.prototype.name).equal('Error'); }); it('should set properties in constructor', () => { - var error = new GstoreError('Something went wrong'); + const error = new GstoreError('Something went wrong'); expect(error.message).equal('Something went wrong'); expect(error.name).equal('GstoreError'); }); it('should have static errors', () => { - expect(GstoreError.ValidationError).exist; - expect(GstoreError.ValidatorError).exist; + 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', + }; + const error = new ValidationError(errorData); + + expect(error.message).equal(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).equal(`${entityKind} validation failed`); + }); + + it('should return "Validation failed" if called without param', () => { + const error = new ValidationError(); + + 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/error/validation-test.js b/test/error/validation-test.js deleted file mode 100644 index 5e3fbb3..0000000 --- a/test/error/validation-test.js +++ /dev/null @@ -1,41 +0,0 @@ -var chai = require('chai'); -var expect= chai.expect; - -var gstore = require('../../'); -var Model = require('../../lib/model'); -var Schema = require('../../lib/schema'); -var ValidationError = require('../../lib/error/validation'); - -describe('ValidationError', () => { - "use strict"; - - it('should extend Error', () => { - expect(ValidationError.prototype.name).equal('Error'); - }); - - it('should return error data passed in param', () => { - let errorData = { - code : 400, - message: 'Something went really bad' - }; - let error = new ValidationError(errorData); - - expect(error.message).equal(errorData); - }); - - it('should return "{entityKind} validation failed" if called with entity instance', () => { - let entityKind = 'Blog'; - let schema = new Schema({}); - let ModelInstance = Model.compile(entityKind, schema, gstore); - let model = new ModelInstance({}); - let error = new ValidationError(model); - - expect(error.message).equal(entityKind + ' validation failed'); - }); - - it('should return "Validation failed" if called without param', () => { - let error = new ValidationError(); - - expect(error.message).equal('Validation failed'); - }); -}); diff --git a/test/error/validator-test.js b/test/error/validator-test.js deleted file mode 100644 index e2637ac..0000000 --- a/test/error/validator-test.js +++ /dev/null @@ -1,40 +0,0 @@ -var chai = require('chai'); -var expect= chai.expect; - -var ValidatorError = require('../../lib/error/validator'); - -describe('ValidatorError', () => { - "use strict"; - - it('should extend Error', () => { - expect(ValidatorError.prototype.name).equal('Error'); - }); - - it('should return error data passed in param', () => { - let errorData = { - code : 400, - message: 'Something went really bad' - }; - let 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', () => { - let errorData = { - code : 400, - errorName: 'Required', - message: 'Something went really bad' - }; - let error = new ValidatorError(errorData); - - expect(error.message.errorName).equal(errorData.errorName); - }); - - it('should return "Validation failed" if called without param', () => { - let error = new ValidatorError(); - - expect(error.message).equal('Value validation failed'); - }); -}); diff --git a/test/helpers/defaultValues.js b/test/helpers/defaultValues.js index 9414c19..01bfbe3 100644 --- a/test/helpers/defaultValues.js +++ b/test/helpers/defaultValues.js @@ -1,28 +1,27 @@ +'use strict'; -const chai = require('chai'); -const expect = chai.expect; -const sinon = require('sinon'); +const chai = require('chai'); const defaultValues = require('../../lib/helpers/defaultValues'); -describe('Query Helpers', () => { - "use strict"; +const expect = chai.expect; +describe('Query Helpers', () => { describe('defaultValues constants handler()', () => { it('should return the current time', () => { - let value = defaultValues.NOW; - let result = defaultValues.__handler__(value); + const value = defaultValues.NOW; + const result = defaultValues.__handler__(value); /** * we might have a slightly difference, that's ok :) */ - let dif = Math.abs(result.getTime() - new Date().getTime()); + const dif = Math.abs(result.getTime() - new Date().getTime()); - expect(dif).to.be.below(10); + expect(dif).to.be.below(100); }); it('should return null if value passed not in map', () => { - let value = 'DOES_NOT_EXIST'; - let result = defaultValues.__handler__(value); + const value = 'DOES_NOT_EXIST'; + const result = defaultValues.__handler__(value); expect(result).equal(null); }); diff --git a/test/helpers/queryhelpers-test.js b/test/helpers/queryhelpers-test.js index bcba780..225f273 100644 --- a/test/helpers/queryhelpers-test.js +++ b/test/helpers/queryhelpers-test.js @@ -1,12 +1,13 @@ -const chai = require('chai'); -const expect = chai.expect; -const sinon = require('sinon'); -const ds = require('@google-cloud/datastore')(); +'use strict'; +const chai = require('chai'); +const sinon = require('sinon'); +const ds = require('@google-cloud/datastore')(); const queryHelpers = require('../../lib/helper').QueryHelpers; +const expect = chai.expect; + describe('Query Helpers', () => { - "use strict"; let query; describe('should build a Query from options', () => { @@ -14,20 +15,20 @@ describe('Query Helpers', () => { query = ds.createQuery(); }); - it ('and throw error if no query passed', () => { - let fn = () => {queryHelpers.buildFromOptions();}; + it('and throw error if no query passed', () => { + const fn = () => { queryHelpers.buildFromOptions(); }; expect(fn).to.throw(Error); }); - it ('and throw error if query is not a gcloud Query', () => { - let fn = () => {queryHelpers.buildFromOptions({});}; + it('and throw error if query is not a gcloud Query', () => { + const fn = () => { queryHelpers.buildFromOptions({}); }; expect(fn).to.throw(Error); }); - it ('and not modify query if no options passed', () => { - let originalQuery = {}; + it('and not modify query if no options passed', () => { + const originalQuery = {}; Object.keys(query).forEach((k) => { originalQuery[k] = query[k]; }); @@ -40,12 +41,12 @@ describe('Query Helpers', () => { expect(query.selectVal).deep.equal(originalQuery.selectVal); }); - it ('and update query', () => { - let options = { - limit : 10, - order : {property:'name', descending:true}, - filters : [], - select : 'name' + it('and update query', () => { + const options = { + limit: 10, + order: { property: 'name', descending: true }, + filters: [], + select: 'name', }; query = queryHelpers.buildFromOptions(query, options); @@ -57,9 +58,9 @@ describe('Query Helpers', () => { expect(query.selectVal).deep.equal(['name']); }); - it ('and allow order on serveral properties', () => { - let options = { - order : [{property:'name', descending:true}, {property:'age'}] + it('and allow order on serveral properties', () => { + const options = { + order: [{ property: 'name', descending: true }, { property: 'age' }], }; query = queryHelpers.buildFromOptions(query, options); @@ -67,9 +68,9 @@ describe('Query Helpers', () => { expect(query.orders.length).equal(2); }); - it ('and allow select to be an Array', () => { - let options = { - select : ['name', 'lastname', 'email'] + it('and allow select to be an Array', () => { + const options = { + select: ['name', 'lastname', 'email'], }; query = queryHelpers.buildFromOptions(query, options, ds); @@ -78,8 +79,8 @@ describe('Query Helpers', () => { }); it('and update hasAncestor in query', () => { - let options = { - ancestors: ['Parent', 1234] + const options = { + ancestors: ['Parent', 1234], }; query = queryHelpers.buildFromOptions(query, options, ds); @@ -89,21 +90,21 @@ describe('Query Helpers', () => { expect(query.filters[0].val.id).equal(1234); }); - it ('and throw Error if no Datastore instance passed when passing ancestors', () => { - let options = { - ancestors: ['Parent', 123] + it('and throw Error if no Datastore instance passed when passing ancestors', () => { + const options = { + ancestors: ['Parent', 123], }; - let fn = () => { + const fn = () => { query = queryHelpers.buildFromOptions(query, options); }; expect(fn).to.throw(Error); }); - it ('and define one filter', () => { - let options = { - filters: ['name', '=', 'John'] + it('and define one filter', () => { + const options = { + filters: ['name', '=', 'John'], }; query = queryHelpers.buildFromOptions(query, options, ds); @@ -114,9 +115,9 @@ describe('Query Helpers', () => { expect(query.filters[0].val).equal('John'); }); - it ('and define several filters', () => { - let options = { - filters: [['name', '=', 'John'], ['lastname', 'Snow'], ['age', '<', 30]] + it('and define several filters', () => { + const options = { + filters: [['name', '=', 'John'], ['lastname', 'Snow'], ['age', '<', 30]], }; query = queryHelpers.buildFromOptions(query, options, ds); @@ -129,22 +130,22 @@ describe('Query Helpers', () => { }); it('and execute a function in a filter value, without modifying the filters Array', () => { - let spy = sinon.spy(); - let options = { - filters: [['modifiedOn', '<', spy]] + const spy = sinon.spy(); + const options = { + filters: [['modifiedOn', '<', spy]], }; query = queryHelpers.buildFromOptions(query, options, ds); - expect(spy.calledOnce).be.true; + expect(spy.calledOnce).equal(true); expect(options.filters[0][2]).to.equal(spy); }); - it ('and throw error if wrong format for filters', () => { - let options = { - filters: 'name' + it('and throw error if wrong format for filters', () => { + const options = { + filters: 'name', }; - let fn = () => { + const fn = () => { query = queryHelpers.buildFromOptions(query, options, ds); }; @@ -152,8 +153,8 @@ describe('Query Helpers', () => { }); it('and add start cursor', () => { - let options = { - start: 'abcdef' + const options = { + start: 'abcdef', }; query = queryHelpers.buildFromOptions(query, options, ds); diff --git a/test/index-test.js b/test/index-test.js index 7409417..4a117f2 100644 --- a/test/index-test.js +++ b/test/index-test.js @@ -1,27 +1,27 @@ -/*jshint -W030 */ -const chai = require('chai'); -const expect = chai.expect; -const sinon = require('sinon'); +'use strict'; + +const chai = require('chai'); +const sinon = require('sinon'); const gstore = require('../lib'); -const Model = require('../lib/model'); -const pkg = require('../package.json'); +const pkg = require('../package.json'); + +const expect = chai.expect; +const assert = chai.assert; const ds = require('@google-cloud/datastore')({ - namespace : 'com.mydomain', - apiEndpoint: 'http://localhost:8080' + namespace: 'com.mydomain', + apiEndpoint: 'http://localhost:8080', }); -describe('Datastools', function() { - "use strict"; - +describe('gstore-node', () => { let schema; it('should initialized its properties', () => { - expect(gstore.models).to.exist; - expect(gstore.modelSchemas).to.exist; - expect(gstore.options).to.exist; - expect(gstore.Schema).to.exist; + assert.isDefined(gstore.models); + assert.isDefined(gstore.modelSchemas); + assert.isDefined(gstore.options); + assert.isDefined(gstore.Schema); }); it('should save ds instance', () => { @@ -29,8 +29,8 @@ describe('Datastools', function() { expect(gstore.ds).to.equal(ds); }); - it('should throw an error if ds passed on connect is not a Datastore instance', function() { - let fn = () => { + it('should throw an error if ds passed on connect is not a Datastore instance', () => { + const fn = () => { gstore.connect({}); }; @@ -40,94 +40,90 @@ describe('Datastools', function() { describe('should create models', () => { beforeEach(() => { schema = new gstore.Schema({ - title : {type:'string'} + title: { type: 'string' }, }); - gstore.models = {}; + gstore.models = {}; gstore.modelSchemas = {}; - gstore.options = {}; + gstore.options = {}; }); it('and add it with its schema to the cache', () => { - var Model = gstore.model('Blog', schema); + const Model = gstore.model('Blog', schema); - expect(Model).to.exist; - expect(gstore.models.Blog).to.exist; - expect(gstore.modelSchemas.Blog).to.exist; + assert.isDefined(Model); + assert.isDefined(gstore.models.Blog); + assert.isDefined(gstore.modelSchemas.Blog); }); it('and convert schema object to Schema class instance', () => { schema = {}; - var Model = gstore.model('Blog', schema); + const Model = gstore.model('Blog', schema); expect(Model.schema.constructor.name).to.equal('Schema'); }); it('and attach schema to compiled Model', () => { - let Blog = gstore.model('Blog', schema); - let schemaUser = new gstore.Schema({name: {type: 'string'}}); - let User = gstore.model('User', schemaUser); + const Blog = gstore.model('Blog', schema); + const schemaUser = new gstore.Schema({ name: { type: 'string' } }); + const User = gstore.model('User', schemaUser); expect(Blog.schema).not.equal(User.schema); }); it('and not add them to cache if told so', () => { - let options = {cache:false}; + const options = { cache: false }; gstore.model('Image', schema, options); - expect(gstore.models.Image).be.undefined; + assert.isUndefined(gstore.models.Image); }); - it ('reading them from cache', () => { - let mockModel = {schema: schema}; + it('reading them from cache', () => { + const mockModel = { schema }; gstore.models.Blog = mockModel; - let model = gstore.model('Blog', schema); + const model = gstore.model('Blog', schema); expect(model).equal(mockModel); }); - it ('allowing to pass an existing Schema', () => { + it('allowing to pass an existing Schema', () => { gstore.modelSchemas.Blog = schema; - let model = gstore.model('Blog', schema); + const model = gstore.model('Blog', schema); expect(model.schema).to.equal(schema); }); - it ('and throw error if trying to override schema', () => { - let newSchema = new gstore.Schema({}); - let mockModel = {schema: schema}; + it('and throw error if trying to override schema', () => { + const newSchema = new gstore.Schema({}); + const mockModel = { schema }; gstore.models.Blog = mockModel; - let fn = () => { - return gstore.model('Blog', newSchema); - }; + const fn = () => gstore.model('Blog', newSchema); expect(fn).to.throw(Error); }); - it ('and throw error if no Schema is passed', () => { - let fn = () => { - return gstore.model('Blog'); - }; + it('and throw error if no Schema is passed', () => { + const fn = () => gstore.model('Blog'); expect(fn).to.throw(Error); }); }); it('should return the models names', () => { - gstore.models = {Blog:{}, Image:{}}; + gstore.models = { Blog: {}, Image: {} }; - let names = gstore.modelNames(); + const names = gstore.modelNames(); expect(names).eql(['Blog', 'Image']); }); it('should return the package version', () => { - let version = pkg.version; + const version = pkg.version; expect(gstore.version).equal(version); }); @@ -142,9 +138,9 @@ describe('Datastools', function() { gstore.connect(ds); sinon.spy(ds, 'transaction'); - var transaction = gstore.transaction(); + const transaction = gstore.transaction(); - expect(ds.transaction.called).be.true; + expect(ds.transaction.called).equal(true); expect(transaction.constructor.name).equal('Transaction'); }); }); diff --git a/test/mocks/datastore.js b/test/mocks/datastore.js index d28b476..00dcd95 100644 --- a/test/mocks/datastore.js +++ b/test/mocks/datastore.js @@ -1,71 +1,68 @@ 'use strict'; -const is = require('is'); -let googleDatastore; +const ds = require('@google-cloud/datastore'); class Datastore { - + constructor(options) { - googleDatastore = require('@google-cloud/datastore')(options); + this.googleDatastore = ds(options); } key(options) { - return googleDatastore.key(options); + return this.googleDatastore.key(options); } - save(entity, cb) { - return cb(null); + save() { + return Promise.resolve(this); } - get(entityKey, cb) { - return cb(null); + get() { + return Promise.resolve(this); } - delete(key, cb) { - return cb(null); + delete() { + return Promise.resolve(this); } createQuery() { - return googleDatastore.createQuery.apply(googleDatastore, arguments); + return this.googleDatastore.createQuery.apply(this.googleDatastore, arguments); } - runQuery(namespace, query, cb) { - return cb(null, [], {moreResults : 'MORE_RESULT'}); + runQuery() { + return Promise.resolve([[], { moreResults: 'MORE_RESULT', __ref: this }]); } transaction() { - return {}; + return { __ref: this }; } int() { - return googleDatastore.int.apply(googleDatastore, arguments); + return this.googleDatastore.int.apply(this.googleDatastore, arguments); } double() { - return googleDatastore.double.apply(googleDatastore, arguments); + return this.googleDatastore.double.apply(this.googleDatastore, arguments); } geoPoint() { - return googleDatastore.geoPoint.apply(googleDatastore, arguments); + return this.googleDatastore.geoPoint.apply(this.googleDatastore, arguments); } get MORE_RESULTS_AFTER_LIMIT() { - return googleDatastore.MORE_RESULTS_AFTER_LIMIT; + return this.googleDatastore.MORE_RESULTS_AFTER_LIMIT; } get MORE_RESULTS_AFTER_CURSOR() { - return googleDatastore.MORE_RESULTS_AFTER_CURSOR; + return this.googleDatastore.MORE_RESULTS_AFTER_CURSOR; } get NO_MORE_RESULTS() { - return googleDatastore.NO_MORE_RESULTS; + return this.googleDatastore.NO_MORE_RESULTS; } get KEY() { - return googleDatastore.KEY; + return this.googleDatastore.KEY; } } -module.exports = function(options) { - return new Datastore(options); -} \ No newline at end of file +module.exports = options => new Datastore(options); diff --git a/test/mocks/query.js b/test/mocks/query.js new file mode 100644 index 0000000..85487c7 --- /dev/null +++ b/test/mocks/query.js @@ -0,0 +1,28 @@ +'use strict'; + +class Query { + constructor(ds, mocks, info) { + this.ds = ds; + this.mocks = mocks; + this.info = info; + } + run() { + const info = this.info || { + moreResults: this.ds.MORE_RESULTS_AFTER_LIMIT, + endCursor: 'abcdef', + }; + return Promise.resolve([this.mocks.entities, info]); + } + + limit() { return this; } + + order() { return this; } + + filter() { return this; } + + select() { return this; } + + hasAncestor(ancestors) { this.ancestors = ancestors; } +} + +module.exports = Query; diff --git a/test/mocks/transaction.js b/test/mocks/transaction.js new file mode 100644 index 0000000..7e2fc54 --- /dev/null +++ b/test/mocks/transaction.js @@ -0,0 +1,18 @@ +'use strict'; + +function Transaction() { + const _this = this; + this.run = () => { }; + this.get = () => { }; + this.save = () => { }; + this.delete = () => { }; + this.commit = () => Promise.resolve(); + this.rollback = () => Promise.resolve(); + this.createQuery = () => ({ + filter: () => { }, + scope: _this, + }); + this.runQuery = () => Promise.resolve(); +} + +module.exports = Transaction; diff --git a/test/model-test.js b/test/model-test.js index b595d46..1b283b5 100644 --- a/test/model-test.js +++ b/test/model-test.js @@ -1,193 +1,147 @@ -/*jshint -W030 */ + 'use strict'; -const chai = require('chai'); +const chai = require('chai'); +const sinon = require('sinon'); +const is = require('is'); + +require('sinon-as-promised'); + const expect = chai.expect; -const sinon = require('sinon'); -const async = require('async'); -const is = require('is'); +const assert = chai.assert; const ds = require('./mocks/datastore')({ - namespace : 'com.mydomain' + namespace: 'com.mydomain', }); -const gstore = require('../'); -const Model = require('../lib/model'); -const Entity = require('../lib/entity'); -const Schema = require('../lib').Schema; +const Transaction = require('./mocks/transaction'); +const Query = require('./mocks/query'); + +const gstore = require('../'); +const Model = require('../lib/model'); +const Entity = require('../lib/entity'); +const Schema = require('../lib').Schema; const datastoreSerializer = require('../lib/serializer').Datastore; -const queryHelpers = require('../lib/helper').QueryHelpers; - -describe('Model', function() { - var schema; - var ModelInstance; - var clock; - var mockEntity; - var mockEntities; - var transaction; - - beforeEach('Before each Model (global)', function() { - gstore.models = {}; +const queryHelpers = require('../lib/helper').QueryHelpers; + +describe('Model', () => { + let schema; + let ModelInstance; + let mockEntity; + let mockEntities; + let transaction; + + beforeEach('Before each Model (global)', () => { + gstore.models = {}; gstore.modelSchemas = {}; - gstore.options = {}; + gstore.options = {}; gstore.connect(ds); - clock = sinon.useFakeTimers(); - schema = new Schema({ - name: {type: 'string'}, - lastname: {type: 'string', excludeFromIndexes:true}, - password: {read:false}, - age: {type: 'int', excludeFromIndexes:true}, - birthday: {type: 'datetime'}, - street: {}, - website: {validate: 'isURL'}, - email: {validate: 'isEmail'}, - modified: {type: 'boolean'}, - tags: {type:'array'}, - prefs: {type:'object'}, - price: {type:'double', write:false}, - icon: {type:'buffer'}, - location: {type:'geoPoint'}, - color: {validate:'isHexColor'}, - type: {values:['image', 'video']} - }); - - schema.virtual('fullname').get(function() {}); - - sinon.stub(ds, 'save', (entity, cb) => { - setTimeout(() => { - cb(null ,entity); - }, 20); - }); + name: { type: 'string' }, + lastname: { type: 'string', excludeFromIndexes: true }, + password: { read: false }, + age: { type: 'int', excludeFromIndexes: true }, + birthday: { type: 'datetime' }, + street: {}, + website: { validate: 'isURL' }, + email: { validate: 'isEmail' }, + modified: { type: 'boolean' }, + tags: { type: 'array' }, + prefs: { type: 'object' }, + price: { type: 'double', write: false }, + icon: { type: 'buffer' }, + location: { type: 'geoPoint' }, + color: { validate: 'isHexColor' }, + type: { values: ['image', 'video'] }, + }); + + schema.virtual('fullname').get(() => { }); mockEntity = { - name:'John', - lastname:'Snow', - email:'john@snow.com' + name: 'John', + lastname: 'Snow', + email: 'john@snow.com', }; mockEntity[ds.KEY] = ds.key(['BlogPost', 1234]); - const mockEntity2 = {name: 'John', lastname : 'Snow', password:'xxx'}; + const mockEntity2 = { name: 'John', lastname: 'Snow', password: 'xxx' }; mockEntity2[ds.KEY] = ds.key(['BlogPost', 1234]); - const mockEntit3 = {name: 'Mick', lastname : 'Jagger'}; + const mockEntit3 = { name: 'Mick', lastname: 'Jagger' }; mockEntit3[ds.KEY] = ds.key(['BlogPost', 'keyname']); mockEntities = [mockEntity2, mockEntit3]; - - sinon.stub(ds, 'runQuery', function(namespace, query, cb) { - let args = []; - for (let i = 0; i < arguments.length; i++) { - args.push(arguments[i]); - } - cb = args.pop(); - - //setTimeout(function() { - return cb(null, mockEntities, { - moreResults : ds.MORE_RESULTS_AFTER_LIMIT, - endCursor: 'abcdef' - }); - //}, 20); - }); - - function Transaction() { - var _this = this; - this.run = function(cb) {cb();}; - this.get = function(cb) {cb();}; - this.save = function(cb) {cb();}; - this.delete = function(cb) {cb();}; - this.commit = function(cb) {cb();}; - this.rollback = function(cb) {cb();}; - this.createQuery = function() {return { - filter:() => {}, - scope: _this - }}; - this.runQuery = function() {}; - } transaction = new Transaction(); - sinon.stub(transaction, 'get', (key, cb) => { - //setTimeout(() => { - cb(null, mockEntity); - //}, 20); - }); - - sinon.stub(transaction, 'save', function() { - setTimeout(function() { - return true; - }, 20); - }); - - sinon.spy(transaction, 'run'); + sinon.spy(ds, 'save'); + sinon.stub(ds, 'transaction', () => transaction); + sinon.spy(transaction, 'save'); sinon.spy(transaction, 'commit'); sinon.spy(transaction, 'rollback'); + sinon.stub(transaction, 'get').resolves([mockEntity]); + sinon.stub(transaction, 'run').resolves([transaction, { apiData: 'ok' }]); ModelInstance = gstore.model('Blog', schema, gstore); }); - afterEach(function() { + afterEach(() => { ds.save.restore(); - ds.runQuery.restore(); - transaction.get.restore(); + ds.transaction.restore(); transaction.save.restore(); - transaction.run.restore(); transaction.commit.restore(); transaction.rollback.restore(); }); - describe('compile()', function() { - beforeEach('Reset before compile', function() { - gstore.models = {}; + describe('compile()', () => { + beforeEach('Reset before compile', () => { + gstore.models = {}; gstore.modelSchemas = {}; ModelInstance = gstore.model('Blog', schema); }); it('should set properties on compile and return ModelInstance', () => { - expect(ModelInstance.schema).exist; - expect(ModelInstance.gstore).exist; - expect(ModelInstance.hooks).exist; - expect(ModelInstance.hooks).deep.equal(schema.s.hooks); - expect(ModelInstance.entityKind).exist; - expect(ModelInstance.init).exist; + assert.isDefined(ModelInstance.schema); + assert.isDefined(ModelInstance.gstore); + assert.isDefined(ModelInstance.entityKind); }); it('should create new models classes', () => { - let User = Model.compile('User', new Schema({}), gstore); + const User = Model.compile('User', new Schema({}), gstore); expect(User.entityKind).equal('User'); expect(ModelInstance.entityKind).equal('Blog'); }); it('should execute methods passed to schema.methods', () => { - let imageSchema = new Schema({}); - let ImageModel = gstore.model('Image', imageSchema); + const imageSchema = new Schema({}); + const ImageModel = gstore.model('Image', imageSchema); sinon.stub(ImageModel, 'get', (id, cb) => { cb(null, mockEntities[0]); }); - schema.methods.fullName = function(cb) { - cb(null, this.get('name') + ' ' + this.get('lastname')); + schema.methods.fullName = function fullName(cb) { + return cb(null, `${this.get('name')} ${this.get('lastname')}`); }; - schema.methods.getImage = function(cb) { + schema.methods.getImage = function getImage(cb) { return this.model('Image').get(this.entityData.imageIdx, cb); }; ModelInstance = gstore.model('MyEntity', schema); - var model = new ModelInstance({name:'John', lastname:'Snow'}); + const model = new ModelInstance({ name: 'John', lastname: 'Snow' }); model.fullName((err, result) => { expect(result).equal('John Snow'); }); - model.getImage.call(model, function(err, result) { + model.getImage.call(model, (err, result) => { expect(result).equal(mockEntities[0]); }); }); it('should execute static methods', () => { - let schema = new Schema({}); + schema = new Schema({}); schema.statics.doSomething = () => 123; ModelInstance = gstore.model('MyEntity', schema); @@ -196,10 +150,10 @@ describe('Model', function() { }); it('should throw error is trying to override reserved methods', () => { - let schema = new Schema({}); + schema = new Schema({}); schema.statics.get = () => 123; - let fn = () => gstore.model('MyEntity', schema); + const fn = () => gstore.model('MyEntity', schema); expect(fn).throw(Error); }); @@ -207,17 +161,17 @@ describe('Model', function() { describe('sanitize()', () => { it('should remove keys not "writable"', () => { - let data = {price: 20, unknown:'hello', name:'John'}; + let data = { price: 20, unknown: 'hello', name: 'John' }; data = ModelInstance.sanitize(data); - expect(data.price).not.exist; - expect(data.unknown).not.exist; + assert.isUndefined(data.price); + assert.isUndefined(data.unknown); }); it('should convert "null" string to null', () => { let data = { - name : 'null' + name: 'null', }; data = ModelInstance.sanitize(data); @@ -234,31 +188,30 @@ describe('Model', function() { }); }); - describe('key()', function() { + describe('key()', () => { it('should create from entityKind', () => { - let key = ModelInstance.key(); + const key = ModelInstance.key(); expect(key.path[0]).equal('Blog'); - expect(key.path[1]).not.exist; + assert.isUndefined(key.path[1]); }); it('should parse string id "123" to integer', () => { - let key = ModelInstance.key('123'); - + const key = ModelInstance.key('123'); expect(key.path[1]).equal(123); }); it('should create array of ids', () => { - let keys = ModelInstance.key([22, 69]); + const keys = ModelInstance.key([22, 69]); - expect(is.array(keys)).be.true; + expect(is.array(keys)).equal(true); expect(keys.length).equal(2); expect(keys[1].path[1]).equal(69); }); it('should create array of ids with ancestors and namespace', () => { - let namespace = 'com.mydomain-dev'; - let keys = ModelInstance.key([22, 69], ['Parent', 'keyParent'], namespace); + const namespace = 'com.mydomain-dev'; + const keys = ModelInstance.key([22, 69], ['Parent', 'keyParent'], namespace); expect(keys[0].path[0]).equal('Parent'); expect(keys[0].path[1]).equal('keyParent'); @@ -270,15 +223,9 @@ describe('Model', function() { let entity; beforeEach(() => { - entity = { - key: ds.key(['BlogPost', 123]), - data:{name:'John'} - }; - sinon.stub(ds, 'get', (key, cb) => { - //setTimeout(function() { - return cb(null, entity); - //}, 20); - }); + entity = { name: 'John' }; + entity[ds.KEY] = ds.key(['BlogPost', 123]); + sinon.stub(ds, 'get').resolves([entity]); }); afterEach(() => { @@ -286,331 +233,274 @@ describe('Model', function() { }); it('passing an integer id', () => { - let result; - ModelInstance.get(123, (err, entity) => { - result = entity; - }); + return ModelInstance.get(123).then(onResult); - expect(ds.get.getCall(0).args[0].constructor.name).equal('Key'); - expect(result instanceof Entity).be.true; + function onResult(data) { + entity = data[0]; + expect(ds.get.getCall(0).args[0].constructor.name).equal('Key'); + expect(entity instanceof Entity).equal(true); + } }); - it('passing an string id', () => { - let result; - ModelInstance.get('keyname', (err, res) => {result = res;}); - - expect(result instanceof Entity).be.true; - }); + it('passing an string id', () => ModelInstance.get('keyname').then((data) => { + entity = data[0]; + expect(entity instanceof Entity).equal(true); + })); it('passing an array of ids', () => { ds.get.restore(); - let entity1 = {name:'John'}; + const entity1 = { name: 'John' }; entity1[ds.KEY] = ds.key(['BlogPost', 22]); - let entity2 = {name:'John'}; + const entity2 = { name: 'John' }; entity2[ds.KEY] = ds.key(['BlogPost', 69]); - sinon.stub(ds, 'get', (key, cb) => { - setTimeout(function() { - return cb(null, [entity2, entity1]); // not sorted - }, 20); - }); + sinon.stub(ds, 'get').resolves([[entity2, entity1]]); // not sorted - ModelInstance.get([22, 69], null, null, null, {preserveOrder:true}, (err, res) => { - expect(is.array(ds.get.getCall(0).args[0])).be.true; - expect(is.array(res)).be.true; - expect(res[0].entityKey.id).equal(22); // sorted - }); + return ModelInstance.get([22, 69], null, null, null, { preserveOrder: true }).then(onResult); - clock.tick(20); + function onResult(data) { + entity = data[0]; + expect(is.array(ds.get.getCall(0).args[0])).equal(true); + expect(is.array(entity)).equal(true); + expect(entity[0].entityKey.id).equal(22); // sorted + } }); - it('converting a string integer to real integer', () => { - ModelInstance.get('123', () => {}); - - expect(ds.get.getCall(0).args[0].name).not.exist; + it('converting a string integer to real integer', () => ModelInstance.get('123').then(() => { + assert.isUndefined(ds.get.getCall(0).args[0].name); expect(ds.get.getCall(0).args[0].id).equal(123); - }); - - it('not converting string with mix of number and non number', () => { - ModelInstance.get('123:456', () => {}); + })); + it('not converting string with mix of number and non number', () => ModelInstance.get('123:456').then(() => { expect(ds.get.getCall(0).args[0].name).equal('123:456'); - }); + })); it('passing an ancestor path array', () => { - let ancestors = ['Parent', 'keyname']; - - ModelInstance.get(123, ancestors, (err, result) => {}); + const ancestors = ['Parent', 'keyname']; - expect(ds.get.getCall(0).args[0].constructor.name).equal('Key'); - expect(ds.get.getCall(0).args[0].parent.kind).equal(ancestors[0]); - expect(ds.get.getCall(0).args[0].parent.name).equal(ancestors[1]); + return ModelInstance.get(123, ancestors).then(() => { + expect(ds.get.getCall(0).args[0].constructor.name).equal('Key'); + expect(ds.get.getCall(0).args[0].parent.kind).equal(ancestors[0]); + expect(ds.get.getCall(0).args[0].parent.name).equal(ancestors[1]); + }); }); it('should allow a namespace', () => { - let namespace = 'com.mydomain-dev'; + const namespace = 'com.mydomain-dev'; - ModelInstance.get(123, null, namespace, (err, result) => {}); - - expect(ds.get.getCall(0).args[0].namespace).equal(namespace); + return ModelInstance.get(123, null, namespace).then(() => { + expect(ds.get.getCall(0).args[0].namespace).equal(namespace); + }); }); - it('on datastore get error, should return its error', () => { + it('on datastore get error, should reject error', () => { ds.get.restore(); + const error = { code: 500, message: 'Something went really bad' }; + sinon.stub(ds, 'get').rejects(error); - let error = {code:500, message:'Something went really bad'}; - sinon.stub(ds, 'get', (key, cb) => { - return cb(error); - }); - - ModelInstance.get(123, (err, entity) => { - expect(err).equal(error); - expect(entity).not.exist; - }); + return ModelInstance.get(123) + .catch((err) => { + expect(err).equal(error); + }); }); it('on no entity found, should return a 404 error', () => { ds.get.restore(); - sinon.stub(ds, 'get', (key, cb) => { - return cb(null); - }); + sinon.stub(ds, 'get').resolves([]); - ModelInstance.get(123, (err, entity) => { + return ModelInstance.get(123).catch((err) => { expect(err.code).equal(404); }); }); - it('should get in a transaction', function() { - ModelInstance.get(123, null, null, transaction, function(err, entity) { - expect(transaction.get.called).be.true; - expect(ds.get.called).be.false; - expect(entity.className).equal('Entity'); - }); - }); + it('should get in a transaction', () => ModelInstance.get(123, null, null, transaction).then((data) => { + entity = data[0]; + expect(transaction.get.called).equal(true); + expect(ds.get.called).equal(false); + expect(entity.className).equal('Entity'); + })); - it('should throw error if transaction not an instance of glcoud Transaction', function() { - var fn = function() { - ModelInstance.get(123, null, null, {}, (err, entity) => { - expect(transaction.get.called).be.true; - }); - }; + it('should throw error if transaction not an instance of glcoud Transaction', + () => ModelInstance.get(123, null, null, {}).catch((err) => { + expect(err.message).equal('Transaction needs to be a gcloud Transaction'); + })); - expect(fn).to.throw(Error); - }); - - it('should return error from Transaction.get()', function() { + it('should return error from Transaction.get()', () => { transaction.get.restore(); - var error = {code:500, message: 'Houston we really need you'}; - sinon.stub(transaction, 'get', function(key, cb) { - cb(error); - }); + const error = { code: 500, message: 'Houston we really need you' }; + sinon.stub(transaction, 'get').rejects(error); - ModelInstance.get(123, null, null, transaction, (err, entity) => { + return ModelInstance.get(123, null, null, transaction).catch((err) => { expect(err).equal(error); - expect(entity).not.exist; }); }); - }); - - describe('update()', () => { - beforeEach(function() { - sinon.stub(ds, 'transaction', function(cb, done) { - // return cb(transaction, function() { - // done(); - // }); - return transaction; - }); - }); - - afterEach(() => { - ds.transaction.restore(); - }); - - it('should run in a transaction', function(){ - ModelInstance.update(123, () => {}); - expect(ds.transaction.called).be.true; - expect(transaction.run.called).be.true; - expect(transaction.commit.called).be.true; - }); + it('should still work with a callback', () => { + return ModelInstance.get(123, onResult); - it('should run an entity instance', function(){ - ModelInstance.update(123, (err, entity) => { - expect(entity.className).equal('Entity'); - }); + function onResult(err, result) { + expect(ds.get.getCall(0).args[0].constructor.name).equal('Key'); + expect(result instanceof Entity).equal(true); + } }); + }); - it('should first get the entity by Key', () => { - ModelInstance.update(123, () => {}); - + describe('update()', () => { + it('should run in a transaction', () => ModelInstance.update(123).then(() => { + expect(ds.transaction.called).equal(true); + expect(transaction.run.called).equal(true); + expect(transaction.commit.called).equal(true); + })); + + it('should return an entity instance', () => ModelInstance.update(123).then((data) => { + const entity = data[0]; + expect(entity.className).equal('Entity'); + })); + + it('should first get the entity by Key', () => ModelInstance.update(123).then(() => { expect(transaction.get.getCall(0).args[0].constructor.name).equal('Key'); expect(transaction.get.getCall(0).args[0].path[1]).equal(123); - }); + })); - it('not converting string id with mix of number and alpha chars', () => { - ModelInstance.update('123:456', () => {}); + it('should not convert a string id with mix of number and alpha chars', + () => ModelInstance.update('123:456').then(() => { + expect(transaction.get.getCall(0).args[0].name).equal('123:456'); + })); - expect(transaction.get.getCall(0).args[0].name).equal('123:456'); + it('should return transaction info', () => { + const info = { success: true }; + transaction.commit.restore(); + sinon.stub(transaction, 'commit').resolves([info]); + return ModelInstance.update('123:456').then((result) => { + expect(result[1]).equal(info); + }); }); - it('should rollback if error while getting entity', function(done) { + it('should rollback if error while getting entity', () => { transaction.get.restore(); - let error = {code:500, message:'Houston we got a problem'}; - sinon.stub(transaction, 'get', (key, cb) => { - return cb(error); - }); + const error = { code: 500, message: 'Houston we got a problem' }; + sinon.stub(transaction, 'get').rejects(error); - ModelInstance.update(123, (err) => { - expect(err).equal(error); - expect(transaction.commit.called).be.false; - done(); + return ModelInstance.update(123).catch((err) => { + expect(err).deep.equal(error); + expect(transaction.rollback.called).equal(true); + expect(transaction.commit.called).equal(false); }); }); it('should return 404 if entity not found', () => { transaction.get.restore(); - sinon.stub(transaction, 'get', (key, cb) => { - return cb(null); - }); + sinon.stub(transaction, 'get').resolves([]); - ModelInstance.update('keyname', (err, entity) => { + return ModelInstance.update('keyname').catch((err) => { expect(err.code).equal(404); - expect(entity).not.exist; }); }); - it('should return error if any while saving', (done) => { + it('should return error if any while saving', () => { transaction.run.restore(); - let error = {code:500, message: 'Houston wee need you.'}; - sinon.stub(transaction, 'run', function(cb) { - return cb(error); - }); + const error = { code: 500, message: 'Houston wee need you.' }; + sinon.stub(transaction, 'run').rejects(error); - ModelInstance.update(123, (err) => { + return ModelInstance.update(123).catch((err) => { expect(err).equal(error); - done(); }); - - clock.tick(40); }); it('accept an ancestor path', () => { - let ancestors = ['Parent', 'keyname']; + const ancestors = ['Parent', 'keyname']; - ModelInstance.update(123, {}, ancestors, (err, entity) => { + return ModelInstance.update(123, {}, ancestors).then(() => { expect(transaction.get.getCall(0).args[0].path[0]).equal('Parent'); expect(transaction.get.getCall(0).args[0].path[1]).equal('keyname'); }); }); it('should allow a namespace', () => { - let namespace = 'com.mydomain-dev'; + const namespace = 'com.mydomain-dev'; - ModelInstance.update(123, {}, null, namespace, (err, result) => { + return ModelInstance.update(123, {}, null, namespace).then(() => { expect(transaction.get.getCall(0).args[0].namespace).equal(namespace); }); }); - it('should save and replace data', (done) => { - let data = { - name : 'Mick' + it('should save and replace data', () => { + const data = { + name: 'Mick', }; - ModelInstance.update(123, data, null, null, null, {replace:true}, (err, entity) => { - expect(entity.entityData.name).equal('Mick'); - expect(entity.entityData.lastname).not.exist; - expect(entity.entityData.email).not.exist; - }); - - clock.tick(60); - done(); - }); - - it('should merge the new data with the entity data', (done) => { - let data = { - name : 'Sebas', - lastname : 'Snow' + return ModelInstance.update(123, data, null, null, null, { replace: true }) + .then((result) => { + const entity = result[0]; + expect(entity.entityData.name).equal('Mick'); + expect(entity.entityData.lastname).equal(null); + expect(entity.entityData.email).equal(null); + }); + }); + + it('should merge the new data with the entity data', () => { + const data = { + name: 'Sebas', + lastname: 'Snow', }; - ModelInstance.update(123, data, ['Parent', 'keyNameParent'], (err, entity) => { - expect(entity.entityData.name).equal('Sebas'); - expect(entity.entityData.lastname).equal('Snow'); - expect(entity.entityData.email).equal('john@snow.com'); - }); - - clock.tick(60); - done(); - }); - - it('should call save() on the transaction', (done) => { - ModelInstance.update(123, {}, (err, entity) => {}); - - clock.tick(40); - - expect(transaction.save.called).be.true; - - done(); + return ModelInstance.update(123, data, ['Parent', 'keyNameParent']) + .then((result) => { + const entity = result[0]; + expect(entity.entityData.name).equal('Sebas'); + expect(entity.entityData.lastname).equal('Snow'); + expect(entity.entityData.email).equal('john@snow.com'); + }); }); - it('should return error and rollback transaction if not passing validation', function(done) { - ModelInstance.update(123, {unknown:1}, (err, entity) => { - expect(err).exist; - expect(entity).not.exist; - expect(transaction.rollback.called).be.true; - done(); + it('should call save() on the transaction', () => { + ModelInstance.update(123, {}).then(() => { + expect(transaction.save.called).equal(true); }); - - clock.tick(20); }); - it('should return error if not passing validation', function(done) { - ModelInstance.update(123, {unknown:1}, null, null, null, {replace:true}, (err, entity) => { - expect(err).exist; - expect(entity).not.exist; - done(); - }); + it('should return error and rollback transaction if not passing validation', + () => ModelInstance.update(123, { unknown: 1 }).catch((err) => { + assert.isDefined(err); + expect(transaction.rollback.called).equal(true); + })); - clock.tick(20); - }); + it('should return error if not passing validation', + () => ModelInstance.update(123, { unknown: 1 }, null, null, null, { replace: true }) + .catch((err) => { + assert.isDefined(err); + })); - it('should run inside an EXISTING transaction', () => { - ModelInstance.update(123, {}, null, null, transaction, (err, entity) => { - expect(ds.transaction.called).be.false; - expect(transaction.get.called).be.true; - expect(transaction.save.called).be.true; - expect(entity.className).equal('Entity'); - }); + it('should run inside an EXISTING transaction', () => ModelInstance.update(123, {}, null, null, transaction) + .then((result) => { + const entity = result[0]; + expect(ds.transaction.called).equal(false); + expect(transaction.get.called).equal(true); + expect(transaction.save.called).equal(true); + expect(entity.className).equal('Entity'); + })); - clock.tick(40); - }); - - it('should throw error if transaction passed is not instance of gcloud Transaction', () => { - var fn = function() { - ModelInstance.update(123, {}, null, null, {}, (err, entity) => {}); - }; + it('should throw error if transaction passed is not instance of gcloud Transaction', + () => ModelInstance.update(123, {}, null, null, {}) + .catch((err) => { + expect(err.message).equal('Transaction needs to be a gcloud Transaction'); + })); - expect(fn).to.throw(Error); - }); + it('should set save options "op" to "update" ', () => ModelInstance.update(123, {}).then((result) => { + const info = result[1]; + expect(info.op).equal('update'); + })); - it('should set save options "op" to "update" ', (done) => { - ModelInstance.update(123, {}, (err, entity, info) => { - expect(info.op).equal('update'); - done(); - }); - - clock.tick(40); - }); + it('should still work passing a callback', () => ModelInstance.update(123, (err, entity) => { + expect(entity.className).equal('Entity'); + })); }); describe('delete()', () => { beforeEach(() => { - sinon.stub(ds, 'delete', (key, cb) => { - cb(null, {indexUpdates:3}); - }); - sinon.stub(transaction, 'delete', (key) => { - return true; - }); + sinon.stub(ds, 'delete').resolves([{ indexUpdates: 3 }]); + sinon.stub(transaction, 'delete', () => true); }); afterEach(() => { @@ -618,1305 +508,1338 @@ describe('Model', function() { transaction.delete.restore(); }); - it('should call ds.delete with correct Key (int id)', (done) => { - ModelInstance.delete(123, (err, response) => { - expect(ds.delete.called).be.true; - expect(ds.delete.getCall(0).args[0].constructor.name).equal('Key'); - expect(response.success).be.true; - done(); - }); - }); + it('should call ds.delete with correct Key (int id)', () => ModelInstance.delete(123).then((response) => { + expect(ds.delete.called).equal(true); + expect(ds.delete.getCall(0).args[0].constructor.name).equal('Key'); + expect(response[0]).equal(true); + })); - it('should call ds.delete with correct Key (string id)', (done) => { - ModelInstance.delete('keyName', (err, response) => { - expect(ds.delete.called).be.true; - expect(ds.delete.getCall(0).args[0].path[1]).equal('keyName'); - expect(response.success).be.true; - done(); - }); - }); + it('should call ds.delete with correct Key (string id)', + () => ModelInstance.delete('keyName') + .then((response) => { + expect(ds.delete.called).equal(true); + expect(ds.delete.getCall(0).args[0].path[1]).equal('keyName'); + expect(response[0]).equal(true); + })); - it('not converting string id with mix of number and alpha chars', (done) => { - ModelInstance.delete('123:456', () => { - expect(ds.delete.getCall(0).args[0].name).equal('123:456'); - done(); - }); - }); + it('not converting string id with mix of number and alpha chars', + () => ModelInstance.delete('123:456') + .then(() => { + expect(ds.delete.getCall(0).args[0].name).equal('123:456'); + })); - it('should allow array of ids', (done) => { - ModelInstance.delete([22, 69], (err, response) => { - expect(is.array(ds.delete.getCall(0).args[0])).be.true; - done(); - }); - }); + it('should allow array of ids', () => ModelInstance.delete([22, 69]).then(() => { + expect(is.array(ds.delete.getCall(0).args[0])).equal(true); + })); - it('should allow ancestors', (done) => { - ModelInstance.delete(123, ['Parent', 123], () => { - let key = ds.delete.getCall(0).args[0]; + it('should allow ancestors', () => ModelInstance.delete(123, ['Parent', 123]).then(() => { + const key = ds.delete.getCall(0).args[0]; - expect(key.parent.kind).equal('Parent'); - expect(key.parent.id).equal(123); - done(); - }); - }); + expect(key.parent.kind).equal('Parent'); + expect(key.parent.id).equal(123); + })); - it('should allow a namespace', (done) => { - let namespace = 'com.mydomain-dev'; + it('should allow a namespace', () => { + const namespace = 'com.mydomain-dev'; - ModelInstance.delete('keyName', null, namespace, (err, response) => { - let key = ds.delete.getCall(0).args[0]; + return ModelInstance.delete('keyName', null, namespace).then(() => { + const key = ds.delete.getCall(0).args[0]; expect(key.namespace).equal(namespace); - done(); }); }); - it('should delete entity in a transaction', function(done) { - ModelInstance.delete(123, null, null, transaction, function(err, result) { - expect(transaction.delete.called).be.true; - expect(transaction.delete.getCall(0).args[0].path[1]).equal(123); - done(); + it('should delete entity in a transaction', + () => ModelInstance.delete(123, null, null, transaction) + .then(() => { + expect(transaction.delete.called).equal(true); + expect(transaction.delete.getCall(0).args[0].path[1]).equal(123); + })); + + it('should deal with empty responses', () => { + ds.delete.restore(); + sinon.stub(ds, 'delete').resolves(); + return ModelInstance.delete(1).then((result) => { + assert.isDefined(result[1].key); }); - clock.tick(20); }); - it('should throw error if transaction passed is not instance of gcloud Transaction', () => { - var fn = function() { - ModelInstance.delete(123, null, null, {}, function(err, result) {}); - }; - - clock.tick(20); - - expect(fn).to.throw(Error); + it('should delete entity in a transaction in sync', () => { + ModelInstance.delete(123, null, null, transaction); + expect(transaction.delete.called).equal(true); + expect(transaction.delete.getCall(0).args[0].path[1]).equal(123); }); - it ('should set "success" to false if no entity deleted', () => { + it('should throw error if transaction passed is not instance of gcloud Transaction', + () => ModelInstance.delete(123, null, null, {}) + .catch((err) => { + expect(err.message).equal('Transaction needs to be a gcloud Transaction'); + })); + + it('should set "success" to false if no entity deleted', () => { ds.delete.restore(); - sinon.stub(ds, 'delete', (key, cb) => { - cb(null, {indexUpdates:0}); - }); + sinon.stub(ds, 'delete').resolves([{ indexUpdates: 0 }]); - ModelInstance.delete(123, (err, response) => { - expect(response.success).be.false; + return ModelInstance.delete(123).then((response) => { + expect(response[0]).equal(false); }); }); - it ('should not set success neither apiRes', () => { + it('should not set success neither apiRes', () => { ds.delete.restore(); - sinon.stub(ds, 'delete', (key, cb) => { - cb(null, {}); - }); + sinon.stub(ds, 'delete').resolves([{}]); - ModelInstance.delete(123, (err, response) => { - expect(response.success).not.exist; - expect(response.apiRes).not.exist; + return ModelInstance.delete(123).then((response) => { + assert.isUndefined(response[0]); }); }); - it ('should deal with err response', () => { + it('should deal with err response', () => { ds.delete.restore(); - let error = {code:500, message:'We got a problem Houston'}; - sinon.stub(ds, 'delete', (key, cb) => { - return cb(error); - }); + const error = { code: 500, message: 'We got a problem Houston' }; + sinon.stub(ds, 'delete').rejects(error); - ModelInstance.delete(123, (err, success) => { - expect(err).deep.equal(error); - expect(success).not.exist; + return ModelInstance.delete(123).catch((err) => { + expect(err).equal(error); }); }); it('should call pre hooks', () => { - let spyPre = sinon.spy(); - schema.pre('delete', (next) => { - spyPre(); - next(); - }); + const spy = { + beforeSave: () => Promise.resolve(), + }; + sinon.spy(spy, 'beforeSave'); + schema.pre('delete', spy.beforeSave); ModelInstance = Model.compile('Blog', schema, gstore); - ModelInstance.delete(123, (err, success) => {}); + return ModelInstance.delete(123).then(() => { + expect(spy.beforeSave.calledBefore(ds.delete)).equal(true); + }); + }); - expect(spyPre.calledBefore(ds.delete)).be.true; + it('pre hook should override id passed', () => { + const spy = { + beforeSave: () => Promise.resolve({ __override: [666] }), + }; + sinon.spy(spy, 'beforeSave'); + schema.pre('delete', spy.beforeSave); + ModelInstance = Model.compile('Blog', schema, gstore); + + return ModelInstance.delete(123).then(() => { + expect(ds.delete.getCall(0).args[0].id).equal(666); + }); }); it('should set "pre" hook scope to entity being deleted', () => { - schema.pre('delete', function(next) { + schema.pre('delete', function preDelete() { expect(this.className).equal('Entity'); - next(); + return Promise.resolve(); }); ModelInstance = Model.compile('Blog', schema, gstore); - ModelInstance.delete(123, (err, success) => {}); + return ModelInstance.delete(123); }); - it('should NOT set "pre" hook scope if deleting array of ids', () => { - schema.pre('delete', function(next) { - expect(this).not.exist; - next(); + it('should NOT set "pre" hook scope if deleting an array of ids', () => { + schema.pre('delete', function preDelete() { + expect(this).equal(null); + return Promise.resolve(); }); ModelInstance = Model.compile('Blog', schema, gstore); - ModelInstance.delete([123, 456], (err, success) => {}); + return ModelInstance.delete([123, 456], () => {}); }); - it('should call post hooks', (done) => { - let spyPost = sinon.spy(); - schema.post('delete', () => { - spyPost(); - }); + it('should call post hooks', () => { + const spy = { + afterDelete: () => Promise.resolve(), + }; + sinon.spy(spy, 'afterDelete'); + schema.post('delete', spy.afterDelete); ModelInstance = Model.compile('Blog', schema, gstore); - ModelInstance.delete(123, (err, result) => { - expect(spyPost.called).be.true; - done(); + return ModelInstance.delete(123).then(() => { + expect(spy.afterDelete.called).equal(true); }); }); - it('should pass key deleted to post hooks', (done) => { - schema.post('delete', function(keys) { - expect(keys.constructor.name).equal('Key'); - expect(keys.id).equal(123); - done(); + it('should pass key deleted to post hooks', () => { + schema.post('delete', (result) => { + expect(result[1].key.constructor.name).equal('Key'); + expect(result[1].key.id).equal(123); + return Promise.resolve(); }); ModelInstance = Model.compile('Blog', schema, gstore); - ModelInstance.delete(123, (err, result) => {}); + return ModelInstance.delete(123).then(() => {}); }); - it('should pass array of keys deleted to post hooks', (done) => { - var ids = [123,456]; - schema.post('delete', function(keys) { - expect(keys.length).equal(ids.length); - expect(keys[1].id).equal(456); - done(); + it('should pass array of keys deleted to post hooks', () => { + const ids = [123, 456]; + schema.post('delete', (response) => { + expect(response[1].key.length).equal(ids.length); + expect(response[1].key[1].id).equal(456); + return Promise.resolve(); }); ModelInstance = Model.compile('Blog', schema, gstore); - ModelInstance.delete(ids, (err, result) => {}); + return ModelInstance.delete(ids).then(() => { }); }); - it('transaction.execPostHooks() should call post hooks', (done) => { - let spyPost = sinon.spy(); - schema = new Schema({name:{type:'string'}}); - schema.post('delete', spyPost); + it('transaction.execPostHooks() should call post hooks', () => { + const spy = { + afterDelete: () => Promise.resolve(), + }; + sinon.spy(spy, 'afterDelete'); + schema = new Schema({ name: { type: 'string' } }); + schema.post('delete', spy.afterDelete); ModelInstance = Model.compile('Blog', schema, gstore); - ModelInstance.delete(123, null, null, transaction, (err, result) => { - transaction.execPostHooks(); - expect(spyPost.called).be.true; - done(); + return ModelInstance.delete(123, null, null, transaction).then(() => { + transaction.execPostHooks().then(() => { + expect(spy.afterDelete.called).equal(true); + expect(spy.afterDelete.calledOnce).equal(true); + }); + }); + }); + + it('should still work passing a callback', () => { + ModelInstance.delete('keyName', (err, success) => { + expect(ds.delete.called).equal(true); + expect(ds.delete.getCall(0).args[0].path[1]).equal('keyName'); + expect(success).equal(true); }); }); }); - describe('hooksTransaction()', function() { + describe('hooksTransaction()', () => { + beforeEach(() => { + delete transaction.hooks; + }); + it('should add hooks to a transaction', () => { - ModelInstance.hooksTransaction(transaction); + ModelInstance.hooksTransaction(transaction, [() => { }, () => { }]); - expect(transaction.hooks).exist; - expect(transaction.hooks.post.length).equal(0); - expect(transaction.addHook).exist; - expect(transaction.execPostHooks).exist; + assert.isDefined(transaction.hooks.post); + expect(transaction.hooks.post.length).equal(2); + assert.isDefined(transaction.execPostHooks); }); - it ('should not override hooks on transition', function() { - var hooks = {post:[]}; - transaction.hooks = hooks; + it('should not override previous hooks on transaction', () => { + const fn = () => { }; + transaction.hooks = { + post: [fn], + }; + ModelInstance.hooksTransaction(transaction, [() => { }]); - ModelInstance.hooksTransaction(transaction); + expect(transaction.hooks.post[0]).equal(fn); + }); - expect(transaction.hooks).equal(hooks); - }) + it('--> execPostHooks() should chain each Promised hook from transaction', () => { + const postHook1 = sinon.stub().resolves(1); + const postHook2 = sinon.stub().resolves(2); + ModelInstance.hooksTransaction(transaction, [postHook1, postHook2]); - it('--> execPostHooks() should call each post hook on transaction', () => { - let spy = sinon.spy(); - ModelInstance.hooksTransaction(transaction); - transaction.hooks.post = [spy, spy]; + return transaction.execPostHooks().then((result) => { + expect(postHook1.called).equal(true); + expect(postHook2.called).equal(true); + expect(result).equal(2); + }); + }); - transaction.execPostHooks(); + it('--> execPostHooks() should resolve if no hooks', () => { + ModelInstance.hooksTransaction(transaction, []); + delete transaction.hooks.post; - expect(spy.callCount).equal(2); + return transaction.execPostHooks().then(() => { + expect(true).equal(true); + }); }); }); describe('gcloud-node queries', () => { - it ('should be able to create gcloud-node Query object', () => { - let query = ModelInstance.query(); + let query; + + beforeEach(() => { + const responseQueries = [mockEntities, { + moreResults: ds.MORE_RESULTS_AFTER_LIMIT, + endCursor: 'abcdef', + }]; + + query = ModelInstance.query(); + sinon.stub(query, '__originalRun').resolves(responseQueries); + }); + + it('should create gcloud-node Query object', () => { + query = ModelInstance.query(); expect(query.constructor.name).equal('Query'); }); - it ('should be able to execute all gcloud-node queries', () => { - let fn = () => { - let query = ModelInstance.query() + it('should be able to execute all gcloud-node queries', () => { + const fn = () => { + query = ModelInstance.query() .filter('name', '=', 'John') - .filter('age', '>=', 4) - .order('lastname', { - descending: true - }); + .groupBy(['name']) + .select(['name']) + .order('lastname', { descending: true }) + .limit(1) + .offset(1) + .start('X'); return query; }; expect(fn).to.not.throw(Error); }); - it ('should throw error if calling unregistered query method', () => { - let fn = () => { - let query = ModelInstance.query() - .unkown('test', false); + it('should throw error if calling unregistered query method', () => { + const fn = () => { + query = ModelInstance.query() + .unkown('test', false); return query; }; expect(fn).to.throw(Error); }); - it('should run query', (done) => { - let query = ModelInstance.query() - .filter('name', '=', 'John'); + it('should run query', () => query.run().then((data) => { + const response = data[0]; + // We add manually the id in the mocks to be able to deep compare + mockEntities[0].id = 1234; + mockEntities[1].id = 'keyname'; - query.run((err, response) => { - // We add manually the id in the mocks to deep compare - mockEntities[0].id = 1234; - mockEntities[1].id = 'keyname'; + // we delete from the mock the property + // 'password' it has been defined with read: false + delete mockEntities[0].password; - // we delete from the mock to be able to deep compare - /// as property 'password' is set as read: false - delete mockEntities[0].password; + expect(query.__originalRun.called).equal(true); + expect(response.entities.length).equal(2); + assert.isUndefined(response.entities[0].password); + expect(response.entities).deep.equal(mockEntities); + expect(response.nextPageCursor).equal('abcdef'); - expect(ds.runQuery.getCall(0).args[0]).equal(query); - expect(response.entities.length).equal(2); - expect(response.entities[0].password).not.to.exist; - expect(response.entities).deep.equal(mockEntities); - expect(response.nextPageCursor).equal('abcdef'); + delete mockEntities[0].id; + delete mockEntities[1].id; + })); - done(); - }); - }); - - it('should add id to entities', (done) => { - let query = ModelInstance.query() - .run((err, response) => { - expect(response.entities[0].id).equal(mockEntities[0][ds.KEY].id); - expect(response.entities[1].id).equal(mockEntities[1][ds.KEY].name); - done(); - }); - }); + it('should add id to entities', () => query.run() + .then((data) => { + const response = data[0]; + expect(response.entities[0].id).equal(mockEntities[0][ds.KEY].id); + expect(response.entities[1].id).equal(mockEntities[1][ds.KEY].name); + })); - it('should accept "readAll" option', (done) => { - let query = ModelInstance.query() - .run(({readAll:true}), (err, response) => { - expect(response.entities[0].password).to.exist; - done(); - }); - }); + it('should accept "readAll" option', () => query.run(({ readAll: true })) + .then((data) => { + const response = data[0]; + assert.isDefined(response.entities[0].password); + })); - it('should not add endCursor to response', function(done){ - ds.runQuery.restore(); - sinon.stub(ds, 'runQuery', function(query, cb) { - return cb(null, [], {moreResults : ds.NO_MORE_RESULTS}); - }); - let query = ModelInstance.query() - .filter('name', '=', 'John'); + it('should not add endCursor to response', () => { + query.__originalRun.restore(); + sinon.stub(query, '__originalRun').resolves([[], { moreResults: ds.NO_MORE_RESULTS }]); - query.run((err, response) => { - expect(response.nextPageCursor).not.exist; - done(); + return query.run().then((data) => { + const response = data[0]; + assert.isUndefined(response.nextPageCursor); }); }); - it('should not return entities', (done) => { - ds.runQuery.restore(); - sinon.stub(ds, 'runQuery', (query, cb) => { - cb({code:400, message: 'Something went wrong doctor'}); - }); - let query = ModelInstance.query(); - var result; + it('should catch error thrown in query run()', () => { + const error = { code: 400, message: 'Something went wrong doctor' }; + query.__originalRun.restore(); + sinon.stub(query, '__originalRun').rejects(error); - query.run((err, entities) => { - if (!err) { - result = entities; - } - done(); + return query.run().catch((err) => { + expect(err).equal(error); }); - - expect(result).to.not.exist; }); it('should allow a namespace for query', () => { - let namespace = 'com.mydomain-dev'; - let query = ModelInstance.query(namespace); + const namespace = 'com.mydomain-dev'; + query = ModelInstance.query(namespace); expect(query.namespace).equal(namespace); }); - it('should create query on existing transaction', function(done) { - let query = ModelInstance.query(null, transaction); - query.filter('name', '=', 'John'); - - query.run((err, response) => { - expect(response.entities.length).equal(2); - expect(response.nextPageCursor).equal('abcdef'); - expect(query.scope.constructor.name).equal('Transaction'); - done(); - }); + it('should create query on existing transaction', () => { + query = ModelInstance.query(null, transaction); + expect(query.scope.constructor.name).equal('Transaction'); }); - it('should not set transaction if not an instance of gcloud Transaction', function() { - var fn = function() { - let query = ModelInstance.query(null, {}); + it('should not set transaction if not an instance of gcloud Transaction', () => { + const fn = () => { + query = ModelInstance.query(null, {}); }; expect(fn).to.throw(Error); }); - }); - - describe('shortcut queries', () => { - describe('list', () => { - it('should work with no settings defined', function() { - ds.runQuery.restore(); - sinon.stub(ds, 'runQuery', function(query, cb) { - setTimeout(function() { - return cb(null, mockEntities, { - moreResults : ds.MORE_RESULTS_AFTER_LIMIT, - endCursor: 'abcdef' - }); - }, 20); - }); - ModelInstance.list((err, response) => { - expect(response.entities.length).equal(2); - expect(response.nextPageCursor).equal('abcdef'); - expect(response.entities[0].password).not.to.exist; - }); + it('should still work with a callback', () => { + query = ModelInstance.query() + .filter('name', 'John'); - clock.tick(20); - }); - - it('should add id to entities', (done) => { - ModelInstance.list((err, response) => { - expect(response.entities[0].id).equal(mockEntities[0][ds.KEY].id); - expect(response.entities[1].id).equal(mockEntities[1][ds.KEY].name); - done(); - }); + query.run((err, response) => { + expect(ds.runQuery.getCall(0).args[0]).equal(query); + expect(response.entities.length).equal(2); + expect(response.nextPageCursor).equal('abcdef'); }); + }); + }); - it('should not add endCursor to response', function(){ - ds.runQuery.restore(); - sinon.stub(ds, 'runQuery', function(query, cb) { - setTimeout(function() { - return cb(null, mockEntities, {moreResults : ds.NO_MORE_RESULTS}); - }, 20); - }); + describe('shortcut queries', () => { + let queryMock; + beforeEach(() => { + queryMock = new Query(ds, { entities: mockEntities }); + sinon.stub(ds, 'createQuery', () => queryMock); + sinon.spy(queryHelpers, 'buildFromOptions'); + sinon.spy(queryMock, 'run'); + sinon.spy(queryMock, 'filter'); + sinon.spy(queryMock, 'hasAncestor'); + sinon.spy(queryMock, 'order'); + sinon.spy(queryMock, 'limit'); + }); - ModelInstance.list((err, response) => { - expect(response.nextPageCursor).not.exist; + afterEach(() => { + ds.createQuery.restore(); + queryHelpers.buildFromOptions.restore(); + queryMock.run.restore(); + queryMock.filter.restore(); + queryMock.hasAncestor.restore(); + queryMock.order.restore(); + queryMock.limit.restore(); + }); + + describe('list', () => { + it('should work with no settings defined', () => ModelInstance.list().then((data) => { + const response = data[0]; + expect(response.entities.length).equal(2); + expect(response.nextPageCursor).equal('abcdef'); + assert.isUndefined(response.entities[0].password); + })); + + it('should add id to entities', () => ModelInstance.list().then((data) => { + const response = data[0]; + expect(response.entities[0].id).equal(mockEntities[0][ds.KEY].id); + expect(response.entities[1].id).equal(mockEntities[1][ds.KEY].name); + })); + + it('should not add endCursor to response', () => { + ds.createQuery.restore(); + sinon.stub(ds, 'createQuery', + () => new Query(ds, { entities: mockEntities }, { moreResults: ds.NO_MORE_RESULTS })); + + return ModelInstance.list().then((data) => { + const response = data[0]; + assert.isUndefined(response.nextPageCursor); }); - - clock.tick(20); }); - it('should read settings defined', () => { - let querySettings = { - limit:10 + it('should read settings passed', () => { + const querySettings = { + limit: 10, }; schema.queries('list', querySettings); ModelInstance = Model.compile('Blog', schema, gstore); - sinon.spy(queryHelpers, 'buildFromOptions'); - - ModelInstance.list(() => {}); - - expect(queryHelpers.buildFromOptions.getCall(0).args[1].limit).equal(querySettings.limit); - expect(ds.runQuery.getCall(0).args[0].limitVal).equal(10); - queryHelpers.buildFromOptions.restore(); + return ModelInstance.list().then(() => { + expect(queryHelpers.buildFromOptions.getCall(0).args[1].limit).equal(querySettings.limit); + expect(queryMock.limit.getCall(0).args[0]).equal(querySettings.limit); + }); }); - it('should override setting with options', (done) => { - let querySettings = { - limit:10, - readAll: true + it('should override global setting with options', () => { + const querySettings = { + limit: 10, + readAll: true, }; schema.queries('list', querySettings); ModelInstance = Model.compile('Blog', schema, gstore); - sinon.spy(queryHelpers, 'buildFromOptions'); - ModelInstance.list({limit:15}, (err, response) => { + return ModelInstance.list({ limit: 15 }).then((data) => { + const response = data[0]; expect(queryHelpers.buildFromOptions.getCall(0).args[1]).not.deep.equal(querySettings); - expect(ds.runQuery.getCall(0).args[0].limitVal).equal(15); - expect(response.entities[0].password).to.exist; - - queryHelpers.buildFromOptions.restore(); - done(); + expect(queryMock.limit.getCall(0).args[0]).equal(15); + assert.isDefined(response.entities[0].password); }); - }); it('should deal with err response', () => { - ds.runQuery.restore(); - sinon.stub(ds, 'runQuery', (query, cb) => { - return cb({code:500, message:'Server error'}); - }); + queryMock.run.restore(); + const error = { code: 500, message: 'Server error' }; + sinon.stub(queryMock, 'run').rejects(error); - let result; - ModelInstance.list((err, entities) => { - if (!err) { - result = entities; - } + return ModelInstance.list().catch((err) => { + expect(err).equal(err); }); - - expect(result).not.exist; }); it('should accept a namespace ', () => { - let namespace = 'com.mydomain-dev'; - - ModelInstance.list({namespace:namespace}, () => {}); + const namespace = 'com.mydomain-dev'; - let query = ds.runQuery.getCall(0).args[0]; - expect(query.namespace).equal(namespace); + return ModelInstance.list({ namespace }).then(() => { + expect(queryHelpers.buildFromOptions.getCall(0).args[1]).deep.equal({ namespace }); + }); }); + + it('should still work with a callback', () => ModelInstance.list((err, response) => { + expect(response.entities.length).equal(2); + expect(response.nextPageCursor).equal('abcdef'); + assert.isUndefined(response.entities[0].password); + })); }); describe('deleteAll()', () => { beforeEach(() => { - sinon.spy(ModelInstance, 'delete'); - - sinon.stub(ds, 'delete', function() { - let args = Array.prototype.slice.call(arguments); - let cb = args.pop(); - return cb(null, {}); - }); - - sinon.spy(async, 'eachSeries'); + sinon.stub(ds, 'delete').resolves([{ indexUpdates: 3 }]); }); afterEach(() => { - ModelInstance.delete.restore(); ds.delete.restore(); - async.eachSeries.restore(); }); - it('should get all entities through Query', (done) => { - ModelInstance.deleteAll(() => {done();}); - let arg = ds.runQuery.getCall(0).args[0]; + it('should get all entities through Query', () => ModelInstance.deleteAll().then(() => { + expect(queryMock.run.called).equal(true); + expect(ds.createQuery.getCall(0).args.length).equal(1); + })); - expect(ds.runQuery.called).true; - expect(arg.constructor.name).equal('Query'); - expect(arg.kinds[0]).equal('Blog'); - expect(arg.namespace).equal('com.mydomain'); - }); - - it('should return error if could not fetch entities', (done) => { - let error = {code:500, message:'Something went wrong'}; - ds.runQuery.restore(); - sinon.stub(ds, 'runQuery', function() { - let args = Array.prototype.slice.call(arguments); - let cb = args.pop(); - return cb(error); - }); + it('should catch error if could not fetch entities', () => { + const error = { code: 500, message: 'Something went wrong' }; + queryMock.run.restore(); + sinon.stub(queryMock, 'run').rejects(error); - ModelInstance.deleteAll((err) => { - expect(err).deep.equal(error); - done(); + return ModelInstance.deleteAll().catch((err) => { + expect(err).equal(error); }); }); - it('if pre OR post hooks, should call delete on all entities found (in series)', function(done) { + it('if pre hooks, should call "delete" on all entities found (in series)', () => { schema = new Schema({}); - schema.pre('delete', function(next){next();}); - schema.post('delete', function() {}); + const spies = { + pre: () => Promise.resolve(), + }; + sinon.spy(spies, 'pre'); + + schema.pre('delete', spies.pre); + ModelInstance = gstore.model('NewBlog', schema); sinon.spy(ModelInstance, 'delete'); - ModelInstance.deleteAll(function(){ - expect(async.eachSeries.called).be.true; - expect(ModelInstance.delete.callCount).equal(2); - expect(ModelInstance.delete.getCall(0).args.length).equal(6); + return ModelInstance.deleteAll().then(() => { + expect(spies.pre.callCount).equal(mockEntities.length); + expect(ModelInstance.delete.callCount).equal(mockEntities.length); + expect(ModelInstance.delete.getCall(0).args.length).equal(5); expect(ModelInstance.delete.getCall(0).args[4].constructor.name).equal('Key'); - done(); }); }); - it('if NO hooks, should call delete passing an array of keys', function(done) { - ModelInstance.deleteAll(function() { - expect(ModelInstance.delete.callCount).equal(1); + it('if post hooks, should call "delete" on all entities found (in series)', () => { + schema = new Schema({}); + const spies = { + post: () => Promise.resolve(), + }; + sinon.spy(spies, 'post'); + schema.post('delete', spies.post); - let args = ModelInstance.delete.getCall(0).args; - expect(args.length).equal(6); - expect(is.array(args[4])).be.true; - expect(args[4]).deep.equal([mockEntities[0][ds.KEY], mockEntities[1][ds.KEY]]); + ModelInstance = gstore.model('NewBlog', schema); + sinon.spy(ModelInstance, 'delete'); - done(); + return ModelInstance.deleteAll().then(() => { + expect(spies.post.callCount).equal(mockEntities.length); + expect(ModelInstance.delete.callCount).equal(2); }); }); - it('should call with ancestors', (done) => { - let ancestors = ['Parent', 'keyname']; - ModelInstance.deleteAll(ancestors, () => {done();}); - - let arg = ds.runQuery.getCall(0).args[0]; - expect(arg.filters[0].op).equal('HAS_ANCESTOR'); - expect(arg.filters[0].val.path).deep.equal(ancestors); - }); + it('if NO hooks, should call delete passing an array of keys', () => { + sinon.spy(ModelInstance, 'delete'); - it('should call with namespace', (done) => { - let namespace = 'com.new-domain.dev'; - ModelInstance.deleteAll(null, namespace, () => {done();}); + return ModelInstance.deleteAll().then(() => { + expect(ModelInstance.delete.callCount).equal(1); - let arg = ds.runQuery.getCall(0).args[0]; - expect(arg.namespace).equal(namespace); - }); + const args = ModelInstance.delete.getCall(0).args; + expect(args.length).equal(5); + expect(is.array(args[4])).equal(true); + expect(args[4]).deep.equal([mockEntities[0][ds.KEY], mockEntities[1][ds.KEY]]); - it ('should return success:true if all ok', (done) => { - ModelInstance.deleteAll((err, msg) => { - expect(err).not.exist; - expect(msg.success).be.true; - done(); + ModelInstance.delete.restore(); }); }); - it ('should return error if any while deleting', (done) => { - let error = {code:500, message:'Could not delete'}; - ModelInstance.delete.restore(); - sinon.stub(ModelInstance, 'delete', function() { - let args = Array.prototype.slice.call(arguments); - let cb = args.pop(); - cb(error); - }); + it('should call with ancestors', () => { + const ancestors = ['Parent', 'keyname']; - ModelInstance.deleteAll((err, msg) => { - expect(err).equal(error); - expect(msg).not.exist; - done(); + return ModelInstance.deleteAll(ancestors).then(() => { + expect(queryMock.hasAncestor.calledOnce).equal(true); + expect(queryMock.ancestors.path).deep.equal(ancestors); }); }); - }); - - describe('findAround()', function() { - - it('should get 3 entities after a given date', function() { - ModelInstance.findAround('createdOn', '2016-1-1', {after:3}, (err, entities) => { - let query = ds.runQuery.getCall(0).args[0]; - expect(query.filters[0].name).equal('createdOn'); - expect(query.filters[0].op).equal('>'); - expect(query.filters[0].val).equal('2016-1-1'); - expect(query.limitVal).equal(3); + it('should call with namespace', () => { + const namespace = 'com.new-domain.dev'; - // Make sure to not show properties where read is set to false - expect(entities[0].password).not.to.exist; + return ModelInstance.deleteAll(null, namespace).then(() => { + expect(ds.createQuery.getCall(0).args[0]).equal(namespace); }); }); - it ('should get 3 entities before a given date', function() { - ModelInstance.findAround('createdOn', '2016-1-1', {before:12}, () => {}); - let query = ds.runQuery.getCall(0).args[0]; + it('should return success:true if all ok', () => ModelInstance.deleteAll().then((data) => { + const msg = data[0]; + expect(msg.success).equal(true); + })); - expect(query.filters[0].op).equal('<'); - expect(query.limitVal).equal(12); - }); + it('should return error if any while deleting', () => { + const error = { code: 500, message: 'Could not delete' }; + sinon.stub(ModelInstance, 'delete').rejects(error); - it('should validate that all arguments are passed', function() { - ModelInstance.findAround('createdOn', '2016-1-1', (err) => { - expect(err.code).equal(400); - expect(err.message).equal('Argument missing'); + return ModelInstance.deleteAll().catch((err) => { + expect(err).equal(error); }); }); + }); - it('should validate that options passed is an object', function(done) { + describe('findAround()', () => { + it('should get 3 entities after a given date', + () => ModelInstance.findAround('createdOn', '2016-1-1', { after: 3 }) + .then((result) => { + const entities = result[0]; + expect(queryMock.filter.getCall(0).args) + .deep.equal(['createdOn', '>', '2016-1-1']); + expect(queryMock.order.getCall(0).args) + .deep.equal(['createdOn', { descending: true }]); + expect(queryMock.limit.getCall(0).args[0]).equal(3); + + // Make sure to not show properties where read is set to false + assert.isUndefined(entities[0].password); + })); + + it('should get 3 entities before a given date', () => + ModelInstance.findAround('createdOn', '2016-1-1', { before: 12 }).then(() => { + expect(queryMock.filter.getCall(0).args) + .deep.equal(['createdOn', '<', '2016-1-1']); + expect(queryMock.limit.getCall(0).args[0]).equal(12); + })); + + it('should throw error if not all arguments are passed', () => + ModelInstance.findAround('createdOn', '2016-1-1') + .catch((err) => { + expect(err.code).equal(400); + expect(err.message).equal('Argument missing'); + })); + + it('should validate that options passed is an object', () => ModelInstance.findAround('createdOn', '2016-1-1', 'string', (err) => { expect(err.code).equal(400); - done(); - }); - }); + })); - it('should validate that options has a "after" or "before" property', function(done) { + it('should validate that options has a "after" or "before" property', () => ModelInstance.findAround('createdOn', '2016-1-1', {}, (err) => { expect(err.code).equal(400); - done(); - }); - }); + })); - it('should validate that options has not both "after" & "before" properties', function() { - ModelInstance.findAround('createdOn', '2016-1-1', {after:3, before:3}, (err) => { + it('should validate that options has not both "after" & "before" properties', () => + ModelInstance.findAround('createdOn', '2016-1-1', { after: 3, before: 3 }, (err) => { expect(err.code).equal(400); - }); - }); + })); - it('should add id to entities', (done) => { - ModelInstance.findAround('createdOn', '2016-1-1', {before:3}, (err, entities) => { + it('should add id to entities', () => + ModelInstance.findAround('createdOn', '2016-1-1', { before: 3 }).then((result) => { + const entities = result[0]; expect(entities[0].id).equal(mockEntities[0][ds.KEY].id); expect(entities[1].id).equal(mockEntities[1][ds.KEY].name); - done(); - }); - }); - - it('should read all properties', (done) => { - ModelInstance.findAround('createdOn', '2016-1-1', {before:3, readAll:true}, (err, entities) => { - expect(entities[0].password).exist; - done(); + })); + + it('should read all properties', () => + ModelInstance.findAround('createdOn', '2016-1-1', { before: 3, readAll: true }).then((result) => { + const entities = result[0]; + assert.isDefined(entities[0].password); + })); + + it('should accept a namespace', () => { + const namespace = 'com.new-domain.dev'; + ModelInstance.findAround('createdOn', '2016-1-1', { before: 3 }, namespace).then(() => { + expect(ds.createQuery.getCall(0).args[0]).equal(namespace); }); }); - it('should accept a namespace', function() { - let namespace = 'com.new-domain.dev'; - ModelInstance.findAround('createdOn', '2016-1-1', {before:3}, namespace, () => {}); - - let query = ds.runQuery.getCall(0).args[0]; - expect(query.namespace).equal(namespace); - }); - it('should deal with err response', () => { - ds.runQuery.restore(); - sinon.stub(ds, 'runQuery', (query, cb) => { - return cb({code:500, message:'Server error'}); - }); + queryMock.run.restore(); + const error = { code: 500, message: 'Server error' }; + sinon.stub(queryMock, 'run').rejects(error); - ModelInstance.findAround('createdOn', '2016-1-1', {after:3}, (err) => { - expect(err.code).equal(500); + return ModelInstance.findAround('createdOn', '2016-1-1', { after: 3 }).catch((err) => { + expect(err).equal(error); }); }); + + it('should still work passing a callback', + () => ModelInstance.findAround('createdOn', '2016-1-1', { after: 3 }, (err, entities) => { + expect(queryMock.filter.getCall(0).args) + .deep.equal(['createdOn', '>', '2016-1-1']); + expect(queryMock.order.getCall(0).args) + .deep.equal(['createdOn', { descending: true }]); + expect(queryMock.limit.getCall(0).args[0]).equal(3); + + // Make sure to not show properties where read is set to false + assert.isUndefined(entities[0].password); + })); }); describe('findOne()', () => { it('should call pre and post hooks', () => { - var spy = { - fnPre : function(next) { - //console.log('Spy Pre Method Schema'); - next(); - }, - fnPost: function(next) { - //console.log('Spy Post Method Schema'); - next(); - } + const spies = { + pre: () => Promise.resolve(), + post: () => Promise.resolve(), }; - var findOnePre = sinon.spy(spy, 'fnPre'); - var findOnePost = sinon.spy(spy, 'fnPost'); - schema.pre('findOne', findOnePre); - schema.post('findOne', findOnePost); + sinon.spy(spies, 'pre'); + sinon.spy(spies, 'post'); + schema.pre('findOne', spies.pre); + schema.post('findOne', spies.post); ModelInstance = Model.compile('Blog', schema, gstore); - ModelInstance.findOne({}, () => {}); - - expect(findOnePre.calledOnce).be.true; - expect(findOnePost.calledOnce).to.be.true; + ModelInstance.findOne({}).then(() => { + expect(spies.pre.calledOnce).equal(true); + expect(spies.post.calledOnce).equal(true); + expect(spies.pre.calledBefore(queryMock.run)).equal(true); + expect(spies.post.calledAfter(queryMock.run)).equal(true); + }); }); - it('should run correct gcloud Query', function(done) { - ModelInstance.findOne({name:'John', email:'john@snow.com'}, () => { - let query = ds.runQuery.getCall(0).args[0]; + it('should run correct gcloud Query', () => + ModelInstance.findOne({ name: 'John', email: 'john@snow.com' }).then(() => { + expect(queryMock.filter.getCall(0).args) + .deep.equal(['name', 'John']); - expect(query.filters[0].name).equal('name'); - expect(query.filters[0].op).equal('='); - expect(query.filters[0].val).equal('John'); + expect(queryMock.filter.getCall(1).args) + .deep.equal(['email', 'john@snow.com']); + })); - expect(query.filters[1].name).equal('email'); - expect(query.filters[1].op).equal('='); - expect(query.filters[1].val).equal('john@snow.com'); - done(); - }); - }); - - it('should return a Model instance', function(done) { - ModelInstance.findOne({name:'John'}, (err, entity) => { + it('should return a Model instance', () => + ModelInstance.findOne({ name: 'John' }).then((result) => { + const entity = result[0]; expect(entity.entityKind).equal('Blog'); - expect(entity instanceof Model).be.true; - done(); - }); - }); + expect(entity instanceof Model).equal(true); + })); - it('should validate that params passed are object', function() { - ModelInstance.findOne('some string', (err, entity) => { + it('should validate that params passed are object', () => + ModelInstance.findOne('some string').catch((err) => { expect(err.code).equal(400); - }); - }); - - it('should accept ancestors', function(done) { - let ancestors = ['Parent', 'keyname']; + })); - ModelInstance.findOne({name:'John'}, ancestors, () => { - let query = ds.runQuery.getCall(0).args[0]; + it('should accept ancestors', () => { + const ancestors = ['Parent', 'keyname']; - expect(query.filters[1].name).equal('__key__'); - expect(query.filters[1].op).equal('HAS_ANCESTOR'); - expect(query.filters[1].val.path).deep.equal(ancestors); - done(); + return ModelInstance.findOne({ name: 'John' }, ancestors, () => { + expect(queryMock.hasAncestor.getCall(0).args[0].path) + .deep.equal(ancestors); }); }); - it('should accept a namespace', function(done) { - let namespace = 'com.new-domain.dev'; + it('should accept a namespace', () => { + const namespace = 'com.new-domain.dev'; - ModelInstance.findOne({name:'John'}, null, namespace, () => { - let query = ds.runQuery.getCall(0).args[0]; - - expect(query.namespace).equal(namespace); - done(); + return ModelInstance.findOne({ name: 'John' }, null, namespace, () => { + expect(ds.createQuery.getCall(0).args[0]).equal(namespace); }); }); - it('should deal with err response', (done) => { - ds.runQuery.restore(); - sinon.stub(ds, 'runQuery', (query, cb) => { - return cb({code:500, message:'Server error'}); - }); + it('should deal with err response', () => { + queryMock.run.restore(); + const error = { code: 500, message: 'Server error' }; + sinon.stub(queryMock, 'run').rejects(error); - ModelInstance.findOne({name:'John'}, (err, entities) => { - expect(err.code).equal(500); - done(); + return ModelInstance.findOne({ name: 'John' }).catch((err) => { + expect(err).equal(error); }); }); - it('if entity not found should return 404', (done) => { - ds.runQuery.restore(); - sinon.stub(ds, 'runQuery', (query, cb) => { - return cb(null); - }); + it('if entity not found should return 404', () => { + queryMock.run.restore(); + sinon.stub(queryMock, 'run').resolves(); - ModelInstance.findOne({name:'John'}, (err, entities) => { + return ModelInstance.findOne({ name: 'John' }).catch((err) => { expect(err.code).equal(404); - done(); }); }); - }) - describe('excludeFromIndexes', function() { - it('should add properties to schema as optional', function() { - let arr = ['newProp', 'url']; + it('should still work with a callback', () => + ModelInstance.findOne({ name: 'John' }, (err, entity) => { + expect(entity.entityKind).equal('Blog'); + expect(entity instanceof Model).equal(true); + })); + }); + + describe('excludeFromIndexes', () => { + it('should add properties to schema as optional', () => { + const arr = ['newProp', 'url']; ModelInstance.excludeFromIndexes(arr); - let model = new ModelInstance({}); + const model = new ModelInstance({}); expect(model.excludeFromIndexes).deep.equal(['lastname', 'age'].concat(arr)); - expect(schema.path('newProp').optional).be.true; + expect(schema.path('newProp').optional).equal(true); }); - it('should only modifiy excludeFromIndexes on properties that already exist', function() { - let prop = 'lastname'; + it('should only modifiy excludeFromIndexes on properties that already exist', () => { + const prop = 'lastname'; ModelInstance.excludeFromIndexes(prop); - let model = new ModelInstance({}); + const model = new ModelInstance({}); expect(model.excludeFromIndexes).deep.equal(['lastname', 'age']); - expect(schema.path('lastname').optional).not.exist; - expect(schema.path('lastname').excludeFromIndexes).be.true; + assert.isUndefined(schema.path('lastname').optional); + expect(schema.path('lastname').excludeFromIndexes).equal(true); }); }); }); describe('save()', () => { let model; - let data = {name:'John', lastname:'Snow'}; + const data = { name: 'John', lastname: 'Snow' }; beforeEach(() => { model = new ModelInstance(data); }); it('---> should validate() before', () => { - let validateSpy = sinon.spy(model, 'validate'); + const validateSpy = sinon.spy(model, 'validate'); - model.save(() => {}); - - expect(validateSpy.called).be.true; + return model.save().then(() => { + expect(validateSpy.called).equal(true); + }); }); it('---> should NOT validate() data before', () => { - schema = new Schema({}, {validateBeforeSave: false}); + schema = new Schema({}, { validateBeforeSave: false }); ModelInstance = Model.compile('Blog', schema, gstore); - model = new ModelInstance({name: 'John'}); - let validateSpy = sinon.spy(model, 'validate'); - - model.save(() => {}); + model = new ModelInstance({ name: 'John' }); + const validateSpy = sinon.spy(model, 'validate'); - expect(validateSpy.called).be.false; + return model.save().then(() => { + expect(validateSpy.called).equal(false); + }); }); it('should NOT save to Datastore if it didn\'t pass property validation', () => { - model = new ModelInstance({unknown:'John'}); - - model.save(() => {}); + model = new ModelInstance({ unknown: 'John' }); - expect(ds.save.called).be.false; + return model.save().catch((err) => { + assert.isDefined(err); + expect(ds.save.called).equal(false); + }); }); it('should NOT save to Datastore if it didn\'t pass value validation', () => { - model = new ModelInstance({website:'mydomain'}); + model = new ModelInstance({ website: 'mydomain' }); - model.save(() => {}); - - expect(ds.save.called).be.false; + return model.save().catch((err) => { + assert.isDefined(err); + expect(ds.save.called).equal(false); + }); }); - it('should convert to Datastore format before saving to Datastore', function(done) { - let spySerializerToDatastore = sinon.spy(datastoreSerializer, 'toDatastore'); - - model.save((err, entity) => {}); - clock.tick(20); + it('should convert to Datastore format before saving to Datastore', () => { + const spySerializerToDatastore = sinon.spy(datastoreSerializer, 'toDatastore'); - expect(model.gstore.ds.save.calledOnce).be.true; - expect(spySerializerToDatastore.called).be.true; - expect(spySerializerToDatastore.getCall(0).args[0]).equal(model.entityData); - expect(spySerializerToDatastore.getCall(0).args[1]).equal(model.excludeFromIndexes); - expect(model.gstore.ds.save.getCall(0).args[0].key).exist; - expect(model.gstore.ds.save.getCall(0).args[0].key.constructor.name).equal('Key'); - expect(model.gstore.ds.save.getCall(0).args[0].data).exist; - expect(model.gstore.ds.save.getCall(0).args[0].data[0].excludeFromIndexes).exist; + model.save().then(() => { + expect(model.gstore.ds.save.calledOnce).equal(true); + expect(spySerializerToDatastore.called).equal(true); + expect(spySerializerToDatastore.getCall(0).args[0]).equal(model.entityData); + expect(spySerializerToDatastore.getCall(0).args[1]).equal(model.excludeFromIndexes); + assert.isDefined(model.gstore.ds.save.getCall(0).args[0].key); + expect(model.gstore.ds.save.getCall(0).args[0].key.constructor.name).equal('Key'); + assert.isDefined(model.gstore.ds.save.getCall(0).args[0].data); + assert.isDefined(model.gstore.ds.save.getCall(0).args[0].data[0].excludeFromIndexes); - done(); - spySerializerToDatastore.restore(); + spySerializerToDatastore.restore(); + }); }); - it('if Datastore error, return the error and don\'t call emit', () => { + it('on Datastore error, return the error', () => { ds.save.restore(); - let error = { - code:500, - message:'Server Error' + const error = { + code: 500, + message: 'Server Error', }; - sinon.stub(ds, 'save', (entity, cb) => { - return cb(error); - }); + sinon.stub(ds, 'save').rejects(error); - let model = new ModelInstance({}); + model = new ModelInstance({}); - model.save((err, entity) => { + return model.save().catch((err) => { expect(err).equal(error); }); }); - it('should save entity in a transaction', function() { - model.save(transaction, {}, function(err, entity, info) { - expect(transaction.save.called).be.true; - expect(entity.entityData).exist; - expect(info.op).equal('save'); - }); - - clock.tick(20); - }); + it('should save entity in a transaction and return transaction', + () => model.save(transaction, {}) + .then((result) => { + const entity = result[0]; + const info = result[1]; + const transPassed = result[2]; + expect(transaction.save.called).equal(true); + assert.isDefined(entity.entityData); + expect(transPassed).equal(transaction); + expect(info.op).equal('save'); + })); - it('should save entity in a transaction WITHOUT passing callback', function() { + it('should save entity in a transaction in sync', () => { + const schema2 = new Schema({}, { validateBeforeSave: false }); + const ModelInstance2 = gstore.model('NewType', schema2, gstore); + model = new ModelInstance2({}); model.save(transaction); - - clock.tick(20); - - expect(transaction.save.called).be.true; + expect(true).equal(true); }); - it('should throw error if transaction not instance of Transaction', function() { - var fn = function() { - model.save({id:0}, {}, function() {}); - }; + it('should save entity in a transaction synchronous when validateBeforeSave desactivated', () => { + schema = new Schema({ + name: { type: 'string' }, + }, { + validateBeforeSave: false, // Only synchronous if no "pre" validation middleware + }); - clock.tick(20); + const ModelInstanceTemp = gstore.model('BlogTemp', schema, gstore); + model = new ModelInstanceTemp({}); - expect(fn).to.throw(Error); + model.save(transaction); + expect(transaction.save.called).equal(true); }); - it('should call pre hooks', () => { - let mockDs = {save:function() {}, key:function(){}}; - let spyPre = sinon.spy(); - let spySave = sinon.spy(mockDs, 'save'); - - schema = new Schema({name:{type:'string'}}); - schema.pre('save', (next) => { - spyPre(); - next(); + it('should save entity in a transaction synchronous when disabling hook', () => { + schema = new Schema({ + name: { type: 'string' }, }); - ModelInstance = Model.compile('Blog', schema, gstore); - let model = new ModelInstance({name:'John'}); - model.save(() => {}); - clock.tick(20); + const ModelInstanceTemp = gstore.model('BlogTemp', schema, gstore); + model = new ModelInstanceTemp({}); + model.preHooksEnabled = false; + model.save(transaction); + + const model2 = new ModelInstanceTemp({}); + const transaction2 = new Transaction(); + sinon.spy(transaction2, 'save'); + model2.save(transaction2); - expect(spyPre.calledBefore(spySave)).be.true; + expect(transaction.save.called).equal(true); + expect(transaction2.save.called).equal(false); }); - it('should emit "save" after', (done) => { - let model = new ModelInstance({}); - let emitStub = sinon.stub(model, 'emit'); - let callbackSpy = sinon.spy(); + it('should throw error if transaction not instance of Transaction', + () => model.save({ id: 0 }, {}) + .catch((err) => { + assert.isDefined(err); + expect(err.message).equal('Transaction needs to be a gcloud Transaction'); + })); - model.save(callbackSpy); - clock.tick(20); + it('should call pre hooks', () => { + const spyPre = sinon.stub().resolves(); - expect(emitStub.calledWithExactly('save')).be.true; - expect(emitStub.calledBefore(callbackSpy)).be.true; - done(); + schema = new Schema({ name: { type: 'string' } }); + schema.pre('save', () => spyPre()); + ModelInstance = Model.compile('Blog', schema, gstore); + model = new ModelInstance({ name: 'John' }); - emitStub.restore(); + return model.save().then(() => { + expect(spyPre.calledBefore(ds.save)).equal(true); + }); }); it('should call post hooks', () => { - let spyPost = sinon.spy(); + const spyPost = sinon.stub().resolves(123); + schema = new Schema({ name: { type: 'string' } }); + schema.post('save', () => spyPost()); + ModelInstance = Model.compile('Blog', schema, gstore); + model = new ModelInstance({ name: 'John' }); - schema = new Schema({name:{type:'string'}}); - schema.post('save', () => { - spyPost(); + return model.save().then((result) => { + expect(spyPost.called).equal(true); + expect(result).equal(123); }); + }); + it('error in post hooks should be added to response', () => { + const error = { code: 500 }; + const spyPost = sinon.stub().rejects(error); + schema = new Schema({ name: { type: 'string' } }); + schema.post('save', () => spyPost()); ModelInstance = Model.compile('Blog', schema, gstore); - let model = new ModelInstance({name:'John'}); - - model.save(() => {}); - clock.tick(20); + model = new ModelInstance({ name: 'John' }); - expect(spyPost.called).be.true; + return model.save().then((savedData) => { + assert.isDefined(savedData.result); + assert.isDefined(savedData.errorsPostHook); + expect(savedData.errorsPostHook[0]).equal(error); + }); }); it('transaction.execPostHooks() should call post hooks', () => { - let spyPost = sinon.spy(); - schema = new Schema({name:{type:'string'}}); + const spyPost = sinon.stub().resolves(123); + schema = new Schema({ name: { type: 'string' } }); schema.post('save', spyPost); ModelInstance = Model.compile('Blog', schema, gstore); - let model = new ModelInstance({name:'John'}); + model = new ModelInstance({ name: 'John' }); - model.save(transaction, () => { - transaction.execPostHooks(); - }); - clock.tick(20); + return model.save(transaction) + .then(() => transaction.execPostHooks()) + .then(() => { + expect(spyPost.called).equal(true); + expect(spyPost.callCount).equal(1); + }); + }); - expect(spyPost.called).be.true; + it('if transaction.execPostHooks() is NOT called post middleware should not be called', () => { + const spyPost = sinon.stub().resolves(123); + schema = new Schema({ name: { type: 'string' } }); + schema.post('save', spyPost); + + ModelInstance = Model.compile('Blog', schema, gstore); + model = new ModelInstance({ name: 'John' }); + + return model.save(transaction) + .then(() => { + expect(spyPost.called).equal(false); + }); }); it('should update modifiedOn to new Date if property in Schema', () => { - schema = new Schema({modifiedOn: {type: 'datetime'}}); - var model = gstore.model('BlogPost', schema); + schema = new Schema({ modifiedOn: { type: 'datetime' } }); + ModelInstance = gstore.model('BlogPost', schema); + const entity = new ModelInstance({}); - var entity = new model({}); - entity.save((err, entity) => {}); - clock.tick(20); - - expect(entity.entityData.modifiedOn).to.exist; - expect(entity.entityData.modifiedOn.toString()).to.equal(new Date().toString()); + return entity.save().then(() => { + assert.isDefined(entity.entityData.modifiedOn); + expect(entity.entityData.modifiedOn.toString()).to.equal(new Date().toString()); + }); }); }); describe('validate()', () => { it('properties passed ok', () => { - let model = new ModelInstance({name:'John', lastname:'Snow'}); + const model = new ModelInstance({ name: 'John', lastname: 'Snow' }); - let valid = model.validate(); - expect(valid.success).be.true; + const valid = model.validate(); + expect(valid.success).equal(true); }); it('properties passed ko', () => { - let model = new ModelInstance({unknown:123}); + const model = new ModelInstance({ unknown: 123 }); - let valid = model.validate(); + const valid = model.validate(); - expect(valid.success).be.false; + expect(valid.success).equal(false); }); it('should remove virtuals', () => { - let model = new ModelInstance({fullname:'John Snow'}); + const model = new ModelInstance({ fullname: 'John Snow' }); - let valid = model.validate(); + const valid = model.validate(); - expect(valid.success).be.true; - expect(model.entityData.fullname).not.exist; + expect(valid.success).equal(true); + assert.isUndefined(model.entityData.fullname); }); it('accept unkwown properties', () => { schema = new Schema({ - name: {type: 'string'}, + name: { type: 'string' }, }, { - explicitOnly : false + explicitOnly: false, }); ModelInstance = Model.compile('Blog', schema, gstore); - let model = new ModelInstance({unknown:123}); + const model = new ModelInstance({ unknown: 123 }); - let valid = model.validate(); + const valid = model.validate(); - expect(valid.success).be.true; + expect(valid.success).equal(true); }); it('required property', () => { schema = new Schema({ - name: {type: 'string'}, - email: {type: 'string', required:true} + name: { type: 'string' }, + email: { type: 'string', required: true }, }); ModelInstance = Model.compile('Blog', schema, gstore); - let model = new ModelInstance({name:'John Snow'}); - let model2 = new ModelInstance({name:'John Snow', email:''}); - let model3 = new ModelInstance({name:'John Snow', email:' '}); - let model4 = new ModelInstance({name:'John Snow', email: null}); + 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 }); - let valid = model.validate(); - let valid2 = model2.validate(); - let valid3 = model3.validate(); - let valid4 = model4.validate(); + const valid = model.validate(); + const valid2 = model2.validate(); + const valid3 = model3.validate(); + const valid4 = model4.validate(); - expect(valid.success).be.false; - expect(valid2.success).be.false; - expect(valid3.success).be.false; - expect(valid4.success).be.false; + 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 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).be.true; - expect(valid2.success).be.true; - expect(valid3.success).be.true; + expect(valid.success).equal(true); + expect(valid2.success).equal(true); + expect(valid3.success).equal(true); }); - it ('no type validation', () => { - let model = new ModelInstance({street:123}); - let model2 = new ModelInstance({street:'123'}); - let model3 = new ModelInstance({street:true}); + it('no type validation', () => { + const model = new ModelInstance({ street: 123 }); + const model2 = new ModelInstance({ street: '123' }); + const model3 = new ModelInstance({ street: true }); - let valid = model.validate(); - let valid2 = model2.validate(); - let valid3 = model3.validate(); + const valid = model.validate(); + const valid2 = model2.validate(); + const valid3 = model3.validate(); - expect(valid.success).be.true; - expect(valid2.success).be.true; - expect(valid3.success).be.true; + expect(valid.success).equal(true); + expect(valid2.success).equal(true); + expect(valid3.success).equal(true); }); - it ('--> string', () => { - let model = new ModelInstance({name:123}); + it('--> string', () => { + const model = new ModelInstance({ name: 123 }); - let valid = model.validate(); + const valid = model.validate(); - expect(valid.success).be.false; + expect(valid.success).equal(false); }); - it ('--> number', () => { - let model = new ModelInstance({age:'string'}); + it('--> number', () => { + const model = new ModelInstance({ age: 'string' }); - let valid = model.validate(); + const valid = model.validate(); - expect(valid.success).be.false; + expect(valid.success).equal(false); }); it('--> int', () => { - let model = new ModelInstance({age:ds.int('str')}); - let valid = model.validate(); + const model = new ModelInstance({ age: ds.int('str') }); + const valid = model.validate(); - let model2 = new ModelInstance({age:ds.int('7')}); - let valid2 = model2.validate(); + const model2 = new ModelInstance({ age: ds.int('7') }); + const valid2 = model2.validate(); - let model3 = new ModelInstance({age:ds.int(7)}); - let valid3 = model3.validate(); + const model3 = new ModelInstance({ age: ds.int(7) }); + const valid3 = model3.validate(); - let model4 = new ModelInstance({age:'string'}); - let valid4 = model4.validate(); + const model4 = new ModelInstance({ age: 'string' }); + const valid4 = model4.validate(); - let model5 = new ModelInstance({age:'7'}); - let valid5 = model5.validate(); + const model5 = new ModelInstance({ age: '7' }); + const valid5 = model5.validate(); - let model6 = new ModelInstance({age:7}); - let valid6 = model6.validate(); + const model6 = new ModelInstance({ age: 7 }); + const valid6 = model6.validate(); - expect(valid.success).be.false; - expect(valid2.success).be.true; - expect(valid3.success).be.true; - expect(valid4.success).be.false; - expect(valid5.success).be.false; - expect(valid6.success).be.true; + 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', () => { - let model = new ModelInstance({price:ds.double('str')}); - let valid = model.validate(); + const model = new ModelInstance({ price: ds.double('str') }); + const valid = model.validate(); - let model2 = new ModelInstance({price:ds.double('1.2')}); - let valid2 = model2.validate(); + const model2 = new ModelInstance({ price: ds.double('1.2') }); + const valid2 = model2.validate(); - let model3 = new ModelInstance({price:ds.double(7.0)}); - let valid3 = model3.validate(); + const model3 = new ModelInstance({ price: ds.double(7.0) }); + const valid3 = model3.validate(); - let model4 = new ModelInstance({price:'string'}); - let valid4 = model4.validate(); + const model4 = new ModelInstance({ price: 'string' }); + const valid4 = model4.validate(); - let model5 = new ModelInstance({price:'7'}); - let valid5 = model5.validate(); + const model5 = new ModelInstance({ price: '7' }); + const valid5 = model5.validate(); - let model6 = new ModelInstance({price:7}); - let valid6 = model6.validate(); + const model6 = new ModelInstance({ price: 7 }); + const valid6 = model6.validate(); - let model7 = new ModelInstance({price:7.59}); - let valid7 = model7.validate(); + const model7 = new ModelInstance({ price: 7.59 }); + const valid7 = model7.validate(); - expect(valid.success).be.false; - expect(valid2.success).be.true; - expect(valid3.success).be.true; - expect(valid4.success).be.false; - expect(valid5.success).be.false; - expect(valid6.success).be.true; - expect(valid7.success).be.true; + 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', () => { - let model = new ModelInstance({icon:'string'}); - let valid = model.validate(); + const model = new ModelInstance({ icon: 'string' }); + const valid = model.validate(); - let model2 = new ModelInstance({icon:new Buffer('\uD83C\uDF69')}); - let valid2 = model2.validate(); + const model2 = new ModelInstance({ icon: new Buffer('\uD83C\uDF69') }); + const valid2 = model2.validate(); - expect(valid.success).be.false; - expect(valid2.success).be.true; + expect(valid.success).equal(false); + expect(valid2.success).equal(true); }); - it ('--> boolean', () => { - let model = new ModelInstance({modified:'string'}); + it('--> boolean', () => { + const model = new ModelInstance({ modified: 'string' }); - let valid = model.validate(); + const valid = model.validate(); - expect(valid.success).be.false; + expect(valid.success).equal(false); }); it('--> object', () => { - let model = new ModelInstance({prefs:{check:true}}); + const model = new ModelInstance({ prefs: { check: true } }); - let valid = model.validate(); + const valid = model.validate(); - expect(valid.success).be.true; + expect(valid.success).equal(true); }); it('--> geoPoint', () => { - let model = new ModelInstance({location:'string'}); - let valid = model.validate(); + const model = new ModelInstance({ location: 'string' }); + const valid = model.validate(); - let model2 = new ModelInstance({location:ds.geoPoint({ - latitude: 40.6894, - longitude: -74.0447 - })}); - let valid2 = model2.validate(); + const model2 = new ModelInstance({ + location: ds.geoPoint({ + latitude: 40.6894, + longitude: -74.0447, + }), + }); + const valid2 = model2.validate(); - expect(valid.success).be.false; - expect(valid2.success).be.true; + expect(valid.success).equal(false); + expect(valid2.success).equal(true); }); it('--> array ok', () => { - let model = new ModelInstance({tags:[]}); + const model = new ModelInstance({ tags: [] }); - let valid = model.validate(); + const valid = model.validate(); - expect(valid.success).be.true; + expect(valid.success).equal(true); }); it('--> array ko', () => { - let model = new ModelInstance({tags:{}}); - let model2 = new ModelInstance({tags:'string'}); - let model3 = new ModelInstance({tags:123}); + const model = new ModelInstance({ tags: {} }); + const model2 = new ModelInstance({ tags: 'string' }); + const model3 = new ModelInstance({ tags: 123 }); - let valid = model.validate(); - let valid2 = model2.validate(); - let valid3 = model3.validate(); + const valid = model.validate(); + const valid2 = model2.validate(); + const valid3 = model3.validate(); - expect(valid.success).be.false; - expect(valid2.success).be.false; - expect(valid3.success).be.false; + expect(valid.success).equal(false); + expect(valid2.success).equal(false); + expect(valid3.success).equal(false); }); it('--> date ok', () => { - let model = new ModelInstance({birthday:'2015-01-01'}); - let model2 = new ModelInstance({birthday:new Date()}); + const model = new ModelInstance({ birthday: '2015-01-01' }); + const model2 = new ModelInstance({ birthday: new Date() }); - let valid = model.validate(); - let valid2 = model2.validate(); + const valid = model.validate(); + const valid2 = model2.validate(); - expect(valid.success).be.true; - expect(valid2.success).be.true; + expect(valid.success).equal(true); + expect(valid2.success).equal(true); }); - it ('--> date ko', () => { - let model = new ModelInstance({birthday:'01-2015-01'}); - let model2 = new ModelInstance({birthday:'01-01-2015'}); - let model3 = new ModelInstance({birthday:'2015/01/01'}); - let model4 = new ModelInstance({birthday:'01/01/2015'}); - let model5 = new ModelInstance({birthday:12345}); // No number allowed - let model6 = new ModelInstance({birthday:'string'}); + 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' }); - let valid = model.validate(); - let valid2 = model2.validate(); - let valid3 = model3.validate(); - let valid4 = model4.validate(); - let valid5 = model5.validate(); - let valid6 = model6.validate(); + 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).be.false; - expect(valid2.success).be.false; - expect(valid3.success).be.false; - expect(valid4.success).be.false; - expect(valid5.success).be.false; - expect(valid6.success).be.false; + 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', () => { - let model = new ModelInstance({website:'http://google.com'}); - let model2 = new ModelInstance({website:'google.com'}); + it('--> is URL ok', () => { + const model = new ModelInstance({ website: 'http://google.com' }); + const model2 = new ModelInstance({ website: 'google.com' }); - let valid = model.validate(); - let valid2 = model2.validate(); + const valid = model.validate(); + const valid2 = model2.validate(); - expect(valid.success).be.true; - expect(valid2.success).be.true; + expect(valid.success).equal(true); + expect(valid2.success).equal(true); }); - it ('--> is URL ko', () => { - let model = new ModelInstance({website:'domain.k'}); + it('--> is URL ko', () => { + const model = new ModelInstance({ website: 'domain.k' }); - let valid = model.validate(); + const valid = model.validate(); - expect(valid.success).be.false; + expect(valid.success).equal(false); }); - it ('--> is EMAIL ok', () => { - let model = new ModelInstance({email:'john@snow.com'}); + it('--> is EMAIL ok', () => { + const model = new ModelInstance({ email: 'john@snow.com' }); - let valid = model.validate(); + const valid = model.validate(); - expect(valid.success).be.true; + expect(valid.success).equal(true); }); - it ('--> is EMAIL ko', () => { - let model = new ModelInstance({email:'john@snow'}); - let model2 = new ModelInstance({email:'john@snow.'}); - let model3 = new ModelInstance({email:'john@snow.k'}); - let model4 = new ModelInstance({email:'johnsnow.com'}); + 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' }); - let valid = model.validate(); - let valid2 = model2.validate(); - let valid3 = model3.validate(); - let valid4 = model4.validate(); + const valid = model.validate(); + const valid2 = model2.validate(); + const valid3 = model3.validate(); + const valid4 = model4.validate(); - expect(valid.success).be.false; - expect(valid2.success).be.false; - expect(valid3.success).be.false; - expect(valid4.success).be.false; + expect(valid.success).equal(false); + expect(valid2.success).equal(false); + expect(valid3.success).equal(false); + expect(valid4.success).equal(false); }); it('--> is HexColor', () => { - let model = new ModelInstance({color:'#fff'}); - let model2 = new ModelInstance({color:'white'}); + const model = new ModelInstance({ color: '#fff' }); + const model2 = new ModelInstance({ color: 'white' }); - let valid = model.validate(); - let valid2 = model2.validate(); + const valid = model.validate(); + const valid2 = model2.validate(); - expect(valid.success).be.true; - expect(valid2.success).be.false; + expect(valid.success).equal(true); + expect(valid2.success).equal(false); }); - it ('and only accept value in default values', () => { - let model = new ModelInstance({type:'other'}); + it('and only accept value in default values', () => { + const model = new ModelInstance({ type: 'other' }); - let valid = model.validate(); + const valid = model.validate(); - expect(valid.success).be.false; + expect(valid.success).equal(false); }); }); }); diff --git a/test/schema-test.js b/test/schema-test.js index 3907073..e02f2b1 100644 --- a/test/schema-test.js +++ b/test/schema-test.js @@ -1,143 +1,131 @@ -/*jshint -W030 */ -var chai = require('chai'); -var expect = chai.expect; +'use strict'; -var Schema = require('../lib').Schema; +const chai = require('chai'); +const Schema = require('../lib').Schema; -describe('Schema', () => { - "use strict"; +const expect = chai.expect; +const assert = chai.assert; +describe('Schema', () => { describe('contructor', () => { it('should initialized properties', () => { - let schema = new Schema({}); - - expect(schema.methods).to.exist; - expect(schema.shortcutQueries).to.exist; - expect(schema.paths).to.exist; - expect(schema.callQueue).to.exist; - expect(schema.s).to.exist; - expect(schema.s.hooks.constructor.name).to.equal('Kareem'); - expect(schema.options).to.exist; - expect(schema.options.queries).deep.equal({ - readAll : false - }); + const schema = new Schema({}); + + assert.isDefined(schema.methods); + assert.isDefined(schema.shortcutQueries); + assert.isDefined(schema.paths); + assert.isDefined(schema.callQueue); + assert.isDefined(schema.options); + expect(schema.options.queries).deep.equal({ readAll: false }); }); - it ('should merge options passed', () => { - let schema = new Schema({}, { - newOption:'myValue', - queries:{ - simplifyResult:false - } + it('should merge options passed', () => { + const schema = new Schema({}, { + newOption: 'myValue', + queries: { simplifyResult: false }, }); expect(schema.options.newOption).equal('myValue'); - expect(schema.options.queries.simplifyResult).be.false; + expect(schema.options.queries.simplifyResult).equal(false); }); - it ('should create its paths from obj passed', () => { - let schema = new Schema({ - property1:{type:'string'}, - property2:{type:'number'} + it('should create its paths from obj passed', () => { + const schema = new Schema({ + property1: { type: 'string' }, + property2: { type: 'number' }, }); - expect(schema.paths.property1).to.exist; - expect(schema.paths.property2).to.exist; + assert.isDefined(schema.paths.property1); + assert.isDefined(schema.paths.property2); }); - // it('if no type passed, default to string', () => { - // let schema = new Schema({name:{}}); - // - // expect(schema.paths.name.type).equal('string'); - // }); - - it ('should not allowed reserved properties on schema', function() { - let fn = () => { - let schema = new Schema({ds:123}); + it('should not allowed reserved properties on schema', () => { + const fn = () => { + const schema = new Schema({ ds: 123 }); + return schema; }; expect(fn).to.throw(Error); }); it('should register default middelwares', () => { - let schema = new Schema({}); + const schema = new Schema({}); - expect(schema.callQueue.length).equal(1); - expect(schema.callQueue[0][0]).equal('pre'); - expect(schema.callQueue[0][1][0]).equal('save'); + assert.isDefined(schema.callQueue.entity.save); + expect(schema.callQueue.entity.save.pres.length).equal(1); }); }); describe('add method', () => { let schema; - beforeEach(function() { + beforeEach(() => { schema = new Schema({}); schema.methods = {}; }); - it ('should add it to its methods table', () => { - let fn = () => {}; + it('should add it to its methods table', () => { + const fn = () => { }; schema.method('doSomething', fn); - expect(schema.methods.doSomething).to.exist; + assert.isDefined(schema.methods.doSomething); expect(schema.methods.doSomething).to.equal(fn); }); - it ('should not do anything if value passed is not a function', () => { + it('should not do anything if value passed is not a function', () => { schema.method('doSomething', 123); - expect(schema.methods.doSomething).to.not.exist; + assert.isUndefined(schema.methods.doSomething); }); - it ('should allow to pass a table of functions and validate type', () => { - let fn = () => {}; + it('should allow to pass a table of functions and validate type', () => { + const fn = () => { }; schema.method({ - doSomething:fn, - doAnotherThing:123 + doSomething: fn, + doAnotherThing: 123, }); - expect(schema.methods.doSomething).exist; + assert.isDefined(schema.methods.doSomething); expect(schema.methods.doSomething).to.equal(fn); - expect(schema.methods.doAnotherThing).not.exist; + assert.isUndefined(schema.methods.doAnotherThing); }); - it ('should only allow function and object to be passed', () => { - schema.method(10, () => {}); + it('should only allow function and object to be passed', () => { + schema.method(10, () => { }); expect(Object.keys(schema.methods).length).equal(0); }); }); describe('modify / access paths table', () => { - it ('should read', function() { - let data = {keyname: {type: 'string'}}; - let schema = new Schema(data); + it('should read', () => { + const data = { keyname: { type: 'string' } }; + const schema = new Schema(data); - let pathValue = schema.path('keyname'); + const pathValue = schema.path('keyname'); expect(pathValue).to.equal(data.keyname); }); - it ('should not return anything if does not exist', () => { - let schema = new Schema({}); + it('should not return anything if does not exist', () => { + const schema = new Schema({}); - let pathValue = schema.path('keyname'); + const pathValue = schema.path('keyname'); - expect(pathValue).to.not.exist; + assert.isUndefined(pathValue); }); - it ('should set', function() { - let schema = new Schema({}); - schema.path('keyname', {type:'string'}); + it('should set', () => { + const schema = new Schema({}); + schema.path('keyname', { type: 'string' }); - expect(schema.paths.keyname).to.exist; + assert.isDefined(schema.paths.keyname); }); - it ('should not allow to set reserved key', function() { - let schema = new Schema({}); + it('should not allow to set reserved key', () => { + const schema = new Schema({}); - let fn = () => { + const fn = () => { schema.path('ds', {}); }; @@ -147,52 +135,32 @@ describe('Schema', () => { describe('callQueue', () => { it('should add pre hooks to callQueue', () => { - let schema = new Schema({}); - schema.callQueue = []; + const preMiddleware = () => { }; + const schema = new Schema({}); + schema.callQueue = { model: {}, entity: {} }; - schema.pre('save', (next) => { - next(); - }); + schema.pre('save', preMiddleware); - expect(schema.callQueue.length).equal(1); + assert.isDefined(schema.callQueue.entity.save); + expect(schema.callQueue.entity.save.pres[0]).equal(preMiddleware); }); it('should add post hooks to callQueue', () => { - let schema = new Schema({}); - schema.callQueue = []; - - schema.post('save', (next) => { - next(); - }); - - expect(schema.callQueue.length).equal(1); - }); - }); - - describe('query hooks', () => { - it('should add pre findOne query hook to Kareem', () => { - let schema = new Schema({}); - - schema.pre('findOne', (next) => { - next(); - }); - - expect(schema.s.hooks._pres.findOne).to.exist; - }); - - it('should add post findOne query hook to Kareem', () => { - let schema = new Schema({}); + const postMiddleware = () => { }; + const schema = new Schema({}); + schema.callQueue = { model: {}, entity: {} }; - schema.post('findOne', () => {}); + schema.post('save', postMiddleware); - expect(schema.s.hooks._posts.findOne).to.exist; + assert.isDefined(schema.callQueue.entity.save); + expect(schema.callQueue.entity.save.post[0]).equal(postMiddleware); }); }); describe('virtual()', () => { it('should create new VirtualType', () => { - var schema = new Schema({}); - var fn = () => {}; + const schema = new Schema({}); + const fn = () => {}; schema.virtual('fullname', fn); expect(schema.virtuals.fullname.constructor.name).equal('VirtualType'); @@ -200,12 +168,12 @@ describe('Schema', () => { }); it('add shortCut queries settings', () => { - let schema = new Schema({}); - let listQuerySettings = {limit:10, filters:[]}; + const schema = new Schema({}); + const listQuerySettings = { limit: 10, filters: [] }; schema.queries('list', listQuerySettings); - expect(schema.shortcutQueries.list).to.exist; + assert.isDefined(schema.shortcutQueries.list); expect(schema.shortcutQueries.list).to.equal(listQuerySettings); }); }); diff --git a/test/serializers/datastore-test.js b/test/serializers/datastore-test.js index 17d5803..2dac385 100644 --- a/test/serializers/datastore-test.js +++ b/test/serializers/datastore-test.js @@ -1,93 +1,86 @@ 'use strict'; const chai = require('chai'); -const expect = chai.expect; const gstore = require('../../lib'); - const Schema = require('../../lib').Schema; const datastoreSerializer = require('../../lib/serializer').Datastore; -describe('Datastore serializer', () => { +const expect = chai.expect; +const assert = chai.assert; - var ModelInstance; +describe('Datastore serializer', () => { + let ModelInstance; - beforeEach(function() { - gstore.models = {}; + beforeEach(() => { + gstore.models = {}; gstore.modelSchemas = {}; - var schema = new Schema({ - name: {type: 'string'}, - email : {type:'string', read:false} + const schema = new Schema({ + name: { type: 'string' }, + email: { type: 'string', read: false }, }); ModelInstance = gstore.model('Blog', schema, {}); }); - describe('should convert data FROM Datastore format', function() { + describe('should convert data FROM Datastore format', () => { let datastoreMock; - let legacyDatastoreMock; - let entity; const key = { namespace: undefined, id: 1234, - kind: "BlogPost", - path: ["BlogPost", 1234] + kind: 'BlogPost', + path: ['BlogPost', 1234], }; let data; - beforeEach(function() { + beforeEach(() => { data = { - name: "John", - lastname : 'Snow', - email : 'john@snow.com' + name: 'John', + lastname: 'Snow', + email: 'john@snow.com', }; datastoreMock = data; datastoreMock[ModelInstance.gstore.ds.KEY] = key; + }); - legacyDatastoreMock = { - key: key, - data: data - }; - }) - - it ('and add Symbol("KEY") id to entity', () => { - var serialized = datastoreSerializer.fromDatastore.call(ModelInstance, datastoreMock); + it('and add Symbol("KEY") id to entity', () => { + const serialized = datastoreSerializer.fromDatastore.call(ModelInstance, datastoreMock); - //expect(serialized).equal = datastoreMock; + // expect(serialized).equal = datastoreMock; expect(serialized.id).equal(key.id); - expect(serialized.email).not.exist; + assert.isUndefined(serialized.email); }); it('accepting "readAll" param', () => { - var serialized = datastoreSerializer.fromDatastore.call(ModelInstance, datastoreMock, true); + const serialized = datastoreSerializer.fromDatastore.call(ModelInstance, datastoreMock, true); - expect(serialized.email).exist; + assert.isDefined(serialized.email); }); }); - describe ('should convert data TO Datastore format', () => { - it ('without passing non-indexed properties', () => { - var expected = { - name:'name', - value:'John', - excludeFromIndexes:false + describe('should convert data TO Datastore format', () => { + it('without passing non-indexed properties', () => { + const expected = { + name: 'name', + value: 'John', + excludeFromIndexes: false, }; - var serialized = datastoreSerializer.toDatastore({name:'John'}); + const serialized = datastoreSerializer.toDatastore({ name: 'John' }); expect(serialized[0]).to.deep.equal(expected); }); - it ('and not into account undefined variables', () => { - var serialized = datastoreSerializer.toDatastore({name:'John', lastname:undefined}); - expect(serialized[0].lastname).to.not.exist; + it('and not into account undefined variables', () => { + const serialized = datastoreSerializer.toDatastore({ name: 'John', lastname: undefined }); + assert.isUndefined(serialized[0].lastname); }); - it ('and set excludeFromIndexes properties', () => { - var serialized = datastoreSerializer.toDatastore({name:'John'}, ['name']); - expect(serialized[0].excludeFromIndexes).to.be.true; + it('and set excludeFromIndexes properties', () => { + const serialized = datastoreSerializer.toDatastore({ name: 'John' }, ['name']); + expect(serialized[0].excludeFromIndexes).equal(true); }); }); }); diff --git a/test/utils-test.js b/test/utils-test.js index c4309bf..82a97d1 100644 --- a/test/utils-test.js +++ b/test/utils-test.js @@ -1,35 +1,24 @@ -var chai = require('chai'); -var expect = chai.expect; -var utils = require('../lib/utils'); +'use strict'; -describe('Utils', () => { - "use strict"; - - describe('should create shallow copy of default options', () => { - it ('passing existing options', () => { - var options = {sendMail:true}; - var defaultOptions = {modified:false}; - - options = utils.options(defaultOptions, options); - - expect(options.modified).to.exist; - }); - - it ('not overriding an existing key', () => { - var options = {modified:true}; - var defaultOptions = {modified:false}; +const chai = require('chai'); +const utils = require('../lib/utils'); - options = utils.options(defaultOptions, options); +const expect = chai.expect; - expect(options.modified).to.be.true; - }); - - it ('and create object if nothing is passed', () => { - var defaultOptions = {modified:false}; - - var options = utils.options(defaultOptions); - - expect(options.modified).to.exist; +describe('Utils', () => { + describe('promisify()', () => { + it('should not promisify methods already promisified', () => { + class Test { + save() { + return Promise.resolve(this); + } + } + + Test.prototype.save.__promisified = true; + const ref = Test.prototype.save; + Test.save = utils.promisify(Test.prototype.save); + + expect(Test.save).equal(ref); }); }); }); diff --git a/test/virtualType-test.js b/test/virtualType-test.js index 537cc93..55606d3 100644 --- a/test/virtualType-test.js +++ b/test/virtualType-test.js @@ -1,58 +1,57 @@ 'use strict'; -var chai = require('chai'); -var expect = chai.expect; -var sinon = require('sinon'); +const chai = require('chai'); +const VirtualType = require('../lib/virtualType'); -var VirtualType = require('../lib/virtualType'); +const expect = chai.expect; -describe('VirtualType', function() { +describe('VirtualType', () => { it('should add function to getter array', () => { - var virtualType = new VirtualType('fullname'); + const virtualType = new VirtualType('fullname'); - virtualType.get(() => {}); + virtualType.get(() => { }); - expect(virtualType.getter).not.be.null; + expect(virtualType.getter).not.equal(null); }); it('should throw error if not passing a function', () => { - var virtualType = new VirtualType('fullname'); + const virtualType = new VirtualType('fullname'); - let fn = () => { + const fn = () => { virtualType.get('string'); - } + }; expect(fn).throw(Error); }); it('should add function to setter array', () => { - var virtualType = new VirtualType('fullname'); + const virtualType = new VirtualType('fullname'); - virtualType.set(() => {}); + virtualType.set(() => { }); - expect(virtualType.setter).not.be.null; + expect(virtualType.setter).not.equal(null); }); it('should throw error if not passing a function', () => { - var virtualType = new VirtualType('fullname'); + const virtualType = new VirtualType('fullname'); - let fn = () => { + const fn = () => { virtualType.set('string'); - } + }; expect(fn).throw(Error); }); it('should applyGetter with scope', () => { - var virtualType = new VirtualType('fullname'); + const virtualType = new VirtualType('fullname'); - virtualType.get(function() { - return this.name + ' ' + this.lastname; + virtualType.get(function getName() { + return `${this.name} ${this.lastname}`; }); - var entityData = { + const entityData = { name: 'John', - lastname : 'Snow' + lastname: 'Snow', }; virtualType.applyGetters(entityData); @@ -60,24 +59,24 @@ describe('VirtualType', function() { }); it('should return null if no getter', () => { - var virtualType = new VirtualType('fullname'); + const virtualType = new VirtualType('fullname'); - var entityData = {}; + const entityData = {}; - let v = virtualType.applyGetters(entityData); - expect(v).be.null; + const v = virtualType.applyGetters(entityData); + expect(v).equal(null); }); it('should applySetter with scope', () => { - var virtualType = new VirtualType('fullname'); + const virtualType = new VirtualType('fullname'); - virtualType.set(function(name) { - let split = name.split(' '); + virtualType.set(function setName(name) { + const split = name.split(' '); this.firstname = split[0]; - this.lastname = split[1]; + this.lastname = split[1]; }); - var entityData = {}; + const entityData = {}; virtualType.applySetters('John Snow', entityData); expect(entityData.firstname).equal('John'); @@ -85,11 +84,11 @@ describe('VirtualType', function() { }); it('should not do anything if no setter', () => { - var virtualType = new VirtualType('fullname'); + const virtualType = new VirtualType('fullname'); - var entityData = {}; + const entityData = {}; virtualType.applySetters('John Snow', entityData); expect(Object.keys(entityData).length).equal(0); }); -}); \ No newline at end of file +});