Skip to content

Commit 025ea8a

Browse files
committed
Merge branch 'upstream/alpha' into moumouls/include-prevention-complexity
# Conflicts: # src/GraphQL/ParseGraphQLServer.js
2 parents 251e9b8 + 9ed9af4 commit 025ea8a

File tree

18 files changed

+486
-67
lines changed

18 files changed

+486
-67
lines changed

changelogs/CHANGELOG_alpha.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
1+
# [8.5.0-alpha.12](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.11...8.5.0-alpha.12) (2025-11-19)
2+
3+
4+
### Features
5+
6+
* Add `beforePasswordResetRequest` hook ([#9906](https://github.com/parse-community/parse-server/issues/9906)) ([94cee5b](https://github.com/parse-community/parse-server/commit/94cee5bfafca10c914c73cf17fcdb627a9f0837b))
7+
8+
# [8.5.0-alpha.11](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.10...8.5.0-alpha.11) (2025-11-17)
9+
10+
11+
### Bug Fixes
12+
13+
* Deprecation warning logged at server launch for nested Parse Server option even if option is explicitly set ([#9934](https://github.com/parse-community/parse-server/issues/9934)) ([c22cb0a](https://github.com/parse-community/parse-server/commit/c22cb0ae58e64cd0e4597ab9610d57a1155c44a2))
14+
15+
# [8.5.0-alpha.10](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.9...8.5.0-alpha.10) (2025-11-17)
16+
17+
18+
### Bug Fixes
19+
20+
* Queries with object field `authData.provider.id` are incorrectly transformed to `_auth_data_provider.id` for custom classes ([#9932](https://github.com/parse-community/parse-server/issues/9932)) ([7b9fa18](https://github.com/parse-community/parse-server/commit/7b9fa18f968ec084ea0b35dad2b5ba0451d59787))
21+
22+
# [8.5.0-alpha.9](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.8...8.5.0-alpha.9) (2025-11-17)
23+
24+
25+
### Bug Fixes
26+
27+
* Race condition can cause multiple Apollo server initializations under load ([#9929](https://github.com/parse-community/parse-server/issues/9929)) ([7d5e9fc](https://github.com/parse-community/parse-server/commit/7d5e9fcf3ceb0abad8ab49c75bc26f521a0f1bde))
28+
129
# [8.5.0-alpha.8](https://github.com/parse-community/parse-server/compare/8.5.0-alpha.7...8.5.0-alpha.8) (2025-11-17)
230

331

package-lock.json

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "parse-server",
3-
"version": "8.5.0-alpha.8",
3+
"version": "8.5.0-alpha.12",
44
"description": "An express module providing a Parse-compatible API server",
55
"main": "lib/index.js",
66
"repository": {

spec/CloudCode.spec.js

Lines changed: 159 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3307,19 +3307,19 @@ describe('afterFind hooks', () => {
33073307
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
33083308
expect(() => {
33093309
Parse.Cloud.beforeLogin('SomeClass', () => { });
3310-
}).toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
3310+
}).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
33113311
expect(() => {
33123312
Parse.Cloud.afterLogin(() => { });
3313-
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
3313+
}).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
33143314
expect(() => {
33153315
Parse.Cloud.afterLogin('_User', () => { });
3316-
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
3316+
}).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
33173317
expect(() => {
33183318
Parse.Cloud.afterLogin(Parse.User, () => { });
3319-
}).not.toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
3319+
}).not.toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
33203320
expect(() => {
33213321
Parse.Cloud.afterLogin('SomeClass', () => { });
3322-
}).toThrow('Only the _User class is allowed for the beforeLogin and afterLogin triggers');
3322+
}).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
33233323
expect(() => {
33243324
Parse.Cloud.afterLogout(() => { });
33253325
}).not.toThrow();
@@ -4656,3 +4656,157 @@ describe('sendEmail', () => {
46564656
);
46574657
});
46584658
});
4659+
4660+
describe('beforePasswordResetRequest hook', () => {
4661+
it('should run beforePasswordResetRequest with valid user', async () => {
4662+
let hit = 0;
4663+
let sendPasswordResetEmailCalled = false;
4664+
const emailAdapter = {
4665+
sendVerificationEmail: () => Promise.resolve(),
4666+
sendPasswordResetEmail: () => {
4667+
sendPasswordResetEmailCalled = true;
4668+
},
4669+
sendMail: () => {},
4670+
};
4671+
4672+
await reconfigureServer({
4673+
appName: 'test',
4674+
emailAdapter: emailAdapter,
4675+
publicServerURL: 'http://localhost:8378/1',
4676+
});
4677+
4678+
Parse.Cloud.beforePasswordResetRequest(req => {
4679+
hit++;
4680+
expect(req.object).toBeDefined();
4681+
expect(req.object.get('email')).toEqual('[email protected]');
4682+
expect(req.object.get('username')).toEqual('testuser');
4683+
});
4684+
4685+
const user = new Parse.User();
4686+
user.setUsername('testuser');
4687+
user.setPassword('password');
4688+
user.set('email', '[email protected]');
4689+
await user.signUp();
4690+
4691+
await Parse.User.requestPasswordReset('[email protected]');
4692+
expect(hit).toBe(1);
4693+
expect(sendPasswordResetEmailCalled).toBe(true);
4694+
});
4695+
4696+
it('should be able to block password reset request if an error is thrown', async () => {
4697+
let hit = 0;
4698+
let sendPasswordResetEmailCalled = false;
4699+
const emailAdapter = {
4700+
sendVerificationEmail: () => Promise.resolve(),
4701+
sendPasswordResetEmail: () => {
4702+
sendPasswordResetEmailCalled = true;
4703+
},
4704+
sendMail: () => {},
4705+
};
4706+
4707+
await reconfigureServer({
4708+
appName: 'test',
4709+
emailAdapter: emailAdapter,
4710+
publicServerURL: 'http://localhost:8378/1',
4711+
});
4712+
4713+
Parse.Cloud.beforePasswordResetRequest(req => {
4714+
hit++;
4715+
throw new Error('password reset blocked');
4716+
});
4717+
4718+
const user = new Parse.User();
4719+
user.setUsername('testuser');
4720+
user.setPassword('password');
4721+
user.set('email', '[email protected]');
4722+
await user.signUp();
4723+
4724+
try {
4725+
await Parse.User.requestPasswordReset('[email protected]');
4726+
throw new Error('should not have sent password reset email.');
4727+
} catch (e) {
4728+
expect(e.message).toBe('password reset blocked');
4729+
}
4730+
expect(hit).toBe(1);
4731+
expect(sendPasswordResetEmailCalled).toBe(false);
4732+
});
4733+
4734+
it('should not run beforePasswordResetRequest if email does not exist', async () => {
4735+
let hit = 0;
4736+
const emailAdapter = {
4737+
sendVerificationEmail: () => Promise.resolve(),
4738+
sendPasswordResetEmail: () => {},
4739+
sendMail: () => {},
4740+
};
4741+
4742+
await reconfigureServer({
4743+
appName: 'test',
4744+
emailAdapter: emailAdapter,
4745+
publicServerURL: 'http://localhost:8378/1',
4746+
});
4747+
4748+
Parse.Cloud.beforePasswordResetRequest(req => {
4749+
hit++;
4750+
});
4751+
4752+
await Parse.User.requestPasswordReset('[email protected]');
4753+
4754+
expect(hit).toBe(0);
4755+
});
4756+
4757+
it('should have expected data in request in beforePasswordResetRequest', async () => {
4758+
const emailAdapter = {
4759+
sendVerificationEmail: () => Promise.resolve(),
4760+
sendPasswordResetEmail: () => {},
4761+
sendMail: () => {},
4762+
};
4763+
4764+
await reconfigureServer({
4765+
appName: 'test',
4766+
emailAdapter: emailAdapter,
4767+
publicServerURL: 'http://localhost:8378/1',
4768+
});
4769+
4770+
const base64 = 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=';
4771+
const file = new Parse.File('myfile.txt', { base64 });
4772+
await file.save();
4773+
4774+
Parse.Cloud.beforePasswordResetRequest(req => {
4775+
expect(req.object).toBeDefined();
4776+
expect(req.object.get('email')).toBeDefined();
4777+
expect(req.object.get('email')).toBe('[email protected]');
4778+
expect(req.object.get('file')).toBeDefined();
4779+
expect(req.object.get('file')).toBeInstanceOf(Parse.File);
4780+
expect(req.object.get('file').name()).toContain('myfile.txt');
4781+
expect(req.headers).toBeDefined();
4782+
expect(req.ip).toBeDefined();
4783+
expect(req.installationId).toBeDefined();
4784+
expect(req.context).toBeDefined();
4785+
expect(req.config).toBeDefined();
4786+
});
4787+
4788+
const user = new Parse.User();
4789+
user.setUsername('testuser2');
4790+
user.setPassword('password');
4791+
user.set('email', '[email protected]');
4792+
user.set('file', file);
4793+
await user.signUp();
4794+
4795+
await Parse.User.requestPasswordReset('[email protected]');
4796+
});
4797+
4798+
it('should validate that only _User class is allowed for beforePasswordResetRequest', () => {
4799+
expect(() => {
4800+
Parse.Cloud.beforePasswordResetRequest('SomeClass', () => { });
4801+
}).toThrow('Only the _User class is allowed for the beforeLogin, afterLogin, and beforePasswordResetRequest triggers');
4802+
expect(() => {
4803+
Parse.Cloud.beforePasswordResetRequest(() => { });
4804+
}).not.toThrow();
4805+
expect(() => {
4806+
Parse.Cloud.beforePasswordResetRequest('_User', () => { });
4807+
}).not.toThrow();
4808+
expect(() => {
4809+
Parse.Cloud.beforePasswordResetRequest(Parse.User, () => { });
4810+
}).not.toThrow();
4811+
});
4812+
});

spec/Deprecator.spec.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,29 @@ describe('Deprecator', () => {
4545
`DeprecationWarning: ${options.usage} is deprecated and will be removed in a future version. ${options.solution}`
4646
);
4747
});
48+
49+
it('logs deprecation for nested option key with dot notation', async () => {
50+
deprecations = [{ optionKey: 'databaseOptions.allowPublicExplain', changeNewDefault: 'false' }];
51+
52+
spyOn(Deprecator, '_getDeprecations').and.callFake(() => deprecations);
53+
const logger = require('../lib/logger').logger;
54+
const logSpy = spyOn(logger, 'warn').and.callFake(() => {});
55+
56+
await reconfigureServer();
57+
expect(logSpy.calls.all()[0].args[0]).toEqual(
58+
`DeprecationWarning: The Parse Server option '${deprecations[0].optionKey}' default will change to '${deprecations[0].changeNewDefault}' in a future version.`
59+
);
60+
});
61+
62+
it('does not log deprecation for nested option key if option is set manually', async () => {
63+
deprecations = [{ optionKey: 'databaseOptions.allowPublicExplain', changeNewDefault: 'false' }];
64+
65+
spyOn(Deprecator, '_getDeprecations').and.callFake(() => deprecations);
66+
const logSpy = spyOn(Deprecator, '_logOption').and.callFake(() => {});
67+
const Config = require('../lib/Config');
68+
const config = Config.get('test');
69+
// Directly test scanParseServerOptions with nested option set
70+
Deprecator.scanParseServerOptions({ databaseOptions: { allowPublicExplain: true } });
71+
expect(logSpy).not.toHaveBeenCalled();
72+
});
4873
});

spec/MongoTransform.spec.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,23 @@ describe('parseObjectToMongoObjectForCreate', () => {
521521
expect(output.authData).toBe('random');
522522
done();
523523
});
524+
525+
it('should only transform authData.provider.id for _User class', () => {
526+
// Test that for _User class, authData.facebook.id is transformed
527+
const userInput = {
528+
'authData.facebook.id': '10000000000000001',
529+
};
530+
const userOutput = transform.transformWhere('_User', userInput, { fields: {} });
531+
expect(userOutput['_auth_data_facebook.id']).toBe('10000000000000001');
532+
533+
// Test that for non-User classes, authData.facebook.id is NOT transformed
534+
const customInput = {
535+
'authData.facebook.id': '10000000000000001',
536+
};
537+
const customOutput = transform.transformWhere('SpamAlerts', customInput, { fields: {} });
538+
expect(customOutput['authData.facebook.id']).toBe('10000000000000001');
539+
expect(customOutput['_auth_data_facebook.id']).toBeUndefined();
540+
});
524541
});
525542

526543
it('cannot have a custom field name beginning with underscore', done => {

spec/ParseGraphQLServer.spec.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,20 @@ describe('ParseGraphQLServer', () => {
118118
expect(server3).not.toBe(server2);
119119
expect(server3).toBe(server4);
120120
});
121+
122+
it('should return same server reference when called 100 times in parallel', async () => {
123+
parseGraphQLServer.server = undefined;
124+
125+
// Call _getServer 100 times in parallel
126+
const promises = Array.from({ length: 100 }, () => parseGraphQLServer._getServer());
127+
const servers = await Promise.all(promises);
128+
129+
// All resolved servers should be the same reference
130+
const firstServer = servers[0];
131+
servers.forEach((server, index) => {
132+
expect(server).toBe(firstServer);
133+
});
134+
});
121135
});
122136

123137
describe('_getGraphQLOptions', () => {

0 commit comments

Comments
 (0)