Repository: maxnowack/signaldb (ts)

Issue:
How to replace a newly created row?
I'm dealing with a situation where I insert a skeleton row into a `ReplicatedCollection`. A skeleton row is a bare minimum that the server needs like an `id` and a `name`. The collection calls its `push` hook and the skeleton row gets sent to the server. The server then notifies all clients that a new row was created. This new row includes other fields added by the server like `createdAt`, `updatedAt`, and `createdBy`. The collection's `onChange` gets called with `{ added: [newCompleteRow], modified: [], removed: [] }`. My issue happens because the complete row added by `onChange` does not replace the initial skeleton row, and I end up with 2 rows with the same `id`.

There are a few possible solutions to my problem.

First, I could figure out if the current client is the one that initially inserted the skeleton row and swap the row (with `modified`) instead of simply adding it. I would rather not meddle with the backend.

The second option is that when `onChange` gets called with `added` rows, they should be upserted into the collection by the row's `id`. If a row is truly new it gets inserted otherwise it swaps the skeleton row.

A third option is to let the persistence adapter/replicated collection's `push`  hook have access to the collection object so I can manually `findOne` by `id` and swap the row if it already exists.

Any idea how to go about this?

Pull request description:
fix: upsert items that are added but already present
resolves #792

Patch:
diff --git a/packages/signaldb/src/Collection/index.ts b/packages/signaldb/src/Collection/index.ts
index 8eb7138..962aa5f 100644
--- a/packages/signaldb/src/Collection/index.ts
+++ b/packages/signaldb/src/Collection/index.ts
@@ -158,6 +158,13 @@ export default class Collection<T extends BaseItem<I> = BaseItem, I = any, U = T
           })
         } else if (changes) {
           changes.added.forEach((item) => {
+            const index = this.memory().findIndex(doc => doc.id === item.id)
+            if (index >= 0) { // item already exists; doing upsert
+              this.memory().splice(index, 1, item)
+              return
+            }
+
+            // item does not exists yet; normal insert
             this.memory().push(item)
             const itemIndex = this.memory().findIndex(doc => doc === item)
             this.idIndex.set(serializeValue(item.id), new Set([itemIndex]))


Test patch:
diff --git a/packages/signaldb/__tests__/persistence.spec.ts b/packages/signaldb/__tests__/persistence.spec.ts
index 0b88726..f6d9fe9 100644
--- a/packages/signaldb/__tests__/persistence.spec.ts
+++ b/packages/signaldb/__tests__/persistence.spec.ts
@@ -405,4 +405,18 @@ describe('Persistence', () => {
     expect(onChange).toBeCalledTimes(1)
     expect(collection.find().fetch()).toEqual([{ id: '1', name: 'John' }, { id: '2', name: 'Jane' }])
   })
+
+  it('should upsert added items from persistence adapter', async () => {
+    const persistence = memoryPersistenceAdapter([{ id: '1', name: 'John' }], true)
+    const collection = new Collection({ persistence })
+    await waitForEvent(collection, 'persistence.init')
+    const items = collection.find().fetch()
+    expect(items).toEqual([{ id: '1', name: 'John' }])
+
+    persistence.addNewItem({ id: '1', name: 'Jane' })
+    await waitForEvent(collection, 'persistence.received')
+    expect(collection.find().fetch()).toEqual([
+      { id: '1', name: 'Jane' },
+    ])
+  })
 })