diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..7053dc1 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +/coverage/ diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..33943e4 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,4 @@ +{ + "extends": "fullcube", + "root": true +} diff --git a/.gitignore b/.gitignore index ddc6a59..1fd04da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -npm-debug.log -node_modules/ -.idea +node_modules +coverage +.nyc_output diff --git a/.jscsrc b/.jscsrc deleted file mode 100644 index f21b369..0000000 --- a/.jscsrc +++ /dev/null @@ -1,23 +0,0 @@ -{ - "preset": "google", - "excludeFiles": ["node_modules"], - "requireCurlyBraces": [ - "else", - "for", - "while", - "do", - "try", - "catch" - ], - "disallowMultipleVarDecl": "exceptUndefined", - "disallowSpacesInsideObjectBrackets": null, - "maximumLineLength": { - "value": 120, - "allowComments": true, - "allowRegex": true - }, - "jsDoc": { - "checkParamNames": true, - "requireParamTypes": true - } -} diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 0be18ac..0000000 --- a/.jshintrc +++ /dev/null @@ -1,26 +0,0 @@ -{ - "node": true, - "esnext": true, - "bitwise": true, - "camelcase": true, - "eqeqeq": true, - "eqnull": true, - "immed": true, - "indent": 2, - "latedef": "nofunc", - "newcap": true, - "nonew": true, - "noarg": true, - "quotmark": "single", - "regexp": true, - "undef": true, - "unused": false, - "trailing": true, - "sub": true, - "maxlen": 120, - "predef": ["-prompt"], - "browser": true, - "devel": true, - "mocha": true, - "expr": true -} diff --git a/lib/calculated.js b/lib/calculated.js index 605ba8e..313c96b 100644 --- a/lib/calculated.js +++ b/lib/calculated.js @@ -1,115 +1,118 @@ -'use strict'; +'use strict' -var debug = require('debug')('loopback-ds-calculated-mixin'); -var _ = require('lodash'); -var Promise = require('bluebird'); +const debug = require('debug')('loopback-ds-calculated-mixin') +const _ = require('lodash') +const Promise = require('bluebird') function getPropertyConfig(property) { if (typeof property === 'string') { return { callback: property, - }; + } } - return property; + return property } -module.exports = function(Model, options) { +module.exports = (Model, options) => { // Sanity Checks - verify that the defined properties and callbacks exist on the model and log a message if not. - _.forEach(options.properties, function(options, property) { - var config = getPropertyConfig(options); + _.forEach(options.properties, (config, property) => { + config = getPropertyConfig(config) if (_.isUndefined(Model.definition.properties[property])) { - debug('Property %s on %s is undefined', property, Model.modelName); + debug('Property %s on %s is undefined', property, Model.modelName) } if (_.isUndefined(config.callback)) { - debug('Callback on %s is undefined', property); + debug('Callback on %s is undefined', property) } if (typeof Model[config.callback] !== 'function') { - debug('Callback %s for %s is not a model function', config.callback, property); + debug('Callback %s for %s is not a model function', config.callback, property) } - }); + }) - debug('Calculated mixin for Model %s with options %o', Model.modelName, options); + debug('Calculated mixin for Model %s with options %o', Model.modelName, options) // The loaded observer is triggered when an item is loaded - Model.observe('before save', function(ctx, next) { + Model.observe('before save', (ctx, next) => { // Allow user to bypass calculation by setting `skipCalculated` option. if (_.get(ctx, 'options.skipCalculated')) { - debug('Not calculating properties %s (skipCalculated was set)'); - return next(); + debug('Not calculating properties %s (skipCalculated was set)') + return next() } - var data = ctx.data || ctx.instance; - var instance = ctx.instance || ctx.currentInstance; + const data = ctx.data || ctx.instance + let instance = ctx.instance || ctx.currentInstance // upsert or updateAll detected - We only act on single model updates. if (!instance) { - var instanceId = ctx.where[Model.getIdName()]; + const instanceId = ctx.where[Model.getIdName()] + if (instanceId) { - instance = ctx.data; - instance = new Model(instance); - } else { - debug('Not calculating properties (updateAll not supported)'); - return next(); + instance = ctx.data + instance = new Model(instance) + } + else { + debug('Not calculating properties (updateAll not supported)') + return next() } } - return Promise.map(Object.keys(options.properties), function(property) { - var config = getPropertyConfig(options.properties[property]); - var callback = config.callback; - var action = ctx.isNewInstance ? 'Calculating' : 'Recalculating'; + return Promise.map(Object.keys(options.properties), property => { + const config = getPropertyConfig(options.properties[property]) + const callback = config.callback + const action = ctx.isNewInstance ? 'Calculating' : 'Recalculating' if (!callback) { - debug('Callback not defined for property %s', property); - return false; + debug('Callback not defined for property %s', property) + return null } if (typeof Model[callback] !== 'function') { - debug('Function %s not found on Model', callback); - return false; + debug('Function %s not found on Model', callback) + return null } if (!ctx.isNewInstance && !config.recalculateOnUpdate) { - debug('Not recalculating property %s (not a new instance and recalculateOnUpdate not set)', property); - return false; + debug('Not recalculating property %s (not a new instance and recalculateOnUpdate not set)', property) + return null } // Convert the data to be plain object to avoid pollutions. - var instanceCopy = instance.toObject(true); + let instanceCopy = instance.toObject(true) // Apply changes from partial update to make available for use in calculation. if (ctx.data) { - instanceCopy = Object.assign(instanceCopy, ctx.data); + instanceCopy = Object.assign(instanceCopy, ctx.data) } // Make the clone an model instance to pass through to callback. - instanceCopy = new Model(instanceCopy); + instanceCopy = new Model(instanceCopy) - debug(`${action} ${property} property for ${Model.modelName} ${instanceCopy.getId()} with callback ${callback}`); + debug(`${action} ${property} property for ${Model.modelName} ${instanceCopy.getId()} with callback ${callback}`) - var value = Model[callback](instanceCopy); + const value = Model[callback](instanceCopy) if (typeof value === 'undefined') { - debug('Callback returned undefined. Not setting property'); - return false; - } else if (!_.get(value, 'then')) { - debug('Setting property %s to %s', property, value); - data[property] = value; - } else { + debug('Callback returned undefined. Not setting property') + return null + } + if (_.get(value, 'then')) { return value - .then(function(res) { + .then(res => { if (typeof res === 'undefined') { - debug('Callback returned undefined. Not setting property'); - return false; + debug('Callback returned undefined. Not setting property') + return null } - debug('Setting property %s to %s', property, res); - data[property] = res; - return data; - }); + debug('Setting property %s to %s', property, res) + data[property] = res + return null + }) } - }); - }); -}; + debug('Setting property %s to %s', property, value) + data[property] = value + return null + }) + }) +} diff --git a/lib/index.js b/lib/index.js index 96af9c0..584e4e0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,9 +1,9 @@ -var deprecate = require('depd')('loopback-ds-calculated-mixin'); -var calculated = require('./calculated'); +const deprecate = require('depd')('loopback-ds-calculated-mixin') +const calculated = require('./calculated') module.exports = function mixin(app) { - 'use strict'; + 'use strict' app.loopback.modelBuilder.mixins.define = deprecate.function(app.loopback.modelBuilder.mixins.define, - 'app.modelBuilder.mixins.define: Use mixinSources instead'); - app.loopback.modelBuilder.mixins.define('Calculated', calculated); -}; + 'app.modelBuilder.mixins.define: Use mixinSources instead') + app.loopback.modelBuilder.mixins.define('Calculated', calculated) +} diff --git a/package.json b/package.json index d378ff5..ef857df 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "url": "https://github.com/fullcube/loopback-ds-calculated-mixin.git" }, "scripts": { - "lint": "jscs lib && jshint lib", - "test": "mocha -R spec --timeout 10000 test.js", + "lint": "eslint .", + "test": "NODE_ENV=test nyc --reporter=lcov --reporter=text --reporter=text-summary mocha test/*test.js", "test:watch": "npm run test -- -w", "pretest": "npm run lint", "semantic-release": "semantic-release pre && npm publish && semantic-release post" @@ -30,12 +30,14 @@ "chai": "latest", "chai-datetime": "^1.4.1", "condition-circle": "^1.5.0", - "jscs": "latest", - "jshint": "latest", + "dirty-chai": "^1.2.2", + "eslint": "^2.11.1", + "eslint-config-fullcube": "^1.0.46", "loopback": "^3.6.0", "loopback-datasource-juggler": "^3.5.0", "mocha": "latest", "mocha-sinon": "latest", + "nyc": "^10.2.0", "semantic-release": "^6.3.2", "sinon": "latest", "sinon-chai": "latest" diff --git a/test/.eslintrc b/test/.eslintrc new file mode 100644 index 0000000..8727dc9 --- /dev/null +++ b/test/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "fullcube/mocha" +} diff --git a/test.js b/test/test.js similarity index 62% rename from test.js rename to test/test.js index c3e3893..38f87bc 100644 --- a/test.js +++ b/test/test.js @@ -1,30 +1,28 @@ -/* jshint mocha: true */ +const utils = require('loopback-datasource-juggler/lib/utils') +const loopback = require('loopback') -var debug = require('debug')('loopback-ds-calculated-mixin'); -var utils = require('loopback-datasource-juggler/lib/utils'); +const chai = require('chai') +const expect = chai.expect -var loopback = require('loopback'); +chai.use(require('chai-datetime')) +chai.use(require('dirty-chai')) +chai.use(require('sinon-chai')) -var chai = require('chai'); -var expect = chai.expect; -var sinon = require('sinon'); -chai.use(require('chai-datetime')); -chai.use(require('sinon-chai')); -require('mocha-sinon'); +require('mocha-sinon') // Create a new loopback app. -var app = loopback(); +const app = loopback() // Set up promise support for loopback in non-ES6 runtime environment. -global.Promise = require('bluebird'); +global.Promise = require('bluebird') -// import our Changed mixin. -require('./')(app); +// import our mixin. +require('../lib')(app) // Connect to db -var dbConnector = loopback.memory(); +const dbConnector = loopback.memory() -var Item = loopback.PersistedModel.extend('item', { +const Item = loopback.PersistedModel.extend('item', { name: String, status: String, readonly: Boolean, @@ -41,38 +39,38 @@ var Item = loopback.PersistedModel.extend('item', { }, promised: 'calculatePromised', }, - } - } -}); + }, + }, +}) Item.calculateReadonly = function calculateReadonly(item) { - return item.status === 'archived'; -}; + return item.status === 'archived' +} -Item.calculateCreated = function calculateCreated(item) { - return new Date(); -}; +Item.calculateCreated = function calculateCreated() { + return new Date() +} Item.calculatePromised = function calculatePromised(item, cb) { - cb = cb || utils.createPromiseCallback(); + cb = cb || utils.createPromiseCallback() process.nextTick(function() { - cb(null, 'As promised I get back to you!'); - }); - return cb.promise; -}; + cb(null, 'As promised I get back to you!') + }) + return cb.promise +} // Attach model to db -Item.attachTo(dbConnector); -app.model(Item); -app.use(loopback.rest()); -app.set('legacyExplorer', false); +Item.attachTo(dbConnector) +app.model(Item) +app.use(loopback.rest()) +app.set('legacyExplorer', false) beforeEach(function() { this.sinon.spy(Item, 'calculateReadonly') this.sinon.spy(Item, 'calculateCreated') this.sinon.spy(Item, 'calculatePromised') - this.clock = this.sinon.useFakeTimers(); -}); + this.clock = this.sinon.useFakeTimers() +}) describe('Creating new items', function() { describe('findOrCreate', function() { @@ -82,32 +80,32 @@ describe('Creating new items', function() { status: 'new', }) .then(item => (this.res = item[0])) - }); + }) describe('basic', function() { it('should call the defined callback once', function() { - expect(Item.calculateCreated).to.have.been.calledOnce - }); + expect(Item.calculateCreated).to.have.been.calledOnce() + }) it('should set property to the callbacks return value', function() { expect(new Date(this.res.created)).to.equalDate(new Date('1970-01-01')) - }); - }); + }) + }) describe('promise', function() { it('should call the defined callback once', function() { - expect(Item.calculatePromised).to.have.been.calledOnce - }); + expect(Item.calculatePromised).to.have.been.calledOnce() + }) it('should set property to the callbacks return value', function() { - expect(this.res.promised).to.equal('As promised I get back to you!'); - }); + expect(this.res.promised).to.equal('As promised I get back to you!') + }) }) describe('recalculateOnUpdate', function() { it('should call the defined callback once', function() { - expect(Item.calculateReadonly).to.have.been.calledOnce - }); + expect(Item.calculateReadonly).to.have.been.calledOnce() + }) it('should set property to the callbacks return value', function() { - expect(this.res.readonly).to.equal(false); - }); - }); - }); + expect(this.res.readonly).to.equal(false) + }) + }) + }) describe('upsert', function() { beforeEach(function() { @@ -116,182 +114,186 @@ describe('Creating new items', function() { status: 'new', }) .then(item => (this.res = item)) - }); + }) describe('basic', function() { it('should call the defined callback once', function() { - expect(Item.calculateCreated).to.have.been.calledOnce - }); + expect(Item.calculateCreated).to.have.been.calledOnce() + }) it('should set property to the callbacks return value', function() { expect(new Date(this.res.created)).to.equalDate(new Date('1970-01-01')) - }); - }); + }) + }) describe('promise', function() { it('should call the defined callback once', function() { - expect(Item.calculatePromised).to.have.been.calledOnce - }); + expect(Item.calculatePromised).to.have.been.calledOnce() + }) it('should set property to the callbacks return value', function() { - expect(this.res.promised).to.equal('As promised I get back to you!'); - }); + expect(this.res.promised).to.equal('As promised I get back to you!') + }) }) describe('recalculateOnUpdate', function() { it('should call the defined callback once', function() { - expect(Item.calculateReadonly).to.have.been.calledOnce - }); + expect(Item.calculateReadonly).to.have.been.calledOnce() + }) it('should set property to the callbacks return value', function() { - expect(this.res.readonly).to.equal(false); - }); - }); - }); + expect(this.res.readonly).to.equal(false) + }) + }) + }) describe('save', function() { beforeEach(function() { - var item = new Item({ + const item = new Item({ name: 'Item', status: 'new', }) + return item.save() - .then(item => (this.res = item)); - }); + .then(updatedItem => (this.res = updatedItem)) + }) describe('basic', function() { it('should call the defined callback once', function() { - expect(Item.calculateCreated).to.have.been.calledOnce - }); + expect(Item.calculateCreated).to.have.been.calledOnce() + }) it('should set property to the callbacks return value', function() { expect(new Date(this.res.created)).to.equalDate(new Date('1970-01-01')) - }); - }); + }) + }) describe('promise', function() { it('should call the defined callback once', function() { - expect(Item.calculatePromised).to.have.been.calledOnce - }); + expect(Item.calculatePromised).to.have.been.calledOnce() + }) it('should set property to the callbacks return value', function() { - expect(this.res.promised).to.equal('As promised I get back to you!'); - }); + expect(this.res.promised).to.equal('As promised I get back to you!') + }) }) describe('recalculateOnUpdate', function() { it('should call the defined callback once', function() { - expect(Item.calculateReadonly).to.have.been.calledOnce - }); + expect(Item.calculateReadonly).to.have.been.calledOnce() + }) it('should set property to the callbacks return value', function() { - expect(this.res.readonly).to.equal(false); - }); - }); - }); -}); + expect(this.res.readonly).to.equal(false) + }) + }) + }) +}) describe('Updating existing items', function() { describe('save', function() { beforeEach(function() { - var item = new Item({ + const item = new Item({ name: 'Item', status: 'new', }) + return item.save() - .then(item => { - item.status = 'archived' - return item.save() + .then(updatedItem => { + updatedItem.status = 'archived' + return updatedItem.save() }) - .then(item => (this.res = item)); - }); + .then(updatedItem => (this.res = updatedItem)) + }) describe('basic', function() { it('should call the defined callback once', function() { - expect(Item.calculateCreated).to.have.been.calledOnce - }); + expect(Item.calculateCreated).to.have.been.calledOnce() + }) it('should set property to the callbacks return value', function() { expect(new Date(this.res.created)).to.equalDate(new Date('1970-01-01')) - }); - }); + }) + }) describe('promise', function() { it('should call the defined callback once', function() { - expect(Item.calculatePromised).to.have.been.calledOnce - }); + expect(Item.calculatePromised).to.have.been.calledOnce() + }) it('should set property to the callbacks return value', function() { - expect(this.res.promised).to.equal('As promised I get back to you!'); - }); + expect(this.res.promised).to.equal('As promised I get back to you!') + }) }) describe('recalculateOnUpdate', function() { it('should call the defined callback twice', function() { - expect(Item.calculateReadonly).to.have.been.calledTwice - }); + expect(Item.calculateReadonly).to.have.been.calledTwice() + }) it('should set property to the callbacks return value', function() { - expect(this.res.readonly).to.equal(true); - }); - }); - }); + expect(this.res.readonly).to.equal(true) + }) + }) + }) describe('upsert', function() { beforeEach(function() { - var item = new Item({ + const item = new Item({ name: 'Item', status: 'new', }) + return item.save() - .then(item => { - item.status = 'archived' - return Item.upsert(item) + .then(updatedItem => { + updatedItem.status = 'archived' + return Item.upsert(updatedItem) }) - .then(item => (this.res = item)); - }); + .then(updatedItem => (this.res = updatedItem)) + }) describe('basic', function() { it('should call the defined callback once', function() { - expect(Item.calculateCreated).to.have.been.calledOnce - }); + expect(Item.calculateCreated).to.have.been.calledOnce() + }) it('should set property to the callbacks return value', function() { expect(new Date(this.res.created)).to.equalDate(new Date('1970-01-01')) - }); - }); + }) + }) describe('promise', function() { it('should call the defined callback once', function() { - expect(Item.calculatePromised).to.have.been.calledOnce - }); + expect(Item.calculatePromised).to.have.been.calledOnce() + }) it('should set property to the callbacks return value', function() { - expect(this.res.promised).to.equal('As promised I get back to you!'); - }); + expect(this.res.promised).to.equal('As promised I get back to you!') + }) }) describe('recalculateOnUpdate', function() { it('should call the defined callback twice', function() { - expect(Item.calculateReadonly).to.have.been.calledTwice - }); + expect(Item.calculateReadonly).to.have.been.calledTwice() + }) it('should set property to the callbacks return value', function() { - expect(this.res.readonly).to.equal(true); - }); - }); - }); + expect(this.res.readonly).to.equal(true) + }) + }) + }) describe('updateAttributes', function() { beforeEach(function() { - var item = new Item({ + const item = new Item({ name: 'Item', status: 'new', }) + return item.save() - .then(item => item.updateAttribute('status', 'archived')) - .then(item => (this.res = item)); - }); + .then(updatedItem => updatedItem.updateAttribute('status', 'archived')) + .then(updatedItem => (this.res = updatedItem)) + }) describe('basic', function() { it('should call the defined callback once', function() { - expect(Item.calculateCreated).to.have.been.calledOnce - }); + expect(Item.calculateCreated).to.have.been.calledOnce() + }) it('should set property to the callbacks return value', function() { expect(new Date(this.res.created)).to.equalDate(new Date('1970-01-01')) - }); - }); + }) + }) describe('promise', function() { it('should call the defined callback once', function() { - expect(Item.calculatePromised).to.have.been.calledOnce - }); + expect(Item.calculatePromised).to.have.been.calledOnce() + }) it('should set property to the callbacks return value', function() { - expect(this.res.promised).to.equal('As promised I get back to you!'); - }); + expect(this.res.promised).to.equal('As promised I get back to you!') + }) }) describe('recalculateOnUpdate', function() { it('should call the defined callback twice', function() { - expect(Item.calculateReadonly).to.have.been.calledTwice - }); + expect(Item.calculateReadonly).to.have.been.calledTwice() + }) it('should set property to the callbacks return value', function() { - expect(this.res.readonly).to.equal(true); - }); - }); - }); + expect(this.res.readonly).to.equal(true) + }) + }) + }) -}); +})