Browse Source

Initial commit

main
Alexandre SOARES 5 years ago
commit
667cc46ad7
  1. 24
      .eslintrc.js
  2. 37
      .gitignore
  3. 4
      .prettierrc
  4. 47
      README.md
  5. 6
      compodoc.server.json
  6. 9
      dev/pg.yml
  7. 7
      example.env
  8. 4
      nest-cli.json
  9. 80
      package.json
  10. 22
      src/app.controller.spec.ts
  11. 20
      src/app.controller.ts
  12. 30
      src/app.module.ts
  13. 8
      src/app.service.ts
  14. 9
      src/config/config.module.ts
  15. 102
      src/config/config.service.spec.ts
  16. 69
      src/config/config.service.ts
  17. 12
      src/main.ts
  18. 6
      src/todos/dto/create-todo.dto.ts
  19. 4
      src/todos/dto/update-todo.dto.ts
  20. 13
      src/todos/entities/todo.entity.ts
  21. 49
      src/todos/todos.controller.ts
  22. 12
      src/todos/todos.module.ts
  23. 49
      src/todos/todos.service.ts
  24. 72
      test/app.e2e-spec.ts
  25. 9
      test/jest-e2e.json
  26. 4
      tsconfig.build.json
  27. 16
      tsconfig.json
  28. 7363
      yarn.lock

24
.eslintrc.js

@ -0,0 +1,24 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

37
.gitignore

@ -0,0 +1,37 @@
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.env
docs/

4
.prettierrc

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

47
README.md

@ -0,0 +1,47 @@
<h1 align="center">Welcome to 2021-2022-devops-final-backend 👋</h1>
> The back end to my new startup idea
## Installation
```bash
$ yarn install
```
## Running the app
```bash
# development
yarn start
# watch mode
yarn start:dev
# production mode
$ yarn build
&&
$ yarn start:prod
```
## Test
```bash
# unit tests
$ yarn test
# e2e tests
$ yarn test:e2e
```
## Lints
```bash
# Check
$ yarn format:check
# Format code
$ yarn format:check
# Lint code
$ yarn lint
```

6
compodoc.server.json

@ -0,0 +1,6 @@
{
"port": 9911,
"name": "TP_CI SERVER",
"output": "docs",
"tsconfig": "./tsconfig.json"
}

9
dev/pg.yml

@ -0,0 +1,9 @@
version: '2'
services:
ognion-boilerplate-postgresql:
image: postgres:9.6.5
environment:
- POSTGRES_USER=tpCi
- POSTGRES_PASSWORD=someNotSecurePassword
ports:
- 5432:5432

7
example.env

@ -0,0 +1,7 @@
## LOGGER
LOG_LEVEL=debug
LOG_SQL_REQUEST=false
PORT=3000
## DB [TypeORM]
DATABASE_URL=postgres://tpCi:someNotSecurePassword@localhost:5432/tpCi

4
nest-cli.json

@ -0,0 +1,4 @@
{
"collection": "@nestjs/schematics",
"sourceRoot": "src"
}

80
package.json

@ -0,0 +1,80 @@
{
"name": "2021-2022-devops-03-ci-tp",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"format:check": "prettier --list-different \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"doc:build": "compodoc -c compodoc.server.json",
"doc": "compodoc -c compodoc.server.json -s -o -w",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^8.0.0",
"@nestjs/core": "^8.0.0",
"@nestjs/mapped-types": "*",
"@nestjs/platform-express": "^8.0.0",
"@nestjs/typeorm": "8.0.2",
"dotenv": "10.0.0",
"pg": "8.7.1",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"typeorm": "0.2.38",
"zod": "3.9.8"
},
"devDependencies": {
"@compodoc/compodoc": "1.1.15",
"@nestjs/cli": "^8.0.0",
"@nestjs/schematics": "^8.0.0",
"@nestjs/testing": "^8.0.0",
"@types/express": "^4.17.13",
"@types/jest": "^26.0.24",
"@types/node": "^16.0.0",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^4.28.2",
"@typescript-eslint/parser": "^4.28.2",
"eslint": "^7.30.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"jest": "27.0.6",
"prettier": "^2.3.2",
"supertest": "^6.1.3",
"ts-jest": "^27.0.3",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "^3.10.1",
"typescript": "^4.3.5"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

22
src/app.controller.spec.ts

@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

20
src/app.controller.ts

@ -0,0 +1,20 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
@Get('env')
getEnv() {
return {
...process.env,
DATABASE_URL: 'URL',
};
}
}

30
src/app.module.ts

@ -0,0 +1,30 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from './config/config.module';
import { ConfigService } from './config/config.service';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { TodosModule } from './todos/todos.module';
@Module({
imports: [
ConfigModule,
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (
configService: ConfigService,
): Promise<TypeOrmModuleOptions> => ({
type: 'postgres',
url: configService.databaseUrl,
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
logging: configService.isLoggingDb ? 'all' : false,
}),
inject: [ConfigService],
}),
TodosModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

8
src/app.service.ts

@ -0,0 +1,8 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

9
src/config/config.module.ts

@ -0,0 +1,9 @@
import { Global, Module } from '@nestjs/common';
import { ConfigService } from './config.service';
@Global()
@Module({
providers: [ConfigService],
exports: [ConfigService],
})
export class ConfigModule {}

102
src/config/config.service.spec.ts

@ -0,0 +1,102 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from './config.service';
import { expand } from 'rxjs/operators';
describe('ConfigService', () => {
const OLD_ENV = process.env;
beforeEach(() => {
jest.resetModules(); // this is important - it clears the cache
process.env = { ...OLD_ENV };
delete process.env.NODE_ENV;
});
afterEach(() => {
process.env = OLD_ENV;
});
const loadService = async (): Promise<ConfigService> => {
const module: TestingModule = await Test.createTestingModule({
providers: [ConfigService],
}).compile();
return module.get<ConfigService>(ConfigService);
};
it('should have default values', async () => {
const dbUrl = 'gotodb please';
const jwtSecrets = 'shhhh, secret';
// set the variables
process.env.NODE_ENV = undefined;
process.env.API_PORT = undefined;
process.env.API_PROTOCOL = undefined;
process.env.LOG_LEVEL = undefined;
process.env.LOG_SQL_REQUEST = undefined;
process.env.DATABASE_URL = dbUrl;
process.env.JWT_SECRET = jwtSecrets;
const configService = await loadService();
expect(configService.nodeEnv).toEqual('development');
expect(configService.loggerLevel).toEqual('debug');
expect(configService.databaseUrl).toEqual(dbUrl);
expect(configService.isLoggingDb).toEqual(false);
});
it('should error when no db url set', async () => {
const jwtSecrets = 'shhhh, secret';
// set the variables
process.env.NODE_ENV = undefined;
process.env.API_PORT = undefined;
process.env.API_PROTOCOL = undefined;
process.env.LOG_LEVEL = undefined;
process.env.LOG_SQL_REQUEST = undefined;
process.env.DATABASE_URL = undefined;
process.env.JWT_SECRET = jwtSecrets;
try {
const configService = await loadService();
fail();
} catch (e) {
expect(e.message).toMatchInlineSnapshot(`
"Config validation error: [
{
\\"code\\": \\"invalid_type\\",
\\"expected\\": \\"string\\",
\\"received\\": \\"undefined\\",
\\"path\\": [
\\"DATABASE_URL\\"
],
\\"message\\": \\"Required\\"
}
]"
`);
}
});
it('should set correct values', async () => {
const dbUrl = 'gotodb please';
const jwtSecrets = 'shhhh, secret';
const nodeEnv = 'test';
const apiPort = 2020;
const logLevel = 'silly';
const logSqlRequest = true;
// set the variables
process.env.NODE_ENV = nodeEnv;
process.env.API_PORT = `${apiPort}`;
process.env.LOG_LEVEL = logLevel;
process.env.LOG_SQL_REQUEST = `${logSqlRequest}`;
process.env.DATABASE_URL = dbUrl;
process.env.JWT_SECRET = jwtSecrets;
const configService = await loadService();
expect(configService.nodeEnv).toEqual(nodeEnv);
expect(configService.loggerLevel).toEqual(logLevel);
expect(configService.databaseUrl).toEqual(dbUrl);
expect(configService.isLoggingDb).toEqual(logSqlRequest);
});
});

69
src/config/config.service.ts

@ -0,0 +1,69 @@
import { Injectable } from '@nestjs/common';
import * as z from 'zod';
import { config as parseConfig } from 'dotenv';
const schema = z.object({
NODE_ENV: z
.enum(['development', 'production', 'test'])
.default('development'),
LOG_LEVEL: z
.enum(['error', 'warning', 'info', 'debug', 'silly'])
.default('debug'),
DATABASE_URL: z.string().nonempty(),
LOG_SQL_REQUEST: z
.string()
.transform((value) => value === 'true')
.default('false'),
PORT: z
.string()
.transform((value) => parseInt(value))
.default('3000'),
});
type EnvConfig = z.infer<typeof schema>;
@Injectable()
export class ConfigService {
private readonly envConfig: EnvConfig;
constructor() {
try {
parseConfig();
} catch (e) {}
this.envConfig = this.validateInput(process.env);
}
/**
* Ensures all needed variables are set, and returns the validated JavaScript object
* including the applied default values.
*/
private validateInput(envConfig: any): EnvConfig {
const result = schema.safeParse(envConfig);
if (result.success === false) {
throw new Error(`Config validation error: ${result.error.toString()}`);
}
return result.data;
}
get nodeEnv(): string {
return this.envConfig.NODE_ENV;
}
get databaseUrl(): string {
return this.envConfig.DATABASE_URL;
}
get isLoggingDb(): boolean {
return this.envConfig.LOG_SQL_REQUEST;
}
get loggerLevel(): string {
return this.envConfig.LOG_LEVEL;
}
get port(): number {
return this.envConfig.PORT;
}
}

12
src/main.ts

@ -0,0 +1,12 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigModule } from './config/config.module';
import { ConfigService } from './config/config.service';
async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: true });
const connfigService = app.select(ConfigModule).get(ConfigService);
await app.listen(connfigService.port);
console.log(`Application is listening on port ${connfigService.port}.`);
}
bootstrap();

6
src/todos/dto/create-todo.dto.ts

@ -0,0 +1,6 @@
import { OmitType, PartialType } from '@nestjs/mapped-types';
import { Todo } from '../entities/todo.entity';
export class CreateTodoDto extends PartialType(
OmitType(Todo, ['id'] as const),
) {}

4
src/todos/dto/update-todo.dto.ts

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateTodoDto } from './create-todo.dto';
export class UpdateTodoDto extends PartialType(CreateTodoDto) {}

13
src/todos/entities/todo.entity.ts

@ -0,0 +1,13 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Todo {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column('text')
title: string;
@Column('boolean')
isDone: boolean;
}

49
src/todos/todos.controller.ts

@ -0,0 +1,49 @@
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
HttpCode,
} from '@nestjs/common';
import { TodosService } from './todos.service';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';
@Controller('todos')
export class TodosController {
constructor(private readonly todosService: TodosService) {}
@Post()
create(@Body() createTodoDto: CreateTodoDto) {
return this.todosService.create(createTodoDto);
}
@Get()
findAll() {
return this.todosService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.todosService.findOne(id);
}
@Post('delete-all')
@HttpCode(200)
deleteAll() {
return this.todosService.deleteAll();
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateTodoDto: UpdateTodoDto) {
return this.todosService.update(id, updateTodoDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.todosService.remove(id);
}
}

12
src/todos/todos.module.ts

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TodosService } from './todos.service';
import { TodosController } from './todos.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Todo } from './entities/todo.entity';
@Module({
imports: [TypeOrmModule.forFeature([Todo])],
controllers: [TodosController],
providers: [TodosService],
})
export class TodosModule {}

49
src/todos/todos.service.ts

@ -0,0 +1,49 @@
import { Injectable } from '@nestjs/common';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';
import { Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { Todo } from './entities/todo.entity';
@Injectable()
export class TodosService {
constructor(
@InjectRepository(Todo)
private usersRepository: Repository<Todo>,
) {}
create(createTodoDto: CreateTodoDto) {
const todo = new Todo();
todo.isDone = createTodoDto.isDone ?? false;
todo.title = createTodoDto.title;
return this.usersRepository.save(todo);
}
findAll() {
return this.usersRepository.find();
}
findOne(id: string) {
return this.usersRepository.findOne({ id });
}
async update(id: string, updateTodoDto: UpdateTodoDto) {
await this.usersRepository.update(
{
id,
},
{ title: updateTodoDto.title, isDone: updateTodoDto.isDone },
);
return this.findOne(id);
}
async remove(id: string) {
const previous = await this.findOne(id);
await this.usersRepository.delete({ id });
return previous;
}
deleteAll() {
return this.usersRepository.delete({});
}
}

72
test/app.e2e-spec.ts

@ -0,0 +1,72 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('App (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
beforeEach(async () => {
await request(app.getHttpServer()).post('/todos/delete-all').expect(200);
});
afterEach(async () => {
await request(app.getHttpServer()).post('/todos/delete-all').expect(200);
await app.close();
});
it('should allow for creation, edition, suppression and listing of todos', async () => {
const created = await request(app.getHttpServer())
.post('/todos')
.send({ title: 'My todo', isDone: false })
.expect(201);
expect(created.body.title).toEqual('My todo');
expect(created.body.isDone).toEqual(false);
const uuid = created.body.id;
const path = `/todos/${uuid}`;
await request(app.getHttpServer())
.get('/todos')
.expect(200)
.expect([created.body]);
await request(app.getHttpServer())
.get(path)
.expect(200)
.expect(created.body);
const newTodos = await request(app.getHttpServer())
.patch(path)
.send({ isDone: true, title: 'New title' })
.expect(200)
.expect({ id: uuid, isDone: true, title: 'New title' });
await request(app.getHttpServer())
.get(path)
.expect(200)
.expect(newTodos.body);
await request(app.getHttpServer())
.delete(path)
.expect(200)
.expect(newTodos.body);
});
it('/ hello world', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});

9
test/jest-e2e.json

@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

4
tsconfig.build.json

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

16
tsconfig.json

@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true
}
}

7363
yarn.lock
File diff suppressed because it is too large
View File

Loading…
Cancel
Save