Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4788,4 +4788,163 @@ 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();
});
});
});
61 changes: 54 additions & 7 deletions src/Routers/FunctionsRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,20 +103,44 @@ export class FunctionsRouter extends PromiseRouter {
});
}

static createResponseObject(resolve, reject) {
return {
static createResponseObject(resolve, reject, statusCode = null) {
let httpStatusCode = statusCode;
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;
}
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;
},
_isResponseSent: () => responseSent,
};
return responseObject;
}
static handleCloudFunction(req) {
const functionName = req.params.functionName;
Expand All @@ -143,7 +167,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') {
Expand Down Expand Up @@ -184,14 +208,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);
});
}
}
37 changes: 36 additions & 1 deletion src/cloud-code/Parse.Cloud.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,22 +107,45 @@ 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('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) {
Expand Down Expand Up @@ -788,9 +811,21 @@ 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)`
*/

/**
* @interface Parse.Cloud.JobRequest
* @property {Object} params The params passed to the background job.
Expand Down
Loading