From 6c5c83b7df4ca2079a9993d3a30b5ccad02900ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hector=20G=C3=B3mez=20Varela?= Date: Fri, 19 Jul 2024 18:01:09 +0200 Subject: [PATCH] Implement CachedQueryResolver --- .../accounts/accounts.datasource.spec.ts | 2 + .../accounts/accounts.datasource.ts | 33 +++++----- .../counterfactual-safes.datasource.spec.ts | 2 + .../counterfactual-safes.datasource.ts | 25 ++++---- src/datasources/db/cached-query-resolver.ts | 61 +++++++++++++++++++ src/datasources/db/utils.ts | 48 --------------- 6 files changed, 90 insertions(+), 81 deletions(-) create mode 100644 src/datasources/db/cached-query-resolver.ts delete mode 100644 src/datasources/db/utils.ts diff --git a/src/datasources/accounts/accounts.datasource.spec.ts b/src/datasources/accounts/accounts.datasource.spec.ts index 5455db20df..28b12565cc 100644 --- a/src/datasources/accounts/accounts.datasource.spec.ts +++ b/src/datasources/accounts/accounts.datasource.spec.ts @@ -4,6 +4,7 @@ import { AccountsDatasource } from '@/datasources/accounts/accounts.datasource'; import { FakeCacheService } from '@/datasources/cache/__tests__/fake.cache.service'; import { MAX_TTL } from '@/datasources/cache/constants'; import { CacheDir } from '@/datasources/cache/entities/cache-dir.entity'; +import { CachedQueryResolver } from '@/datasources/db/cached-query-resolver'; import { PostgresDatabaseMigrator } from '@/datasources/db/postgres-database.migrator'; import { accountDataTypeBuilder } from '@/domain/accounts/entities/__tests__/account-data-type.builder'; import { upsertAccountDataSettingsDtoBuilder } from '@/domain/accounts/entities/__tests__/upsert-account-data-settings.dto.entity.builder'; @@ -43,6 +44,7 @@ describe('AccountsDatasource tests', () => { target = new AccountsDatasource( fakeCacheService, sql, + new CachedQueryResolver(mockLoggingService, fakeCacheService), mockLoggingService, mockConfigurationService, ); diff --git a/src/datasources/accounts/accounts.datasource.ts b/src/datasources/accounts/accounts.datasource.ts index c39bd3c928..39f88ff954 100644 --- a/src/datasources/accounts/accounts.datasource.ts +++ b/src/datasources/accounts/accounts.datasource.ts @@ -5,7 +5,7 @@ import { ICacheService, } from '@/datasources/cache/cache.service.interface'; import { MAX_TTL } from '@/datasources/cache/constants'; -import { getFromCacheOrExecuteAndCache } from '@/datasources/db/utils'; +import { CachedQueryResolver } from '@/datasources/db/cached-query-resolver'; 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 +29,7 @@ export class AccountsDatasource implements IAccountsDatasource, OnModuleInit { constructor( @Inject(CacheService) private readonly cacheService: ICacheService, @Inject('DB_INSTANCE') private readonly sql: postgres.Sql, + private readonly cachedQueryResolver: CachedQueryResolver, @Inject(LoggingService) private readonly loggingService: ILoggingService, @Inject(IConfigurationService) private readonly configurationService: IConfigurationService, @@ -70,15 +71,13 @@ export class AccountsDatasource implements IAccountsDatasource, OnModuleInit { async getAccount(args: { address: `0x${string}` }): Promise { const cacheDir = CacheRouter.getAccountCacheDir(args.address); - const [account] = await getFromCacheOrExecuteAndCache( - this.loggingService, - this.cacheService, + const [account] = await this.cachedQueryResolver.get({ cacheDir, - this.sql< + query: this.sql< Account[] >`SELECT * FROM accounts WHERE address = ${args.address}`, - this.defaultExpirationTimeInSeconds, - ); + ttl: this.defaultExpirationTimeInSeconds, + }); if (!account) { throw new NotFoundException('Error getting account.'); @@ -108,13 +107,11 @@ export class AccountsDatasource implements IAccountsDatasource, OnModuleInit { async getDataTypes(): Promise { const cacheDir = CacheRouter.getAccountDataTypesCacheDir(); - return getFromCacheOrExecuteAndCache( - this.loggingService, - this.cacheService, + return this.cachedQueryResolver.get({ cacheDir, - this.sql`SELECT * FROM account_data_types`, - MAX_TTL, - ); + query: this.sql`SELECT * FROM account_data_types`, + ttl: MAX_TTL, + }); } async getAccountDataSettings(args: { @@ -122,16 +119,14 @@ export class AccountsDatasource implements IAccountsDatasource, OnModuleInit { }): Promise { const account = await this.getAccount(args); const cacheDir = CacheRouter.getAccountDataSettingsCacheDir(args.address); - return getFromCacheOrExecuteAndCache( - this.loggingService, - this.cacheService, + return this.cachedQueryResolver.get({ cacheDir, - this.sql` + query: this.sql` SELECT ads.* FROM account_data_settings ads INNER JOIN account_data_types adt ON ads.account_data_type_id = adt.id WHERE ads.account_id = ${account.id} AND adt.is_active IS TRUE;`, - this.defaultExpirationTimeInSeconds, - ); + ttl: this.defaultExpirationTimeInSeconds, + }); } /** diff --git a/src/datasources/accounts/counterfactual-safes/counterfactual-safes.datasource.spec.ts b/src/datasources/accounts/counterfactual-safes/counterfactual-safes.datasource.spec.ts index 5893ed7248..5063d6324c 100644 --- a/src/datasources/accounts/counterfactual-safes/counterfactual-safes.datasource.spec.ts +++ b/src/datasources/accounts/counterfactual-safes/counterfactual-safes.datasource.spec.ts @@ -3,6 +3,7 @@ import { IConfigurationService } from '@/config/configuration.service.interface' import { CounterfactualSafesDatasource } from '@/datasources/accounts/counterfactual-safes/counterfactual-safes.datasource'; import { FakeCacheService } from '@/datasources/cache/__tests__/fake.cache.service'; import { CacheDir } from '@/datasources/cache/entities/cache-dir.entity'; +import { CachedQueryResolver } from '@/datasources/db/cached-query-resolver'; import { PostgresDatabaseMigrator } from '@/datasources/db/postgres-database.migrator'; import { createCounterfactualSafeDtoBuilder } from '@/domain/accounts/counterfactual-safes/entities/__tests__/create-counterfactual-safe.dto.entity.builder'; import { accountBuilder } from '@/domain/accounts/entities/__tests__/account.builder'; @@ -43,6 +44,7 @@ describe('CounterfactualSafesDatasource tests', () => { target = new CounterfactualSafesDatasource( fakeCacheService, sql, + new CachedQueryResolver(mockLoggingService, fakeCacheService), mockLoggingService, mockConfigurationService, ); diff --git a/src/datasources/accounts/counterfactual-safes/counterfactual-safes.datasource.ts b/src/datasources/accounts/counterfactual-safes/counterfactual-safes.datasource.ts index 603cc2a06f..d10ccc92a1 100644 --- a/src/datasources/accounts/counterfactual-safes/counterfactual-safes.datasource.ts +++ b/src/datasources/accounts/counterfactual-safes/counterfactual-safes.datasource.ts @@ -4,7 +4,7 @@ import { CacheService, ICacheService, } from '@/datasources/cache/cache.service.interface'; -import { getFromCacheOrExecuteAndCache } from '@/datasources/db/utils'; +import { CachedQueryResolver } from '@/datasources/db/cached-query-resolver'; 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 +22,7 @@ export class CounterfactualSafesDatasource constructor( @Inject(CacheService) private readonly cacheService: ICacheService, @Inject('DB_INSTANCE') private readonly sql: postgres.Sql, + private readonly cachedQueryResolver: CachedQueryResolver, @Inject(LoggingService) private readonly loggingService: ILoggingService, @Inject(IConfigurationService) private readonly configurationService: IConfigurationService, @@ -58,16 +59,14 @@ export class CounterfactualSafesDatasource args.chainId, args.predictedAddress, ); - const [counterfactualSafe] = await getFromCacheOrExecuteAndCache< + const [counterfactualSafe] = await this.cachedQueryResolver.get< CounterfactualSafe[] - >( - this.loggingService, - this.cacheService, + >({ cacheDir, - this.sql` + query: this.sql` SELECT * FROM counterfactual_safes WHERE chain_id = ${args.chainId} AND predicted_address = ${args.predictedAddress}`, - this.defaultExpirationTimeInSeconds, - ); + ttl: this.defaultExpirationTimeInSeconds, + }); if (!counterfactualSafe) { throw new NotFoundException('Error getting Counterfactual Safe.'); @@ -82,14 +81,12 @@ export class CounterfactualSafesDatasource const cacheDir = CacheRouter.getCounterfactualSafesCacheDir( args.account.address, ); - return getFromCacheOrExecuteAndCache( - this.loggingService, - this.cacheService, + return this.cachedQueryResolver.get({ cacheDir, - this.sql` + query: this.sql` SELECT * FROM counterfactual_safes WHERE account_id = ${args.account.id}`, - this.defaultExpirationTimeInSeconds, - ); + ttl: this.defaultExpirationTimeInSeconds, + }); } async deleteCounterfactualSafe(args: { diff --git a/src/datasources/db/cached-query-resolver.ts b/src/datasources/db/cached-query-resolver.ts new file mode 100644 index 0000000000..4ce4ad9b70 --- /dev/null +++ b/src/datasources/db/cached-query-resolver.ts @@ -0,0 +1,61 @@ +import { + CacheService, + ICacheService, +} from '@/datasources/cache/cache.service.interface'; +import { CacheDir } from '@/datasources/cache/entities/cache-dir.entity'; +import { ILoggingService, LoggingService } from '@/logging/logging.interface'; +import { asError } from '@/logging/utils'; +import { + Inject, + Injectable, + InternalServerErrorException, +} from '@nestjs/common'; +import postgres from 'postgres'; + +@Injectable() +// TODO: add/implement interface +export class CachedQueryResolver { + constructor( + @Inject(LoggingService) private readonly loggingService: ILoggingService, + @Inject(CacheService) private readonly cacheService: ICacheService, + ) {} + + /** + * Returns the content from cache or executes the query and caches the result. + * If the specified {@link CacheDir} is empty, the query is executed and the result is cached. + * If the specified {@link CacheDir} is not empty, the pointed content is returned. + * + * @param cacheDir {@link CacheDir} to use for caching + * @param query query to execute + * @param ttl time to live for the cache + * @returns content from cache or query result + */ + async get(args: { + cacheDir: CacheDir; + query: postgres.PendingQuery; + ttl: number; + }): Promise { + const { key, field } = args.cacheDir; + const cached = await this.cacheService.get(args.cacheDir); + if (cached != null) { + this.loggingService.debug({ type: 'cache_hit', key, field }); + return JSON.parse(cached); + } + this.loggingService.debug({ type: 'cache_miss', key, field }); + + // log & hide database errors + const result = await args.query.catch((e) => { + this.loggingService.error(asError(e).message); + throw new InternalServerErrorException(); + }); + + if (result.count > 0) { + await this.cacheService.set( + args.cacheDir, + JSON.stringify(result), + args.ttl, + ); + } + return result; + } +} diff --git a/src/datasources/db/utils.ts b/src/datasources/db/utils.ts deleted file mode 100644 index ebc7da1104..0000000000 --- a/src/datasources/db/utils.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ICacheService } from '@/datasources/cache/cache.service.interface'; -import { CacheDir } from '@/datasources/cache/entities/cache-dir.entity'; -import { ILoggingService } from '@/logging/logging.interface'; -import { asError } from '@/logging/utils'; -import { InternalServerErrorException } from '@nestjs/common'; -import postgres from 'postgres'; - -/** - * Returns the content from cache or executes the query and caches the result. - * If the specified {@link CacheDir} is empty, the query is executed and the result is cached. - * If the specified {@link CacheDir} is not empty, the pointed content is returned. - * - * @param loggingService {@link ILoggingService} to use for logging - * @param cacheService {@link ICacheService} to use for caching - * @param cacheDir {@link CacheDir} to use for caching - * @param query query to execute - * @param ttl time to live for the cache - * @returns content from cache or query result - */ -// TODO: add tests -export async function getFromCacheOrExecuteAndCache< - T extends postgres.MaybeRow[], ->( - loggingService: ILoggingService, - cacheService: ICacheService, - cacheDir: CacheDir, - query: postgres.PendingQuery, - ttl: number, -): Promise { - const { key, field } = cacheDir; - const cached = await cacheService.get(cacheDir); - if (cached != null) { - loggingService.debug({ type: 'cache_hit', key, field }); - return JSON.parse(cached); - } - loggingService.debug({ type: 'cache_miss', key, field }); - - // log & hide database errors - const result = await query.catch((e) => { - loggingService.error(asError(e).message); - throw new InternalServerErrorException(); - }); - - if (result.count > 0) { - await cacheService.set(cacheDir, JSON.stringify(result), ttl); - } - return result; -}