UNPKG

botpress

Version:

The world's first CMS for bots. Easily create, manage and extend chatbots.

368 lines (288 loc) 14 kB
'use strict'; 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