BREAKING CHANGE: This upgrades the internally used Express framework from version 4 to 5, which may be a breaking change. If Parse Server is set up to be mounted on an Express application, we recommend to also use version 5 of the Express framework to avoid any compatibility issues. Note that even if there are no issues after upgrading, future releases of Parse Server may introduce issues if Parse Server internally relies on Express 5-specific features which are unsupported by the Express version on which it is mounted. See the Express [migration guide](https://expressjs.com/en/guide/migrating-5.html) and [release announcement](https://expressjs.com/2024/10/15/v5-release.html#breaking-changes) for more info.
196 lines
5.9 KiB
JavaScript
196 lines
5.9 KiB
JavaScript
// FunctionsRouter.js
|
|
|
|
var Parse = require('parse/node').Parse,
|
|
triggers = require('../triggers');
|
|
|
|
import PromiseRouter from '../PromiseRouter';
|
|
import { promiseEnforceMasterKeyAccess, promiseEnsureIdempotency } from '../middlewares';
|
|
import { jobStatusHandler } from '../StatusHandler';
|
|
import _ from 'lodash';
|
|
import { logger } from '../logger';
|
|
|
|
function parseObject(obj, config) {
|
|
if (Array.isArray(obj)) {
|
|
return obj.map(item => {
|
|
return parseObject(item, config);
|
|
});
|
|
} else if (obj && obj.__type == 'Date') {
|
|
return Object.assign(new Date(obj.iso), obj);
|
|
} else if (obj && obj.__type == 'File') {
|
|
return Parse.File.fromJSON(obj);
|
|
} else if (obj && obj.__type == 'Pointer' && config.encodeParseObjectInCloudFunction) {
|
|
return Parse.Object.fromJSON({
|
|
__type: 'Pointer',
|
|
className: obj.className,
|
|
objectId: obj.objectId,
|
|
});
|
|
} else if (obj && typeof obj === 'object') {
|
|
return parseParams(obj, config);
|
|
} else {
|
|
return obj;
|
|
}
|
|
}
|
|
|
|
function parseParams(params, config) {
|
|
return _.mapValues(params, item => parseObject(item, config));
|
|
}
|
|
|
|
export class FunctionsRouter extends PromiseRouter {
|
|
mountRoutes() {
|
|
this.route(
|
|
'POST',
|
|
'/functions/:functionName',
|
|
promiseEnsureIdempotency,
|
|
FunctionsRouter.handleCloudFunction
|
|
);
|
|
this.route(
|
|
'POST',
|
|
'/jobs/:jobName',
|
|
promiseEnsureIdempotency,
|
|
promiseEnforceMasterKeyAccess,
|
|
function (req) {
|
|
return FunctionsRouter.handleCloudJob(req);
|
|
}
|
|
);
|
|
this.route('POST', '/jobs', promiseEnforceMasterKeyAccess, function (req) {
|
|
return FunctionsRouter.handleCloudJob(req);
|
|
});
|
|
}
|
|
|
|
static handleCloudJob(req) {
|
|
const jobName = req.params.jobName || req.body?.jobName;
|
|
const applicationId = req.config.applicationId;
|
|
const jobHandler = jobStatusHandler(req.config);
|
|
const jobFunction = triggers.getJob(jobName, applicationId);
|
|
if (!jobFunction) {
|
|
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid job.');
|
|
}
|
|
let params = Object.assign({}, req.body, req.query);
|
|
params = parseParams(params, req.config);
|
|
const request = {
|
|
params: params,
|
|
log: req.config.loggerController,
|
|
headers: req.config.headers,
|
|
ip: req.config.ip,
|
|
jobName,
|
|
message: jobHandler.setMessage.bind(jobHandler),
|
|
};
|
|
|
|
return jobHandler.setRunning(jobName, params).then(jobStatus => {
|
|
request.jobId = jobStatus.objectId;
|
|
// run the function async
|
|
process.nextTick(() => {
|
|
Promise.resolve()
|
|
.then(() => {
|
|
return jobFunction(request);
|
|
})
|
|
.then(
|
|
result => {
|
|
jobHandler.setSucceeded(result);
|
|
},
|
|
error => {
|
|
jobHandler.setFailed(error);
|
|
}
|
|
);
|
|
});
|
|
return {
|
|
headers: {
|
|
'X-Parse-Job-Status-Id': jobStatus.objectId,
|
|
},
|
|
response: {},
|
|
};
|
|
});
|
|
}
|
|
|
|
static createResponseObject(resolve, reject) {
|
|
return {
|
|
success: function (result) {
|
|
resolve({
|
|
response: {
|
|
result: Parse._encode(result),
|
|
},
|
|
});
|
|
},
|
|
error: function (message) {
|
|
const error = triggers.resolveError(message);
|
|
reject(error);
|
|
},
|
|
};
|
|
}
|
|
static handleCloudFunction(req) {
|
|
const functionName = req.params.functionName;
|
|
const applicationId = req.config.applicationId;
|
|
const theFunction = triggers.getFunction(functionName, applicationId);
|
|
|
|
if (!theFunction) {
|
|
throw new Parse.Error(Parse.Error.SCRIPT_FAILED, `Invalid function: "${functionName}"`);
|
|
}
|
|
let params = Object.assign({}, req.body, req.query);
|
|
params = parseParams(params, req.config);
|
|
const request = {
|
|
params: params,
|
|
master: req.auth && req.auth.isMaster,
|
|
user: req.auth && req.auth.user,
|
|
installationId: req.info.installationId,
|
|
log: req.config.loggerController,
|
|
headers: req.config.headers,
|
|
ip: req.config.ip,
|
|
functionName,
|
|
context: req.info.context,
|
|
};
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
const userString = req.auth && req.auth.user ? req.auth.user.id : undefined;
|
|
const { success, error } = FunctionsRouter.createResponseObject(
|
|
result => {
|
|
try {
|
|
if (req.config.logLevels.cloudFunctionSuccess !== 'silent') {
|
|
const cleanInput = logger.truncateLogMessage(JSON.stringify(params));
|
|
const cleanResult = logger.truncateLogMessage(JSON.stringify(result.response.result));
|
|
logger[req.config.logLevels.cloudFunctionSuccess](
|
|
`Ran cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Result: ${cleanResult}`,
|
|
{
|
|
functionName,
|
|
params,
|
|
user: userString,
|
|
}
|
|
);
|
|
}
|
|
resolve(result);
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
},
|
|
error => {
|
|
try {
|
|
if (req.config.logLevels.cloudFunctionError !== 'silent') {
|
|
const cleanInput = logger.truncateLogMessage(JSON.stringify(params));
|
|
logger[req.config.logLevels.cloudFunctionError](
|
|
`Failed running cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Error: ` +
|
|
JSON.stringify(error),
|
|
{
|
|
functionName,
|
|
error,
|
|
params,
|
|
user: userString,
|
|
}
|
|
);
|
|
}
|
|
reject(error);
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
}
|
|
);
|
|
return Promise.resolve()
|
|
.then(() => {
|
|
return triggers.maybeRunValidator(request, functionName, req.auth);
|
|
})
|
|
.then(() => {
|
|
return theFunction(request);
|
|
})
|
|
.then(success, error);
|
|
});
|
|
}
|
|
}
|