Skip to content

Commit

Permalink
feat: Activity events filters (#1873)
Browse files Browse the repository at this point in the history
* removed old events delete

* latest events query adapted

* fix: merge issue

* requested changes
  • Loading branch information
ncomerci committed Jul 2, 2024
1 parent 8a2246b commit 889d613
Show file tree
Hide file tree
Showing 8 changed files with 54 additions and 42 deletions.
10 changes: 10 additions & 0 deletions src/migrations/1719502578294_create-index-for-events-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { MigrationBuilder } from "node-pg-migrate"
import EventModel from "../models/Event"

export async function up(pgm: MigrationBuilder): Promise<void> {
pgm.createIndex(EventModel.tableName, [`(event_data->>'proposal_id')`])
}

export async function down(pgm: MigrationBuilder): Promise<void> {
pgm.dropIndex(EventModel.tableName, [`(event_data->>'proposal_id')`])
}
20 changes: 8 additions & 12 deletions src/models/Event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,28 @@ export default class EventModel extends Model<Event> {
return result
}

static async getLatest(filters?: EventFilter): Promise<Event[]> {
const { event_type } = filters || {}
static async getLatest(filters: EventFilter): Promise<Event[]> {
const { with_interval, event_type, proposal_id } = filters
const query = SQL`
SELECT *
FROM ${table(EventModel)}
WHERE created_at >= NOW() - INTERVAL '7 day'
WHERE 1=1
${conditional(
with_interval !== undefined ? with_interval : true,
SQL`AND created_at >= NOW() - INTERVAL '7 day'`
)}
${conditional(
!!event_type,
SQL`AND event_type IN (${join(event_type?.map((type) => SQL`${type}`) || [], SQL`, `)})`
)}
${conditional(!!proposal_id, SQL`AND (event_data ->>'proposal_id') = ${proposal_id}`)}
ORDER BY created_at DESC
LIMIT ${LATEST_EVENTS_LIMIT}
`
const result = await this.namedQuery<Event>('get_latest_events', query)
return result
}

static async deleteOldEvents() {
const query = SQL`
DELETE
FROM ${table(EventModel)}
WHERE created_at < NOW() - INTERVAL '7 day'
`
await this.namedQuery('delete_old_events', query)
}

static async isDiscourseEventRegistered(discourseEventId: string) {
const query = SQL`
SELECT count(*)
Expand Down
11 changes: 3 additions & 8 deletions src/routes/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@ import routes from 'decentraland-gatsby/dist/entities/Route/routes'
import { Request } from 'express'

import { EventsService } from '../services/events'
import {
validateDebugAddress,
validateEventTypesFilters,
validateId,
validateRequiredString,
} from '../utils/validations'
import { validateDebugAddress, validateEventFilters, validateId, validateRequiredString } from '../utils/validations'

export default routes((route) => {
const withAuth = auth()
Expand All @@ -19,8 +14,8 @@ export default routes((route) => {
})

async function getLatestEvents(req: Request) {
const eventTypes = validateEventTypesFilters(req)
return await EventsService.getLatest(eventTypes)
const filters = validateEventFilters(req)
return await EventsService.getLatest(filters)
}

async function voted(req: WithAuth) {
Expand Down
2 changes: 0 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,13 @@ import vestings from './routes/vestings'
import score from './routes/votes'
import webhooks from './routes/webhooks'
import { DiscordService } from './services/discord'
import { EventsService } from './services/events'

const jobs = manager()
jobs.cron('@eachMinute', finishProposal)
jobs.cron('@eachMinute', activateProposals)
jobs.cron('@each5Minute', withLock('publishBids', publishBids))
jobs.cron('@each10Second', pingSnapshot)
jobs.cron('30 0 * * *', updateGovernanceBudgets) // Runs at 00:30 daily
jobs.cron('0 1 * * *', EventsService.deleteOldEvents) // Runs at 01:00 daily
jobs.cron('30 1 * * *', runQueuedAirdropJobs) // Runs at 01:30 daily
jobs.cron('30 2 * * *', giveAndRevokeLandOwnerBadges) // Runs at 02:30 daily
jobs.cron('30 3 1 * *', giveTopVoterBadges) // Runs at 03:30 on the first day of the month
Expand Down
10 changes: 1 addition & 9 deletions src/services/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const CLEAR_DELEGATE_SIGNATURE_HASH = '0x9c4f00c4291262731946e308dc2979a56bd22cc
const SET_DELEGATE_SIGNATURE_HASH = '0xa9a7fd460f56bddb880a465a9c3e9730389c70bc53108148f16d55a87a6c468e'

export class EventsService {
static async getLatest(filters?: EventFilter): Promise<ActivityTickerEvent[]> {
static async getLatest(filters: EventFilter): Promise<ActivityTickerEvent[]> {
try {
const latestEvents = await EventModel.getLatest(filters)

Expand Down Expand Up @@ -185,14 +185,6 @@ export class EventsService {
})
}

static async deleteOldEvents() {
try {
await EventModel.deleteOldEvents()
} catch (error) {
ErrorService.report('Error deleting old events', { error, category: ErrorCategory.Events })
}
}

private static getProfileCacheKey(address: string) {
const cacheKey = `profile-${address.toLowerCase()}`
return cacheKey
Expand Down
2 changes: 2 additions & 0 deletions src/shared/types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export enum EventType {

export const EventFilterSchema = z.object({
event_type: z.nativeEnum(EventType).array().optional(),
proposal_id: z.string().uuid().optional(),
with_interval: z.boolean().optional(),
})

export type EventFilter = z.infer<typeof EventFilterSchema>
Expand Down
18 changes: 9 additions & 9 deletions src/utils/validations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import RequestError from 'decentraland-gatsby/dist/entities/Route/error'

import { EventType } from '../shared/types/events'

import { validateEventTypesFilters, validateId } from './validations'
import { validateEventFilters, validateId } from './validations'

describe('validateProposalId', () => {
const UUID = '00000000-0000-0000-0000-000000000000'
Expand All @@ -27,51 +27,51 @@ describe('validateProposalId', () => {
describe('validateEventTypesFilters', () => {
test('Should return an empty object when event_type is not provided', () => {
const req = { query: {} } as never
const result = validateEventTypesFilters(req)
const result = validateEventFilters(req)
expect(result).toEqual({})
})

test('Should convert event_type into an array when it is a string', () => {
const req = { query: { event_type: EventType.Voted } } as never
const result = validateEventTypesFilters(req)
const result = validateEventFilters(req)
expect(result).toEqual({ event_type: [EventType.Voted] })
})

test('Should keep event_type as an array when it is already an array', () => {
const req = { query: { event_type: [EventType.Voted, EventType.ProjectUpdateCommented] } } as never
const result = validateEventTypesFilters(req)
const result = validateEventFilters(req)
expect(result).toEqual({ event_type: [EventType.Voted, EventType.ProjectUpdateCommented] })
})

test('Should throw an error if EventFilterSchema returns an error', () => {
const req = { query: { event_type: 'single_event' } } as never

expect(() => validateEventTypesFilters(req)).toThrow()
expect(() => validateEventFilters(req)).toThrow()
})
})

describe('validateEventTypesFilters', () => {
test('Should return an empty object when event_type is not provided', () => {
const req = { query: {} } as never
const result = validateEventTypesFilters(req)
const result = validateEventFilters(req)
expect(result).toEqual({})
})

test('Should convert event_type into an array when it is a string', () => {
const req = { query: { event_type: EventType.Voted } } as never
const result = validateEventTypesFilters(req)
const result = validateEventFilters(req)
expect(result).toEqual({ event_type: [EventType.Voted] })
})

test('Should keep event_type as an array when it is already an array', () => {
const req = { query: { event_type: [EventType.Voted, EventType.ProjectUpdateCommented] } } as never
const result = validateEventTypesFilters(req)
const result = validateEventFilters(req)
expect(result).toEqual({ event_type: [EventType.Voted, EventType.ProjectUpdateCommented] })
})

test('Should throw an error if EventFilterSchema returns an error', () => {
const req = { query: { event_type: 'single_event' } } as never

expect(() => validateEventTypesFilters(req)).toThrow()
expect(() => validateEventFilters(req)).toThrow()
})
})
23 changes: 21 additions & 2 deletions src/utils/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,16 @@ export function validateAlchemyWebhookSignature(req: Request) {
}
}

export function validateEventTypesFilters(req: Request) {
const { event_type } = req.query
export function validateEventFilters(req: Request) {
const { event_type, proposal_id, with_interval } = req.query
const filters: Record<string, unknown> = {}
if (event_type) {
filters.event_type = isArray(event_type) ? event_type : [event_type]
}
if (proposal_id) {
filters.proposal_id = proposal_id.toString()
}
filters.with_interval = with_interval ? stringToBoolean(with_interval.toString()) : undefined
const parsedEventTypes = EventFilterSchema.safeParse(filters)
if (!parsedEventTypes.success) {
throw new Error('Invalid event types: ' + parsedEventTypes.error.message)
Expand Down Expand Up @@ -218,3 +222,18 @@ export function validateBlockNumber(blockNumber?: unknown | null) {
throw new Error('Invalid blockNumber: must be null, undefined, or a number')
}
}

export function stringToBoolean(str: string) {
switch (str.toLowerCase().trim()) {
case 'true':
case '1':
case 'yes':
return true
case 'false':
case '0':
case 'no':
return false
default:
throw new Error('Invalid boolean value')
}
}

0 comments on commit 889d613

Please sign in to comment.