Skip to content

Commit

Permalink
feat(api): adding CacheModule
Browse files Browse the repository at this point in the history
  • Loading branch information
xmlking committed Feb 18, 2019
1 parent eab2d6b commit 04c5183
Show file tree
Hide file tree
Showing 21 changed files with 312 additions and 47 deletions.
4 changes: 2 additions & 2 deletions apps/api/API-TESTING.md
Expand Up @@ -90,7 +90,7 @@ curl -v -X POST \

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

USERNAME=sumo3
PASSWORD=demo
Expand All @@ -105,7 +105,7 @@ response=$(curl -X POST $OIDC_ISSUER_URL/protocol/openid-connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d username=$USERNAME \
-d password=$PASSWORD \
-d client_id=OIDC_CLIENT_ID \
-d client_id=$OIDC_CLIENT_ID \
-d 'grant_type=password' \
-d 'scope=openid')

Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/app/auth/auth.controller.ts
Expand Up @@ -7,7 +7,7 @@ import { UpdateUserDto } from './dto/update-user.dto';
import { CreateUserDto } from './dto/create-user.dto';

@ApiOAuth2Auth(['read'])
@ApiUseTags('Sumo', 'Auth')
@ApiUseTags('Auth')
@Controller()
export class AuthController extends CrudController<User> {
constructor(private readonly authService: AuthService) {
Expand Down
40 changes: 32 additions & 8 deletions apps/api/src/app/auth/user.entity.ts
@@ -1,16 +1,24 @@
import { Column, CreateDateColumn, Entity, Index, OneToMany, UpdateDateColumn, VersionColumn } from 'typeorm';
import {
Column,
CreateDateColumn,
Entity,
Index,
OneToMany,
RelationId,
UpdateDateColumn,
VersionColumn,
} from 'typeorm';
import { ApiModelProperty } from '@nestjs/swagger';
import { IsAscii, IsEmail, IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
import { Base } from '../core/entities/base.entity';

export enum AccountSourceType {
msId,
hsId,
gitHub,
}
import { Image } from '../user/profile/image.entity';
import { OneToOne } from 'typeorm/decorator/relations/OneToOne';
import { JoinColumn } from 'typeorm/decorator/relations/JoinColumn';
import { Profile } from '../user/profile/profile.entity';
import { User as IUser } from '@ngx-starter-kit/models';

@Entity('user')
export class User extends Base {
export class User extends Base implements IUser {
@ApiModelProperty({ type: String })
@IsString()
@IsNotEmpty()
Expand Down Expand Up @@ -41,12 +49,28 @@ export class User extends Base {
@Column()
userId: string;

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

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

@VersionColumn()
version?: number;

@ApiModelProperty({ type: Image, isArray: true })
@OneToMany(_ => Image, image => image.user)
images?: Image[];

@ApiModelProperty({ type: Profile })
// FIXME: OneToOne downward cascade delete not implemented
@OneToOne(type => Profile, { cascade: ['insert', 'remove'], nullable: true, onDelete: 'SET NULL' })
@JoinColumn()
profile?: Profile;

@ApiModelProperty({ type: Number })
@RelationId((user: User) => user.profile)
profileId?: number;
}
15 changes: 15 additions & 0 deletions apps/api/src/app/cache/cache-config.service.spec.ts
@@ -0,0 +1,15 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CacheConfigService } from './cache-config.service';

describe('CacheConfigService', () => {
let service: CacheConfigService;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CacheConfigService],
}).compile();
service = module.get<CacheConfigService>(CacheConfigService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
34 changes: 34 additions & 0 deletions apps/api/src/app/cache/cache-config.service.ts
@@ -0,0 +1,34 @@
import { CacheModuleOptions, CacheOptionsFactory, Injectable } from '@nestjs/common';
// REf: https://github.com/kyle-mccarthy/nest-next-starter/tree/master/src/cache
@Injectable()
export class CacheConfigService implements CacheOptionsFactory {
constructor() {}

/**
* Example retry strategy for when redis is used for the cache
* This example is only compatible with cache-manager-redis-store because it used node_redis
*/
public retryStrategy() {
return {
retry_strategy: (options: any) => {
if (options.error && options.error.code === 'ECONNREFUSED') {
return new Error('The server refused the connection');
}
if (options.total_retry_time > 1000 * 60) {
return new Error('Retry time exhausted');
}
if (options.attempt > 2) {
return new Error('Max attempts exhausted');
}
return Math.min(options.attempt * 100, 3000);
},
};
}

public createCacheOptions(): CacheModuleOptions {
return {
ttl: 5, // seconds
max: 10, // maximum number of items in cache
};
}
}
16 changes: 16 additions & 0 deletions apps/api/src/app/cache/cache.module.ts
@@ -0,0 +1,16 @@
import { CacheModule as NestCacheModule, Global, Module } from '@nestjs/common';
import { CacheConfigService } from './cache-config.service';
import { CacheService } from './cache.service';

@Global()
@Module({
imports: [
NestCacheModule.registerAsync({
useClass: CacheConfigService,
inject: [CacheConfigService],
}),
],
providers: [CacheConfigService, CacheService],
exports: [CacheService],
})
export class CacheModule {}
57 changes: 57 additions & 0 deletions apps/api/src/app/cache/cache.service.spec.ts
@@ -0,0 +1,57 @@
import { CacheService, ICacheManager } from './cache.service';

describe('CacheService', () => {
let service: CacheService;

let store: any = {};

const Manager = jest.fn<ICacheManager>().mockImplementation(() => {
return {
get: jest.fn((key: string) => store[key]),
set: jest.fn((key: string, value: any, options?: { ttl: number }) => {
store[key] = value;
}),
};
});

const manager = new Manager();

beforeAll(async () => {
service = new CacheService(manager);
});

beforeEach(async () => {
store = {};
});

it('should be defined', () => {
expect(service).toBeDefined();
});

it('should call set', () => {
service.set('test', 'test');

expect(manager.set).toHaveBeenCalledTimes(1);
});

it('should call get', () => {
const res = service.get('test');

expect(res).toBeUndefined();
expect(manager.get).toHaveBeenCalledTimes(1);
});

it('should get set value', () => {
store.testKey = 'test value';

expect(service.get('testKey')).toEqual('test value');
});

it('set should overwrite existing value', () => {
store.current = 0;

expect(store.current).toEqual(0);
service.set('current', 1);
expect(store.current).toEqual(1);
});
});
24 changes: 24 additions & 0 deletions apps/api/src/app/cache/cache.service.ts
@@ -0,0 +1,24 @@
import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common';

export interface ICacheManager {
store: any;
get(key: string): any;
set(key: string, value: string, options?: { ttl: number }): any;
}

@Injectable()
export class CacheService {
private cache!: ICacheManager;

constructor(@Inject(CACHE_MANAGER) cache: ICacheManager) {
this.cache = cache;
}

public get(key: string): Promise<any> {
return this.cache.get(key);
}

public set(key: string, value: any, options?: { ttl: number }): Promise<any> {
return this.cache.set(key, value, options);
}
}
2 changes: 2 additions & 0 deletions apps/api/src/app/cache/index.ts
@@ -0,0 +1,2 @@
export * from './cache.service';
export * from './cache.module';
9 changes: 5 additions & 4 deletions apps/api/src/app/core/crud/crud.controller.ts
Expand Up @@ -2,7 +2,8 @@ import { Get, Post, Put, Delete, Body, Param, HttpStatus } from '@nestjs/common'
import { ApiOperation, ApiResponse } from '@nestjs/swagger';
import { Base } from '../entities/base.entity';
import { DeepPartial } from 'typeorm';
import { ICrudService } from './icube.service';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { ICrudService } from './icrud.service';

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

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

@ApiOperation({ title: 'Delete record' })
@ApiResponse({ status: HttpStatus.NO_CONTENT, description: 'The record has been successfully deleted' })
@ApiResponse({ status: HttpStatus.NOT_FOUND, description: 'Record not found' })
@Delete(':id')
async delete(@Param('id') id: string, options?: any): Promise<any> {
async delete(@Param('id') id: string, ...options: any[]): Promise<any> {
return this.crudService.delete(id);
}
}
32 changes: 27 additions & 5 deletions apps/api/src/app/core/crud/crud.service.ts
Expand Up @@ -8,8 +8,11 @@ import {
Repository,
UpdateResult,
} from 'typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { mergeMap } from 'rxjs/operators';
import { of, throwError } from 'rxjs';
import { Base } from '../entities/base.entity';
import { ICrudService } from './icube.service';
import { ICrudService } from './icrud.service';

export abstract class CrudService<T extends Base> implements ICrudService<T> {
protected constructor(protected readonly repository: Repository<T>) {}
Expand All @@ -33,7 +36,7 @@ export abstract class CrudService<T extends Base> implements ICrudService<T> {
return record;
}

public async create(entity: DeepPartial<T>): Promise<T> {
public async create(entity: DeepPartial<T>, ...options: any[]): Promise<T> {
const obj = this.repository.create(entity);
try {
// https://github.com/Microsoft/TypeScript/issues/21592
Expand All @@ -43,19 +46,38 @@ export abstract class CrudService<T extends Base> implements ICrudService<T> {
}
}

public async update(id: string | number | FindConditions<T>, entity: DeepPartial<T>): Promise<UpdateResult> {
public async update(
id: string | number | FindConditions<T>,
partialEntity: QueryDeepPartialEntity<T>,
...options: any[]
): Promise<UpdateResult | T> {
try {
return await this.repository.update(id, entity);
return await this.repository.update(id, partialEntity);
} catch (err /*: WriteError*/) {
throw new BadRequestException(err);
}
}

public async delete(criteria: string | number | FindConditions<T>): Promise<DeleteResult> {
public async delete(criteria: string | number | FindConditions<T>, ...options: any[]): Promise<DeleteResult> {
try {
return this.repository.delete(criteria);
} catch (err) {
throw new NotFoundException(`The record was not found`, err);
}
}

/**
* e.g., findOneById(id).pipe(map(entity => entity.id), entityNotFound())
*/
private entityNotFound() {
return stream$ =>
stream$.pipe(
mergeMap(signal => {
if (!signal) {
return throwError(new NotFoundException(`The requested record was not found`));
}
return of(signal);
}),
);
}
}
10 changes: 10 additions & 0 deletions apps/api/src/app/core/crud/icrud.service.ts
@@ -0,0 +1,10 @@
import { DeepPartial, DeleteResult, FindConditions, FindManyOptions, FindOneOptions, UpdateResult } from 'typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';

export interface ICrudService<T> {
getAll(filter?: FindManyOptions<T>): Promise<[T[], number]>;
getOne(id: string | number | FindOneOptions<T> | FindConditions<T>, options?: FindOneOptions<T>): Promise<T>;
create(entity: DeepPartial<T>, ...options: any[]): Promise<T>;
update(id: any, entity: QueryDeepPartialEntity<T>, ...options: any[]): Promise<UpdateResult | T>;
delete(id: any, ...options: any[]): Promise<DeleteResult>;
}
9 changes: 0 additions & 9 deletions apps/api/src/app/core/crud/icube.service.ts

This file was deleted.

0 comments on commit 04c5183

Please sign in to comment.