Repository: js-data/js-data
Pull request #462 (2017-04-25 23:20:46)

Added onConflict: 'skip' behavior to Collection/ Simple Store.
Added onConflict: 'skip' behavior to Collection/ Simple Store.
Added test cases.

jmdobry: Is this ready for review?
kvandelden: Yes.
jmdobry: Linter and tests needs to to pass (CircleCI and Travis need to go green)
jmdobry: Fixes #454 

Patch:
diff --git a/src/Collection.js b/src/Collection.js
index 0a6a7c5..920f174 100644
--- a/src/Collection.js
+++ b/src/Collection.js
@@ -46,6 +46,7 @@ const COLLECTION_DEFAULTS = {
    * Possible values:
    * merge
    * replace
+   * skip
    *
    * Merge:
    *
@@ -58,6 +59,10 @@ const COLLECTION_DEFAULTS = {
    * record. Any top-level own properties of the existing record that are _not_
    * on the new record will be removed.
    *
+   * Skip:
+   *
+   * Ignore new record, keep existing record.
+   *
    * @name Collection#onConflict
    * @type {string}
    * @default "merge"
@@ -257,8 +262,8 @@ export default Component.extend({
         // Here, the currently visited record corresponds to a record already
         // in the collection, so we need to merge them
         const onConflict = opts.onConflict || this.onConflict
-        if (onConflict !== 'merge' && onConflict !== 'replace') {
-          throw utils.err(`${DOMAIN}#add`, 'opts.onConflict')(400, 'one of (merge, replace)', onConflict, true)
+        if (onConflict !== 'merge' && onConflict !== 'replace' && onConflict !== 'skip') {
+          throw utils.err(`${DOMAIN}#add`, 'opts.onConflict')(400, 'one of (merge, replace, skip)', onConflict, true)
         }
         const existingNoValidate = existing._get(noValidatePath)
         if (opts.noValidate) {
@@ -274,7 +279,8 @@ export default Component.extend({
             }
           })
           existing.set(record)
-        }
+        } // else if(onConflict === 'skip'){ do nothing }
+
         if (opts.noValidate) {
           // Restore previous `noValidate` value
           existing._set(noValidatePath, existingNoValidate)
diff --git a/src/SimpleStore.js b/src/SimpleStore.js
index 6266688..2cc2dde 100644
--- a/src/SimpleStore.js
+++ b/src/SimpleStore.js
@@ -949,15 +949,21 @@ const props = {
     self._completedQueries[name] = {}
     mapper.relationList || Object.defineProperty(mapper, 'relationList', { value: [] })
 
-    // The SimpleStore uses a subclass of Collection that is "SimpleStore-aware"
-    const collection = self._collections[name] = new self.collectionClass(null, { // eslint-disable-line
+    let collectionOpts = {
       // Make sure the collection has somewhere to store "added" timestamps
       _added: {},
       // Give the collection a reference to this SimpleStore
       datastore: self,
       // The mapper tied to the collection
       mapper
-    })
+    }
+
+    if (opts && ('onConflict' in opts)) {
+      collectionOpts.onConflict = opts.onConflict
+    }
+
+    // The SimpleStore uses a subclass of Collection that is "SimpleStore-aware"
+    const collection = self._collections[name] = new self.collectionClass(null, collectionOpts)  // eslint-disable-line
 
     const schema = mapper.schema || {}
     const properties = schema.properties || {}


Test patch:
diff --git a/test/unit/collection/add.test.js b/test/unit/collection/add.test.js
index dcad1f9..777334d 100644
--- a/test/unit/collection/add.test.js
+++ b/test/unit/collection/add.test.js
@@ -113,7 +113,7 @@ describe('Collection#add', function () {
     collection.add({ id: 1 })
     assert.throws(() => {
       collection.add({ id: 1 }, { onConflict: 'invalid_choice' })
-    }, Error, `[Collection#add:opts.onConflict] expected: one of (merge, replace), found: invalid_choice\nhttp://www.js-data.io/v3.0/docs/errors#400`)
+    }, Error, `[Collection#add:opts.onConflict] expected: one of (merge, replace, skip), found: invalid_choice\nhttp://www.js-data.io/v3.0/docs/errors#400`)
   })
   it('should respect opts.noValidate', function () {
     const mapper = new JSData.Mapper({
@@ -198,6 +198,36 @@ describe('Collection#add', function () {
       id: 5
     })
   })
+  it('should keep existing items', function () {
+    const collection = new JSData.Collection({ mapper: new JSData.Mapper({ name: 'user' }) })
+    const user = collection.add({ id: 1, foo: 'bar', beep: 'boop' })
+    assert.equal(user.id, 1)
+    assert.equal(user.foo, 'bar')
+    assert.equal(user.beep, 'boop')
+    let existing = collection.add({ id: 1, biz: 'baz', foo: 'BAR' }, { onConflict: 'skip' })
+    assert(user === existing)
+    assert.equal(user.id, 1)
+    assert.equal(user.foo, 'bar')
+    assert.equal(user.beep, 'boop')
+    assert(!user.biz)
+
+    const store = new JSData.DataStore()
+    store.defineMapper('test', {
+      onConflict: 'skip',
+      schema: {
+        properties: {
+          id: { type: 'string' },
+          count: { type: 'number' }
+        }
+      }
+    })
+
+    const test = store.createRecord('test', { id: 'abcd', count: 1 })
+    store.add('test', test)
+    const test2 = store.createRecord('test', { id: 'abcd', count: 2 })
+    store.add('test', test2)
+    assert.equal(store.get('test', 'abcd').count, 1)
+  })
   it('should inject 1,000 items', function () {
     let users = []
     for (var i = 0; i < 1000; i++) {