diff --git a/ghost/core/core/server/api/endpoints/members.js b/ghost/core/core/server/api/endpoints/members.js index 9cee71bab749..30dbba7b13f9 100644 --- a/ghost/core/core/server/api/endpoints/members.js +++ b/ghost/core/core/server/api/endpoints/members.js @@ -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); } } }; diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/output/members.js b/ghost/core/core/server/api/endpoints/utils/serializers/output/members.js index 4182e63e74eb..88f61759c22a 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/output/members.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/output/members.js @@ -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 }; } diff --git a/ghost/core/core/server/models/member-click-event.js b/ghost/core/core/server/models/member-click-event.js index 7c86860ff8c0..29250c625f54 100644 --- a/ghost/core/core/server/models/member-click-event.js +++ b/ghost/core/core/server/models/member-click-event.js @@ -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'}); diff --git a/ghost/core/core/server/models/member-paid-subscription-event.js b/ghost/core/core/server/models/member-paid-subscription-event.js index 7769d6823d17..d3315648123d 100644 --- a/ghost/core/core/server/models/member-paid-subscription-event.js +++ b/ghost/core/core/server/models/member-paid-subscription-event.js @@ -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) { diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap new file mode 100644 index 000000000000..0ab04a66629a --- /dev/null +++ b/ghost/core/test/e2e-api/admin/__snapshots__/activity-feed.test.js.snap @@ -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, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + ], + "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, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + ], + "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, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + }, + "type": Any, + }, + 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, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + }, + "type": Any, + }, + 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, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + }, + "type": Any, + }, + 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, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + }, + "type": Any, + }, + 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, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + }, + "type": Any, + }, + 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, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + }, + "type": Any, + }, + 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, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + }, + "type": Any, + }, + 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, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + }, + "type": Any, + }, + ], + "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, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + ], + "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, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + "score": Any, + }, + "type": Any, + }, + 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, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + "score": Any, + }, + "type": Any, + }, + 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, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + "score": Any, + }, + "type": Any, + }, + 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, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + "score": Any, + }, + "type": Any, + }, + 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, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + "score": Any, + }, + "type": Any, + }, + 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, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + "score": Any, + }, + "type": Any, + }, + 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, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + "score": Any, + }, + "type": Any, + }, + 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, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + "score": Any, + }, + "type": Any, + }, + ], + "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, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + ], + "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", +} +`; diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap index 4155c7df4d71..4c8fa6b716c5 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/members.test.js.snap @@ -274,6 +274,12 @@ Object { "type": Any, }, ], + "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, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + ], + "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", diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/stats.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/stats.test.js.snap index 6c22af9e006f..d447c70c8ac1 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/stats.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/stats.test.js.snap @@ -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 { diff --git a/ghost/core/test/e2e-api/admin/activity-feed.test.js b/ghost/core/test/e2e-api/admin/activity-feed.test.js new file mode 100644 index 000000000000..c1607bab6410 --- /dev/null +++ b/ghost/core/test/e2e-api/admin/activity-feed.test.js @@ -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); + }); + }); +}); diff --git a/ghost/core/test/e2e-api/admin/members.test.js b/ghost/core/test/e2e-api/admin/members.test.js index 4a6cae97664b..11eaf32a9863 100644 --- a/ghost/core/test/e2e-api/admin/members.test.js +++ b/ghost/core/test/e2e-api/admin/members.test.js @@ -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' } ]); diff --git a/ghost/core/test/e2e-api/members/__snapshots__/webhooks.test.js.snap b/ghost/core/test/e2e-api/members/__snapshots__/webhooks.test.js.snap index cd5e63da049a..79df7855ee7d 100644 --- a/ghost/core/test/e2e-api/members/__snapshots__/webhooks.test.js.snap +++ b/ghost/core/test/e2e-api/members/__snapshots__/webhooks.test.js.snap @@ -372,6 +372,16 @@ Object { "type": Any, }, ], + "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", diff --git a/ghost/core/test/unit/server/models/member-click-event.test.js b/ghost/core/test/unit/server/models/member-click-event.test.js new file mode 100644 index 000000000000..e89884eccde4 --- /dev/null +++ b/ghost/core/test/unit/server/models/member-click-event.test.js @@ -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(); + }); +}); diff --git a/ghost/core/test/unit/server/models/member-paid-subscription-event.test.js b/ghost/core/test/unit/server/models/member-paid-subscription-event.test.js new file mode 100644 index 000000000000..7d9fec80afb0 --- /dev/null +++ b/ghost/core/test/unit/server/models/member-paid-subscription-event.test.js @@ -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(); + }); +}); diff --git a/ghost/core/test/utils/e2e-framework.js b/ghost/core/test/utils/e2e-framework.js index a1b35a2594d1..9d6086618e56 100644 --- a/ghost/core/test/utils/e2e-framework.js +++ b/ghost/core/test/utils/e2e-framework.js @@ -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} agent + * @returns {Promise>} 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} agent + * @returns {Promise>} 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} agent + * @returns {Promise>} 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} agent + * @returns {Promise>} agent */ const getGhostAPIAgent = async () => { const bootOptions = { @@ -258,7 +258,7 @@ const getGhostAPIAgent = async () => { /** * - * @returns {Promise<{adminAgent: AdminAPITestAgent, membersAgent: MembersAPITestAgent}>} agents + * @returns {Promise<{adminAgent: InstanceType, membersAgent: InstanceType}>} agents */ const getAgentsForMembers = async () => { let membersAgent; diff --git a/ghost/core/test/utils/fixture-utils.js b/ghost/core/test/utils/fixture-utils.js index 307e64ede46a..c98d1b69d6e3 100644 --- a/ghost/core/test/utils/fixture-utils.js +++ b/ghost/core/test/utils/fixture-utils.js @@ -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(); } }; diff --git a/ghost/core/test/utils/fixtures/data-generator.js b/ghost/core/test/utils/fixtures/data-generator.js index 21b6c695605a..ae7daa0e9615 100644 --- a/ghost/core/test/utils/fixtures/data-generator.js +++ b/ghost/core/test/utils/fixtures/data-generator.js @@ -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 }; }()); diff --git a/ghost/members-api/lib/repositories/event.js b/ghost/members-api/lib/repositories/event.js index a1d5923578c4..d341ffa98d89 100644 --- a/ghost/members-api/lib/repositories/event.js +++ b/ghost/members-api/lib/repositories/event.js @@ -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));