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.
152 lines
4.8 KiB
JavaScript
152 lines
4.8 KiB
JavaScript
const Parse = require('parse/node').Parse;
|
|
const path = require('path');
|
|
// These methods handle batch requests.
|
|
const batchPath = '/batch';
|
|
|
|
// Mounts a batch-handler onto a PromiseRouter.
|
|
function mountOnto(router) {
|
|
router.route('POST', batchPath, req => {
|
|
return handleBatch(router, req);
|
|
});
|
|
}
|
|
|
|
function parseURL(urlString) {
|
|
try {
|
|
return new URL(urlString);
|
|
} catch (error) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function makeBatchRoutingPathFunction(originalUrl, serverURL, publicServerURL) {
|
|
serverURL = serverURL ? parseURL(serverURL) : undefined;
|
|
publicServerURL = publicServerURL ? parseURL(publicServerURL) : undefined;
|
|
|
|
const apiPrefixLength = originalUrl.length - batchPath.length;
|
|
let apiPrefix = originalUrl.slice(0, apiPrefixLength);
|
|
|
|
const makeRoutablePath = function (requestPath) {
|
|
// The routablePath is the path minus the api prefix
|
|
if (requestPath.slice(0, apiPrefix.length) != apiPrefix) {
|
|
throw new Parse.Error(Parse.Error.INVALID_JSON, 'cannot route batch path ' + requestPath);
|
|
}
|
|
return path.posix.join('/', requestPath.slice(apiPrefix.length));
|
|
};
|
|
|
|
if (serverURL && publicServerURL && serverURL.pathname != publicServerURL.pathname) {
|
|
const localPath = serverURL.pathname;
|
|
const publicPath = publicServerURL.pathname;
|
|
|
|
// Override the api prefix
|
|
apiPrefix = localPath;
|
|
return function (requestPath) {
|
|
// Figure out which server url was used by figuring out which
|
|
// path more closely matches requestPath
|
|
const startsWithLocal = requestPath.startsWith(localPath);
|
|
const startsWithPublic = requestPath.startsWith(publicPath);
|
|
const pathLengthToUse =
|
|
startsWithLocal && startsWithPublic
|
|
? Math.max(localPath.length, publicPath.length)
|
|
: startsWithLocal
|
|
? localPath.length
|
|
: publicPath.length;
|
|
|
|
const newPath = path.posix.join('/', localPath, '/', requestPath.slice(pathLengthToUse));
|
|
|
|
// Use the method for local routing
|
|
return makeRoutablePath(newPath);
|
|
};
|
|
}
|
|
|
|
return makeRoutablePath;
|
|
}
|
|
|
|
// Returns a promise for a {response} object.
|
|
// TODO: pass along auth correctly
|
|
function handleBatch(router, req) {
|
|
if (!Array.isArray(req.body?.requests)) {
|
|
throw new Parse.Error(Parse.Error.INVALID_JSON, 'requests must be an array');
|
|
}
|
|
|
|
// The batch paths are all from the root of our domain.
|
|
// That means they include the API prefix, that the API is mounted
|
|
// to. However, our promise router does not route the api prefix. So
|
|
// we need to figure out the API prefix, so that we can strip it
|
|
// from all the subrequests.
|
|
if (!req.originalUrl.endsWith(batchPath)) {
|
|
throw 'internal routing problem - expected url to end with batch';
|
|
}
|
|
|
|
const makeRoutablePath = makeBatchRoutingPathFunction(
|
|
req.originalUrl,
|
|
req.config.serverURL,
|
|
req.config.publicServerURL
|
|
);
|
|
|
|
const batch = transactionRetries => {
|
|
let initialPromise = Promise.resolve();
|
|
if (req.body?.transaction === true) {
|
|
initialPromise = req.config.database.createTransactionalSession();
|
|
}
|
|
|
|
return initialPromise.then(() => {
|
|
const promises = req.body?.requests.map(restRequest => {
|
|
const routablePath = makeRoutablePath(restRequest.path);
|
|
|
|
// Construct a request that we can send to a handler
|
|
const request = {
|
|
body: restRequest.body,
|
|
config: req.config,
|
|
auth: req.auth,
|
|
info: req.info,
|
|
};
|
|
|
|
return router.tryRouteRequest(restRequest.method, routablePath, request).then(
|
|
response => {
|
|
return { success: response.response };
|
|
},
|
|
error => {
|
|
return { error: { code: error.code, error: error.message } };
|
|
}
|
|
);
|
|
});
|
|
|
|
return Promise.all(promises)
|
|
.then(results => {
|
|
if (req.body?.transaction === true) {
|
|
if (results.find(result => typeof result.error === 'object')) {
|
|
return req.config.database.abortTransactionalSession().then(() => {
|
|
return Promise.reject({ response: results });
|
|
});
|
|
} else {
|
|
return req.config.database.commitTransactionalSession().then(() => {
|
|
return { response: results };
|
|
});
|
|
}
|
|
} else {
|
|
return { response: results };
|
|
}
|
|
})
|
|
.catch(error => {
|
|
if (
|
|
error &&
|
|
error.response &&
|
|
error.response.find(
|
|
errorItem => typeof errorItem.error === 'object' && errorItem.error.code === 251
|
|
) &&
|
|
transactionRetries > 0
|
|
) {
|
|
return batch(transactionRetries - 1);
|
|
}
|
|
throw error;
|
|
});
|
|
});
|
|
};
|
|
return batch(5);
|
|
}
|
|
|
|
module.exports = {
|
|
mountOnto,
|
|
makeBatchRoutingPathFunction,
|
|
};
|