botpress
Version:
The world's first CMS for bots. Easily create, manage and extend chatbots.
368 lines (288 loc) • 14 kB
JavaScript
;
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _path = require('path');
var _path2 = _interopRequireDefault(_path);
var _fs = require('fs');
var _fs2 = _interopRequireDefault(_fs);
var _bluebird = require('bluebird');
var _bluebird2 = _interopRequireDefault(_bluebird);
var _glob = require('glob');
var _glob2 = _interopRequireDefault(_glob);
var _get = require('lodash/get');
var _get2 = _interopRequireDefault(_get);
var _partition = require('lodash/partition');
var _partition2 = _interopRequireDefault(_partition);
var _mapValues = require('lodash/mapValues');
var _mapValues2 = _interopRequireDefault(_mapValues);
var _uniq = require('lodash/uniq');
var _uniq2 = _interopRequireDefault(_uniq);
var _transparent = require('./transparent');
var _transparent2 = _interopRequireDefault(_transparent);
var _util = require('./util');
var _util2 = require('../util');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new _bluebird2.default(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return _bluebird2.default.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
_bluebird2.default.promisifyAll(_fs2.default);
const globAsync = _bluebird2.default.promisify(_glob2.default);
const REVISIONS_FILE_NAME = '.ghost-revisions';
module.exports = ({ logger, db, projectLocation, enabled }) => {
if (!enabled) {
return (0, _transparent2.default)({ logger, projectLocation });
}
const normalizeFolder = (0, _util.normalizeFolder)(projectLocation);
const pendingRevisionsByFolder = {};
const trackedFolders = [];
const folderOptions = {};
const upsert = ({ knex, tableName, where, data, idField = 'id', trx = null }) => {
const prepareQuery = () => trx ? knex(tableName).transacting(trx) : knex(tableName);
return prepareQuery().where(where).select(idField).then(res => {
const id = (0, _get2.default)(res, '0.id');
return id ? prepareQuery().where(idField, id).update(data).thenReturn(id) : prepareQuery().insert(_extends({}, where, data), 'id').then(([insertedId]) => insertedId);
});
};
const recordFile = (() => {
var _ref = _asyncToGenerator(function* (folderPath, folder, file, { isBinary = false } = {}) {
const knex = yield db.get();
const filePath = _path2.default.join(folderPath, file);
const column = isBinary ? 'binary_content' : 'content';
yield _fs2.default.readFileAsync(filePath, isBinary ? null : 'utf8').then(function (content) {
return upsert({
knex,
tableName: 'ghost_content',
where: { folder, file },
data: { [column]: content }
});
});
});
return function recordFile(_x, _x2, _x3) {
return _ref.apply(this, arguments);
};
})();
const getPendingRevisions = (() => {
var _ref2 = _asyncToGenerator(function* (normalizedFolderName) {
const knex = yield db.get();
return knex('ghost_revisions').join('ghost_content', 'ghost_content.id', '=', 'ghost_revisions.content_id').where('ghost_content.folder', normalizedFolderName).select('ghost_content.file', 'ghost_revisions.id', 'ghost_revisions.revision', 'ghost_revisions.created_on', 'ghost_revisions.created_by').orderBy('ghost_revisions.created_on', 'desc').then();
});
return function getPendingRevisions(_x4) {
return _ref2.apply(this, arguments);
};
})();
const addRootFolder = (() => {
var _ref3 = _asyncToGenerator(function* (rootFolder, options = {}) {
const { folderPath, normalizedFolderName } = normalizeFolder(rootFolder);
const { isBinary = false, filesGlob = '**/*' } = options;
logger.debug(`[Ghost Content Manager] adding folder ${normalizedFolderName}`);
trackedFolders.push(normalizedFolderName);
folderOptions[normalizedFolderName] = options;
// read known revisions
const revisionsFile = _path2.default.join(folderPath, REVISIONS_FILE_NAME);
const fileRevisionsPromise = _fs2.default.readFileAsync(revisionsFile, 'utf8').catch({ code: 'ENOENT' }, function () {
return '';
}).then(function (content) {
return content.trim().split('\n').map(function (s) {
return s.trim();
}).filter(function (s) {
return !!s && !s.startsWith('#');
}).reduce(function (acc, r) {
acc[r] = true;
return acc;
}, {});
});
const [knownRevisions, dbRevisions] = yield _bluebird2.default.all([fileRevisionsPromise, getPendingRevisions(normalizedFolderName)]);
const [revisionsToDelete, remainingRevisions] = (0, _partition2.default)(dbRevisions, function ({ revision }) {
return knownRevisions[revision];
});
const knex = yield db.get();
// cleanup known revisions
if (revisionsToDelete.length) {
logger.debug(`[Ghost Content Manager] ${normalizedFolderName}: deleting ${revisionsToDelete.length} known revision(s).`);
yield knex('ghost_revisions').whereIn('id', revisionsToDelete.map(function ({ id }) {
return id;
})).del();
}
if (remainingRevisions.length) {
logger.debug(`[Ghost Content Manager] ${normalizedFolderName}: ${remainingRevisions.length} pending revision(s).`);
// record remaining revisions if any
pendingRevisionsByFolder[normalizedFolderName] = remainingRevisions;
return;
}
logger.debug(`[Ghost Content Manager] ${normalizedFolderName} has no pending revisions, updating DB from the file system.`);
// otherwise update the content in the DB
const files = yield globAsync(filesGlob, { cwd: folderPath });
yield _bluebird2.default.map(files, function (file) {
return recordFile(folderPath, normalizedFolderName, file, { isBinary });
});
// and also delete the files no longer in the FS
yield knex('ghost_content').whereNotIn('file', files).andWhere('folder', normalizedFolderName).del().then();
});
return function addRootFolder(_x5) {
return _ref3.apply(this, arguments);
};
})();
const updatePendingForFolder = (() => {
var _ref4 = _asyncToGenerator(function* (normalizedFolderName) {
pendingRevisionsByFolder[normalizedFolderName] = yield getPendingRevisions(normalizedFolderName);
if (!pendingRevisionsByFolder[normalizedFolderName].length) {
delete pendingRevisionsByFolder[normalizedFolderName];
}
});
return function updatePendingForFolder(_x6) {
return _ref4.apply(this, arguments);
};
})();
const updatePendingForAllFolders = () => _bluebird2.default.each(trackedFolders, updatePendingForFolder);
const recordRevision = (knex, content_id, trx) => knex('ghost_revisions').transacting(trx).insert({ content_id, revision: (0, _util2.safeId)(), created_by: 'admin' });
const upsertFile = (() => {
var _ref5 = _asyncToGenerator(function* (rootFolder, file, content) {
const knex = yield db.get();
const folder = normalizeFolder(rootFolder).normalizedFolderName;
const { isBinary } = folderOptions[folder];
const column = isBinary ? 'binary_content' : 'content';
if (yield knex('ghost_content').where({ folder, file, [column]: content }).count('id as count').then(function ([res]) {
return Number(res.count) > 0;
})) {
return _bluebird2.default.resolve();
}
return knex.transaction(function (trx) {
upsert({
knex,
tableName: 'ghost_content',
where: { folder, file },
data: { [column]: content },
trx
}).then(function (content_id) {
return recordRevision(knex, content_id, trx);
}).then(trx.commit).then(function () {
return updatePendingForFolder(folder);
}).catch(function (err) {
logger.error('[Ghost Content Manager]', err);
trx.rollback();
});
});
});
return function upsertFile(_x7, _x8, _x9) {
return _ref5.apply(this, arguments);
};
})();
const revertAllPendingChangesForFile = (() => {
var _ref6 = _asyncToGenerator(function* (folder, file) {
const knex = yield db.get();
const { folderPath, normalizedFolderName } = normalizeFolder(folder);
const filePath = _path2.default.join(folderPath, file);
const { isBinary = false } = folderOptions[normalizedFolderName];
yield knex('ghost_revisions').whereIn('id', function () {
// Subquery because SQLite doesn't support delete with joins
this.select('ghost_revisions.id').from('ghost_revisions').join('ghost_content', 'ghost_content.id', '=', 'ghost_revisions.content_id').where('folder', folder).andWhere('file', file);
}).del();
yield updatePendingForFolder(folder);
if (_fs2.default.existsSync(filePath)) {
// If the file exists on disk it means it was initially source controlled
recordFile(folderPath, folder, file, { isBinary });
} else {
yield knex('ghost_content').where('folder', folder).andWhere('file', file).del();
}
});
return function revertAllPendingChangesForFile(_x10, _x11) {
return _ref6.apply(this, arguments);
};
})();
const readFile = (() => {
var _ref7 = _asyncToGenerator(function* (rootFolder, file) {
const knex = yield db.get();
const { normalizedFolderName } = normalizeFolder(rootFolder);
const { isBinary } = folderOptions[normalizedFolderName] || {};
const column = isBinary ? 'binary_content' : 'content';
return knex('ghost_content').select(column).where({ folder: normalizedFolderName, file }).then(function (results) {
if (!results || !results.length) {
return null;
}
const result = results[0];
return result && result[column] || null;
});
});
return function readFile(_x12, _x13) {
return _ref7.apply(this, arguments);
};
})();
const deleteFile = (() => {
var _ref8 = _asyncToGenerator(function* (rootFolder, file) {
const knex = yield db.get();
const { normalizedFolderName } = normalizeFolder(rootFolder);
const id = (0, _get2.default)((yield knex('ghost_content').where({ folder: normalizedFolderName, file, deleted: 0 }).select('id')), '0.id');
if (!id) {
throw new Error(`Can't delete file: ${file}: couldn't find it in folder: ${normalizedFolderName}`);
}
return knex.transaction(function (trx) {
knex('ghost_content').transacting(trx).where({ id }).update({ deleted: 1, content: null, binary_content: null }).then(function () {
return recordRevision(knex, id, trx);
}).then(trx.commit).then(function () {
return updatePendingForFolder(normalizedFolderName);
}).catch(function (err) {
logger.error('[Ghost Content Manager]', err);
trx.rollback();
});
});
});
return function deleteFile(_x14, _x15) {
return _ref8.apply(this, arguments);
};
})();
const directoryListing = (() => {
var _ref9 = _asyncToGenerator(function* (rootFolder, fileEndingPattern = '', pathsToOmit = []) {
const knex = yield db.get();
const { normalizedFolderName } = normalizeFolder(rootFolder);
return knex('ghost_content').select('file').whereNotIn('file', pathsToOmit).andWhere({ folder: normalizedFolderName, deleted: 0 }).andWhere('file', 'like', `%${fileEndingPattern}`).then(function (res) {
return res.map(function (row) {
return row.file;
});
});
});
return function directoryListing(_x16) {
return _ref9.apply(this, arguments);
};
})();
const getPending = () => pendingRevisionsByFolder;
const getPendingWithContentForFolder = ({ stringifyBinary = false }) => (() => {
var _ref10 = _asyncToGenerator(function* (folderInfo, normalizedFolderName) {
const revisions = folderInfo.map(function ({ revision }) {
return revision;
});
const fileNames = (0, _uniq2.default)(folderInfo.map(function ({ file }) {
return file;
}));
const { isBinary } = folderOptions[normalizedFolderName];
const column = isBinary ? 'binary_content' : 'content';
const knex = yield db.get();
const files = yield knex('ghost_content').select('file', column, 'deleted').whereIn('file', fileNames).andWhere({ folder: normalizedFolderName });
if (isBinary) {
files.forEach(function (data) {
data.content = stringifyBinary ? data.binary_content.toString('base64') : data.binary_content;
delete data.binary_content;
});
}
return {
files,
revisions,
binary: isBinary
};
});
return function (_x17, _x18) {
return _ref10.apply(this, arguments);
};
})();
const getPendingWithContent = ({ stringifyBinary = false } = {}) => _bluebird2.default.props((0, _mapValues2.default)(pendingRevisionsByFolder, getPendingWithContentForFolder({ stringifyBinary })));
logger.info('[Ghost Content Manager] Initialized');
return {
addRootFolder,
upsertFile,
readFile,
deleteFile,
directoryListing,
getPending,
getPendingWithContent,
revertAllPendingChangesForFile
};
};
// TODO: switch to ES6 modules
module.exports.REVISIONS_FILE_NAME = REVISIONS_FILE_NAME;
//# sourceMappingURL=index.js.map