• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

safe-global / safe-client-gateway / 10057609107

23 Jul 2024 10:54AM UTC coverage: 48.449% (-0.005%) from 48.454%
10057609107

Pull #1777

github

hectorgomezv
Implement CachedQueryResolver
Pull Request #1777: Extract getFromCacheOrExecuteAndCache utility function

437 of 2633 branches covered (16.6%)

Branch coverage included in aggregate %.

6 of 24 new or added lines in 2 files covered. (25.0%)

1 existing line in 1 file now uncovered.

4328 of 7202 relevant lines covered (60.09%)

13.07 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

15.87
/src/datasources/accounts/accounts.datasource.ts
1
import { IConfigurationService } from '@/config/configuration.service.interface';
16✔
2
import { CacheRouter } from '@/datasources/cache/cache.router';
16✔
3
import {
16✔
4
  CacheService,
5
  ICacheService,
6
} from '@/datasources/cache/cache.service.interface';
7
import { MAX_TTL } from '@/datasources/cache/constants';
16✔
8
import { CachedQueryResolver } from '@/datasources/db/cached-query-resolver';
16✔
9
import { AccountDataSetting } from '@/domain/accounts/entities/account-data-setting.entity';
10
import { AccountDataType } from '@/domain/accounts/entities/account-data-type.entity';
11
import { Account } from '@/domain/accounts/entities/account.entity';
12
import { UpsertAccountDataSettingsDto } from '@/domain/accounts/entities/upsert-account-data-settings.dto.entity';
13
import { IAccountsDatasource } from '@/domain/interfaces/accounts.datasource.interface';
14
import { ILoggingService, LoggingService } from '@/logging/logging.interface';
16✔
15
import { asError } from '@/logging/utils';
16✔
16
import {
16✔
17
  Inject,
18
  Injectable,
19
  NotFoundException,
20
  OnModuleInit,
21
  UnprocessableEntityException,
22
} from '@nestjs/common';
23
import postgres from 'postgres';
16✔
24

25
@Injectable()
26
export class AccountsDatasource implements IAccountsDatasource, OnModuleInit {
16✔
27
  private readonly defaultExpirationTimeInSeconds: number;
28

29
  constructor(
30
    @Inject(CacheService) private readonly cacheService: ICacheService,
×
31
    @Inject('DB_INSTANCE') private readonly sql: postgres.Sql,
×
NEW
32
    private readonly cachedQueryResolver: CachedQueryResolver,
×
UNCOV
33
    @Inject(LoggingService) private readonly loggingService: ILoggingService,
×
34
    @Inject(IConfigurationService)
35
    private readonly configurationService: IConfigurationService,
×
36
  ) {
37
    this.defaultExpirationTimeInSeconds =
×
38
      this.configurationService.getOrThrow<number>(
39
        'expirationTimeInSeconds.default',
40
      );
41
  }
42

43
  /**
44
   * Function executed when the module is initialized.
45
   * It deletes the cache for persistent keys.
46
   */
47
  async onModuleInit(): Promise<void> {
48
    await this.cacheService.deleteByKey(
×
49
      CacheRouter.getAccountDataTypesCacheDir().key,
50
    );
51
  }
52

53
  async createAccount(address: `0x${string}`): Promise<Account> {
54
    const [account] = await this.sql<[Account]>`
×
55
      INSERT INTO accounts (address) VALUES (${address}) RETURNING *`.catch(
56
      (e) => {
57
        this.loggingService.warn(
×
58
          `Error creating account: ${asError(e).message}`,
59
        );
60
        throw new UnprocessableEntityException('Error creating account.');
×
61
      },
62
    );
63
    const cacheDir = CacheRouter.getAccountCacheDir(address);
×
64
    await this.cacheService.set(
×
65
      cacheDir,
66
      JSON.stringify([account]),
67
      this.defaultExpirationTimeInSeconds,
68
    );
69
    return account;
×
70
  }
71

72
  async getAccount(address: `0x${string}`): Promise<Account> {
73
    const cacheDir = CacheRouter.getAccountCacheDir(address);
×
NEW
74
    const [account] = await this.cachedQueryResolver.get<Account[]>({
×
75
      cacheDir,
76
      query: this.sql<
77
        Account[]
78
      >`SELECT * FROM accounts WHERE address = ${address}`,
79
      ttl: this.defaultExpirationTimeInSeconds,
80
    });
81

82
    if (!account) {
×
83
      throw new NotFoundException('Error getting account.');
×
84
    }
85

86
    return account;
×
87
  }
88

89
  async deleteAccount(address: `0x${string}`): Promise<void> {
90
    try {
×
91
      const { count } = await this
×
92
        .sql`DELETE FROM accounts WHERE address = ${address}`;
93
      if (count === 0) {
×
94
        this.loggingService.debug(
×
95
          `Error deleting account ${address}: not found`,
96
        );
97
      }
98
    } finally {
99
      const keys = [
×
100
        CacheRouter.getAccountCacheDir(address).key,
101
        CacheRouter.getAccountDataSettingsCacheDir(address).key,
102
        CacheRouter.getCounterfactualSafesCacheDir(address).key,
103
      ];
104
      await Promise.all(keys.map((key) => this.cacheService.deleteByKey(key)));
×
105
    }
106
  }
107

108
  async getDataTypes(): Promise<AccountDataType[]> {
109
    const cacheDir = CacheRouter.getAccountDataTypesCacheDir();
×
NEW
110
    return this.cachedQueryResolver.get<AccountDataType[]>({
×
111
      cacheDir,
112
      query: this.sql<AccountDataType[]>`SELECT * FROM account_data_types`,
113
      ttl: MAX_TTL,
114
    });
115
  }
116

117
  async getAccountDataSettings(
118
    address: `0x${string}`,
119
  ): Promise<AccountDataSetting[]> {
120
    const account = await this.getAccount(address);
×
121
    const cacheDir = CacheRouter.getAccountDataSettingsCacheDir(address);
×
NEW
122
    return this.cachedQueryResolver.get<AccountDataSetting[]>({
×
123
      cacheDir,
124
      query: this.sql<AccountDataSetting[]>`
125
        SELECT ads.* FROM account_data_settings ads INNER JOIN account_data_types adt
126
          ON ads.account_data_type_id = adt.id
127
        WHERE ads.account_id = ${account.id} AND adt.is_active IS TRUE;`,
128
      ttl: this.defaultExpirationTimeInSeconds,
129
    });
130
  }
131

132
  /**
133
   * Adds or updates the existing account data settings for a given address/account.
134
   * Requirements:
135
   * - The account must exist.
136
   * - The data type must exist.
137
   * - The data type must be active.
138
   *
139
   * @param address - account address.
140
   * @param upsertAccountDataSettings {@link UpsertAccountDataSettingsDto} object.
141
   * @returns {Array<AccountDataSetting>} inserted account data settings.
142
   */
143
  async upsertAccountDataSettings(args: {
144
    address: `0x${string}`;
145
    upsertAccountDataSettingsDto: UpsertAccountDataSettingsDto;
146
  }): Promise<AccountDataSetting[]> {
147
    const { accountDataSettings } = args.upsertAccountDataSettingsDto;
×
148
    await this.checkDataTypes(accountDataSettings);
×
149
    const account = await this.getAccount(args.address);
×
150

151
    const result = await this.sql.begin(async (sql) => {
×
152
      await Promise.all(
×
153
        accountDataSettings.map(async (accountDataSetting) => {
154
          return sql`
×
155
            INSERT INTO account_data_settings (account_id, account_data_type_id, enabled)
156
            VALUES (${account.id}, ${accountDataSetting.dataTypeId}, ${accountDataSetting.enabled})
157
            ON CONFLICT (account_id, account_data_type_id) DO UPDATE SET enabled = EXCLUDED.enabled
158
          `.catch((e) => {
159
            throw new UnprocessableEntityException(
×
160
              `Error updating data settings: ${asError(e).message}`,
161
            );
162
          });
163
        }),
164
      );
165
      return sql<[AccountDataSetting]>`
×
166
        SELECT * FROM account_data_settings WHERE account_id = ${account.id}`;
167
    });
168

169
    const cacheDir = CacheRouter.getAccountDataSettingsCacheDir(args.address);
×
170
    await this.cacheService.set(
×
171
      cacheDir,
172
      JSON.stringify(result),
173
      this.defaultExpirationTimeInSeconds,
174
    );
175
    return result;
×
176
  }
177

178
  private async checkDataTypes(
179
    accountDataSettings: UpsertAccountDataSettingsDto['accountDataSettings'],
180
  ): Promise<void> {
181
    const dataTypes = await this.getDataTypes();
×
182
    const activeDataTypeIds = dataTypes
×
183
      .filter((dt) => dt.is_active)
×
184
      .map((ads) => ads.id);
×
185
    if (
×
186
      !accountDataSettings.every((ads) =>
187
        activeDataTypeIds.includes(Number(ads.dataTypeId)),
×
188
      )
189
    ) {
190
      throw new UnprocessableEntityException(
×
191
        `Data types not found or not active.`,
192
      );
193
    }
194
  }
195
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc