8000 Switch to passing only the current-change in 'changes' emit by pik · Pull Request #387 · js-data/js-data · GitHub
[go: up one dir, main page]

Skip to content

Switch to passing only the current-change in 'changes' emit #387

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 11, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/Mapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,16 @@ const MAPPER_DEFAULTS = {
*/
idAttribute: 'id',

/**
* Whether records created from this mapper keep changeHistory on property changes.
*
* @default true
* @name Mapper#keepChangeHistory
* @since 3.0.0
* @type {boolean}
*/
keepChangeHistory: true,

/**
* Whether this Mapper should emit operational events.
*
Expand Down
2 changes: 2 additions & 0 deletions src/Record.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ function Record (props, opts) {
if (id !== undefined) {
utils.set(this, mapper.idAttribute, id)
}
const keepChangeHistory = opts.keepChangeHistory !== undefined ? opts.keepChangeHistory : (mapper ? mapper.keepChangeHistory : true)
_set('keepChangeHistory', keepChangeHistory)
utils.fillIn(this, props)
_set('creating', false)
_set('noValidate', false)
Expand Down
21 changes: 14 additions & 7 deletions src/Schema.js
Original file line number Diff line number Diff line change
10000 Expand Up @@ -743,6 +743,8 @@ const creatingPath = 'creating'
const eventIdPath = 'eventId'
// boolean - Whether to skip validation for a Record's currently changing property
const noValidatePath = 'noValidate'
// boolean - Whether to preserve Change History for a Record
const keepChangeHistoryPath = 'keepChangeHistory'
// boolean - Whether to skip change notification for a Record's currently
// changing property
const silentPath = 'silent'
Expand Down Expand Up @@ -787,7 +789,6 @@ const makeDescriptor = function (prop, schema, opts) {
const _get = this[getter]
const _set = this[setter]
const _unset = this[unsetter]

// Optionally check that the new value passes validation
if (!_get(noValidatePath)) {
const errors = schema.validate(value, { path: [prop] })
Expand All @@ -802,6 +803,8 @@ const makeDescriptor = function (prop, schema, opts) {
// TODO: Make it so tracking can be turned on for all properties instead of
// only per-property
if (track && !_get(creatingPath)) {
// previous is versioned on database commit
// props are versioned on set()
const previous = _get(previousPath)
const current = _get(keyPath)
let changing = _get(changingPath)
Expand Down Expand Up @@ -853,12 +856,16 @@ const makeDescriptor = function (prop, schema, opts) {
for (i = 0; i < changed.length; i++) {
this.emit('change:' + changed[i], this, utils.get(this, changed[i]))
}
const changes = this.changes()
const changeRecord = utils.plainCopy(changes)
changeRecord.timestamp = new Date().getTime()
const changeHistory = _get(changeHistoryPath) || []
_set(changeHistoryPath, changeHistory)
changeHistory.push(changeRecord)

const changes = utils.diffObjects({ [prop] : value }, { [prop] : current })

if (_get(keepChangeHistoryPath)) {
const changeRecord = utils.plainCopy(changes)
changeRecord.timestamp = new Date().getTime()
let changeHistory = _get(changeHistoryPath)
!changeHistory && _set(changeHistoryPath, (changeHistory = []))
changeHistory.push(changeRecord)
}
this.emit('change', this, changes)
}
_unset(silentPath)
Expand Down
45 changes: 43 additions & 2 deletions test/unit/record/hasChanges.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { assert, JSData } from '../../_setup'
import { assert, JSData, sinon } from '../../_setup'

describe('Record#hasChanges', function () {
it('should be an instance method', function () {
Expand All @@ -13,7 +13,7 @@ describe('Record#hasChanges', function () {
post.author = 'Jake'
assert(post.hasChanges())
})
it('should return true if a tracked field is changed', function () {
it('should return true if a tracked field is changed', function (done) {
const PostMapper = new JSData.Mapper({
name: 'post',
schema: {
Expand All @@ -26,10 +26,51 @@ describe('Record#hasChanges', function () {
}
})
const post = PostMapper.createRecord(this.data.p1)
const listener = sinon.stub()
post.on('change', listener)
assert(!post.hasChanges())
post.author = 'Jake'
assert(post.hasChanges())
post.author = 'John'
assert(!post.hasChanges())
setTimeout(function() {
assert.equal(listener.callCount, 0)
done()
}, 5)
}),

/* The previous test has a property set and changed back within a single event loop
* So no listener is ever called.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just added a code comment to clarify the reason for adding the second test.

* This test checks that hasChanges() is still false (if the state is set back to the previous)
* even if both changes were registered and a listener was called on each change (twice in total).
*/

it('is not affected by timing', function(done) {
const PostMapper = new JSData.Mapper({
name: 'post',
schema: {
properties: {
author: {
type: 'string',
track: true
}
}
}
})
const 8000 post = PostMapper.createRecord(this.data.p1)
const listener = sinon.stub()
post.on('change', listener)
post.author = 'Jake'
assert(post.hasChanges())
const secondSpec = function() {
assert.equal(listener.callCount, 2)
assert(!post.hasChanges())
done()
}
setTimeout(function () {
assert.equal(listener.callCount, 1)
post.author = 'John'
setTimeout(secondSpec, 5)
}, 5)
})
})
86 changes: 86 additions & 0 deletions test/unit/record/on.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { assert, JSData, sinon } from '../../_setup'
const { Record } = JSData

describe("Record#on('change')", function() {
it('Tracking changes to a single property', function(done) {
const Store = new JSData.DataStore()
const Foo = Store.defineMapper('foo', {
schema: {
properties: {
id: { type: 'number' },
name: { type: 'string', track: true }
}
},
})
const foo = Foo.createRecord()
const listener = sinon.stub()
foo.on('change', listener)
foo.name = 'new foo'
const secondSpec = function() {
foo.name = 'updated foo'
setTimeout(function () {
const [record, changes] = listener.args[1]
assert.equal(foo, record, "on 'change' listener called with record which was modified")
assert.isTrue(listener.calledTwice, "on 'change' listener was called when modifying a property")
assert.equal(Object.keys(changes.added).length, 0)
assert.equal(Object.keys(changes.removed).length, 0)
assert.equal(changes.changed.name, 'updated foo', "Only the property changed was emitted in the changeSet")
done()
}, 5)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw the other tests using timeout - but something like an eventloop.after(function() {}) would be a preferable approach for testing, not sure how difficult that is to do ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, something like this might work:

setImmediate(function () {...});

however, the tests need to work in a bunch of different browsers, and I'm not sure how cross-browser setImmediate is, so setTimeout might be just be good enough. See https://github.com/js-data/js-data/blob/master/karma.conf.js#L3

}
setTimeout(function () {
const [record, changes] = listener.args[0]
assert.equal(foo, record, "on 'change' listener called with record which was modified")
assert.isTrue(listener.calledOnce, "on 'change' listener was called when modifying a property")
assert.equal(Object.keys(changes.changed).length, 0)
assert.equal(Object.keys(changes.removed).length, 0)
assert.equal(changes.added.name, 'new foo', "Only the property changed was emitted in the changeSet")
secondSpec()
}, 5)
})

it('keepChangeHistory: false', function(done) {
const Store = new JSData.DataStore()

class FooRecord extends Record {
constructor(props, opts) {
super(props, opts)
this._set('keepChangeHistory', false)
this._set('noValidate', true)
}
}

const Foo = Store.defineMapper('foo', {
recordClass: FooRecord,
schema: {
properties: {
id: { type: 'number' },
name: { type: 'string', track: true }
}
},
})

const Bar = Store.defineMapper('bar', {
schema: {
properties: {
id: { type: 'number' },
name: { type: 'string', track: true }
}
},
})

const foo = Foo.createRecord()
const bar = Bar.createRecord()
const listener = sinon.stub()
foo.on('change', listener)
bar.on('change', listener)
foo.name = 'new foo'
bar.name = 'new bar'
setTimeout(function () {
assert.isTrue(listener.calledTwice, "on 'change' listener was called when modifying properties")
assert.deepEqual(foo.changeHistory(), [], "no changeHistory was stored if keepChangeHistory: false is set")
assert.equal(bar.changeHistory().length, 1, "if keepChangeHistory is true by default changeHistory is present")
done()
}, 5)
})
})
0