diff --git a/ghost/members-analytics-ingress/.eslintrc.js b/ghost/members-analytics-ingress/.eslintrc.js new file mode 100644 index 0000000000..c9c1bcb522 --- /dev/null +++ b/ghost/members-analytics-ingress/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: ['ghost'], + extends: [ + 'plugin:ghost/node' + ] +}; diff --git a/ghost/members-analytics-ingress/LICENSE b/ghost/members-analytics-ingress/LICENSE new file mode 100644 index 0000000000..366ae5f624 --- /dev/null +++ b/ghost/members-analytics-ingress/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2021 Ghost Foundation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ghost/members-analytics-ingress/README.md b/ghost/members-analytics-ingress/README.md new file mode 100644 index 0000000000..9dfbffce55 --- /dev/null +++ b/ghost/members-analytics-ingress/README.md @@ -0,0 +1,39 @@ +# Events Controller + +## Install + +`npm install @tryghost/members-analytics-ingress --save` + +or + +`yarn add @tryghost/members-analytics-ingress` + + +## Usage + + +## Develop + +This is a mono repository, managed with [lerna](https://lernajs.io/). + +Follow the instructions for the top-level repo. +1. `git clone` this repo & `cd` into it as usual +2. Run `yarn` to install top-level dependencies. + + +## Run + +- `yarn dev` + + +## Test + +- `yarn lint` run just eslint +- `yarn test` run lint and tests + + + + +# Copyright & License + +Copyright (c) 2013-2021 Ghost Foundation - Released under the [MIT license](LICENSE). diff --git a/ghost/members-analytics-ingress/index.js b/ghost/members-analytics-ingress/index.js new file mode 100644 index 0000000000..b6638c27ee --- /dev/null +++ b/ghost/members-analytics-ingress/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/EventsController'); diff --git a/ghost/members-analytics-ingress/lib/EventsController.js b/ghost/members-analytics-ingress/lib/EventsController.js new file mode 100644 index 0000000000..7c7c0b3165 --- /dev/null +++ b/ghost/members-analytics-ingress/lib/EventsController.js @@ -0,0 +1,37 @@ +const DomainEvents = require('@tryghost/domain-events'); +const {MemberEntryViewEvent} = require('@tryghost/member-events'); + +/** + * @template Data + * @typedef {object} IEvent + * @prop {Date} timestamp + * @prop {Data} data + */ + +class EventsController { + static createEvents(req, res) { + try { + const {events} = req.body; + for (const event of events) { + if (event.type === 'entry_view') { + const {entryId, entryUrl, memberId, memberStatus, createdAt} = event; + const entryEvent = new MemberEntryViewEvent({ + entryId, + entryUrl, + memberId, + memberStatus + }, createdAt); + DomainEvents.dispatch(entryEvent); + } + } + res.writeHead(201); + return res.end('Created.'); + } catch (err) { + const statusCode = (err && err.statusCode) || 500; + res.writeHead(statusCode); + return res.end('Internal Server Error.'); + } + } +} + +module.exports = EventsController; diff --git a/ghost/members-analytics-ingress/package.json b/ghost/members-analytics-ingress/package.json new file mode 100644 index 0000000000..c980e2fcb6 --- /dev/null +++ b/ghost/members-analytics-ingress/package.json @@ -0,0 +1,28 @@ +{ + "name": "@tryghost/members-analytics-ingress", + "version": "0.0.0", + "repository": "https://github.com/TryGhost/Members/tree/main/packages/members-analytics-ingress", + "author": "Ghost Foundation", + "license": "MIT", + "main": "index.js", + "scripts": { + "dev": "echo \"Implement me!\"", + "test": "NODE_ENV=testing c8 --check-coverage mocha './test/**/*.test.js'", + "lint": "eslint . --ext .js --cache", + "posttest": "yarn lint" + }, + "files": [ + "index.js", + "lib" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "c8": "7.9.0", + "mocha": "9.1.1", + "should": "13.2.3", + "sinon": "11.1.2" + }, + "dependencies": {} +} diff --git a/ghost/members-analytics-ingress/test/.eslintrc.js b/ghost/members-analytics-ingress/test/.eslintrc.js new file mode 100644 index 0000000000..829b601eb0 --- /dev/null +++ b/ghost/members-analytics-ingress/test/.eslintrc.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: ['ghost'], + extends: [ + 'plugin:ghost/test' + ] +}; diff --git a/ghost/members-analytics-ingress/test/hello.test.js b/ghost/members-analytics-ingress/test/hello.test.js new file mode 100644 index 0000000000..85d69d1e08 --- /dev/null +++ b/ghost/members-analytics-ingress/test/hello.test.js @@ -0,0 +1,10 @@ +// Switch these lines once there are useful utils +// const testUtils = require('./utils'); +require('./utils'); + +describe('Hello world', function () { + it('Runs a test', function () { + // TODO: Write me! + 'hello'.should.eql('hello'); + }); +}); diff --git a/ghost/members-analytics-ingress/test/utils/assertions.js b/ghost/members-analytics-ingress/test/utils/assertions.js new file mode 100644 index 0000000000..7364ee8aa1 --- /dev/null +++ b/ghost/members-analytics-ingress/test/utils/assertions.js @@ -0,0 +1,11 @@ +/** + * Custom Should Assertions + * + * Add any custom assertions to this file. + */ + +// Example Assertion +// should.Assertion.add('ExampleAssertion', function () { +// this.params = {operator: 'to be a valid Example Assertion'}; +// this.obj.should.be.an.Object; +// }); diff --git a/ghost/members-analytics-ingress/test/utils/index.js b/ghost/members-analytics-ingress/test/utils/index.js new file mode 100644 index 0000000000..0d67d86ff8 --- /dev/null +++ b/ghost/members-analytics-ingress/test/utils/index.js @@ -0,0 +1,11 @@ +/** + * Test Utilities + * + * Shared utils for writing tests + */ + +// Require overrides - these add globals for tests +require('./overrides'); + +// Require assertions - adds custom should assertions +require('./assertions'); diff --git a/ghost/members-analytics-ingress/test/utils/overrides.js b/ghost/members-analytics-ingress/test/utils/overrides.js new file mode 100644 index 0000000000..90203424ee --- /dev/null +++ b/ghost/members-analytics-ingress/test/utils/overrides.js @@ -0,0 +1,10 @@ +// This file is required before any test is run + +// Taken from the should wiki, this is how to make should global +// Should is a global in our eslint test config +global.should = require('should').noConflict(); +should.extend(); + +// Sinon is a simple case +// Sinon is a global in our eslint test config +global.sinon = require('sinon'); diff --git a/ghost/members-api/lib/MembersAPI.js b/ghost/members-api/lib/MembersAPI.js index c4aa37e85f..f2a034abc6 100644 --- a/ghost/members-api/lib/MembersAPI.js +++ b/ghost/members-api/lib/MembersAPI.js @@ -351,6 +351,10 @@ module.exports = function MembersAPI({ body.json(), (req, res) => routerController.createCheckoutSetupSession(req, res) ), + createEvents: Router().use( + body.json(), + (req, res) => routerController.createEvents(req, res) + ), updateSubscription: Router({mergeParams: true}).use( body.json(), (req, res) => memberController.updateSubscription(req, res) diff --git a/ghost/members-api/lib/controllers/router.js b/ghost/members-api/lib/controllers/router.js index 850921e8d9..a33bdbaaf6 100644 --- a/ghost/members-api/lib/controllers/router.js +++ b/ghost/members-api/lib/controllers/router.js @@ -1,6 +1,8 @@ const common = require('../../lib/common'); const _ = require('lodash'); const errors = require('@tryghost/ignition-errors'); +const DomainEvents = require('@tryghost/domain-events'); +const {MemberEntryViewEvent} = require('@tryghost/member-events'); /** * RouterController @@ -218,6 +220,30 @@ module.exports = class RouterController { } } + async createEvents(req, res) { + try { + const {events} = req.body; + for (const event of events) { + if (event.type === 'entry_view') { + const entryEvent = new MemberEntryViewEvent({ + entryId: event.entry_id, + entryUrl: event.entry_url, + memberId: req.member ? req.member.id : null, + memberStatus: req.member ? req.member.status : null + }, event.created_at); + DomainEvents.dispatch(entryEvent); + } + } + res.writeHead(201); + return res.end('Created.'); + } catch (err) { + const statusCode = (err && err.statusCode) || 500; + common.logging.error(err); + res.writeHead(statusCode); + return res.end('Internal Server Error.'); + } + } + async sendMagicLink(req, res) { const {email, emailType, oldEmail, requestSrc} = req.body; let forceEmailType = false;