Skip to content

Commit

Permalink
백엔드 프로젝트 셋업
Browse files Browse the repository at this point in the history
  • Loading branch information
Heavyrisem committed Jan 17, 2024
1 parent 6eee73b commit 4307432
Show file tree
Hide file tree
Showing 15 changed files with 658 additions and 80 deletions.
3 changes: 3 additions & 0 deletions apps/server/nest-cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
},
"generateOptions": {
"spec": false
}
}
8 changes: 7 additions & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,17 @@
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.2.0",
"@nestjs/typeorm": "^10.0.1",
"@sound-git/types": "workspace:*",
"dotenv": "^16.3.1",
"joi": "^17.11.0",
"mysql2": "^3.7.0",
"oci-common": "^2.75.0",
"oci-objectstorage": "^2.75.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"typeorm": "^0.3.19"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
Expand Down
9 changes: 7 additions & 2 deletions apps/server/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { Module } from '@nestjs/common';
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigurationModule } from './libs/config/config.module';
import { HttpLoggerMiddleware } from './libs/logging/http-logger.middleware';

@Module({
imports: [ConfigurationModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(HttpLoggerMiddleware).forRoutes('*');
}
}
12 changes: 12 additions & 0 deletions apps/server/src/libs/database/core.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CreateDateColumn, DeleteDateColumn, PrimaryGeneratedColumn } from 'typeorm';

export class CoreEntity {
@PrimaryGeneratedColumn()
id: number;

@CreateDateColumn()
createdAt: Date;

@DeleteDateColumn()
deletedAt: Date | null;
}
24 changes: 24 additions & 0 deletions apps/server/src/libs/database/database.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Logger, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { DataSource } from 'typeorm';

import { ConfigurationModule } from '../config/config.module';
import { options } from './typeorm.datasource';

@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigurationModule],
useFactory: () => options,
dataSourceFactory: async (opt) => {
const logger = new Logger('DataBaseModule');
logger.log('♺ Connecting to DataBase');
const dataSource = await new DataSource(opt).initialize();
logger.log('✔ DataBase connect Success');
return dataSource;
},
}),
],
})
export class DatabaseModule {}
25 changes: 25 additions & 0 deletions apps/server/src/libs/database/typeorm.datasource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { config } from 'dotenv';
import { DataSource, DataSourceOptions } from 'typeorm';

config({
path: `.env.${process.env.NODE_ENV}`,
});

export const options: DataSourceOptions = {
type: 'mysql',
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
synchronize: true,
logging: process.env.NODE_ENV !== 'production',
entities: ['dist/**/*.entity.{js,ts}'],
migrations: ['dist/migrations/*{.ts,.js}'],
migrationsRun: false,
// authSource: 'admin',
};

const AppDataSource = new DataSource(options);

export default AppDataSource;
49 changes: 49 additions & 0 deletions apps/server/src/libs/logging/http-logger.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';

import { Request, Response } from 'express';
import { Observable, catchError, finalize, throwError } from 'rxjs';

@Injectable()
export class HttpLoggerInterceptor implements NestInterceptor {
logger = new Logger('HttpLogger');

intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> | Promise<Observable<any>> {
const ctx = context.switchToHttp();
const request = ctx.getRequest<Request>();

const path = request.path;
const method = request.method;

// Nest의 Request Lifecyle 에 따라 Middleware 레벨에서 Exception이 발생하는 경우 Interceptor 까지 요청이 도달하지 않음
// Middleware에서 Exception이 발생하더라도 요청 로그를 남기기 위해 logger.middleware.ts 에서 요청 로그는 따로 남김
// this.logger.log(`${method} ${path} <==`);

let isError = false;
const nextObserver = next.handle().pipe(
finalize(() => {
const response = ctx.getResponse<Response>();
!isError && this.logger.log(`${method} ${path} ==> ${response?.statusCode}`);
}),
catchError((err) => {
isError = true;
let message: string = err.message;
if (err?.response?.message) {
const errorResponseMessage = err?.response?.message;
message = Array.isArray(errorResponseMessage as string[])
? errorResponseMessage.join(',')
: message;
// err = new BadRequestException(message);
}
this.logger.error(
`${method} ${path} ==> ${err?.statusCode || err?.status || err?.code} ${message}`,
);
return throwError(() => err);
}),
);

return nextObserver;
}
}
20 changes: 20 additions & 0 deletions apps/server/src/libs/logging/http-logger.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Injectable, Logger, NestMiddleware } from '@nestjs/common';

import { NextFunction, Request, Response } from 'express';

@Injectable()
export class HttpLoggerMiddleware implements NestMiddleware {
logger = new Logger('HttpLogger');

use(req: Request, res: Response, next: NextFunction) {
const path = req.path;
const method = req.method;

// const _headers = JSON.stringify(req.headers ? req.headers : {});
const _query = JSON.stringify(req.query ? req.query : {});
const _body = JSON.stringify(req.body ? req.body : {});

this.logger.log(`${method} ${path} <== ${_body} ${_query}`);
next();
}
}
18 changes: 16 additions & 2 deletions apps/server/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

import { AppModule } from './app.module';
import { HttpLoggerInterceptor } from './libs/logging/http-logger.interceptor';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = app.get(ConfigService);
await app.listen(config.getOrThrow('PORT'));

const config = new DocumentBuilder()
.setTitle('Sound Git API')
.setDescription('The sound git server API docs')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document);

app.useGlobalInterceptors(new HttpLoggerInterceptor());

const configService = app.get(ConfigService);

await app.listen(configService.getOrThrow('PORT'));
}

bootstrap();
16 changes: 16 additions & 0 deletions apps/server/src/modules/object-storage/entity/objectMeta.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Model } from '@sound-git/types';
import { CoreEntity } from 'src/libs/database/core.entity';
import { Column, Entity, Index } from 'typeorm';

@Entity()
export class ObjectMeta extends CoreEntity implements Model.ObjectMetaInfo {
@Index({ unique: true })
@Column({ nullable: false })
name: string;

@Column({ nullable: false })
type: string;

@Column({ nullable: false })
isPublic: boolean;
}
12 changes: 12 additions & 0 deletions apps/server/src/modules/object-storage/object-storage.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { ObjectMeta } from './entity/objectMeta.entity';
import { ObjectStorageService } from './object-storage.service';

@Module({
imports: [TypeOrmModule.forFeature([ObjectMeta])],
exports: [ObjectStorageService],
providers: [ObjectStorageService],
})
export class ObjectStorageModule {}
115 changes: 115 additions & 0 deletions apps/server/src/modules/object-storage/object-storage.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { Injectable, Logger, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';

import { Region, SimpleAuthenticationDetailsProvider } from 'oci-common';
import { ObjectStorageClient } from 'oci-objectstorage';
import { CreatePreauthenticatedRequestDetails } from 'oci-objectstorage/lib/model';
import { PutObjectRequest } from 'oci-objectstorage/lib/request';
import { Readable } from 'stream';
import { Repository } from 'typeorm';

import { ObjectMeta } from './entity/objectMeta.entity';

@Injectable()
export class ObjectStorageService {
private logger = new Logger(ObjectStorageService.name);
private oracleAuthProvider: SimpleAuthenticationDetailsProvider;
private oracleObjectStorageClient: ObjectStorageClient;

constructor(
@InjectRepository(ObjectMeta)
private readonly objectMetaRepository: Repository<ObjectMeta>,
private readonly configService: ConfigService,
) {
this.oracleAuthProvider = new SimpleAuthenticationDetailsProvider(
configService.getOrThrow('OCI_TENANCY'),
configService.getOrThrow('OCI_USER'),
configService.getOrThrow('OCI_FINGERPRINT'),
configService.getOrThrow('OCI_PRIVATEKEY'),
null,
Region.fromRegionId(configService.getOrThrow('OCI_REGION')),
);

this.oracleObjectStorageClient = new ObjectStorageClient({
authenticationDetailsProvider: this.oracleAuthProvider,
});
}

async findObjectMetaById(id: number) {
return this.objectMetaRepository.findOneBy({ id });
}

async findObjectMetaByName(name: string) {
return this.objectMetaRepository.findOneBy({ name });
}

async save(dataStream: Readable, name: string, type: string, isPublic = false) {
const bucketName = this.configService.getOrThrow(
isPublic ? 'OCI_STORAGE_PUBLIC_BUCKET' : 'OCI_STORAGE_PRIVATE_BUCKET',
);

const putObjectRequest: PutObjectRequest = {
namespaceName: this.configService.getOrThrow('OCI_STORAGE_NAMESPACE'),
bucketName,
objectName: name,
contentType: type,
putObjectBody: dataStream,
};
const putObjectResponse = await this.oracleObjectStorageClient.putObject(putObjectRequest);
this.logger.debug('put finish', putObjectResponse);

const existObject = await this.findObjectMetaByName(name);
if (existObject) {
const mergedObjcet = this.objectMetaRepository.merge(existObject, {
name,
type,
isPublic,
});
return this.objectMetaRepository.save(mergedObjcet);
}

const object = this.objectMetaRepository.create({ name, type, isPublic });
return this.objectMetaRepository.save(object);
}

async getObjectUrl(name: string, expiresInSec = 60) {
const objectMeta = await this.findObjectMetaByName(name);
if (!objectMeta) throw new NotFoundException('object를 찾을 수 없습니다.');

const bucketName = this.configService.getOrThrow(
objectMeta.isPublic ? 'OCI_STORAGE_PUBLIC_BUCKET' : 'OCI_STORAGE_PRIVATE_BUCKET',
);
const preAuthedUrl = await this.createPreAuthedUrl(name, bucketName, expiresInSec);

return preAuthedUrl;
}

private async createPreAuthedUrl(name: string, bucketName: string, expiresInSec: number) {
// const objectMeta = await this.objectMetaRepository.findOneBy({ name });

const expires = new Date(Date.now() + expiresInSec * 1000);
const uniqueRequestId = Date.now();
const createPreauthedRequest =
await this.oracleObjectStorageClient.createPreauthenticatedRequest({
createPreauthenticatedRequestDetails: {
name: uniqueRequestId.toString(),
objectName: name,
accessType: CreatePreauthenticatedRequestDetails.AccessType.ObjectRead,
timeExpires: expires,
},
bucketName,
namespaceName: this.configService.getOrThrow('OCI_STORAGE_NAMESPACE'),
});

const baseUrl =
'https://' +
[
this.configService.getOrThrow('OCI_STORAGE_NAMESPACE'),
'objectstorage',
this.configService.getOrThrow('OCI_REGION'),
'oci.customer-oci.com',
].join('.');
return baseUrl + createPreauthedRequest.preauthenticatedRequest.accessUri;
}
}
1 change: 1 addition & 0 deletions packages/types/src/models/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./user";
export * from "./object";
6 changes: 6 additions & 0 deletions packages/types/src/models/object.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface ObjectMetaInfo {
id: number;
name: string;
type: string;
isPublic: boolean;
}
Loading

0 comments on commit 4307432

Please sign in to comment.