0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Added subscribers controller to v2

refs #9866
This commit is contained in:
Nazar Gargol 2018-10-11 18:09:01 +02:00 committed by Katharina Irrgang
parent 0338ba56c0
commit cfea6375ab
6 changed files with 543 additions and 9 deletions

View file

@ -45,5 +45,9 @@ module.exports = {
get settings() {
return shared.pipeline(require('./settings'), localUtils);
},
get subscribers() {
return shared.pipeline(require('./subscribers'), localUtils);
}
};

View file

@ -0,0 +1,214 @@
const fs = require('fs-extra');
const Promise = require('bluebird');
const models = require('../../models');
const fsLib = require('../../lib/fs');
const common = require('../../lib/common');
const subscribers = {
docName: 'subscribers',
browse: {
options: [
'limit',
'fields',
'filter',
'order',
'debug'
],
permissions: true,
validation: {},
query(frame) {
return models.Subscriber.findPage(frame.options);
}
},
read: {
headers: {},
data: [
'id',
'email'
],
validation: {},
permissions: true,
query(frame) {
return models.Subscriber.findOne(frame.data);
}
},
add: {
statusCode: 201,
headers: {},
validation: {
data: {
email: {required: true}
}
},
permissions: true,
query(frame) {
return models.Subscriber.getByEmail(frame.data.subscribers[0].email)
.then((subscriber) => {
if (subscriber && frame.options.context.external) {
// we don't expose this information
return Promise.resolve(subscriber);
} else if (subscriber) {
return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.api.subscribers.subscriberAlreadyExists')}));
}
return models.Subscriber
.add(frame.data.subscribers[0])
.catch((error) => {
if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.api.subscribers.subscriberAlreadyExists')}));
}
return Promise.reject(error);
});
});
}
},
edit: {
headers: {},
options: [
'id'
],
validation: {
id: {
required: true
}
},
permissions: true,
query(frame) {
return models.Subscriber.edit(frame.data.subscribers[0], frame.options)
.then((model) => {
if (!model) {
return Promise.reject(new common.errors.NotFoundError({
message: common.i18n.t('errors.api.subscribers.subscriberNotFound')
}));
}
return model;
});
}
},
destroy: {
statusCode: 204,
headers: {
cacheInvalidate: true
},
options: [
'id',
'email'
],
validation: {},
permissions: true,
query(frame) {
/**
* ### Delete Subscriber
* If we have an email param, check the subscriber exists
* @type {[type]}
*/
function getSubscriberByEmail(options) {
if (options.email) {
return models.Subscriber.getByEmail(options.email, options)
.then((subscriber) => {
if (!subscriber) {
return Promise.reject(new common.errors.NotFoundError({
message: common.i18n.t('errors.api.subscribers.subscriberNotFound')
}));
}
options.id = subscriber.get('id');
return options;
});
}
return Promise.resolve(options);
}
return getSubscriberByEmail(frame.options)
.then((options) => {
return models.Subscriber
.destroy(options)
.return(null);
});
}
},
exportCSV: {
headers: {
disposition: {
type: 'csv'
}
},
response: {
format: 'plain'
},
permissions: {
method: 'browse'
},
validation: {},
query(frame) {
return models.Subscriber.findAll(frame.options)
.catch((err) => {
return Promise.reject(new common.errors.GhostError({err: err}));
});
}
},
importCSV: {
statusCode: 201,
permissions: {
method: 'add'
},
validation: {},
query(frame) {
let filePath = frame.file.path,
fulfilled = 0,
invalid = 0,
duplicates = 0;
return fsLib.readCSV({
path: filePath,
columnsToExtract: [{name: 'email', lookup: /email/i}]
}).then((result) => {
return Promise.all(result.map((entry) => {
const apiv2 = require('./index');
return apiv2.subscribers.add.query({
data: {subscribers: [{email: entry.email}]},
options: {
context: frame.options.context
}
}).reflect();
})).each((inspection) => {
if (inspection.isFulfilled()) {
fulfilled = fulfilled + 1;
} else {
if (inspection.reason() instanceof common.errors.ValidationError) {
duplicates = duplicates + 1;
} else {
invalid = invalid + 1;
}
}
});
}).then(() => {
return {
meta: {
stats: {
imported: fulfilled,
duplicates: duplicates,
invalid: invalid
}
}
};
}).finally(() => {
// Remove uploaded file from tmp location
return fs.unlink(filePath);
});
}
}
};
module.exports = subscribers;

View file

@ -33,5 +33,9 @@ module.exports = {
get mail() {
return require('./mail');
},
get subscribers() {
return require('./subscribers');
}
};

View file

@ -0,0 +1,83 @@
const common = require('../../../../../lib/common');
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:subscribers');
module.exports = {
browse(models, apiConfig, frame) {
debug('browse');
frame.response = {
subscribers: models.data.map(model => model.toJSON(frame.options)),
meta: models.meta
};
},
read(models, apiConfig, frame) {
debug('read');
if (!models) {
return Promise.reject(new common.errors.NotFoundError({
message: common.i18n.t('errors.api.subscribers.subscriberNotFound')
}));
}
frame.response = {
subscribers: [models.toJSON(frame.options)]
};
},
add(models, apiConfig, frame) {
debug('add');
frame.response = {
subscribers: [models.toJSON(frame.options)]
};
},
edit(models, apiConfig, frame) {
debug('edit');
frame.response = {
subscribers: [models.toJSON(frame.options)]
};
},
destroy(models, apiConfig, frame) {
frame.response = models;
},
exportCSV(models, apiConfig, frame) {
debug('exportCSV');
function formatCSV(data) {
let 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;
}
frame.response = formatCSV(models.toJSON(frame.options), frame.options);
},
importCSV(models, apiConfig, frame) {
debug('importCSV');
frame.response = models;
}
};

View file

@ -73,21 +73,21 @@ module.exports = function apiRoutes() {
router.del('/tags/:id', mw.authAdminAPI, api.http(api.tags.destroy));
// ## Subscribers
router.get('/subscribers', shared.middlewares.labs.subscribers, mw.authAdminAPI, api.http(api.subscribers.browse));
router.get('/subscribers/csv', shared.middlewares.labs.subscribers, mw.authAdminAPI, api.http(api.subscribers.exportCSV));
router.get('/subscribers', shared.middlewares.labs.subscribers, mw.authAdminAPI, apiv2.http(apiv2.subscribers.browse));
router.get('/subscribers/csv', shared.middlewares.labs.subscribers, mw.authAdminAPI, apiv2.http(apiv2.subscribers.exportCSV));
router.post('/subscribers/csv',
shared.middlewares.labs.subscribers,
mw.authAdminAPI,
upload.single('subscribersfile'),
shared.middlewares.validation.upload({type: 'subscribers'}),
api.http(api.subscribers.importCSV)
apiv2.http(apiv2.subscribers.importCSV)
);
router.get('/subscribers/:id', shared.middlewares.labs.subscribers, mw.authAdminAPI, api.http(api.subscribers.read));
router.get('/subscribers/email/:email', shared.middlewares.labs.subscribers, mw.authAdminAPI, api.http(api.subscribers.read));
router.post('/subscribers', shared.middlewares.labs.subscribers, mw.authAdminAPI, api.http(api.subscribers.add));
router.put('/subscribers/:id', shared.middlewares.labs.subscribers, mw.authAdminAPI, api.http(api.subscribers.edit));
router.del('/subscribers/:id', shared.middlewares.labs.subscribers, mw.authAdminAPI, api.http(api.subscribers.destroy));
router.del('/subscribers/email/:email', shared.middlewares.labs.subscribers, mw.authAdminAPI, api.http(api.subscribers.destroy));
router.get('/subscribers/:id', shared.middlewares.labs.subscribers, mw.authAdminAPI, apiv2.http(apiv2.subscribers.read));
router.get('/subscribers/email/:email', shared.middlewares.labs.subscribers, mw.authAdminAPI, apiv2.http(apiv2.subscribers.read));
router.post('/subscribers', shared.middlewares.labs.subscribers, mw.authAdminAPI, apiv2.http(apiv2.subscribers.add));
router.put('/subscribers/:id', shared.middlewares.labs.subscribers, mw.authAdminAPI, apiv2.http(apiv2.subscribers.edit));
router.del('/subscribers/:id', shared.middlewares.labs.subscribers, mw.authAdminAPI, apiv2.http(apiv2.subscribers.destroy));
router.del('/subscribers/email/:email', shared.middlewares.labs.subscribers, mw.authAdminAPI, apiv2.http(apiv2.subscribers.destroy));
// ## Roles
router.get('/roles/', mw.authAdminAPI, apiv2.http(apiv2.roles.browse));

View file

@ -0,0 +1,229 @@
const path = require('path');
const should = require('should');
const supertest = require('supertest');
const sinon = require('sinon');
const testUtils = require('../../../../utils');
const localUtils = require('./utils');
const config = require('../../../../../../core/server/config');
const labs = require('../../../../../../core/server/services/labs');
const ghost = testUtils.startGhost;
const sandbox = sinon.sandbox.create();
let request;
describe('Subscribers API', function () {
before(function () {
sandbox.stub(labs, 'isSet').withArgs('subscribers').returns(true);
});
after(function () {
sandbox.restore();
});
before(function () {
return ghost()
.then(function () {
request = supertest.agent(config.get('url'));
})
.then(function () {
return localUtils.doAuth(request, 'subscriber');
});
});
it('browse', function () {
return request
.get(localUtils.API.getApiQuery('subscribers/'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.subscribers);
jsonResponse.subscribers.should.have.length(1);
testUtils.API.checkResponse(jsonResponse.subscribers[0], 'subscriber');
testUtils.API.isISO8601(jsonResponse.subscribers[0].created_at).should.be.true();
jsonResponse.subscribers[0].created_at.should.be.an.instanceof(String);
jsonResponse.meta.pagination.should.have.property('page', 1);
jsonResponse.meta.pagination.should.have.property('limit', 15);
jsonResponse.meta.pagination.should.have.property('pages', 1);
jsonResponse.meta.pagination.should.have.property('total', 1);
jsonResponse.meta.pagination.should.have.property('next', null);
jsonResponse.meta.pagination.should.have.property('prev', null);
});
});
it('read', function () {
return request
.get(localUtils.API.getApiQuery(`subscribers/${testUtils.DataGenerator.Content.subscribers[0].id}/`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.subscribers);
jsonResponse.subscribers.should.have.length(1);
testUtils.API.checkResponse(jsonResponse.subscribers[0], 'subscriber');
});
});
it('add', function () {
const subscriber = {
name: 'test',
email: 'subscriberTestAdd@test.com'
};
return request
.post(localUtils.API.getApiQuery(`subscribers/`))
.send({subscribers: [subscriber]})
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.subscribers);
jsonResponse.subscribers.should.have.length(1);
// testUtils.API.checkResponse(jsonResponse.subscribers[0], 'subscriber'); // TODO: modify checked schema
jsonResponse.subscribers[0].name.should.equal(subscriber.name);
jsonResponse.subscribers[0].email.should.equal(subscriber.email);
});
});
it('edit by id', function () {
const subscriberToChange = {
name: 'changed',
email: 'subscriber1Changed@test.com'
};
const subscriberChanged = {
name: 'changed',
email: 'subscriber1Changed@test.com'
};
return request
.post(localUtils.API.getApiQuery(`subscribers/`))
.send({subscribers: [subscriberToChange]})
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.subscribers);
jsonResponse.subscribers.should.have.length(1);
return jsonResponse.subscribers[0];
})
.then((newSubscriber) => {
return request
.put(localUtils.API.getApiQuery(`subscribers/${newSubscriber.id}/`))
.send({subscribers:[subscriberChanged]})
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.subscribers);
jsonResponse.subscribers.should.have.length(1);
testUtils.API.checkResponse(jsonResponse.subscribers[0], 'subscriber');
jsonResponse.subscribers[0].name.should.equal(subscriberChanged.name);
jsonResponse.subscribers[0].email.should.equal(subscriberChanged.email);
});
});
});
it('destroy', function () {
const subscriber = {
name: 'test',
email: 'subscriberTestDestroy@test.com'
};
return request
.post(localUtils.API.getApiQuery(`subscribers/`))
.send({subscribers: [subscriber]})
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.subscribers);
return jsonResponse.subscribers[0];
})
.then((newSubscriber) => {
return request
.delete(localUtils.API.getApiQuery(`subscribers/${newSubscriber.id}`))
.set('Origin', config.get('url'))
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(204)
.then(() => newSubscriber);
})
.then((newSubscriber) => {
return request
.get(localUtils.API.getApiQuery(`subscribers/${newSubscriber.id}/`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(404);
});
});
it('exportCSV', function () {
return request
.get(localUtils.API.getApiQuery(`subscribers/csv/`))
.set('Origin', config.get('url'))
.expect('Content-Type', /text\/csv/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(200)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
res.text.should.match(/id,email,created_at,deleted_at/);
res.text.should.match(/subscriber1@test.com/);
});
});
it('importCSV', function () {
return request
.post(localUtils.API.getApiQuery(`subscribers/csv/`))
.attach('subscribersfile', path.join(__dirname, '/../../../../utils/fixtures/csv/single-column-with-header.csv'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(201)
.then((res) => {
should.not.exist(res.headers['x-cache-invalidate']);
const jsonResponse = res.body;
should.exist(jsonResponse);
should.exist(jsonResponse.meta);
should.exist(jsonResponse.meta.stats);
jsonResponse.meta.stats.imported.should.equal(2);
jsonResponse.meta.stats.duplicates.should.equal(0);
jsonResponse.meta.stats.invalid.should.equal(1); // TODO: should return 0
});
});
});