Skip to content

Commit

Permalink
feat: add object store integration (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
mishraomp authored Feb 23, 2024
1 parent 334956d commit bc50ffc
Show file tree
Hide file tree
Showing 16 changed files with 2,046 additions and 688 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/.deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ jobs:
--set global.autoscaling=${{ inputs.autoscaling }} \
--set-string global.repository=${{ github.repository }} \
--set-string global.tag="${{ env.package_tag }}" \
--set-string global.secrets.osBucket="${{ secrets.osBucket }}" \
--set-string global.secrets.osAccessKeyId="${{ secrets.osAccessKeyId }}" \
--set-string global.secrets.osSecretAccessKeyId="${{ secrets.osSecretAccessKeyId }}" \
--set-string global.secrets.osEndpoint="${{ secrets.osEndpoint }}" \
${{ inputs.params }} \
--install --wait --atomic ${{ env.repo_release }} \
--timeout ${{ inputs.timeout-minutes }}m \
Expand Down
13 changes: 10 additions & 3 deletions .github/workflows/merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,14 @@ jobs:
uses: ./.github/workflows/.deploy.yml
secrets: inherit
with:
autoscaling: false
environment: test
tag: ${{ needs.vars.outputs.pr }}
release: test

params: |
--set backend.deploymentStrategy=RollingUpdate \
--set frontend.deploymentStrategy=RollingUpdate \
--set backend.pdb.enabled=true \
--set frontend.pdb.enabled=true
deploy-prod:
Expand All @@ -51,7 +54,11 @@ jobs:
environment: prod
tag: ${{ needs.vars.outputs.pr }}
release: prod
params: --set backend.deploymentStrategy=RollingUpdate --set frontend.deploymentStrategy=RollingUpdate
params: |
--set backend.deploymentStrategy=RollingUpdate \
--set frontend.deploymentStrategy=RollingUpdate \
--set backend.pdb.enabled=true \
--set frontend.pdb.enabled=true
promote:
name: Promote Images
Expand Down
1,902 changes: 1,727 additions & 175 deletions backend/package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,25 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.515.0",
"@nestjs/cli": "^10.1.16",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/schedule": "^4.0.1",
"@nestjs/schematics": "^10.0.0",
"@nestjs/swagger": "^7.0.3",
"@nestjs/terminus": "^10.2.1",
"@nestjs/testing": "^10.0.0",
"@nestjs/throttler": "^5.1.2",
"@prisma/client": "^5.7.0",
"dotenv": "^16.0.1",
"express-prom-bundle": "^7.0.0",
"helmet": "^7.0.0",
"nest-winston": "^1.9.4",
"nestjs-prisma": "^0.23.0",
"papaparse": "^5.4.1",
"pg": "^8.11.3",
"prom-client": "^15.1.0",
"reflect-metadata": "^0.2.0",
Expand All @@ -49,6 +53,7 @@
"@types/express": "^4.17.15",
"@types/jest": "^29.0.0",
"@types/node": "^20.0.0",
"@types/papaparse": "^5.3.14",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.5.0",
"@typescript-eslint/parser": "^6.5.0",
Expand Down
15 changes: 8 additions & 7 deletions backend/src/app.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Test, TestingModule } from "@nestjs/testing";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { ObjectStoreService } from "./v1/object-store/object.store.service";

describe('AppController', () => {
describe("AppController", () => {
let appController: AppController;

beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
providers: [AppService, ObjectStoreService],
}).compile();

appController = app.get<AppController>(AppController);
});

describe('root', () => {
describe("root", () => {
it('should return "Hello Backend!"', () => {
expect(appController.getHello()).toBe('Hello Backend!');
expect(appController.getHello()).toBe("Hello Backend!");
});
});
});
9 changes: 8 additions & 1 deletion backend/src/app.controller.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { ObjectStoreService } from './v1/object-store/object.store.service'
import { OmrrData } from './v1/types/omrr-data'

@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
constructor(private readonly appService: AppService,
private readonly objectStoreService: ObjectStoreService) {}

@Get()
getHello(): string {
return this.appService.getHello();
}
@Get('/omrr')
async getAllOmrrRecords(): Promise<OmrrData[]> {
return this.objectStoreService.getLatestOMRRFileContents();
}
}
69 changes: 42 additions & 27 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,37 @@
import "dotenv/config";
import { Logger, MiddlewareConsumer, Module, RequestMethod } from "@nestjs/common";
import { HTTPLoggerMiddleware } from "./middleware/req.res.logger";
import { loggingMiddleware, PrismaModule } from "nestjs-prisma";
import { ConfigModule } from "@nestjs/config";
import { AppService } from "./app.service";
import { AppController } from "./app.controller";
import { MetricsController } from "./metrics.controller";
import { TerminusModule } from '@nestjs/terminus';
import { HealthController } from "./health.controller";
import 'dotenv/config'
import { Logger, MiddlewareConsumer, Module, RequestMethod } from '@nestjs/common'
import { HTTPLoggerMiddleware } from './middleware/req.res.logger'
import { loggingMiddleware, PrismaModule } from 'nestjs-prisma'
import { ConfigModule } from '@nestjs/config'
import { AppService } from './app.service'
import { AppController } from './app.controller'
import { MetricsController } from './metrics.controller'
import { TerminusModule } from '@nestjs/terminus'
import { HealthController } from './health.controller'
import { ThrottlerModule } from '@nestjs/throttler'
import { ObjectStoreModule } from './v1/object-store/object.store.module'
import { ObjectStoreService } from './v1/object-store/object.store.service'
import { TasksModule } from './v1/tasks/task.module'
import { ScheduleModule } from '@nestjs/schedule'

const DB_HOST = process.env.POSTGRES_HOST || "localhost";
const DB_USER = process.env.POSTGRES_USER || "postgres";
const DB_PWD = encodeURIComponent(process.env.POSTGRES_PASSWORD || "default"); // this needs to be encoded, if the password contains special characters it will break connection string.
const DB_PORT = process.env.POSTGRES_PORT || 5432;
const DB_NAME = process.env.POSTGRES_DATABASE || "postgres";
const DB_SCHEMA = process.env.DB_SCHEMA || "users";
const DB_HOST = process.env.POSTGRES_HOST || 'localhost'
const DB_USER = process.env.POSTGRES_USER || 'postgres'
const DB_PWD = encodeURIComponent(process.env.POSTGRES_PASSWORD || 'default') // this needs to be encoded, if the password contains special characters it will break connection string.
const DB_PORT = process.env.POSTGRES_PORT || 5432
const DB_NAME = process.env.POSTGRES_DATABASE || 'postgres'
const DB_SCHEMA = process.env.DB_SCHEMA || 'users'

function getMiddlewares() {
if (process.env.PRISMA_LOGGING) {
return [
// configure your prisma middleware
loggingMiddleware({
logger: new Logger("PrismaMiddleware"),
logLevel: "debug"
})
];
logger: new Logger('PrismaMiddleware'),
logLevel: 'debug',
}),
]
}
return [];
return []
}

@Module({
Expand All @@ -35,21 +40,31 @@ function getMiddlewares() {
TerminusModule,
PrismaModule.forRoot({
isGlobal: true,
prismaServiceOptions:{
prismaOptions:{
log: ["error", "warn"],
errorFormat: "pretty",
prismaServiceOptions: {
prismaOptions: {
log: ['error', 'warn'],
errorFormat: 'pretty',
datasourceUrl: `postgresql://${DB_USER}:${DB_PWD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?schema=${DB_SCHEMA}&connection_limit=5`,
},
middlewares: getMiddlewares(),
},
}),
ThrottlerModule.forRoot([{
ttl: 60000,
limit: 1000,
}]),
ScheduleModule.forRoot(),
TasksModule,
ObjectStoreModule,
],
controllers: [AppController, MetricsController, HealthController],
providers: [AppService]
providers: [AppService, ObjectStoreService],
})
export class AppModule { // let's add a middleware on all routes
configure(consumer: MiddlewareConsumer) {
consumer.apply(HTTPLoggerMiddleware).exclude({ path: 'metrics', method: RequestMethod.ALL }, { path: 'health', method: RequestMethod.ALL }).forRoutes('*');
consumer.apply(HTTPLoggerMiddleware).exclude({ path: 'metrics', method: RequestMethod.ALL }, {
path: 'health',
method: RequestMethod.ALL,
}).forRoutes('*')
}
}
10 changes: 10 additions & 0 deletions backend/src/v1/object-store/object.store.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common'
import { ObjectStoreService } from './object.store.service'

@Module({
providers: [ObjectStoreService],
exports: [ObjectStoreService],
})
export class ObjectStoreModule{

}
104 changes: 104 additions & 0 deletions backend/src/v1/object-store/object.store.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'
import { GetObjectCommand, ListObjectsCommand, S3Client } from '@aws-sdk/client-s3'
import * as process from 'process'
import { logger } from '../../logger'
import { Readable } from 'stream'
import Papa from 'papaparse'
import { OmrrData } from '../types/omrr-data'

@Injectable()
export class ObjectStoreService implements OnModuleDestroy, OnModuleInit {
private readonly _s3Client: S3Client
private _omrrData: OmrrData[] = []

constructor() {
this._s3Client = new S3Client({
credentials: {
accessKeyId: process.env.OS_ACCESS_KEY_ID,
secretAccessKey: process.env.OS_SECRET_ACCESS_KEY_ID,
},
endpoint: process.env.OS_ENDPOINT,
forcePathStyle: true,
region: 'ca-central-1',
})
}

async onModuleInit() {
try {
logger.info('Initializing ObjectStoreService');
await this.getLatestOmrrDataFromObjectStore()
} catch (e) {
logger.error(e)
process.exit(1)
}
}

onModuleDestroy() {
this._s3Client.destroy()
}

async parseCSVToObject(csv: string): Promise<any[]> {
return new Promise((resolve, reject) => {
Papa.parse(csv, {
header: true,
complete: (results: any) => {
resolve(results.data)
},
error: (error: any) => {
reject(error)
},
})
})
}

async convertCSVToJson(stream: Readable): Promise<any[]> {
return new Promise((resolve, reject) => {
let csvData = ''
stream.on('data', (chunk) => {
csvData += chunk
})
stream.on('end', async () => {
try {
const jsonData = await this.parseCSVToObject(csvData)
resolve(jsonData)
} catch (error) {
reject(error)
}
})
stream.on('error', (error) => {
reject(error)
})
})
}

async getLatestOMRRFileContents(): Promise<OmrrData[]> {
return this._omrrData
}

async getLatestOmrrDataFromObjectStore(): Promise<OmrrData[]> {
try {
const response = await this._s3Client.send(
new ListObjectsCommand({ Bucket: process.env.OS_BUCKET }),
)
let sortedData: any = response.Contents.sort((a: any, b: any) => {
const modifiedDateA: any = new Date(a.LastModified)
const modifiedDateB: any = new Date(b.LastModified)
return modifiedDateB - modifiedDateA
})
const fileName = sortedData[0]?.Key
logger.info(`fileName is ${fileName}`)

const result = await this._s3Client.send(
new GetObjectCommand({ Bucket: process.env.OS_BUCKET, Key: fileName }),
)
const fileStream: any = result?.Body
if (fileStream) {
this._omrrData = await this.convertCSVToJson(fileStream)
return this._omrrData
}
} catch (e) {
}

throw new Error('No file found')
}
}
9 changes: 9 additions & 0 deletions backend/src/v1/tasks/task.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { TasksService } from './task.service';
import { ObjectStoreModule } from '../object-store/object.store.module'

@Module({
imports: [ObjectStoreModule],
providers: [TasksService],
})
export class TasksModule {}
20 changes: 20 additions & 0 deletions backend/src/v1/tasks/task.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Injectable, Logger } from '@nestjs/common'
import { Cron } from '@nestjs/schedule'
import { ObjectStoreService } from '../object-store/object.store.service'
import { logger } from '../../logger'

@Injectable()
export class TasksService {

constructor(private readonly objectStoreService: ObjectStoreService) {

}

@Cron('0 0 0/4 * * *')
async refreshCache() {
logger.info('refresh cache every 4 hours')
await this.objectStoreService.getLatestOmrrDataFromObjectStore()

}

}
Loading

0 comments on commit bc50ffc

Please sign in to comment.