Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
227 changes: 227 additions & 0 deletions spec/CloudCode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
69 changes: 62 additions & 7 deletions src/Routers/FunctionsRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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') {
Expand Down Expand Up @@ -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);
});
}
}
Loading
Loading