From 6f65ec54bda422a3beba32b43beb46aaa4b83683 Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 19 Jul 2023 13:25:04 +1000 Subject: [PATCH 01/16] feat: allow short circuit of beforeFind --- spec/CloudCode.spec.js | 55 ++++++++++++++++++++++++++++++++++++++++++ src/rest.js | 10 ++++++++ src/triggers.js | 10 ++++++++ 3 files changed, 75 insertions(+) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 90ab313826..c38199b3d0 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -201,6 +201,61 @@ describe('Cloud Code', () => { } }); + it('beforeFind can short circuit', async () => { + Parse.Cloud.beforeFind('beforeFind', () => { + return new Parse.Object('TestObject', { foo: 'bar' }); + }); + Parse.Cloud.afterFind('beforeFind', () => { + throw 'afterFind should not run'; + }); + const obj = new Parse.Object('beforeFind'); + await obj.save(); + const newObj = await new Parse.Query('beforeFind').first(); + expect(newObj.className).toBe('TestObject'); + expect(newObj.toJSON()).toEqual({ foo: 'bar' }); + }); + + it('beforeFind can short circuit arrays', async () => { + Parse.Cloud.beforeFind('beforeFind', () => { + return [new Parse.Object('TestObject', { foo: 'bar' })]; + }); + Parse.Cloud.afterFind('beforeFind', () => { + throw 'afterFind should not run'; + }); + const obj = new Parse.Object('beforeFind'); + await obj.save(); + const newObj = await new Parse.Query('beforeFind').first(); + expect(newObj.className).toBe('TestObject'); + expect(newObj.toJSON()).toEqual({ foo: 'bar' }); + }); + + it('beforeFind can short circuit get', async () => { + Parse.Cloud.beforeFind('beforeFind', () => { + return [new Parse.Object('TestObject', { foo: 'bar' })]; + }); + Parse.Cloud.afterFind('beforeFind', () => { + throw 'afterFind should not run'; + }); + const obj = new Parse.Object('beforeFind'); + await obj.save(); + const newObj = await new Parse.Query('beforeFind').get(obj.id); + expect(newObj.className).toBe('TestObject'); + expect(newObj.toJSON()).toEqual({ foo: 'bar' }); + }); + + it('beforeFind can short circuit empty array', async () => { + Parse.Cloud.beforeFind('beforeFind', () => { + return []; + }); + Parse.Cloud.afterFind('beforeFind', () => { + throw 'afterFind should not run'; + }); + const obj = new Parse.Object('beforeFind'); + await obj.save(); + const newObj = await new Parse.Query('beforeFind').first(); + expect(newObj).toBeUndefined(); + }); + it('beforeSave rejection with custom error code', function (done) { Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function () { throw new Parse.Error(999, 'Nope'); diff --git a/src/rest.js b/src/rest.js index e1e53668a6..5f7266b0b7 100644 --- a/src/rest.js +++ b/src/rest.js @@ -39,6 +39,11 @@ function find(config, auth, className, restWhere, restOptions, clientSDK, contex .then(result => { restWhere = result.restWhere || restWhere; restOptions = result.restOptions || restOptions; + if (result?.objects) { + return { + results: result.objects.map(row => row._toFullJSON()), + }; + } const query = new RestQuery( config, auth, @@ -71,6 +76,11 @@ const get = (config, auth, className, objectId, restOptions, clientSDK, context) .then(result => { restWhere = result.restWhere || restWhere; restOptions = result.restOptions || restOptions; + if (result?.objects) { + return { + results: result.objects.map(row => row._toFullJSON()), + }; + } const query = new RestQuery( config, auth, diff --git a/src/triggers.js b/src/triggers.js index b5f11435df..42219aba03 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -586,9 +586,19 @@ export function maybeRunQueryTrigger( restOptions = restOptions || {}; restOptions.subqueryReadPreference = requestObject.subqueryReadPreference; } + let objects = undefined; + if (result instanceof Parse.Object) { + objects = [result]; + } else if ( + Array.isArray(result) && + (!result.length || result.some(obj => obj instanceof Parse.Object)) + ) { + objects = result; + } return { restWhere, restOptions, + objects, }; }, err => { From 41bf1b74ec82cdb69e0157ef856571437af446df Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sun, 23 Jul 2023 15:23:13 +0200 Subject: [PATCH 02/16] improve test description Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- spec/CloudCode.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index c38199b3d0..8465ab68d9 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -201,7 +201,7 @@ describe('Cloud Code', () => { } }); - it('beforeFind can short circuit', async () => { + it('beforeFind can return object without DB operation', async () => { Parse.Cloud.beforeFind('beforeFind', () => { return new Parse.Object('TestObject', { foo: 'bar' }); }); @@ -215,7 +215,7 @@ describe('Cloud Code', () => { expect(newObj.toJSON()).toEqual({ foo: 'bar' }); }); - it('beforeFind can short circuit arrays', async () => { + it('beforeFind can return array of objects without DB operation', async () => { Parse.Cloud.beforeFind('beforeFind', () => { return [new Parse.Object('TestObject', { foo: 'bar' })]; }); @@ -229,7 +229,7 @@ describe('Cloud Code', () => { expect(newObj.toJSON()).toEqual({ foo: 'bar' }); }); - it('beforeFind can short circuit get', async () => { + it('beforeFind can return object for get query without DB operation', async () => { Parse.Cloud.beforeFind('beforeFind', () => { return [new Parse.Object('TestObject', { foo: 'bar' })]; }); @@ -243,7 +243,7 @@ describe('Cloud Code', () => { expect(newObj.toJSON()).toEqual({ foo: 'bar' }); }); - it('beforeFind can short circuit empty array', async () => { + it('beforeFind can return empty array without DB operation', async () => { Parse.Cloud.beforeFind('beforeFind', () => { return []; }); From 21c9289fb0a6086cd038e3ad8d18e6f67eccf7fd Mon Sep 17 00:00:00 2001 From: dblythy Date: Sun, 23 Jul 2023 23:43:25 +1000 Subject: [PATCH 03/16] Update CloudCode.spec.js --- spec/CloudCode.spec.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 8465ab68d9..1ddf710a8e 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -208,11 +208,10 @@ describe('Cloud Code', () => { Parse.Cloud.afterFind('beforeFind', () => { throw 'afterFind should not run'; }); - const obj = new Parse.Object('beforeFind'); - await obj.save(); const newObj = await new Parse.Query('beforeFind').first(); expect(newObj.className).toBe('TestObject'); expect(newObj.toJSON()).toEqual({ foo: 'bar' }); + await newObj.save(); }); it('beforeFind can return array of objects without DB operation', async () => { @@ -222,11 +221,10 @@ describe('Cloud Code', () => { Parse.Cloud.afterFind('beforeFind', () => { throw 'afterFind should not run'; }); - const obj = new Parse.Object('beforeFind'); - await obj.save(); const newObj = await new Parse.Query('beforeFind').first(); expect(newObj.className).toBe('TestObject'); expect(newObj.toJSON()).toEqual({ foo: 'bar' }); + await newObj.save(); }); it('beforeFind can return object for get query without DB operation', async () => { @@ -236,11 +234,10 @@ describe('Cloud Code', () => { Parse.Cloud.afterFind('beforeFind', () => { throw 'afterFind should not run'; }); - const obj = new Parse.Object('beforeFind'); - await obj.save(); - const newObj = await new Parse.Query('beforeFind').get(obj.id); + const newObj = await new Parse.Query('beforeFind').get('objId'); expect(newObj.className).toBe('TestObject'); expect(newObj.toJSON()).toEqual({ foo: 'bar' }); + await newObj.save(); }); it('beforeFind can return empty array without DB operation', async () => { From ca749293202d9e411ebbb9d21fe92d713b79d8b7 Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 24 Jul 2023 22:26:22 +1000 Subject: [PATCH 04/16] wip --- spec/CloudCode.spec.js | 23 ++++---- src/rest.js | 123 ++++++++++++++++++++--------------------- src/triggers.js | 3 + 3 files changed, 77 insertions(+), 72 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 1ddf710a8e..7c1274a0f2 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -200,13 +200,14 @@ describe('Cloud Code', () => { done(); } }); - - it('beforeFind can return object without DB operation', async () => { + fit('beforeFind can return object without DB operation', async () => { + await reconfigureServer({ silent: false }); Parse.Cloud.beforeFind('beforeFind', () => { return new Parse.Object('TestObject', { foo: 'bar' }); }); - Parse.Cloud.afterFind('beforeFind', () => { - throw 'afterFind should not run'; + Parse.Cloud.afterFind('beforeFind', req => { + expect(req.objects).toBeDefined(); + expect(req.objects[0].className).toBe('TestObject'); }); const newObj = await new Parse.Query('beforeFind').first(); expect(newObj.className).toBe('TestObject'); @@ -214,12 +215,13 @@ describe('Cloud Code', () => { await newObj.save(); }); - it('beforeFind can return array of objects without DB operation', async () => { + fit('beforeFind can return array of objects without DB operation', async () => { Parse.Cloud.beforeFind('beforeFind', () => { return [new Parse.Object('TestObject', { foo: 'bar' })]; }); - Parse.Cloud.afterFind('beforeFind', () => { - throw 'afterFind should not run'; + Parse.Cloud.afterFind('beforeFind', req => { + expect(req.objects).toBeDefined(); + expect(req.objects[0].className).toBe('TestObject'); }); const newObj = await new Parse.Query('beforeFind').first(); expect(newObj.className).toBe('TestObject'); @@ -227,12 +229,13 @@ describe('Cloud Code', () => { await newObj.save(); }); - it('beforeFind can return object for get query without DB operation', async () => { + fit('beforeFind can return object for get query without DB operation', async () => { Parse.Cloud.beforeFind('beforeFind', () => { return [new Parse.Object('TestObject', { foo: 'bar' })]; }); - Parse.Cloud.afterFind('beforeFind', () => { - throw 'afterFind should not run'; + Parse.Cloud.afterFind('beforeFind', req => { + expect(req.objects).toBeDefined(); + expect(req.objects[0].className).toBe('TestObject'); }); const newObj = await new Parse.Query('beforeFind').get('objId'); expect(newObj.className).toBe('TestObject'); diff --git a/src/rest.js b/src/rest.js index 5f7266b0b7..4f11135d40 100644 --- a/src/rest.js +++ b/src/rest.js @@ -23,76 +23,75 @@ function checkLiveQuery(className, config) { return config.liveQueryController && config.liveQueryController.hasLiveQuery(className); } -// Returns a promise for an object with optional keys 'results' and 'count'. -function find(config, auth, className, restWhere, restOptions, clientSDK, context) { - enforceRoleSecurity('find', className, auth); - return triggers - .maybeRunQueryTrigger( - triggers.Types.beforeFind, +async function runFindTriggers( + config, + auth, + className, + restWhere, + restOptions, + clientSDK, + context, + isGet +) { + const result = await triggers.maybeRunQueryTrigger( + triggers.Types.beforeFind, + className, + restWhere, + restOptions, + config, + auth, + context, + isGet + ); + restWhere = result.restWhere || restWhere; + restOptions = result.restOptions || restOptions; + if (result?.objects) { + const objects = result.objects; + await triggers.maybeRunAfterFindTrigger( + triggers.Types.afterFind, + auth, className, - restWhere, - restOptions, + objects, config, - auth, + restWhere, context - ) - .then(result => { - restWhere = result.restWhere || restWhere; - restOptions = result.restOptions || restOptions; - if (result?.objects) { - return { - results: result.objects.map(row => row._toFullJSON()), - }; - } - const query = new RestQuery( - config, - auth, - className, - restWhere, - restOptions, - clientSDK, - true, - context - ); - return query.execute(); - }); + ); + return { + results: objects.map(row => row._toFullJSON()), + }; + } + const query = new RestQuery( + config, + auth, + className, + restWhere, + restOptions, + clientSDK, + true, + context + ); + return await query.execute(); } +// Returns a promise for an object with optional keys 'results' and 'count'. +const find = (config, auth, className, restWhere, restOptions, clientSDK, context) => { + enforceRoleSecurity('find', className, auth); + return runFindTriggers(config, auth, className, restWhere, restOptions, clientSDK, context); +}; + // get is just like find but only queries an objectId. const get = (config, auth, className, objectId, restOptions, clientSDK, context) => { - var restWhere = { objectId }; enforceRoleSecurity('get', className, auth); - return triggers - .maybeRunQueryTrigger( - triggers.Types.beforeFind, - className, - restWhere, - restOptions, - config, - auth, - context, - true - ) - .then(result => { - restWhere = result.restWhere || restWhere; - restOptions = result.restOptions || restOptions; - if (result?.objects) { - return { - results: result.objects.map(row => row._toFullJSON()), - }; - } - const query = new RestQuery( - config, - auth, - className, - restWhere, - restOptions, - clientSDK, - true, - context - ); - return query.execute(); - }); + return runFindTriggers( + config, + auth, + className, + { objectId }, + restOptions, + clientSDK, + context, + true + ); }; // Returns a promise that doesn't resolve to any useful value. diff --git a/src/triggers.js b/src/triggers.js index 42219aba03..fb867967cd 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -454,6 +454,9 @@ export function maybeRunAfterFindTrigger( ); request.objects = objects.map(object => { //setting the class name to transform into parse object + if (object instanceof Parse.Object) { + return object; + } object.className = className; return Parse.Object.fromJSON(object); }); From 207c3c04ca80ca31db6af0a0181431bd8fdcdcfe Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 24 Jul 2023 22:40:15 +1000 Subject: [PATCH 05/16] wip --- spec/CloudCode.spec.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 7c1274a0f2..720a91c42d 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -200,8 +200,7 @@ describe('Cloud Code', () => { done(); } }); - fit('beforeFind can return object without DB operation', async () => { - await reconfigureServer({ silent: false }); + it('beforeFind can return object without DB operation', async () => { Parse.Cloud.beforeFind('beforeFind', () => { return new Parse.Object('TestObject', { foo: 'bar' }); }); @@ -215,7 +214,7 @@ describe('Cloud Code', () => { await newObj.save(); }); - fit('beforeFind can return array of objects without DB operation', async () => { + it('beforeFind can return array of objects without DB operation', async () => { Parse.Cloud.beforeFind('beforeFind', () => { return [new Parse.Object('TestObject', { foo: 'bar' })]; }); @@ -229,7 +228,7 @@ describe('Cloud Code', () => { await newObj.save(); }); - fit('beforeFind can return object for get query without DB operation', async () => { + it('beforeFind can return object for get query without DB operation', async () => { Parse.Cloud.beforeFind('beforeFind', () => { return [new Parse.Object('TestObject', { foo: 'bar' })]; }); From bc68816045614487a034141f2dc7607e3bd55832 Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 25 Jul 2023 18:38:16 +1000 Subject: [PATCH 06/16] wip --- spec/CloudCode.spec.js | 10 +++++----- src/triggers.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 720a91c42d..db8e81dab7 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -206,7 +206,7 @@ describe('Cloud Code', () => { }); Parse.Cloud.afterFind('beforeFind', req => { expect(req.objects).toBeDefined(); - expect(req.objects[0].className).toBe('TestObject'); + expect(req.objects[0].get('foo')).toBe('bar'); }); const newObj = await new Parse.Query('beforeFind').first(); expect(newObj.className).toBe('TestObject'); @@ -220,7 +220,7 @@ describe('Cloud Code', () => { }); Parse.Cloud.afterFind('beforeFind', req => { expect(req.objects).toBeDefined(); - expect(req.objects[0].className).toBe('TestObject'); + expect(req.objects[0].get('foo')).toBe('bar'); }); const newObj = await new Parse.Query('beforeFind').first(); expect(newObj.className).toBe('TestObject'); @@ -234,7 +234,7 @@ describe('Cloud Code', () => { }); Parse.Cloud.afterFind('beforeFind', req => { expect(req.objects).toBeDefined(); - expect(req.objects[0].className).toBe('TestObject'); + expect(req.objects[0].get('foo')).toBe('bar'); }); const newObj = await new Parse.Query('beforeFind').get('objId'); expect(newObj.className).toBe('TestObject'); @@ -246,8 +246,8 @@ describe('Cloud Code', () => { Parse.Cloud.beforeFind('beforeFind', () => { return []; }); - Parse.Cloud.afterFind('beforeFind', () => { - throw 'afterFind should not run'; + Parse.Cloud.afterFind('beforeFind', req => { + expect(req.objects.length).toBe(0); }); const obj = new Parse.Object('beforeFind'); await obj.save(); diff --git a/src/triggers.js b/src/triggers.js index fb867967cd..a7e997e3a1 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -454,10 +454,10 @@ export function maybeRunAfterFindTrigger( ); request.objects = objects.map(object => { //setting the class name to transform into parse object + object.className = className; if (object instanceof Parse.Object) { return object; } - object.className = className; return Parse.Object.fromJSON(object); }); return Promise.resolve() From 34f77413f177172cd7ea27b5ef00c63b3ca5a04d Mon Sep 17 00:00:00 2001 From: dblythy Date: Wed, 19 Jul 2023 13:25:04 +1000 Subject: [PATCH 07/16] feat: allow short circuit of beforeFind --- spec/CloudCode.spec.js | 55 +++++++++++++++++++++++++++ src/rest.js | 85 +++++++++++++++++++++++++++++++----------- src/triggers.js | 10 +++++ 3 files changed, 128 insertions(+), 22 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 59ae534df2..c4cb06f065 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -202,6 +202,61 @@ describe('Cloud Code', () => { } }); + it('beforeFind can short circuit', async () => { + Parse.Cloud.beforeFind('beforeFind', () => { + return new Parse.Object('TestObject', { foo: 'bar' }); + }); + Parse.Cloud.afterFind('beforeFind', () => { + throw 'afterFind should not run'; + }); + const obj = new Parse.Object('beforeFind'); + await obj.save(); + const newObj = await new Parse.Query('beforeFind').first(); + expect(newObj.className).toBe('TestObject'); + expect(newObj.toJSON()).toEqual({ foo: 'bar' }); + }); + + it('beforeFind can short circuit arrays', async () => { + Parse.Cloud.beforeFind('beforeFind', () => { + return [new Parse.Object('TestObject', { foo: 'bar' })]; + }); + Parse.Cloud.afterFind('beforeFind', () => { + throw 'afterFind should not run'; + }); + const obj = new Parse.Object('beforeFind'); + await obj.save(); + const newObj = await new Parse.Query('beforeFind').first(); + expect(newObj.className).toBe('TestObject'); + expect(newObj.toJSON()).toEqual({ foo: 'bar' }); + }); + + it('beforeFind can short circuit get', async () => { + Parse.Cloud.beforeFind('beforeFind', () => { + return [new Parse.Object('TestObject', { foo: 'bar' })]; + }); + Parse.Cloud.afterFind('beforeFind', () => { + throw 'afterFind should not run'; + }); + const obj = new Parse.Object('beforeFind'); + await obj.save(); + const newObj = await new Parse.Query('beforeFind').get(obj.id); + expect(newObj.className).toBe('TestObject'); + expect(newObj.toJSON()).toEqual({ foo: 'bar' }); + }); + + it('beforeFind can short circuit empty array', async () => { + Parse.Cloud.beforeFind('beforeFind', () => { + return []; + }); + Parse.Cloud.afterFind('beforeFind', () => { + throw 'afterFind should not run'; + }); + const obj = new Parse.Object('beforeFind'); + await obj.save(); + const newObj = await new Parse.Query('beforeFind').first(); + expect(newObj).toBeUndefined(); + }); + it('beforeSave rejection with custom error code', function (done) { Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function () { throw new Parse.Error(999, 'Nope'); diff --git a/src/rest.js b/src/rest.js index 1f9dbacb73..811cfcea45 100644 --- a/src/rest.js +++ b/src/rest.js @@ -26,33 +26,74 @@ function checkLiveQuery(className, config) { // Returns a promise for an object with optional keys 'results' and 'count'. const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => { - const query = await RestQuery({ - method: RestQuery.Method.find, - config, - auth, - className, - restWhere, - restOptions, - clientSDK, - context, - }); - return query.execute(); + enforceRoleSecurity('find', className, auth); + return triggers + .maybeRunQueryTrigger( + triggers.Types.beforeFind, + className, + restWhere, + restOptions, + config, + auth, + context + ) + .then(async result => { + restWhere = result.restWhere || restWhere; + restOptions = result.restOptions || restOptions; + if (result?.objects) { + return { + results: result.objects.map(row => row._toFullJSON()), + }; + } + const query = await RestQuery({ + method: RestQuery.Method.find, + config, + auth, + className, + restWhere, + restOptions, + clientSDK, + context, + }); + return query.execute(); + }); }; // get is just like find but only queries an objectId. const get = async (config, auth, className, objectId, restOptions, clientSDK, context) => { var restWhere = { objectId }; - const query = await RestQuery({ - method: RestQuery.Method.get, - config, - auth, - className, - restWhere, - restOptions, - clientSDK, - context, - }); - return query.execute(); + enforceRoleSecurity('get', className, auth); + return triggers + .maybeRunQueryTrigger( + triggers.Types.beforeFind, + className, + restWhere, + restOptions, + config, + auth, + context, + true + ) + .then(async result => { + restWhere = result.restWhere || restWhere; + restOptions = result.restOptions || restOptions; + if (result?.objects) { + return { + results: result.objects.map(row => row._toFullJSON()), + }; + } + const query = await RestQuery({ + method: RestQuery.Method.get, + config, + auth, + className, + restWhere, + restOptions, + clientSDK, + context, + }); + return query.execute(); + }); }; // Returns a promise that doesn't resolve to any useful value. diff --git a/src/triggers.js b/src/triggers.js index 2dfbeff7ac..5c19dff240 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -607,9 +607,19 @@ export function maybeRunQueryTrigger( restOptions = restOptions || {}; restOptions.subqueryReadPreference = requestObject.subqueryReadPreference; } + let objects = undefined; + if (result instanceof Parse.Object) { + objects = [result]; + } else if ( + Array.isArray(result) && + (!result.length || result.some(obj => obj instanceof Parse.Object)) + ) { + objects = result; + } return { restWhere, restOptions, + objects, }; }, err => { From 5825abf827ac08d815abffd68a5477fe31a45190 Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Sun, 23 Jul 2023 15:23:13 +0200 Subject: [PATCH 08/16] improve test description Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- spec/CloudCode.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index c4cb06f065..7f3895d99d 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -202,7 +202,7 @@ describe('Cloud Code', () => { } }); - it('beforeFind can short circuit', async () => { + it('beforeFind can return object without DB operation', async () => { Parse.Cloud.beforeFind('beforeFind', () => { return new Parse.Object('TestObject', { foo: 'bar' }); }); @@ -216,7 +216,7 @@ describe('Cloud Code', () => { expect(newObj.toJSON()).toEqual({ foo: 'bar' }); }); - it('beforeFind can short circuit arrays', async () => { + it('beforeFind can return array of objects without DB operation', async () => { Parse.Cloud.beforeFind('beforeFind', () => { return [new Parse.Object('TestObject', { foo: 'bar' })]; }); @@ -230,7 +230,7 @@ describe('Cloud Code', () => { expect(newObj.toJSON()).toEqual({ foo: 'bar' }); }); - it('beforeFind can short circuit get', async () => { + it('beforeFind can return object for get query without DB operation', async () => { Parse.Cloud.beforeFind('beforeFind', () => { return [new Parse.Object('TestObject', { foo: 'bar' })]; }); @@ -244,7 +244,7 @@ describe('Cloud Code', () => { expect(newObj.toJSON()).toEqual({ foo: 'bar' }); }); - it('beforeFind can short circuit empty array', async () => { + it('beforeFind can return empty array without DB operation', async () => { Parse.Cloud.beforeFind('beforeFind', () => { return []; }); From 6daa039cc4e605e5a896ab3d9d2e34a67e88bb2a Mon Sep 17 00:00:00 2001 From: dblythy Date: Sun, 23 Jul 2023 23:43:25 +1000 Subject: [PATCH 09/16] Update CloudCode.spec.js --- spec/CloudCode.spec.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 7f3895d99d..09194cde17 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -209,11 +209,10 @@ describe('Cloud Code', () => { Parse.Cloud.afterFind('beforeFind', () => { throw 'afterFind should not run'; }); - const obj = new Parse.Object('beforeFind'); - await obj.save(); const newObj = await new Parse.Query('beforeFind').first(); expect(newObj.className).toBe('TestObject'); expect(newObj.toJSON()).toEqual({ foo: 'bar' }); + await newObj.save(); }); it('beforeFind can return array of objects without DB operation', async () => { @@ -223,11 +222,10 @@ describe('Cloud Code', () => { Parse.Cloud.afterFind('beforeFind', () => { throw 'afterFind should not run'; }); - const obj = new Parse.Object('beforeFind'); - await obj.save(); const newObj = await new Parse.Query('beforeFind').first(); expect(newObj.className).toBe('TestObject'); expect(newObj.toJSON()).toEqual({ foo: 'bar' }); + await newObj.save(); }); it('beforeFind can return object for get query without DB operation', async () => { @@ -237,11 +235,10 @@ describe('Cloud Code', () => { Parse.Cloud.afterFind('beforeFind', () => { throw 'afterFind should not run'; }); - const obj = new Parse.Object('beforeFind'); - await obj.save(); - const newObj = await new Parse.Query('beforeFind').get(obj.id); + const newObj = await new Parse.Query('beforeFind').get('objId'); expect(newObj.className).toBe('TestObject'); expect(newObj.toJSON()).toEqual({ foo: 'bar' }); + await newObj.save(); }); it('beforeFind can return empty array without DB operation', async () => { From ec37789cd3553e9e438f9008d65a76fe79bdbc4f Mon Sep 17 00:00:00 2001 From: dblythy Date: Mon, 24 Jul 2023 22:26:22 +1000 Subject: [PATCH 10/16] wip --- src/rest.js | 138 ++++++++++++++++++++++++++---------------------- src/triggers.js | 3 ++ 2 files changed, 77 insertions(+), 64 deletions(-) diff --git a/src/rest.js b/src/rest.js index 811cfcea45..323d001139 100644 --- a/src/rest.js +++ b/src/rest.js @@ -23,80 +23,90 @@ function checkTriggers(className, config, types) { function checkLiveQuery(className, config) { return config.liveQueryController && config.liveQueryController.hasLiveQuery(className); } +async function runFindTriggers( + config, + auth, + className, + restWhere, + restOptions, + clientSDK, + context, + isGet +) { + const result = await triggers.maybeRunQueryTrigger( + triggers.Types.beforeFind, + className, + restWhere, + restOptions, + config, + auth, + context, + isGet + ); -// Returns a promise for an object with optional keys 'results' and 'count'. -const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => { - enforceRoleSecurity('find', className, auth); - return triggers - .maybeRunQueryTrigger( - triggers.Types.beforeFind, + restWhere = result.restWhere || restWhere; + restOptions = result.restOptions || restOptions; + + if (result?.objects) { + const objects = result.objects; + + await triggers.maybeRunAfterFindTrigger( + triggers.Types.afterFind, + auth, className, - restWhere, - restOptions, + objects, config, - auth, + restWhere, context - ) - .then(async result => { - restWhere = result.restWhere || restWhere; - restOptions = result.restOptions || restOptions; - if (result?.objects) { - return { - results: result.objects.map(row => row._toFullJSON()), - }; - } - const query = await RestQuery({ - method: RestQuery.Method.find, - config, - auth, - className, - restWhere, - restOptions, - clientSDK, - context, - }); - return query.execute(); - }); + ); + + return { + results: objects.map(row => row._toFullJSON()), + }; + } + + const query = await RestQuery({ + method: isGet ? RestQuery.Method.get : RestQuery.Method.find, + config, + auth, + className, + restWhere, + restOptions, + clientSDK, + context, + }); + + return query.execute(); +} + +const find = async (config, auth, className, restWhere, restOptions, clientSDK, context) => { + enforceRoleSecurity('find', className, auth); + return runFindTriggers( + config, + auth, + className, + restWhere, + restOptions, + clientSDK, + context, + false + ); }; -// get is just like find but only queries an objectId. const get = async (config, auth, className, objectId, restOptions, clientSDK, context) => { - var restWhere = { objectId }; enforceRoleSecurity('get', className, auth); - return triggers - .maybeRunQueryTrigger( - triggers.Types.beforeFind, - className, - restWhere, - restOptions, - config, - auth, - context, - true - ) - .then(async result => { - restWhere = result.restWhere || restWhere; - restOptions = result.restOptions || restOptions; - if (result?.objects) { - return { - results: result.objects.map(row => row._toFullJSON()), - }; - } - const query = await RestQuery({ - method: RestQuery.Method.get, - config, - auth, - className, - restWhere, - restOptions, - clientSDK, - context, - }); - return query.execute(); - }); + return runFindTriggers( + config, + auth, + className, + { objectId }, + restOptions, + clientSDK, + context, + true + ); }; -// Returns a promise that doesn't resolve to any useful value. function del(config, auth, className, objectId, context) { if (typeof objectId !== 'string') { throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad objectId'); diff --git a/src/triggers.js b/src/triggers.js index 5c19dff240..06aec4d115 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -471,6 +471,9 @@ export function maybeRunAfterFindTrigger( ); request.objects = objects.map(object => { //setting the class name to transform into parse object + if (object instanceof Parse.Object) { + return object; + } object.className = className; return Parse.Object.fromJSON(object); }); From 09fcde25f7d1ad5c78a3cf97fc673cd294b9e74e Mon Sep 17 00:00:00 2001 From: dblythy Date: Tue, 25 Jul 2023 18:38:16 +1000 Subject: [PATCH 11/16] wip --- spec/CloudCode.spec.js | 19 +++++++++++-------- src/triggers.js | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 09194cde17..95c85fa3a1 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -206,8 +206,9 @@ describe('Cloud Code', () => { Parse.Cloud.beforeFind('beforeFind', () => { return new Parse.Object('TestObject', { foo: 'bar' }); }); - Parse.Cloud.afterFind('beforeFind', () => { - throw 'afterFind should not run'; + Parse.Cloud.afterFind('beforeFind', req => { + expect(req.objects).toBeDefined(); + expect(req.objects[0].get('foo')).toBe('bar'); }); const newObj = await new Parse.Query('beforeFind').first(); expect(newObj.className).toBe('TestObject'); @@ -219,8 +220,9 @@ describe('Cloud Code', () => { Parse.Cloud.beforeFind('beforeFind', () => { return [new Parse.Object('TestObject', { foo: 'bar' })]; }); - Parse.Cloud.afterFind('beforeFind', () => { - throw 'afterFind should not run'; + Parse.Cloud.afterFind('beforeFind', req => { + expect(req.objects).toBeDefined(); + expect(req.objects[0].get('foo')).toBe('bar'); }); const newObj = await new Parse.Query('beforeFind').first(); expect(newObj.className).toBe('TestObject'); @@ -232,8 +234,9 @@ describe('Cloud Code', () => { Parse.Cloud.beforeFind('beforeFind', () => { return [new Parse.Object('TestObject', { foo: 'bar' })]; }); - Parse.Cloud.afterFind('beforeFind', () => { - throw 'afterFind should not run'; + Parse.Cloud.afterFind('beforeFind', req => { + expect(req.objects).toBeDefined(); + expect(req.objects[0].get('foo')).toBe('bar'); }); const newObj = await new Parse.Query('beforeFind').get('objId'); expect(newObj.className).toBe('TestObject'); @@ -245,8 +248,8 @@ describe('Cloud Code', () => { Parse.Cloud.beforeFind('beforeFind', () => { return []; }); - Parse.Cloud.afterFind('beforeFind', () => { - throw 'afterFind should not run'; + Parse.Cloud.afterFind('beforeFind', req => { + expect(req.objects.length).toBe(0); }); const obj = new Parse.Object('beforeFind'); await obj.save(); diff --git a/src/triggers.js b/src/triggers.js index 06aec4d115..c2cc9e3644 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -471,10 +471,10 @@ export function maybeRunAfterFindTrigger( ); request.objects = objects.map(object => { //setting the class name to transform into parse object + object.className = className; if (object instanceof Parse.Object) { return object; } - object.className = className; return Parse.Object.fromJSON(object); }); return Promise.resolve() From dd7506255774aea79d879f8bd0fd6d8c8ebc6d15 Mon Sep 17 00:00:00 2001 From: EmpiDev Date: Tue, 27 May 2025 13:48:13 +0200 Subject: [PATCH 12/16] refactor: remove redundant beforeFind tests and clean up afterFind hooks --- spec/CloudCode.spec.js | 506 +++++++++++++++++++---------------------- 1 file changed, 239 insertions(+), 267 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index dd197ce4ae..78a0ed2121 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -256,66 +256,6 @@ describe('Cloud Code', () => { expect(newObj).toBeUndefined(); }); - it('beforeFind can return object without DB operation', async () => { - Parse.Cloud.beforeFind('beforeFind', () => { - return new Parse.Object('TestObject', { foo: 'bar' }); - }); - Parse.Cloud.afterFind('beforeFind', req => { - expect(req.objects).toBeDefined(); - expect(req.objects[0].get('foo')).toBe('bar'); - }); - const newObj = await new Parse.Query('beforeFind').first(); - expect(newObj.className).toBe('TestObject'); - expect(newObj.toJSON()).toEqual({ foo: 'bar' }); - await newObj.save(); - }); - - it('beforeFind can return array of objects without DB operation', async () => { - Parse.Cloud.beforeFind('beforeFind', () => { - return [new Parse.Object('TestObject', { foo: 'bar' })]; - }); - Parse.Cloud.afterFind('beforeFind', req => { - expect(req.objects).toBeDefined(); - expect(req.objects[0].get('foo')).toBe('bar'); - }); - const newObj = await new Parse.Query('beforeFind').first(); - expect(newObj.className).toBe('TestObject'); - expect(newObj.toJSON()).toEqual({ foo: 'bar' }); - await newObj.save(); - }); - - it('beforeFind can return object for get query without DB operation', async () => { - Parse.Cloud.beforeFind('beforeFind', () => { - return [new Parse.Object('TestObject', { foo: 'bar' })]; - }); - Parse.Cloud.afterFind('beforeFind', req => { - expect(req.objects).toBeDefined(); - expect(req.objects[0].get('foo')).toBe('bar'); - }); - const newObj = await new Parse.Query('beforeFind').get('objId'); - expect(newObj.className).toBe('TestObject'); - expect(newObj.toJSON()).toEqual({ foo: 'bar' }); - await newObj.save(); - }); - - it('beforeFind can return empty array without DB operation', async () => { - Parse.Cloud.beforeFind('beforeFind', () => { - return []; - }); - Parse.Cloud.afterFind('beforeFind', req => { - expect(req.objects.length).toBe(0); - }); - const obj = new Parse.Object('beforeFind'); - await obj.save(); - const newObj = await new Parse.Query('beforeFind').first(); - expect(newObj).toBeUndefined(); - }); - - it('beforeSave rejection with custom error code', function (done) { - Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function () { - throw new Parse.Error(999, 'Nope'); - }); - const obj = new Parse.Object('BeforeSaveFailWithErrorCode'); obj.set('foo', 'bar'); obj.save().then( @@ -3028,55 +2968,61 @@ describe('afterFind hooks', () => { }).toThrow('Only the _Session class is allowed for the afterLogout trigger.'); }); - it_id('c16159b5-e8ee-42d5-8fe3-e2f7c006881d')(it)('should skip afterFind hooks for aggregate', done => { - const hook = { - method: function () { - return Promise.reject(); - }, - }; - spyOn(hook, 'method').and.callThrough(); - Parse.Cloud.afterFind('MyObject', hook.method); - const obj = new Parse.Object('MyObject'); - const pipeline = [ - { - $group: { _id: {} }, - }, - ]; - obj - .save() - .then(() => { - const query = new Parse.Query('MyObject'); - return query.aggregate(pipeline); - }) - .then(results => { - expect(results[0].objectId).toEqual(null); - expect(hook.method).not.toHaveBeenCalled(); - done(); - }); - }); + it_id('c16159b5-e8ee-42d5-8fe3-e2f7c006881d')(it)( + 'should skip afterFind hooks for aggregate', + done => { + const hook = { + method: function () { + return Promise.reject(); + }, + }; + spyOn(hook, 'method').and.callThrough(); + Parse.Cloud.afterFind('MyObject', hook.method); + const obj = new Parse.Object('MyObject'); + const pipeline = [ + { + $group: { _id: {} }, + }, + ]; + obj + .save() + .then(() => { + const query = new Parse.Query('MyObject'); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results[0].objectId).toEqual(null); + expect(hook.method).not.toHaveBeenCalled(); + done(); + }); + } + ); - it_id('ca55c90d-36db-422c-9060-a30583ce5224')(it)('should skip afterFind hooks for distinct', done => { - const hook = { - method: function () { - return Promise.reject(); - }, - }; - spyOn(hook, 'method').and.callThrough(); - Parse.Cloud.afterFind('MyObject', hook.method); - const obj = new Parse.Object('MyObject'); - obj.set('score', 10); - obj - .save() - .then(() => { - const query = new Parse.Query('MyObject'); - return query.distinct('score'); - }) - .then(results => { - expect(results[0]).toEqual(10); - expect(hook.method).not.toHaveBeenCalled(); - done(); - }); - }); + it_id('ca55c90d-36db-422c-9060-a30583ce5224')(it)( + 'should skip afterFind hooks for distinct', + done => { + const hook = { + method: function () { + return Promise.reject(); + }, + }; + spyOn(hook, 'method').and.callThrough(); + Parse.Cloud.afterFind('MyObject', hook.method); + const obj = new Parse.Object('MyObject'); + obj.set('score', 10); + obj + .save() + .then(() => { + const query = new Parse.Query('MyObject'); + return query.distinct('score'); + }) + .then(results => { + expect(results[0]).toEqual(10); + expect(hook.method).not.toHaveBeenCalled(); + done(); + }); + } + ); it('should throw error if context header is malformed', async () => { let calledBefore = false; @@ -3142,37 +3088,40 @@ describe('afterFind hooks', () => { expect(calledAfter).toBe(false); }); - it_id('55ef1741-cf72-4a7c-a029-00cb75f53233')(it)('should expose context in beforeSave/afterSave via header', async () => { - let calledBefore = false; - let calledAfter = false; - Parse.Cloud.beforeSave('TestObject', req => { - expect(req.object.get('foo')).toEqual('bar'); - expect(req.context.otherKey).toBe(1); - expect(req.context.key).toBe('value'); - calledBefore = true; - }); - Parse.Cloud.afterSave('TestObject', req => { - expect(req.object.get('foo')).toEqual('bar'); - expect(req.context.otherKey).toBe(1); - expect(req.context.key).toBe('value'); - calledAfter = true; - }); - const req = request({ - method: 'POST', - url: 'http://localhost:8378/1/classes/TestObject', - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Cloud-Context': '{"key":"value","otherKey":1}', - }, - body: { - foo: 'bar', - }, - }); - await req; - expect(calledBefore).toBe(true); - expect(calledAfter).toBe(true); - }); + it_id('55ef1741-cf72-4a7c-a029-00cb75f53233')(it)( + 'should expose context in beforeSave/afterSave via header', + async () => { + let calledBefore = false; + let calledAfter = false; + Parse.Cloud.beforeSave('TestObject', req => { + expect(req.object.get('foo')).toEqual('bar'); + expect(req.context.otherKey).toBe(1); + expect(req.context.key).toBe('value'); + calledBefore = true; + }); + Parse.Cloud.afterSave('TestObject', req => { + expect(req.object.get('foo')).toEqual('bar'); + expect(req.context.otherKey).toBe(1); + expect(req.context.key).toBe('value'); + calledAfter = true; + }); + const req = request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Cloud-Context': '{"key":"value","otherKey":1}', + }, + body: { + foo: 'bar', + }, + }); + await req; + expect(calledBefore).toBe(true); + expect(calledAfter).toBe(true); + } + ); it('should override header context with body context in beforeSave/afterSave', async () => { let calledBefore = false; @@ -3456,20 +3405,23 @@ describe('beforeLogin hook', () => { expect(response).toEqual(error); }); - it_id('5656d6d7-65ef-43d1-8ca6-6942ae3614d5')(it)('should have expected data in request in beforeLogin', async done => { - Parse.Cloud.beforeLogin(req => { - expect(req.object).toBeDefined(); - expect(req.user).toBeUndefined(); - expect(req.headers).toBeDefined(); - expect(req.ip).toBeDefined(); - expect(req.installationId).toBeDefined(); - expect(req.context).toBeDefined(); - }); + it_id('5656d6d7-65ef-43d1-8ca6-6942ae3614d5')(it)( + 'should have expected data in request in beforeLogin', + async done => { + Parse.Cloud.beforeLogin(req => { + expect(req.object).toBeDefined(); + expect(req.user).toBeUndefined(); + expect(req.headers).toBeDefined(); + expect(req.ip).toBeDefined(); + expect(req.installationId).toBeDefined(); + expect(req.context).toBeDefined(); + }); - await Parse.User.signUp('tupac', 'shakur'); - await Parse.User.logIn('tupac', 'shakur'); - done(); - }); + await Parse.User.signUp('tupac', 'shakur'); + await Parse.User.logIn('tupac', 'shakur'); + done(); + } + ); it('afterFind should not be triggered when saving an object', async () => { let beforeSaves = 0; @@ -3573,20 +3525,23 @@ describe('afterLogin hook', () => { done(); }); - it_id('e86155c4-62e1-4c6e-ab4a-9ac6c87c60f2')(it)('should have expected data in request in afterLogin', async done => { - Parse.Cloud.afterLogin(req => { - expect(req.object).toBeDefined(); - expect(req.user).toBeDefined(); - expect(req.headers).toBeDefined(); - expect(req.ip).toBeDefined(); - expect(req.installationId).toBeDefined(); - expect(req.context).toBeDefined(); - }); + it_id('e86155c4-62e1-4c6e-ab4a-9ac6c87c60f2')(it)( + 'should have expected data in request in afterLogin', + async done => { + Parse.Cloud.afterLogin(req => { + expect(req.object).toBeDefined(); + expect(req.user).toBeDefined(); + expect(req.headers).toBeDefined(); + expect(req.ip).toBeDefined(); + expect(req.installationId).toBeDefined(); + expect(req.context).toBeDefined(); + }); - await Parse.User.signUp('testuser', 'p@ssword'); - await Parse.User.logIn('testuser', 'p@ssword'); - done(); - }); + await Parse.User.signUp('testuser', 'p@ssword'); + await Parse.User.logIn('testuser', 'p@ssword'); + done(); + } + ); it('context options should override _context object property when saving a new object', async () => { Parse.Cloud.beforeSave('TestObject', req => { @@ -3611,9 +3566,8 @@ describe('afterLogin hook', () => { 'X-Parse-REST-API-Key': 'rest', 'X-Parse-Cloud-Context': '{"a":"a"}', }, - body: JSON.stringify({_context: { hello: 'world' }}), + body: JSON.stringify({ _context: { hello: 'world' } }), }); - }); it('should have access to context when saving a new object', async () => { @@ -4038,7 +3992,7 @@ describe('saveFile hooks', () => { }); }); -describe('Parse.File hooks', () => { +describe('Parse.File hooks', () => { it('find hooks should run', async () => { const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); await file.save({ useMasterKey: true }); @@ -4156,68 +4110,77 @@ describe('Parse.File hooks', () => { }); expect(response.headers['content-disposition']).toBe(`attachment;filename=${file._name}`); }); - }); +}); describe('Cloud Config hooks', () => { function testConfig() { return Parse.Config.save({ internal: 'i', string: 's', number: 12 }, { internal: true }); } - it_id('997fe20a-96f7-454a-a5b0-c155b8d02f05')(it)('beforeSave(Parse.Config) can run hook with new config', async () => { - let count = 0; - Parse.Cloud.beforeSave(Parse.Config, (req) => { - expect(req.object).toBeDefined(); - expect(req.original).toBeUndefined(); - expect(req.user).toBeUndefined(); - expect(req.headers).toBeDefined(); - expect(req.ip).toBeDefined(); - expect(req.installationId).toBeDefined(); - expect(req.context).toBeDefined(); - const config = req.object; + it_id('997fe20a-96f7-454a-a5b0-c155b8d02f05')(it)( + 'beforeSave(Parse.Config) can run hook with new config', + async () => { + let count = 0; + Parse.Cloud.beforeSave(Parse.Config, req => { + expect(req.object).toBeDefined(); + expect(req.original).toBeUndefined(); + expect(req.user).toBeUndefined(); + expect(req.headers).toBeDefined(); + expect(req.ip).toBeDefined(); + expect(req.installationId).toBeDefined(); + expect(req.context).toBeDefined(); + const config = req.object; + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + count += 1; + }); + await testConfig(); + const config = await Parse.Config.get({ useMasterKey: true }); expect(config.get('internal')).toBe('i'); expect(config.get('string')).toBe('s'); expect(config.get('number')).toBe(12); - count += 1; - }); - await testConfig(); - const config = await Parse.Config.get({ useMasterKey: true }); - expect(config.get('internal')).toBe('i'); - expect(config.get('string')).toBe('s'); - expect(config.get('number')).toBe(12); - expect(count).toBe(1); - }); + expect(count).toBe(1); + } + ); - it_id('06a9b66c-ffb4-43d1-a025-f7d2192500e7')(it)('beforeSave(Parse.Config) can run hook with existing config', async () => { - let count = 0; - Parse.Cloud.beforeSave(Parse.Config, (req) => { - if (count === 0) { - expect(req.object.get('number')).toBe(12); - expect(req.original).toBeUndefined(); - } - if (count === 1) { - expect(req.object.get('number')).toBe(13); - expect(req.original.get('number')).toBe(12); - } - count += 1; - }); - await testConfig(); - await Parse.Config.save({ number: 13 }); - expect(count).toBe(2); - }); + it_id('06a9b66c-ffb4-43d1-a025-f7d2192500e7')(it)( + 'beforeSave(Parse.Config) can run hook with existing config', + async () => { + let count = 0; + Parse.Cloud.beforeSave(Parse.Config, req => { + if (count === 0) { + expect(req.object.get('number')).toBe(12); + expect(req.original).toBeUndefined(); + } + if (count === 1) { + expect(req.object.get('number')).toBe(13); + expect(req.original.get('number')).toBe(12); + } + count += 1; + }); + await testConfig(); + await Parse.Config.save({ number: 13 }); + expect(count).toBe(2); + } + ); - it_id('ca76de8e-671b-4c2d-9535-bd28a855fa1a')(it)('beforeSave(Parse.Config) should not change config if nothing is returned', async () => { - let count = 0; - Parse.Cloud.beforeSave(Parse.Config, () => { - count += 1; - return; - }); - await testConfig(); - const config = await Parse.Config.get({ useMasterKey: true }); - expect(config.get('internal')).toBe('i'); - expect(config.get('string')).toBe('s'); - expect(config.get('number')).toBe(12); - expect(count).toBe(1); - }); + it_id('ca76de8e-671b-4c2d-9535-bd28a855fa1a')(it)( + 'beforeSave(Parse.Config) should not change config if nothing is returned', + async () => { + let count = 0; + Parse.Cloud.beforeSave(Parse.Config, () => { + count += 1; + return; + }); + await testConfig(); + const config = await Parse.Config.get({ useMasterKey: true }); + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + expect(count).toBe(1); + } + ); it('beforeSave(Parse.Config) throw custom error', async () => { Parse.Cloud.beforeSave(Parse.Config, () => { @@ -4258,60 +4221,69 @@ describe('Cloud Config hooks', () => { } }); - it_id('3e7a75c0-6c2e-4c7e-b042-6eb5f23acf94')(it)('afterSave(Parse.Config) can run hook with new config', async () => { - let count = 0; - Parse.Cloud.afterSave(Parse.Config, (req) => { - expect(req.object).toBeDefined(); - expect(req.original).toBeUndefined(); - expect(req.user).toBeUndefined(); - expect(req.headers).toBeDefined(); - expect(req.ip).toBeDefined(); - expect(req.installationId).toBeDefined(); - expect(req.context).toBeDefined(); - const config = req.object; + it_id('3e7a75c0-6c2e-4c7e-b042-6eb5f23acf94')(it)( + 'afterSave(Parse.Config) can run hook with new config', + async () => { + let count = 0; + Parse.Cloud.afterSave(Parse.Config, req => { + expect(req.object).toBeDefined(); + expect(req.original).toBeUndefined(); + expect(req.user).toBeUndefined(); + expect(req.headers).toBeDefined(); + expect(req.ip).toBeDefined(); + expect(req.installationId).toBeDefined(); + expect(req.context).toBeDefined(); + const config = req.object; + expect(config.get('internal')).toBe('i'); + expect(config.get('string')).toBe('s'); + expect(config.get('number')).toBe(12); + count += 1; + }); + await testConfig(); + const config = await Parse.Config.get({ useMasterKey: true }); expect(config.get('internal')).toBe('i'); expect(config.get('string')).toBe('s'); expect(config.get('number')).toBe(12); - count += 1; - }); - await testConfig(); - const config = await Parse.Config.get({ useMasterKey: true }); - expect(config.get('internal')).toBe('i'); - expect(config.get('string')).toBe('s'); - expect(config.get('number')).toBe(12); - expect(count).toBe(1); - }); - - it_id('5cffb28a-2924-4857-84bb-f5778d80372a')(it)('afterSave(Parse.Config) can run hook with existing config', async () => { - let count = 0; - Parse.Cloud.afterSave(Parse.Config, (req) => { - if (count === 0) { - expect(req.object.get('number')).toBe(12); - expect(req.original).toBeUndefined(); - } - if (count === 1) { - expect(req.object.get('number')).toBe(13); - expect(req.original.get('number')).toBe(12); - } - count += 1; - }); - await testConfig(); - await Parse.Config.save({ number: 13 }); - expect(count).toBe(2); - }); + expect(count).toBe(1); + } + ); - it_id('49883992-ce91-4797-85f9-7cce1f819407')(it)('afterSave(Parse.Config) should throw error', async () => { - Parse.Cloud.afterSave(Parse.Config, () => { - throw new Parse.Error(400, 'It should fail'); - }); - try { + it_id('5cffb28a-2924-4857-84bb-f5778d80372a')(it)( + 'afterSave(Parse.Config) can run hook with existing config', + async () => { + let count = 0; + Parse.Cloud.afterSave(Parse.Config, req => { + if (count === 0) { + expect(req.object.get('number')).toBe(12); + expect(req.original).toBeUndefined(); + } + if (count === 1) { + expect(req.object.get('number')).toBe(13); + expect(req.original.get('number')).toBe(12); + } + count += 1; + }); await testConfig(); - fail('error should have thrown'); - } catch (e) { - expect(e.code).toBe(400); - expect(e.message).toBe('It should fail'); + await Parse.Config.save({ number: 13 }); + expect(count).toBe(2); } - }); + ); + + it_id('49883992-ce91-4797-85f9-7cce1f819407')(it)( + 'afterSave(Parse.Config) should throw error', + async () => { + Parse.Cloud.afterSave(Parse.Config, () => { + throw new Parse.Error(400, 'It should fail'); + }); + try { + await testConfig(); + fail('error should have thrown'); + } catch (e) { + expect(e.code).toBe(400); + expect(e.message).toBe('It should fail'); + } + } + ); }); describe('sendEmail', () => { From 8d6aae6b3c56785e8c37ee4e3c096115830be5c9 Mon Sep 17 00:00:00 2001 From: EmpiDev Date: Fri, 30 May 2025 09:43:01 +0200 Subject: [PATCH 13/16] refactor: improve condition checks and format function parameters for better readability --- src/triggers.js | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/triggers.js b/src/triggers.js index c2cc9e3644..532c8dd732 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -615,7 +615,7 @@ export function maybeRunQueryTrigger( objects = [result]; } else if ( Array.isArray(result) && - (!result.length || result.some(obj => obj instanceof Parse.Object)) + (!result.length || result.every(obj => obj instanceof Parse.Object)) ) { objects = result; } @@ -872,7 +872,9 @@ export function maybeRunTrigger( } return new Promise(function (resolve, reject) { var trigger = getTrigger(parseObject.className, triggerType, config.applicationId); - if (!trigger) { return resolve(); } + if (!trigger) { + return resolve(); + } var request = getRequestObject( triggerType, auth, @@ -1044,12 +1046,26 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth) return fileObject; } -export async function maybeRunGlobalConfigTrigger(triggerType, auth, configObject, originalConfigObject, config, context) { +export async function maybeRunGlobalConfigTrigger( + triggerType, + auth, + configObject, + originalConfigObject, + config, + context +) { const GlobalConfigClassName = getClassName(Parse.Config); const configTrigger = getTrigger(GlobalConfigClassName, triggerType, config.applicationId); if (typeof configTrigger === 'function') { try { - const request = getRequestObject(triggerType, auth, configObject, originalConfigObject, config, context); + const request = getRequestObject( + triggerType, + auth, + configObject, + originalConfigObject, + config, + context + ); await maybeRunValidator(request, `${triggerType}.${GlobalConfigClassName}`, auth); if (request.skipWithMasterKey) { return configObject; From c5dd665acd365d8cc1fdac4f99fa995373e03aea Mon Sep 17 00:00:00 2001 From: EmpiDev Date: Fri, 30 May 2025 09:45:18 +0200 Subject: [PATCH 14/16] test: update beforeFind test to create and save a new object --- spec/CloudCode.spec.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 78a0ed2121..1b31d0999e 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -237,7 +237,9 @@ describe('Cloud Code', () => { expect(req.objects).toBeDefined(); expect(req.objects[0].get('foo')).toBe('bar'); }); - const newObj = await new Parse.Query('beforeFind').get('objId'); + const testObj = new Parse.Object('beforeFind'); + await testObj.save(); + const newObj = await new Parse.Query('beforeFind').get(testObj.id); expect(newObj.className).toBe('TestObject'); expect(newObj.toJSON()).toEqual({ foo: 'bar' }); await newObj.save(); From d8525e90eef75845052bacd46958c7cc5e4df584 Mon Sep 17 00:00:00 2001 From: EmpiDev Date: Mon, 2 Jun 2025 11:58:46 +0200 Subject: [PATCH 15/16] fix : Unexpected token '}' 2057 --- spec/CloudCode.spec.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 1b31d0999e..3180352aeb 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -258,6 +258,11 @@ describe('Cloud Code', () => { expect(newObj).toBeUndefined(); }); + it('beforeSave rejection with custom error code', function (done) { + Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function () { + throw new Parse.Error(999, 'Nope'); + }); + const obj = new Parse.Object('BeforeSaveFailWithErrorCode'); obj.set('foo', 'bar'); obj.save().then( @@ -1547,9 +1552,9 @@ describe('Cloud Code', () => { }); /* - TODO: fix for Postgres - trying to delete a field that doesn't exists doesn't play nice - */ + TODO: fix for Postgres + trying to delete a field that doesn't exists doesn't play nice + */ it_exclude_dbs(['postgres'])( 'should fully delete objects when using `unset` and `set` with beforeSave (regression test for #1840)', done => { @@ -2055,7 +2060,6 @@ describe('Cloud Code', () => { } }); }); - describe('cloud functions', () => { it('Should have request ip', done => { Parse.Cloud.define('myFunction', req => { From c4171184c2c27f05686d4e103f928f1c470c770f Mon Sep 17 00:00:00 2001 From: EmpiDev Date: Wed, 11 Jun 2025 14:12:48 +0200 Subject: [PATCH 16/16] refactor: Improve afterFind trigger handling and object processing --- src/rest.js | 13 ++++----- src/triggers.js | 76 +++++++++++++++++++++++++++++++------------------ 2 files changed, 55 insertions(+), 34 deletions(-) diff --git a/src/rest.js b/src/rest.js index 399e1977c1..3411250e95 100644 --- a/src/rest.js +++ b/src/rest.js @@ -48,25 +48,23 @@ async function runFindTriggers( restOptions = result.restOptions || restOptions; if (result?.objects) { - const objects = result.objects; + const objectsFromBeforeFind = result.objects; - // Déclencher le trigger afterFind si des objets sont retournés - await triggers.maybeRunAfterFindTrigger( + const afterFindProcessedObjects = await triggers.maybeRunAfterFindTrigger( triggers.Types.afterFind, auth, className, - objects, + objectsFromBeforeFind, config, - restWhere, + new Parse.Query(className).withJSON({ where: restWhere, ...restOptions }), context ); return { - results: objects.map(row => row._toFullJSON()), + results: afterFindProcessedObjects, }; } - // Conserver la distinction entre get et find const query = await RestQuery({ method: isGet ? RestQuery.Method.get : RestQuery.Method.find, config, @@ -76,6 +74,7 @@ async function runFindTriggers( restOptions, clientSDK, context, + runBeforeFind: false, }); return query.execute(); diff --git a/src/triggers.js b/src/triggers.js index 532c8dd732..65534f1313 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -182,8 +182,11 @@ export function toJSONwithObjects(object, className) { } toJSON[key] = val._toFullJSON(); } + // Preserve original object's className if no override className is provided if (className) { toJSON.className = className; + } else if (object.className && !toJSON.className) { + toJSON.className = object.className; } return toJSON; } @@ -437,72 +440,91 @@ function logTriggerErrorBeforeHook(triggerType, className, input, auth, error, l export function maybeRunAfterFindTrigger( triggerType, auth, - className, - objects, + classNameQuery, + objectsInput, config, query, context ) { return new Promise((resolve, reject) => { - const trigger = getTrigger(className, triggerType, config.applicationId); + const trigger = getTrigger(classNameQuery, triggerType, config.applicationId); + if (!trigger) { - return resolve(); + if (objectsInput && objectsInput.length > 0 && objectsInput[0] instanceof Parse.Object) { + return resolve(objectsInput.map(obj => toJSONwithObjects(obj))); + } + return resolve(objectsInput || []); } + const request = getRequestObject(triggerType, auth, null, null, config, context); - if (query) { + if (query instanceof Parse.Query) { request.query = query; + } else if (typeof query === 'object' && query !== null) { + const parseQueryInstance = new Parse.Query(classNameQuery); + if (query.where) { + parseQueryInstance.withJSON(query); + } else { + parseQueryInstance.withJSON({ where: query }); + } + request.query = parseQueryInstance; + } else { + request.query = new Parse.Query(classNameQuery); } + const { success, error } = getResponseObject( request, - object => { - resolve(object); + processedObjectsJSON => { + resolve(processedObjectsJSON); }, - error => { - reject(error); + errorData => { + reject(errorData); } ); logTriggerSuccessBeforeHook( triggerType, - className, - 'AfterFind', - JSON.stringify(objects), + classNameQuery, + 'AfterFind Input (Pre-Transform)', + JSON.stringify( + objectsInput.map(o => (o instanceof Parse.Object ? o.id + ':' + o.className : o)) + ), auth, config.logLevels.triggerBeforeSuccess ); - request.objects = objects.map(object => { - //setting the class name to transform into parse object - object.className = className; - if (object instanceof Parse.Object) { - return object; + request.objects = objectsInput.map(currentObject => { + if (currentObject instanceof Parse.Object) { + return currentObject; } - return Parse.Object.fromJSON(object); + // Preserve the original className if it exists, otherwise use the query className + const originalClassName = currentObject.className || classNameQuery; + const tempObjectWithClassName = { ...currentObject, className: originalClassName }; + return Parse.Object.fromJSON(tempObjectWithClassName); }); return Promise.resolve() .then(() => { - return maybeRunValidator(request, `${triggerType}.${className}`, auth); + return maybeRunValidator(request, `${triggerType}.${classNameQuery}`, auth); }) .then(() => { if (request.skipWithMasterKey) { return request.objects; } - const response = trigger(request); - if (response && typeof response.then === 'function') { - return response.then(results => { + const responseFromTrigger = trigger(request); + if (responseFromTrigger && typeof responseFromTrigger.then === 'function') { + return responseFromTrigger.then(results => { return results; }); } - return response; + return responseFromTrigger; }) .then(success, error); - }).then(results => { + }).then(resultsAsJSON => { logTriggerAfterHook( triggerType, - className, - JSON.stringify(results), + classNameQuery, + JSON.stringify(resultsAsJSON), auth, config.logLevels.triggerAfter ); - return results; + return resultsAsJSON; }); }