mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-28 05:14:12 +03:00
Added a function to parse a NQL subset
refs https://github.com/TryGhost/Team/issues/1277 - This will allow to filter events within `getEventTimeline` - The subset of NQL has the following rules: - Only one level of filters, now parenthesis allowed - Only three filter keys allowed - No `or` allowed outside of the bracket notation (this is allowed: `type:-[email_opened_event,email_failed_event]` but this isn't: `type:1,data.created_at:1`) - The return is an object with a NQL filter by allowed filter key
This commit is contained in:
parent
e4e28aae3d
commit
1370682a60
@ -1,3 +1,4 @@
|
||||
const errors = require('@tryghost/errors');
|
||||
const nql = require('@nexes/nql');
|
||||
|
||||
module.exports = class EventRepository {
|
||||
@ -215,6 +216,63 @@ module.exports = class EventRepository {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a subset of NQL.
|
||||
* There are only a few properties allowed.
|
||||
* Parenthesis are forbidden.
|
||||
* Only ANDs are supported when combining properties.
|
||||
*/
|
||||
getNQLSubset(filter) {
|
||||
if (!filter) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const lex = nql(filter).lex();
|
||||
|
||||
const allowedFilters = ['type','data.created_at','data.member_id'];
|
||||
const properties = lex
|
||||
.filter(x => x.token === 'PROP')
|
||||
.map(x => x.matched.slice(0, -1));
|
||||
if (properties.some(prop => !allowedFilters.includes(prop))) {
|
||||
throw new errors.IncorrectUsageError({
|
||||
message: 'The only allowed filters are `type`, `data.created_at` and `data.member_id`'
|
||||
});
|
||||
}
|
||||
|
||||
if (lex.find(x => x.token === 'LPAREN')) {
|
||||
throw new errors.IncorrectUsageError({
|
||||
message: 'The filter can\'t contain parenthesis.'
|
||||
});
|
||||
}
|
||||
|
||||
const jsonFilter = nql(filter).toJSON();
|
||||
const keys = Object.keys(jsonFilter);
|
||||
|
||||
if (keys.length === 1 && keys[0] === '$or') {
|
||||
throw new errors.IncorrectUsageError({
|
||||
message: 'The top level-filters can only combined with ANDs (+) and not ORs (,).'
|
||||
});
|
||||
}
|
||||
|
||||
// The filter is validated, it only contains one level of filters concatenated with `+`
|
||||
const filters = filter.split('+');
|
||||
|
||||
/** @type {Object.<string, string>} */
|
||||
let result = {};
|
||||
|
||||
for (const f of filters) {
|
||||
// dirty way to parse a property, but it works according to https://github.com/NexesJS/NQL-Lang/blob/0e12d799a3a9c4d8651444e9284ce16c19cbc4f0/src/nql.l#L18
|
||||
const key = f.split(':')[0];
|
||||
if (!result[key]) {
|
||||
result[key] = f;
|
||||
} else {
|
||||
result[key] += '+' + f;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async getEventTimeline(options = {}) {
|
||||
if (!options.limit) {
|
||||
options.limit = 10;
|
||||
@ -240,10 +298,6 @@ module.exports = class EventRepository {
|
||||
return allEvents.sort((a, b) => {
|
||||
return new Date(b.data.created_at) - new Date(a.data.created_at);
|
||||
}).reduce((memo, event, i) => {
|
||||
if (options.filter && !nql(options.filter).queryJSON(event)) {
|
||||
// Filter out event
|
||||
return memo;
|
||||
}
|
||||
if (event.type === 'newsletter_event' && event.data.subscribed) {
|
||||
const previousEvent = allEvents[i - 1];
|
||||
const nextEvent = allEvents[i + 1];
|
||||
|
78
ghost/members-api/test/unit/lib/repositories/event.test.js
Normal file
78
ghost/members-api/test/unit/lib/repositories/event.test.js
Normal file
@ -0,0 +1,78 @@
|
||||
const should = require('should');
|
||||
const EventRepository = require('../../../../lib/repositories/event');
|
||||
const errors = require('@tryghost/errors');
|
||||
|
||||
describe('EventRepository', function () {
|
||||
describe('getNQLSubset', function () {
|
||||
let eventRepository;
|
||||
|
||||
before(function () {
|
||||
eventRepository = new EventRepository({
|
||||
EmailRecipient: null,
|
||||
MemberSubscribeEvent: null,
|
||||
MemberPaymentEvent: null,
|
||||
MemberStatusEvent: null,
|
||||
MemberLoginEvent: null,
|
||||
MemberPaidSubscriptionEvent: null,
|
||||
labsService: null
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when processing a filter with parenthesis', function () {
|
||||
should.throws(() => {
|
||||
eventRepository.getNQLSubset('(type:1)');
|
||||
}, errors.IncorrectUsageError);
|
||||
should.throws(() => {
|
||||
eventRepository.getNQLSubset('type:1+(data.created_at:1+data.member_id:1)');
|
||||
}, errors.IncorrectUsageError);
|
||||
});
|
||||
|
||||
it('throws when using properties that aren\'t in the allowlist', function () {
|
||||
should.throws(() => {
|
||||
eventRepository.getNQLSubset('(types:1)');
|
||||
}, errors.IncorrectUsageError);
|
||||
});
|
||||
|
||||
it('throws when using an OR', function () {
|
||||
should.throws(() => {
|
||||
eventRepository.getNQLSubset('type:1,data.created_at:1');
|
||||
}, errors.IncorrectUsageError);
|
||||
|
||||
should.throws(() => {
|
||||
eventRepository.getNQLSubset('type:1+data.created_at:1,data.member_id:1');
|
||||
}, errors.IncorrectUsageError);
|
||||
|
||||
should.throws(() => {
|
||||
eventRepository.getNQLSubset('type:1,data.created_at:1+data.member_id:1');
|
||||
}, errors.IncorrectUsageError);
|
||||
});
|
||||
|
||||
it('passes when using it correctly with one filter', function () {
|
||||
const res = eventRepository.getNQLSubset('type:email_delivered_event');
|
||||
res.should.be.an.Object();
|
||||
res.should.deepEqual({
|
||||
type: 'type:email_delivered_event'
|
||||
});
|
||||
});
|
||||
|
||||
it('passes when using it correctly with multiple filters', function () {
|
||||
const res = eventRepository.getNQLSubset('type:-[email_delivered_event,email_opened_event,email_failed_event]+data.created_at:<0+data.member_id:123');
|
||||
res.should.be.an.Object();
|
||||
res.should.deepEqual({
|
||||
'data.created_at': 'data.created_at:<0',
|
||||
'data.member_id': 'data.member_id:123',
|
||||
type: 'type:-[email_delivered_event,email_opened_event,email_failed_event]'
|
||||
});
|
||||
});
|
||||
|
||||
it('passes when using it correctly with multiple filters used several times', function () {
|
||||
const res = eventRepository.getNQLSubset('type:-email_delivered_event+data.created_at:<0+data.member_id:123+type:-[email_opened_event,email_failed_event]+data.created_at:>10');
|
||||
res.should.be.an.Object();
|
||||
res.should.deepEqual({
|
||||
'data.created_at': 'data.created_at:<0+data.created_at:>10',
|
||||
'data.member_id': 'data.member_id:123',
|
||||
type: 'type:-email_delivered_event+type:-[email_opened_event,email_failed_event]'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user