diff --git a/ghost/admin/app/helpers/activity-feed-fetcher.js b/ghost/admin/app/helpers/activity-feed-fetcher.js index b8d98927fb..0668321643 100644 --- a/ghost/admin/app/helpers/activity-feed-fetcher.js +++ b/ghost/admin/app/helpers/activity-feed-fetcher.js @@ -11,7 +11,7 @@ const actions = { showNext: 'showNext' }; -export default class MembersEventsFetcher extends Resource { +export default class ActivityFeedFetcher extends Resource { @service ajax; @service ghostPaths; @service store; @@ -27,7 +27,7 @@ export default class MembersEventsFetcher extends Resource { @tracked shownEvents = 0; @tracked totalEvents = 0; - // save last event's date of each page for easy navigation to previous page + // Save the pagination filter for each page so we can return easily @tracked eventsBookmarks = []; get value() { @@ -53,12 +53,12 @@ export default class MembersEventsFetcher extends Resource { async setup() { const currentTime = moment.utc().format('YYYY-MM-DD HH:mm:ss'); let filter = `data.created_at:<'${currentTime}'`; + this.eventsBookmarks.push(filter); if (this.args.named.filter) { filter += `+${this.args.named.filter}`; } - this.eventsBookmarks.push(currentTime); await this.loadEventsTask.perform({filter}, actions.showNext); } @@ -66,20 +66,22 @@ export default class MembersEventsFetcher extends Resource { loadNextPage() { const lastEvent = this.data[this.data.length - 1]; const lastEventDate = moment.utc(lastEvent.data.created_at).format('YYYY-MM-DD HH:mm:ss'); - let filter = `data.created_at:<'${lastEventDate}'`; + const lastEventId = lastEvent.data.id; + + let filter = `(data.created_at:<'${lastEventDate}',(data.created_at:'${lastEventDate}'+id:<'${lastEventId}'))`; + this.eventsBookmarks.push(filter); if (this.args.named.filter) { filter += `+${this.args.named.filter}`; } - this.eventsBookmarks.push(lastEventDate); this.loadEventsTask.perform({filter}, actions.showNext); } @action loadPreviousPage() { this.eventsBookmarks.pop(); - let filter = `data.created_at:<'${this.eventsBookmarks[this.eventsBookmarks.length - 1]}'`; + let filter = this.eventsBookmarks[this.eventsBookmarks.length - 1]; if (this.args.named.filter) { filter += `+${this.args.named.filter}`; diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js b/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js index 26c13fa2bf..d90ec1aba1 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js @@ -53,6 +53,10 @@ const clickEventMapper = (json, frame) => { response.created_at = data.created_at; } + if (data.id) { + response.id = data.id; + } + return { ...json, data: response 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 index 96518d38f5..90da9b97f0 100644 --- 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 @@ -1,5 +1,2073 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Activity Feed API Can do filter based pagination 1: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "1", + "next": null, + "page": null, + "pages": 13, + "prev": null, + "total": 13, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 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": "1164", + "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 do filter based pagination 3: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "1", + "next": null, + "page": null, + "pages": 12, + "prev": null, + "total": 12, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 4: [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": "3291", + "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 do filter based pagination 5: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "1", + "next": null, + "page": null, + "pages": 11, + "prev": null, + "total": 11, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 6: [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": "3244", + "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 do filter based pagination 7: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "1", + "next": null, + "page": null, + "pages": 10, + "prev": null, + "total": 10, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 8: [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": "3283", + "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 do filter based pagination 9: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "1", + "next": null, + "page": null, + "pages": 9, + "prev": null, + "total": 9, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 10: [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": "753", + "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 do filter based pagination 11: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "1", + "next": null, + "page": null, + "pages": 8, + "prev": null, + "total": 8, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 12: [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": "590", + "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 do filter based pagination 13: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "1", + "next": null, + "page": null, + "pages": 7, + "prev": null, + "total": 7, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 14: [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": "523", + "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 do filter based pagination 15: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "1", + "next": null, + "page": null, + "pages": 6, + "prev": null, + "total": 6, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 16: [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": "559", + "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 do filter based pagination 17: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "1", + "next": null, + "page": null, + "pages": 5, + "prev": null, + "total": 5, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 18: [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": "1299", + "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 do filter based pagination 19: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "1", + "next": null, + "page": null, + "pages": 4, + "prev": null, + "total": 4, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 20: [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": "1299", + "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 do filter based pagination 21: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "1", + "next": null, + "page": null, + "pages": 3, + "prev": null, + "total": 3, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 22: [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": "1296", + "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 do filter based pagination 23: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "1", + "next": null, + "page": null, + "pages": 2, + "prev": null, + "total": 2, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 24: [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": "1288", + "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 do filter based pagination 25: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "1", + "next": null, + "page": null, + "pages": 1, + "prev": null, + "total": 1, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 26: [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": "1294", + "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 do filter based pagination 27: [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": 7, + "prev": null, + "total": 13, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 28: [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": "4348", + "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 do filter based pagination 29: [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": 6, + "prev": null, + "total": 11, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 30: [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": "6420", + "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 do filter based pagination 31: [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": 5, + "prev": null, + "total": 9, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 32: [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 Can do filter based pagination 33: [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": 7, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 34: [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": "978", + "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 do filter based pagination 35: [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": 3, + "prev": null, + "total": 5, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 36: [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": "2494", + "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 do filter based pagination 37: [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": 2, + "prev": null, + "total": 3, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 38: [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": "2480", + "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 do filter based pagination 39: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "2", + "next": null, + "page": null, + "pages": 1, + "prev": null, + "total": 1, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 40: [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": "1294", + "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 do filter based pagination 41: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "3", + "next": null, + "page": null, + "pages": 5, + "prev": null, + "total": 13, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 42: [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": "7486", + "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 do filter based pagination 43: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "3", + "next": null, + "page": null, + "pages": 4, + "prev": null, + "total": 10, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 44: [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": "4417", + "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 do filter based pagination 45: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "3", + "next": null, + "page": null, + "pages": 3, + "prev": null, + "total": 7, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 46: [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": "2173", + "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 do filter based pagination 47: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "3", + "next": null, + "page": null, + "pages": 2, + "prev": null, + "total": 4, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 48: [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": "3675", + "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 do filter based pagination 49: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "3", + "next": null, + "page": null, + "pages": 1, + "prev": null, + "total": 1, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 50: [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": "1294", + "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 do filter based pagination 51: [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, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "4", + "next": null, + "page": null, + "pages": 4, + "prev": null, + "total": 13, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 52: [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": "10663", + "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 do filter based pagination 53: [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, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "4", + "next": null, + "page": null, + "pages": 3, + "prev": null, + "total": 9, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 54: [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": "2113", + "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 do filter based pagination 55: [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, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "4", + "next": null, + "page": null, + "pages": 2, + "prev": null, + "total": 5, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 56: [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": "4870", + "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 do filter based pagination 57: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "4", + "next": null, + "page": null, + "pages": 1, + "prev": null, + "total": 1, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 58: [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": "1294", + "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 do filter based pagination 59: [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": "5", + "next": null, + "page": null, + "pages": 3, + "prev": null, + "total": 13, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 60: [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": "11312", + "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 do filter based pagination 61: [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": "5", + "next": null, + "page": null, + "pages": 2, + "prev": null, + "total": 8, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 62: [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": "3854", + "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 do filter based pagination 63: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "5", + "next": null, + "page": null, + "pages": 1, + "prev": null, + "total": 3, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 64: [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": "3670", + "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 do filter based pagination 65: [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, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "6", + "next": null, + "page": null, + "pages": 3, + "prev": null, + "total": 13, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 66: [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": "11798", + "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 do filter based pagination 67: [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, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "6", + "next": null, + "page": null, + "pages": 2, + "prev": null, + "total": 7, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 68: [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": "5744", + "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 do filter based pagination 69: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "6", + "next": null, + "page": null, + "pages": 1, + "prev": null, + "total": 1, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 70: [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": "1294", + "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 do filter based pagination 71: [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, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "7", + "next": null, + "page": null, + "pages": 2, + "prev": null, + "total": 13, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 72: [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": "12217", + "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 do filter based pagination 73: [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, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "7", + "next": null, + "page": null, + "pages": 1, + "prev": null, + "total": 6, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 74: [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": "6515", + "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 do filter based pagination 75: [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": "8", + "next": null, + "page": null, + "pages": 2, + "prev": null, + "total": 13, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 76: [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": "12672", + "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 do filter based pagination 77: [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": "8", + "next": null, + "page": null, + "pages": 1, + "prev": null, + "total": 5, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 78: [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": "6060", + "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 do filter based pagination 79: [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, + }, + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "9", + "next": null, + "page": null, + "pages": 2, + "prev": null, + "total": 13, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 80: [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": "13867", + "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 do filter based pagination 81: [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, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "9", + "next": null, + "page": null, + "pages": 1, + "prev": null, + "total": 4, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 82: [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": "4865", + "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 do filter based pagination 83: [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, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "10", + "next": null, + "page": null, + "pages": 2, + "prev": null, + "total": 13, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 84: [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": "15063", + "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 do filter based pagination 85: [body] 1`] = ` +Object { + "events": Array [ + 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": 3, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 86: [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": "3671", + "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 do filter based pagination 87: [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, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "11", + "next": null, + "page": null, + "pages": 2, + "prev": null, + "total": 13, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 88: [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": "16255", + "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 do filter based pagination 89: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "11", + "next": null, + "page": null, + "pages": 1, + "prev": null, + "total": 2, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 90: [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": "2479", + "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 do filter based pagination 91: [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, + }, + 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": "12", + "next": null, + "page": null, + "pages": 2, + "prev": null, + "total": 13, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 92: [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": "17439", + "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 do filter based pagination 93: [body] 1`] = ` +Object { + "events": Array [ + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": "12", + "next": null, + "page": null, + "pages": 1, + "prev": null, + "total": 1, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 94: [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": "1295", + "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 do filter based pagination 95: [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, + }, + 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": "13", + "next": null, + "page": null, + "pages": 1, + "prev": null, + "total": 13, + }, + }, +} +`; + +exports[`Activity Feed API Can do filter based pagination 96: [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": "18629", + "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 filter events by post id 1: [body] 1`] = ` Object { "events": Array [ @@ -81,7 +2149,7 @@ 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": "20770", + "content-length": "21026", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -118,7 +2186,523 @@ 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": "1240", + "content-length": "4348", + "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 only combine type and other filters at the root level 1: [body] 1`] = ` +Object { + "errors": Array [ + Object { + "code": null, + "context": null, + "details": null, + "ghostErrorCode": null, + "help": null, + "id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + "message": "This filter is not supported because you cannot combine type filters with other filters except at the root level in an AND.", + "property": null, + "type": "IncorrectUsageError", + }, + ], +} +`; + +exports[`Activity Feed API Can only combine type and other filters at the root level 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": "315", + "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 use NQL OR for type only 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, + }, + 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": 10, + }, + }, +} +`; + +exports[`Activity Feed API Can use NQL OR for type only 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": "4858", + "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 use OR as long as it is not combined with type 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, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": 10, + "next": null, + "page": null, + "pages": 2, + "prev": null, + "total": 15, + }, + }, +} +`; + +exports[`Activity Feed API Can use proper pagination 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": "5", + "next": null, + "page": null, + "pages": 3, + "prev": null, + "total": 15, + }, + }, +} +`; + +exports[`Activity Feed API Can use proper pagination 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": "13748", + "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 use proper pagination 3: [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": "5", + "next": null, + "page": null, + "pages": 3, + "prev": null, + "total": 15, + }, + }, +} +`; + +exports[`Activity Feed API Cannot combine type filter with OR filter 1: [body] 1`] = ` +Object { + "errors": Array [ + Object { + "code": null, + "context": null, + "details": null, + "ghostErrorCode": null, + "help": null, + "id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + "message": "This filter is not supported because you cannot combine type filters with other filters in an OR.", + "property": null, + "type": "IncorrectUsageError", + }, + ], +} +`; + +exports[`Activity Feed API Cannot combine type filter with OR filter 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": "289", + "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 Filter splitting Can AND two ORS 1: [body] 1`] = ` +Object { + "events": Array [ + 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": 3, + }, + }, +} +`; + +exports[`Activity Feed API Filter splitting Can AND two ORs 1: [body] 1`] = ` +Object { + "events": Array [ + 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": 3, + }, + }, +} +`; + +exports[`Activity Feed API Filter splitting Can only combine type and other filters at the root level 1: [body] 1`] = ` +Object { + "errors": Array [ + Object { + "code": null, + "context": null, + "details": null, + "ghostErrorCode": null, + "help": null, + "id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + "message": "This filter is not supported because you cannot combine type filters with other filters except at the root level in an AND.", + "property": null, + "type": "IncorrectUsageError", + }, + ], +} +`; + +exports[`Activity Feed API Filter splitting Can only combine type and other filters at the root level 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": "315", + "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 Filter splitting Can use NQL OR for type only 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, + }, + 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": 10, + }, + }, +} +`; + +exports[`Activity Feed API Filter splitting Can use NQL OR for type only 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": "5114", + "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 Filter splitting Can use OR as long as it is not combined with type 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, + }, + Object { + "data": Any, + "type": Any, + }, + Object { + "data": Any, + "type": Any, + }, + ], + "meta": Object { + "pagination": Object { + "limit": 10, + "next": null, + "page": null, + "pages": 2, + "prev": null, + "total": 15, + }, + }, +} +`; + +exports[`Activity Feed API Filter splitting Cannot combine type filter with OR filter 1: [body] 1`] = ` +Object { + "errors": Array [ + Object { + "code": null, + "context": null, + "details": null, + "ghostErrorCode": null, + "help": null, + "id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + "message": "This filter is not supported because you cannot combine type filters with other filters in an OR.", + "property": null, + "type": "IncorrectUsageError", + }, + ], +} +`; + +exports[`Activity Feed API Filter splitting Cannot combine type filter with OR filter 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": "289", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -132,135 +2716,21 @@ Object { Object { "data": Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "link": Object { - "from": "/r/0", + "from": "/r/7", "to": "https:://ghost.org", }, "member": Object { "avatar_image": null, - "email": "member1@test.com", + "email": "with-product@test.com", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": "Mr Egg", + "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": "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", + "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\\}/, }, @@ -270,6 +2740,7 @@ Object { Object { "data": Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "link": Object { "from": "/r/6", "to": "https:://ghost.org", @@ -293,20 +2764,141 @@ Object { Object { "data": Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "link": Object { - "from": "/r/7", + "from": "/r/5", "to": "https:://ghost.org", }, "member": Object { "avatar_image": null, - "email": "with-product@test.com", + "email": "vip@test.com", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": "Dana Barrett", + "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 scheduled post!!", + "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\\}/, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "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\\}/, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "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\\}/, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "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\\}/, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "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\\}/, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "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\\}/, }, @@ -331,7 +2923,7 @@ 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-length": "3978", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -401,7 +2993,7 @@ exports[`Activity Feed API Returns email delivered events in activity feed 2: [h 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": "1271", + "content-length": "1303", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -434,7 +3026,7 @@ exports[`Activity Feed API Returns email opened events in activity feed 2: [head 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": "1268", + "content-length": "1300", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -483,7 +3075,7 @@ exports[`Activity Feed API Returns email sent events in activity feed 2: [header 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": "5899", + "content-length": "6059", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Version, Origin, Accept-Encoding", @@ -500,119 +3092,14 @@ Object { "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "member": Object { "avatar_image": null, - "email": "member1@test.com", + "email": "with-product@test.com", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": "Mr Egg", + "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": "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", + "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\\}/, }, @@ -647,14 +3134,119 @@ Object { "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "member": Object { "avatar_image": null, - "email": "with-product@test.com", + "email": "vip@test.com", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": "Dana Barrett", + "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 scheduled post!!", + "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": "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": "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": "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": "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": "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\\}/, }, diff --git a/ghost/core/test/e2e-api/admin/activity-feed.test.js b/ghost/core/test/e2e-api/admin/activity-feed.test.js index 282b97686a..568762b939 100644 --- a/ghost/core/test/e2e-api/admin/activity-feed.test.js +++ b/ghost/core/test/e2e-api/admin/activity-feed.test.js @@ -1,8 +1,9 @@ const {agentProvider, mockManager, fixtureManager, matchers} = require('../../utils/e2e-framework'); -const {anyEtag, anyObjectId, anyUuid, anyISODate, anyString, anyObject, anyNumber} = matchers; +const {anyEtag, anyErrorId, anyObjectId, anyUuid, anyISODate, anyString, anyObject, anyNumber} = matchers; const models = require('../../../core/server/models'); const assert = require('assert'); +const moment = require('moment'); let agent; describe('Activity Feed API', function () { @@ -21,6 +22,97 @@ describe('Activity Feed API', function () { mockManager.restore(); }); + describe('Filter splitting',function () { + it('Can use NQL OR for type only', async function () { + // Check activity feed + await agent + .get(`/members/events?filter=type:comment_event,type:click_event`) + .expectStatus(200) + .matchHeaderSnapshot({ + etag: anyEtag + }) + .matchBodySnapshot({ + events: new Array(10).fill({ + type: anyString, + data: anyObject + }) + }) + .expect(({body}) => { + assert(!body.events.find(e => e.type !== 'click_event' && e.type !== 'comment_event'), 'Expected only click and comment events'); + }); + }); + + it('Cannot combine type filter with OR filter', async function () { + // This query is not allowed because we need to split the filter in two AND filters + await agent + .get(`/members/events?filter=type:comment_event,data.post_id:123`) + .expectStatus(400) + .matchHeaderSnapshot({ + etag: anyEtag + }) + .matchBodySnapshot({ + errors: [ + { + id: anyErrorId + } + ] + }); + }); + + it('Can only combine type and other filters at the root level', async function () { + await agent + .get(`/members/events?filter=${encodeURIComponent('(type:comment_event+data.post_id:123)+data.post_id:123')}`) + .expectStatus(400) + .matchHeaderSnapshot({ + etag: anyEtag + }) + .matchBodySnapshot({ + errors: [ + { + id: anyErrorId + } + ] + }); + }); + + it('Can use OR as long as it is not combined with type', async function () { + const postId = fixtureManager.get('posts', 0).id; + const memberId = fixtureManager.get('members', 0).id; + + await agent + .get(`/members/events?filter=${encodeURIComponent(`data.post_id:${postId},data.member_id:${memberId}`)}`) + .expectStatus(200) + .matchBodySnapshot({ + events: new Array(10).fill({ + type: anyString, + data: anyObject + }) + }) + .expect(({body}) => { + assert(!body.events.find(e => (e.data?.post?.id ?? e.data?.attribution?.id ?? e.data?.email?.post_id) !== postId && e.data?.member?.id !== memberId), 'Expected only events either from the given post or member'); + }); + }); + + it('Can AND two ORs', async function () { + const postId = fixtureManager.get('posts', 0).id; + const memberId = fixtureManager.get('members', 0).id; + + await agent + .get(`/members/events?filter=${encodeURIComponent(`(type:comment_event,type:click_event)+(data.post_id:${postId},data.member_id:${memberId})`)}`) + .expectStatus(200) + .matchBodySnapshot({ + events: new Array(3).fill({ + type: anyString, + data: anyObject + }) + }) + .expect(({body}) => { + assert(!body.events.find(e => e.type !== 'click_event' && e.type !== 'comment_event'), 'Expected only click and comment events'); + assert(!body.events.find(e => (e.data?.post?.id ?? e.data?.attribution?.id ?? e.data?.email?.post_id) !== postId && e.data?.member?.id !== memberId), 'Expected only events either from the given post or member'); + }); + }); + }); + // Activity feed it('Returns comments in activity feed', async function () { // Check activity feed @@ -54,6 +146,7 @@ describe('Activity Feed API', function () { events: new Array(8).fill({ type: anyString, data: { + id: anyObjectId, created_at: anyISODate, member: { id: anyObjectId, @@ -219,6 +312,78 @@ describe('Activity Feed API', function () { }); }); + it('Can do filter based pagination', async function () { + const totalExpected = 13; + const postId = fixtureManager.get('posts', 0).id; + + // There is an annoying restriction in the pagination. It doesn't work for mutliple email events at the same time because they have the same id (causes issues as we use id to deduplicate the created_at timestamp) + // If that is ever fixed (it is difficult) we can update this test to not use a filter + const skippedTypes = ['email_opened_event', 'email_failed_event', 'email_delivered_event']; + + // To make the test cover more edge cases, we test different limit configurations + for (let limit = 1; limit <= totalExpected; limit++) { + const {body: firstPage} = await agent + .get(`/members/events?filter=${encodeURIComponent(`type:-[${skippedTypes.join(',')}]+data.post_id:${postId}`)}&limit=${limit}`) + .expectStatus(200) + .matchHeaderSnapshot({ + etag: anyEtag + }) + .matchBodySnapshot({ + events: new Array(limit).fill({ + type: anyString, + data: anyObject + }) + }) + .expect(({body}) => { + assert(!body.events.find(e => (e.data?.post?.id ?? e.data?.attribution?.id ?? e.data?.email?.post_id) !== postId), 'Should only return events for the post'); + + // Assert total is correct + assert.equal(body.meta.pagination.total, totalExpected); + }); + let previousPage = firstPage; + let page = 1; + + const allEvents = previousPage.events; + + while (allEvents.length < totalExpected && page < 20) { + page += 1; + + // Calculate next page + let lastId = previousPage.events[previousPage.events.length - 1].data.id; + let lastCreatedAt = moment(previousPage.events[previousPage.events.length - 1].data.created_at).format('YYYY-MM-DD HH:mm:ss'); + + const remaining = totalExpected - (page - 1) * limit; + + const {body: secondPage} = await agent + .get(`/members/events?filter=${encodeURIComponent(`type:-[${skippedTypes.join(',')}]+data.post_id:${postId}+(data.created_at:<'${lastCreatedAt}',(data.created_at:'${lastCreatedAt}'+id:<${lastId}))`)}&limit=${limit}`) + .expectStatus(200) + .matchHeaderSnapshot({ + etag: anyEtag + }) + .matchBodySnapshot({ + events: new Array(Math.min(remaining, limit)).fill({ + type: anyString, + data: anyObject + }) + }) + .expect(({body}) => { + assert(!body.events.find(e => (e.data?.post?.id ?? e.data?.attribution?.id ?? e.data?.email?.post_id) !== postId), 'Should only return events for the post'); + + // Assert total is correct + assert.equal(body.meta.pagination.total, remaining, 'Expected total to be correct for page ' + page); + }); + allEvents.push(...secondPage.events); + } + + // Check if the ordering is correct and we didn't receive duplicate events + assert.equal(allEvents.length, totalExpected); + for (const event of allEvents) { + // Check no other events have the same id + assert.equal(allEvents.filter(e => e.data.id === event.data.id).length, 1); + } + } + }); + it('Can limit events', async function () { const postId = fixtureManager.get('posts', 0).id; await agent diff --git a/ghost/core/test/e2e-api/admin/members.test.js b/ghost/core/test/e2e-api/admin/members.test.js index d51de7b1aa..75648b6d8e 100644 --- a/ghost/core/test/e2e-api/admin/members.test.js +++ b/ghost/core/test/e2e-api/admin/members.test.js @@ -15,6 +15,18 @@ const memberAttributionService = require('../../../core/server/services/member-a const urlService = require('../../../core/server/services/url'); const urlUtils = require('../../../core/shared/url-utils'); +/** + * Assert that haystack and needles match, ignoring the order. + */ +function matchArrayWithoutOrder(haystack, needles) { + // Order shouldn't matter here + for (const a of needles) { + haystack.should.matchAny(a); + } + + assert.equal(haystack.length, needles.length, `Expected ${needles.length} items, but got ${haystack.length}`); +} + async function assertMemberEvents({eventType, memberId, asserts}) { const events = await models[eventType].where('member_id', memberId).fetchAll(); const eventsJSON = events.map(e => e.toJSON()); @@ -1728,7 +1740,9 @@ describe('Members API', function () { }); const events = eventsBody.events; - events.should.match([ + + // The order will be different in each test because two newsletter_events have the same created_at timestamp. And events are ordered by created_at desc, id desc (id will be different each time). + matchArrayWithoutOrder(events, [ { type: 'newsletter_event', data: { diff --git a/ghost/members-api/lib/repositories/event.js b/ghost/members-api/lib/repositories/event.js index 7137f4e72f..b14eec9529 100644 --- a/ghost/members-api/lib/repositories/event.js +++ b/ghost/members-api/lib/repositories/event.js @@ -1,5 +1,19 @@ const errors = require('@tryghost/errors'); const nql = require('@tryghost/nql'); +const mingo = require('mingo'); +const {replaceFilters, expandFilters, splitFilter, getUsedKeys, chainTransformers, mapKeys} = require('@tryghost/mongo-utils'); + +/** + * This mongo transformer ignores the provided filter option and replaces the filter with a custom filter that was provided to the transformer. Allowing us to set a mongo filter instead of a string based NQL filter. + */ +function replaceCustomFilterTransformer(filter) { + // Instead of adding an existing filter, we replace a filter, because mongo transformers are only applied if there is any filter (so not executed for empty filters) + return function (existingFilter) { + return replaceFilters(existingFilter, { + custom: filter + }); + }; +} module.exports = class EventRepository { constructor({ @@ -36,11 +50,12 @@ module.exports = class EventRepository { if (!options.limit) { options.limit = 10; } - let filters = this.getNQLSubset(options.filter); + + const [typeFilter, otherFilter] = this.getNQLSubset(options.filter); // Changing this order might need a change in the query functions // because of the different underlying models. - options.order = 'created_at desc'; + options.order = 'created_at desc, id desc'; // Create a list of all events that can be queried const pageActions = [ @@ -51,7 +66,7 @@ module.exports = class EventRepository { ]; // Some events are not filterable by post_id - if (!filters['data.post_id']) { + if (!getUsedKeys(otherFilter).includes('data.post_id')) { pageActions.push( {type: 'newsletter_event', action: 'getNewsletterSubscriptionEvents'}, {type: 'login_event', action: 'getLoginEvents'}, @@ -71,10 +86,17 @@ module.exports = class EventRepository { } //Filter events to query - const filteredPages = filters.type ? pageActions.filter(page => nql(filters.type).queryJSON(page)) : pageActions; + let filteredPages = pageActions; + if (typeFilter) { + // Ideally we should be able to create a NQL filter without having a string + const query = new mingo.Query(typeFilter); + filteredPages = filteredPages.filter(page => query.test(page)); + } //Start the promises - const pages = filteredPages.map(page => this[page.action](options, filters)); + const pages = filteredPages.map((page) => { + return this[page.action](options, otherFilter); + }); const allEventPages = await Promise.all(pages); @@ -84,7 +106,11 @@ module.exports = class EventRepository { return { events: allEvents.sort( (a, b) => { - return new Date(b.data.created_at).getTime() - new Date(a.data.created_at).getTime(); + const diff = new Date(b.data.created_at).getTime() - new Date(a.data.created_at).getTime(); + if (diff !== 0) { + return diff; + } + return b.data.id.localeCompare(a.data.id); } ).slice(0, options.limit), meta: { @@ -109,22 +135,23 @@ module.exports = class EventRepository { }); } - async getNewsletterSubscriptionEvents(options = {}, filters = {}) { + async getNewsletterSubscriptionEvents(options = {}, filter) { options = { ...options, withRelated: ['member', 'newsletter'], - filter: [] + filter: 'custom:true', + mongoTransformer: chainTransformers( + // First set the filter manually + replaceCustomFilterTransformer(filter), + + // Map the used keys in that filter + ...mapKeys({ + 'data.created_at': 'created_at', + 'data.source': 'source', + 'data.member_id': 'member_id' + }) + ) }; - if (filters['data.created_at']) { - options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'created_at:')); - } - if (filters['data.source']) { - options.filter.push(filters['data.source'].replace(/data.source:/g, 'source:')); - } - if (filters['data.member_id']) { - options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:')); - } - options.filter = options.filter.join('+'); const {data: models, meta} = await this._MemberSubscribeEvent.findPage(options); @@ -141,20 +168,23 @@ module.exports = class EventRepository { }; } - async getSubscriptionEvents(options = {}, filters = {}) { + async getSubscriptionEvents(options = {}, filter) { if (!this._labsService.isSet('memberAttribution')){ options = { ...options, withRelated: ['member'], - filter: [] + filter: 'custom:true', + mongoTransformer: chainTransformers( + // First set the filter manually + replaceCustomFilterTransformer(filter), + + // Map the used keys in that filter + ...mapKeys({ + 'data.created_at': 'created_at', + 'data.member_id': 'member_id' + }) + ) }; - if (filters['data.created_at']) { - options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'created_at:')); - } - if (filters['data.member_id']) { - options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:')); - } - options.filter = options.filter.join('+'); const {data: models, meta} = await this._MemberPaidSubscriptionEvent.findPage(options); @@ -183,19 +213,27 @@ module.exports = class EventRepository { // This is rediculous, but we need the tier name (we'll be able to shorten this later when we switch to the subscriptions table) 'stripeSubscription.stripePrice.stripeProduct.product' ], - filter: [] + filter: 'custom:true', + mongoTransformer: chainTransformers( + // First set the filter manually + replaceCustomFilterTransformer(filter), + + // Map the used keys in that filter + ...mapKeys({ + 'data.created_at': 'created_at', + 'data.member_id': 'member_id' + }), + + (f) => { + // Special one: when data.post_id is used, replace it with two filters: subscriptionCreatedEvent.attribution_id:x+subscriptionCreatedEvent.attribution_type:post + return expandFilters(f, [{ + key: 'data.post_id', + replacement: 'subscriptionCreatedEvent.attribution_id', + expansion: {'subscriptionCreatedEvent.attribution_type': 'post'} + }]); + } + ) }; - if (filters['data.created_at']) { - options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'created_at:')); - } - 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); @@ -219,19 +257,22 @@ module.exports = class EventRepository { }; } - async getPaymentEvents(options = {}, filters = {}) { + async getPaymentEvents(options = {}, filter) { options = { ...options, withRelated: ['member'], - filter: [] + filter: 'custom:true', + mongoTransformer: chainTransformers( + // First set the filter manually + replaceCustomFilterTransformer(filter), + + // Map the used keys in that filter + ...mapKeys({ + 'data.created_at': 'created_at', + 'data.member_id': 'member_id' + }) + ) }; - if (filters['data.created_at']) { - options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'created_at:')); - } - if (filters['data.member_id']) { - options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:')); - } - options.filter = options.filter.join('+'); const {data: models, meta} = await this._MemberPaymentEvent.findPage(options); @@ -248,19 +289,22 @@ module.exports = class EventRepository { }; } - async getLoginEvents(options = {}, filters = {}) { + async getLoginEvents(options = {}, filter) { options = { ...options, withRelated: ['member'], - filter: [] + filter: 'custom:true', + mongoTransformer: chainTransformers( + // First set the filter manually + replaceCustomFilterTransformer(filter), + + // Map the used keys in that filter + ...mapKeys({ + 'data.created_at': 'created_at', + 'data.member_id': 'member_id' + }) + ) }; - if (filters['data.created_at']) { - options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'created_at:')); - } - if (filters['data.member_id']) { - options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:')); - } - options.filter = options.filter.join('+'); const {data: models, meta} = await this._MemberLoginEvent.findPage(options); @@ -277,20 +321,23 @@ module.exports = class EventRepository { }; } - async getSignupEvents(options = {}, filters = {}) { + async getSignupEvents(options = {}, filter) { if (!this._labsService.isSet('memberAttribution')){ options = { ...options, withRelated: ['member'], - filter: ['from_status:null'] + filter: 'from_status:null+custom:true', + mongoTransformer: chainTransformers( + // First set the filter manually + replaceCustomFilterTransformer(filter), + + // Map the used keys in that filter + ...mapKeys({ + 'data.created_at': 'created_at', + 'data.member_id': 'member_id' + }) + ) }; - if (filters['data.created_at']) { - options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'created_at:')); - } - if (filters['data.member_id']) { - options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:')); - } - options.filter = options.filter.join('+'); const {data: models, meta} = await this._MemberStatusEvent.findPage(options); @@ -307,10 +354,10 @@ module.exports = class EventRepository { }; } - return this.getCreatedEvents(options, filters); + return this.getCreatedEvents(options, filter); } - async getCreatedEvents(options = {}, filters = {}) { + async getCreatedEvents(options = {}, filter) { options = { ...options, withRelated: [ @@ -319,22 +366,28 @@ module.exports = class EventRepository { 'userAttribution', 'tagAttribution' ], - filter: ['subscriptionCreatedEvent.id:null'] + filter: 'subscriptionCreatedEvent.id:null+custom:true', + mongoTransformer: chainTransformers( + // First set the filter manually + replaceCustomFilterTransformer(filter), + + // Map the used keys in that filter + ...mapKeys({ + 'data.created_at': 'created_at', + 'data.member_id': 'member_id', + 'data.source': 'source' + }), + + (f) => { + // Special one: when data.post_id is used, replace it with two filters: attribution_id:x+attribution_type:post + return expandFilters(f, [{ + key: 'data.post_id', + replacement: 'attribution_id', + expansion: {attribution_type: 'post'} + }]); + } + ) }; - if (filters['data.created_at']) { - options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'created_at:')); - } - if (filters['data.member_id']) { - options.filter.push(filters['data.member_id'].replace(/data.member_id:/g, 'member_id:')); - } - 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); @@ -354,22 +407,23 @@ module.exports = class EventRepository { }; } - async getCommentEvents(options = {}, filters = {}) { + async getCommentEvents(options = {}, filter) { options = { ...options, withRelated: ['member', 'post', 'parent'], - filter: ['member_id:-null'] + filter: 'member_id:-null+custom:true', + mongoTransformer: chainTransformers( + // First set the filter manually + replaceCustomFilterTransformer(filter), + + // Map the used keys in that filter + ...mapKeys({ + 'data.created_at': 'created_at', + 'data.member_id': 'member_id', + 'data.post_id': 'post_id' + }) + ) }; - if (filters['data.created_at']) { - options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'created_at:')); - } - 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); @@ -386,22 +440,23 @@ module.exports = class EventRepository { }; } - async getClickEvents(options = {}, filters = {}) { + async getClickEvents(options = {}, filter) { options = { ...options, withRelated: ['member', 'link', 'link.post'], - filter: [] + filter: 'custom:true', + mongoTransformer: chainTransformers( + // First set the filter manually + replaceCustomFilterTransformer(filter), + + // Map the used keys in that filter + ...mapKeys({ + 'data.created_at': 'created_at', + 'data.member_id': 'member_id', + 'data.post_id': 'post_id' + }) + ) }; - if (filters['data.created_at']) { - options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'created_at:')); - } - 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); @@ -418,22 +473,23 @@ module.exports = class EventRepository { }; } - async getFeedbackEvents(options = {}, filters = {}) { + async getFeedbackEvents(options = {}, filter) { options = { ...options, withRelated: ['member', 'post'], - filter: [] + filter: 'custom:true', + mongoTransformer: chainTransformers( + // First set the filter manually + replaceCustomFilterTransformer(filter), + + // Map the used keys in that filter + ...mapKeys({ + 'data.created_at': 'created_at', + 'data.member_id': 'member_id', + 'data.post_id': 'post_id' + }) + ) }; - if (filters['data.created_at']) { - options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'created_at:')); - } - 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); @@ -450,22 +506,23 @@ module.exports = class EventRepository { }; } - async getEmailSentEvents(options = {}, filters = {}) { + async getEmailSentEvents(options = {}, filter) { options = { ...options, withRelated: ['member', 'email'], - filter: ['failed_at:null', 'processed_at:-null'] + filter: 'failed_at:null+processed_at:-null+custom:true', + mongoTransformer: chainTransformers( + // First set the filter manually + replaceCustomFilterTransformer(filter), + + // Map the used keys in that filter + ...mapKeys({ + 'data.created_at': 'processed_at', + 'data.member_id': 'member_id', + 'data.post_id': 'email.post_id' + }) + ) }; - if (filters['data.created_at']) { - options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'processed_at:')); - } - 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, 'email.post_id:')); - } - options.filter = options.filter.join('+'); options.order = options.order.replace(/created_at/g, 'processed_at'); const {data: models, meta} = await this._EmailRecipient.findPage( @@ -476,6 +533,7 @@ module.exports = class EventRepository { return { type: 'email_sent_event', data: { + id: model.id, member_id: model.get('member_id'), created_at: model.get('processed_at'), member: model.related('member').toJSON(), @@ -490,22 +548,23 @@ module.exports = class EventRepository { }; } - async getEmailDeliveredEvents(options = {}, filters = {}) { + async getEmailDeliveredEvents(options = {}, filter) { options = { ...options, withRelated: ['member', 'email'], - filter: ['delivered_at:-null'] + filter: 'delivered_at:-null+custom:true', + mongoTransformer: chainTransformers( + // First set the filter manually + replaceCustomFilterTransformer(filter), + + // Map the used keys in that filter + ...mapKeys({ + 'data.created_at': 'delivered_at', + 'data.member_id': 'member_id', + 'data.post_id': 'email.post_id' + }) + ) }; - if (filters['data.created_at']) { - options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'delivered_at:')); - } - 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, 'email.post_id:')); - } - options.filter = options.filter.join('+'); options.order = options.order.replace(/created_at/g, 'delivered_at'); const {data: models, meta} = await this._EmailRecipient.findPage( @@ -516,6 +575,7 @@ module.exports = class EventRepository { return { type: 'email_delivered_event', data: { + id: model.id, member_id: model.get('member_id'), created_at: model.get('delivered_at'), member: model.related('member').toJSON(), @@ -530,22 +590,23 @@ module.exports = class EventRepository { }; } - async getEmailOpenedEvents(options = {}, filters = {}) { + async getEmailOpenedEvents(options = {}, filter) { options = { ...options, withRelated: ['member', 'email'], - filter: ['opened_at:-null'] + filter: 'opened_at:-null+custom:true', + mongoTransformer: chainTransformers( + // First set the filter manually + replaceCustomFilterTransformer(filter), + + // Map the used keys in that filter + ...mapKeys({ + 'data.created_at': 'opened_at', + 'data.member_id': 'member_id', + 'data.post_id': 'email.post_id' + }) + ) }; - if (filters['data.created_at']) { - options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'opened_at:')); - } - 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, 'email.post_id:')); - } - options.filter = options.filter.join('+'); options.order = options.order.replace(/created_at/g, 'opened_at'); const {data: models, meta} = await this._EmailRecipient.findPage( @@ -556,6 +617,7 @@ module.exports = class EventRepository { return { type: 'email_opened_event', data: { + id: model.id, member_id: model.get('member_id'), created_at: model.get('opened_at'), member: model.related('member').toJSON(), @@ -570,22 +632,23 @@ module.exports = class EventRepository { }; } - async getEmailFailedEvents(options = {}, filters = {}) { + async getEmailFailedEvents(options = {}, filter) { options = { ...options, withRelated: ['member', 'email'], - filter: ['failed_at:-null'] + filter: 'failed_at:-null+custom:true', + mongoTransformer: chainTransformers( + // First set the filter manually + replaceCustomFilterTransformer(filter), + + // Map the used keys in that filter + ...mapKeys({ + 'data.created_at': 'failed_at', + 'data.member_id': 'member_id', + 'data.post_id': 'email.post_id' + }) + ) }; - if (filters['data.created_at']) { - options.filter.push(filters['data.created_at'].replace(/data.created_at:/g, 'failed_at:')); - } - 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, 'email.post_id:')); - } - options.filter = options.filter.join('+'); options.order = options.order.replace(/created_at/g, 'failed_at'); const {data: models, meta} = await this._EmailRecipient.findPage( @@ -596,6 +659,7 @@ module.exports = class EventRepository { return { type: 'email_failed_event', data: { + id: model.id, member_id: model.get('member_id'), created_at: model.get('failed_at'), member: model.related('member').toJSON(), @@ -611,83 +675,36 @@ 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. + * Split the filter in two parts: + * - One with 'type' that will be applied to all the pages + * - Other filter that will be applied to each individual page + * + * Throws if splitting is not possible (e.g. OR'ing type with other filters) */ getNQLSubset(filter) { if (!filter) { - return {}; + return [undefined, undefined]; } - const lex = nql(filter).lex(); + const allowList = ['data.created_at', 'data.member_id', 'data.post_id', 'type', 'id']; + const parsed = nql(filter).parse(); + const keys = getUsedKeys(parsed); - 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)); - 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.} */ - 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; + for (const key of keys) { + if (!allowList.includes(key)) { + throw new errors.IncorrectUsageError({ + message: 'Cannot filter by ' + key + }); } } - return result; - } - - async getSubscriptions() { - const results = await this._MemberSubscribeEvent.findAll({ - aggregateSubscriptionDeltas: true - }); - - const resultsJSON = results.toJSON(); - - const cumulativeResults = resultsJSON.reduce((accumulator, result, index) => { - if (index === 0) { - return [{ - date: result.date, - subscribed: result.subscribed_delta - }]; - } - return accumulator.concat([{ - date: result.date, - subscribed: result.subscribed_delta + accumulator[index - 1].subscribed - }]); - }, []); - - return cumulativeResults; + try { + return splitFilter(parsed, ['type']); + } catch (e) { + throw new errors.IncorrectUsageError({ + message: e.message + }); + } } async getMRR() { @@ -721,37 +738,6 @@ module.exports = class EventRepository { return cumulativeResults; } - async getVolume() { - const results = await this._MemberPaymentEvent.findAll({ - aggregatePaymentVolume: true - }); - - const resultsJSON = results.toJSON(); - - const cumulativeResults = resultsJSON.reduce((accumulator, result) => { - if (!accumulator[result.currency]) { - return { - ...accumulator, - [result.currency]: [{ - date: result.date, - volume: result.volume_delta, - currency: result.currency - }] - }; - } - return { - ...accumulator, - [result.currency]: accumulator[result.currency].concat([{ - date: result.date, - volume: result.volume_delta + accumulator[result.currency].slice(-1)[0].volume, - currency: result.currency - }]) - }; - }, {}); - - return cumulativeResults; - } - async getStatuses() { const results = await this._MemberStatusEvent.findAll({ aggregateStatusCounts: true diff --git a/ghost/members-api/test/unit/lib/repositories/event.test.js b/ghost/members-api/test/unit/lib/repositories/event.test.js index 15ba6e15a9..2f086096a7 100644 --- a/ghost/members-api/test/unit/lib/repositories/event.test.js +++ b/ghost/members-api/test/unit/lib/repositories/event.test.js @@ -19,15 +19,6 @@ describe('EventRepository', function () { }); }); - 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)'); @@ -50,29 +41,70 @@ describe('EventRepository', function () { 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' + res.should.be.an.Array(); + res.should.have.lengthOf(2); + + res[0].should.eql({ + type: 'email_delivered_event' }); + should(res[1]).be.undefined(); }); 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]' + res.should.be.an.Array(); + res.should.have.lengthOf(2); + + res[0].should.eql({ + type: { + $nin: [ + 'email_delivered_event', + 'email_opened_event', + 'email_failed_event' + ] + } + }); + res[1].should.eql({ + $and: [{ + 'data.created_at': { + $lt: 0 + } + }, { + 'data.member_id': 123 + }] }); }); 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]' + res.should.be.an.Array(); + res.should.have.lengthOf(2); + res[0].should.eql({ + $and: [{ + type: { + $ne: 'email_delivered_event' + } + }, { + type: { + $nin: [ + 'email_opened_event', + 'email_failed_event' + ] + } + }] + }); + res[1].should.eql({ + $and: [{ + 'data.created_at': { + $lt: 0 + } + }, { + 'data.member_id': 123 + }, { + 'data.created_at': { + $gt: 10 + } + }] }); }); }); @@ -106,20 +138,23 @@ describe('EventRepository', function () { }, { type: 'unused' }); - fake.calledOnceWithExactly({ + sinon.assert.calledOnce(fake); + fake.getCall(0).firstArg.should.match({ withRelated: ['member', 'newsletter'], - filter: '' - }).should.be.eql(true); + filter: 'custom:true' + }); }); it('works when setting a created_at filter', async function () { await eventRepository.getNewsletterSubscriptionEvents({}, { 'data.created_at': 'data.created_at:123' }); - fake.calledOnceWithExactly({ + + sinon.assert.calledOnce(fake); + fake.getCall(0).firstArg.should.match({ withRelated: ['member', 'newsletter'], - filter: 'created_at:123' - }).should.be.eql(true); + filter: 'custom:true' + }); }); it('works when setting a combination of filters', async function () { @@ -127,10 +162,12 @@ describe('EventRepository', function () { 'data.created_at': 'data.created_at:123+data.created_at:<99999', 'data.member_id': 'data.member_id:-[3,4,5]+data.member_id:-[1,2,3]' }); - fake.calledOnceWithExactly({ + + sinon.assert.calledOnce(fake); + fake.getCall(0).firstArg.should.match({ withRelated: ['member', 'newsletter'], - filter: 'created_at:123+created_at:<99999+member_id:-[3,4,5]+member_id:-[1,2,3]' - }).should.be.eql(true); + filter: 'custom:true' + }); }); }); @@ -160,42 +197,48 @@ describe('EventRepository', function () { it('works when setting no filters', async function () { await eventRepository.getEmailFailedEvents({ filter: 'no used', - order: 'created_at desc' + order: 'created_at desc, id desc' }, { type: 'unused' }); - fake.calledOnceWithExactly({ + + fake.calledOnce.should.be.eql(true); + fake.getCall(0).firstArg.should.match({ withRelated: ['member', 'email'], - filter: 'failed_at:-null', - order: 'failed_at desc' - }).should.be.eql(true); + filter: 'failed_at:-null+custom:true', + order: 'failed_at desc, id desc' + }); }); it('works when setting a created_at filter', async function () { await eventRepository.getEmailDeliveredEvents({ - order: 'created_at desc' + order: 'created_at desc, id desc' }, { 'data.created_at': 'data.created_at:123' }); - fake.calledOnceWithExactly({ + + fake.calledOnce.should.be.eql(true); + fake.getCall(0).firstArg.should.match({ withRelated: ['member', 'email'], - filter: 'delivered_at:-null+delivered_at:123', - order: 'delivered_at desc' - }).should.be.eql(true); + filter: 'delivered_at:-null+custom:true', + order: 'delivered_at desc, id desc' + }); }); it('works when setting a combination of filters', async function () { await eventRepository.getEmailOpenedEvents({ - order: 'created_at desc' + order: 'created_at desc, id desc' }, { 'data.created_at': 'data.created_at:123+data.created_at:<99999', 'data.member_id': 'data.member_id:-[3,4,5]+data.member_id:-[1,2,3]' }); - fake.calledOnceWithExactly({ + + fake.calledOnce.should.be.eql(true); + fake.getCall(0).firstArg.should.match({ withRelated: ['member', 'email'], - filter: 'opened_at:-null+opened_at:123+opened_at:<99999+member_id:-[3,4,5]+member_id:-[1,2,3]', - order: 'opened_at desc' - }).should.be.eql(true); + filter: 'opened_at:-null+custom:true', + order: 'opened_at desc, id desc' + }); }); }); });