mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-06 22:40:14 -05:00
parent
b899a6fec8
commit
9f2d68a027
7 changed files with 492 additions and 8 deletions
|
@ -31,6 +31,10 @@ module.exports = {
|
||||||
return shared.pipeline(require('./posts'), localUtils);
|
return shared.pipeline(require('./posts'), localUtils);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get notifications() {
|
||||||
|
return shared.pipeline(require('./notifications'), localUtils);
|
||||||
|
},
|
||||||
|
|
||||||
get settings() {
|
get settings() {
|
||||||
return shared.pipeline(require('./settings'), localUtils);
|
return shared.pipeline(require('./settings'), localUtils);
|
||||||
}
|
}
|
||||||
|
|
196
core/server/api/v2/notifications.js
Normal file
196
core/server/api/v2/notifications.js
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
const moment = require('moment-timezone');
|
||||||
|
const semver = require('semver');
|
||||||
|
const Promise = require('bluebird');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const settingsCache = require('../../services/settings/cache');
|
||||||
|
const ghostVersion = require('../../lib/ghost-version');
|
||||||
|
const common = require('../../lib/common');
|
||||||
|
const ObjectId = require('bson-objectid');
|
||||||
|
const api = require('./index');
|
||||||
|
const internalContext = {context: {internal: true}};
|
||||||
|
const _private = {};
|
||||||
|
|
||||||
|
_private.fetchAllNotifications = () => {
|
||||||
|
let allNotifications = settingsCache.get('notifications');
|
||||||
|
|
||||||
|
allNotifications.forEach((notification) => {
|
||||||
|
notification.addedAt = moment(notification.addedAt).toDate();
|
||||||
|
});
|
||||||
|
|
||||||
|
return allNotifications;
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
docName: 'notifications',
|
||||||
|
|
||||||
|
browse: {
|
||||||
|
permissions: true,
|
||||||
|
query() {
|
||||||
|
let allNotifications = _private.fetchAllNotifications();
|
||||||
|
allNotifications = _.orderBy(allNotifications, 'addedAt', 'desc');
|
||||||
|
|
||||||
|
allNotifications = allNotifications.filter((notification) => {
|
||||||
|
// CASE: do not return old release notification
|
||||||
|
if (!notification.custom && notification.message) {
|
||||||
|
const notificationVersion = notification.message.match(/(\d+\.)(\d+\.)(\d+)/),
|
||||||
|
blogVersion = ghostVersion.full.match(/^(\d+\.)(\d+\.)(\d+)/);
|
||||||
|
|
||||||
|
if (notificationVersion && blogVersion && semver.gt(notificationVersion[0], blogVersion[0])) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return notification.seen !== true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return allNotifications;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
add: {
|
||||||
|
statusCode(result) {
|
||||||
|
if (result.notifications.length) {
|
||||||
|
return 201;
|
||||||
|
} else {
|
||||||
|
return 200;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
permissions: true,
|
||||||
|
query(frame) {
|
||||||
|
const defaults = {
|
||||||
|
dismissible: true,
|
||||||
|
location: 'bottom',
|
||||||
|
status: 'alert',
|
||||||
|
id: ObjectId.generate()
|
||||||
|
};
|
||||||
|
|
||||||
|
const overrides = {
|
||||||
|
seen: false,
|
||||||
|
addedAt: moment().toDate()
|
||||||
|
};
|
||||||
|
|
||||||
|
let notificationsToCheck = frame.data.notifications;
|
||||||
|
let addedNotifications = [];
|
||||||
|
|
||||||
|
const allNotifications = _private.fetchAllNotifications();
|
||||||
|
|
||||||
|
notificationsToCheck.forEach((notification) => {
|
||||||
|
const isDuplicate = allNotifications.find((n) => {
|
||||||
|
return n.id === notification.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isDuplicate) {
|
||||||
|
addedNotifications.push(Object.assign({}, defaults, notification, overrides));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasReleaseNotification = notificationsToCheck.find((notification) => {
|
||||||
|
return !notification.custom;
|
||||||
|
});
|
||||||
|
|
||||||
|
// CASE: remove any existing release notifications if a new release notification comes in
|
||||||
|
if (hasReleaseNotification) {
|
||||||
|
_.remove(allNotifications, (el) => {
|
||||||
|
return !el.custom;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// CASE: nothing to add, skip
|
||||||
|
if (!addedNotifications.length) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const addedReleaseNotifications = addedNotifications.filter((notification) => {
|
||||||
|
return !notification.custom;
|
||||||
|
});
|
||||||
|
|
||||||
|
// CASE: only latest release notification
|
||||||
|
if (addedReleaseNotifications.length > 1) {
|
||||||
|
addedNotifications = addedNotifications.filter((notification) => {
|
||||||
|
return notification.custom;
|
||||||
|
});
|
||||||
|
addedNotifications.push(_.orderBy(addedReleaseNotifications, 'created_at', 'desc')[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.settings.edit({
|
||||||
|
settings: [{
|
||||||
|
key: 'notifications',
|
||||||
|
value: allNotifications.concat(addedNotifications)
|
||||||
|
}]
|
||||||
|
}, internalContext).then(() => {
|
||||||
|
return addedNotifications;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy: {
|
||||||
|
statusCode: 204,
|
||||||
|
options: ['notification_id'],
|
||||||
|
validation: {
|
||||||
|
options: {
|
||||||
|
notification_id: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
permissions: true,
|
||||||
|
query(frame) {
|
||||||
|
const allNotifications = _private.fetchAllNotifications();
|
||||||
|
|
||||||
|
const notificationToMarkAsSeen = allNotifications.find((notification) => {
|
||||||
|
return notification.id === frame.options.notification_id;
|
||||||
|
}),
|
||||||
|
notificationToMarkAsSeenIndex = allNotifications.findIndex((notification) => {
|
||||||
|
return notification.id === frame.options.notification_id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (notificationToMarkAsSeenIndex > -1 && !notificationToMarkAsSeen.dismissible) {
|
||||||
|
return Promise.reject(new common.errors.NoPermissionError({
|
||||||
|
message: common.i18n.t('errors.api.notifications.noPermissionToDismissNotif')
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationToMarkAsSeenIndex < 0) {
|
||||||
|
return Promise.reject(new common.errors.NotFoundError({
|
||||||
|
message: common.i18n.t('errors.api.notifications.notificationDoesNotExist')
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notificationToMarkAsSeen.seen) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
allNotifications[notificationToMarkAsSeenIndex].seen = true;
|
||||||
|
|
||||||
|
return api.settings.edit({
|
||||||
|
settings: [{
|
||||||
|
key: 'notifications',
|
||||||
|
value: allNotifications
|
||||||
|
}]
|
||||||
|
}, internalContext).return();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
destroyAll: {
|
||||||
|
statusCode: 204,
|
||||||
|
permissions: {
|
||||||
|
method: 'destroy'
|
||||||
|
},
|
||||||
|
query() {
|
||||||
|
const allNotifications = _private.fetchAllNotifications();
|
||||||
|
|
||||||
|
allNotifications.forEach((notification) => {
|
||||||
|
notification.seen = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return api.settings.edit({
|
||||||
|
settings: [{
|
||||||
|
key: 'notifications',
|
||||||
|
value: allNotifications
|
||||||
|
}]
|
||||||
|
}, internalContext).return();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -21,5 +21,9 @@ module.exports = {
|
||||||
|
|
||||||
get settings() {
|
get settings() {
|
||||||
return require('./settings');
|
return require('./settings');
|
||||||
|
},
|
||||||
|
|
||||||
|
get notifications() {
|
||||||
|
return require('./notifications');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
28
core/server/api/v2/utils/serializers/output/notifications.js
Normal file
28
core/server/api/v2/utils/serializers/output/notifications.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:notifications');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
all(response, apiConfig, frame) {
|
||||||
|
if (!response) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response || !response.length) {
|
||||||
|
frame.response = {
|
||||||
|
notifications: []
|
||||||
|
};
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.forEach((notification) => {
|
||||||
|
delete notification.seen;
|
||||||
|
delete notification.addedAt;
|
||||||
|
});
|
||||||
|
|
||||||
|
frame.response = {
|
||||||
|
notifications: response
|
||||||
|
};
|
||||||
|
|
||||||
|
debug(frame.response);
|
||||||
|
}
|
||||||
|
};
|
|
@ -124,9 +124,9 @@ module.exports = function apiRoutes() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// ## Notifications
|
// ## Notifications
|
||||||
router.get('/notifications', mw.authAdminAPI, api.http(api.notifications.browse));
|
router.get('/notifications', mw.authAdminAPI, apiv2.http(apiv2.notifications.browse));
|
||||||
router.post('/notifications', mw.authAdminAPI, api.http(api.notifications.add));
|
router.post('/notifications', mw.authAdminAPI, apiv2.http(apiv2.notifications.add));
|
||||||
router.del('/notifications/:id', mw.authAdminAPI, api.http(api.notifications.destroy));
|
router.del('/notifications/:notification_id', mw.authAdminAPI, apiv2.http(apiv2.notifications.destroy));
|
||||||
|
|
||||||
// ## DB
|
// ## DB
|
||||||
router.get('/db', mw.authAdminAPI, api.http(api.db.exportContent));
|
router.get('/db', mw.authAdminAPI, api.http(api.db.exportContent));
|
||||||
|
|
253
core/test/functional/api/v2/admin/notifications_spec.js
Normal file
253
core/test/functional/api/v2/admin/notifications_spec.js
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
const should = require('should');
|
||||||
|
const supertest = require('supertest');
|
||||||
|
const testUtils = require('../../../../utils');
|
||||||
|
const localUtils = require('./utils');
|
||||||
|
const config = require('../../../../../../core/server/config');
|
||||||
|
const ghost = testUtils.startGhost;
|
||||||
|
let request;
|
||||||
|
|
||||||
|
describe('Notifications API V2', function () {
|
||||||
|
let ghostServer;
|
||||||
|
|
||||||
|
before(function () {
|
||||||
|
return ghost()
|
||||||
|
.then(function (_ghostServer) {
|
||||||
|
ghostServer = _ghostServer;
|
||||||
|
request = supertest.agent(config.get('url'));
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
return localUtils.doAuth(request);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Add', function () {
|
||||||
|
it('creates a new notification and sets default fields', function (done) {
|
||||||
|
const newNotification = {
|
||||||
|
type: 'info',
|
||||||
|
message: 'test notification',
|
||||||
|
custom: true,
|
||||||
|
id: 'customId'
|
||||||
|
};
|
||||||
|
|
||||||
|
request.post(localUtils.API.getApiQuery('notifications/'))
|
||||||
|
.set('Origin', config.get('url'))
|
||||||
|
.send({notifications: [newNotification]})
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||||
|
.expect(201)
|
||||||
|
.end(function (err, res) {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonResponse = res.body;
|
||||||
|
|
||||||
|
should.exist(jsonResponse.notifications);
|
||||||
|
|
||||||
|
testUtils.API.checkResponse(jsonResponse.notifications[0], 'notification');
|
||||||
|
|
||||||
|
jsonResponse.notifications[0].type.should.equal(newNotification.type);
|
||||||
|
jsonResponse.notifications[0].message.should.equal(newNotification.message);
|
||||||
|
jsonResponse.notifications[0].status.should.equal('alert');
|
||||||
|
jsonResponse.notifications[0].dismissible.should.be.true();
|
||||||
|
should.exist(jsonResponse.notifications[0].location);
|
||||||
|
jsonResponse.notifications[0].location.should.equal('bottom');
|
||||||
|
jsonResponse.notifications[0].id.should.be.a.String();
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates duplicate', function (done) {
|
||||||
|
const newNotification = {
|
||||||
|
type: 'info',
|
||||||
|
message: 'add twice',
|
||||||
|
custom: true,
|
||||||
|
id: 'customId-2'
|
||||||
|
};
|
||||||
|
|
||||||
|
request.post(localUtils.API.getApiQuery('notifications/'))
|
||||||
|
.set('Origin', config.get('url'))
|
||||||
|
.send({notifications: [newNotification]})
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||||
|
.expect(201)
|
||||||
|
.end(function (err, res) {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonResponse = res.body;
|
||||||
|
should.exist(jsonResponse.notifications);
|
||||||
|
jsonResponse.notifications.should.be.an.Array().with.lengthOf(1);
|
||||||
|
jsonResponse.notifications[0].message.should.equal(newNotification.message);
|
||||||
|
|
||||||
|
request.post(localUtils.API.getApiQuery('notifications/'))
|
||||||
|
.set('Origin', config.get('url'))
|
||||||
|
.send({notifications: [newNotification]})
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||||
|
.expect(200)
|
||||||
|
.end(function (err, res) {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonResponse = res.body;
|
||||||
|
should.exist(jsonResponse.notifications);
|
||||||
|
jsonResponse.notifications.should.be.an.Array().with.lengthOf(0);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have correct order', function () {
|
||||||
|
const firstNotification = {
|
||||||
|
status: 'alert',
|
||||||
|
type: 'info',
|
||||||
|
custom: true,
|
||||||
|
id: 'firstId',
|
||||||
|
dismissible: true,
|
||||||
|
message: '1'
|
||||||
|
};
|
||||||
|
|
||||||
|
const secondNotification = {
|
||||||
|
status: 'alert',
|
||||||
|
type: 'info',
|
||||||
|
custom: true,
|
||||||
|
id: 'secondId',
|
||||||
|
dismissible: true,
|
||||||
|
message: '2'
|
||||||
|
};
|
||||||
|
|
||||||
|
return request.post(localUtils.API.getApiQuery('notifications/'))
|
||||||
|
.set('Origin', config.get('url'))
|
||||||
|
.send({notifications: [firstNotification]})
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||||
|
.expect(201)
|
||||||
|
.then(() => {
|
||||||
|
return request.post(localUtils.API.getApiQuery('notifications/'))
|
||||||
|
.set('Origin', config.get('url'))
|
||||||
|
.send({notifications: [secondNotification]})
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||||
|
.expect(201);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
return request.get(localUtils.API.getApiQuery('notifications/'))
|
||||||
|
.set('Origin', config.get('url'))
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||||
|
.expect(200)
|
||||||
|
.then(res => {
|
||||||
|
const jsonResponse = res.body;
|
||||||
|
|
||||||
|
jsonResponse.notifications.should.be.an.Array().with.lengthOf(4);
|
||||||
|
jsonResponse.notifications[0].id.should.equal(secondNotification.id);
|
||||||
|
jsonResponse.notifications[1].id.should.equal(firstNotification.id);
|
||||||
|
jsonResponse.notifications[2].id.should.equal('customId-2');
|
||||||
|
jsonResponse.notifications[3].id.should.equal('customId');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Delete', function () {
|
||||||
|
var newNotification = {
|
||||||
|
type: 'info',
|
||||||
|
message: 'test notification',
|
||||||
|
status: 'alert',
|
||||||
|
custom: true
|
||||||
|
};
|
||||||
|
|
||||||
|
it('deletes a notification', function (done) {
|
||||||
|
// create the notification that is to be deleted
|
||||||
|
request.post(localUtils.API.getApiQuery('notifications/'))
|
||||||
|
.set('Origin', config.get('url'))
|
||||||
|
.send({notifications: [newNotification]})
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||||
|
.expect(201)
|
||||||
|
.end(function (err, res) {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const jsonResponse = res.body;
|
||||||
|
|
||||||
|
should.exist(jsonResponse.notifications);
|
||||||
|
testUtils.API.checkResponse(jsonResponse.notifications[0], 'notification');
|
||||||
|
jsonResponse.notifications.length.should.eql(1);
|
||||||
|
|
||||||
|
jsonResponse.notifications[0].type.should.equal(newNotification.type);
|
||||||
|
jsonResponse.notifications[0].message.should.equal(newNotification.message);
|
||||||
|
jsonResponse.notifications[0].status.should.equal(newNotification.status);
|
||||||
|
|
||||||
|
// begin delete test
|
||||||
|
request.del(localUtils.API.getApiQuery(`notifications/${jsonResponse.notifications[0].id}/`))
|
||||||
|
.set('Origin', config.get('url'))
|
||||||
|
.expect(204)
|
||||||
|
.end(function (err, res) {
|
||||||
|
if (err) {
|
||||||
|
return done(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.body.should.be.empty();
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns 404 when removing notification with unknown id', function () {
|
||||||
|
return request.del(localUtils.API.getApiQuery('notifications/unknown'))
|
||||||
|
.set('Origin', config.get('url'))
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||||
|
.expect(404)
|
||||||
|
.then(res => {
|
||||||
|
res.body.errors[0].message.should.equal('Notification does not exist.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('As Editor', function () {
|
||||||
|
before(function () {
|
||||||
|
return ghost()
|
||||||
|
.then(function (_ghostServer) {
|
||||||
|
ghostServer = _ghostServer;
|
||||||
|
request = supertest.agent(config.get('url'));
|
||||||
|
})
|
||||||
|
.then(function () {
|
||||||
|
return testUtils.createUser({
|
||||||
|
user: testUtils.DataGenerator.forKnex.createUser({
|
||||||
|
email: 'test+1@ghost.org'
|
||||||
|
}),
|
||||||
|
role: testUtils.DataGenerator.Content.roles[2].name
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((user) => {
|
||||||
|
request.user = user;
|
||||||
|
return localUtils.doAuth(request);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Add notification', function () {
|
||||||
|
const newNotification = {
|
||||||
|
type: 'info',
|
||||||
|
message: 'test notification',
|
||||||
|
custom: true,
|
||||||
|
id: 'customId'
|
||||||
|
};
|
||||||
|
|
||||||
|
return request.post(localUtils.API.getApiQuery('notifications/'))
|
||||||
|
.set('Origin', config.get('url'))
|
||||||
|
.send({notifications: [newNotification]})
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -934,11 +934,10 @@ startGhost = function startGhost(options) {
|
||||||
})
|
})
|
||||||
.then((clients) => {
|
.then((clients) => {
|
||||||
module.exports.existingData.clients = clients.toJSON();
|
module.exports.existingData.clients = clients.toJSON();
|
||||||
|
return models.User.findAll({columns: ['id', 'email']});
|
||||||
return models.User.findAll({columns: ['id']});
|
|
||||||
})
|
})
|
||||||
.then((users) => {
|
.then((users) => {
|
||||||
module.exports.existingData.users = users.toJSON();
|
module.exports.existingData.users = users.toJSON(module.exports.context.internal);
|
||||||
|
|
||||||
return models.Tag.findAll({columns: ['id']});
|
return models.Tag.findAll({columns: ['id']});
|
||||||
})
|
})
|
||||||
|
@ -1009,10 +1008,10 @@ startGhost = function startGhost(options) {
|
||||||
.then((clients) => {
|
.then((clients) => {
|
||||||
module.exports.existingData.clients = clients.toJSON();
|
module.exports.existingData.clients = clients.toJSON();
|
||||||
|
|
||||||
return models.User.findAll({columns: ['id']});
|
return models.User.findAll({columns: ['id', 'email']});
|
||||||
})
|
})
|
||||||
.then((users) => {
|
.then((users) => {
|
||||||
module.exports.existingData.users = users.toJSON();
|
module.exports.existingData.users = users.toJSON(module.exports.context.internal);
|
||||||
|
|
||||||
return models.Tag.findAll({columns: ['id']});
|
return models.Tag.findAll({columns: ['id']});
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue