2
1
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2023-12-13 21:00:40 +01:00

Subscribers: Model, API & CSV import/export

- subscriber model
- subscriber app updates
- subscriber end points
- import/export CSV
- added headers to export file
- added dynamic email field detection for import
- returns stats object after CSV import
- mask error message from DB
This commit is contained in:
Sebastian Gierlinger 2016-04-14 22:44:05 +02:00 committed by Hannah Wolfe
parent 4ca0c67f9c
commit 01ae7ae49f
17 changed files with 690 additions and 18 deletions

View file

@ -5,6 +5,7 @@
// from a theme, an app, or from an external app, you'll use the Ghost JSON API to do so.
var _ = require('lodash'),
Promise = require('bluebird'),
config = require('../config'),
// Include Endpoints
configuration = require('./configuration'),
@ -19,6 +20,7 @@ var _ = require('lodash'),
themes = require('./themes'),
users = require('./users'),
slugs = require('./slugs'),
subscribers = require('./subscribers'),
authentication = require('./authentication'),
uploads = require('./upload'),
exporter = require('../data/export'),
@ -28,7 +30,8 @@ var _ = require('lodash'),
addHeaders,
cacheInvalidationHeader,
locationHeader,
contentDispositionHeader,
contentDispositionHeaderExport,
contentDispositionHeaderSubscribers,
init;
/**
@ -138,12 +141,18 @@ locationHeader = function locationHeader(req, result) {
* @see http://tools.ietf.org/html/rfc598
* @return {string}
*/
contentDispositionHeader = function contentDispositionHeader() {
contentDispositionHeaderExport = function contentDispositionHeaderExport() {
return exporter.fileName().then(function then(filename) {
return 'Attachment; filename="' + filename + '"';
});
};
contentDispositionHeaderSubscribers = function contentDispositionHeaderSubscribers() {
var datetime = (new Date()).toJSON().substring(0, 10);
return Promise.resolve('Attachment; filename="subscribers.' + datetime + '.csv"');
};
addHeaders = function addHeaders(apiMethod, req, res, result) {
var cacheInvalidation,
location,
@ -164,15 +173,24 @@ addHeaders = function addHeaders(apiMethod, req, res, result) {
}
}
// Add Export Content-Disposition Header
if (apiMethod === db.exportContent) {
contentDisposition = contentDispositionHeader()
.then(function addContentDispositionHeader(header) {
// Add Content-Disposition Header
if (apiMethod === db.exportContent) {
res.set({
'Content-Disposition': header
});
}
contentDisposition = contentDispositionHeaderExport()
.then(function addContentDispositionHeaderExport(header) {
res.set({
'Content-Disposition': header
});
});
}
// Add Subscribers Content-Disposition Header
if (apiMethod === subscribers.exportCSV) {
contentDisposition = contentDispositionHeaderSubscribers()
.then(function addContentDispositionHeaderSubscribers(header) {
res.set({
'Content-Disposition': header,
'Content-Type': 'text/csv'
});
});
}
@ -195,7 +213,7 @@ http = function http(apiMethod) {
var object = req.body,
options = _.extend({}, req.file, req.query, req.params, {
context: {
user: (req.user && req.user.id) ? req.user.id : null
user: ((req.user && req.user.id) || (req.user && req.user.id === 0)) ? req.user.id : null
}
});
@ -213,7 +231,10 @@ http = function http(apiMethod) {
if (req.method === 'DELETE') {
return res.status(204).end();
}
// Keep CSV header and formatting
if (res.get('Content-Type') && res.get('Content-Type').indexOf('text/csv') === 0) {
return res.status(200).send(response);
}
// Send a properly formatting HTTP response containing the data with correct headers
res.json(response || {});
}).catch(function onAPIError(error) {
@ -243,6 +264,7 @@ module.exports = {
themes: themes,
users: users,
slugs: slugs,
subscribers: subscribers,
authentication: authentication,
uploads: uploads,
slack: slack

View file

@ -0,0 +1,367 @@
// # Tag API
// RESTful API for the Tag resource
var Promise = require('bluebird'),
_ = require('lodash'),
fs = require('fs'),
pUnlink = Promise.promisify(fs.unlink),
readline = require('readline'),
dataProvider = require('../models'),
errors = require('../errors'),
utils = require('./utils'),
pipeline = require('../utils/pipeline'),
i18n = require('../i18n'),
docName = 'subscribers',
subscribers;
/**
* ### Subscribers API Methods
*
* **See:** [API Methods](index.js.html#api%20methods)
*/
subscribers = {
/**
* ## Browse
* @param {{context}} options
* @returns {Promise<Subscriber>} Subscriber Collection
*/
browse: function browse(options) {
var tasks;
/**
* ### Model Query
* Make the call to the Model layer
* @param {Object} options
* @returns {Object} options
*/
function doQuery(options) {
return dataProvider.Subscriber.findPage(options);
}
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
utils.validate(docName, {opts: utils.browseDefaultOptions}),
// TODO: handlePermissions
doQuery
];
// Pipeline calls each task passing the result of one to be the arguments for the next
return pipeline(tasks, options);
},
/**
* ## Read
* @param {{id}} options
* @return {Promise<Subscriber>} Subscriber
*/
read: function read(options) {
var attrs = ['id'],
tasks;
/**
* ### Model Query
* Make the call to the Model layer
* @param {Object} options
* @returns {Object} options
*/
function doQuery(options) {
return dataProvider.Subscriber.findOne(options.data, _.omit(options, ['data']));
}
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
utils.validate(docName, {attrs: attrs}),
// TODO: handlePermissions
doQuery
];
// Pipeline calls each task passing the result of one to be the arguments for the next
return pipeline(tasks, options).then(function formatResponse(result) {
if (result) {
return {subscribers: [result.toJSON(options)]};
}
return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.subscriber.subscriberNotFound')));
});
},
/**
* ## Add
* @param {Subscriber} object the subscriber to create
* @returns {Promise(Subscriber)} Newly created Subscriber
*/
add: function add(object, options) {
var tasks;
function cleanError(error) {
if (error.message.toLowerCase().indexOf('unique') !== -1) {
return new errors.DataImportError('Email already exists.');
}
return error;
}
/**
* ### Model Query
* Make the call to the Model layer
* @param {Object} options
* @returns {Object} options
*/
function doQuery(options) {
return dataProvider.Subscriber.add(options.data.subscribers[0], _.omit(options, ['data'])).catch(function (error) {
if (error.errno) {
// DB error
return Promise.reject(cleanError(error));
}
return Promise.reject(error[0]);
});
}
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
utils.validate(docName),
// TODO: handlePermissions
doQuery
];
// Pipeline calls each task passing the result of one to be the arguments for the next
return pipeline(tasks, object, options).then(function formatResponse(result) {
var subscriber = result.toJSON(options);
return {subscribers: [subscriber]};
});
},
/**
* ## Edit
*
* @public
* @param {Subscriber} object Subscriber or specific properties to update
* @param {{id, context, include}} options
* @return {Promise<Subscriber>} Edited Subscriber
*/
edit: function edit(object, options) {
var tasks;
/**
* Make the call to the Model layer
* @param {Object} options
* @returns {Object} options
*/
function doQuery(options) {
return dataProvider.Subscriber.edit(options.data.subscribers[0], _.omit(options, ['data']));
}
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
utils.validate(docName, {opts: utils.idDefaultOptions}),
// TODO: handlePermissions
doQuery
];
// Pipeline calls each task passing the result of one to be the arguments for the next
return pipeline(tasks, object, options).then(function formatResponse(result) {
if (result) {
var subscriber = result.toJSON(options);
return {subscribers: [subscriber]};
}
return Promise.reject(new errors.NotFoundError(i18n.t('errors.api.subscriber.subscriberNotFound')));
});
},
/**
* ## Destroy
*
* @public
* @param {{id, context}} options
* @return {Promise}
*/
destroy: function destroy(options) {
var tasks;
/**
* ### Delete Subscriber
* Make the call to the Model layer
* @param {Object} options
*/
function doQuery(options) {
return dataProvider.Subscriber.destroy(options).return(null);
}
// Push all of our tasks into a `tasks` array in the correct order
tasks = [
utils.validate(docName, {opts: utils.idDefaultOptions}),
// TODO: handlePermissions
doQuery
];
// Pipeline calls each task passing the result of one to be the arguments for the next
return pipeline(tasks, options);
},
/**
* ### Export Subscribers
* Generate the CSV to export
*
* @public
* @param {{context}} options
* @returns {Promise} Ghost Export CSV format
*/
exportCSV: function exportCSV(options) {
var tasks = [];
options = options || {};
function formatCSV(data) {
var fields = ['id', 'email', 'created_at', 'deleted_at'],
csv = fields.join(',') + '\r\n',
subscriber,
field,
j,
i;
for (j = 0; j < data.length; j = j + 1) {
subscriber = data[j];
for (i = 0; i < fields.length; i = i + 1) {
field = fields[i];
csv += subscriber[field] !== null ? subscriber[field] : '';
if (i !== fields.length - 1) {
csv += ',';
}
}
csv += '\r\n';
}
return csv;
}
// Export data, otherwise send error 500
function exportSubscribers() {
return dataProvider.Subscriber.findPage(options).then(function (data) {
return formatCSV(data.subscribers);
}).catch(function (error) {
return Promise.reject(new errors.InternalServerError(error.message || error));
});
}
tasks = [
// TODO: handlePermissions
exportSubscribers
];
return pipeline(tasks, options);
},
/**
* ### Import CSV
* Import subscribers from a CSV file
*
* @public
* @param {{context}} options
* @returns {Promise} Success
*/
importCSV: function (options) {
var tasks = [];
options = options || {};
function validate(options) {
options.name = options.originalname;
options.type = options.mimetype;
// Check if a file was provided
if (!utils.checkFileExists(options)) {
return Promise.reject(new errors.ValidationError(i18n.t('errors.api.db.selectFileToImport')));
}
// TODO: check for valid entries
return options;
}
function importCSV(options) {
return new Promise(function (resolve, reject) {
var filePath = options.path,
importTasks = [],
emailIdx = -1,
firstLine = true,
rl;
rl = readline.createInterface({
input: fs.createReadStream(filePath),
terminal: false
});
rl.on('line', function (line) {
var dataToImport = line.split(',');
if (firstLine) {
emailIdx = _.findIndex(dataToImport, function (columnName) {
if (columnName.match(/email/g)) {
return true;
} else {
return false;
}
});
if (emailIdx === -1) {
return reject(new errors.ValidationError('Email column not found'));
}
firstLine = false;
} else if (emailIdx > -1) {
importTasks.push(function () {
return subscribers.add({
subscribers: [{
email: dataToImport[emailIdx]
}
]}, {context: options.context});
});
}
});
rl.on('close', function () {
var fulfilled = 0,
duplicates = 0,
invalid = 0;
Promise.all(importTasks.map(function (promise) {
return promise().reflect();
})).each(function (inspection) {
if (inspection.isFulfilled()) {
fulfilled = fulfilled + 1;
} else {
if (inspection.reason().errorType === 'ValidationError') {
invalid = invalid + 1;
} else if (inspection.reason().errorType === 'DataImportError') {
duplicates = duplicates + 1;
}
}
}).then(function () {
return resolve({
stats: [{
imported: fulfilled,
duplicates: duplicates,
invalid: invalid
}]
});
}).catch(function (err) {
return reject(err);
}).finally(function () {
// Remove uploaded file from tmp location
return pUnlink(filePath);
});
});
});
}
tasks = [
validate,
// TODO: handlePermissions
importCSV
];
return pipeline(tasks, options);
}
};
module.exports = subscribers;

View file

@ -11,7 +11,7 @@ module.exports = {
// Correct way to register a helper from an app
ghost.helpers.register('form_subscribe', function formSubscribeHelper(options) {
var data = _.merge({}, options.hash, {
action: path.join(config.paths.subdir, config.routeKeywords.subscribe) + '/'
action: path.join('/', config.paths.subdir, config.routeKeywords.subscribe, '/')
});
return template.execute('form_subscribe', data, options);
});

View file

@ -2,6 +2,7 @@ var path = require('path'),
express = require('express'),
templates = require('../../../controllers/frontend/templates'),
setResponseContext = require('../../../controllers/frontend/context'),
api = require('../../../api'),
subscribeRouter = express.Router();
function controller(req, res) {
@ -21,12 +22,20 @@ function controller(req, res) {
}
}
function storeSubscriber(req, res, next) {
return api.subscribers.add({subscribers: [req.body]}, {context: {external: true}}).then(function (result) {
next();
});
}
// subscribe frontend route
subscribeRouter.route('/')
.get(
controller
)
.post(
storeSubscriber,
controller
);

View file

@ -194,7 +194,7 @@ ConfigManager.prototype.set = function (config) {
private: 'private',
subscribe: 'subscribe'
},
internalApps: ['private-blogging'],
internalApps: ['private-blogging', 'subscribers'],
slugs: {
// Used by generateSlug to generate slugs for posts, tags, users, ..
// reserved slugs are reserved but can be extended/removed by apps

View file

@ -99,6 +99,7 @@ auth = {
} else if (isBearerAutorizationHeader(req)) {
return errors.handleAPIError(new errors.UnauthorizedError(i18n.t('errors.middleware.auth.accessDenied')), req, res, next);
} else if (req.client) {
req.user = {id: 0};
return next();
}
@ -110,7 +111,7 @@ auth = {
// Workaround for missing permissions
// TODO: rework when https://github.com/TryGhost/Ghost/issues/3911 is done
requiresAuthorizedUser: function requiresAuthorizedUser(req, res, next) {
if (req.user) {
if (req.user && req.user.id) {
return next();
} else {
return errors.handleAPIError(new errors.NoPermissionError(i18n.t('errors.middleware.auth.pleaseSignIn')), req, res, next);
@ -122,7 +123,7 @@ auth = {
if (labs.isSet('publicAPI') === true) {
return next();
} else {
if (req.user) {
if (req.user && req.user.id) {
return next();
} else {
return errors.handleAPIError(new errors.NoPermissionError(i18n.t('errors.middleware.auth.pleaseSignIn')), req, res, next);

View file

@ -26,6 +26,7 @@ var bodyParser = require('body-parser'),
uncapitalise = require('./uncapitalise'),
cors = require('./cors'),
netjet = require('netjet'),
labs = require('./labs'),
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
BearerStrategy = require('passport-http-bearer').Strategy,
@ -44,7 +45,8 @@ middleware = {
requiresAuthorizedUser: auth.requiresAuthorizedUser,
requiresAuthorizedUserPublicAPI: auth.requiresAuthorizedUserPublicAPI,
errorHandler: errors.handleAPIError,
cors: cors
cors: cors,
labs: labs
}
};

View file

@ -0,0 +1,15 @@
var errors = require('../errors'),
labsUtil = require('../utils/labs'),
labs;
labs = {
subscribers: function subscribers(req, res, next) {
if (labsUtil.isSet('subscribers') === true) {
return next();
} else {
return errors.handleAPIError(new errors.NotFoundError(), req, res, next);
}
}
};
module.exports = labs;

View file

@ -134,11 +134,13 @@ ghostBookshelf.Model = ghostBookshelf.Model.extend({
// Get the user from the options object
contextUser: function contextUser(options) {
// Default to context user
if (options.context && options.context.user) {
if ((options.context && options.context.user) || (options.context && options.context.user === 0)) {
return options.context.user;
// Other wise use the internal override
} else if (options.context && options.context.internal) {
return 1;
} else if (options.context && options.context.external) {
return 0;
} else {
errors.logAndThrowError(new Error(i18n.t('errors.models.base.index.missingContext')));
}

View file

@ -25,6 +25,7 @@ models = [
'refreshtoken',
'role',
'settings',
'subscriber',
'tag',
'user'
];

View file

@ -0,0 +1,46 @@
var ghostBookshelf = require('./base'),
Subscriber,
Subscribers;
Subscriber = ghostBookshelf.Model.extend({
tableName: 'subscribers'
}, {
orderDefaultOptions: function orderDefaultOptions() {
return {};
},
/**
* @deprecated in favour of filter
*/
processOptions: function processOptions(options) {
return options;
},
permittedOptions: function permittedOptions(methodName) {
var options = ghostBookshelf.Model.permittedOptions(),
// whitelists for the `options` hash argument on methods, by method name.
// these are the only options that can be passed to Bookshelf / Knex.
validOptions = {
findPage: ['page', 'limit', 'columns', 'filter', 'order'],
findAll: ['columns']
};
if (validOptions[methodName]) {
options = options.concat(validOptions[methodName]);
}
return options;
}
});
Subscribers = ghostBookshelf.Collection.extend({
model: Subscriber
});
module.exports = {
Subscriber: ghostBookshelf.model('Subscriber', Subscriber),
Subscribers: ghostBookshelf.collection('Subscriber', Subscribers)
};

View file

@ -65,6 +65,20 @@ apiRoutes = function apiRoutes(middleware) {
router.put('/tags/:id', authenticatePrivate, api.http(api.tags.edit));
router.del('/tags/:id', authenticatePrivate, api.http(api.tags.destroy));
// ## Subscribers
router.get('/subscribers', middleware.api.labs.subscribers, authenticatePrivate, api.http(api.subscribers.browse));
router.get('/subscribers/csv', middleware.api.labs.subscribers, authenticatePrivate, api.http(api.subscribers.exportCSV));
router.post('/subscribers/csv',
middleware.api.labs.subscribers,
authenticatePrivate,
middleware.upload.single('subscribersfile'),
api.http(api.subscribers.importCSV)
);
router.get('/subscribers/:id', middleware.api.labs.subscribers, authenticatePrivate, api.http(api.subscribers.read));
router.post('/subscribers', middleware.api.labs.subscribers, authenticatePublic, api.http(api.subscribers.add));
router.put('/subscribers/:id', middleware.api.labs.subscribers, authenticatePrivate, api.http(api.subscribers.edit));
router.del('/subscribers/:id', middleware.api.labs.subscribers, authenticatePrivate, api.http(api.subscribers.destroy));
// ## Roles
router.get('/roles/', authenticatePrivate, api.http(api.roles.browse));

View file

@ -333,6 +333,9 @@
"tags": {
"tagNotFound": "Tag not found."
},
"subscriber": {
"subscriberNotFound": "Subscriber not found."
},
"themes": {
"noPermissionToBrowseThemes": "You do not have permission to browse themes.",
"noPermissionToEditThemes": "You do not have permission to edit themes.",

View file

@ -0,0 +1,176 @@
/*globals describe, before, beforeEach, afterEach, it */
var testUtils = require('../../utils'),
should = require('should'),
Promise = require('bluebird'),
_ = require('lodash'),
// Stuff we are testing
context = testUtils.context,
SubscribersAPI = require('../../../server/api/subscribers');
describe('Subscribers API', function () {
// Keep the DB clean
before(testUtils.teardown);
afterEach(testUtils.teardown);
beforeEach(testUtils.setup('users:roles', 'permission', 'perms:init', 'subscriber'));
should.exist(SubscribersAPI);
describe('Add', function () {
var newSubscriber;
beforeEach(function () {
newSubscriber = _.clone(testUtils.DataGenerator.forKnex.createSubscriber(testUtils.DataGenerator.Content.subscribers[1]));
Promise.resolve(newSubscriber);
});
it('can add a subscriber (admin)', function (done) {
SubscribersAPI.add({subscribers: [newSubscriber]}, testUtils.context.admin)
.then(function (results) {
should.exist(results);
should.exist(results.subscribers);
results.subscribers.length.should.be.above(0);
done();
}).catch(done);
});
it('can add a subscriber (external)', function (done) {
SubscribersAPI.add({subscribers: [newSubscriber]}, testUtils.context.external)
.then(function (results) {
should.exist(results);
should.exist(results.subscribers);
results.subscribers.length.should.be.above(0);
done();
}).catch(done);
});
it('CANNOT add subscriber without context', function (done) {
SubscribersAPI.add({subscribers: [newSubscriber]}).then(function () {
done(new Error('Add subscriber is not denied without authentication.'));
}, function () {
done();
}).catch(done);
});
});
describe('Edit', function () {
var newSubscriberEmail = 'subscriber@updated.com',
firstSubscriber = 1;
it('can edit a subscriber (admin)', function (done) {
SubscribersAPI.edit({subscribers: [{email: newSubscriberEmail}]}, _.extend({}, context.admin, {id: firstSubscriber}))
.then(function (results) {
should.exist(results);
should.exist(results.subscribers);
results.subscribers.length.should.be.above(0);
done();
}).catch(done);
});
it('can edit a subscriber (editor)', function (done) {
SubscribersAPI.edit({subscribers: [{email: newSubscriberEmail}]}, _.extend({}, context.editor, {id: firstSubscriber}))
.then(function (results) {
should.exist(results);
should.exist(results.subscribers);
results.subscribers.length.should.be.above(0);
done();
}).catch(done);
});
// needs permissions to work properly
it.skip('CANNOT edit subscriber (external)', function (done) {
SubscribersAPI.edit({subscribers: [{email: newSubscriberEmail}]}, _.extend({}, context.external, {id: firstSubscriber}))
.then(function () {
done(new Error('Edit subscriber is not denied with external context.'));
}, function () {
done();
}).catch(done);
});
it('CANNOT edit subscriber that doesn\'t exit', function (done) {
SubscribersAPI.edit({subscribers: [{email: newSubscriberEmail}]}, _.extend({}, context.internal, {id: 999}))
.then(function () {
done(new Error('Edit non-existent subscriber is possible.'));
}, function (err) {
should.exist(err);
err.message.should.eql('Subscriber not found.');
done();
}).catch(done);
});
});
describe('Destroy', function () {
var firstSubscriber = 1;
it('can destroy subscriber', function (done) {
SubscribersAPI.destroy(_.extend({}, testUtils.context.admin, {id: firstSubscriber}))
.then(function (results) {
should.not.exist(results);
done();
}).catch(done);
});
});
describe('Browse', function () {
it('can browse (internal)', function (done) {
SubscribersAPI.browse(testUtils.context.internal).then(function (results) {
should.exist(results);
should.exist(results.subscribers);
results.subscribers.should.have.lengthOf(1);
testUtils.API.checkResponse(results.subscribers[0], 'subscriber');
results.subscribers[0].created_at.should.be.an.instanceof(Date);
results.meta.pagination.should.have.property('page', 1);
results.meta.pagination.should.have.property('limit', 15);
results.meta.pagination.should.have.property('pages', 1);
results.meta.pagination.should.have.property('total', 1);
results.meta.pagination.should.have.property('next', null);
results.meta.pagination.should.have.property('prev', null);
done();
}).catch(done);
});
// needs permissions to work properly
it.skip('CANNOT browse subscriber (external)', function (done) {
SubscribersAPI.browse(testUtils.context.external).then(function () {
done(new Error('Browse subscriber is not denied with external context.'));
}, function () {
done();
}).catch(done);
});
});
describe('Read', function () {
function extractFirstSubscriber(subscribers) {
return _.filter(subscribers, {id: 1})[0];
}
it('with id', function (done) {
SubscribersAPI.browse({context: {user: 1}}).then(function (results) {
should.exist(results);
should.exist(results.subscribers);
results.subscribers.length.should.be.above(0);
var firstSubscriber = extractFirstSubscriber(results.subscribers);
return SubscribersAPI.read({context: {user: 1}, id: firstSubscriber.id});
}).then(function (found) {
should.exist(found);
testUtils.API.checkResponse(found.subscribers[0], 'subscriber');
done();
}).catch(done);
});
it('cannot fetch a subscriber which doesn\'t exist', function (done) {
SubscribersAPI.read({context: {user: 1}, id: 999}).then(function () {
done(new Error('Should not return a result'));
}).catch(function (err) {
should.exist(err);
err.message.should.eql('Subscriber not found.');
done();
});
});
});
});

View file

@ -13,6 +13,7 @@ var _ = require('lodash'),
tags: ['tags', 'meta'],
users: ['users', 'meta'],
settings: ['settings', 'meta'],
subscribers: ['subscribers', 'meta'],
roles: ['roles'],
pagination: ['page', 'limit', 'pages', 'total', 'next', 'prev'],
slugs: ['slugs'],
@ -25,6 +26,7 @@ var _ = require('lodash'),
// Tag API swaps parent_id to parent
tag: _(schema.tags).keys().without('parent_id').concat('parent').value(),
setting: _.keys(schema.settings),
subscriber: _.keys(schema.subscribers),
accesstoken: _.keys(schema.accesstokens),
role: _.keys(schema.roles),
permission: _.keys(schema.permissions),

View file

@ -236,6 +236,15 @@ DataGenerator.Content = {
key: 'setting',
value: 'value'
}
],
subscribers: [
{
email: 'subscriber1@test.com'
},
{
email: 'subscriber2@test.com'
}
]
};
@ -440,6 +449,7 @@ DataGenerator.forKnex = (function () {
createAppField: createAppField,
createAppSetting: createAppSetting,
createToken: createToken,
createSubscriber: createBasic,
posts: posts,
tags: tags,

View file

@ -386,6 +386,7 @@ toDoList = {
role: function insertRole() { return fixtures.insertOne('roles', 'createRole'); },
roles: function insertRoles() { return fixtures.insertRoles(); },
tag: function insertTag() { return fixtures.insertOne('tags', 'createTag'); },
subscriber: function insertSubscriber() { return fixtures.insertOne('subscribers', 'createSubscriber'); },
posts: function insertPosts() { return fixtures.insertPosts(); },
'posts:mu': function insertMultiAuthorPosts() { return fixtures.insertMultiAuthorPosts(); },
@ -581,6 +582,7 @@ module.exports = {
// Helpers to make it easier to write tests which are easy to read
context: {
internal: {context: {internal: true}},
external: {context: {external: true}},
owner: {context: {user: 1}},
admin: {context: {user: 2}},
editor: {context: {user: 3}},