0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-04-15 03:01:37 -05:00

Added post_id filter and total to activity feed API (#15650)

fixes https://github.com/TryGhost/Team/issues/2091
fixes https://github.com/TryGhost/Team/issues/2089

- Added new fixtures to make testing easier for the activity feed
- Improved E2E test coverage of activity feed with separate test file
- Added data.post_id filter to enable filtering by events related to a
given post
- Fixed return types in JSDoc of test agents (TypeScript interprets
these as `typeof Agent` if we don't add `InstanceType<Agent>`)
- Added total pagination metadata to activity feed API (to allow a basic
type of pagination using filters)
This commit is contained in:
Simon Backx 2022-10-18 15:52:04 +02:00 committed by GitHub
parent 2f43e71f7f
commit a01fb5f1aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1051 additions and 89 deletions

View file

@ -435,10 +435,7 @@ module.exports = {
method: 'browse'
},
async query(frame) {
const events = await membersService.api.events.getEventTimeline(frame.options);
return {
events
};
return await membersService.api.events.getEventTimeline(frame.options);
}
}
};

View file

@ -76,11 +76,12 @@ function bulkAction(bulkActionResult, _apiConfig, frame) {
/**
*
* @returns {{events: any[]}}
* @returns {{events: any[], meta: any}}
*/
function activityFeed(data, _apiConfig, frame) {
return {
events: data.events.map(e => mappers.activityFeedEvents(e, frame))
events: data.events.map(e => mappers.activityFeedEvents(e, frame)),
meta: data.meta
};
}

View file

@ -10,7 +10,31 @@ const MemberClickEvent = ghostBookshelf.Model.extend({
member() {
return this.belongsTo('Member', 'member_id', 'id');
},
filterExpansions: function filterExpansions() {
const expansions = [{
key: 'post_id',
replacement: 'link.post_id'
}];
return expansions;
},
filterRelations() {
return {
link: {
// Mongo-knex doesn't support belongsTo relations
tableName: 'redirects',
tableNameAs: 'link',
type: 'manyToMany',
joinTable: 'members_click_events',
joinFrom: 'id',
joinTo: 'redirect_id'
}
};
}
}, {
async edit() {
throw new errors.IncorrectUsageError({message: 'Cannot edit MemberClickEvent'});

View file

@ -25,6 +25,21 @@ const MemberPaidSubscriptionEvent = ghostBookshelf.Model.extend({
.groupByRaw('currency, DATE(created_at)')
.orderByRaw('DATE(created_at)');
}
},
filterRelations() {
return {
subscriptionCreatedEvent: {
// Mongo-knex doesn't support belongsTo relations
tableName: 'members_subscription_created_events',
tableNameAs: 'subscriptionCreatedEvent',
type: 'manyToMany',
joinTable: 'members_paid_subscription_events',
joinFrom: 'id',
joinToForeign: 'subscription_id',
joinTo: 'subscription_id'
}
};
}
}, {
permittedOptions(methodName) {

View file

@ -0,0 +1,607 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Activity Feed API Can filter events by post id 1: [body] 1`] = `
Object {
"events": Array [
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
],
"meta": Object {
"pagination": Object {
"limit": 10,
"next": null,
"page": null,
"pages": 1,
"prev": null,
"total": 8,
},
},
}
`;
exports[`Activity Feed API Can filter events by post id 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "15074",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Activity Feed API Can limit events 1: [body] 1`] = `
Object {
"events": Array [
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
],
"meta": Object {
"pagination": Object {
"limit": "2",
"next": null,
"page": null,
"pages": 4,
"prev": null,
"total": 8,
},
},
}
`;
exports[`Activity Feed API Can limit events 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1239",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Activity Feed API Returns click events in activity feed 1: [body] 1`] = `
Object {
"events": Array [
Object {
"data": Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"link": Object {
"from": "/r/0",
"to": "https:://ghost.org",
},
"member": Object {
"avatar_image": null,
"email": "member1@test.com",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Mr Egg",
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"post": Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"title": "HTML Ipsum",
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
},
"type": Any<String>,
},
Object {
"data": Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"link": Object {
"from": "/r/1",
"to": "https:://ghost.org",
},
"member": Object {
"avatar_image": null,
"email": "member2@test.com",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": null,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"post": Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"title": "Ghostly Kitchen Sink",
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
},
"type": Any<String>,
},
Object {
"data": Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"link": Object {
"from": "/r/2",
"to": "https:://ghost.org",
},
"member": Object {
"avatar_image": null,
"email": "paid@test.com",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Egon Spengler",
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"post": Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"title": "Short and Sweet",
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
},
"type": Any<String>,
},
Object {
"data": Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"link": Object {
"from": "/r/3",
"to": "https:://ghost.org",
},
"member": Object {
"avatar_image": null,
"email": "trialing@test.com",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Ray Stantz",
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"post": Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"title": "Not finished yet",
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
},
"type": Any<String>,
},
Object {
"data": Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"link": Object {
"from": "/r/4",
"to": "https:://ghost.org",
},
"member": Object {
"avatar_image": null,
"email": "comped@test.com",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Vinz Clortho",
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"post": Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"title": "Not so short, bit complex",
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
},
"type": Any<String>,
},
Object {
"data": Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"link": Object {
"from": "/r/5",
"to": "https:://ghost.org",
},
"member": Object {
"avatar_image": null,
"email": "vip@test.com",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Winston Zeddemore",
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"post": Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"title": "This is a static page",
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
},
"type": Any<String>,
},
Object {
"data": Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"link": Object {
"from": "/r/6",
"to": "https:://ghost.org",
},
"member": Object {
"avatar_image": null,
"email": "vip-paid@test.com",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Peter Venkman",
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"post": Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"title": "This is a draft static page",
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
},
"type": Any<String>,
},
Object {
"data": Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"link": Object {
"from": "/r/7",
"to": "https:://ghost.org",
},
"member": Object {
"avatar_image": null,
"email": "with-product@test.com",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Dana Barrett",
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"post": Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"title": "This is a scheduled post!!",
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
},
"type": Any<String>,
},
],
"meta": Object {
"pagination": Object {
"limit": 10,
"next": null,
"page": null,
"pages": 1,
"prev": null,
"total": 8,
},
},
}
`;
exports[`Activity Feed API Returns click events in activity feed 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "3722",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Activity Feed API Returns comments in activity feed 1: [body] 1`] = `
Object {
"events": Array [
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
],
"meta": Object {
"pagination": Object {
"limit": 10,
"next": null,
"page": null,
"pages": 1,
"prev": null,
"total": 2,
},
},
}
`;
exports[`Activity Feed API Returns comments in activity feed 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "1238",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Activity Feed API Returns feedback events in activity feed 1: [body] 1`] = `
Object {
"events": Array [
Object {
"data": Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"member": Object {
"avatar_image": null,
"email": "member1@test.com",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Mr Egg",
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"post": Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"title": "HTML Ipsum",
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"score": Any<Number>,
},
"type": Any<String>,
},
Object {
"data": Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"member": Object {
"avatar_image": null,
"email": "member2@test.com",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": null,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"post": Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"title": "Ghostly Kitchen Sink",
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"score": Any<Number>,
},
"type": Any<String>,
},
Object {
"data": Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"member": Object {
"avatar_image": null,
"email": "paid@test.com",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Egon Spengler",
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"post": Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"title": "Short and Sweet",
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"score": Any<Number>,
},
"type": Any<String>,
},
Object {
"data": Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"member": Object {
"avatar_image": null,
"email": "trialing@test.com",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Ray Stantz",
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"post": Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"title": "Not finished yet",
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"score": Any<Number>,
},
"type": Any<String>,
},
Object {
"data": Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"member": Object {
"avatar_image": null,
"email": "comped@test.com",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Vinz Clortho",
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"post": Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"title": "Not so short, bit complex",
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"score": Any<Number>,
},
"type": Any<String>,
},
Object {
"data": Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"member": Object {
"avatar_image": null,
"email": "vip@test.com",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Winston Zeddemore",
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"post": Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"title": "This is a static page",
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"score": Any<Number>,
},
"type": Any<String>,
},
Object {
"data": Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"member": Object {
"avatar_image": null,
"email": "vip-paid@test.com",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Peter Venkman",
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"post": Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"title": "This is a draft static page",
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"score": Any<Number>,
},
"type": Any<String>,
},
Object {
"data": Object {
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"member": Object {
"avatar_image": null,
"email": "with-product@test.com",
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Dana Barrett",
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"post": Object {
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"title": "This is a scheduled post!!",
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
"score": Any<Number>,
},
"type": Any<String>,
},
],
"meta": Object {
"pagination": Object {
"limit": 10,
"next": null,
"page": null,
"pages": 1,
"prev": null,
"total": 8,
},
},
}
`;
exports[`Activity Feed API Returns feedback events in activity feed 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "3690",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Activity Feed API Returns signup events in activity feed 1: [body] 1`] = `
Object {
"events": Array [
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
],
"meta": Object {
"pagination": Object {
"limit": 10,
"next": null,
"page": null,
"pages": 1,
"prev": null,
"total": 8,
},
},
}
`;
exports[`Activity Feed API Returns signup events in activity feed 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "23027",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;

View file

@ -274,6 +274,12 @@ Object {
"type": Any<String>,
},
],
"meta": Object {
"pagination": Object {
"limit": 10,
"total": 5,
},
},
}
`;
@ -281,7 +287,56 @@ exports[`Members API - member attribution Returns sign up attributions in activi
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "9204",
"content-length": "9249",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Members API - member attribution Returns sign up attributions of all types in activity feed 1: [body] 1`] = `
Object {
"events": Array [
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
Object {
"data": Any<Object>,
"type": Any<String>,
},
],
"meta": Object {
"pagination": Object {
"limit": 10,
"next": null,
"page": null,
"pages": 1,
"prev": null,
"total": 5,
},
},
}
`;
exports[`Members API - member attribution Returns sign up attributions of all types in activity feed 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "9295",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
@ -4002,7 +4057,7 @@ exports[`Members API Can subscribe to a newsletter 5: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "5053",
"content-length": "5144",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",

View file

@ -96,8 +96,8 @@ Object {
"count": 1,
"date": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"negative_delta": 0,
"positive_delta": 1,
"signups": 1,
"positive_delta": 4,
"signups": 4,
"tier": StringMatching /\\[a-f0-9\\]\\{24\\}/,
},
Object {

View file

@ -0,0 +1,178 @@
const {agentProvider, mockManager, fixtureManager, matchers} = require('../../utils/e2e-framework');
const {anyEtag, anyObjectId, anyUuid, anyISODate, anyString, anyObject, anyNumber} = matchers;
const assert = require('assert');
let agent;
describe('Activity Feed API', function () {
before(async function () {
agent = await agentProvider.getAdminAPIAgent();
await fixtureManager.init('posts', 'newsletters', 'members:newsletters', 'comments', 'redirects', 'clicks', 'feedback');
await agent.loginAsOwner();
});
beforeEach(function () {
mockManager.mockStripe();
mockManager.mockMail();
});
afterEach(function () {
mockManager.restore();
});
// Activity feed
it('Returns comments in activity feed', async function () {
// Check activity feed
await agent
.get(`/members/events?filter=type:comment_event`)
.expectStatus(200)
.matchHeaderSnapshot({
etag: anyEtag
})
.matchBodySnapshot({
events: new Array(2).fill({
type: anyString,
data: anyObject
})
})
.expect(({body}) => {
assert(body.events.find(e => e.type === 'comment_event'), 'Expected a comment event');
assert(!body.events.find(e => e.type !== 'comment_event'), 'Expected only comment events');
});
});
it('Returns click events in activity feed', async function () {
// Check activity feed
await agent
.get(`/members/events?filter=type:click_event`)
.expectStatus(200)
.matchHeaderSnapshot({
etag: anyEtag
})
.matchBodySnapshot({
events: new Array(8).fill({
type: anyString,
data: {
created_at: anyISODate,
member: {
id: anyObjectId,
uuid: anyUuid
},
post: {
id: anyObjectId,
uuid: anyUuid,
url: anyString
}
}
})
})
.expect(({body}) => {
assert(body.events.find(e => e.type === 'click_event'), 'Expected a click event');
assert(!body.events.find(e => e.type !== 'click_event'), 'Expected only click events');
});
});
it('Returns feedback events in activity feed', async function () {
// Check activity feed
await agent
.get(`/members/events?filter=type:feedback_event`)
.expectStatus(200)
.matchHeaderSnapshot({
etag: anyEtag
})
.matchBodySnapshot({
events: new Array(8).fill({
type: anyString,
data: {
created_at: anyISODate,
id: anyObjectId,
member: {
id: anyObjectId,
uuid: anyUuid
},
post: {
id: anyObjectId,
uuid: anyUuid,
url: anyString
},
score: anyNumber
}
})
})
.expect(({body}) => {
assert(body.events.find(e => e.type === 'feedback_event'), 'Expected a feedback event');
assert(!body.events.find(e => e.type !== 'feedback_event'), 'Expected only feedback events');
});
});
it('Returns signup events in activity feed', async function () {
// Check activity feed
await agent
.get(`/members/events?filter=type:signup_event`)
.expectStatus(200)
.matchHeaderSnapshot({
etag: anyEtag
})
.matchBodySnapshot({
events: new Array(8).fill({
type: anyString,
data: anyObject
})
})
.expect(({body}) => {
assert(body.events.find(e => e.type === 'signup_event'), 'Expected a signup event');
assert(!body.events.find(e => e.type !== 'signup_event'), 'Expected only signup events');
});
});
it('Can filter events by post id', async function () {
const postId = fixtureManager.get('posts', 0).id;
await agent
.get(`/members/events?filter=data.post_id:${postId}`)
.expectStatus(200)
.matchHeaderSnapshot({
etag: anyEtag
})
.matchBodySnapshot({
events: new Array(8).fill({
type: anyString,
data: anyObject
})
})
.expect(({body}) => {
assert(!body.events.find(e => (e.data?.post?.id ?? e.data?.attribution?.id) !== postId), 'Should only return events for the post');
// Check all post_id event types are covered by this test
assert(body.events.find(e => e.type === 'click_event'), 'Expected a click event');
assert(body.events.find(e => e.type === 'comment_event'), 'Expected a comment event');
assert(body.events.find(e => e.type === 'feedback_event'), 'Expected a feedback event');
assert(body.events.find(e => e.type === 'signup_event'), 'Expected a signup event');
assert(body.events.find(e => e.type === 'subscription_event'), 'Expected a subscription event');
// Assert total is correct
assert.equal(body.meta.pagination.total, 8);
});
});
it('Can limit events', async function () {
const postId = fixtureManager.get('posts', 0).id;
await agent
.get(`/members/events?filter=data.post_id:${postId}&limit=2`)
.expectStatus(200)
.matchHeaderSnapshot({
etag: anyEtag
})
.matchBodySnapshot({
events: new Array(2).fill({
type: anyString,
data: anyObject
})
})
.expect(({body}) => {
assert(!body.events.find(e => (e.data?.post?.id ?? e.data?.attribution?.id) !== postId), 'Should only return events for the post');
// Assert total is correct
assert.equal(body.meta.pagination.total, 8);
});
});
});

View file

@ -390,7 +390,7 @@ describe('Members API - member attribution', function () {
});
// Activity feed
it('Returns sign up attributions in activity feed', async function () {
it('Returns sign up attributions of all types in activity feed', async function () {
// Check activity feed
await agent
.get(`/members/events/?filter=type:signup_event`)
@ -431,56 +431,6 @@ describe('Members API', function () {
mockManager.restore();
});
// Activity feed
it('Returns comments in activity feed', async function () {
// Check activity feed
await agent
.get(`/members/events?filter=type:comment_event`)
.expectStatus(200)
.matchHeaderSnapshot({
etag: anyEtag
})
.matchBodySnapshot({
events: new Array(2).fill({
type: anyString,
data: anyObject
})
})
.expect(({body}) => {
should(body.events.find(e => e.type === 'comment_event')).not.be.undefined();
});
});
it('Returns click events in activity feed', async function () {
// Check activity feed
await agent
.get(`/members/events?filter=type:click_event`)
.expectStatus(200)
.matchHeaderSnapshot({
etag: anyEtag
})
.matchBodySnapshot({
events: new Array(8).fill({
type: anyString,
data: {
created_at: anyISODate,
member: {
id: anyObjectId,
uuid: anyUuid
},
post: {
id: anyObjectId,
uuid: anyUuid,
url: anyString
}
}
})
})
.expect(({body}) => {
should(body.events.find(e => e.type === 'click_event')).not.be.undefined();
});
});
// List Members
it('Can browse', async function () {
@ -1801,6 +1751,9 @@ describe('Members API', function () {
}
}
},
{
type: 'signup_event'
},
{
type: 'newsletter_event',
data: {
@ -1811,9 +1764,6 @@ describe('Members API', function () {
id: newsletters[0].id
}
}
},
{
type: 'signup_event'
}
]);

View file

@ -372,6 +372,16 @@ Object {
"type": Any<String>,
},
],
"meta": Object {
"pagination": Object {
"limit": 10,
"next": null,
"page": null,
"pages": 1,
"prev": null,
"total": 8,
},
},
}
`;
@ -391,7 +401,7 @@ exports[`Members API Member attribution Returns subscription created attribution
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "14722",
"content-length": "14813",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
@ -402,6 +412,16 @@ Object {
exports[`Members API Member attribution empty initial activity feed 1: [body] 1`] = `
Object {
"events": Array [],
"meta": Object {
"pagination": Object {
"limit": 10,
"next": null,
"page": null,
"pages": 0,
"prev": null,
"total": 0,
},
},
}
`;
@ -409,7 +429,7 @@ exports[`Members API Member attribution empty initial activity feed 2: [headers]
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "13",
"content-length": "104",
"content-type": "application/json; charset=utf-8",
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",

View file

@ -0,0 +1,23 @@
const sinon = require('sinon');
const models = require('../../../../core/server/models');
describe('Unit: models/MemberClickEvent', function () {
before(function () {
models.init();
});
afterEach(function () {
sinon.restore();
});
it('Has link and member relations', function () {
const model = models.MemberClickEvent.forge({id: 'any'});
model.link();
model.member();
});
it('Has filter relations', function () {
const model = models.MemberClickEvent.forge({id: 'any'});
model.filterRelations();
});
});

View file

@ -0,0 +1,23 @@
const sinon = require('sinon');
const models = require('../../../../core/server/models');
describe('Unit: models/MemberPaidSubscriptionEvent', function () {
before(function () {
models.init();
});
afterEach(function () {
sinon.restore();
});
it('Has member and subscriptionCreatedEvent relations', function () {
const model = models.MemberPaidSubscriptionEvent.forge({id: 'any'});
model.member();
model.subscriptionCreatedEvent();
});
it('Has filter relations', function () {
const model = models.MemberPaidSubscriptionEvent.forge({id: 'any'});
model.filterRelations();
});
});

View file

@ -158,7 +158,7 @@ const resetData = async () => {
* Creates a ContentAPITestAgent which is a drop-in substitution for supertest.
* It is automatically hooked up to the Content API so you can make requests to e.g.
* agent.get('/posts/') without having to worry about URL paths
* @returns {Promise<ContentAPITestAgent>} agent
* @returns {Promise<InstanceType<ContentAPITestAgent>>} agent
*/
const getContentAPIAgent = async () => {
try {
@ -182,7 +182,7 @@ const getContentAPIAgent = async () => {
*
* @param {Object} [options={}]
* @param {Boolean} [options.members] Include members in the boot process
* @returns {Promise<AdminAPITestAgent>} agent
* @returns {Promise<InstanceType<AdminAPITestAgent>>} agent
*/
const getAdminAPIAgent = async (options = {}) => {
const bootOptions = {};
@ -210,7 +210,7 @@ const getAdminAPIAgent = async (options = {}) => {
* It is automatically hooked up to the Members API so you can make requests to e.g.
* agent.get('/webhooks/stripe/') without having to worry about URL paths
*
* @returns {Promise<MembersAPITestAgent>} agent
* @returns {Promise<InstanceType<MembersAPITestAgent>>} agent
*/
const getMembersAPIAgent = async () => {
const bootOptions = {
@ -235,7 +235,7 @@ const getMembersAPIAgent = async () => {
* It is automatically hooked up to the Ghost API so you can make requests to e.g.
* agent.get('/well-known/jwks.json') without having to worry about URL paths
*
* @returns {Promise<GhostAPITestAgent>} agent
* @returns {Promise<InstanceType<GhostAPITestAgent>>} agent
*/
const getGhostAPIAgent = async () => {
const bootOptions = {
@ -258,7 +258,7 @@ const getGhostAPIAgent = async () => {
/**
*
* @returns {Promise<{adminAgent: AdminAPITestAgent, membersAgent: MembersAPITestAgent}>} agents
* @returns {Promise<{adminAgent: InstanceType<AdminAPITestAgent>, membersAgent: InstanceType<MembersAPITestAgent>}>} agents
*/
const getAgentsForMembers = async () => {
let membersAgent;

View file

@ -655,6 +655,12 @@ const fixtures = {
}));
},
insertFeedback: async function insertFeedback() {
await Promise.all(DataGenerator.forKnex.members_feedback.map((feedback) => {
return models.MemberFeedback.add(feedback, context.internal);
}));
},
insertSnippets: function insertSnippets() {
return Promise.map(DataGenerator.forKnex.snippets, function (snippet) {
return models.Snippet.add(snippet, context.internal);
@ -794,6 +800,9 @@ const toDoList = {
},
clicks: function insertClicks() {
return fixtures.insertClicks();
},
feedback: function insertFeedback() {
return fixtures.insertFeedback();
}
};

View file

@ -1659,7 +1659,20 @@ DataGenerator.forKnex = (function () {
const members_paid_subscription_events = [
createBasic(DataGenerator.Content.members_paid_subscription_events[0]),
createBasic(DataGenerator.Content.members_paid_subscription_events[1]),
createBasic(DataGenerator.Content.members_paid_subscription_events[2])
createBasic(DataGenerator.Content.members_paid_subscription_events[2]),
...members_subscription_created_events.map((e) => {
return {
id: ObjectId().toHexString(),
type: 'created',
mrr_delta: 1000,
currency: 'usd',
source: 'stripe',
subscription_id: e.subscription_id,
member_id: e.member_id,
from_plan: null,
to_plan: '173e16a1fffa7d232b398e4a9b08d266a456ae8f3d23e5f11cc608ced6730bb8'
};
})
];
const redirects = posts.map((post, index) => {
@ -1682,6 +1695,16 @@ DataGenerator.forKnex = (function () {
};
});
const members_feedback = posts.map((redirect, index) => {
return {
id: ObjectId().toHexString(),
member_id: members[index].id,
post_id: redirect.id,
score: index % 2,
created_at: new Date()
};
});
const snippets = [
createBasic(DataGenerator.Content.snippets[0])
];
@ -1761,7 +1784,8 @@ DataGenerator.forKnex = (function () {
members_paid_subscription_events,
members_created_events,
members_subscription_created_events,
members_click_events
members_click_events,
members_feedback
};
}());

View file

@ -36,6 +36,7 @@ module.exports = class EventRepository {
if (!options.limit) {
options.limit = 10;
}
let filters = this.getNQLSubset(options.filter);
// Changing this order might need a change in the query functions
// because of the different underlying models.
@ -43,15 +44,21 @@ module.exports = class EventRepository {
// Create a list of all events that can be queried
const pageActions = [
{type: 'newsletter_event', action: 'getNewsletterSubscriptionEvents'},
{type: 'subscription_event', action: 'getSubscriptionEvents'},
{type: 'login_event', action: 'getLoginEvents'},
{type: 'payment_event', action: 'getPaymentEvents'},
{type: 'signup_event', action: 'getSignupEvents'},
{type: 'comment_event', action: 'getCommentEvents'},
{type: 'click_event', action: 'getClickEvents'}
{type: 'click_event', action: 'getClickEvents'},
{type: 'signup_event', action: 'getSignupEvents'},
{type: 'subscription_event', action: 'getSubscriptionEvents'}
];
// Some events are not filterable by post_id
if (!filters['data.post_id']) {
pageActions.push(
{type: 'newsletter_event', action: 'getNewsletterSubscriptionEvents'},
{type: 'login_event', action: 'getLoginEvents'},
{type: 'payment_event', action: 'getPaymentEvents'}
);
}
if (this._EmailRecipient) {
pageActions.push({type: 'email_delivered_event', action: 'getEmailDeliveredEvents'});
pageActions.push({type: 'email_opened_event', action: 'getEmailOpenedEvents'});
@ -62,8 +69,6 @@ module.exports = class EventRepository {
pageActions.push({type: 'feedback_event', action: 'getFeedbackEvents'});
}
let filters = this.getNQLSubset(options.filter);
//Filter events to query
const filteredPages = filters.type ? pageActions.filter(page => nql(filters.type).queryJSON(page)) : pageActions;
@ -72,14 +77,28 @@ module.exports = class EventRepository {
const allEventPages = await Promise.all(pages);
const allEvents = allEventPages.reduce((accumulator, page) => accumulator.concat(page.data), []);
const allEvents = allEventPages.flatMap(page => page.data);
const totalEvents = allEventPages.reduce((accumulator, page) => accumulator + page.meta.pagination.total, 0);
return allEvents.sort((a, b) => {
return new Date(b.data.created_at) - new Date(a.data.created_at);
}).reduce((memo, event) => {
//disable the event filtering
return memo.concat(event);
}, []).slice(0, options.limit);
return {
events: allEvents.sort(
(a, b) => {
return new Date(b.data.created_at).getTime() - new Date(a.data.created_at).getTime();
}
).slice(0, options.limit),
meta: {
pagination: {
limit: options.limit,
total: totalEvents,
pages: options.limit > 0 ? Math.ceil(totalEvents / options.limit) : null,
// Other values are unavailable (not possible to calculate easily)
page: null,
next: null,
prev: null
}
}
};
}
async registerPayment(data) {
@ -162,6 +181,10 @@ module.exports = class EventRepository {
if (filters['data.member_id']) {
options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:'));
}
if (filters['data.post_id']) {
options.filter.push(filters['data.post_id'].replace(/data.post_id:/g, 'subscriptionCreatedEvent.attribution_id:'));
options.filter.push('subscriptionCreatedEvent.attribution_type:post');
}
options.filter = options.filter.join('+');
const {data: models, meta} = await this._MemberPaidSubscriptionEvent.findPage(options);
@ -288,6 +311,10 @@ module.exports = class EventRepository {
if (filters['data.source']) {
options.filter.push(filters['data.source'].replace(/data.source:/g, 'source:'));
}
if (filters['data.post_id']) {
options.filter.push(filters['data.post_id'].replace(/data.post_id:/g, 'attribution_id:'));
options.filter.push('attribution_type:post');
}
options.filter = options.filter.join('+');
const {data: models, meta} = await this._MemberCreatedEvent.findPage(options);
@ -320,6 +347,9 @@ module.exports = class EventRepository {
if (filters['data.member_id']) {
options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:'));
}
if (filters['data.post_id']) {
options.filter.push(filters['data.post_id'].replace(/data.post_id:/g, 'post_id:'));
}
options.filter = options.filter.join('+');
const {data: models, meta} = await this._Comment.findPage(options);
@ -349,6 +379,9 @@ module.exports = class EventRepository {
if (filters['data.member_id']) {
options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:'));
}
if (filters['data.post_id']) {
options.filter.push(filters['data.post_id'].replace(/data.post_id:/g, 'post_id:'));
}
options.filter = options.filter.join('+');
const {data: models, meta} = await this._MemberLinkClickEvent.findPage(options);
@ -378,6 +411,9 @@ module.exports = class EventRepository {
if (filters['data.member_id']) {
options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:'));
}
if (filters['data.post_id']) {
options.filter.push(filters['data.post_id'].replace(/data.post_id:/g, 'post_id:'));
}
options.filter = options.filter.join('+');
const {data: models, meta} = await this._MemberFeedback.findPage(options);
@ -519,7 +555,7 @@ module.exports = class EventRepository {
const lex = nql(filter).lex();
const allowedFilters = ['type','data.created_at','data.member_id'];
const allowedFilters = ['type','data.created_at','data.member_id', 'data.post_id'];
const properties = lex
.filter(x => x.token === 'PROP')
.map(x => x.matched.slice(0, -1));