Skip to content

Commit

Permalink
refactor: use generic cache manager adaptor for redis
Browse files Browse the repository at this point in the history
  • Loading branch information
okjodom committed Nov 2, 2024
1 parent 1f190e3 commit e55cac1
Show file tree
Hide file tree
Showing 13 changed files with 120 additions and 55 deletions.
19 changes: 13 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ bun install

## Compile and run the project

```bash
# development with docker
$ bun start
```
### docker dev

- `bun start` to start all the services in a docker compose environment
- `bun stop` to shutdown all the services run via docker compose

## Compile and run individual services
## individual services

```bash
# development with docker
# general pattern to run an app
$ bun dev <app>

# for example, to run the swap microservice
Expand All @@ -34,6 +34,13 @@ $ bun dev api
# unit tests
$ bun test

# target a specific test
$ bun test <test-name-or-file-path>

# watch for changes and re-run tests
$ bun test:watch
$ bun test:watch <test-name-or-file-path>

# e2e tests
$ bun test:e2e

Expand Down
21 changes: 20 additions & 1 deletion apps/api/src/swap/swap.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,20 @@ describe('SwapService', () => {
status: 'PENDING',
};
}),
listSwaps: jest.fn().mockImplementation(async () => {
return {
swaps: [
{
id: mock_id,
rate: mock_rate.toString(),
status: 'PENDING',
},
],
page: 0,
size: 10,
pages: 2,
};
}),
};
});

Expand Down Expand Up @@ -131,7 +145,12 @@ describe('SwapService', () => {

describe('getOnrampTransactions', () => {
it('should return status 200', () => {
expect(swapService.getOnrampTransactions()).toEqual({ status: 200 });
expect(
swapService.getOnrampTransactions({
page: 0,
size: 10,
}),
).toBeDefined();
});
});

Expand Down
2 changes: 1 addition & 1 deletion apps/swap/src/events.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('EventsController', () => {
{
provide: SwapService,
useValue: {
processSwapUpdate: jest.fn(), //.mockImplementation((data: MpesaTransactionUpdateDto) => {}),
processSwapUpdate: jest.fn(),
},
},
],
Expand Down
2 changes: 1 addition & 1 deletion apps/swap/src/fx/fx.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TestingModule } from '@nestjs/testing';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { createTestingModuleWithValidation } from '@bitsacco/common';
import { CacheModule } from '@nestjs/cache-manager';
Expand Down
10 changes: 6 additions & 4 deletions apps/swap/src/fx/fx.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { AxiosError } from 'axios';
import { firstValueFrom, catchError } from 'rxjs';
import { Inject, Injectable, Logger } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { ConfigService } from '@nestjs/config';
import { HttpService } from '@nestjs/axios';
import type { Cache } from 'cache-manager';
import { AxiosError } from 'axios';
import { CustomStore } from '@bitsacco/common';

interface CurrencyApiResponse {
meta: {
Expand All @@ -27,13 +27,15 @@ export class FxService {
constructor(
private readonly configService: ConfigService,
private readonly httpService: HttpService,
@Inject(CACHE_MANAGER) private cacheManager: Cache,
@Inject(CACHE_MANAGER) private cacheManager: CustomStore,
) {
this.logger.log('FxService initialized');
}

private async getCurrencyApiRates() {
const cachedData = await this.cacheManager.get(this.CACHE_KEY);
const cachedData = await this.cacheManager.get<{
btcToKesRate: string;
} | void>(this.CACHE_KEY);
if (cachedData) {
this.logger.log('Returning cached currency rates');
return cachedData;
Expand Down
7 changes: 5 additions & 2 deletions apps/swap/src/swap.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Currency, createTestingModuleWithValidation } from '@bitsacco/common';
import {
CreateOnrampSwapDto,
Currency,
createTestingModuleWithValidation,
} from '@bitsacco/common';
import { TestingModule } from '@nestjs/testing';
import { SwapService } from './swap.service';
import { SwapController } from './swap.controller';
import { CreateOnrampSwapDto } from './dto';

describe('SwapController', () => {
let swapController: SwapController;
Expand Down
10 changes: 5 additions & 5 deletions apps/swap/src/swap.module.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import * as Joi from 'joi';
import { redisStore } from 'cache-manager-redis-store';
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { CACHE_MANAGER, CacheModule, CacheStore } from '@nestjs/cache-manager';
import { LoggerModule } from '@bitsacco/common';
import { CACHE_MANAGER, CacheModule } from '@nestjs/cache-manager';
import { CustomStore, LoggerModule } from '@bitsacco/common';
import { SwapController } from './swap.controller';
import { SwapService } from './swap.service';
import { FxService } from './fx/fx.service';
import { PrismaService } from './prisma.service';
import { IntasendService } from './intasend/intasend.service';
import { EventsController } from './events.controller';
import { FedimintService } from './fedimint/fedimint.service';
import { RedisStore, redisStore } from 'cache-manager-redis-store';

@Module({
imports: [
Expand Down Expand Up @@ -48,7 +48,7 @@ import { RedisStore, redisStore } from 'cache-manager-redis-store';
});

return {
store: store as unknown as CacheStore,
store: new CustomStore(store, undefined /* TODO: inject logger */),
ttl: 60 * 60 * 5, // 5 hours
};
},
Expand All @@ -64,7 +64,7 @@ import { RedisStore, redisStore } from 'cache-manager-redis-store';
intasendService: IntasendService,
fedimintService: FedimintService,
prismaService: PrismaService,
cacheManager: RedisStore,
cacheManager: CustomStore,
) => {
return new SwapService(
fxService,
Expand Down
13 changes: 6 additions & 7 deletions apps/swap/src/swap.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
btcFromKes,
CreateOnrampSwapDto,
createTestingModuleWithValidation,
Currency,
SwapStatus,
Expand All @@ -11,8 +12,8 @@ import { FxService } from './fx/fx.service';
import { SwapService } from './swap.service';
import { IntasendService } from './intasend/intasend.service';
import { MpesaTractactionState } from './intasend/intasend.types';
import { CreateOnrampSwapDto, MpesaTransactionUpdateDto } from './dto';
import { FedimintService } from './fedimint/fedimint.service';
import { MpesaTransactionUpdateDto } from './dto';

const mock_rate = 8708520.117232416;

Expand All @@ -24,13 +25,11 @@ describe('SwapService', () => {
let mockCacheManager: {
get: jest.Mock;
set: jest.Mock;
getOrThrow: jest.Mock;
};

beforeEach(async () => {
mockCacheManager = {
get: jest.fn(),
getOrThrow: jest.fn(),
set: jest.fn(),
};

Expand Down Expand Up @@ -154,7 +153,7 @@ describe('SwapService', () => {
ref: 'test-onramp-swap',
};

(mockCacheManager.getOrThrow as jest.Mock).mockImplementation(
(mockCacheManager.get as jest.Mock).mockImplementation(
(_key: string) => cache,
);

Expand Down Expand Up @@ -215,7 +214,7 @@ describe('SwapService', () => {
state: MpesaTractactionState.Pending,
};

(mockCacheManager.getOrThrow as jest.Mock).mockImplementation(
(mockCacheManager.get as jest.Mock).mockImplementation(
(_key: string) => cache,
);

Expand All @@ -226,7 +225,7 @@ describe('SwapService', () => {
expect(swap).toBeDefined();
expect(swap.rate).toEqual(cache.rate);
expect(swap.status).toEqual(SwapStatus.PENDING);
expect(mockCacheManager.getOrThrow).toHaveBeenCalled();
expect(mockCacheManager.get).toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -255,7 +254,7 @@ describe('SwapService', () => {
};

it('creates a new swap tx if there was none recorded before', async () => {
(mockCacheManager.getOrThrow as jest.Mock).mockImplementation(
(mockCacheManager.get as jest.Mock).mockImplementation(
(_key: string) => ({
lightning: 'lnbtcexampleinvoicee',
phone: '0700000000',
Expand Down
31 changes: 7 additions & 24 deletions apps/swap/src/swap.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import {
SwapStatus,
CreateOnrampSwapDto,
FindSwapDto,
cacheSetOrThrow,
cacheGetOrThrow,
CustomStore,
} from '@bitsacco/common';
import { v4 as uuidv4 } from 'uuid';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
Expand All @@ -22,7 +21,6 @@ import { IntasendService } from './intasend/intasend.service';
import { MpesaTransactionUpdateDto } from './dto';
import { MpesaTractactionState } from './intasend/intasend.types';
import { FedimintService } from './fedimint/fedimint.service';
import { RedisStore } from 'cache-manager-redis-store';

@Injectable()
export class SwapService {
Expand All @@ -34,7 +32,7 @@ export class SwapService {
private readonly intasendService: IntasendService,
private readonly fedimintService: FedimintService,
private readonly prismaService: PrismaService,
@Inject(CACHE_MANAGER) private readonly cacheManager: RedisStore,
@Inject(CACHE_MANAGER) private readonly cacheManager: CustomStore,
) {
this.logger.log('SwapService initialized');
}
Expand All @@ -60,13 +58,7 @@ export class SwapService {
expiry: expiry.toString(),
};

await cacheSetOrThrow(
quote.id,
quote,
this.CACHE_TTL_SECS,
this.cacheManager,
this.logger,
);
await this.cacheManager.set(quote.id, quote, this.CACHE_TTL_SECS);

return quote;
} catch (error) {
Expand All @@ -83,8 +75,7 @@ export class SwapService {
lightning,
}: CreateOnrampSwapDto): Promise<OnrampSwapResponse> {
let currentQuote: QuoteResponse | undefined =
quote &&
(await cacheGetOrThrow(quote.id, this.cacheManager, this.logger));
quote && (await this.cacheManager.get<QuoteResponse>(quote.id));

if (
!currentQuote ||
Expand All @@ -108,7 +99,7 @@ export class SwapService {
// We record stk push response to a temporary cache
// so we can track status of the swap later
// NOTE: we use mpesa ids as cache keys
await cacheSetOrThrow(
this.cacheManager.set(
mpesa.id,
{
lightning,
Expand All @@ -119,8 +110,6 @@ export class SwapService {
ref,
},
this.CACHE_TTL_SECS,
this.cacheManager,
this.logger,
);

const swap = await this.prismaService.mpesaOnrampSwap.create({
Expand Down Expand Up @@ -165,11 +154,7 @@ export class SwapService {
// FIXME!
// Look up stk push response in cache
// NOTE: we use mpesa ids as cache keys
const stk: STKPushCache = await cacheGetOrThrow(
id,
this.cacheManager,
this.logger,
);
const stk: STKPushCache = await this.cacheManager.get<STKPushCache>(id);

resp = {
id,
Expand Down Expand Up @@ -226,10 +211,8 @@ export class SwapService {
});
} catch {
// look up mpesa tx in cache
const stk: STKPushCache = await cacheGetOrThrow(
const stk: STKPushCache = await this.cacheManager.get<STKPushCache>(
mpesa.id,
this.cacheManager,
this.logger,
);

// record a new swap in db
Expand Down
3 changes: 1 addition & 2 deletions libs/common/src/utils/cache.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Logger } from '@nestjs/common';
import { RedisStore } from 'cache-manager-redis-store';
import { cacheGetOrThrow, cacheSetOrThrow } from './cache';
import { cacheGetOrThrow, cacheSetOrThrow, RedisStore } from './cache';

describe('Cache Utils', () => {
let mockCache: jest.Mocked<RedisStore>;
Expand Down
54 changes: 53 additions & 1 deletion libs/common/src/utils/cache.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,57 @@
import type { Store } from 'cache-manager';
import type { RedisClientType } from 'redis';
import { CacheStore } from '@nestjs/cache-manager';
import { Logger } from '@nestjs/common';
import { RedisStore } from 'cache-manager-redis-store';

export interface RedisStore extends Store {
name: string;
getClient: () => RedisClientType;
isCacheableValue: any;
set: (key: any, value: any, options: any, cb: any) => Promise<any>;
get: (key: any, options: any, cb: any) => Promise<any>;
del: (...args: any[]) => Promise<any>;
mset: (...args: any[]) => Promise<any>;
mget: (...args: any[]) => Promise<any>;
mdel: (...args: any[]) => Promise<any>;
reset: (cb: any) => Promise<any>;
keys: (pattern: string, cb: any) => Promise<any>;
ttl: (key: any, cb: any) => Promise<any>;
}

/**
* Adapt `RedisStore` to `CacheStore` interface
* Nest has a generic CacheStore interface to abstract cache implementations
* This is a workaround to make it work with RedisStore
* and cache-manager v4.1.0
* @see https://docs.nestjs.com/techniques/caching
* @see https://www.npmjs.com/package/cache-manager/v/4.1.0
*/
export class CustomStore implements CacheStore {
constructor(
private readonly cache: RedisStore,
private readonly logger?: Logger,
) {}

async get<T = any>(key: string): Promise<T> {
return cacheGetOrThrow<T>(key, this.cache, this.logger);
}

async set<T = any>(key: string, value: T, ttl: number): Promise<void> {
return cacheSetOrThrow<T>(key, value, ttl, this.cache, this.logger);
}

async del(key: string): Promise<void> {
return new Promise((resolve, reject) => {
this.cache.del(key, (err) => {
if (err) {
this.logger?.error(err);
return reject(err);
}
return resolve();
});
});
}
}

export async function cacheGetOrThrow<T>(
key: string,
Expand Down
Loading

0 comments on commit e55cac1

Please sign in to comment.