11 Commits

Author SHA1 Message Date
semantic-release-bot
3d395b3ce5 chore(release): 9.1.1-alpha.1 [skip ci]
## [9.1.1-alpha.1](https://github.com/parse-community/parse-server/compare/9.1.0...9.1.1-alpha.1) (2025-12-16)

### Bug Fixes

* Server-Side Request Forgery (SSRF) in Instagram auth adapter [GHSA-3f5f-xgrj-97pf](https://github.com/parse-community/parse-server/security/advisories/GHSA-3f5f-xgrj-97pf) ([#9988](https://github.com/parse-community/parse-server/issues/9988)) ([fbcc938](fbcc938b5a))
2025-12-16 01:25:34 +00:00
Manuel
fbcc938b5a fix: Server-Side Request Forgery (SSRF) in Instagram auth adapter [GHSA-3f5f-xgrj-97pf](https://github.com/parse-community/parse-server/security/advisories/GHSA-3f5f-xgrj-97pf) (#9988) 2025-12-16 02:24:37 +01:00
semantic-release-bot
2e06fa1139 chore(release): 9.1.0 [skip ci]
# [9.1.0](https://github.com/parse-community/parse-server/compare/9.0.0...9.1.0) (2025-12-14)

### Bug Fixes

* Cross-Site Scripting (XSS) via HTML pages for password reset and email verification [GHSA-jhgf-2h8h-ggxv](https://github.com/parse-community/parse-server/security/advisories/GHSA-jhgf-2h8h-ggxv) ([#9985](https://github.com/parse-community/parse-server/issues/9985)) ([3074eb7](3074eb70f5))

### Features

* Add option `logLevels.signupUsernameTaken` to change log level of username already exists sign-up rejection ([#9962](https://github.com/parse-community/parse-server/issues/9962)) ([f18f307](f18f3073d7))
* Add support for custom HTTP status code and headers to Cloud Function response with Express-style syntax ([#9980](https://github.com/parse-community/parse-server/issues/9980)) ([8eeab8d](8eeab8dc57))
* Log more debug info when failing to set duplicate value for field with unique values ([#9919](https://github.com/parse-community/parse-server/issues/9919)) ([a23b192](a23b192466))
2025-12-14 15:49:33 +00:00
Manuel
8c4d67a0fe build: Release (#9987) 2025-12-14 16:48:45 +01:00
GitHub Actions
ae0781d0ac empty commit to trigger CI 2025-12-14 15:41:22 +00:00
semantic-release-bot
0e308feaa7 chore(release): 9.1.0-alpha.4 [skip ci]
# [9.1.0-alpha.4](https://github.com/parse-community/parse-server/compare/9.1.0-alpha.3...9.1.0-alpha.4) (2025-12-14)

### Features

* Log more debug info when failing to set duplicate value for field with unique values ([#9919](https://github.com/parse-community/parse-server/issues/9919)) ([a23b192](a23b192466))
2025-12-14 15:40:04 +00:00
Rahul Lanjewar
a23b192466 feat: Log more debug info when failing to set duplicate value for field with unique values (#9919) 2025-12-14 16:39:17 +01:00
semantic-release-bot
98a42e5277 chore(release): 9.1.0-alpha.3 [skip ci]
# [9.1.0-alpha.3](https://github.com/parse-community/parse-server/compare/9.1.0-alpha.2...9.1.0-alpha.3) (2025-12-14)

### Bug Fixes

* Cross-Site Scripting (XSS) via HTML pages for password reset and email verification [GHSA-jhgf-2h8h-ggxv](https://github.com/parse-community/parse-server/security/advisories/GHSA-jhgf-2h8h-ggxv) ([#9985](https://github.com/parse-community/parse-server/issues/9985)) ([3074eb7](3074eb70f5))
2025-12-14 14:45:10 +00:00
Manuel
3074eb70f5 fix: Cross-Site Scripting (XSS) via HTML pages for password reset and email verification [GHSA-jhgf-2h8h-ggxv](https://github.com/parse-community/parse-server/security/advisories/GHSA-jhgf-2h8h-ggxv) (#9985) 2025-12-14 15:44:04 +01:00
semantic-release-bot
7028e0385c chore(release): 9.1.0-alpha.2 [skip ci]
# [9.1.0-alpha.2](https://github.com/parse-community/parse-server/compare/9.1.0-alpha.1...9.1.0-alpha.2) (2025-12-14)

### Features

* Add support for custom HTTP status code and headers to Cloud Function response with Express-style syntax ([#9980](https://github.com/parse-community/parse-server/issues/9980)) ([8eeab8d](8eeab8dc57))
2025-12-14 14:25:39 +00:00
Copilot
8eeab8dc57 feat: Add support for custom HTTP status code and headers to Cloud Function response with Express-style syntax (#9980) 2025-12-14 15:24:51 +01:00
18 changed files with 498 additions and 35 deletions

View File

@@ -1,3 +1,31 @@
## [9.1.1-alpha.1](https://github.com/parse-community/parse-server/compare/9.1.0...9.1.1-alpha.1) (2025-12-16)
### Bug Fixes
* Server-Side Request Forgery (SSRF) in Instagram auth adapter [GHSA-3f5f-xgrj-97pf](https://github.com/parse-community/parse-server/security/advisories/GHSA-3f5f-xgrj-97pf) ([#9988](https://github.com/parse-community/parse-server/issues/9988)) ([fbcc938](https://github.com/parse-community/parse-server/commit/fbcc938b5ade5ff4c30598ac51272ef7ecef0616))
# [9.1.0-alpha.4](https://github.com/parse-community/parse-server/compare/9.1.0-alpha.3...9.1.0-alpha.4) (2025-12-14)
### Features
* Log more debug info when failing to set duplicate value for field with unique values ([#9919](https://github.com/parse-community/parse-server/issues/9919)) ([a23b192](https://github.com/parse-community/parse-server/commit/a23b1924668920f3c92fec0566b57091d0e8aae8))
# [9.1.0-alpha.3](https://github.com/parse-community/parse-server/compare/9.1.0-alpha.2...9.1.0-alpha.3) (2025-12-14)
### Bug Fixes
* Cross-Site Scripting (XSS) via HTML pages for password reset and email verification [GHSA-jhgf-2h8h-ggxv](https://github.com/parse-community/parse-server/security/advisories/GHSA-jhgf-2h8h-ggxv) ([#9985](https://github.com/parse-community/parse-server/issues/9985)) ([3074eb7](https://github.com/parse-community/parse-server/commit/3074eb70f5b58bf72b528ae7b7804ed2d90455ce))
# [9.1.0-alpha.2](https://github.com/parse-community/parse-server/compare/9.1.0-alpha.1...9.1.0-alpha.2) (2025-12-14)
### Features
* Add support for custom HTTP status code and headers to Cloud Function response with Express-style syntax ([#9980](https://github.com/parse-community/parse-server/issues/9980)) ([8eeab8d](https://github.com/parse-community/parse-server/commit/8eeab8dc57edef3751aa188d8247f296a270b083))
# [9.1.0-alpha.1](https://github.com/parse-community/parse-server/compare/9.0.0...9.1.0-alpha.1) (2025-12-14)

View File

@@ -1,3 +1,16 @@
# [9.1.0](https://github.com/parse-community/parse-server/compare/9.0.0...9.1.0) (2025-12-14)
### Bug Fixes
* Cross-Site Scripting (XSS) via HTML pages for password reset and email verification [GHSA-jhgf-2h8h-ggxv](https://github.com/parse-community/parse-server/security/advisories/GHSA-jhgf-2h8h-ggxv) ([#9985](https://github.com/parse-community/parse-server/issues/9985)) ([3074eb7](https://github.com/parse-community/parse-server/commit/3074eb70f5b58bf72b528ae7b7804ed2d90455ce))
### Features
* Add option `logLevels.signupUsernameTaken` to change log level of username already exists sign-up rejection ([#9962](https://github.com/parse-community/parse-server/issues/9962)) ([f18f307](https://github.com/parse-community/parse-server/commit/f18f3073d70a292bc70b5d572ef58e4845de89ca))
* Add support for custom HTTP status code and headers to Cloud Function response with Express-style syntax ([#9980](https://github.com/parse-community/parse-server/issues/9980)) ([8eeab8d](https://github.com/parse-community/parse-server/commit/8eeab8dc57edef3751aa188d8247f296a270b083))
* Log more debug info when failing to set duplicate value for field with unique values ([#9919](https://github.com/parse-community/parse-server/issues/9919)) ([a23b192](https://github.com/parse-community/parse-server/commit/a23b1924668920f3c92fec0566b57091d0e8aae8))
# [9.0.0](https://github.com/parse-community/parse-server/compare/8.6.0...9.0.0) (2025-12-14)

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "parse-server",
"version": "9.1.0-alpha.1",
"version": "9.1.1-alpha.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "parse-server",
"version": "9.1.0-alpha.1",
"version": "9.1.1-alpha.1",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "parse-server",
"version": "9.1.0-alpha.1",
"version": "9.1.1-alpha.1",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {

View File

@@ -14,9 +14,9 @@
<body>
<h1>{{appName}}</h1>
<h1>Expired verification link!</h1>
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
<input name="token" type="hidden" value="{{{token}}}">
<input name="locale" type="hidden" value="{{{locale}}}">
<form method="POST" action="{{publicServerUrl}}/apps/{{appId}}/resend_verification_email">
<input name="token" type="hidden" value="{{token}}">
<input name="locale" type="hidden" value="{{locale}}">
<button type="submit">Resend Link</button>
</form>
</body>

View File

@@ -23,11 +23,11 @@
<p>You can set a new Password for your account: {{username}}</p>
<br />
<p>{{error}}</p>
<form id='form' action='{{{publicServerUrl}}}/apps/{{{appId}}}/request_password_reset' method='POST'>
<form id='form' action='{{publicServerUrl}}/apps/{{appId}}/request_password_reset' method='POST'>
<input name='utf-8' type='hidden' value='✓' />
<input name="username" type="hidden" id="username" value="{{{username}}}" />
<input name="token" type="hidden" id="token" value="{{{token}}}" />
<input name="locale" type="hidden" id="locale" value="{{{locale}}}" />
<input name="username" type="hidden" id="username" value="{{username}}" />
<input name="token" type="hidden" id="token" value="{{token}}" />
<input name="locale" type="hidden" id="locale" value="{{locale}}" />
<p>New Password</p>
<input name="new_password" type="password" id="password" />

View File

@@ -14,9 +14,9 @@
<body>
<h1>{{appName}}</h1>
<h1>Expired verification link!</h1>
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
<input name="token" type="hidden" value="{{{token}}}">
<input name="locale" type="hidden" value="{{{locale}}}">
<form method="POST" action="{{publicServerUrl}}/apps/{{appId}}/resend_verification_email">
<input name="token" type="hidden" value="{{token}}">
<input name="locale" type="hidden" value="{{locale}}">
<button type="submit">Resend Link</button>
</form>
</body>

View File

@@ -23,11 +23,11 @@
<p>You can set a new Password for your account: {{username}}</p>
<br />
<p>{{error}}</p>
<form id='form' action='{{{publicServerUrl}}}/apps/{{{appId}}}/request_password_reset' method='POST'>
<form id='form' action='{{publicServerUrl}}/apps/{{appId}}/request_password_reset' method='POST'>
<input name='utf-8' type='hidden' value='✓' />
<input name="username" type="hidden" id="username" value="{{{username}}}" />
<input name="token" type="hidden" id="token" value="{{{token}}}" />
<input name="locale" type="hidden" id="locale" value="{{{locale}}}" />
<input name="username" type="hidden" id="username" value="{{username}}" />
<input name="token" type="hidden" id="token" value="{{token}}" />
<input name="locale" type="hidden" id="locale" value="{{locale}}" />
<p>New Password</p>
<input name="new_password" type="password" id="password" />

View File

@@ -14,9 +14,9 @@
<body>
<h1>{{appName}}</h1>
<h1>Expired verification link!</h1>
<form method="POST" action="{{{publicServerUrl}}}/apps/{{{appId}}}/resend_verification_email">
<input name="token" type="hidden" value="{{{token}}}">
<input name="locale" type="hidden" value="{{{locale}}}">
<form method="POST" action="{{publicServerUrl}}/apps/{{appId}}/resend_verification_email">
<input name="token" type="hidden" value="{{token}}">
<input name="locale" type="hidden" value="{{locale}}">
<button type="submit">Resend Link</button>
</form>
</body>

View File

@@ -23,11 +23,11 @@
<p>You can set a new Password for your account: {{username}}</p>
<br />
<p>{{error}}</p>
<form id='form' action='{{{publicServerUrl}}}/apps/{{{appId}}}/request_password_reset' method='POST'>
<form id='form' action='{{publicServerUrl}}/apps/{{appId}}/request_password_reset' method='POST'>
<input name='utf-8' type='hidden' value='✓' />
<input name="username" type="hidden" id="username" value="{{{username}}}" />
<input name="token" type="hidden" id="token" value="{{{token}}}" />
<input name="locale" type="hidden" id="locale" value="{{{locale}}}" />
<input name="username" type="hidden" id="username" value="{{username}}" />
<input name="token" type="hidden" id="token" value="{{token}}" />
<input name="locale" type="hidden" id="locale" value="{{locale}}" />
<p>New Password</p>
<input name="new_password" type="password" id="password" />

View File

@@ -101,6 +101,31 @@ describe('InstagramAdapter', function () {
'Instagram auth is invalid for this user.'
);
});
it('should ignore client-provided apiURL and use hardcoded endpoint', async () => {
const accessToken = 'mockAccessToken';
const authData = {
id: 'mockUserId',
apiURL: 'https://example.com/',
};
mockFetch([
{
url: 'https://graph.instagram.com/me?fields=id&access_token=mockAccessToken',
method: 'GET',
response: {
ok: true,
json: () =>
Promise.resolve({
id: 'mockUserId',
}),
},
},
]);
const user = await adapter.getUserFromAccessToken(accessToken, authData);
expect(user).toEqual({ id: 'mockUserId' });
});
});
describe('InstagramAdapter E2E Test', function () {

View File

@@ -4788,4 +4788,231 @@ describe('beforePasswordResetRequest hook', () => {
Parse.Cloud.beforePasswordResetRequest(Parse.User, () => { });
}).not.toThrow();
});
describe('Express-style cloud functions with (req, res) parameters', () => {
it('should support express-style cloud function with res.success()', async () => {
Parse.Cloud.define('expressStyleFunction', (req, res) => {
res.success({ message: 'Hello from express style!' });
});
const result = await Parse.Cloud.run('expressStyleFunction', {});
expect(result.message).toEqual('Hello from express style!');
});
it('should support express-style cloud function with res.error()', async () => {
Parse.Cloud.define('expressStyleError', (req, res) => {
res.error('Custom error message');
});
await expectAsync(Parse.Cloud.run('expressStyleError', {})).toBeRejectedWith(
new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Custom error message')
);
});
it('should support setting custom HTTP status code with res.status().success()', async () => {
Parse.Cloud.define('customStatusCode', (req, res) => {
res.status(201).success({ created: true });
});
const response = await request({
method: 'POST',
url: 'http://localhost:8378/1/functions/customStatusCode',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
json: true,
body: {},
});
expect(response.status).toBe(201);
expect(response.data.result.created).toBe(true);
});
it('should support 401 unauthorized status code with error', async () => {
Parse.Cloud.define('unauthorizedFunction', (req, res) => {
if (!req.user) {
res.status(401).error('Unauthorized access');
} else {
res.success({ message: 'Authorized' });
}
});
await expectAsync(
request({
method: 'POST',
url: 'http://localhost:8378/1/functions/unauthorizedFunction',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
json: true,
body: {},
})
).toBeRejected();
});
it('should support 404 not found status code with error', async () => {
Parse.Cloud.define('notFoundFunction', (req, res) => {
res.status(404).error('Resource not found');
});
await expectAsync(
request({
method: 'POST',
url: 'http://localhost:8378/1/functions/notFoundFunction',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
json: true,
body: {},
})
).toBeRejected();
});
it('should default to 200 status code when not specified', async () => {
Parse.Cloud.define('defaultStatusCode', (req, res) => {
res.success({ message: 'Default status' });
});
const response = await request({
method: 'POST',
url: 'http://localhost:8378/1/functions/defaultStatusCode',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
json: true,
body: {},
});
expect(response.status).toBe(200);
expect(response.data.result.message).toBe('Default status');
});
it('should maintain backward compatibility with single-parameter functions', async () => {
Parse.Cloud.define('traditionalFunction', (req) => {
return { message: 'Traditional style works!' };
});
const result = await Parse.Cloud.run('traditionalFunction', {});
expect(result.message).toEqual('Traditional style works!');
});
it('should maintain backward compatibility with implicit return functions', async () => {
Parse.Cloud.define('implicitReturnFunction', () => 'Implicit return works!');
const result = await Parse.Cloud.run('implicitReturnFunction', {});
expect(result).toEqual('Implicit return works!');
});
it('should support async express-style functions', async () => {
Parse.Cloud.define('asyncExpressStyle', async (req, res) => {
await new Promise(resolve => setTimeout(resolve, 10));
res.success({ async: true });
});
const result = await Parse.Cloud.run('asyncExpressStyle', {});
expect(result.async).toBe(true);
});
it('should access request parameters in express-style functions', async () => {
Parse.Cloud.define('expressWithParams', (req, res) => {
const { name } = req.params;
res.success({ greeting: `Hello, ${name}!` });
});
const result = await Parse.Cloud.run('expressWithParams', { name: 'World' });
expect(result.greeting).toEqual('Hello, World!');
});
it('should access user in express-style functions', async () => {
const user = new Parse.User();
user.set('username', 'testuser');
user.set('password', 'testpass');
await user.signUp();
Parse.Cloud.define('expressWithUser', (req, res) => {
if (req.user) {
res.success({ username: req.user.get('username') });
} else {
res.status(401).error('Not authenticated');
}
});
const result = await Parse.Cloud.run('expressWithUser', {});
expect(result.username).toEqual('testuser');
await Parse.User.logOut();
});
it('should support setting custom headers with res.header()', async () => {
Parse.Cloud.define('customHeaderFunction', (req, res) => {
res.header('X-Custom-Header', 'custom-value').success({ message: 'OK' });
});
const response = await request({
method: 'POST',
url: 'http://localhost:8378/1/functions/customHeaderFunction',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
json: true,
body: {},
});
expect(response.status).toBe(200);
expect(response.headers['x-custom-header']).toBe('custom-value');
expect(response.data.result.message).toBe('OK');
});
it('should support setting multiple custom headers', async () => {
Parse.Cloud.define('multipleHeadersFunction', (req, res) => {
res.header('X-Header-One', 'value1')
.header('X-Header-Two', 'value2')
.success({ message: 'Multiple headers' });
});
const response = await request({
method: 'POST',
url: 'http://localhost:8378/1/functions/multipleHeadersFunction',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
json: true,
body: {},
});
expect(response.status).toBe(200);
expect(response.headers['x-header-one']).toBe('value1');
expect(response.headers['x-header-two']).toBe('value2');
expect(response.data.result.message).toBe('Multiple headers');
});
it('should support combining status code and custom headers', async () => {
Parse.Cloud.define('statusAndHeaderFunction', (req, res) => {
res.status(201)
.header('X-Resource-Id', '12345')
.success({ created: true });
});
const response = await request({
method: 'POST',
url: 'http://localhost:8378/1/functions/statusAndHeaderFunction',
headers: {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
json: true,
body: {},
});
expect(response.status).toBe(201);
expect(response.headers['x-resource-id']).toBe('12345');
expect(response.data.result.created).toBe(true);
});
});
});

View File

@@ -1180,4 +1180,72 @@ describe('Pages Router', () => {
});
});
});
describe('XSS Protection', () => {
beforeEach(async () => {
await reconfigureServer({
appId: 'test',
appName: 'exampleAppname',
publicServerURL: 'http://localhost:8378/1',
pages: { enableRouter: true },
});
});
it('should escape XSS payloads in token parameter', async () => {
const xssPayload = '"><script>alert("XSS")</script>';
const response = await request({
url: `http://localhost:8378/1/apps/choose_password?token=${encodeURIComponent(xssPayload)}&username=test&appId=test`,
});
expect(response.status).toBe(200);
expect(response.text).not.toContain('<script>alert("XSS")</script>');
expect(response.text).toContain('&quot;&gt;&lt;script&gt;');
});
it('should escape XSS in username parameter', async () => {
const xssUsername = '<img src=x onerror=alert(1)>';
const response = await request({
url: `http://localhost:8378/1/apps/choose_password?username=${encodeURIComponent(xssUsername)}&appId=test`,
});
expect(response.status).toBe(200);
expect(response.text).not.toContain('<img src=x onerror=alert(1)>');
expect(response.text).toContain('&lt;img');
});
it('should escape XSS in locale parameter', async () => {
const xssLocale = '"><svg/onload=alert(1)>';
const response = await request({
url: `http://localhost:8378/1/apps/choose_password?locale=${encodeURIComponent(xssLocale)}&appId=test`,
});
expect(response.status).toBe(200);
expect(response.text).not.toContain('<svg/onload=alert(1)>');
expect(response.text).toContain('&quot;&gt;&lt;svg');
});
it('should handle legitimate usernames with quotes correctly', async () => {
const username = "O'Brien";
const response = await request({
url: `http://localhost:8378/1/apps/choose_password?username=${encodeURIComponent(username)}&appId=test`,
});
expect(response.status).toBe(200);
// Should be properly escaped as HTML entity
expect(response.text).toContain('O&#39;Brien');
// Should NOT contain unescaped quote that breaks HTML
expect(response.text).not.toContain('value="O\'Brien"');
});
it('should handle legitimate usernames with ampersands correctly', async () => {
const username = 'Smith & Co';
const response = await request({
url: `http://localhost:8378/1/apps/choose_password?username=${encodeURIComponent(username)}&appId=test`,
});
expect(response.status).toBe(200);
// Should be properly escaped
expect(response.text).toContain('Smith &amp; Co');
});
});
});

View File

@@ -3842,6 +3842,7 @@ describe('schemas', () => {
});
it_id('cbd5d897-b938-43a4-8f5a-5d02dd2be9be')(it_exclude_dbs(['postgres']))('cannot update to duplicate value on unique index', done => {
loggerErrorSpy.calls.reset();
const index = {
code: 1,
};
@@ -3868,6 +3869,12 @@ describe('schemas', () => {
.then(done.fail)
.catch(error => {
expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE);
// Client should only see generic message (no schema info exposed)
expect(error.message).toEqual('A duplicate value for a field with unique values was provided');
// Server logs should contain full MongoDB error message with detailed information
expect(loggerErrorSpy).toHaveBeenCalledWith('Duplicate key error:', jasmine.stringContaining('E11000 duplicate key error'));
expect(loggerErrorSpy).toHaveBeenCalledWith('Duplicate key error:', jasmine.stringContaining('test_UniqueIndexClass'));
expect(loggerErrorSpy).toHaveBeenCalledWith('Duplicate key error:', jasmine.stringContaining('code_1'));
done();
});
});

View File

@@ -96,8 +96,7 @@ class InstagramAdapter extends BaseAuthCodeAdapter {
}
async getUserFromAccessToken(accessToken, authData) {
const defaultURL = 'https://graph.instagram.com/';
const apiURL = authData.apiURL || defaultURL;
const apiURL = 'https://graph.instagram.com/';
const path = `${apiURL}me?fields=id&access_token=${accessToken}`;
const response = await fetch(path);

View File

@@ -519,7 +519,7 @@ export class MongoStorageAdapter implements StorageAdapter {
.then(() => ({ ops: [mongoObject] }))
.catch(error => {
if (error.code === 11000) {
// Duplicate value
logger.error('Duplicate key error:', error.message);
const err = new Parse.Error(
Parse.Error.DUPLICATE_VALUE,
'A duplicate value for a field with unique values was provided'
@@ -605,6 +605,7 @@ export class MongoStorageAdapter implements StorageAdapter {
.then(result => mongoObjectToParseObject(className, result, schema))
.catch(error => {
if (error.code === 11000) {
logger.error('Duplicate key error:', error.message);
throw new Parse.Error(
Parse.Error.DUPLICATE_VALUE,
'A duplicate value for a field with unique values was provided'

View File

@@ -103,20 +103,52 @@ export class FunctionsRouter extends PromiseRouter {
});
}
static createResponseObject(resolve, reject) {
return {
static createResponseObject(resolve, reject, statusCode = null) {
let httpStatusCode = statusCode;
const customHeaders = {};
let responseSent = false;
const responseObject = {
success: function (result) {
resolve({
if (responseSent) {
throw new Error('Cannot call success() after response has already been sent. Make sure to call success() or error() only once per cloud function execution.');
}
responseSent = true;
const response = {
response: {
result: Parse._encode(result),
},
});
};
if (httpStatusCode !== null) {
response.status = httpStatusCode;
}
if (Object.keys(customHeaders).length > 0) {
response.headers = customHeaders;
}
resolve(response);
},
error: function (message) {
if (responseSent) {
throw new Error('Cannot call error() after response has already been sent. Make sure to call success() or error() only once per cloud function execution.');
}
responseSent = true;
const error = triggers.resolveError(message);
// If a custom status code was set, attach it to the error
if (httpStatusCode !== null) {
error.status = httpStatusCode;
}
reject(error);
},
status: function (code) {
httpStatusCode = code;
return responseObject;
},
header: function (key, value) {
customHeaders[key] = value;
return responseObject;
},
_isResponseSent: () => responseSent,
};
return responseObject;
}
static handleCloudFunction(req) {
const functionName = req.params.functionName;
@@ -143,7 +175,7 @@ export class FunctionsRouter extends PromiseRouter {
return new Promise(function (resolve, reject) {
const userString = req.auth && req.auth.user ? req.auth.user.id : undefined;
const { success, error } = FunctionsRouter.createResponseObject(
const responseObject = FunctionsRouter.createResponseObject(
result => {
try {
if (req.config.logLevels.cloudFunctionSuccess !== 'silent') {
@@ -184,14 +216,37 @@ export class FunctionsRouter extends PromiseRouter {
}
}
);
const { success, error } = responseObject;
return Promise.resolve()
.then(() => {
return triggers.maybeRunValidator(request, functionName, req.auth);
})
.then(() => {
return theFunction(request);
// Check if function expects 2 parameters (req, res) - Express style
if (theFunction.length >= 2) {
return theFunction(request, responseObject);
} else {
// Traditional style - single parameter
return theFunction(request);
}
})
.then(success, error);
.then(result => {
// For Express-style functions, only send response if not already sent
if (theFunction.length >= 2) {
if (!responseObject._isResponseSent()) {
// If Express-style function returns a value without calling res.success/error
if (result !== undefined) {
success(result);
}
// If no response sent and no value returned, this is an error in user code
// but we don't handle it here to maintain backward compatibility
}
} else {
// For traditional functions, always call success with the result (even if undefined)
success(result);
}
}, error);
});
}
}

View File

@@ -107,22 +107,49 @@ var ParseCloud = {};
*
* **Available in Cloud Code only.**
*
* **Traditional Style:**
* ```
* Parse.Cloud.define('functionName', (request) => {
* // code here
* return result;
* }, (request) => {
* // validation code here
* });
*
* Parse.Cloud.define('functionName', (request) => {
* // code here
* return result;
* }, { ...validationObject });
* ```
*
* **Express Style with Custom HTTP Status Codes:**
* ```
* Parse.Cloud.define('functionName', (request, response) => {
* // Set custom HTTP status code and send response
* response.status(201).success({ message: 'Created' });
* });
*
* Parse.Cloud.define('unauthorizedFunction', (request, response) => {
* if (!request.user) {
* response.status(401).error('Unauthorized');
* } else {
* response.success({ data: 'OK' });
* }
* });
*
* Parse.Cloud.define('withCustomHeaders', (request, response) => {
* response.header('X-Custom-Header', 'value').success({ data: 'OK' });
* });
*
* Parse.Cloud.define('errorFunction', (request, response) => {
* response.error('Something went wrong');
* });
* ```
*
* @static
* @memberof Parse.Cloud
* @param {String} name The name of the Cloud Function
* @param {Function} data The Cloud Function to register. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}.
* @param {Function} data The Cloud Function to register. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or two parameters (request, response) for Express-style functions where response is a {@link Parse.Cloud.FunctionResponse}.
* @param {(Object|Function)} validator An optional function to help validating cloud code. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}, or a {@link Parse.Cloud.ValidatorObject}.
*/
ParseCloud.define = function (functionName, handler, validationHandler) {
@@ -788,9 +815,22 @@ 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 {String} ip The IP address of the client making the request.
* @property {Object} headers The original HTTP headers for the request.
* @property {Object} log The current logger inside Parse Server.
* @property {String} functionName The name of the cloud function.
* @property {Object} context The context of the cloud function call.
* @property {Object} config The Parse Server config.
*/
/**
* @interface Parse.Cloud.FunctionResponse
* @property {function} success Call this function to return a successful response with an optional result. Usage: `response.success(result)`
* @property {function} error Call this function to return an error response with an error message. Usage: `response.error(message)`
* @property {function} status Call this function to set a custom HTTP status code for the response. Returns the response object for chaining. Usage: `response.status(code).success(result)` or `response.status(code).error(message)`
* @property {function} header Call this function to set a custom HTTP header for the response. Returns the response object for chaining. Usage: `response.header('X-Custom-Header', 'value').success(result)`
*/
/**
* @interface Parse.Cloud.JobRequest
* @property {Object} params The params passed to the background job.