diff --git a/packages/backend/src/peripherals/database/IndexerStateRepository.test.ts b/packages/backend/src/peripherals/database/IndexerStateRepository.test.ts new file mode 100644 index 00000000..19d72386 --- /dev/null +++ b/packages/backend/src/peripherals/database/IndexerStateRepository.test.ts @@ -0,0 +1,76 @@ +import { Logger } from '@l2beat/backend-tools' +import { expect } from 'earl' + +import { setupDatabaseTestSuite } from '../../test/database' +import { IndexerStateRepository } from './IndexerStateRepository' + +describe(IndexerStateRepository.name, () => { + const { database } = setupDatabaseTestSuite() + const repository = new IndexerStateRepository(database, Logger.SILENT) + + before(() => repository.deleteAll()) + afterEach(() => repository.deleteAll()) + + it('adds single record and queries it', async () => { + const record = { + id: 'id', + height: 1, + } + + await repository.addOrUpdate(record) + + const actual = await repository.getAll() + + expect(actual).toEqual([record]) + }) + + it('updates existing record', async () => { + const record = { + id: 'id', + height: 1, + } + + await repository.addOrUpdate(record) + await repository.addOrUpdate({ ...record, height: 2 }) + + const actual = await repository.getAll() + + expect(actual).toEqual([{ ...record, height: 2 }]) + }) + + it('finds record by id', async () => { + const record = { + id: 'id', + height: 1, + } + + const record2 = { + id: 'id2', + height: 2, + } + + await repository.addOrUpdate(record) + await repository.addOrUpdate(record2) + + const actual = await repository.findById('id2') + + expect(actual).toEqual(record2) + }) + + it('delete all records', async () => { + await repository.addOrUpdate({ + id: 'id', + height: 1, + }) + await repository.addOrUpdate({ + id: 'id2', + height: 2, + }) + + await repository.deleteAll() + + const actual = await repository.getAll() + + expect(actual).toEqual([]) + }) +}) diff --git a/packages/backend/src/peripherals/database/IndexerStateRepository.ts b/packages/backend/src/peripherals/database/IndexerStateRepository.ts new file mode 100644 index 00000000..8e8e78b7 --- /dev/null +++ b/packages/backend/src/peripherals/database/IndexerStateRepository.ts @@ -0,0 +1,56 @@ +import { Logger } from '@l2beat/backend-tools' +import type { IndexerStateRow } from 'knex/types/tables' + +import { BaseRepository, CheckConvention } from './shared/BaseRepository' +import { Database } from './shared/Database' + +export interface IndexerStateRecord { + id: string // TODO: Maybe branded string? + height: number +} + +export class IndexerStateRepository extends BaseRepository { + constructor(database: Database, logger: Logger) { + super(database, logger) + this.autoWrap>(this) + } + + async addOrUpdate(record: IndexerStateRecord): Promise { + const row = toRow(record) + const knex = await this.knex() + await knex('indexer_states').insert(row).onConflict('id').merge() + return record.id + } + + async findById(id: string): Promise { + const knex = await this.knex() + const row = await knex('indexer_states').where('id', id).first() + return row && toRecord(row) + } + + async getAll(): Promise { + const knex = await this.knex() + const rows = await knex('indexer_states').select('*') + return rows.map(toRecord) + } + + async deleteAll(): Promise { + const knex = await this.knex() + return knex('indexer_states').delete() + } +} + +function toRow(record: IndexerStateRecord): IndexerStateRow { + return { + id: record.id, + height: record.height, + last_updated: new Date(), + } +} + +function toRecord(row: IndexerStateRow): IndexerStateRecord { + return { + id: row.id, + height: row.height, + } +} diff --git a/packages/backend/src/peripherals/database/migrations/002_indexer_states.ts b/packages/backend/src/peripherals/database/migrations/002_indexer_states.ts new file mode 100644 index 00000000..052d8232 --- /dev/null +++ b/packages/backend/src/peripherals/database/migrations/002_indexer_states.ts @@ -0,0 +1,26 @@ +/* + ====== IMPORTANT NOTICE ====== + +DO NOT EDIT OR RENAME THIS FILE + +This is a migration file. Once created the file should not be renamed or edited, +because migrations are only run once on the production server. + +If you find that something was incorrectly set up in the `up` function you +should create a new migration file that fixes the issue. + +*/ + +import { Knex } from 'knex' + +export async function up(knex: Knex): Promise { + await knex.schema.createTable('indexer_states', (table) => { + table.string('id').primary() + table.integer('height').notNullable() + table.dateTime('last_updated', { useTz: false }).notNullable() + }) +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTable('indexer_states') +} diff --git a/packages/backend/src/peripherals/database/shared/types.ts b/packages/backend/src/peripherals/database/shared/types.ts index aaf1b8c9..346cf868 100644 --- a/packages/backend/src/peripherals/database/shared/types.ts +++ b/packages/backend/src/peripherals/database/shared/types.ts @@ -5,7 +5,14 @@ declare module 'knex/types/tables' { block_number: number } + interface IndexerStateRow { + id: string + height: number + last_updated: Date + } + interface Tables { block_numbers: BlockNumberRow + indexer_states: IndexerStateRow } }