Skip to content

Commit 04c5183

Browse files
committed
feat(api): adding CacheModule
1 parent eab2d6b commit 04c5183

21 files changed

+312
-47
lines changed

apps/api/API-TESTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ curl -v -X POST \
9090

9191
```bash
9292
OIDC_ISSUER_URL=https://keycloak-ngx.1d35.starter-us-east-1.openshiftapps.com/auth/realms/ngx
93-
OIDC_CLIENT_ID=ngxapp
93+
OIDC_CLIENT_ID=ngxapi
9494

9595
USERNAME=sumo3
9696
PASSWORD=demo
@@ -105,7 +105,7 @@ response=$(curl -X POST $OIDC_ISSUER_URL/protocol/openid-connect/token \
105105
-H "Content-Type: application/x-www-form-urlencoded" \
106106
-d username=$USERNAME \
107107
-d password=$PASSWORD \
108-
-d client_id=OIDC_CLIENT_ID \
108+
-d client_id=$OIDC_CLIENT_ID \
109109
-d 'grant_type=password' \
110110
-d 'scope=openid')
111111

apps/api/src/app/auth/auth.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { UpdateUserDto } from './dto/update-user.dto';
77
import { CreateUserDto } from './dto/create-user.dto';
88

99
@ApiOAuth2Auth(['read'])
10-
@ApiUseTags('Sumo', 'Auth')
10+
@ApiUseTags('Auth')
1111
@Controller()
1212
export class AuthController extends CrudController<User> {
1313
constructor(private readonly authService: AuthService) {

apps/api/src/app/auth/user.entity.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
1-
import { Column, CreateDateColumn, Entity, Index, OneToMany, UpdateDateColumn, VersionColumn } from 'typeorm';
1+
import {
2+
Column,
3+
CreateDateColumn,
4+
Entity,
5+
Index,
6+
OneToMany,
7+
RelationId,
8+
UpdateDateColumn,
9+
VersionColumn,
10+
} from 'typeorm';
211
import { ApiModelProperty } from '@nestjs/swagger';
312
import { IsAscii, IsEmail, IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
413
import { Base } from '../core/entities/base.entity';
5-
6-
export enum AccountSourceType {
7-
msId,
8-
hsId,
9-
gitHub,
10-
}
14+
import { Image } from '../user/profile/image.entity';
15+
import { OneToOne } from 'typeorm/decorator/relations/OneToOne';
16+
import { JoinColumn } from 'typeorm/decorator/relations/JoinColumn';
17+
import { Profile } from '../user/profile/profile.entity';
18+
import { User as IUser } from '@ngx-starter-kit/models';
1119

1220
@Entity('user')
13-
export class User extends Base {
21+
export class User extends Base implements IUser {
1422
@ApiModelProperty({ type: String })
1523
@IsString()
1624
@IsNotEmpty()
@@ -41,12 +49,28 @@ export class User extends Base {
4149
@Column()
4250
userId: string;
4351

52+
@ApiModelProperty({ type: 'string', format: 'date-time', example: '2018-11-21T06:20:32.232Z' })
4453
@CreateDateColumn({ type: 'timestamptz' })
4554
createdAt?: Date;
4655

56+
@ApiModelProperty({ type: 'string', format: 'date-time', example: '2018-11-21T06:20:32.232Z' })
4757
@UpdateDateColumn({ type: 'timestamptz' })
4858
updatedAt?: Date;
4959

5060
@VersionColumn()
5161
version?: number;
62+
63+
@ApiModelProperty({ type: Image, isArray: true })
64+
@OneToMany(_ => Image, image => image.user)
65+
images?: Image[];
66+
67+
@ApiModelProperty({ type: Profile })
68+
// FIXME: OneToOne downward cascade delete not implemented
69+
@OneToOne(type => Profile, { cascade: ['insert', 'remove'], nullable: true, onDelete: 'SET NULL' })
70+
@JoinColumn()
71+
profile?: Profile;
72+
73+
@ApiModelProperty({ type: Number })
74+
@RelationId((user: User) => user.profile)
75+
profileId?: number;
5276
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Test, TestingModule } from '@nestjs/testing';
2+
import { CacheConfigService } from './cache-config.service';
3+
4+
describe('CacheConfigService', () => {
5+
let service: CacheConfigService;
6+
beforeAll(async () => {
7+
const module: TestingModule = await Test.createTestingModule({
8+
providers: [CacheConfigService],
9+
}).compile();
10+
service = module.get<CacheConfigService>(CacheConfigService);
11+
});
12+
it('should be defined', () => {
13+
expect(service).toBeDefined();
14+
});
15+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { CacheModuleOptions, CacheOptionsFactory, Injectable } from '@nestjs/common';
2+
// REf: https://github.com/kyle-mccarthy/nest-next-starter/tree/master/src/cache
3+
@Injectable()
4+
export class CacheConfigService implements CacheOptionsFactory {
5+
constructor() {}
6+
7+
/**
8+
* Example retry strategy for when redis is used for the cache
9+
* This example is only compatible with cache-manager-redis-store because it used node_redis
10+
*/
11+
public retryStrategy() {
12+
return {
13+
retry_strategy: (options: any) => {
14+
if (options.error && options.error.code === 'ECONNREFUSED') {
15+
return new Error('The server refused the connection');
16+
}
17+
if (options.total_retry_time > 1000 * 60) {
18+
return new Error('Retry time exhausted');
19+
}
20+
if (options.attempt > 2) {
21+
return new Error('Max attempts exhausted');
22+
}
23+
return Math.min(options.attempt * 100, 3000);
24+
},
25+
};
26+
}
27+
28+
public createCacheOptions(): CacheModuleOptions {
29+
return {
30+
ttl: 5, // seconds
31+
max: 10, // maximum number of items in cache
32+
};
33+
}
34+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { CacheModule as NestCacheModule, Global, Module } from '@nestjs/common';
2+
import { CacheConfigService } from './cache-config.service';
3+
import { CacheService } from './cache.service';
4+
5+
@Global()
6+
@Module({
7+
imports: [
8+
NestCacheModule.registerAsync({
9+
useClass: CacheConfigService,
10+
inject: [CacheConfigService],
11+
}),
12+
],
13+
providers: [CacheConfigService, CacheService],
14+
exports: [CacheService],
15+
})
16+
export class CacheModule {}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { CacheService, ICacheManager } from './cache.service';
2+
3+
describe('CacheService', () => {
4+
let service: CacheService;
5+
6+
let store: any = {};
7+
8+
const Manager = jest.fn<ICacheManager>().mockImplementation(() => {
9+
return {
10+
get: jest.fn((key: string) => store[key]),
11+
set: jest.fn((key: string, value: any, options?: { ttl: number }) => {
12+
store[key] = value;
13+
}),
14+
};
15+
});
16+
17+
const manager = new Manager();
18+
19+
beforeAll(async () => {
20+
service = new CacheService(manager);
21+
});
22+
23+
beforeEach(async () => {
24+
store = {};
25+
});
26+
27+
it('should be defined', () => {
28+
expect(service).toBeDefined();
29+
});
30+
31+
it('should call set', () => {
32+
service.set('test', 'test');
33+
34+
expect(manager.set).toHaveBeenCalledTimes(1);
35+
});
36+
37+
it('should call get', () => {
38+
const res = service.get('test');
39+
40+
expect(res).toBeUndefined();
41+
expect(manager.get).toHaveBeenCalledTimes(1);
42+
});
43+
44+
it('should get set value', () => {
45+
store.testKey = 'test value';
46+
47+
expect(service.get('testKey')).toEqual('test value');
48+
});
49+
50+
it('set should overwrite existing value', () => {
51+
store.current = 0;
52+
53+
expect(store.current).toEqual(0);
54+
service.set('current', 1);
55+
expect(store.current).toEqual(1);
56+
});
57+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common';
2+
3+
export interface ICacheManager {
4+
store: any;
5+
get(key: string): any;
6+
set(key: string, value: string, options?: { ttl: number }): any;
7+
}
8+
9+
@Injectable()
10+
export class CacheService {
11+
private cache!: ICacheManager;
12+
13+
constructor(@Inject(CACHE_MANAGER) cache: ICacheManager) {
14+
this.cache = cache;
15+
}
16+
17+
public get(key: string): Promise<any> {
18+
return this.cache.get(key);
19+
}
20+
21+
public set(key: string, value: any, options?: { ttl: number }): Promise<any> {
22+
return this.cache.set(key, value, options);
23+
}
24+
}

apps/api/src/app/cache/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './cache.service';
2+
export * from './cache.module';

apps/api/src/app/core/crud/crud.controller.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { Get, Post, Put, Delete, Body, Param, HttpStatus } from '@nestjs/common'
22
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
33
import { Base } from '../entities/base.entity';
44
import { DeepPartial } from 'typeorm';
5-
import { ICrudService } from './icube.service';
5+
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
6+
import { ICrudService } from './icrud.service';
67

78
@ApiResponse({ status: HttpStatus.UNAUTHORIZED, description: 'Unauthorized' })
89
@ApiResponse({ status: HttpStatus.FORBIDDEN, description: 'Forbidden' })
@@ -31,7 +32,7 @@ export abstract class CrudController<T extends Base> {
3132
description: 'Invalid input, The response body may contain clues as to what went wrong',
3233
})
3334
@Post()
34-
async create(@Body() entity: DeepPartial<T>, options?: any): Promise<T> {
35+
async create(@Body() entity: DeepPartial<T>, ...options: any[]): Promise<T> {
3536
return this.crudService.create(entity);
3637
}
3738

@@ -43,15 +44,15 @@ export abstract class CrudController<T extends Base> {
4344
description: 'Invalid input, The response body may contain clues as to what went wrong',
4445
})
4546
@Put(':id')
46-
async update(@Param('id') id: string, @Body() entity: DeepPartial<T>, options?: any): Promise<any> {
47+
async update(@Param('id') id: string, @Body() entity: QueryDeepPartialEntity<T>, ...options: any[]): Promise<any> {
4748
return this.crudService.update(id, entity); // FIXME: https://github.com/typeorm/typeorm/issues/1544
4849
}
4950

5051
@ApiOperation({ title: 'Delete record' })
5152
@ApiResponse({ status: HttpStatus.NO_CONTENT, description: 'The record has been successfully deleted' })
5253
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Record not found' })
5354
@Delete(':id')
54-
async delete(@Param('id') id: string, options?: any): Promise<any> {
55+
async delete(@Param('id') id: string, ...options: any[]): Promise<any> {
5556
return this.crudService.delete(id);
5657
}
5758
}

0 commit comments

Comments
 (0)