diff --git a/src/datasources/accounts/accounts.datasource.ts b/src/datasources/accounts/accounts.datasource.ts index 80bee982ca..5c24eecafc 100644 --- a/src/datasources/accounts/accounts.datasource.ts +++ b/src/datasources/accounts/accounts.datasource.ts @@ -6,6 +6,7 @@ import { } from '@/datasources/cache/cache.service.interface'; import { MAX_TTL } from '@/datasources/cache/constants'; import { CachedQueryResolver } from '@/datasources/db/cached-query-resolver'; +import { ICachedQueryResolver } from '@/datasources/db/cached-query-resolver.interface'; import { AccountDataSetting } from '@/domain/accounts/entities/account-data-setting.entity'; import { AccountDataType } from '@/domain/accounts/entities/account-data-type.entity'; import { Account } from '@/domain/accounts/entities/account.entity'; @@ -29,6 +30,7 @@ export class AccountsDatasource implements IAccountsDatasource, OnModuleInit { constructor( @Inject(CacheService) private readonly cacheService: ICacheService, @Inject('DB_INSTANCE') private readonly sql: postgres.Sql, + @Inject(ICachedQueryResolver) private readonly cachedQueryResolver: CachedQueryResolver, @Inject(LoggingService) private readonly loggingService: ILoggingService, @Inject(IConfigurationService) diff --git a/src/datasources/accounts/counterfactual-safes/counterfactual-safes.datasource.ts b/src/datasources/accounts/counterfactual-safes/counterfactual-safes.datasource.ts index fe1d48f289..607c006c36 100644 --- a/src/datasources/accounts/counterfactual-safes/counterfactual-safes.datasource.ts +++ b/src/datasources/accounts/counterfactual-safes/counterfactual-safes.datasource.ts @@ -5,6 +5,7 @@ import { ICacheService, } from '@/datasources/cache/cache.service.interface'; import { CachedQueryResolver } from '@/datasources/db/cached-query-resolver'; +import { ICachedQueryResolver } from '@/datasources/db/cached-query-resolver.interface'; import { CounterfactualSafe } from '@/domain/accounts/counterfactual-safes/entities/counterfactual-safe.entity'; import { CreateCounterfactualSafeDto } from '@/domain/accounts/counterfactual-safes/entities/create-counterfactual-safe.dto.entity'; import { Account } from '@/domain/accounts/entities/account.entity'; @@ -22,6 +23,7 @@ export class CounterfactualSafesDatasource constructor( @Inject(CacheService) private readonly cacheService: ICacheService, @Inject('DB_INSTANCE') private readonly sql: postgres.Sql, + @Inject(ICachedQueryResolver) private readonly cachedQueryResolver: CachedQueryResolver, @Inject(LoggingService) private readonly loggingService: ILoggingService, @Inject(IConfigurationService) diff --git a/src/datasources/db/cached-query-resolver.interface.ts b/src/datasources/db/cached-query-resolver.interface.ts new file mode 100644 index 0000000000..aafe0385a1 --- /dev/null +++ b/src/datasources/db/cached-query-resolver.interface.ts @@ -0,0 +1,12 @@ +import { CacheDir } from '@/datasources/cache/entities/cache-dir.entity'; +import postgres from 'postgres'; + +export const ICachedQueryResolver = Symbol('ICachedQueryResolver'); + +export interface ICachedQueryResolver { + get(args: { + cacheDir: CacheDir; + query: postgres.PendingQuery; + ttl: number; + }): Promise; +} diff --git a/src/datasources/db/cached-query-resolver.spec.ts b/src/datasources/db/cached-query-resolver.spec.ts new file mode 100644 index 0000000000..b42c5e49c6 --- /dev/null +++ b/src/datasources/db/cached-query-resolver.spec.ts @@ -0,0 +1,74 @@ +import { fakeJson } from '@/__tests__/faker'; +import { FakeCacheService } from '@/datasources/cache/__tests__/fake.cache.service'; +import { CachedQueryResolver } from '@/datasources/db/cached-query-resolver'; +import { ILoggingService } from '@/logging/logging.interface'; +import { faker } from '@faker-js/faker'; +import postgres, { MaybeRow } from 'postgres'; + +const mockLoggingService = jest.mocked({ + debug: jest.fn(), +} as jest.MockedObjectDeep); + +const mockQuery = jest.mocked({ catch: jest.fn() } as jest.MockedObjectDeep< + postgres.PendingQuery +>); + +describe('CachedQueryResolver', () => { + let fakeCacheService: FakeCacheService; + let target: CachedQueryResolver; + + beforeAll(() => { + fakeCacheService = new FakeCacheService(); + target = new CachedQueryResolver(mockLoggingService, fakeCacheService); + }); + + afterEach(() => { + jest.clearAllMocks(); + fakeCacheService.clear(); + }); + + describe('get', () => { + it('should return the content from cache if it exists', async () => { + const cacheDir = { key: 'key', field: 'field' }; + const ttl = faker.number.int({ min: 1, max: 1000 }); + const value = fakeJson(); + await fakeCacheService.set(cacheDir, JSON.stringify(value), ttl); + + const actual = await target.get({ + cacheDir, + query: mockQuery, + ttl, + }); + + expect(actual).toBe(value); + expect(mockLoggingService.debug).toHaveBeenCalledWith({ + type: 'cache_hit', + key: 'key', + field: 'field', + }); + expect(mockQuery.catch).not.toHaveBeenCalled(); + }); + + it('should execute the query and cache the result if the cache is empty', async () => { + const cacheDir = { key: 'key', field: 'field' }; + const ttl = faker.number.int({ min: 1, max: 1000 }); + const dbResult = { ...JSON.parse(fakeJson()), count: 1 }; + mockQuery.catch.mockResolvedValue(dbResult); + + const actual = await target.get({ + cacheDir, + query: mockQuery, + ttl, + }); + + expect(actual).toBe(dbResult); + expect(mockLoggingService.debug).toHaveBeenCalledWith({ + type: 'cache_miss', + key: 'key', + field: 'field', + }); + const cacheContent = await fakeCacheService.get(cacheDir); + expect(cacheContent).toBe(JSON.stringify(dbResult)); + }); + }); +}); diff --git a/src/datasources/db/cached-query-resolver.ts b/src/datasources/db/cached-query-resolver.ts index 4ce4ad9b70..33cf825fb2 100644 --- a/src/datasources/db/cached-query-resolver.ts +++ b/src/datasources/db/cached-query-resolver.ts @@ -3,6 +3,7 @@ import { ICacheService, } from '@/datasources/cache/cache.service.interface'; import { CacheDir } from '@/datasources/cache/entities/cache-dir.entity'; +import { ICachedQueryResolver } from '@/datasources/db/cached-query-resolver.interface'; import { ILoggingService, LoggingService } from '@/logging/logging.interface'; import { asError } from '@/logging/utils'; import { @@ -13,8 +14,7 @@ import { import postgres from 'postgres'; @Injectable() -// TODO: add/implement interface -export class CachedQueryResolver { +export class CachedQueryResolver implements ICachedQueryResolver { constructor( @Inject(LoggingService) private readonly loggingService: ILoggingService, @Inject(CacheService) private readonly cacheService: ICacheService, diff --git a/src/datasources/db/postgres-database.module.ts b/src/datasources/db/postgres-database.module.ts index c8fb93e79f..936b33b56b 100644 --- a/src/datasources/db/postgres-database.module.ts +++ b/src/datasources/db/postgres-database.module.ts @@ -5,6 +5,8 @@ import { IConfigurationService } from '@/config/configuration.service.interface' import { PostgresDatabaseMigrationHook } from '@/datasources/db/postgres-database.migration.hook'; import fs from 'fs'; import { PostgresDatabaseMigrator } from '@/datasources/db/postgres-database.migrator'; +import { ICachedQueryResolver } from '@/datasources/db/cached-query-resolver.interface'; +import { CachedQueryResolver } from '@/datasources/db/cached-query-resolver'; function dbFactory(configurationService: IConfigurationService): postgres.Sql { const caPath = configurationService.get('db.postgres.ssl.caPath'); @@ -48,9 +50,13 @@ function migratorFactory(sql: postgres.Sql): PostgresDatabaseMigrator { useFactory: migratorFactory, inject: ['DB_INSTANCE'], }, + { + provide: ICachedQueryResolver, + useClass: CachedQueryResolver, + }, PostgresDatabaseShutdownHook, PostgresDatabaseMigrationHook, ], - exports: ['DB_INSTANCE'], + exports: ['DB_INSTANCE', ICachedQueryResolver], }) export class PostgresDatabaseModule {}