@staticcms/proxy-server-lite
Version:
Proxy server to be used with Static CMS proxy backend
372 lines • 18.4 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerMiddleware = exports.localGitMiddleware = exports.getSchema = exports.validateRepo = void 0;
const path_1 = __importDefault(require("path"));
const fs_1 = require("fs");
const what_the_diff_1 = require("what-the-diff");
// eslint-disable-next-line import/no-named-as-default
const simple_git_1 = __importDefault(require("simple-git"));
const async_mutex_1 = require("async-mutex");
const joi_1 = require("../joi");
const customValidators_1 = require("../joi/customValidators");
const fs_2 = require("../utils/fs");
const entries_1 = require("../utils/entries");
const APIUtils_1 = require("../utils/APIUtils");
async function commit(git, commitMessage) {
await git.add('.');
await git.commit(commitMessage, undefined, {
// setting the value to a string passes name=value
// any other value passes just the key
'--no-verify': null,
'--no-gpg-sign': null,
});
}
async function getCurrentBranch(git) {
const currentBranch = await git.branchLocal().then(summary => summary.current);
return currentBranch;
}
async function runOnBranch(git, branch, func) {
const currentBranch = await getCurrentBranch(git);
try {
if (currentBranch !== branch) {
await git.checkout(branch);
}
const result = await func();
return result;
}
finally {
await git.checkout(currentBranch);
}
}
function branchDescription(branch) {
return `branch.${branch}.description`;
}
async function commitEntry(git, repoPath, dataFiles, assets, commitMessage) {
// save entry content
await Promise.all(dataFiles.map(dataFile => (0, fs_2.writeFile)(path_1.default.join(repoPath, dataFile.path), dataFile.raw)));
// save assets
await Promise.all(assets.map(a => (0, fs_2.writeFile)(path_1.default.join(repoPath, a.path), Buffer.from(a.content, a.encoding))));
if (dataFiles.every(dataFile => dataFile.newPath)) {
dataFiles.forEach(async (dataFile) => {
await (0, fs_2.move)(path_1.default.join(repoPath, dataFile.path), path_1.default.join(repoPath, dataFile.newPath));
});
}
// commits files
await commit(git, commitMessage);
}
async function rebase(git, branch) {
const gpgSign = await git.raw(['config', 'commit.gpgsign']);
try {
if (gpgSign === 'true') {
await git.addConfig('commit.gpgsign', 'false');
}
await git.rebase([branch, '--no-verify']);
}
finally {
if (gpgSign === 'true') {
await git.addConfig('commit.gpgsign', gpgSign);
}
}
}
async function merge(git, from, to) {
const gpgSign = await git.raw(['config', 'commit.gpgsign']);
try {
if (gpgSign === 'true') {
await git.addConfig('commit.gpgsign', 'false');
}
await git.mergeFromTo(from, to);
}
finally {
if (gpgSign === 'true') {
await git.addConfig('commit.gpgsign', gpgSign);
}
}
}
async function isBranchExists(git, branch) {
const branchExists = await git.branchLocal().then(({ all }) => all.includes(branch));
return branchExists;
}
async function getDiffs(git, source, dest) {
const rawDiff = await git.diff([source, dest]);
const diffs = (0, what_the_diff_1.parse)(rawDiff).map(d => {
var _a, _b;
const oldPath = ((_a = d.oldPath) === null || _a === void 0 ? void 0 : _a.replace(/b\//, '')) || '';
const newPath = ((_b = d.newPath) === null || _b === void 0 ? void 0 : _b.replace(/b\//, '')) || '';
const path = newPath || oldPath;
return {
oldPath,
newPath,
status: d.status,
newFile: d.status === 'added',
path,
id: path,
binary: d.binary || /.svg$/.test(path),
};
});
return diffs;
}
async function validateRepo({ repoPath }) {
const git = (0, simple_git_1.default)(repoPath);
const isRepo = await git.checkIsRepo();
if (!isRepo) {
throw Error(`${repoPath} is not a valid git repository`);
}
}
exports.validateRepo = validateRepo;
function getSchema({ repoPath }) {
const schema = (0, joi_1.defaultSchema)({ path: (0, customValidators_1.pathTraversal)(repoPath) });
return schema;
}
exports.getSchema = getSchema;
function localGitMiddleware({ repoPath, logger }) {
const git = (0, simple_git_1.default)(repoPath);
// we can only perform a single git operation at any given time
const mutex = (0, async_mutex_1.withTimeout)(new async_mutex_1.Mutex(), 3000, new Error('Request timed out'));
return async function (req, res) {
let release;
try {
release = await mutex.acquire();
const { body } = req;
if (body.action === 'info') {
res.json({
repo: path_1.default.basename(repoPath),
publish_modes: ['simple', 'editorial_workflow'],
type: 'local_git',
});
return;
}
const { branch } = body.params;
const branchExists = await isBranchExists(git, branch);
if (!branchExists) {
const message = `Default branch '${branch}' doesn't exist`;
res.status(422).json({ error: message });
return;
}
switch (body.action) {
case 'entriesByFolder': {
const payload = body.params;
const { folder, extension, depth } = payload;
const entries = await runOnBranch(git, branch, () => (0, fs_2.listRepoFiles)(repoPath, folder, extension, depth).then(files => (0, entries_1.entriesFromFiles)(repoPath, files.map(file => ({ path: file.file })))));
res.json(entries);
break;
}
case 'entriesByFiles': {
const payload = body.params;
const entries = await runOnBranch(git, branch, () => (0, entries_1.entriesFromFiles)(repoPath, payload.files));
res.json(entries);
break;
}
case 'getEntry': {
const payload = body.params;
const [entry] = await runOnBranch(git, branch, () => (0, entries_1.entriesFromFiles)(repoPath, [{ path: payload.path }]));
res.json(entry);
break;
}
case 'unpublishedEntries': {
const cmsBranches = await git
.branchLocal()
.then(result => result.all.filter(b => b.startsWith(`${APIUtils_1.CMS_BRANCH_PREFIX}/`)));
res.json(cmsBranches.map(APIUtils_1.contentKeyFromBranch));
break;
}
case 'unpublishedEntry': {
let { id, collection, slug, cmsLabelPrefix } = body.params;
if (id) {
({ collection, slug } = (0, APIUtils_1.parseContentKey)(id));
}
const contentKey = (0, APIUtils_1.generateContentKey)(collection, slug);
const cmsBranch = (0, APIUtils_1.branchFromContentKey)(contentKey);
const branchExists = await isBranchExists(git, cmsBranch);
if (branchExists) {
const diffs = await getDiffs(git, branch, cmsBranch);
const label = await git.raw(['config', branchDescription(cmsBranch)]);
const status = label && (0, APIUtils_1.labelToStatus)(label.trim(), cmsLabelPrefix || '');
const updatedAt = diffs.length >= 0
? await runOnBranch(git, cmsBranch, async () => {
const dates = await Promise.all(diffs.map(({ newPath }) => (0, fs_2.getUpdateDate)(repoPath, newPath)));
return dates.reduce((a, b) => {
return a > b ? a : b;
});
})
: new Date();
const unpublishedEntry = {
collection,
slug,
status,
diffs,
updatedAt,
};
res.json(unpublishedEntry);
}
else {
return res.status(404).json({ message: 'Not Found' });
}
break;
}
case 'unpublishedEntryDataFile': {
const { path, collection, slug } = body.params;
const contentKey = (0, APIUtils_1.generateContentKey)(collection, slug);
const cmsBranch = (0, APIUtils_1.branchFromContentKey)(contentKey);
const [entry] = await runOnBranch(git, cmsBranch, () => (0, entries_1.entriesFromFiles)(repoPath, [{ path }]));
res.json({ data: entry.data });
break;
}
case 'unpublishedEntryMediaFile': {
const { path, collection, slug } = body.params;
const contentKey = (0, APIUtils_1.generateContentKey)(collection, slug);
const cmsBranch = (0, APIUtils_1.branchFromContentKey)(contentKey);
const file = await runOnBranch(git, cmsBranch, () => (0, entries_1.readMediaFile)(repoPath, path));
res.json(file);
break;
}
case 'deleteUnpublishedEntry': {
const { collection, slug } = body.params;
const contentKey = (0, APIUtils_1.generateContentKey)(collection, slug);
const cmsBranch = (0, APIUtils_1.branchFromContentKey)(contentKey);
const currentBranch = await getCurrentBranch(git);
if (currentBranch === cmsBranch) {
await git.checkoutLocalBranch(branch);
}
await git.branch(['-D', cmsBranch]);
res.json({ message: `deleted branch: ${cmsBranch}` });
break;
}
case 'persistEntry': {
const { cmsLabelPrefix, entry, dataFiles = [entry], assets, options, } = body.params;
if (!options.useWorkflow) {
await runOnBranch(git, branch, async () => {
await commitEntry(git, repoPath, dataFiles, assets, options.commitMessage);
});
}
else {
const slug = dataFiles[0].slug;
const collection = options.collectionName;
const contentKey = (0, APIUtils_1.generateContentKey)(collection, slug);
const cmsBranch = (0, APIUtils_1.branchFromContentKey)(contentKey);
await runOnBranch(git, branch, async () => {
var _a;
const branchExists = await isBranchExists(git, cmsBranch);
if (branchExists) {
await git.checkout(cmsBranch);
}
else {
await git.checkoutLocalBranch(cmsBranch);
}
await rebase(git, branch);
const diffs = await getDiffs(git, branch, cmsBranch);
// delete media files that have been removed from the entry
const toDelete = diffs.filter(d => d.binary && !assets.map(a => a.path).includes(d.path));
await Promise.all(toDelete.map(f => fs_1.promises.unlink(path_1.default.join(repoPath, f.path))));
await commitEntry(git, repoPath, dataFiles, assets, options.commitMessage);
// add status for new entries
if (!branchExists) {
const description = (0, APIUtils_1.statusToLabel)((_a = options.status) !== null && _a !== void 0 ? _a : 'draft', cmsLabelPrefix || '');
await git.addConfig(branchDescription(cmsBranch), description);
}
});
}
res.json({ message: 'entry persisted' });
break;
}
case 'updateUnpublishedEntryStatus': {
const { collection, slug, newStatus, cmsLabelPrefix } = body.params;
const contentKey = (0, APIUtils_1.generateContentKey)(collection, slug);
const cmsBranch = (0, APIUtils_1.branchFromContentKey)(contentKey);
const description = (0, APIUtils_1.statusToLabel)(newStatus, cmsLabelPrefix || '');
await git.addConfig(branchDescription(cmsBranch), description);
res.json({ message: `${branch} description was updated to ${description}` });
break;
}
case 'publishUnpublishedEntry': {
const { collection, slug } = body.params;
const contentKey = (0, APIUtils_1.generateContentKey)(collection, slug);
const cmsBranch = (0, APIUtils_1.branchFromContentKey)(contentKey);
await merge(git, cmsBranch, branch);
await git.deleteLocalBranch(cmsBranch);
res.json({ message: `branch ${cmsBranch} merged to ${branch}` });
break;
}
case 'getMedia': {
const { mediaFolder, publicFolder = mediaFolder } = body.params;
const fsItems = await runOnBranch(git, branch, async () => {
return await (0, fs_2.listRepoFiles)(repoPath, mediaFolder, '', 1);
});
res.json(fsItems.map(item => ({
path: item.file.replace(/\\/g, '/'),
url: item.file
.replace(/\\/g, '/')
.replace(mediaFolder.replace(/^\//g, ''), publicFolder),
isDirectory: item.isDirectory,
})));
break;
}
case 'getMediaFile': {
const { path } = body.params;
const mediaFile = await runOnBranch(git, branch, () => {
return (0, entries_1.readMediaFile)(repoPath, path);
});
res.json(mediaFile);
break;
}
case 'persistMedia': {
const { asset, options: { commitMessage }, } = body.params;
const file = await runOnBranch(git, branch, async () => {
await (0, fs_2.writeFile)(path_1.default.join(repoPath, asset.path), Buffer.from(asset.content, asset.encoding));
await commit(git, commitMessage);
return (0, entries_1.readMediaFile)(repoPath, asset.path);
});
res.json(file);
break;
}
case 'deleteFile': {
const { path: filePath, options: { commitMessage }, } = body.params;
await runOnBranch(git, branch, async () => {
await (0, fs_2.deleteFile)(repoPath, filePath);
await commit(git, commitMessage);
});
res.json({ message: `deleted file ${filePath}` });
break;
}
case 'deleteFiles': {
const { paths, options: { commitMessage }, } = body.params;
await runOnBranch(git, branch, async () => {
await Promise.all(paths.map(filePath => (0, fs_2.deleteFile)(repoPath, filePath)));
await commit(git, commitMessage);
});
res.json({ message: `deleted files ${paths.join(', ')}` });
break;
}
case 'getDeployPreview': {
res.json(null);
break;
}
default: {
const message = `Unknown action ${body.action}`;
res.status(422).json({ error: message });
break;
}
}
}
catch (e) {
logger.error(`Error handling ${JSON.stringify(req.body)}: ${e instanceof Error ? e.message : 'Unknown error'}`);
res.status(500).json({ error: 'Unknown error' });
}
finally {
release && release();
}
};
}
exports.localGitMiddleware = localGitMiddleware;
async function registerMiddleware(app, options) {
const { logger } = options;
const repoPath = path_1.default.resolve(process.env.GIT_REPO_DIRECTORY || process.cwd());
await validateRepo({ repoPath });
app.post('/api/v1', (0, joi_1.joi)(getSchema({ repoPath })));
app.post('/api/v1', localGitMiddleware({ repoPath, logger }));
logger.info(`Static CMS Git Proxy Server configured with ${repoPath}`);
}
exports.registerMiddleware = registerMiddleware;
//# sourceMappingURL=index.js.map