Compare commits
6 Commits
9.1.0-alph
...
9.1.0-alph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e308feaa7 | ||
|
|
a23b192466 | ||
|
|
98a42e5277 | ||
|
|
3074eb70f5 | ||
|
|
7028e0385c | ||
|
|
8eeab8dc57 |
@@ -1,3 +1,24 @@
|
||||
# [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)
|
||||
|
||||
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "9.1.0-alpha.1",
|
||||
"version": "9.1.0-alpha.4",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "parse-server",
|
||||
"version": "9.1.0-alpha.1",
|
||||
"version": "9.1.0-alpha.4",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "parse-server",
|
||||
"version": "9.1.0-alpha.1",
|
||||
"version": "9.1.0-alpha.4",
|
||||
"description": "An express module providing a Parse-compatible API server",
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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('"><script>');
|
||||
});
|
||||
|
||||
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('<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('"><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'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 & Co');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user