From e2e9ba2469a10ba07c91b8ba963c3998bc580319 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 28 Jan 2021 08:21:47 +1100 Subject: [PATCH 01/17] Update README.md --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7a5a707575..0f79f5c8c5 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ $ docker run --name my-mongo -d mongo $ docker run --name my-parse-server -v config-vol:/parse-server/config -p 1337:1337 --link my-mongo:mongo -d parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test ``` -***Note:*** *If you want to use [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/) feature, please add `-v cloud-code-vol:/parse-server/cloud --cloud /parse-server/cloud/main.js` to command above. Make sure the `main.js` file is available in the `cloud-code-vol` directory before run this command. Otherwise, an error will occur.* +***Note:*** *If you want to use [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/) feature, please add `-v cloud-code-vol:/parse-server/cloud --cloud /parse-server/cloud/main.js` to command above. Make sure the `main.js` file is available in the `cloud-code-vol` directory before running this command. Otherwise, an error will occur.* You can use any arbitrary string as your application id and master key. These will be used by your clients to authenticate with the Parse Server. @@ -383,7 +383,7 @@ For the full list of configurable environment variables, run `parse-server --hel ### Available Adapters -All official adapters are distributed as scoped pacakges on [npm (@parse)](https://www.npmjs.com/search?q=scope%3Aparse). +All official adapters are distributed as scoped packages on [npm (@parse)](https://www.npmjs.com/search?q=scope%3Aparse). Some well maintained adapters are also available on the [Parse Server Modules](https://github.com/parse-server-modules) organization. @@ -399,13 +399,13 @@ Parse Server allows developers to choose from several options when hosting files `GridFSBucketAdapter` is used by default and requires no setup, but if you're interested in using S3 or Google Cloud Storage, additional configuration information is available in the [Parse Server guide](http://docs.parseplatform.org/parse-server/guide/#configuring-file-adapters). -### Idempodency Enforcement +### Idempotency Enforcement **Caution, this is an experimental feature that may not be appropriate for production.** -This feature deduplicates identical requests that are received by Parse Server mutliple times, typically due to network issues or network adapter access restrictions on mobile operating systems. +This feature deduplicates identical requests that are received by Parse Server multiple times, typically due to network issues or network adapter access restrictions on mobile operating systems. -Identical requests are identified by their request header `X-Parse-Request-Id`. Therefore a client request has to include this header for deduplication to be applied. Requests that do not contain this header cannot be deduplicated and are processed normally by Parse Server. This means rolling out this feature to clients is seamless as Parse Server still processes request without this header when this feature is enbabled. +Identical requests are identified by their request header `X-Parse-Request-Id`. Therefore a client request has to include this header for deduplication to be applied. Requests that do not contain this header cannot be deduplicated and are processed normally by Parse Server. This means rolling out this feature to clients is seamless as Parse Server still processes requests without this header when this feature is enabled. > This feature needs to be enabled on the client side to send the header and on the server to process the header. Refer to the specific Parse SDK docs to see whether the feature is supported yet. @@ -425,7 +425,7 @@ let api = new ParseServer({ | Parameter | Optional | Type | Default value | Example values | Environment variable | Description | |-----------|----------|--------|---------------|-----------|-----------|-------------| | `idempotencyOptions` | yes | `Object` | `undefined` | | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS | Setting this enables idempotency enforcement for the specified paths. | -| `idempotencyOptions.paths`| yes | `Array` | `[]` | `.*` (all paths, includes the examples below),
`functions/.*` (all functions),
`jobs/.*` (all jobs),
`classes/.*` (all classes),
`functions/.*` (all functions),
`users` (user creation / update),
`installations` (installation creation / update) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS | An array of path patterns that have to match the request path for request deduplication to be enabled. The mount path must not be included, for example to match the request path `/parse/functions/myFunction` specifiy the path pattern `functions/myFunction`. A trailing slash of the request path is ignored, for example the path pattern `functions/myFunction` matches both `/parse/functions/myFunction` and `/parse/functions/myFunction/`. | +| `idempotencyOptions.paths`| yes | `Array` | `[]` | `.*` (all paths, includes the examples below),
`functions/.*` (all functions),
`jobs/.*` (all jobs),
`classes/.*` (all classes),
`functions/.*` (all functions),
`users` (user creation / update),
`installations` (installation creation / update) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS | An array of path patterns that have to match the request path for request deduplication to be enabled. The mount path must not be included, for example to match the request path `/parse/functions/myFunction` specify the path pattern `functions/myFunction`. A trailing slash of the request path is ignored, for example the path pattern `functions/myFunction` matches both `/parse/functions/myFunction` and `/parse/functions/myFunction/`. | | `idempotencyOptions.ttl` | yes | `Integer` | `300` | `60` (60 seconds) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL | The duration in seconds after which a request record is discarded from the database. Duplicate requests due to network issues can be expected to arrive within milliseconds up to several seconds. This value must be greater than `0`. | #### Notes @@ -442,7 +442,7 @@ Logs are also viewable in Parse Dashboard. **Want to log each request and response?** Set the `VERBOSE` environment variable when starting `parse-server`. Usage :- `VERBOSE='1' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY` -**Want logs to be in placed in a different folder?** Pass the `PARSE_SERVER_LOGS_FOLDER` environment variable when starting `parse-server`. Usage :- `PARSE_SERVER_LOGS_FOLDER='' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY` +**Want logs to be placed in a different folder?** Pass the `PARSE_SERVER_LOGS_FOLDER` environment variable when starting `parse-server`. Usage :- `PARSE_SERVER_LOGS_FOLDER='' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY` **Want to log specific levels?** Pass the `logLevel` parameter when starting `parse-server`. Usage :- `parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --logLevel LOG_LEVEL` @@ -491,7 +491,7 @@ $ docker run --name my-mongo -d mongo $ docker run --name my-parse-server --link my-mongo:mongo -v config-vol:/parse-server/config -p 1337:1337 -d parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test --publicServerURL http://localhost:1337/parse --mountGraphQL --mountPlayground ``` -***Note:*** *If you want to use [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/) feature, please add `-v cloud-code-vol:/parse-server/cloud --cloud /parse-server/cloud/main.js` to command above. Make sure the `main.js` file is available in the `cloud-code-vol` directory before run this command. Otherwise, an error will occur.* +***Note:*** *If you want to use [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/) feature, please add `-v cloud-code-vol:/parse-server/cloud --cloud /parse-server/cloud/main.js` to command above. Make sure the `main.js` file is available in the `cloud-code-vol` directory before running this command. Otherwise, an error will occur.* After starting the server, you can visit http://localhost:1337/playground in your browser to start playing with your GraphQL API. From 5bf154fca9a3198977c890ef612b2acd00a9a09d Mon Sep 17 00:00:00 2001 From: dblythy Date: Sun, 31 Jan 2021 17:58:06 +1100 Subject: [PATCH 02/17] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0f79f5c8c5..98aed7f616 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ $ docker run --name my-mongo -d mongo $ docker run --name my-parse-server -v config-vol:/parse-server/config -p 1337:1337 --link my-mongo:mongo -d parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test ``` -***Note:*** *If you want to use [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/) feature, please add `-v cloud-code-vol:/parse-server/cloud --cloud /parse-server/cloud/main.js` to command above. Make sure the `main.js` file is available in the `cloud-code-vol` directory before running this command. Otherwise, an error will occur.* +***Note:*** *If you want to use [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/), add `-v cloud-code-vol:/parse-server/cloud --cloud /parse-server/cloud/main.js` to command above. Make sure `main.js` is in the `cloud-code-vol` directory before starting Parse Server.* You can use any arbitrary string as your application id and master key. These will be used by your clients to authenticate with the Parse Server. @@ -491,7 +491,7 @@ $ docker run --name my-mongo -d mongo $ docker run --name my-parse-server --link my-mongo:mongo -v config-vol:/parse-server/config -p 1337:1337 -d parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test --publicServerURL http://localhost:1337/parse --mountGraphQL --mountPlayground ``` -***Note:*** *If you want to use [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/) feature, please add `-v cloud-code-vol:/parse-server/cloud --cloud /parse-server/cloud/main.js` to command above. Make sure the `main.js` file is available in the `cloud-code-vol` directory before running this command. Otherwise, an error will occur.* +***Note:*** *If you want to use [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/), add `-v cloud-code-vol:/parse-server/cloud --cloud /parse-server/cloud/main.js` to command above. Make sure `main.js` is in the `cloud-code-vol` directory before starting Parse Server.* After starting the server, you can visit http://localhost:1337/playground in your browser to start playing with your GraphQL API. From 44047b2eeb3914e12da53425cb13f8b92ab97bc8 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 1 Feb 2021 01:48:53 +1100 Subject: [PATCH 03/17] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 98aed7f616..ffee4a48e6 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ $ docker run --name my-mongo -d mongo $ docker run --name my-parse-server -v config-vol:/parse-server/config -p 1337:1337 --link my-mongo:mongo -d parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test ``` -***Note:*** *If you want to use [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/), add `-v cloud-code-vol:/parse-server/cloud --cloud /parse-server/cloud/main.js` to command above. Make sure `main.js` is in the `cloud-code-vol` directory before starting Parse Server.* +***Note:*** *If you want to use [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/), add `-v cloud-code-vol:/parse-server/cloud --cloud /parse-server/cloud/main.js` to the command above. Make sure `main.js` is in the `cloud-code-vol` directory before starting Parse Server.* You can use any arbitrary string as your application id and master key. These will be used by your clients to authenticate with the Parse Server. @@ -491,7 +491,7 @@ $ docker run --name my-mongo -d mongo $ docker run --name my-parse-server --link my-mongo:mongo -v config-vol:/parse-server/config -p 1337:1337 -d parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --databaseURI mongodb://mongo/test --publicServerURL http://localhost:1337/parse --mountGraphQL --mountPlayground ``` -***Note:*** *If you want to use [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/), add `-v cloud-code-vol:/parse-server/cloud --cloud /parse-server/cloud/main.js` to command above. Make sure `main.js` is in the `cloud-code-vol` directory before starting Parse Server.* +***Note:*** *If you want to use [Cloud Code](https://docs.parseplatform.org/cloudcode/guide/), add `-v cloud-code-vol:/parse-server/cloud --cloud /parse-server/cloud/main.js` to the command above. Make sure `main.js` is in the `cloud-code-vol` directory before starting Parse Server.* After starting the server, you can visit http://localhost:1337/playground in your browser to start playing with your GraphQL API. From ed32317e9957730a142119d573ef9304674d7e1e Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 11 Feb 2021 04:10:10 +1100 Subject: [PATCH 04/17] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4066a5dc8c..6e9daf8d82 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ Before you start make sure you have installed: ### Compatibility #### MongoDB Support -Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. The rests run against the latest patch version of each MongoDB release. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and only test against versions that are officially supported by MongoDB and have not reached their end-of-life date yet. +Parse Server is continuously tested with the most recent releases of MongoDB to ensure compatibility. The tests run against the latest patch version of each MongoDB release. We follow the [MongoDB support schedule](https://www.mongodb.com/support-policy) and only test against versions that are officially supported by MongoDB and have not reached their end-of-life date yet. | Version | Latest Patch Version | End-of-Life Date | Compatibility | |-------------|----------------------|------------------|--------------------| @@ -456,7 +456,7 @@ let api = new ParseServer({ | Parameter | Optional | Type | Default value | Example values | Environment variable | Description | |----------------------------|----------|-----------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `idempotencyOptions` | yes | `Object` | `undefined` | | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_OPTIONS | Setting this enables idempotency enforcement for the specified paths. | -| `idempotencyOptions.paths` | yes | `Array` | `[]` | `.*` (all paths, includes the examples below),
`functions/.*` (all functions),
`jobs/.*` (all jobs),
`classes/.*` (all classes),
`functions/.*` (all functions),
`users` (user creation / update),
`installations` (installation creation / update) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS | An array of path patterns that have to match the request path for request deduplication to be enabled. The mount path must not be included, for example to match the request path `/parse/functions/myFunction` specifiy the path pattern `functions/myFunction`. A trailing slash of the request path is ignored, for example the path pattern `functions/myFunction` matches both `/parse/functions/myFunction` and `/parse/functions/myFunction/`. | +| `idempotencyOptions.paths` | yes | `Array` | `[]` | `.*` (all paths, includes the examples below),
`functions/.*` (all functions),
`jobs/.*` (all jobs),
`classes/.*` (all classes),
`functions/.*` (all functions),
`users` (user creation / update),
`installations` (installation creation / update) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_PATHS | An array of path patterns that have to match the request path for request deduplication to be enabled. The mount path must not be included, for example to match the request path `/parse/functions/myFunction` specify the path pattern `functions/myFunction`. A trailing slash of the request path is ignored, for example the path pattern `functions/myFunction` matches both `/parse/functions/myFunction` and `/parse/functions/myFunction/`. | | `idempotencyOptions.ttl` | yes | `Integer` | `300` | `60` (60 seconds) | PARSE_SERVER_EXPERIMENTAL_IDEMPOTENCY_TTL | The duration in seconds after which a request record is discarded from the database. Duplicate requests due to network issues can be expected to arrive within milliseconds up to several seconds. This value must be greater than `0`. | ### Notes @@ -536,7 +536,7 @@ Pros: - All files are complete in their content and can be easily opened and previewed by viewing the file in a browser. Cons: -- In most cases, a localized page differs only slighly from the default page, which could cause a lot of duplicate code that is difficult to maintain. +- In most cases, a localized page differs only slightly from the default page, which could cause a lot of duplicate code that is difficult to maintain. #### Localization with JSON Resource @@ -546,7 +546,7 @@ Pages are localized by adding placeholders in the HTML files and providing a JSO ```js root/ ├── public/ // pages base path -│ ├── example.html // the page containg placeholders +│ ├── example.html // the page containing placeholders ├── private/ // folder outside of public scope │ └── translations.json // JSON resource file ``` From 00d231900d154a56394962cd9865ae39c60f5026 Mon Sep 17 00:00:00 2001 From: dblythy Date: Sat, 10 Sep 2022 12:58:49 +1000 Subject: [PATCH 05/17] Update RestWrite.js --- src/RestWrite.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index 2be833ad30..74183c43be 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1688,7 +1688,7 @@ RestWrite.prototype._updateResponseWithData = function (response, data) { continue; } const value = response[key]; - if (value == null || (value.__type && value.__type === 'Pointer') || data[key] === value) { + if (value == null || (value.__type && value.__type === 'Pointer') || _.isEqual(data[key], value)) { delete response[key]; } } From 47e195f169a0b88295655100f077fc225d7de228 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 12 Sep 2022 09:42:47 +1000 Subject: [PATCH 06/17] Update RestWrite.js --- src/RestWrite.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index 74183c43be..6897eb6171 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -12,6 +12,7 @@ var passwordCrypto = require('./password'); var Parse = require('parse/node'); var triggers = require('./triggers'); var ClientSDK = require('./ClientSDK'); +const util = require('util'); import RestQuery from './RestQuery'; import _ from 'lodash'; import logger from './logger'; @@ -1688,7 +1689,11 @@ RestWrite.prototype._updateResponseWithData = function (response, data) { continue; } const value = response[key]; - if (value == null || (value.__type && value.__type === 'Pointer') || _.isEqual(data[key], value)) { + if ( + value == null || + (value.__type && value.__type === 'Pointer') || + util.isDeepStrictEqual(data[key], value) + ) { delete response[key]; } } From 2160c345b81320697f81088b3063be3263ed7207 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 14 Sep 2022 22:53:25 +1000 Subject: [PATCH 07/17] enforce keys --- spec/ParseAPI.spec.js | 109 +++++++++++++++++++++++------------------- src/RestWrite.js | 8 +--- 2 files changed, 62 insertions(+), 55 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 7bb6c9889b..708fabed53 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -951,57 +951,68 @@ describe('miscellaneous', function () { ); }); - it('should return the updated fields on PUT', done => { + it('should return the updated fields on PUT', async () => { const obj = new Parse.Object('GameScore'); - obj - .save({ a: 'hello', c: 1, d: ['1'], e: ['1'], f: ['1', '2'] }) - .then(() => { - const headers = { - 'Content-Type': 'application/json', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Installation-Id': 'yolo', - }; - request({ - method: 'PUT', - headers: headers, - url: 'http://localhost:8378/1/classes/GameScore/' + obj.id, - body: JSON.stringify({ - a: 'b', - c: { __op: 'Increment', amount: 2 }, - d: { __op: 'Add', objects: ['2'] }, - e: { __op: 'AddUnique', objects: ['1', '2'] }, - f: { __op: 'Remove', objects: ['2'] }, - selfThing: { - __type: 'Pointer', - className: 'GameScore', - objectId: obj.id, - }, - }), - }).then(response => { - try { - const body = response.data; - expect(body.a).toBeUndefined(); - expect(body.c).toEqual(3); // 2+1 - expect(body.d.length).toBe(2); - expect(body.d.indexOf('1') > -1).toBe(true); - expect(body.d.indexOf('2') > -1).toBe(true); - expect(body.e.length).toBe(2); - expect(body.e.indexOf('1') > -1).toBe(true); - expect(body.e.indexOf('2') > -1).toBe(true); - expect(body.f.length).toBe(1); - expect(body.f.indexOf('1') > -1).toBe(true); - // return nothing on other self - expect(body.selfThing).toBeUndefined(); - // updatedAt is always set - expect(body.updatedAt).not.toBeUndefined(); - } catch (e) { - fail(e); - } - done(); - }); + const pointer = new Parse.Object('Child'); + Parse.Cloud.beforeSave('GameScore', request => { + return request.object; + }); + Parse.Cloud.afterSave('GameScore', request => { + return request.object; + }); + await pointer.save(); + obj.set( + 'point', + new Parse.GeoPoint({ + latitude: 37.4848, + longitude: -122.1483, }) - .catch(done.fail); + ); + obj.set('array', ['obj1', 'obj2']); + obj.set('objects', { a: 'b' }); + obj.set('string', 'abc'); + obj.set('bool', true); + obj.set('number', 1); + obj.set('date', new Date()); + obj.set('pointer', pointer); + await obj.save({ a: 'hello', c: 1, d: ['1'], e: ['1'], f: ['1', '2'] }); + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Installation-Id': 'yolo', + }; + const response = await request({ + method: 'PUT', + headers: headers, + url: 'http://localhost:8378/1/classes/GameScore/' + obj.id, + body: JSON.stringify({ + a: 'b', + c: { __op: 'Increment', amount: 2 }, + d: { __op: 'Add', objects: ['2'] }, + e: { __op: 'AddUnique', objects: ['1', '2'] }, + f: { __op: 'Remove', objects: ['2'] }, + selfThing: { + __type: 'Pointer', + className: 'GameScore', + objectId: obj.id, + }, + }), + }); + const body = response.data; + expect(Object.keys(body).sort()).toEqual(['c', 'd', 'e', 'f', 'objectId', 'updatedAt']); + expect(body.a).toBeUndefined(); + expect(body.c).toEqual(3); // 2+1 + expect(body.d.length).toBe(2); + expect(body.d.indexOf('1') > -1).toBe(true); + expect(body.d.indexOf('2') > -1).toBe(true); + expect(body.e.length).toBe(2); + expect(body.e.indexOf('1') > -1).toBe(true); + expect(body.e.indexOf('2') > -1).toBe(true); + expect(body.f.length).toBe(1); + expect(body.f.indexOf('1') > -1).toBe(true); + expect(body.selfThing).toBeUndefined(); + expect(body.updatedAt).not.toBeUndefined(); }); it('test cloud function error handling', done => { diff --git a/src/RestWrite.js b/src/RestWrite.js index 6897eb6171..9e201c8bfa 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1678,12 +1678,7 @@ RestWrite.prototype._updateResponseWithData = function (response, data) { this.storage.fieldsChangedByTrigger.push(key); } } - const skipKeys = [ - 'objectId', - 'createdAt', - 'updatedAt', - ...(requiredColumns.read[this.className] || []), - ]; + const skipKeys = ['objectId', 'updatedAt', ...(requiredColumns.read[this.className] || [])]; for (const key in response) { if (skipKeys.includes(key)) { continue; @@ -1691,6 +1686,7 @@ RestWrite.prototype._updateResponseWithData = function (response, data) { const value = response[key]; if ( value == null || + data[key] == null || (value.__type && value.__type === 'Pointer') || util.isDeepStrictEqual(data[key], value) ) { From e976fe1c77c6c8cae7c40e22710c1ad542712b23 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 14 Sep 2022 23:14:03 +1000 Subject: [PATCH 08/17] fix tests --- spec/ParseAPI.spec.js | 2 +- src/RestWrite.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 708fabed53..920a9c21ce 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -1000,7 +1000,7 @@ describe('miscellaneous', function () { }), }); const body = response.data; - expect(Object.keys(body).sort()).toEqual(['c', 'd', 'e', 'f', 'objectId', 'updatedAt']); + expect(Object.keys(body).sort()).toEqual(['c', 'd', 'e', 'f', 'updatedAt']); expect(body.a).toBeUndefined(); expect(body.c).toEqual(3); // 2+1 expect(body.d.length).toBe(2); diff --git a/src/RestWrite.js b/src/RestWrite.js index 9e201c8bfa..e20b8b646d 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1418,7 +1418,6 @@ RestWrite.prototype.runDatabaseOperation = function () { ) .then(response => { response.updatedAt = this.updatedAt; - this._updateResponseWithData(response, this.data); this.response = { response }; }); }); @@ -1678,7 +1677,10 @@ RestWrite.prototype._updateResponseWithData = function (response, data) { this.storage.fieldsChangedByTrigger.push(key); } } - const skipKeys = ['objectId', 'updatedAt', ...(requiredColumns.read[this.className] || [])]; + const skipKeys = ['updatedAt', ...(requiredColumns.read[this.className] || [])]; + if (!this.query) { + skipKeys.push('objectId', 'createdAt'); + } for (const key in response) { if (skipKeys.includes(key)) { continue; From 1f16ad87a4411ecb4dbb9f7c02762e2d6745d5e7 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 14 Sep 2022 23:16:52 +1000 Subject: [PATCH 09/17] Update RestWrite.js --- src/RestWrite.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/RestWrite.js b/src/RestWrite.js index e20b8b646d..a2430f1d48 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1418,6 +1418,7 @@ RestWrite.prototype.runDatabaseOperation = function () { ) .then(response => { response.updatedAt = this.updatedAt; + this._updateResponseWithData(response, this.data); this.response = { response }; }); }); From 19389cd41a37a7248c5aac4fbd1cbfecf6fcb96d Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 14 Sep 2022 23:58:25 +1000 Subject: [PATCH 10/17] Update RestWrite.js --- src/RestWrite.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index a2430f1d48..43d3c5190e 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1539,6 +1539,7 @@ RestWrite.prototype.runAfterSaveTrigger = function () { } const { originalObject, updatedObject } = this.buildParseObjects(); + const frozenJSON = {...updatedObject.toJSON()} updatedObject._handleSaveResponse(this.response.response, this.response.status || 200); this.config.database.loadSchema().then(schemaController => { @@ -1568,8 +1569,17 @@ RestWrite.prototype.runAfterSaveTrigger = function () { this.pendingOps = {}; this.response.response = result; } else { + const json = (result || updatedObject).toJSON(); + for (const key in json) { + if (key === 'objectId') { + continue; + } + if (util.isDeepStrictEqual(json[key], frozenJSON[key])) { + delete json[key]; + } + } this.response.response = this._updateResponseWithData( - (result || updatedObject).toJSON(), + json, this.data ); } @@ -1681,6 +1691,8 @@ RestWrite.prototype._updateResponseWithData = function (response, data) { const skipKeys = ['updatedAt', ...(requiredColumns.read[this.className] || [])]; if (!this.query) { skipKeys.push('objectId', 'createdAt'); + } else { + delete response.objectId; } for (const key in response) { if (skipKeys.includes(key)) { @@ -1689,7 +1701,6 @@ RestWrite.prototype._updateResponseWithData = function (response, data) { const value = response[key]; if ( value == null || - data[key] == null || (value.__type && value.__type === 'Pointer') || util.isDeepStrictEqual(data[key], value) ) { From 7b10627e001b7b638c3ed70647ea59cbcbd19c0f Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 15 Sep 2022 00:02:32 +1000 Subject: [PATCH 11/17] Update RestWrite.js --- src/RestWrite.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index 43d3c5190e..3b4ee9918d 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1539,7 +1539,7 @@ RestWrite.prototype.runAfterSaveTrigger = function () { } const { originalObject, updatedObject } = this.buildParseObjects(); - const frozenJSON = {...updatedObject.toJSON()} + const frozenJSON = Object.freeze(updatedObject.toJSON()) updatedObject._handleSaveResponse(this.response.response, this.response.status || 200); this.config.database.loadSchema().then(schemaController => { From ac791c5c15d15be1ee7ea9676703be986c15b283 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 15 Sep 2022 01:00:52 +1000 Subject: [PATCH 12/17] add tests for with an without cloud functions --- spec/ParseAPI.spec.js | 90 ++++++++++++++++++++++++++++++++++++++++++- src/RestWrite.js | 10 ++--- 2 files changed, 92 insertions(+), 8 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 920a9c21ce..9b1a97af87 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -951,7 +951,79 @@ describe('miscellaneous', function () { ); }); - it('should return the updated fields on PUT', async () => { + it('return the updated fields on PUT', async () => { + const obj = new Parse.Object('GameScore'); + const pointer = new Parse.Object('Child'); + await pointer.save(); + obj.set( + 'point', + new Parse.GeoPoint({ + latitude: 37.4848, + longitude: -122.1483, + }) + ); + obj.set('array', ['obj1', 'obj2']); + obj.set('objects', { a: 'b' }); + obj.set('string', 'abc'); + obj.set('bool', true); + obj.set('number', 1); + obj.set('date', new Date()); + obj.set('pointer', pointer); + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Installation-Id': 'yolo', + }; + const saveResponse = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/classes/GameScore', + body: JSON.stringify({ + a: 'hello', + c: 1, + d: ['1'], + e: ['1'], + f: ['1', '2'], + ...obj.toJSON(), + }), + }); + expect(Object.keys(saveResponse.data).sort()).toEqual(['createdAt', 'objectId']); + obj.id = saveResponse.data.objectId; + const response = await request({ + method: 'PUT', + headers: headers, + url: 'http://localhost:8378/1/classes/GameScore/' + obj.id, + body: JSON.stringify({ + a: 'b', + c: { __op: 'Increment', amount: 2 }, + d: { __op: 'Add', objects: ['2'] }, + e: { __op: 'AddUnique', objects: ['1', '2'] }, + f: { __op: 'Remove', objects: ['2'] }, + selfThing: { + __type: 'Pointer', + className: 'GameScore', + objectId: obj.id, + }, + }), + }); + const body = response.data; + expect(Object.keys(body).sort()).toEqual(['c', 'd', 'e', 'f', 'updatedAt']); + expect(body.a).toBeUndefined(); + expect(body.c).toEqual(3); // 2+1 + expect(body.d.length).toBe(2); + expect(body.d.indexOf('1') > -1).toBe(true); + expect(body.d.indexOf('2') > -1).toBe(true); + expect(body.e.length).toBe(2); + expect(body.e.indexOf('1') > -1).toBe(true); + expect(body.e.indexOf('2') > -1).toBe(true); + expect(body.f.length).toBe(1); + expect(body.f.indexOf('1') > -1).toBe(true); + expect(body.selfThing).toBeUndefined(); + expect(body.updatedAt).not.toBeUndefined(); + }); + + it('should response should not change with triggers', async () => { const obj = new Parse.Object('GameScore'); const pointer = new Parse.Object('Child'); Parse.Cloud.beforeSave('GameScore', request => { @@ -975,13 +1047,27 @@ describe('miscellaneous', function () { obj.set('number', 1); obj.set('date', new Date()); obj.set('pointer', pointer); - await obj.save({ a: 'hello', c: 1, d: ['1'], e: ['1'], f: ['1', '2'] }); const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', 'X-Parse-Installation-Id': 'yolo', }; + const saveResponse = await request({ + method: 'POST', + headers: headers, + url: 'http://localhost:8378/1/classes/GameScore', + body: JSON.stringify({ + a: 'hello', + c: 1, + d: ['1'], + e: ['1'], + f: ['1', '2'], + ...obj.toJSON(), + }), + }); + expect(Object.keys(saveResponse.data).sort()).toEqual(['createdAt', 'objectId']); + obj.id = saveResponse.data.objectId; const response = await request({ method: 'PUT', headers: headers, diff --git a/src/RestWrite.js b/src/RestWrite.js index 3b4ee9918d..54ec73d236 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1539,7 +1539,7 @@ RestWrite.prototype.runAfterSaveTrigger = function () { } const { originalObject, updatedObject } = this.buildParseObjects(); - const frozenJSON = Object.freeze(updatedObject.toJSON()) + const frozenJSON = Object.freeze(updatedObject.toJSON()); updatedObject._handleSaveResponse(this.response.response, this.response.status || 200); this.config.database.loadSchema().then(schemaController => { @@ -1578,10 +1578,7 @@ RestWrite.prototype.runAfterSaveTrigger = function () { delete json[key]; } } - this.response.response = this._updateResponseWithData( - json, - this.data - ); + this.response.response = this._updateResponseWithData(json, this.data); } }) .catch(function (err) { @@ -1688,10 +1685,11 @@ RestWrite.prototype._updateResponseWithData = function (response, data) { this.storage.fieldsChangedByTrigger.push(key); } } - const skipKeys = ['updatedAt', ...(requiredColumns.read[this.className] || [])]; + const skipKeys = [...(requiredColumns.read[this.className] || [])]; if (!this.query) { skipKeys.push('objectId', 'createdAt'); } else { + skipKeys.push('updatedAt'); delete response.objectId; } for (const key in response) { From 2902cb6838be9409ffe8a222ff58f6a1d4a68477 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 15 Sep 2022 01:10:47 +1000 Subject: [PATCH 13/17] Update RestQuery.spec.js --- spec/RestQuery.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index 2e37c96b7c..02af2fc576 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -521,7 +521,6 @@ describe('RestQuery.each', () => { 'createdAt', 'initialToRemove', 'objectId', - 'updatedAt', ]); }); }); From d8689613b80f449e02d2b1ee19cd8dbfb5dcfd3a Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 15 Sep 2022 01:57:28 +1000 Subject: [PATCH 14/17] Update RestWrite.js --- src/RestWrite.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/RestWrite.js b/src/RestWrite.js index 54ec73d236..7ab7bcc043 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1574,6 +1574,9 @@ RestWrite.prototype.runAfterSaveTrigger = function () { if (key === 'objectId') { continue; } + if (this.storage.fieldsChangedByTrigger.includes(key)) { + continue; + } if (util.isDeepStrictEqual(json[key], frozenJSON[key])) { delete json[key]; } From 1e468855a620586ccda0f884a0c4622cde87ca7a Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 15 Sep 2022 02:26:34 +1000 Subject: [PATCH 15/17] Update RestWrite.js --- src/RestWrite.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index 7ab7bcc043..559d855323 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1574,7 +1574,10 @@ RestWrite.prototype.runAfterSaveTrigger = function () { if (key === 'objectId') { continue; } - if (this.storage.fieldsChangedByTrigger.includes(key)) { + if ( + this.storage.fieldsChangedByTrigger && + this.storage.fieldsChangedByTrigger.includes(key) + ) { continue; } if (util.isDeepStrictEqual(json[key], frozenJSON[key])) { From e83584d967572f264fedcb4fd92e295103a64c04 Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 15 Sep 2022 10:53:10 +1000 Subject: [PATCH 16/17] Update RestWrite.js --- src/RestWrite.js | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index 559d855323..dcd101abfa 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1539,7 +1539,6 @@ RestWrite.prototype.runAfterSaveTrigger = function () { } const { originalObject, updatedObject } = this.buildParseObjects(); - const frozenJSON = Object.freeze(updatedObject.toJSON()); updatedObject._handleSaveResponse(this.response.response, this.response.status || 200); this.config.database.loadSchema().then(schemaController => { @@ -1570,20 +1569,6 @@ RestWrite.prototype.runAfterSaveTrigger = function () { this.response.response = result; } else { const json = (result || updatedObject).toJSON(); - for (const key in json) { - if (key === 'objectId') { - continue; - } - if ( - this.storage.fieldsChangedByTrigger && - this.storage.fieldsChangedByTrigger.includes(key) - ) { - continue; - } - if (util.isDeepStrictEqual(json[key], frozenJSON[key])) { - delete json[key]; - } - } this.response.response = this._updateResponseWithData(json, this.data); } }) @@ -1706,7 +1691,8 @@ RestWrite.prototype._updateResponseWithData = function (response, data) { if ( value == null || (value.__type && value.__type === 'Pointer') || - util.isDeepStrictEqual(data[key], value) + util.isDeepStrictEqual(data[key], value) || + util.isDeepStrictEqual((this.originalData || {})[key], value) ) { delete response[key]; } From debdff5c4da917328004c580764dcf9a30dcd6bd Mon Sep 17 00:00:00 2001 From: dblythy Date: Thu, 15 Sep 2022 11:14:42 +1000 Subject: [PATCH 17/17] Update RestWrite.js --- src/RestWrite.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index dcd101abfa..2e1ea18ea5 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -1568,8 +1568,10 @@ RestWrite.prototype.runAfterSaveTrigger = function () { this.pendingOps = {}; this.response.response = result; } else { - const json = (result || updatedObject).toJSON(); - this.response.response = this._updateResponseWithData(json, this.data); + this.response.response = this._updateResponseWithData( + (result || updatedObject).toJSON(), + this.data + ); } }) .catch(function (err) {