mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-25 13:49:40 +00:00
feat: upload document to oci bucket
This commit is contained in:
@ -9,6 +9,14 @@ MIGRATIONS_RUN=true
|
||||
SWAGGER_API_DOCS_PATH="/api-docs"
|
||||
|
||||
|
||||
OCI_TENANCY_ID=
|
||||
OCI_USER_ID=
|
||||
OCI_FINGERPRINT=
|
||||
OCI_PRIVATE_KEY=
|
||||
OCI_PASSPHRASE=""
|
||||
OCI_NAMESPACE=
|
||||
OCI_REGION=
|
||||
|
||||
|
||||
MAIL_HOST=smtp.gmail.com
|
||||
MAIL_USER=aahalhmad@gmail.com
|
||||
|
1782
package-lock.json
generated
1782
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -49,8 +49,8 @@
|
||||
"nestjs-i18n": "^10.4.9",
|
||||
"nestjs-pino": "^4.1.0",
|
||||
"nodemailer": "^6.9.16",
|
||||
"oci-common": "^2.98.1",
|
||||
"oci-sdk": "^2.98.1",
|
||||
"oci-common": "^2.99.0",
|
||||
"oci-sdk": "^2.99.0",
|
||||
"pg": "^8.13.1",
|
||||
"pino-http": "^10.3.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { MiddlewareConsumer, Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { APP_FILTER, APP_PIPE } from '@nestjs/core';
|
||||
import { ClientProxyFactory, Transport } from '@nestjs/microservices';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { I18nMiddleware, I18nModule } from 'nestjs-i18n';
|
||||
import { LoggerModule } from 'nestjs-pino';
|
||||
@ -10,6 +9,7 @@ import { buildConfigOptions, buildLoggerOptions, buildTypeormOptions } from './c
|
||||
import { buildI18nOptions } from './core/module-options/i18n-options';
|
||||
import { buildValidationPipe } from './core/pipes';
|
||||
import { migrations } from './db';
|
||||
import { DocumentModule } from './document/document.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
@Module({
|
||||
controllers: [],
|
||||
@ -26,6 +26,9 @@ import { HealthModule } from './health/health.module';
|
||||
}),
|
||||
I18nModule.forRoot(buildI18nOptions()),
|
||||
HealthModule,
|
||||
|
||||
// Application Modules
|
||||
DocumentModule,
|
||||
],
|
||||
providers: [
|
||||
// Global Pipes
|
||||
|
@ -1,20 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateTemplateTable1731310840593 implements MigrationInterface {
|
||||
name = 'CreateTemplateTable1731310840593';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "template" (
|
||||
"id" SERIAL,
|
||||
"name" character varying(255) NOT NULL,
|
||||
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "PK_ac51aa5181ee2036f5ca482857d" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE "template"`);
|
||||
}
|
||||
}
|
23
src/db/migrations/1732434281561-create-document-entity.ts
Normal file
23
src/db/migrations/1732434281561-create-document-entity.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateDocumentEntity1732434281561 implements MigrationInterface {
|
||||
name = 'CreateDocumentEntity1732434281561';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "documents" (
|
||||
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||
"name" character varying(255) NOT NULL,
|
||||
"extension" character varying(255) NOT NULL,
|
||||
"documentType" character varying(255) NOT NULL,
|
||||
"accessType" character varying(255) NOT NULL,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
|
||||
CONSTRAINT "PK_ac51aa5181ee2036f5ca482857c" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE "documents"`);
|
||||
}
|
||||
}
|
@ -1 +1 @@
|
||||
export * from './1731310840593-create-template-table';
|
||||
export * from './1732434281561-create-document-entity';
|
||||
|
6
src/document/constants/buckets.constant.ts
Normal file
6
src/document/constants/buckets.constant.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { DocumentType } from '../enums';
|
||||
|
||||
export const BUCKETS: Record<DocumentType, string> = {
|
||||
[DocumentType.PROFILE_PICTURE]: 'profile-pictures',
|
||||
[DocumentType.PASSPORT]: 'passports',
|
||||
};
|
1
src/document/constants/index.ts
Normal file
1
src/document/constants/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './buckets.constant';
|
53
src/document/controllers/document.controller.ts
Normal file
53
src/document/controllers/document.controller.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { Body, Controller, Get, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||
import { memoryStorage } from 'multer';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { UploadDocumentRequestDto } from '../dtos/request';
|
||||
import { DocumentMetaResponseDto } from '../dtos/response';
|
||||
import { AccessType, DocumentType } from '../enums';
|
||||
import { DocumentService } from '../services';
|
||||
@Controller('document')
|
||||
@ApiTags('document')
|
||||
export class DocumentController {
|
||||
constructor(private readonly documentService: DocumentService) {}
|
||||
|
||||
@Post()
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiBody({
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
document: {
|
||||
type: 'string',
|
||||
format: 'binary',
|
||||
},
|
||||
documentType: {
|
||||
type: 'string',
|
||||
enum: Object.values(DocumentType),
|
||||
},
|
||||
accessType: {
|
||||
type: 'string',
|
||||
enum: Object.values(AccessType),
|
||||
},
|
||||
},
|
||||
required: ['document', 'documentType', 'accessType'],
|
||||
},
|
||||
})
|
||||
@UseInterceptors(FileInterceptor('document', { storage: memoryStorage() }))
|
||||
async createDocument(
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
@Body() uploadedDocumentRequest: UploadDocumentRequestDto,
|
||||
) {
|
||||
const document = await this.documentService.createDocument(file, uploadedDocumentRequest);
|
||||
|
||||
return ResponseFactory.data(new DocumentMetaResponseDto(document));
|
||||
}
|
||||
|
||||
@Get(':documentId')
|
||||
async findDocumentById(@Param('documentId') documentId: string) {
|
||||
const document = await this.documentService.findDocumentById(documentId);
|
||||
console.log(document);
|
||||
return ResponseFactory.data(new DocumentMetaResponseDto(document));
|
||||
}
|
||||
}
|
1
src/document/controllers/index.ts
Normal file
1
src/document/controllers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './document.controller';
|
13
src/document/document.module.ts
Normal file
13
src/document/document.module.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { DocumentController } from './controllers';
|
||||
import { Document } from './entities';
|
||||
import { DocumentRepository } from './repositories';
|
||||
import { DocumentService, OciService } from './services';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Document])],
|
||||
controllers: [DocumentController],
|
||||
providers: [DocumentService, OciService, DocumentRepository],
|
||||
})
|
||||
export class DocumentModule {}
|
1
src/document/dtos/request/index.ts
Normal file
1
src/document/dtos/request/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './upload-document.request.dto';
|
11
src/document/dtos/request/upload-document.request.dto.ts
Normal file
11
src/document/dtos/request/upload-document.request.dto.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { IsEnum } from 'class-validator';
|
||||
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
|
||||
import { AccessType, DocumentType } from '~/document/enums';
|
||||
|
||||
export class UploadDocumentRequestDto {
|
||||
@IsEnum(DocumentType, { message: i18n('validation.IsEnum', { path: 'general', property: 'document.documentType' }) })
|
||||
documentType!: DocumentType;
|
||||
|
||||
@IsEnum(AccessType, { message: i18n('validation.IsEnum', { path: 'general', property: 'document.accessType' }) })
|
||||
accessType!: AccessType;
|
||||
}
|
36
src/document/dtos/response/document-meta.response.dto.ts
Normal file
36
src/document/dtos/response/document-meta.response.dto.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Document } from '~/document/entities';
|
||||
import { AccessType, DocumentType } from '~/document/enums';
|
||||
|
||||
export class DocumentMetaResponseDto {
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@ApiProperty()
|
||||
extension!: string;
|
||||
|
||||
@ApiProperty()
|
||||
documentType!: DocumentType;
|
||||
|
||||
@ApiProperty()
|
||||
acessType!: AccessType;
|
||||
|
||||
@ApiProperty()
|
||||
createdAt!: Date;
|
||||
|
||||
@ApiProperty()
|
||||
updatedAt!: Date;
|
||||
|
||||
constructor(document: Document) {
|
||||
this.id = document.id;
|
||||
this.name = document.name;
|
||||
this.extension = document.extension;
|
||||
this.documentType = document.documentType;
|
||||
this.acessType = document.accessType;
|
||||
this.createdAt = document.createdAt;
|
||||
this.updatedAt = document.updatedAt;
|
||||
}
|
||||
}
|
2
src/document/dtos/response/index.ts
Normal file
2
src/document/dtos/response/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './document-meta.response.dto';
|
||||
export * from './upload.response.dto';
|
19
src/document/dtos/response/upload.response.dto.ts
Normal file
19
src/document/dtos/response/upload.response.dto.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { AccessType, DocumentType } from '~/document/enums';
|
||||
|
||||
export class UploadResponseDto {
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@ApiProperty()
|
||||
documentType!: DocumentType;
|
||||
|
||||
@ApiProperty()
|
||||
accessType!: AccessType;
|
||||
|
||||
@ApiProperty()
|
||||
extension!: string;
|
||||
|
||||
@ApiProperty()
|
||||
url!: string;
|
||||
}
|
26
src/document/entities/document.entity.ts
Normal file
26
src/document/entities/document.entity.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||
import { AccessType, DocumentType } from '../enums';
|
||||
|
||||
@Entity('documents')
|
||||
export class Document {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
name!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
extension!: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
documentType!: DocumentType;
|
||||
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
accessType!: AccessType;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
|
||||
createdAt!: Date;
|
||||
}
|
1
src/document/entities/index.ts
Normal file
1
src/document/entities/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './document.entity';
|
4
src/document/enums/access-type.enum.ts
Normal file
4
src/document/enums/access-type.enum.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum AccessType {
|
||||
PUBLIC = 'PUBLIC',
|
||||
PRIVATE = 'PRIVATE',
|
||||
}
|
4
src/document/enums/document-type.enum.ts
Normal file
4
src/document/enums/document-type.enum.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum DocumentType {
|
||||
PROFILE_PICTURE = 'PROFILE_PICTURE',
|
||||
PASSPORT = 'PASSPORT',
|
||||
}
|
2
src/document/enums/index.ts
Normal file
2
src/document/enums/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './access-type.enum';
|
||||
export * from './document-type.enum';
|
25
src/document/repositories/document.repository.ts
Normal file
25
src/document/repositories/document.repository.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { UploadResponseDto } from '../dtos/response';
|
||||
import { Document } from '../entities';
|
||||
|
||||
@Injectable()
|
||||
export class DocumentRepository {
|
||||
constructor(@InjectRepository(Document) private documentRepository: Repository<Document>) {}
|
||||
|
||||
createDocument(document: UploadResponseDto) {
|
||||
return this.documentRepository.save(
|
||||
this.documentRepository.create({
|
||||
name: document.name,
|
||||
documentType: document.documentType,
|
||||
extension: document.extension,
|
||||
accessType: document.accessType,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
findDocumentById(documentId: string) {
|
||||
return this.documentRepository.findOne({ where: { id: documentId } });
|
||||
}
|
||||
}
|
1
src/document/repositories/index.ts
Normal file
1
src/document/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './document.repository';
|
24
src/document/services/document.service.ts
Normal file
24
src/document/services/document.service.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { UploadDocumentRequestDto } from '../dtos/request';
|
||||
import { DocumentRepository } from '../repositories';
|
||||
import { OciService } from './oci.service';
|
||||
|
||||
@Injectable()
|
||||
export class DocumentService {
|
||||
constructor(private readonly ociService: OciService, private readonly documentRepository: DocumentRepository) {}
|
||||
async createDocument(file: Express.Multer.File, uploadedDocumentRequest: UploadDocumentRequestDto) {
|
||||
const uploadedFile = await this.ociService.uploadFile(file, uploadedDocumentRequest);
|
||||
console.log('test');
|
||||
return this.documentRepository.createDocument(uploadedFile);
|
||||
}
|
||||
|
||||
async findDocumentById(documentId: string) {
|
||||
const document = await this.documentRepository.findDocumentById(documentId);
|
||||
|
||||
if (!document) {
|
||||
throw new NotFoundException('DOCUMENT.NOT_FOUND');
|
||||
}
|
||||
|
||||
return document;
|
||||
}
|
||||
}
|
2
src/document/services/index.ts
Normal file
2
src/document/services/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './document.service';
|
||||
export * from './oci.service';
|
68
src/document/services/oci.service.ts
Normal file
68
src/document/services/oci.service.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { Region, SimpleAuthenticationDetailsProvider } from 'oci-common';
|
||||
import { ObjectStorageClient } from 'oci-objectstorage';
|
||||
import path from 'path';
|
||||
import { BUCKETS } from '../constants';
|
||||
import { UploadDocumentRequestDto } from '../dtos/request';
|
||||
import { UploadResponseDto } from '../dtos/response';
|
||||
import { generateNewFileName } from '../utils';
|
||||
const ONE_DAY = Date.now() + 24 * 60 * 60 * 1000;
|
||||
|
||||
@Injectable()
|
||||
export class OciService {
|
||||
private readonly ociClient: ObjectStorageClient;
|
||||
private readonly tenancyId: string = this.configService.getOrThrow('OCI_TENANCY_ID');
|
||||
private readonly userId: string = this.configService.getOrThrow('OCI_USER_ID');
|
||||
private readonly fingerprint: string = this.configService.getOrThrow('OCI_FINGERPRINT');
|
||||
private readonly privateKey: string = this.configService.getOrThrow('OCI_PRIVATE_KEY');
|
||||
private readonly passPhrase: string = this.configService.getOrThrow('OCI_PASSPHRASE');
|
||||
private readonly namespace: string = this.configService.getOrThrow('OCI_NAMESPACE');
|
||||
private readonly region: string = this.configService.getOrThrow('OCI_REGION');
|
||||
private readonly logger = new Logger(OciService.name);
|
||||
constructor(private configService: ConfigService) {
|
||||
this.ociClient = new ObjectStorageClient({
|
||||
authenticationDetailsProvider: new SimpleAuthenticationDetailsProvider(
|
||||
this.tenancyId,
|
||||
this.userId,
|
||||
this.fingerprint,
|
||||
Buffer.from(this.privateKey, 'base64').toString('utf-8'),
|
||||
this.passPhrase,
|
||||
Region.fromRegionId(this.region),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
async uploadFile(
|
||||
file: Express.Multer.File,
|
||||
{ documentType, accessType }: UploadDocumentRequestDto,
|
||||
): Promise<UploadResponseDto> {
|
||||
const bucketName = BUCKETS[documentType];
|
||||
const objectName = generateNewFileName(file.originalname);
|
||||
|
||||
if (!bucketName) {
|
||||
this.logger.error('Could not find bucket name for document type', documentType);
|
||||
throw new BadRequestException('DOCUMENT.TYPE_NOT_SUPPORTED');
|
||||
}
|
||||
|
||||
this.logger.debug(`Uploading file to bucket ${bucketName} with object name ${objectName}`);
|
||||
await this.ociClient.putObject({
|
||||
namespaceName: this.namespace,
|
||||
bucketName,
|
||||
putObjectBody: file.buffer,
|
||||
contentLength: file.buffer.length,
|
||||
objectName,
|
||||
});
|
||||
|
||||
this.logger.log(`File uploaded successfully to bucket ${bucketName} with object name ${objectName}`);
|
||||
|
||||
return plainToInstance(UploadResponseDto, {
|
||||
name: objectName,
|
||||
extension: path.extname(file.originalname),
|
||||
url: `https://objectstorage.${this.region}.oraclecloud.com/n/${this.namespace}/b/${bucketName}/o/${objectName}`,
|
||||
documentType,
|
||||
accessType,
|
||||
});
|
||||
}
|
||||
}
|
6
src/document/utils/file-names-generator.util.ts
Normal file
6
src/document/utils/file-names-generator.util.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import path from 'path';
|
||||
export function generateNewFileName(origianlName: string): string {
|
||||
const ext = path.extname(origianlName);
|
||||
const nameWithoutExtension = path.basename(origianlName, ext);
|
||||
return nameWithoutExtension + '-' + Date.now() + ext;
|
||||
}
|
1
src/document/utils/index.ts
Normal file
1
src/document/utils/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './file-names-generator.util';
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"CUSTOM": {
|
||||
"TEMPLATE_ERROR": "خطأ في القالب"
|
||||
"DOCUMENTS": {
|
||||
"TYPE_NOT_SUPPORTED": "نوع الملف غير مدعوم"
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,9 @@
|
||||
"PROPERTY_MAPPINGS": {
|
||||
"paginationPage": "رقم الصفحة",
|
||||
"paginationSize": "حجم الصفحة",
|
||||
"test": {
|
||||
"name": "الاسم"
|
||||
"document": {
|
||||
"documentType": "نوع الملف",
|
||||
"accessType": "نوع الوصول"
|
||||
}
|
||||
},
|
||||
"UNAUTHORIZED_ERROR": "يجب تسجيل الدخول مره اخرى",
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"CUSTOM": {
|
||||
"TEMPLATE_ERROR": "Template error"
|
||||
"DOCUMENTS": {
|
||||
"TYPE_NOT_SUPPORTED": "Document type is not supported"
|
||||
}
|
||||
}
|
||||
|
@ -2,9 +2,9 @@
|
||||
"PROPERTY_MAPPINGS": {
|
||||
"paginationPage": "page",
|
||||
"paginationSize": "page size",
|
||||
"test": {
|
||||
"name": "name",
|
||||
"age": "age"
|
||||
"document": {
|
||||
"documentType": "Document type",
|
||||
"accessType": "Access type"
|
||||
}
|
||||
},
|
||||
"UNAUTHORIZED_ERROR": "You have to login again",
|
||||
|
Reference in New Issue
Block a user