2 Commits

Author SHA1 Message Date
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
6 changed files with 340 additions and 11 deletions

View File

@@ -1,3 +1,10 @@
# [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
View File

@@ -1,12 +1,12 @@
{
"name": "parse-server",
"version": "9.1.0-alpha.1",
"version": "9.1.0-alpha.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "parse-server",
"version": "9.1.0-alpha.1",
"version": "9.1.0-alpha.2",
"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.0-alpha.2",
"description": "An express module providing a Parse-compatible API server",
"main": "lib/index.js",
"repository": {

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

@@ -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.