Nest.jsで外部APIで、トークンを自動更新する

## やりたいこと
- interceptorsを利用して、トークンのキャッシュがある場合はセットする
- interceptorsを利用して、401が返って来た場合は、トークンを取り直す
- トークン取得関数はメモ化する
- APIの取得結果は一定時間キャッシュする

## 参考
- Axios Interceptor を使用して401が返ったときにトークン(JWT)を更新する｜tanoshima　https://zenn.dev/tanoshima/articles/db552b7962086a
- "NestJS tip: how to inject multiple versions of the same provider into one module (e.g.: many Axios instances)" by Micael Levi L. C.  https://dev.to/micalevisk/nestjs-tip-how-to-inject-multiple-versions-of-the-same-provider-into-one-module-eg-many-axios-instances-5agc
- [JavaScript] lodashのmemoizeで演算結果をキャッシュして高速化 https://qiita.com/etet-etet/items/e3a790caa2bd83a58498
- Lodash memoize with a timeout http://disq.us/t/429zou9
- [https://docs.nestjs.com/techniques/cachin](https://docs.nestjs.com/techniques/caching)
- [https://zenn.dev/kisihara_c/books/nest-officialdoc-jp/viewer/fundamentals-customproviders#エイリアスプロバイダ、useexisting](https://zenn.dev/kisihara_c/books/nest-officialdoc-jp/viewer/fundamentals-customproviders#%E3%82%A8%E3%82%A4%E3%83%AA%E3%82%A2%E3%82%B9%E3%83%97%E3%83%AD%E3%83%90%E3%82%A4%E3%83%80%E3%80%81useexisting)

## 事前準備
```
npm i --save @nestjs/axios axios
npm i --save @nestjs/cache-manager cache-manager
npm i --save lodash
```

## ファイル構成
```
src
└─ pet
   ├─ http-service
   │  ├─ login
   │  │  ├─ pet-login.modules.ts
   │  │  └─ pet-login.http.service.ts
   │  └─ api
   │     ├─ pet-api.modules.ts
   │     └─ pet-api.http.service.ts
   ├─ pet.module.ts
   └─ pet.repository.ts
```
```typescript
// pet-login.modules.ts
import { Module, OnModuleInit } from '@nestjs/common';
import { HttpModule, HttpService } from '@nestjs/axios';
import { PetLoginHttpService } from './pet-login.http.service';

const BASE_URL = '{リフレッシュトークン取得URL}';

@Module({
  imports: [
    HttpModule.register({
      baseURL: BASE_URL,
    }),
  ],
  providers: [
    {
      provide: PetLoginHttpService,
      useExisting: HttpService,
    },
  ],
  exports: [PetLoginHttpService],
})
export class PetLoginModule implements OnModuleInit {
  constructor(private readonly httpService: HttpService) {}
  onModuleInit() {
    this.httpService.axiosRef.defaults.headers.common['Accept'] =
      'application/json';
  }
}
```
```typescript
// pet-login.http.service.ts
import { HttpService } from '@nestjs/axios';
export abstract class PetLoginHttpService extends HttpService {}
```
```typescript
// pet-api.modules.ts
import { Module, OnModuleInit } from '@nestjs/common';
import { HttpModule, HttpService } from '@nestjs/axios';
import { PetApiHttpService } from './pet-api.http.service';

const BASE_URL = '{APIURL}';

@Module({
  imports: [
    HttpModule.register({
      baseURL: BASE_URL,
    }),
  ],
  providers: [
    {
      provide: PetApiHttpService,
      useExisting: HttpService,
    },
  ],
  exports: [PetAPiHttpService],
})
export class PetApiModule implements OnModuleInit {
  constructor(private readonly httpService: HttpService) {}
  onModuleInit() {
    this.httpService.axiosRef.defaults.headers.common['Accept'] =
      'application/json';
  }
}
```
```typescript
// pet-api.http.service.ts
import { HttpService } from '@nestjs/axios';
export abstract class PetApiHttpService extends HttpService {}
```
```typescript
// pet.module.ts
import { Module } from '@nestjs/common';
import { PetRepository } from './pet.repository';
import { PetLoginModule } from './http-service/login/pet-login.modules';
import { PetApiModule } from './http-service/api/pet-api.modules';

@Module({
  imports: [PetLoginModule, PetApiModule, CacheModule.register()],
  providers: [PetRepository],
  exports: [PetRepository],
})
export class PetoModule {}
```
```typescript
// pet.repository.ts
import { Inject, Injectable } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { ConfigUtil } from '../../../config/config.util';
import { PetLoginHttpService } from './http-service/login//pet-login.http.service';
import { PetApiHttpService } from './http-service/api/pet-api.http.service';
import { lastValueFrom, map } from 'rxjs';
import _ from 'lodash';

@Injectable()
export class PetRepository {
  constructor(
    private readonly configUtil: ConfigUtil,
    private readonly apiHttpService: PetLoginHttpService,
    private readonly loginHttpService: PetApiHttpService,
    @Inject(CACHE_MANAGER) private cacheManager: Cache,
  ) {
    // interceptor開始
    this.interceptor();
  }

  async gerPet(): Promise<void> {
    const ttl = 60 * 60 * 1000;
    // 結果は一定期間キャッシュする
    return await this.cacheManager.wrap(
      'cashkey-pet',
      async () => {
          const result = await lastValueFrom(
            this.apiHttpService
              .get('{APIのURL}')
              .pipe(map((response) => response.data)),
          );
          return result
          });
      },
      ttl,
    );
  }
 
  private interceptor() {
    // トークンのキャッシュがある場合はセットする
    this.apiHttpService.axiosRef.interceptors.request.use(
      async (config) => {
        const token = await this.cacheManager.get(
          'cashkey-access-token',
        );
        if (token) {
          config.headers.authorization = `Bearer ${token}`;
        } else {
          // ここでトークン取りにいったほうがいいかも？
        }

        return config;
      },
      (error) => Promise.reject(error),
    );
    // 401が返ったら、トークンを取得する
    this.apiHttpService.axiosRef.interceptors.response.use(
      (response) => response,
      async (error) => {
        if (error?.response?.status === 401) {
          // 切れたトークンをキーにする
          await memoizedRefreshToken(error.config.headers.authorization);
          return await lastValueFrom(this.apiHttpService.request(error.config));
        }

        return Promise.reject(error);
      },
    );
  }

  private async refreshToken() {
    const result = await lastValueFrom(
      this.loginHttpService
        .post('{リフレッシュトークンURL}')
        .pipe(map((response) => response.data)),
    );
    const ttl = 10 * 60 * 1000;
    await this.cacheManager.set(
        'cashkey-access-token',
        result.access_token,
        ttl,
      );
    );

    return result.access_token;
  };

  // メモ化
  const memoizedRefreshToken = _.memoize(refreshToken);
}
```