apostrophe
Version:
Apostrophe is a user-friendly content management system. You'll need more than this core module. See apostrophenow.org to get started.
228 lines (212 loc) • 9 kB
JavaScript
var _ = require('lodash');
var async = require('async');
/**
* mongodb
* @augments Augments the apos object with methods relating to
* MongoDB and the MongoDB collections that Apostrophe requires.
*/
module.exports = function(self) {
function checkVersion(callback) {
self.db.command({ 'serverStatus': 1 }, function(err, result) {
if (err) {
if (err.code === 13) {
// We don't have the privileges to know what
// version of MongoDB is running. Not a lot we
// can do but trust the user
return callback(null);
}
return callback(err);
}
var versions = result.version.split(/\./);
if ((versions[0] < 2) || ((versions[0] == 2) && (versions[1] < 6))) {
return callback(new Error('\n\nMONGODB TOO OLD: your server must be at least MongoDB 2.6.0.\nYou currently have: ' + result.version));
}
return callback(null);
});
}
function setupPages(callback) {
self.db.collection('aposPages', function(err, collection) {
function indexType(callback) {
self.pages.ensureIndex({ type: 1 }, { safe: true }, callback);
}
function indexSlug(callback) {
self.pages.ensureIndex({ slug: 1 }, { safe: true, unique: true }, callback);
}
function indexPath(callback) {
// Careful, non-tree pages will not have a path, must be a sparse index
self.pages.ensureIndex({ path: 1 }, { safe: true, unique: true, sparse: true }, callback);
}
// For typical getDescendants calls
function indexPathLevelRank(callback) {
self.pages.ensureIndex({ path: 1, level: 1, rank: 1 }, { safe: true }, callback);
}
function indexSortTitle(callback) {
self.pages.ensureIndex({ sortTitle: 1 }, { safe: true }, callback);
}
function indexStartDate(callback) {
self.pages.ensureIndex({ startDate: 1 }, { safe: true }, callback);
}
function indexStart(callback) {
self.pages.ensureIndex({ start: 1 }, { safe: true }, callback);
}
function indexStartAndSortTitle(callback) {
self.pages.ensureIndex({ start: 1, sortTitle: 1 }, { safe: true }, callback);
}
function indexEndDate(callback) {
self.pages.ensureIndex({ endDate: 1 }, { safe: true }, callback);
}
function indexEnd(callback) {
self.pages.ensureIndex({ end: 1 }, { safe: true }, callback);
}
function indexTags(callback) {
self.pages.ensureIndex({ tags: 1 }, { safe: true }, callback);
}
function indexPublished(callback) {
self.pages.ensureIndex({ published: 1 }, { safe: true }, callback);
}
function indexText(callback) {
return self.ensureTextIndex(function(err) {
if (err) {
console.error('WARNING: unable to ensure text index, apostrophe:migrate can fix that');
}
return callback(null);
});
}
// geo property is reserved for a geoJSON point
function indexGeo(callback) {
self.pages.ensureIndex({ geo: '2dsphere' }, { safe: true }, callback);
}
// Must be able to sort case insensitively on names
function indexSortName(callback) {
self.pages.ensureIndex({ sortLastName: 1, sortFirstName: 1 }, { safe: true }, callback);
}
self.pages = collection;
async.series([ indexType, indexSlug, indexPath, indexSortTitle, indexStartDate, indexStart, indexEndDate, indexStartAndSortTitle, indexEnd, indexTags, indexPublished, indexText, indexGeo, indexPathLevelRank, indexSortName ], callback);
// ... more index functions
});
}
// Each time a page or area is updated with putArea or putPage, a new version
// object is also created. Regardless of whether putArea or putPage is called,
// if the area is in the context of a page it is the entire page that is
// versioned. A pageId or areaId property is added, which is a non-unique index
// allowing us to fetch prior versions of any page or independently stored
// area. Also createdAt and author. Author is a string to avoid issues with
// references to deleted users.
//
// Note that this also provides full versioning for types built upon pages, such as
// blog posts and snippets.
function setupVersions(callback) {
self.db.collection('aposVersions', function(err, collection) {
function index(callback) {
self.versions.ensureIndex({ pageId: 1, createdAt: -1 }, { safe: true }, callback);
}
self.versions = collection;
async.series([index], callback);
// ... more index functions
});
}
function setupFiles(callback) {
self.db.collection('aposFiles', function(err, collection) {
self.files = collection;
return callback(err);
});
}
function setupVideos(callback) {
self.db.collection('aposVideos', function(err, collection) {
// Index the URLs
function videoIndex(callback) {
self.videos.ensureIndex({ video: 1 }, { safe: true }, callback);
}
self.videos = collection;
return async.series([videoIndex], callback);
});
}
function setupRedirects(callback) {
self.db.collection('aposRedirects', function(err, collection) {
self.redirects = collection;
collection.ensureIndex({ from: 1 }, { safe: true, unique: true }, function(err) {
return callback(err);
});
});
}
function setupCache(callback) {
return self.db.collection('aposCache', function(err, collection) {
self._cache = collection;
return async.series({
keyIndex: function(callback) {
return self._cache.ensureIndex({ key: 1, cache: 1 }, { safe: true, unique: true }, callback);
},
expireIndex: function(callback) {
return self._cache.ensureIndex({ updated: 1 }, { expireAfterSeconds: 0 }, callback);
}
}, callback);
});
}
// This collection is a home for tags that were created
// globally via the tag manager. To allow the same code to
// be used to scan it, it has the same structure as
// any other taggable collection, so each entry has a
// tags array property with one tag in it. And that's all.
function setupAllowedTags(callback) {
return self.db.collection('aposAllowedTags', function(err, collection) {
self.allowedTags = collection;
return callback(err);
});
}
/**
* Ensure the MongoDB collections required by Apostrophe are available in the expected
* properties (`apos.pages`, `apos.videos`, `apos.files`, `apos.versions`, `apos.redirects`). Also sets up indexes and checks version of mongodb.
*/
self.initCollections = function(callback) {
return async.series([checkVersion, setupPages, setupVersions, setupFiles, setupVideos, setupRedirects, setupCache, setupAllowedTags ], callback);
};
self.ensureTextIndex = function(callback) {
return self.pages.ensureIndex( { highSearchText: 'text', lowSearchText: 'text', title: 'text', searchBoost: 'text' }, { weights: { title: 100, searchBoost: 150, highSearchText: 10, lowSearchText: 2 }, safe: true }, callback);
};
// Is this MongoDB error related to uniqueness? Great for retrying on duplicates.
// Used heavily by the pages module and no doubt will be by other things.
//
// There are three error codes for this: 13596 ("cannot change _id of a document")
// and 11000 and 11001 which specifically relate to the uniqueness of an index.
// 13596 can arise on an upsert operation, especially when the _id is assigned
// by the caller rather than by MongoDB.
//
// IMPORTANT: you are responsible for making sure ALL of your unique indexes
// are accounted for before retrying... otherwise an infinite loop will
// likely result.
self.isUniqueError = function(err) {
if (!err) {
return false;
}
if (err.code === 13596) {
return true;
}
return ((err.code === 13596) || (err.code === 11000) || (err.code === 11001));
};
// `ids` should be an array of mongodb IDs. The elements of the `items` array, which
// should be the result of a mongodb query, are returned in the order specified by `ids`.
// This is useful after performing an `$in` query with MongoDB (note that `$in` does NOT sort its
// results in the order given).
//
// Any IDs that do not actually exist for an item in the `items` array are not returned,
// and vice versa. You should not assume the result will have the same length as
// either array.
//
// Optionally you may specify a property name other than _id as the third argument.
self.orderById = function(ids, items, idProperty) {
if (idProperty === undefined) {
idProperty = '_id';
}
var byId = {};
_.each(items, function(item) {
byId[item[idProperty]] = item;
});
items = [];
_.each(ids, function(_id) {
if (byId.hasOwnProperty(_id)) {
items.push(byId[_id]);
}
});
return items;
};
};