mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
parent
0338ba56c0
commit
cfea6375ab
6 changed files with 543 additions and 9 deletions
|
@ -45,5 +45,9 @@ module.exports = {
|
|||
|
||||
get settings() {
|
||||
return shared.pipeline(require('./settings'), localUtils);
|
||||
},
|
||||
|
||||
get subscribers() {
|
||||
return shared.pipeline(require('./subscribers'), localUtils);
|
||||
}
|
||||
};
|
||||
|
|
214
core/server/api/v2/subscribers.js
Normal file
214
core/server/api/v2/subscribers.js
Normal 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;
|
|
@ -33,5 +33,9 @@ module.exports = {
|
|||
|
||||
get mail() {
|
||||
return require('./mail');
|
||||
},
|
||||
|
||||
get subscribers() {
|
||||
return require('./subscribers');
|
||||
}
|
||||
};
|
||||
|
|
83
core/server/api/v2/utils/serializers/output/subscribers.js
Normal file
83
core/server/api/v2/utils/serializers/output/subscribers.js
Normal 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;
|
||||
}
|
||||
};
|
|
@ -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));
|
||||
|
|
229
core/test/functional/api/v2/admin/subscribers_spec.js
Normal file
229
core/test/functional/api/v2/admin/subscribers_spec.js
Normal 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
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue