Skip to content

Commit

Permalink
Track Service Refactor (#343)
Browse files Browse the repository at this point in the history
  • Loading branch information
jacob-xhio authored Mar 22, 2024
1 parent bf39b2d commit 390dd53
Show file tree
Hide file tree
Showing 6 changed files with 504 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,65 +7,33 @@

package io.xh.hoist.admin

import grails.gorm.transactions.ReadOnly
import io.xh.hoist.BaseController
import io.xh.hoist.data.filter.Filter
import io.xh.hoist.security.Access
import io.xh.hoist.track.TrackLog
import io.xh.hoist.track.TrackService
import io.xh.hoist.track.TrackLogAdminService

import java.time.LocalDate

import static io.xh.hoist.util.DateTimeUtils.appDay
import static io.xh.hoist.util.DateTimeUtils.parseLocalDate

import static io.xh.hoist.util.DateTimeUtils.*
import static java.lang.Integer.parseInt

@Access(['HOIST_ADMIN_READER'])
class TrackLogAdminController extends BaseController {

TrackService trackService
TrackLogAdminService trackLogAdminService

@ReadOnly
def index() {
if (!trackService.enabled) {
renderJSON([])
}

def startDay = parseLocalDate(params.startDay),
endDay = parseLocalDate(params.endDay)

// NOTE that querying + serializing large numbers of TrackLogs below requires a significant
// allocation of memory. Be mindful if customizing maxRow-related configs above defaults!
def conf = trackService.conf,
maxDefault = conf.maxRows.default as Integer,
maxLimit = conf.maxRows.limit as Integer,
maxRows = [(params.maxRows ? parseInt(params.maxRows) : maxDefault), maxLimit].min()
def query = parseRequestJSON(),
startDay = query.startDay ? parseLocalDate(query.startDay) : LocalDate.of(1970, 1, 1),
endDay = query.endDay ? parseLocalDate(query.endDay) : appDay(),
filter = Filter.parse(query.filters),
maxRows = query.maxRows

def results = TrackLog.findAll(max: maxRows, sort: 'dateCreated', order: 'desc') {
if (startDay) dateCreated >= appStartOfDay(startDay)
if (endDay) dateCreated <= appEndOfDay(endDay)
if (params.category) category =~ "%$params.category%"
if (params.username) username =~ "%$params.username%"
if (params.browser) browser =~ "%$params.browser%"
if (params.device) device =~ "%$params.device%"
if (params.msg) msg =~ "%$params.msg%"
}

renderJSON(results)
renderJSON(trackLogAdminService.queryTrackLog(startDay, endDay, filter, maxRows))
}

def lookups() {
renderJSON([
categories: distinctVals('category'),
browsers: distinctVals('browser'),
devices: distinctVals('device'),
usernames: distinctVals('username'),
])
}

//------------------------
// Implementation
//------------------------
private List distinctVals(String property) {
return TrackLog.createCriteria().list {
projections { distinct(property) }
}.sort()
renderJSON(trackLogAdminService.lookups())
}

}
65 changes: 65 additions & 0 deletions grails-app/services/io/xh/hoist/track/TrackLogAdminService.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.xh.hoist.track

import grails.gorm.transactions.ReadOnly
import io.xh.hoist.BaseService;
import io.xh.hoist.config.ConfigService
import io.xh.hoist.data.filter.Filter
import io.xh.hoist.exception.DataNotAvailableException
import org.hibernate.Criteria
import org.hibernate.SessionFactory
import java.time.LocalDate;

import static io.xh.hoist.util.DateTimeUtils.appEndOfDay
import static io.xh.hoist.util.DateTimeUtils.appStartOfDay
import static org.hibernate.criterion.Order.desc
import static org.hibernate.criterion.Restrictions.between

class TrackLogAdminService extends BaseService {
ConfigService configService
SessionFactory sessionFactory

Boolean getEnabled() {
return conf.enabled == true
}

@ReadOnly
List<TrackLog> queryTrackLog(LocalDate startDay, LocalDate endDay, Filter filter, Integer maxRows = null) {
if (!enabled) throw new DataNotAvailableException('TrackService not available.')

def maxDefault = conf.maxRows.default as Integer,
maxLimit = conf.maxRows.limit as Integer

maxRows = [(maxRows ? maxRows : maxDefault), maxLimit].min()

def session = sessionFactory.currentSession
Criteria c = session.createCriteria(TrackLog)
c.maxResults = maxRows
c.addOrder(desc('dateCreated'))
c.add(between('dateCreated', appStartOfDay(startDay), appEndOfDay(endDay)))
if (filter) {
c.add(filter.criterion)
}
c.list() as List<TrackLog>
}

@ReadOnly
Map lookups() {[
category: distinctVals('category'),
browser: distinctVals('browser'),
device: distinctVals('device'),
username: distinctVals('username')
] }

//------------------------
// Implementation
//------------------------
private List distinctVals(String property) {
TrackLog.createCriteria().list {
projections { distinct(property) }
}.sort()
}

private Map getConf() {
configService.getMap('xhActivityTrackingConfig')
}
}
62 changes: 62 additions & 0 deletions src/main/groovy/io/xh/hoist/data/filter/CompoundFilter.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* This file belongs to Hoist, an application development toolkit
* developed by Extremely Heavy Industries (www.xh.io | [email protected])
*
* Copyright © 2024 Extremely Heavy Industries Inc.
*/

package io.xh.hoist.data.filter

import io.xh.hoist.json.JSONFormat
import org.hibernate.criterion.Criterion

/**
* Combines multiple filters (including other nested CompoundFilters) via an AND or OR operator.
*/
class CompoundFilter extends Filter implements JSONFormat {

final List<Filter> filters
final String op

CompoundFilter(List filters, String op) {
op = op ? op.toUpperCase() : 'AND'
if (op != 'AND' && op != 'OR') throw new RuntimeException('CompoundFilter requires "op" value of "AND" or "OR"')
this.filters = filters.collect { parse(it) }.findAll()
this.op = op
}

Map formatForJSON() {
return [
filters: filters,
op: op
]
}

//---------------------
// Overrides
//----------------------
List<String> getAllFields() {
filters.collectMany { it.allFields }.unique()
}

Criterion getCriterion() {
op == 'AND' ? and(filters*.criterion) : or(filters*.criterion)
}

Closure<Boolean> getTestFn() {
if (!filters) return { true }
def tests = filters*.testFn
return op == 'AND' ?
{ tests.every { test -> test(it) } } :
{ tests.any { test -> test(it) } }
}

boolean equals(Filter other) {
if (other === this) return true;
return (
other instanceof CompoundFilter &&
other.op == op &&
other.filters == filters
)
}
}
Loading

0 comments on commit 390dd53

Please sign in to comment.