2022-01-24 20:53:10 +03:00
const errors = require ( '@tryghost/errors' ) ;
2022-01-21 19:56:15 +03:00
const nql = require ( '@nexes/nql' ) ;
2022-01-21 13:16:24 +03:00
2021-02-15 17:16:58 +03:00
module . exports = class EventRepository {
constructor ( {
2022-01-18 17:53:51 +03:00
EmailRecipient ,
2021-02-15 17:16:58 +03:00
MemberSubscribeEvent ,
MemberPaymentEvent ,
MemberStatusEvent ,
2021-02-18 14:16:39 +03:00
MemberLoginEvent ,
2022-01-18 17:53:51 +03:00
MemberPaidSubscriptionEvent ,
labsService
2021-02-15 17:16:58 +03:00
} ) {
this . _MemberSubscribeEvent = MemberSubscribeEvent ;
this . _MemberPaidSubscriptionEvent = MemberPaidSubscriptionEvent ;
this . _MemberPaymentEvent = MemberPaymentEvent ;
this . _MemberStatusEvent = MemberStatusEvent ;
2021-02-18 14:16:39 +03:00
this . _MemberLoginEvent = MemberLoginEvent ;
2022-01-18 17:53:51 +03:00
this . _EmailRecipient = EmailRecipient ;
this . _labsService = labsService ;
2021-02-15 17:16:58 +03:00
}
async registerPayment ( data ) {
await this . _MemberPaymentEvent . add ( {
... data ,
source : 'stripe'
} ) ;
}
2022-01-25 14:20:34 +03:00
async getNewsletterSubscriptionEvents ( options = { } , filters = { } ) {
2022-01-21 19:56:15 +03:00
options = {
... options ,
withRelated : [ 'member' ] ,
2022-01-25 14:20:34 +03:00
filter : [ ]
2022-01-21 19:56:15 +03:00
} ;
2022-01-25 14:20:34 +03:00
if ( filters [ 'data.created_at' ] ) {
options . filter . push ( filters [ 'data.created_at' ] . replace ( /data.created_at:/g , 'created_at:' ) ) ;
}
2022-02-03 18:02:33 +03:00
if ( filters [ 'data.source' ] ) {
options . filter . push ( filters [ 'data.source' ] . replace ( /data.source:/g , 'source:' ) ) ;
}
2022-01-25 14:20:34 +03:00
if ( filters [ 'data.member_id' ] ) {
options . filter . push ( filters [ 'data.member_id' ] . replace ( /data.member_id:/g , 'member_id:' ) ) ;
}
options . filter = options . filter . join ( '+' ) ;
2021-02-18 14:16:39 +03:00
const { data : models , meta } = await this . _MemberSubscribeEvent . findPage ( options ) ;
2022-01-25 18:44:29 +03:00
const data = models . map ( ( model ) => {
2021-02-18 14:16:39 +03:00
return {
type : 'newsletter_event' ,
2022-01-25 18:44:29 +03:00
data : model . toJSON ( options )
2021-02-18 14:16:39 +03:00
} ;
} ) ;
return {
data ,
meta
} ;
}
2022-01-25 14:20:34 +03:00
async getSubscriptionEvents ( options = { } , filters = { } ) {
2022-01-21 19:56:15 +03:00
options = {
... options ,
withRelated : [ 'member' ] ,
2022-01-25 14:20:34 +03:00
filter : [ ]
2022-01-21 19:56:15 +03:00
} ;
2022-01-25 14:20:34 +03:00
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 ( '+' ) ;
2021-02-18 14:16:39 +03:00
const { data : models , meta } = await this . _MemberPaidSubscriptionEvent . findPage ( options ) ;
2022-01-25 18:44:29 +03:00
const data = models . map ( ( model ) => {
2021-02-18 14:16:39 +03:00
return {
type : 'subscription_event' ,
2022-01-25 18:44:29 +03:00
data : model . toJSON ( options )
2021-02-18 14:16:39 +03:00
} ;
} ) ;
return {
data ,
meta
} ;
}
2022-01-25 14:20:34 +03:00
async getPaymentEvents ( options = { } , filters = { } ) {
2022-01-21 19:56:15 +03:00
options = {
... options ,
withRelated : [ 'member' ] ,
2022-01-25 14:20:34 +03:00
filter : [ ]
2022-01-21 19:56:15 +03:00
} ;
2022-01-25 14:20:34 +03:00
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 ( '+' ) ;
2021-02-18 14:16:39 +03:00
const { data : models , meta } = await this . _MemberPaymentEvent . findPage ( options ) ;
2022-01-25 18:44:29 +03:00
const data = models . map ( ( model ) => {
2021-02-18 14:16:39 +03:00
return {
type : 'payment_event' ,
2022-01-25 18:44:29 +03:00
data : model . toJSON ( options )
2021-02-18 14:16:39 +03:00
} ;
} ) ;
return {
data ,
meta
} ;
}
2022-01-25 14:20:34 +03:00
async getLoginEvents ( options = { } , filters = { } ) {
2022-01-21 19:56:15 +03:00
options = {
... options ,
withRelated : [ 'member' ] ,
2022-01-25 14:20:34 +03:00
filter : [ ]
2022-01-21 19:56:15 +03:00
} ;
2022-01-25 14:20:34 +03:00
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 ( '+' ) ;
2021-02-18 14:16:39 +03:00
const { data : models , meta } = await this . _MemberLoginEvent . findPage ( options ) ;
2022-01-25 18:44:29 +03:00
const data = models . map ( ( model ) => {
2021-02-18 14:16:39 +03:00
return {
type : 'login_event' ,
2022-01-25 18:44:29 +03:00
data : model . toJSON ( options )
2021-02-18 14:16:39 +03:00
} ;
} ) ;
return {
data ,
meta
} ;
}
2022-01-25 14:20:34 +03:00
async getSignupEvents ( options = { } , filters = { } ) {
2022-01-21 19:56:15 +03:00
options = {
... options ,
withRelated : [ 'member' ] ,
2022-01-25 14:20:34 +03:00
filter : [ 'from_status:null' ]
2022-01-21 19:56:15 +03:00
} ;
2022-01-25 14:20:34 +03:00
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 ( '+' ) ;
2021-03-05 15:38:30 +03:00
const { data : models , meta } = await this . _MemberStatusEvent . findPage ( options ) ;
2022-01-25 18:44:29 +03:00
const data = models . map ( ( model ) => {
2021-03-05 15:38:30 +03:00
return {
type : 'signup_event' ,
2022-01-25 18:44:29 +03:00
data : model . toJSON ( options )
2021-03-05 15:38:30 +03:00
} ;
} ) ;
return {
data ,
meta
} ;
}
2022-02-01 17:47:15 +03:00
async getEmailDeliveredEvents ( options = { } , filters = { } ) {
2022-01-21 19:56:15 +03:00
options = {
... options ,
withRelated : [ 'member' , 'email' ] ,
2022-01-25 14:20:34 +03:00
filter : [ 'delivered_at:-null' ]
2022-01-21 19:56:15 +03:00
} ;
2022-01-25 14:20:34 +03:00
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:' ) ) ;
}
options . filter = options . filter . join ( '+' ) ;
2022-02-01 17:47:15 +03:00
options . order = options . order . replace ( /created_at/g , 'delivered_at' ) ;
2022-01-25 14:20:34 +03:00
2022-01-18 17:53:51 +03:00
const { data : models , meta } = await this . _EmailRecipient . findPage (
options
) ;
2022-01-25 18:44:29 +03:00
const data = models . map ( ( model ) => {
2022-01-18 17:53:51 +03:00
return {
type : 'email_delivered_event' ,
data : {
2022-01-25 18:44:29 +03:00
member _id : model . get ( 'member_id' ) ,
created _at : model . get ( 'delivered_at' ) ,
member : model . related ( 'member' ) . toJSON ( ) ,
email : model . related ( 'email' ) . toJSON ( )
2022-01-18 17:53:51 +03:00
}
} ;
} ) ;
return {
data ,
meta
} ;
}
2022-01-25 14:20:34 +03:00
async getEmailOpenedEvents ( options = { } , filters = { } ) {
2022-01-21 19:56:15 +03:00
options = {
... options ,
withRelated : [ 'member' , 'email' ] ,
2022-01-25 14:20:34 +03:00
filter : [ 'opened_at:-null' ]
2022-01-21 19:56:15 +03:00
} ;
2022-01-25 14:20:34 +03:00
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:' ) ) ;
}
options . filter = options . filter . join ( '+' ) ;
2022-02-01 17:47:15 +03:00
options . order = options . order . replace ( /created_at/g , 'opened_at' ) ;
2022-01-25 14:20:34 +03:00
2022-01-18 17:53:51 +03:00
const { data : models , meta } = await this . _EmailRecipient . findPage (
options
) ;
2022-01-25 18:44:29 +03:00
const data = models . map ( ( model ) => {
2022-01-18 17:53:51 +03:00
return {
type : 'email_opened_event' ,
data : {
2022-01-25 18:44:29 +03:00
member _id : model . get ( 'member_id' ) ,
created _at : model . get ( 'opened_at' ) ,
member : model . related ( 'member' ) . toJSON ( ) ,
email : model . related ( 'email' ) . toJSON ( )
2022-01-18 17:53:51 +03:00
}
} ;
} ) ;
return {
data ,
meta
} ;
}
2022-01-25 14:20:34 +03:00
async getEmailFailedEvents ( options = { } , filters = { } ) {
2022-01-21 19:56:15 +03:00
options = {
... options ,
withRelated : [ 'member' , 'email' ] ,
2022-01-25 14:20:34 +03:00
filter : [ 'failed_at:-null' ]
2022-01-21 19:56:15 +03:00
} ;
2022-01-25 14:20:34 +03:00
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:' ) ) ;
}
options . filter = options . filter . join ( '+' ) ;
2022-02-01 17:47:15 +03:00
options . order = options . order . replace ( /created_at/g , 'failed_at' ) ;
2022-01-25 14:20:34 +03:00
2022-01-18 17:53:51 +03:00
const { data : models , meta } = await this . _EmailRecipient . findPage (
options
) ;
2022-01-25 18:44:29 +03:00
const data = models . map ( ( model ) => {
2022-01-18 17:53:51 +03:00
return {
type : 'email_failed_event' ,
data : {
2022-01-25 18:44:29 +03:00
member _id : model . get ( 'member_id' ) ,
created _at : model . get ( 'failed_at' ) ,
member : model . related ( 'member' ) . toJSON ( ) ,
email : model . related ( 'email' ) . toJSON ( )
2022-01-18 17:53:51 +03:00
}
} ;
} ) ;
return {
data ,
meta
} ;
}
2022-01-24 20:53:10 +03:00
/ * *
* Extract a subset of NQL .
* There are only a few properties allowed .
* Parenthesis are forbidden .
* Only ANDs are supported when combining properties .
* /
getNQLSubset ( filter ) {
if ( ! filter ) {
return { } ;
}
const lex = nql ( filter ) . lex ( ) ;
const allowedFilters = [ 'type' , 'data.created_at' , 'data.member_id' ] ;
const properties = lex
. filter ( x => x . token === 'PROP' )
. map ( x => x . matched . slice ( 0 , - 1 ) ) ;
if ( properties . some ( prop => ! allowedFilters . includes ( prop ) ) ) {
throw new errors . IncorrectUsageError ( {
message : 'The only allowed filters are `type`, `data.created_at` and `data.member_id`'
} ) ;
}
if ( lex . find ( x => x . token === 'LPAREN' ) ) {
throw new errors . IncorrectUsageError ( {
message : 'The filter can\'t contain parenthesis.'
} ) ;
}
const jsonFilter = nql ( filter ) . toJSON ( ) ;
const keys = Object . keys ( jsonFilter ) ;
if ( keys . length === 1 && keys [ 0 ] === '$or' ) {
throw new errors . IncorrectUsageError ( {
message : 'The top level-filters can only combined with ANDs (+) and not ORs (,).'
} ) ;
}
// The filter is validated, it only contains one level of filters concatenated with `+`
const filters = filter . split ( '+' ) ;
/** @type {Object.<string, string>} */
let result = { } ;
for ( const f of filters ) {
// dirty way to parse a property, but it works according to https://github.com/NexesJS/NQL-Lang/blob/0e12d799a3a9c4d8651444e9284ce16c19cbc4f0/src/nql.l#L18
const key = f . split ( ':' ) [ 0 ] ;
if ( ! result [ key ] ) {
result [ key ] = f ;
} else {
result [ key ] += '+' + f ;
}
}
return result ;
}
2021-02-18 14:16:39 +03:00
async getEventTimeline ( options = { } ) {
if ( ! options . limit ) {
options . limit = 10 ;
}
2022-02-01 17:47:15 +03:00
// Changing this order might need a change in the query functions
// because of the different underlying models.
2021-03-05 15:37:49 +03:00
options . order = 'created_at desc' ;
2022-01-25 14:20:34 +03:00
// Create a list of all events that can be queried
const pageActions = [
{ type : 'newsletter_event' , action : 'getNewsletterSubscriptionEvents' } ,
{ type : 'subscription_event' , action : 'getSubscriptionEvents' } ,
{ type : 'login_event' , action : 'getLoginEvents' } ,
2022-02-01 13:14:48 +03:00
{ type : 'payment_event' , action : 'getPaymentEvents' } ,
2022-01-25 14:20:34 +03:00
{ type : 'signup_event' , action : 'getSignupEvents' }
2022-01-18 17:53:51 +03:00
] ;
if ( this . _labsService . isSet ( 'membersActivityFeed' ) && this . _EmailRecipient ) {
2022-02-02 15:08:11 +03:00
pageActions . push ( { type : 'email_delivered_event' , action : 'getEmailDeliveredEvents' } ) ;
2022-01-25 14:20:34 +03:00
pageActions . push ( { type : 'email_opened_event' , action : 'getEmailOpenedEvents' } ) ;
pageActions . push ( { type : 'email_failed_event' , action : 'getEmailFailedEvents' } ) ;
2022-01-18 17:53:51 +03:00
}
2022-01-25 14:20:34 +03:00
let filters = this . getNQLSubset ( options . filter ) ;
//Filter events to query
const filteredPages = filters . type ? pageActions . filter ( page => nql ( filters . type ) . queryJSON ( page ) ) : pageActions ;
//Start the promises
const pages = filteredPages . map ( page => this [ page . action ] ( options , filters ) ) ;
2022-01-18 17:53:51 +03:00
const allEventPages = await Promise . all ( pages ) ;
2021-02-18 14:16:39 +03:00
2022-01-25 14:20:34 +03:00
const allEvents = allEventPages . reduce ( ( accumulator , page ) => accumulator . concat ( page . data ) , [ ] ) ;
2021-02-18 14:16:39 +03:00
return allEvents . sort ( ( a , b ) => {
return new Date ( b . data . created _at ) - new Date ( a . data . created _at ) ;
2021-03-05 15:38:56 +03:00
} ) . reduce ( ( memo , event , i ) => {
2022-01-25 18:38:25 +03:00
if ( this . _labsService . isSet ( 'membersActivityFeed' ) ) {
//disable the event filtering
return memo . concat ( event ) ;
}
2021-03-05 15:38:56 +03:00
if ( event . type === 'newsletter_event' && event . data . subscribed ) {
const previousEvent = allEvents [ i - 1 ] ;
const nextEvent = allEvents [ i + 1 ] ;
const currentMember = event . data . member _id ;
if ( previousEvent && previousEvent . type === 'signup_event' ) {
const previousMember = previousEvent . data . member _id ;
if ( currentMember === previousMember ) {
return memo ;
}
}
if ( nextEvent && nextEvent . type === 'signup_event' ) {
const nextMember = nextEvent . data . member _id ;
if ( currentMember === nextMember ) {
return memo ;
}
}
}
return memo . concat ( event ) ;
} , [ ] ) . slice ( 0 , options . limit ) ;
2021-02-18 14:16:39 +03:00
}
2021-02-15 17:16:58 +03:00
async getSubscriptions ( ) {
const results = await this . _MemberSubscribeEvent . findAll ( {
aggregateSubscriptionDeltas : true
} ) ;
const resultsJSON = results . toJSON ( ) ;
2022-01-25 18:44:29 +03:00
const cumulativeResults = resultsJSON . reduce ( ( accumulator , result , index ) => {
2021-02-15 17:16:58 +03:00
if ( index === 0 ) {
return [ {
date : result . date ,
subscribed : result . subscribed _delta
} ] ;
}
2022-01-25 18:44:29 +03:00
return accumulator . concat ( [ {
2021-02-15 17:16:58 +03:00
date : result . date ,
2022-01-25 18:44:29 +03:00
subscribed : result . subscribed _delta + accumulator [ index - 1 ] . subscribed
2021-02-15 17:16:58 +03:00
} ] ) ;
} , [ ] ) ;
return cumulativeResults ;
}
async getMRR ( ) {
const results = await this . _MemberPaidSubscriptionEvent . findAll ( {
aggregateMRRDeltas : true
} ) ;
const resultsJSON = results . toJSON ( ) ;
2022-01-25 18:44:29 +03:00
const cumulativeResults = resultsJSON . reduce ( ( accumulator , result ) => {
if ( ! accumulator [ result . currency ] ) {
2021-02-15 17:16:58 +03:00
return {
2022-01-25 18:44:29 +03:00
... accumulator ,
2021-02-15 17:16:58 +03:00
[ result . currency ] : [ {
date : result . date ,
mrr : result . mrr _delta ,
currency : result . currency
} ]
} ;
}
return {
2022-01-25 18:44:29 +03:00
... accumulator ,
[ result . currency ] : accumulator [ result . currency ] . concat ( [ {
2021-02-15 17:16:58 +03:00
date : result . date ,
2022-01-25 18:44:29 +03:00
mrr : result . mrr _delta + accumulator [ result . currency ] . slice ( - 1 ) [ 0 ] . mrr ,
2021-02-15 17:16:58 +03:00
currency : result . currency
} ] )
} ;
} , { } ) ;
return cumulativeResults ;
}
async getVolume ( ) {
const results = await this . _MemberPaymentEvent . findAll ( {
aggregatePaymentVolume : true
} ) ;
const resultsJSON = results . toJSON ( ) ;
2022-01-25 18:44:29 +03:00
const cumulativeResults = resultsJSON . reduce ( ( accumulator , result ) => {
if ( ! accumulator [ result . currency ] ) {
2021-02-15 17:16:58 +03:00
return {
2022-01-25 18:44:29 +03:00
... accumulator ,
2021-02-15 17:16:58 +03:00
[ result . currency ] : [ {
date : result . date ,
volume : result . volume _delta ,
currency : result . currency
} ]
} ;
}
return {
2022-01-25 18:44:29 +03:00
... accumulator ,
[ result . currency ] : accumulator [ result . currency ] . concat ( [ {
2021-02-15 17:16:58 +03:00
date : result . date ,
2022-01-25 18:44:29 +03:00
volume : result . volume _delta + accumulator [ result . currency ] . slice ( - 1 ) [ 0 ] . volume ,
2021-02-15 17:16:58 +03:00
currency : result . currency
} ] )
} ;
} , { } ) ;
return cumulativeResults ;
}
async getStatuses ( ) {
const results = await this . _MemberStatusEvent . findAll ( {
aggregateStatusCounts : true
} ) ;
const resultsJSON = results . toJSON ( ) ;
2022-01-25 18:44:29 +03:00
const cumulativeResults = resultsJSON . reduce ( ( accumulator , result , index ) => {
2021-02-15 17:16:58 +03:00
if ( index === 0 ) {
return [ {
date : result . date ,
paid : result . paid _delta ,
comped : result . comped _delta ,
free : result . free _delta
} ] ;
}
2022-01-25 18:44:29 +03:00
return accumulator . concat ( [ {
2021-02-15 17:16:58 +03:00
date : result . date ,
2022-01-25 18:44:29 +03:00
paid : result . paid _delta + accumulator [ index - 1 ] . paid ,
comped : result . comped _delta + accumulator [ index - 1 ] . comped ,
free : result . free _delta + accumulator [ index - 1 ] . free
2021-02-15 17:16:58 +03:00
} ] ) ;
} , [ ] ) ;
return cumulativeResults ;
}
} ;