From 56d6516b5b95fb26b3c9a4c13af996dd74110acd Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Fri, 24 Jun 2022 12:45:49 +0200 Subject: [PATCH 01/19] feat: add Parse Server config to standard request object --- spec/CloudCode.spec.js | 87 ++++++++++++++++++++++++++++++ src/Controllers/HooksController.js | 2 + src/Routers/FunctionsRouter.js | 2 + src/cloud-code/Parse.Cloud.js | 6 +++ src/triggers.js | 3 ++ 5 files changed, 100 insertions(+) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index bf1a2c8c42..fab4882865 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1913,6 +1913,14 @@ describe('cloud functions', () => { Parse.Cloud.run('myFunction', {}).then(() => done()); }); + + it('should have request config', async () => { + Parse.Cloud.define('myConfigFunction', req => { + expect(req.config).toBeDefined(); + return 'success'; + }); + await Parse.Cloud.run('myConfigFunction', {}); + }); }); describe('beforeSave hooks', () => { @@ -1936,6 +1944,16 @@ describe('beforeSave hooks', () => { myObject.save().then(() => done()); }); + it('should have request config', async () => { + Parse.Cloud.beforeSave('MyObject', req => { + expect(req.config).toBeDefined(); + }); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + }); + it('should respect custom object ids (#6733)', async () => { Parse.Cloud.beforeSave('TestObject', req => { expect(req.object.id).toEqual('test_6733'); @@ -1991,6 +2009,16 @@ describe('afterSave hooks', () => { myObject.save().then(() => done()); }); + it('should have request config', async () => { + Parse.Cloud.afterSave('MyObject', req => { + expect(req.config).toBeDefined(); + }); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + }); + it('should unset in afterSave', async () => { Parse.Cloud.afterSave( 'MyObject', @@ -2048,6 +2076,17 @@ describe('beforeDelete hooks', () => { .then(myObj => myObj.destroy()) .then(() => done()); }); + + it('should have request config', async () => { + Parse.Cloud.beforeDelete('MyObject', req => { + expect(req.config).toBeDefined(); + }); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + await myObject.destroy(); + }); }); describe('afterDelete hooks', () => { @@ -2076,6 +2115,17 @@ describe('afterDelete hooks', () => { .then(myObj => myObj.destroy()) .then(() => done()); }); + + it('should have request config', async () => { + Parse.Cloud.afterDelete('MyObject', req => { + expect(req.ip).toBeDefined(); + }); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + await myObject.destroy(); + }); }); describe('beforeFind hooks', () => { @@ -2332,6 +2382,19 @@ describe('beforeFind hooks', () => { }) .then(() => done()); }); + + it('should have request config', async () => { + Parse.Cloud.beforeFind('MyObject', req => { + expect(req.config).toBeDefined(); + }); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + const query = new Parse.Query('MyObject'); + query.equalTo('objectId', myObject.id); + await Promise.all([query.get(myObject.id), query.first(), query.find()]); + }); }); describe('afterFind hooks', () => { @@ -2663,6 +2726,19 @@ describe('afterFind hooks', () => { .catch(done.fail); }); + it('should have request config', async () => { + Parse.Cloud.afterFind('MyObject', req => { + expect(req.ip).toBeDefined(); + }); + + const MyObject = Parse.Object.extend('MyObject'); + const myObject = new MyObject(); + await myObject.save(); + const query = new Parse.Query('MyObject'); + query.equalTo('objectId', myObject.id); + await Promise.all([query.get(myObject.id), query.first(), query.find()]); + }); + it('should validate triggers correctly', () => { expect(() => { Parse.Cloud.beforeSave('_Session', () => {}); @@ -3120,6 +3196,7 @@ describe('beforeLogin hook', () => { expect(req.ip).toBeDefined(); expect(req.installationId).toBeDefined(); expect(req.context).toBeUndefined(); + expect(req.config).toBeDefined(); }); await Parse.User.signUp('tupac', 'shakur'); @@ -3237,6 +3314,7 @@ describe('afterLogin hook', () => { expect(req.ip).toBeDefined(); expect(req.installationId).toBeDefined(); expect(req.context).toBeUndefined(); + expect(req.config).toBeDefined(); }); await Parse.User.signUp('testuser', 'p@ssword'); @@ -3431,6 +3509,15 @@ describe('saveFile hooks', () => { } }); + it('beforeSaveFile should have config', async () => { + await reconfigureServer({ filesAdapter: mockAdapter }); + Parse.Cloud.beforeSave(Parse.File, req => { + expect(req.config).toBeDefined(); + }); + const file = new Parse.File('popeye.txt', [1, 2, 3], 'text/plain'); + await file.save({ useMasterKey: true }); + }); + it('beforeSaveFile should change values of uploaded file by editing fileObject directly', async () => { await reconfigureServer({ filesAdapter: mockAdapter }); const createFileSpy = spyOn(mockAdapter, 'createFile').and.callThrough(); diff --git a/src/Controllers/HooksController.js b/src/Controllers/HooksController.js index 9cc5f427e8..dc30aae417 100644 --- a/src/Controllers/HooksController.js +++ b/src/Controllers/HooksController.js @@ -188,6 +188,8 @@ function wrapToHTTPRequest(hook, key) { return req => { const jsonBody = {}; for (var i in req) { + // Parse Server config is not serializable + if (i === 'config') continue; jsonBody[i] = req[i]; } if (req.object) { diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index d239908103..1b42805647 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -67,6 +67,7 @@ export class FunctionsRouter extends PromiseRouter { headers: req.config.headers, ip: req.config.ip, jobName, + config: req.config, message: jobHandler.setMessage.bind(jobHandler), }; @@ -123,6 +124,7 @@ export class FunctionsRouter extends PromiseRouter { params = parseParams(params); const request = { params: params, + config: req.config, master: req.auth && req.auth.isMaster, user: req.auth && req.auth.user, installationId: req.info.installationId, diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 1ee02fdb60..1eaa536b44 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -735,6 +735,7 @@ module.exports = ParseCloud; * @property {String} triggerName The name of the trigger (`beforeSave`, `afterSave`, ...) * @property {Object} log The current logger inside Parse Server. * @property {Parse.Object} original If set, the object, as currently stored. + * @property {Object} config The Parse Server config. */ /** @@ -749,6 +750,7 @@ module.exports = ParseCloud; * @property {Object} headers The original HTTP headers for the request. * @property {String} triggerName The name of the trigger (`beforeSaveFile`, `afterSaveFile`) * @property {Object} log The current logger inside Parse Server. + * @property {Object} config The Parse Server config. */ /** @@ -786,6 +788,7 @@ module.exports = ParseCloud; * @property {String} triggerName The name of the trigger (`beforeSave`, `afterSave`, ...) * @property {Object} log The current logger inside Parse Server. * @property {Boolean} isGet wether the query a `get` or a `find` + * @property {Object} config The Parse Server config. */ /** @@ -799,6 +802,7 @@ module.exports = ParseCloud; * @property {Object} headers The original HTTP headers for the request. * @property {String} triggerName The name of the trigger (`beforeSave`, `afterSave`, ...) * @property {Object} log The current logger inside Parse Server. + * @property {Object} config The Parse Server config. */ /** @@ -807,12 +811,14 @@ module.exports = ParseCloud; * @property {Boolean} master If true, means the master key was used. * @property {Parse.User} user If set, the user that made the request. * @property {Object} params The params passed to the cloud function. + * @property {Object} config The Parse Server config. */ /** * @interface Parse.Cloud.JobRequest * @property {Object} params The params passed to the background job. * @property {function} message If message is called with a string argument, will update the current message to be stored in the job status. + * @property {Object} config The Parse Server config. */ /** diff --git a/src/triggers.js b/src/triggers.js index 4ba21b32ea..c8709064b3 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -260,6 +260,7 @@ export function getRequestObject( log: config.loggerController, headers: config.headers, ip: config.ip, + config, }; if (originalParseObject) { @@ -304,6 +305,7 @@ export function getRequestQueryObject(triggerType, auth, query, count, config, c headers: config.headers, ip: config.ip, context: context || {}, + config, }; if (!auth) { @@ -932,6 +934,7 @@ export function getRequestFileObject(triggerType, auth, fileObject, config) { log: config.loggerController, headers: config.headers, ip: config.ip, + config, }; if (!auth) { From d85f37f53c46ed7d2fc4aade91165e42727ecfde Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Sun, 12 Mar 2023 18:09:30 +0100 Subject: [PATCH 02/19] wip --- package-lock.json | 24 +++++++--------- spec/requestContextMiddleware.spec.js | 40 +++++++++++++++++++++++++++ src/GraphQL/ParseGraphQLServer.js | 1 + src/Options/index.js | 2 ++ src/ParseServer.js | 14 +++++++++- 5 files changed, 66 insertions(+), 15 deletions(-) create mode 100644 spec/requestContextMiddleware.spec.js diff --git a/package-lock.json b/package-lock.json index 18f5a16b4e..3a4970d128 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11489,12 +11489,14 @@ } }, "node_modules/mock-files-adapter": { - "resolved": "spec/dependencies/mock-files-adapter", - "link": true + "version": "1.0.0", + "resolved": "file:spec/dependencies/mock-files-adapter", + "dev": true }, "node_modules/mock-mail-adapter": { - "resolved": "spec/dependencies/mock-mail-adapter", - "link": true + "version": "1.0.0", + "resolved": "file:spec/dependencies/mock-mail-adapter", + "dev": true }, "node_modules/modify-values": { "version": "1.0.1", @@ -20454,14 +20456,6 @@ "dependencies": { "zen-observable": "0.8.15" } - }, - "spec/dependencies/mock-files-adapter": { - "version": "1.0.0", - "dev": true - }, - "spec/dependencies/mock-mail-adapter": { - "version": "1.0.0", - "dev": true } }, "dependencies": { @@ -29166,10 +29160,12 @@ } }, "mock-files-adapter": { - "version": "file:spec/dependencies/mock-files-adapter" + "version": "1.0.0", + "dev": true }, "mock-mail-adapter": { - "version": "file:spec/dependencies/mock-mail-adapter" + "version": "1.0.0", + "dev": true }, "modify-values": { "version": "1.0.1", diff --git a/spec/requestContextMiddleware.spec.js b/spec/requestContextMiddleware.spec.js new file mode 100644 index 0000000000..54132ac779 --- /dev/null +++ b/spec/requestContextMiddleware.spec.js @@ -0,0 +1,40 @@ +const { ApolloClient, gql, InMemoryCache } = require('@apollo/client'); +describe('requestContextMiddleware', () => { + const requestContextMiddleware = (req, res, next) => { + req.config.aCustomController = 'aCustomController'; + next(); + }; + + fit('should support dependency injection on rest api', async () => { + Parse.Cloud.beforeSave('_User', request => { + expect(request.config.aCustomController).toEqual('aCustomController'); + }); + await reconfigureServer({ requestContextMiddleware }); + const user = new Parse.User(); + user.setUsername('test'); + user.setPassword('test'); + await user.signUp(); + }); + it('should support dependency injection on graphql api', async () => { + Parse.Cloud.beforeSave('_User', request => { + expect(request.config.aCustomController).toEqual('aCustomController'); + }); + await reconfigureServer({ requestContextMiddleware, graphQLPath: '/graphql' }); + const client = new ApolloClient({ + uri: 'http://localhost:13377/graphql', + cache: new InMemoryCache(), + }); + + await client.mutate({ + mutation: gql` + mutation { + createUser(username: "test", password: "test") { + user { + objectId + } + } + } + `, + }); + }); +}); diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index 44ea34f47b..024c850d60 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -83,6 +83,7 @@ class ParseGraphQLServer { app.use(this.config.graphQLPath, corsMiddleware()); app.use(this.config.graphQLPath, handleParseHeaders); app.use(this.config.graphQLPath, handleParseSession); + this.applyRequestContextMiddleware(app, this.parseServer.config); app.use(this.config.graphQLPath, handleParseErrors); app.use(this.config.graphQLPath, async (req, res) => { const server = await this._getServer(); diff --git a/src/Options/index.js b/src/Options/index.js index a4d83f94fc..8d0ed91fc5 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -300,6 +300,8 @@ export interface ParseServerOptions { /* Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.

ℹ️ Mind the following limitations:
- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses
- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable
- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case. :DEFAULT: [] */ rateLimit: ?(RateLimitOptions[]); + /* Options to customize the request context using inversion of control/dependency injection.*/ + requestContextMiddleware: ?(req: any, res: any, next: any) => void; } export interface RateLimitOptions { diff --git a/src/ParseServer.js b/src/ParseServer.js index 04379ecfd3..2af5e15edc 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -175,6 +175,18 @@ class ParseServer { }); } + /** + * @static + * Allow developers to customize each request with inversion of control/dependency injection + */ + static applyRequestContextMiddleware(api, options) { + if (options.requestContextMiddleware) { + if (typeof options.requestContextMiddleware !== 'function') { + throw new Error('requestContextMiddleware must be a function'); + } + api.use(options.requestContextMiddleware); + } + } /** * @static * Create an express app for the parse server @@ -220,7 +232,7 @@ class ParseServer { middlewares.addRateLimit(route, options); } api.use(middlewares.handleParseSession); - + this.applyRequestContextMiddleware(api, options); const appRouter = ParseServer.promiseRouter({ appId }); api.use(appRouter.expressRouter()); From 65f83c11374b161a09a1248066c56ca9d7454efc Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Sun, 12 Mar 2023 19:22:43 +0100 Subject: [PATCH 03/19] feat: requestContextMiddleware and config in hooks --- spec/requestContextMiddleware.spec.js | 24 ++++++++++++++++++++---- src/GraphQL/ParseGraphQLServer.js | 13 +++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/spec/requestContextMiddleware.spec.js b/spec/requestContextMiddleware.spec.js index 54132ac779..b91df1f41b 100644 --- a/spec/requestContextMiddleware.spec.js +++ b/spec/requestContextMiddleware.spec.js @@ -1,34 +1,49 @@ const { ApolloClient, gql, InMemoryCache } = require('@apollo/client'); +const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args)); describe('requestContextMiddleware', () => { const requestContextMiddleware = (req, res, next) => { req.config.aCustomController = 'aCustomController'; next(); }; - fit('should support dependency injection on rest api', async () => { + it('should support dependency injection on rest api', async () => { + let called; Parse.Cloud.beforeSave('_User', request => { expect(request.config.aCustomController).toEqual('aCustomController'); + called = true; }); await reconfigureServer({ requestContextMiddleware }); const user = new Parse.User(); user.setUsername('test'); user.setPassword('test'); await user.signUp(); + expect(called).toBeTruthy(); }); it('should support dependency injection on graphql api', async () => { + let called = false; Parse.Cloud.beforeSave('_User', request => { expect(request.config.aCustomController).toEqual('aCustomController'); + called = true; + }); + await reconfigureServer({ + requestContextMiddleware, + mountGraphQL: true, + graphQLPath: '/graphql', }); - await reconfigureServer({ requestContextMiddleware, graphQLPath: '/graphql' }); const client = new ApolloClient({ - uri: 'http://localhost:13377/graphql', + uri: 'http://localhost:8378/graphql', cache: new InMemoryCache(), + fetch, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, }); await client.mutate({ mutation: gql` mutation { - createUser(username: "test", password: "test") { + createUser(input: { fields: { username: "test", password: "test" } }) { user { objectId } @@ -36,5 +51,6 @@ describe('requestContextMiddleware', () => { } `, }); + expect(called).toBeTruthy(); }); }); diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index 024c850d60..017716fa8c 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -75,6 +75,19 @@ class ParseGraphQLServer { ); } + /** + * @static + * Allow developers to customize each request with inversion of control/dependency injection + */ + applyRequestContextMiddleware(api, options) { + if (options.requestContextMiddleware) { + if (typeof options.requestContextMiddleware !== 'function') { + throw new Error('requestContextMiddleware must be a function'); + } + api.use(options.requestContextMiddleware); + } + } + applyGraphQL(app) { if (!app || !app.use) { requiredParameter('You must provide an Express.js app instance!'); From 8340f5b71a69b9a026f22a9154bb44df5e6b61cb Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Sun, 12 Mar 2023 19:26:27 +0100 Subject: [PATCH 04/19] fix: restore lock --- package-lock.json | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3a4970d128..18f5a16b4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11489,14 +11489,12 @@ } }, "node_modules/mock-files-adapter": { - "version": "1.0.0", - "resolved": "file:spec/dependencies/mock-files-adapter", - "dev": true + "resolved": "spec/dependencies/mock-files-adapter", + "link": true }, "node_modules/mock-mail-adapter": { - "version": "1.0.0", - "resolved": "file:spec/dependencies/mock-mail-adapter", - "dev": true + "resolved": "spec/dependencies/mock-mail-adapter", + "link": true }, "node_modules/modify-values": { "version": "1.0.1", @@ -20456,6 +20454,14 @@ "dependencies": { "zen-observable": "0.8.15" } + }, + "spec/dependencies/mock-files-adapter": { + "version": "1.0.0", + "dev": true + }, + "spec/dependencies/mock-mail-adapter": { + "version": "1.0.0", + "dev": true } }, "dependencies": { @@ -29160,12 +29166,10 @@ } }, "mock-files-adapter": { - "version": "1.0.0", - "dev": true + "version": "file:spec/dependencies/mock-files-adapter" }, "mock-mail-adapter": { - "version": "1.0.0", - "dev": true + "version": "file:spec/dependencies/mock-mail-adapter" }, "modify-values": { "version": "1.0.1", From 8c8e1eb39a20a61d322ac6e6738f05e854935043 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Sun, 12 Mar 2023 19:28:40 +0100 Subject: [PATCH 05/19] fix: defs --- src/Options/Definitions.js | 5 +++++ src/Options/docs.js | 1 + 2 files changed, 6 insertions(+) diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index b2f0542256..0af65647aa 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -435,6 +435,11 @@ module.exports.ParseServerOptions = { env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY', help: 'Read-only key, which has the same capabilities as MasterKey without writes', }, + requestContextMiddleware: { + env: 'PARSE_SERVER_REQUEST_CONTEXT_MIDDLEWARE', + help: + 'Options to customize the request context using inversion of control/dependency injection.', + }, requestKeywordDenylist: { env: 'PARSE_SERVER_REQUEST_KEYWORD_DENYLIST', help: diff --git a/src/Options/docs.js b/src/Options/docs.js index 1ab8c03d58..f22701b544 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -81,6 +81,7 @@ * @property {Any} push Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications * @property {RateLimitOptions[]} rateLimit Options to limit repeated requests to Parse Server APIs. This can be used to protect sensitive endpoints such as `/requestPasswordReset` from brute-force attacks or Parse Server as a whole from denial-of-service (DoS) attacks.

ℹ️ Mind the following limitations:
- rate limits applied per IP address; this limits protection against distributed denial-of-service (DDoS) attacks where many requests are coming from various IP addresses
- if multiple Parse Server instances are behind a load balancer or ran in a cluster, each instance will calculate it's own request rates, independent from other instances; this limits the applicability of this feature when using a load balancer and another rate limiting solution that takes requests across all instances into account may be more suitable
- this feature provides basic protection against denial-of-service attacks, but a more sophisticated solution works earlier in the request flow and prevents a malicious requests to even reach a server instance; it's therefore recommended to implement a solution according to architecture and user case. * @property {String} readOnlyMasterKey Read-only key, which has the same capabilities as MasterKey without writes + * @property {Function} requestContextMiddleware Options to customize the request context using inversion of control/dependency injection. * @property {RequestKeywordDenylist[]} requestKeywordDenylist An array of keys and values that are prohibited in database read and write requests to prevent potential security vulnerabilities. It is possible to specify only a key (`{"key":"..."}`), only a value (`{"value":"..."}`) or a key-value pair (`{"key":"...","value":"..."}`). The specification can use the following types: `boolean`, `numeric` or `string`, where `string` will be interpreted as a regex notation. Request data is deep-scanned for matching definitions to detect also any nested occurrences. Defaults are patterns that are likely to be used in malicious requests. Setting this option will override the default patterns. * @property {String} restAPIKey Key for REST calls * @property {Boolean} revokeSessionOnPasswordReset When a user changes their password, either through the reset password email or while logged in, all sessions are revoked if this is true. Set to false if you don't want to revoke sessions. From 748f68f5dc204d27f05afea66244e08d17af63ee Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Sun, 12 Mar 2023 19:47:34 +0100 Subject: [PATCH 06/19] fix: import --- spec/requestContextMiddleware.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requestContextMiddleware.spec.js b/spec/requestContextMiddleware.spec.js index b91df1f41b..e086a8b415 100644 --- a/spec/requestContextMiddleware.spec.js +++ b/spec/requestContextMiddleware.spec.js @@ -1,4 +1,4 @@ -const { ApolloClient, gql, InMemoryCache } = require('@apollo/client'); +const { ApolloClient, gql, InMemoryCache } = require('@apollo/client/core'); const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args)); describe('requestContextMiddleware', () => { const requestContextMiddleware = (req, res, next) => { From fbe389b65fd2e051fc4432fa7ec799ad0c95007c Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Wed, 8 Oct 2025 21:45:23 +0200 Subject: [PATCH 07/19] fix: lint --- spec/CloudCode.spec.js | 2 +- src/Controllers/HooksController.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 3565a893a2..f3886c170e 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2545,7 +2545,7 @@ describe('beforeFind hooks', () => { const query = new Parse.Query('MyObject'); query.equalTo('objectId', myObject.id); await Promise.all([query.get(myObject.id), query.first(), query.find()]); -}) + }) it('should run beforeFind on pointers and array of pointers from an object', async () => { const obj1 = new Parse.Object('TestObject'); const obj2 = new Parse.Object('TestObject2'); diff --git a/src/Controllers/HooksController.js b/src/Controllers/HooksController.js index 77dfcd3c7f..fef01946f6 100644 --- a/src/Controllers/HooksController.js +++ b/src/Controllers/HooksController.js @@ -189,7 +189,7 @@ function wrapToHTTPRequest(hook, key) { const jsonBody = {}; for (var i in req) { // Parse Server config is not serializable - if (i === 'config') continue; + if (i === 'config') { continue; } jsonBody[i] = req[i]; } if (req.object) { From 344116a255b674cef7c2442cd9d8575f3785ab67 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Wed, 8 Oct 2025 22:55:54 +0200 Subject: [PATCH 08/19] test: fix --- spec/requestContextMiddleware.spec.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/requestContextMiddleware.spec.js b/spec/requestContextMiddleware.spec.js index e086a8b415..b1ac5e7694 100644 --- a/spec/requestContextMiddleware.spec.js +++ b/spec/requestContextMiddleware.spec.js @@ -1,5 +1,4 @@ const { ApolloClient, gql, InMemoryCache } = require('@apollo/client/core'); -const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args)); describe('requestContextMiddleware', () => { const requestContextMiddleware = (req, res, next) => { req.config.aCustomController = 'aCustomController'; @@ -33,7 +32,6 @@ describe('requestContextMiddleware', () => { const client = new ApolloClient({ uri: 'http://localhost:8378/graphql', cache: new InMemoryCache(), - fetch, headers: { 'X-Parse-Application-Id': 'test', 'X-Parse-Master-Key': 'test', From 9bbc7df8139d7d041db5d2731cffc2ef0777f9e7 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Wed, 8 Oct 2025 23:00:14 +0200 Subject: [PATCH 09/19] fix: ai suggestion --- spec/CloudCode.spec.js | 60 +++++++++++++++++++++--------------------- src/Options/index.js | 2 +- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index f3886c170e..832fd647de 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -11,8 +11,8 @@ const mockAdapter = { name: filename, location: `http://www.somewhere.com/${filename}`, }), - deleteFile: () => {}, - getFileData: () => {}, + deleteFile: () => { }, + getFileData: () => { }, getFileLocation: (config, filename) => `http://www.somewhere.com/${filename}`, validateFilename: () => { return null; @@ -49,7 +49,7 @@ describe('Cloud Code', () => { }); it('cloud code must be valid type', async () => { - spyOn(console, 'error').and.callFake(() => {}); + spyOn(console, 'error').and.callFake(() => { }); await expectAsync(reconfigureServer({ cloud: true })).toBeRejectedWith( "argument 'cloud' must either be a string or a function" ); @@ -114,7 +114,7 @@ describe('Cloud Code', () => { it('show warning on duplicate cloud functions', done => { const logger = require('../lib/logger').logger; - spyOn(logger, 'warn').and.callFake(() => {}); + spyOn(logger, 'warn').and.callFake(() => { }); Parse.Cloud.define('hello', () => { return 'Hello world!'; }); @@ -1332,7 +1332,7 @@ describe('Cloud Code', () => { }); it('trivial beforeSave should not affect fetched pointers (regression test for #1238)', done => { - Parse.Cloud.beforeSave('BeforeSaveUnchanged', () => {}); + Parse.Cloud.beforeSave('BeforeSaveUnchanged', () => { }); const TestObject = Parse.Object.extend('TestObject'); const NoBeforeSaveObject = Parse.Object.extend('NoBeforeSave'); @@ -1405,7 +1405,7 @@ describe('Cloud Code', () => { }); it('beforeSave should not affect fetched pointers', done => { - Parse.Cloud.beforeSave('BeforeSaveUnchanged', () => {}); + Parse.Cloud.beforeSave('BeforeSaveUnchanged', () => { }); Parse.Cloud.beforeSave('BeforeSaveChanged', function (req) { req.object.set('foo', 'baz'); @@ -1719,7 +1719,7 @@ describe('Cloud Code', () => { }); it('pointer should not be cleared by triggers', async () => { - Parse.Cloud.afterSave('MyObject', () => {}); + Parse.Cloud.afterSave('MyObject', () => { }); const foo = await new Parse.Object('Test', { foo: 'bar' }).save(); const obj = await new Parse.Object('MyObject', { foo }).save(); const foo2 = obj.get('foo'); @@ -1727,7 +1727,7 @@ describe('Cloud Code', () => { }); it('can set a pointer in triggers', async () => { - Parse.Cloud.beforeSave('MyObject', () => {}); + Parse.Cloud.beforeSave('MyObject', () => { }); Parse.Cloud.afterSave( 'MyObject', async ({ object }) => { @@ -1828,7 +1828,7 @@ describe('Cloud Code', () => { it('should not run without master key', done => { expect(() => { - Parse.Cloud.job('myJob', () => {}); + Parse.Cloud.job('myJob', () => { }); }).not.toThrow(); request({ @@ -2219,7 +2219,7 @@ describe('afterDelete hooks', () => { it('should have request config', async () => { Parse.Cloud.afterDelete('MyObject', req => { - expect(req.ip).toBeDefined(); + expect(req.config).toBeDefined(); }); const MyObject = Parse.Object.extend('MyObject'); @@ -2932,7 +2932,7 @@ describe('afterFind hooks', () => { it('should have request config', async () => { Parse.Cloud.afterFind('MyObject', req => { - expect(req.ip).toBeDefined(); + expect(req.config).toBeDefined(); }); const MyObject = Parse.Object.extend('MyObject'); @@ -2945,52 +2945,52 @@ describe('afterFind hooks', () => { it('should validate triggers correctly', () => { expect(() => { - Parse.Cloud.beforeSave('_Session', () => {}); + Parse.Cloud.beforeSave('_Session', () => { }); }).toThrow('Only the afterLogout trigger is allowed for the _Session class.'); expect(() => { - Parse.Cloud.afterSave('_Session', () => {}); + Parse.Cloud.afterSave('_Session', () => { }); }).toThrow('Only the afterLogout trigger is allowed for the _Session class.'); expect(() => { - Parse.Cloud.beforeSave('_PushStatus', () => {}); + Parse.Cloud.beforeSave('_PushStatus', () => { }); }).toThrow('Only afterSave is allowed on _PushStatus'); expect(() => { - Parse.Cloud.afterSave('_PushStatus', () => {}); + Parse.Cloud.afterSave('_PushStatus', () => { }); }).not.toThrow(); expect(() => { - Parse.Cloud.beforeLogin(() => {}); + Parse.Cloud.beforeLogin(() => { }); }).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); expect(() => { - Parse.Cloud.beforeLogin('_User', () => {}); + Parse.Cloud.beforeLogin('_User', () => { }); }).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); expect(() => { - Parse.Cloud.beforeLogin(Parse.User, () => {}); + Parse.Cloud.beforeLogin(Parse.User, () => { }); }).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); expect(() => { - Parse.Cloud.beforeLogin('SomeClass', () => {}); + Parse.Cloud.beforeLogin('SomeClass', () => { }); }).toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); expect(() => { - Parse.Cloud.afterLogin(() => {}); + Parse.Cloud.afterLogin(() => { }); }).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); expect(() => { - Parse.Cloud.afterLogin('_User', () => {}); + Parse.Cloud.afterLogin('_User', () => { }); }).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); expect(() => { - Parse.Cloud.afterLogin(Parse.User, () => {}); + Parse.Cloud.afterLogin(Parse.User, () => { }); }).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); expect(() => { - Parse.Cloud.afterLogin('SomeClass', () => {}); + Parse.Cloud.afterLogin('SomeClass', () => { }); }).toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers'); expect(() => { - Parse.Cloud.afterLogout(() => {}); + Parse.Cloud.afterLogout(() => { }); }).not.toThrow(); expect(() => { - Parse.Cloud.afterLogout('_Session', () => {}); + Parse.Cloud.afterLogout('_Session', () => { }); }).not.toThrow(); expect(() => { - Parse.Cloud.afterLogout('_User', () => {}); + Parse.Cloud.afterLogout('_User', () => { }); }).toThrow('Only the _Session class is allowed for the afterLogout trigger.'); expect(() => { - Parse.Cloud.afterLogout('SomeClass', () => {}); + Parse.Cloud.afterLogout('SomeClass', () => { }); }).toThrow('Only the _Session class is allowed for the afterLogout trigger.'); }); @@ -4062,7 +4062,7 @@ describe('Parse.File hooks', () => { beforeFind() { throw 'unauthorized'; }, - afterFind() {}, + afterFind() { }, }; for (const hook in hooks) { spyOn(hooks, hook).and.callThrough(); @@ -4090,7 +4090,7 @@ describe('Parse.File hooks', () => { await file.save({ useMasterKey: true }); const user = await Parse.User.signUp('username', 'password'); const hooks = { - beforeFind() {}, + beforeFind() { }, afterFind() { throw 'unauthorized'; }, @@ -4309,7 +4309,7 @@ describe('sendEmail', () => { it('cannot send email without adapter', async () => { const logger = require('../lib/logger').logger; - spyOn(logger, 'error').and.callFake(() => {}); + spyOn(logger, 'error').and.callFake(() => { }); await Parse.Cloud.sendEmail({}); expect(logger.error).toHaveBeenCalledWith( 'Failed to send email because no mail adapter is configured for Parse Server.' diff --git a/src/Options/index.js b/src/Options/index.js index 2f014369c4..29ac1628f7 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -343,7 +343,7 @@ export interface ParseServerOptions { :DEFAULT: [] */ rateLimit: ?(RateLimitOptions[]); /* Options to customize the request context using inversion of control/dependency injection.*/ - requestContextMiddleware: ?(req: any, res: any, next: any) => void; + requestContextMiddleware: ?((req: any, res: any, next: any) => void); } export interface RateLimitOptions { From 98b1287617f5ef87ef52db29ad02f83e8c4d1d86 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Thu, 9 Oct 2025 09:23:30 +0200 Subject: [PATCH 10/19] test: use pure fetch --- spec/requestContextMiddleware.spec.js | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/spec/requestContextMiddleware.spec.js b/spec/requestContextMiddleware.spec.js index b1ac5e7694..9bcd8ae6c3 100644 --- a/spec/requestContextMiddleware.spec.js +++ b/spec/requestContextMiddleware.spec.js @@ -1,4 +1,3 @@ -const { ApolloClient, gql, InMemoryCache } = require('@apollo/client/core'); describe('requestContextMiddleware', () => { const requestContextMiddleware = (req, res, next) => { req.config.aCustomController = 'aCustomController'; @@ -29,25 +28,25 @@ describe('requestContextMiddleware', () => { mountGraphQL: true, graphQLPath: '/graphql', }); - const client = new ApolloClient({ - uri: 'http://localhost:8378/graphql', - cache: new InMemoryCache(), + + await fetch('http://localhost:8378/graphql', { + method: 'POST', headers: { + 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', 'X-Parse-Master-Key': 'test', }, - }); - - await client.mutate({ - mutation: gql` - mutation { - createUser(input: { fields: { username: "test", password: "test" } }) { - user { - objectId + body: JSON.stringify({ + query: ` + mutation { + createUser(input: { fields: { username: "test", password: "test" } }) { + user { + objectId + } } } - } - `, + `, + }), }); expect(called).toBeTruthy(); }); From 099d1092dfb5a19fabc50aacdb55fed6e4ef12f1 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Thu, 9 Oct 2025 09:45:32 +0200 Subject: [PATCH 11/19] test: try to fix --- spec/requestContextMiddleware.spec.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/spec/requestContextMiddleware.spec.js b/spec/requestContextMiddleware.spec.js index 9bcd8ae6c3..51a5050754 100644 --- a/spec/requestContextMiddleware.spec.js +++ b/spec/requestContextMiddleware.spec.js @@ -1,10 +1,11 @@ describe('requestContextMiddleware', () => { - const requestContextMiddleware = (req, res, next) => { - req.config.aCustomController = 'aCustomController'; - next(); - }; it('should support dependency injection on rest api', async () => { + const requestContextMiddleware = (req, res, next) => { + req.config.aCustomController = 'aCustomController'; + next(); + }; + let called; Parse.Cloud.beforeSave('_User', request => { expect(request.config.aCustomController).toEqual('aCustomController'); @@ -18,6 +19,10 @@ describe('requestContextMiddleware', () => { expect(called).toBeTruthy(); }); it('should support dependency injection on graphql api', async () => { + const requestContextMiddleware = (req, res, next) => { + req.config.aCustomController = 'aCustomController'; + next(); + }; let called = false; Parse.Cloud.beforeSave('_User', request => { expect(request.config.aCustomController).toEqual('aCustomController'); From d08b15701bfdf1b9f2969a08a969682621861173 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Thu, 9 Oct 2025 10:04:40 +0200 Subject: [PATCH 12/19] test: try to fix --- ...> requestContextMiddlewareGraphQL.spec.js} | 32 ++++--------------- spec/requestContextMiddlewareRest.spec.js | 21 ++++++++++++ 2 files changed, 28 insertions(+), 25 deletions(-) rename spec/{requestContextMiddleware.spec.js => requestContextMiddlewareGraphQL.spec.js} (51%) create mode 100644 spec/requestContextMiddlewareRest.spec.js diff --git a/spec/requestContextMiddleware.spec.js b/spec/requestContextMiddlewareGraphQL.spec.js similarity index 51% rename from spec/requestContextMiddleware.spec.js rename to spec/requestContextMiddlewareGraphQL.spec.js index 51a5050754..bd33c4a897 100644 --- a/spec/requestContextMiddleware.spec.js +++ b/spec/requestContextMiddlewareGraphQL.spec.js @@ -1,23 +1,5 @@ -describe('requestContextMiddleware', () => { +describe('requestContextMiddlewareGraphQL', () => { - it('should support dependency injection on rest api', async () => { - const requestContextMiddleware = (req, res, next) => { - req.config.aCustomController = 'aCustomController'; - next(); - }; - - let called; - Parse.Cloud.beforeSave('_User', request => { - expect(request.config.aCustomController).toEqual('aCustomController'); - called = true; - }); - await reconfigureServer({ requestContextMiddleware }); - const user = new Parse.User(); - user.setUsername('test'); - user.setPassword('test'); - await user.signUp(); - expect(called).toBeTruthy(); - }); it('should support dependency injection on graphql api', async () => { const requestContextMiddleware = (req, res, next) => { req.config.aCustomController = 'aCustomController'; @@ -43,14 +25,14 @@ describe('requestContextMiddleware', () => { }, body: JSON.stringify({ query: ` - mutation { - createUser(input: { fields: { username: "test", password: "test" } }) { - user { - objectId + mutation { + createUser(input: { fields: { username: "test", password: "test" } }) { + user { + objectId + } } } - } - `, + `, }), }); expect(called).toBeTruthy(); diff --git a/spec/requestContextMiddlewareRest.spec.js b/spec/requestContextMiddlewareRest.spec.js new file mode 100644 index 0000000000..f0cfdc1543 --- /dev/null +++ b/spec/requestContextMiddlewareRest.spec.js @@ -0,0 +1,21 @@ +describe('requestContextMiddlewareRest', () => { + + it('should support dependency injection on rest api', async () => { + const requestContextMiddleware = (req, res, next) => { + req.config.aCustomController = 'aCustomController'; + next(); + }; + + let called; + Parse.Cloud.beforeSave('_User', request => { + expect(request.config.aCustomController).toEqual('aCustomController'); + called = true; + }); + await reconfigureServer({ requestContextMiddleware }); + const user = new Parse.User(); + user.setUsername('test'); + user.setPassword('test'); + await user.signUp(); + expect(called).toBeTruthy(); + }); +}); From 29a0d90c8a208d624c47ec42a66cff99e8717192 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Thu, 9 Oct 2025 10:17:32 +0200 Subject: [PATCH 13/19] test: try again --- ...ec.js => requestContextMiddleware.spec.js} | 28 ++++++++++++++++--- spec/requestContextMiddlewareRest.spec.js | 21 -------------- 2 files changed, 24 insertions(+), 25 deletions(-) rename spec/{requestContextMiddlewareGraphQL.spec.js => requestContextMiddleware.spec.js} (65%) delete mode 100644 spec/requestContextMiddlewareRest.spec.js diff --git a/spec/requestContextMiddlewareGraphQL.spec.js b/spec/requestContextMiddleware.spec.js similarity index 65% rename from spec/requestContextMiddlewareGraphQL.spec.js rename to spec/requestContextMiddleware.spec.js index bd33c4a897..64e16aed25 100644 --- a/spec/requestContextMiddlewareGraphQL.spec.js +++ b/spec/requestContextMiddleware.spec.js @@ -6,16 +6,17 @@ describe('requestContextMiddlewareGraphQL', () => { next(); }; let called = false; - Parse.Cloud.beforeSave('_User', request => { - expect(request.config.aCustomController).toEqual('aCustomController'); - called = true; - }); await reconfigureServer({ requestContextMiddleware, mountGraphQL: true, graphQLPath: '/graphql', }); + Parse.Cloud.beforeSave('_User', request => { + expect(request.config.aCustomController).toEqual('aCustomController'); + called = true; + }); + await fetch('http://localhost:8378/graphql', { method: 'POST', headers: { @@ -37,4 +38,23 @@ describe('requestContextMiddlewareGraphQL', () => { }); expect(called).toBeTruthy(); }); + + it('should support dependency injection on rest api', async () => { + const requestContextMiddleware = (req, res, next) => { + req.config.aCustomController = 'aCustomController'; + next(); + }; + + let called; + await reconfigureServer({ requestContextMiddleware }); + Parse.Cloud.beforeSave('_User', request => { + expect(request.config.aCustomController).toEqual('aCustomController'); + called = true; + }); + const user = new Parse.User(); + user.setUsername('test'); + user.setPassword('test'); + await user.signUp(); + expect(called).toBeTruthy(); + }); }); diff --git a/spec/requestContextMiddlewareRest.spec.js b/spec/requestContextMiddlewareRest.spec.js deleted file mode 100644 index f0cfdc1543..0000000000 --- a/spec/requestContextMiddlewareRest.spec.js +++ /dev/null @@ -1,21 +0,0 @@ -describe('requestContextMiddlewareRest', () => { - - it('should support dependency injection on rest api', async () => { - const requestContextMiddleware = (req, res, next) => { - req.config.aCustomController = 'aCustomController'; - next(); - }; - - let called; - Parse.Cloud.beforeSave('_User', request => { - expect(request.config.aCustomController).toEqual('aCustomController'); - called = true; - }); - await reconfigureServer({ requestContextMiddleware }); - const user = new Parse.User(); - user.setUsername('test'); - user.setPassword('test'); - await user.signUp(); - expect(called).toBeTruthy(); - }); -}); From 264b1fa67b3569227d47b13e7db5321d80d984ca Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Thu, 9 Oct 2025 10:18:01 +0200 Subject: [PATCH 14/19] fix: describe name --- spec/requestContextMiddleware.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requestContextMiddleware.spec.js b/spec/requestContextMiddleware.spec.js index 64e16aed25..0e54c5deb2 100644 --- a/spec/requestContextMiddleware.spec.js +++ b/spec/requestContextMiddleware.spec.js @@ -1,4 +1,4 @@ -describe('requestContextMiddlewareGraphQL', () => { +describe('requestContextMiddleware', () => { it('should support dependency injection on graphql api', async () => { const requestContextMiddleware = (req, res, next) => { From 1841072aa244ef1113a240fe1e53f5a52e9fa3b4 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Thu, 9 Oct 2025 10:36:42 +0200 Subject: [PATCH 15/19] fix: use scoped path --- src/GraphQL/ParseGraphQLServer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GraphQL/ParseGraphQLServer.js b/src/GraphQL/ParseGraphQLServer.js index 826dc1d5a0..bf7e14f7e2 100644 --- a/src/GraphQL/ParseGraphQLServer.js +++ b/src/GraphQL/ParseGraphQLServer.js @@ -137,7 +137,7 @@ class ParseGraphQLServer { if (typeof options.requestContextMiddleware !== 'function') { throw new Error('requestContextMiddleware must be a function'); } - api.use(options.requestContextMiddleware); + api.use(this.config.graphQLPath, options.requestContextMiddleware); } } From 03bc9373f4f68281554e521420f422bb29432a7e Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Thu, 9 Oct 2025 10:51:22 +0200 Subject: [PATCH 16/19] test: use single test --- spec/requestContextMiddleware.spec.js | 39 +++++++-------------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/spec/requestContextMiddleware.spec.js b/spec/requestContextMiddleware.spec.js index 0e54c5deb2..60b619efe3 100644 --- a/spec/requestContextMiddleware.spec.js +++ b/spec/requestContextMiddleware.spec.js @@ -1,21 +1,21 @@ describe('requestContextMiddleware', () => { - it('should support dependency injection on graphql api', async () => { + it('should support dependency injection on graphql and rest api', async () => { const requestContextMiddleware = (req, res, next) => { req.config.aCustomController = 'aCustomController'; next(); }; - let called = false; - await reconfigureServer({ - requestContextMiddleware, - mountGraphQL: true, - graphQLPath: '/graphql', - }); + let called = 0 + await reconfigureServer({ requestContextMiddleware, mountGraphQL: true, graphQLPath: '/graphql' }); Parse.Cloud.beforeSave('_User', request => { expect(request.config.aCustomController).toEqual('aCustomController'); - called = true; + called++; }); + const user = new Parse.User(); + user.setUsername('test'); + user.setPassword('test'); + await user.signUp(); await fetch('http://localhost:8378/graphql', { method: 'POST', @@ -27,7 +27,7 @@ describe('requestContextMiddleware', () => { body: JSON.stringify({ query: ` mutation { - createUser(input: { fields: { username: "test", password: "test" } }) { + createUser(input: { fields: { username: "test2", password: "test2" } }) { user { objectId } @@ -36,25 +36,6 @@ describe('requestContextMiddleware', () => { `, }), }); - expect(called).toBeTruthy(); - }); - - it('should support dependency injection on rest api', async () => { - const requestContextMiddleware = (req, res, next) => { - req.config.aCustomController = 'aCustomController'; - next(); - }; - - let called; - await reconfigureServer({ requestContextMiddleware }); - Parse.Cloud.beforeSave('_User', request => { - expect(request.config.aCustomController).toEqual('aCustomController'); - called = true; - }); - const user = new Parse.User(); - user.setUsername('test'); - user.setPassword('test'); - await user.signUp(); - expect(called).toBeTruthy(); + expect(called).toBe(2); }); }); From 5804a19838992e6aab0743d3ca86f21834ee29b1 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Thu, 9 Oct 2025 21:03:54 +0200 Subject: [PATCH 17/19] test: split --- spec/ParseGraphQLServer.spec.js | 35 +++++++++++++++++++++++ spec/requestContextMiddleware.spec.js | 41 --------------------------- spec/rest.spec.js | 22 ++++++++++++++ 3 files changed, 57 insertions(+), 41 deletions(-) delete mode 100644 spec/requestContextMiddleware.spec.js diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index cac3f448ce..1133dfb44f 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -607,6 +607,41 @@ describe('ParseGraphQLServer', () => { ]); }; + describe('Context', () => { + it('should support dependency injection on graphql api', async () => { + const requestContextMiddleware = (req, res, next) => { + req.config.aCustomController = 'aCustomController'; + next(); + }; + + let called + const parseServer = await reconfigureServer({ requestContextMiddleware }); + createGQLFromParseServer(parseServer); + Parse.Cloud.beforeSave('_User', request => { + expect(request.config.aCustomController).toEqual('aCustomController'); + called = true; + }); + + await apolloClient.query({ + query: gql` + mutation { + createUser(input: { fields: { username: "test", password: "test" } }) { + user { + objectId + } + } + } + `, + context: { + headers: { + 'X-Parse-Master-Key': 'test', + }, + } + }) + expect(called).toBe(true); + }) + }) + describe('Introspection', () => { it('should have public introspection disabled by default without master key', async () => { diff --git a/spec/requestContextMiddleware.spec.js b/spec/requestContextMiddleware.spec.js deleted file mode 100644 index 60b619efe3..0000000000 --- a/spec/requestContextMiddleware.spec.js +++ /dev/null @@ -1,41 +0,0 @@ -describe('requestContextMiddleware', () => { - - it('should support dependency injection on graphql and rest api', async () => { - const requestContextMiddleware = (req, res, next) => { - req.config.aCustomController = 'aCustomController'; - next(); - }; - - let called = 0 - await reconfigureServer({ requestContextMiddleware, mountGraphQL: true, graphQLPath: '/graphql' }); - Parse.Cloud.beforeSave('_User', request => { - expect(request.config.aCustomController).toEqual('aCustomController'); - called++; - }); - const user = new Parse.User(); - user.setUsername('test'); - user.setPassword('test'); - await user.signUp(); - - await fetch('http://localhost:8378/graphql', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test', - }, - body: JSON.stringify({ - query: ` - mutation { - createUser(input: { fields: { username: "test2", password: "test2" } }) { - user { - objectId - } - } - } - `, - }), - }); - expect(called).toBe(2); - }); -}); diff --git a/spec/rest.spec.js b/spec/rest.spec.js index fed64c988b..1fff4fad59 100644 --- a/spec/rest.spec.js +++ b/spec/rest.spec.js @@ -1139,3 +1139,25 @@ describe('read-only masterKey', () => { }); }); }); + +describe('rest context', () => { + it('should support dependency injection on rest api', async () => { + const requestContextMiddleware = (req, res, next) => { + req.config.aCustomController = 'aCustomController'; + next(); + }; + + let called + await reconfigureServer({ requestContextMiddleware }); + Parse.Cloud.beforeSave('_User', request => { + expect(request.config.aCustomController).toEqual('aCustomController'); + called = true; + }); + const user = new Parse.User(); + user.setUsername('test'); + user.setPassword('test'); + await user.signUp(); + + expect(called).toBe(true); + }); +}); From 481a5e40441dec612260de8fa3d43b72a0e8f3cc Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Thu, 9 Oct 2025 23:36:57 +0200 Subject: [PATCH 18/19] test: avoid race --- spec/ParseGraphQLServer.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 1133dfb44f..02fc6dbb36 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -616,7 +616,7 @@ describe('ParseGraphQLServer', () => { let called const parseServer = await reconfigureServer({ requestContextMiddleware }); - createGQLFromParseServer(parseServer); + await createGQLFromParseServer(parseServer); Parse.Cloud.beforeSave('_User', request => { expect(request.config.aCustomController).toEqual('aCustomController'); called = true; From cedb2d2316949fb0f19c73b33baa2155d5533f0d Mon Sep 17 00:00:00 2001 From: Manuel <5673677+mtrezza@users.noreply.github.com> Date: Fri, 10 Oct 2025 00:48:22 +0200 Subject: [PATCH 19/19] Update spec/ParseGraphQLServer.spec.js Signed-off-by: Manuel <5673677+mtrezza@users.noreply.github.com> --- spec/ParseGraphQLServer.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/ParseGraphQLServer.spec.js b/spec/ParseGraphQLServer.spec.js index 02fc6dbb36..aee3575079 100644 --- a/spec/ParseGraphQLServer.spec.js +++ b/spec/ParseGraphQLServer.spec.js @@ -614,7 +614,7 @@ describe('ParseGraphQLServer', () => { next(); }; - let called + let called; const parseServer = await reconfigureServer({ requestContextMiddleware }); await createGQLFromParseServer(parseServer); Parse.Cloud.beforeSave('_User', request => {