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

safe-global / safe-client-gateway / 8630594257

10 Apr 2024 11:38AM UTC coverage: 45.232% (-47.6%) from 92.862%
8630594257

Pull #1369

github

iamacook
Only store signer address in JWT payload
Pull Request #1369: Add `AuthGuard`

293 of 2225 branches covered (13.17%)

Branch coverage included in aggregate %.

4 of 9 new or added lines in 3 files covered. (44.44%)

2495 existing lines in 237 files now uncovered.

3540 of 6249 relevant lines covered (56.65%)

8.62 hits per line

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

36.08
/src/datasources/cache/cache.first.data.source.ts
1
import { Inject, Injectable } from '@nestjs/common';
14✔
2
import {
14✔
3
  CacheService,
4
  ICacheService,
5
} from '@/datasources/cache/cache.service.interface';
6
import { CacheDir } from '@/datasources/cache/entities/cache-dir.entity';
14✔
7
import { NetworkResponseError } from '@/datasources/network/entities/network.error.entity';
14✔
8
import { NetworkRequest } from '@/datasources/network/entities/network.request.entity';
9
import {
14✔
10
  INetworkService,
11
  NetworkService,
12
} from '@/datasources/network/network.service.interface';
13
import { ILoggingService, LoggingService } from '@/logging/logging.interface';
14✔
14
import { Page } from '@/domain/entities/page.entity';
15
import {
14✔
16
  isMultisigTransaction,
17
  isEthereumTransaction,
18
  isModuleTransaction,
19
  isCreationTransaction,
20
  Transaction,
21
} from '@/domain/safe/entities/transaction.entity';
22
import { IConfigurationService } from '@/config/configuration.service.interface';
14✔
23
import { isArray } from 'lodash';
14✔
24

25
/**
26
 * A data source which tries to retrieve values from cache using
27
 * {@link CacheService} and fallbacks to {@link NetworkService}
28
 * if the cache entry expired or is not present.
29
 *
30
 * This is the recommended data source that should be used when
31
 * a feature requires both networking and caching the respective
32
 * responses.
33
 */
34
@Injectable()
35
export class CacheFirstDataSource {
14✔
36
  private readonly isHistoryDebugLogsEnabled: boolean;
37

38
  constructor(
39
    @Inject(CacheService) private readonly cacheService: ICacheService,
14✔
40
    @Inject(NetworkService) private readonly networkService: INetworkService,
14✔
41
    @Inject(LoggingService) private readonly loggingService: ILoggingService,
14✔
42
    @Inject(IConfigurationService)
43
    private readonly configurationService: IConfigurationService,
14✔
44
  ) {
45
    this.isHistoryDebugLogsEnabled =
14✔
46
      this.configurationService.getOrThrow<boolean>(
47
        'features.historyDebugLogs',
48
      );
49
  }
50

51
  /**
52
   * Gets the cached value behind {@link CacheDir}.
53
   * If the value is not present, it tries to get the respective JSON
54
   * payload from {@link url}.
55
   * 404 errors are cached with {@link notFoundExpireTimeSeconds} seconds expiration time.
56
   *
57
   * @param args.cacheDir - {@link CacheDir} containing the key and field to be used to retrieve from cache
58
   * @param args.url - the HTTP endpoint to retrieve the JSON payload
59
   * @param args.networkRequest - the HTTP request to be used if there is a cache miss
60
   * @param args.expireTimeSeconds - the time to live in seconds for the payload behind {@link CacheDir}
61
   * @param args.notFoundExpireTimeSeconds - the time to live in seconds for the error when the item is not found
62
   */
63
  async get<T>(args: {
64
    cacheDir: CacheDir;
65
    url: string;
66
    notFoundExpireTimeSeconds: number;
67
    networkRequest?: NetworkRequest;
68
    expireTimeSeconds?: number;
69
  }): Promise<T> {
70
    const cached = await this.cacheService.get(args.cacheDir);
14✔
71
    if (cached != null) return this._getFromCachedData(args.cacheDir, cached);
14!
72

73
    try {
14✔
74
      return await this._getFromNetworkAndWriteCache(args);
14✔
75
    } catch (error) {
UNCOV
76
      if (
×
77
        error instanceof NetworkResponseError &&
×
78
        error.response.status === 404
79
      ) {
UNCOV
80
        await this.cacheNotFoundError(
×
81
          args.cacheDir,
82
          error,
83
          args.notFoundExpireTimeSeconds,
84
        );
85
      }
UNCOV
86
      throw error;
×
87
    }
88
  }
89

90
  /**
91
   * Gets the data from the contents stored in the cache.
92
   */
93
  private _getFromCachedData<T>(
94
    { key, field }: CacheDir,
95
    cached: string,
96
  ): Promise<T> {
UNCOV
97
    this.loggingService.debug({ type: 'cache_hit', key, field });
×
UNCOV
98
    const cachedData = JSON.parse(cached);
×
UNCOV
99
    if (cachedData?.response?.status === 404) {
×
UNCOV
100
      throw new NetworkResponseError(
×
101
        cachedData.url,
102
        cachedData.response,
103
        cachedData?.data,
×
104
      );
105
    }
UNCOV
106
    return cachedData;
×
107
  }
108

109
  /**
110
   * Gets the data from the network and caches the result.
111
   */
112
  private async _getFromNetworkAndWriteCache<T>(args: {
113
    cacheDir: CacheDir;
114
    url: string;
115
    networkRequest?: NetworkRequest;
116
    expireTimeSeconds?: number;
117
  }): Promise<T> {
118
    const { key, field } = args.cacheDir;
14✔
119
    this.loggingService.debug({ type: 'cache_miss', key, field });
14✔
120
    const startTimeMs = Date.now();
14✔
121
    const { data } = await this.networkService.get<T>({
14✔
122
      url: args.url,
123
      networkRequest: args.networkRequest,
124
    });
125

126
    const shouldBeCached = await this._shouldBeCached(key, startTimeMs);
14✔
127
    if (shouldBeCached) {
14✔
128
      await this.cacheService.set(
14✔
129
        args.cacheDir,
130
        JSON.stringify(data),
131
        args.expireTimeSeconds,
132
      );
133

134
      // TODO: transient logging for debugging
135
      if (
14!
136
        this.isHistoryDebugLogsEnabled &&
7!
137
        args.url.includes('all-transactions')
138
      ) {
139
        this.logTransactionsCacheWrite(
×
140
          startTimeMs,
141
          args.cacheDir,
142
          data as Page<Transaction>,
143
        );
144
      }
145
    }
146
    return data;
14✔
147
  }
148

149
  /**
150
   * Validates that the request is more recent than the last invalidation recorded for the item,
151
   * preventing a race condition where outdated data is stored due to the request being initiated
152
   * before the source communicated a change (via webhook or by other means).
153
   *
154
   * Returns true if (any of the following):
155
   * 1. An invalidationTimeMs entry for the key received as param is *not* found in the cache.
156
   * 2. An entry *is* found and contains an integer that is less than the received startTimeMs param.
157
   *
158
   * @param key key part of the {@link CacheDir} holding the requested item
159
   * @param startTimeMs Unix epoch timestamp in ms when the request was initiated
160
   * @returns true if any of the above conditions is met
161
   */
162
  private async _shouldBeCached(
163
    key: string,
164
    startTimeMs: number,
165
  ): Promise<boolean> {
166
    const invalidationTimeMsStr = await this.cacheService.get(
14✔
167
      new CacheDir(`invalidationTimeMs:${key}`, ''),
168
    );
169

170
    if (!invalidationTimeMsStr) return true;
14✔
171

UNCOV
172
    const invalidationTimeMs = Number(invalidationTimeMsStr);
×
UNCOV
173
    return (
×
174
      Number.isInteger(invalidationTimeMs) && invalidationTimeMs < startTimeMs
×
175
    );
176
  }
177

178
  /**
179
   * Caches a not found error.
180
   * @param cacheDir - {@link CacheDir} where the error should be placed
181
   */
182
  private async cacheNotFoundError(
183
    cacheDir: CacheDir,
184
    error: NetworkResponseError,
185
    notFoundExpireTimeSeconds?: number,
186
  ): Promise<void> {
UNCOV
187
    return this.cacheService.set(
×
188
      cacheDir,
189
      JSON.stringify({
190
        data: error.data,
191
        response: { status: error.response.status },
192
        url: error.url,
193
      }),
194
      notFoundExpireTimeSeconds,
195
    );
196
  }
197

198
  /**
199
   * Logs the type and the hash of the transactions present in the data parameter.
200
   * NOTE: this is a debugging-only function.
201
   * TODO: remove this function after debugging.
202
   */
203
  private logTransactionsCacheWrite(
204
    requestStartTime: number,
205
    cacheDir: CacheDir,
206
    data: Page<Transaction>,
207
  ): void {
208
    this.loggingService.info({
×
209
      type: 'cache_write',
210
      cacheKey: cacheDir.key,
211
      cacheField: cacheDir.field,
212
      cacheWriteTime: new Date(),
213
      requestStartTime: new Date(requestStartTime),
214
      txHashes:
215
        isArray(data?.results) && // no validation executed yet at this point
×
216
        data.results.map((transaction) => {
217
          if (isMultisigTransaction(transaction)) {
×
218
            return {
×
219
              txType: 'multisig',
220
              safeTxHash: transaction.safeTxHash,
221
            };
222
          } else if (isEthereumTransaction(transaction)) {
×
223
            return {
×
224
              txType: 'ethereum',
225
              txHash: transaction.txHash,
226
            };
227
          } else if (isModuleTransaction(transaction)) {
×
228
            return {
×
229
              txType: 'module',
230
              transactionHash: transaction.transactionHash,
231
            };
232
          } else if (isCreationTransaction(transaction)) {
×
233
            return {
×
234
              txType: 'creation',
235
              transactionHash: transaction.transactionHash,
236
            };
237
          }
238
        }),
239
    });
240
  }
241
}
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