Repository: parse-community/Parse-SDK-JS (JavaScript)

Issue:
Keys are stripped upon subscription update with LiveQuery.select
### New Issue Checklist
<!--
    Check every following box [x] before submitting your issue.
    Click the "Preview" tab for better readability.
    Thanks for contributing to Parse Platform!
-->

- [x] I am not disclosing a [vulnerability](https://github.com/parse-community/parse-server/blob/master/SECURITY.md).
- [x] I am not just asking a [question](https://github.com/parse-community/.github/blob/master/SUPPORT.md).
- [x] I have searched through [existing issues](https://github.com/parse-community/Parse-SDK-JS/issues?q=is%3Aissue).
- [x] I can reproduce the issue with the latest versions of [Parse Server](https://github.com/parse-community/parse-server/releases) and the [Parse JS SDK](https://github.com/parse-community/Parse-SDK-JS/releases). <!-- We don't investigate issues for outdated releases. -->

### Issue Description
<!-- What is the specific issue? -->

When you have a LiveQuery using `.select`, the Parse JS SDK removes all other data from any existing matching object.

### Steps to reproduce
<!-- How can someone else reproduce the issue? -->

1. Create an object using .save, such as:

```js
const obj = new Parse.Object('Test')
obj.set('foo', 'bar');
await obj.save();
```

2. Use query.subscribe  on that object, using .select for certain keys

```js
const subscription = await new Parse.Query('Test').select('key').subscribe();
```
4. Trigger a subscription event from the cloud / a different client
5. Now `obj.get('foo')` will be undefined



### Actual Outcome
<!-- What outcome, for example query result, did you get? -->

Incoming Live Query payload overrides all keys

### Expected Outcome
<!-- What outcome, for example query result, did you expect? -->

New incoming changes to merge with existing object

###  Environment
<!-- Be specific with versions, don't use "latest" or semver ranges like "~x.y.z" or "^x.y.z". -->

Server
- Parse Server version: `5.3`
- Operating system: `macOS`
- Local or remote host (AWS, Azure, Google Cloud, Heroku, Digital Ocean, etc): `localhost`

Database
- System (MongoDB or Postgres): `mongo`
- Database version: `5`
- Local or remote host (MongoDB Atlas, mLab, AWS, Azure, Google Cloud, etc): `atlas`

Client
- Parse JS SDK version: `3.4.2`

### Logs
<!-- Include relevant logs here. Turn on additional logging by configuring VERBOSE=1 in your environment. -->



Discussion:

### Thanks for opening this issue!

- 🚀 You can help us to fix this issue faster by opening a pull request with a failing test. See our [Contribution Guide](https://github.com/parse-community/parse-server/blob/master/CONTRIBUTING.md) for how to make a pull request, or read our [New Contributor's Guide](https://blog.parseplatform.org/learn/tutorial/community/nodejs/2021/02/14/How-to-start-contributing-to-Parse-Server.html) if this is your first time contributing.

<!-- parse-issue-bot-meta-tag-id {"suggestPr":true,"thanksFeature":false,"thanksPr":false} -->


It seems that if an update event is triggered, `Parse.Object.fromJSON` with `override` is called:

https://github.com/parse-community/Parse-SDK-JS/blob/8115e959533d1676fe5e5551bc81888b21fc12ef/src/LiveQueryClient.js#L422

Which creates a new Parse.Object with the new keys, and then calls `object.clearServerData`:

https://github.com/parse-community/Parse-SDK-JS/blob/8115e959533d1676fe5e5551bc81888b21fc12ef/src/ParseObject.js#L1856

As `serverData` is the original object, all these keys are set to `undefined`, and the object is updated according to the LQ payload (which only has selected fields).

https://github.com/parse-community/Parse-SDK-JS/blob/8115e959533d1676fe5e5551bc81888b21fc12ef/src/ParseObject.js#L233
So in your [example](https://github.com/parse-community/Parse-SDK-JS/issues/1487#issue-1261003509), the LiveQuery event would only contain the `key` property (if it was defined)?
In the example, the live query event will only contain the “key” property, so it will unset all other keys on the object
What is the expected behavior of a `select` condition in a LiveQuery?
The object returned by the live query should only contain the selected keys, but it shouldn’t override other objects with the same IDs that have previously fetched keys 
In my setup I have a live query that applies to one column of an object. All other keys are fetched with the initial query.

whenever the livequery is triggered with that singular key selected, all keys on the original object get set to undefined, as the object has the same state reference as the live query object.


```js
const obj = await new Parse.Query("Test").first()
// obj.get("name") is defined
// objects keys are key, name, cash, year 
const subscription = await new Parse.Query("Test").select("key").subscribe()
subscription.on("update", (object) => {
  // if object.id === obj.id, Parse JS uses the incoming object’s state and assigns it to obj. 
  // obj.get("name") is now undefined as a side effect to the live query, even though no additional code has been written 
 // objects keys are key, and that’s it (as expected)
// obj keys are key (not expected)
});
``` 
Got it, thanks for the detailed example. Yes, this looks like a bug.
I suspect there is a reason for this as LQ uses [fields](https://parseplatform.org/Parse-Swift/release/documentation/parseswift/query/fields(_:)-71x3d) to achieve similar behavior to “select”. 

> Suppose the ParseObject Player contains three fields name, id and age. If you are only interested in the change of the name field, you can set query.fields to “name”. In this situation, when the change of a Player ParseObject fulfills the subscription, only the name field will be sent to the clients instead of the full Player ParseObject. If this is called multiple times, then all of the keys specified in each of the calls will be received.

This is also noted in the spec: https://github.com/parse-community/parse-server/wiki/Parse-LiveQuery-Protocol-Specification#subscribe-message
Yes, that's not the issue, it's that it overrides all other keys in objects **in memory** with the same internal reference, meaning that if you first query the three fields name, id and age, and then an LQ event _only_ contains **"age"**, the **original** queried object in memory will only contain the key **age** now

Also I don't think the fields parameter even works as expected. See https://github.com/parse-community/parse-server/issues/8027
@cbaker6 Did https://github.com/parse-community/Parse-SDK-JS/issues/1487#issuecomment-1148500715 answer your concern? If not, let's discuss, if yes I'd go ahead and merge the PR.
Yes, the first part by @dblythy makes sense to me and the PR seems like it should proceed.

Im not sure about the other PR to address “fields” though as I tested it when I originally implemented the spec and i thought it worked then. I will look at that further another time…
Thanks for confirming, I'll go ahead and merge then
@dblythy I retract my comment:

> Im not sure about the other PR to address “fields” though as I tested it when I originally implemented the spec and i thought it worked then. I will look at that further another time…

I was able to confirm that LiveQuery does update whenever `fields` is used and a different field that isn't part of the list `keys` is changed. 
Thanks for the confirmation! It’s a bit strange hey. I don’t know why it wasn’t just named `select`. Perhaps in a past version it did behave differently, but the feature somehow got overriden. 



Patch:
diff --git a/src/LiveQueryClient.js b/src/LiveQueryClient.js
index 3066ac1c6..2210dee4d 100644
--- a/src/LiveQueryClient.js
+++ b/src/LiveQueryClient.js
@@ -419,7 +419,10 @@ class LiveQueryClient extends EventEmitter {
         data.original = ParseObject.fromJSON(data.original, false);
       }
       delete data.object.__type;
-      const parseObject = ParseObject.fromJSON(data.object, override);
+      const parseObject = ParseObject.fromJSON(
+        data.object,
+        !(subscription.query && subscription.query._select) ? override : false
+      );
 
       if (data.original) {
         subscription.emit(data.op, parseObject, data.original, response);


Test patch:
diff --git a/src/__tests__/LiveQueryClient-test.js b/src/__tests__/LiveQueryClient-test.js
index 812ebb978..4329bcd01 100644
--- a/src/__tests__/LiveQueryClient-test.js
+++ b/src/__tests__/LiveQueryClient-test.js
@@ -522,6 +522,43 @@ describe('LiveQueryClient', () => {
     spy.mockRestore();
   });
 
+  it('can handle select in websocket payload', () => {
+    const liveQueryClient = new LiveQueryClient({
+      applicationId: 'applicationId',
+      serverURL: 'ws://test',
+      javascriptKey: 'javascriptKey',
+      masterKey: 'masterKey',
+      sessionToken: 'sessionToken',
+    });
+    // Add mock subscription
+    const subscription = new events.EventEmitter();
+    subscription.query = new ParseQuery('Test').select('foo');
+    liveQueryClient.subscriptions.set(1, subscription);
+    const object = new ParseObject('Test');
+    const original = new ParseObject('Test');
+    object.set('key', 'value');
+    original.set('key', 'old');
+    const data = {
+      op: 'update',
+      clientId: 1,
+      requestId: 1,
+      object: object._toFullJSON(),
+      original: original._toFullJSON(),
+    };
+    const event = {
+      data: JSON.stringify(data),
+    };
+
+    const spy = jest
+      .spyOn(ParseObject, 'fromJSON')
+      .mockImplementationOnce(() => original)
+      .mockImplementationOnce(() => object);
+
+    liveQueryClient._handleWebSocketMessage(event);
+    expect(ParseObject.fromJSON.mock.calls[1][1]).toEqual(false);
+    spy.mockRestore();
+  });
+
   it('can handle WebSocket response unset field', async () => {
     const liveQueryClient = new LiveQueryClient({
       applicationId: 'applicationId',